feat(package): implement first version of espanso-hub provider

This commit is contained in:
Federico Terzi 2021-09-05 22:59:47 +02:00
parent 393f431bc3
commit 23a73f7ea2
7 changed files with 275 additions and 11 deletions

74
Cargo.lock generated
View File

@ -105,6 +105,15 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
[[package]]
name = "block-buffer"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
dependencies = [
"generic-array",
]
[[package]]
name = "bstr"
version = "0.2.15"
@ -302,6 +311,15 @@ version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b"
[[package]]
name = "cpufeatures"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469"
dependencies = [
"libc",
]
[[package]]
name = "crc32fast"
version = "1.2.1"
@ -425,6 +443,15 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
[[package]]
name = "digest"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
dependencies = [
"generic-array",
]
[[package]]
name = "dirs"
version = "1.0.5"
@ -786,6 +813,7 @@ dependencies = [
"anyhow",
"fs_extra",
"glob",
"hex",
"lazy_static",
"log",
"natord",
@ -795,6 +823,7 @@ dependencies = [
"serde",
"serde_json",
"serde_yaml",
"sha2",
"tempdir",
"thiserror",
"zip",
@ -1034,6 +1063,16 @@ version = "0.3.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2"
[[package]]
name = "generic-array"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.1.16"
@ -1105,6 +1144,12 @@ dependencies = [
"libc",
]
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "html2text"
version = "0.2.1"
@ -1338,9 +1383,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "libc"
version = "0.2.98"
version = "0.2.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790"
checksum = "3cb00336871be5ed2c8ed44b60ae9959dc5b9f08539422ed43f09e34ecaeba21"
[[package]]
name = "libdbus-sys"
@ -1785,6 +1830,12 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
[[package]]
name = "opaque-debug"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "opener"
version = "0.5.0"
@ -2350,6 +2401,19 @@ dependencies = [
"yaml-rust 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "sha2"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9204c41a1597a8c5af23c82d1c921cb01ec0a4c59e07a9c7306062829a3903f3"
dependencies = [
"block-buffer",
"cfg-if 1.0.0",
"cpufeatures",
"digest",
"opaque-debug",
]
[[package]]
name = "simplelog"
version = "0.9.0"
@ -2721,6 +2785,12 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
[[package]]
name = "typenum"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec"
[[package]]
name = "unicase"
version = "2.6.0"

View File

@ -19,4 +19,6 @@ lazy_static = "1.4.0"
regex = "1.4.3"
zip = "0.5.13"
scopeguard = "1.1.0"
fs_extra = "1.2.0"
fs_extra = "1.2.0"
sha2 = "0.9.6"
hex = "0.4.3"

View File

@ -22,6 +22,8 @@ use std::path::Path;
use anyhow::{bail, Result};
mod archive;
#[macro_use]
mod logging;
mod manifest;
mod package;
mod provider;
@ -29,12 +31,12 @@ mod resolver;
mod util;
pub use archive::{ArchivedPackage, Archiver, SaveOptions, StoredPackage};
pub use provider::{PackageSpecifier, PackageProvider};
pub use package::Package;
pub use provider::{PackageProvider, PackageSpecifier};
// TODO: once the download is completed, avoid copying files beginning with "."
pub fn get_provider(package: &PackageSpecifier) -> Result<Box<dyn PackageProvider>> {
pub fn get_provider(package: &PackageSpecifier,) -> Result<Box<dyn PackageProvider>> {
if let Some(git_repo_url) = package.git_repo_url.as_deref() {
if !package.use_native_git {
let matches_known_hosts =
@ -73,14 +75,15 @@ pub fn get_provider(package: &PackageSpecifier) -> Result<Box<dyn PackageProvide
// (because it's not authenticated)
Ok(Box::new(provider::git::GitPackageProvider::new()))
} else {
// TODO: use espanso-hub method
bail!("espanso hub method not yet implemented")
// Download from the official espanso hub
Ok(Box::new(provider::hub::EspansoHubPackageProvider::new()))
}
}
pub fn get_archiver(package_dir: &Path) -> Result<Box<dyn Archiver>> {
Ok(Box::new(archive::default::DefaultArchiver::new(package_dir)))
Ok(Box::new(archive::default::DefaultArchiver::new(
package_dir,
)))
}
#[cfg(test)]
@ -94,4 +97,4 @@ pub(crate) mod tests {
action(&tmp_path);
}
}
}

View File

@ -0,0 +1,23 @@
#[macro_export]
macro_rules! info_println {
($($tts:tt)*) => {
println!($($tts)*);
log::info!($($tts)*);
}
}
#[macro_export]
macro_rules! warn_eprintln {
($($tts:tt)*) => {
eprintln!($($tts)*);
log::warn!($($tts)*);
}
}
#[macro_export]
macro_rules! error_eprintln {
($($tts:tt)*) => {
eprintln!($($tts)*);
log::error!($($tts)*);
}
}

View File

@ -0,0 +1,137 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use super::PackageProvider;
use crate::{
package::DefaultPackage, resolver::resolve_package, util::download::read_string_from_url,
Package, PackageSpecifier,
};
use anyhow::{anyhow, Context, Result};
use serde::{Deserialize, Serialize};
pub const ESPANSO_HUB_PACKAGE_INDEX_URL: &str =
"https://github.com/espanso/hub/releases/latest/download/package_index.json";
pub struct EspansoHubPackageProvider {}
impl EspansoHubPackageProvider {
pub fn new() -> Self {
Self {}
}
}
impl PackageProvider for EspansoHubPackageProvider {
fn name(&self) -> String {
"espanso-hub".to_string()
}
fn download(&self, package: &PackageSpecifier) -> Result<Box<dyn Package>> {
// TODO: pass index update flag
let index = self
.get_index(true)
.context("unable to get package index from espanso hub")?;
let package_info = index
.get_package(&package.name, package.version.as_deref())
.ok_or_else(|| {
anyhow!(
"unable to find package '{}@{}' in the espanso hub",
package.name,
package.version.as_deref().unwrap_or("latest")
)
})?;
let archive_sha256 = read_string_from_url(&package_info.archive_sha256_url)
.context("unable to read archive sha256 signature")?;
let temp_dir = tempdir::TempDir::new("espanso-package-download")?;
crate::util::download::download_and_extract_zip_verify_sha256(
&package_info.archive_url,
temp_dir.path(),
Some(&archive_sha256),
)?;
let resolved_package =
resolve_package(temp_dir.path(), &package.name, package.version.as_deref())?;
let package = DefaultPackage::new(
resolved_package.manifest,
temp_dir,
resolved_package.base_dir,
);
Ok(Box::new(package))
}
}
impl EspansoHubPackageProvider {
fn get_index(&self, _force_update: bool) -> Result<PackageIndex> {
// TODO: if force_update is false, we should try to use a locally-cached version of the package index
self.download_index()
}
fn download_index(&self) -> Result<PackageIndex> {
info_println!("fetching package index...");
let json_body = read_string_from_url(ESPANSO_HUB_PACKAGE_INDEX_URL)?;
let index: PackageIndex = serde_json::from_str(&json_body)?;
Ok(index)
}
}
#[derive(Debug, Serialize, Deserialize)]
struct PackageIndex {
last_update: u64,
packages: Vec<PackageInfo>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct PackageInfo {
name: String,
title: String,
author: String,
description: String,
version: String,
archive_url: String,
archive_sha256_url: String,
}
impl PackageIndex {
fn get_package(&self, name: &str, version: Option<&str>) -> Option<PackageInfo> {
let mut matching_packages: Vec<PackageInfo> = self
.packages
.iter()
.filter(|package| package.name == name)
.cloned()
.collect();
matching_packages.sort_by(|a, b| natord::compare(&a.version, &b.version));
if let Some(explicit_version) = version {
matching_packages
.into_iter()
.find(|package| package.version == explicit_version)
} else {
matching_packages.into_iter().last()
}
}
}

View File

@ -21,6 +21,7 @@ use anyhow::Result;
use crate::Package;
pub(crate) mod hub;
pub(crate) mod git;
pub(crate) mod github;

View File

@ -17,15 +17,35 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use anyhow::{Context, Result};
use anyhow::{Context, Result, bail};
use std::io::{copy, Cursor};
use std::path::Path;
use sha2::{Sha256, Digest};
pub fn download_and_extract_zip(url: &str, dest_dir: &Path) -> Result<()> {
download_and_extract_zip_verify_sha256(url, dest_dir, None)
}
pub fn download_and_extract_zip_verify_sha256(url: &str, dest_dir: &Path, sha256: Option<&str>) -> Result<()> {
let data = download(url).context("error downloading archive")?;
if let Some(sha256) = sha256 {
info_println!("validating sha256 signature...");
if !verify_sha256(&data, sha256) {
bail!("signature mismatch");
}
}
extract_zip(data, dest_dir).context("error extracting archive")
}
pub fn read_string_from_url(url: &str) -> Result<String> {
let client = reqwest::blocking::Client::builder();
let client = client.build()?;
let response = client.get(url).send()?;
Ok(response.text()?)
}
fn download(url: &str) -> Result<Vec<u8>> {
let client = reqwest::blocking::Client::builder();
let client = client.build()?;
@ -37,6 +57,14 @@ fn download(url: &str) -> Result<Vec<u8>> {
Ok(buffer)
}
fn verify_sha256(data: &[u8], sha256: &str) -> bool {
let mut hasher = Sha256::new();
hasher.update(data);
let result = hasher.finalize();
let hash = hex::encode(result);
hash == sha256
}
// Adapted from zip-rs extract.rs example
fn extract_zip(data: Vec<u8>, dest_dir: &Path) -> Result<()> {
let reader = Cursor::new(data);