feat(package): progress in package archiver implementation
This commit is contained in:
parent
89e487747a
commit
5714ebe131
|
@ -18,3 +18,5 @@ reqwest = { version = "0.11.4", features = ["blocking"] }
|
|||
lazy_static = "1.4.0"
|
||||
regex = "1.4.3"
|
||||
zip = "0.5.13"
|
||||
scopeguard = "1.1.0"
|
||||
fs_extra = "1.2.0"
|
109
espanso-package/src/archive/default.rs
Normal file
109
espanso-package/src/archive/default.rs
Normal file
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* 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 anyhow::{bail, Context, Result};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::{ArchivedPackage, Archiver, Package, PackageSpecifier, SaveOptions};
|
||||
|
||||
use super::StoredPackage;
|
||||
|
||||
pub struct DefaultArchiver {
|
||||
package_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl DefaultArchiver {
|
||||
pub fn new(package_dir: &Path) -> Self {
|
||||
Self {
|
||||
package_dir: package_dir.to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Archiver for DefaultArchiver {
|
||||
fn save(
|
||||
&self,
|
||||
package: &dyn Package,
|
||||
specifier: &PackageSpecifier,
|
||||
save_options: &SaveOptions,
|
||||
) -> Result<ArchivedPackage> {
|
||||
let target_dir = self.package_dir.join(package.name());
|
||||
|
||||
if target_dir.is_dir() && !save_options.overwrite_existing {
|
||||
bail!("package {} is already installed", package.name());
|
||||
}
|
||||
|
||||
// Backup the previous directory if present
|
||||
let backup_dir = self.package_dir.join(&format!("{}.old", package.name()));
|
||||
let _backup_guard = if target_dir.is_dir() {
|
||||
std::fs::rename(&target_dir, &backup_dir)
|
||||
.context("unable to backup old package directory")?;
|
||||
|
||||
// If the function returns due to an error, restore the previous directory
|
||||
Some(scopeguard::guard(
|
||||
(backup_dir.clone(), target_dir.clone()),
|
||||
|(backup_dir, target_dir)| {
|
||||
if backup_dir.is_dir() {
|
||||
if target_dir.is_dir() {
|
||||
std::fs::remove_dir_all(&target_dir)
|
||||
.expect("unable to remove dirty package directory");
|
||||
}
|
||||
|
||||
std::fs::rename(backup_dir, target_dir).expect("unable to restore backup directory");
|
||||
}
|
||||
},
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
std::fs::create_dir_all(&target_dir).context("unable to create target directory")?;
|
||||
|
||||
super::util::copy_dir_without_dot_files(package.location(), &target_dir)
|
||||
.context("unable to copy package files")?;
|
||||
|
||||
super::util::create_package_source_file(specifier, &target_dir)
|
||||
.context("unable to create _pkgsource.yml file")?;
|
||||
|
||||
// Remove backup
|
||||
if backup_dir.is_dir() {
|
||||
std::fs::remove_dir_all(backup_dir).context("unable to remove backup directory")?;
|
||||
}
|
||||
|
||||
let archived_package =
|
||||
super::read::read_archived_package(&target_dir).context("unable to load archived package")?;
|
||||
|
||||
Ok(archived_package)
|
||||
}
|
||||
|
||||
fn get(&self, name: &str) -> Result<ArchivedPackage> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn list(&self) -> Result<Vec<StoredPackage>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn delete(&self, name: &str) -> Result<()> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: test
|
||||
// TODO: test what happens with "legacy" packages
|
108
espanso-package/src/archive/mod.rs
Normal file
108
espanso-package/src/archive/mod.rs
Normal file
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* 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 anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{Package, PackageSpecifier, manifest::Manifest};
|
||||
|
||||
pub mod default;
|
||||
mod read;
|
||||
mod util;
|
||||
|
||||
pub const PACKAGE_SOURCE_FILE: &str = "_pkgsource.yml";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ArchivedPackage {
|
||||
// Metadata
|
||||
pub manifest: Manifest,
|
||||
|
||||
// Package source information (needed to update)
|
||||
pub source: PackageSource,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LegacyPackage {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum StoredPackage {
|
||||
Legacy(LegacyPackage),
|
||||
Modern(ArchivedPackage),
|
||||
}
|
||||
|
||||
pub trait Archiver {
|
||||
fn get(&self, name: &str) -> Result<ArchivedPackage>;
|
||||
fn save(&self, package: &dyn Package, specifier: &PackageSpecifier, save_options: &SaveOptions) -> Result<ArchivedPackage>;
|
||||
fn list(&self) -> Result<Vec<StoredPackage>>;
|
||||
fn delete(&self, name: &str) -> Result<()>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct SaveOptions {
|
||||
pub overwrite_existing: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PackageSource {
|
||||
Hub,
|
||||
Git {
|
||||
repo_url: String,
|
||||
repo_branch: Option<String>,
|
||||
use_native_git: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<&PackageSpecifier> for PackageSource {
|
||||
fn from(package: &PackageSpecifier) -> Self {
|
||||
if let Some(git_repo_url) = package.git_repo_url.as_deref() {
|
||||
Self::Git {
|
||||
repo_url: git_repo_url.to_string(),
|
||||
repo_branch: package.git_branch.clone(),
|
||||
use_native_git: package.use_native_git,
|
||||
}
|
||||
} else {
|
||||
Self::Hub
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ArchivedPackage> for PackageSpecifier {
|
||||
fn from(package: &ArchivedPackage) -> Self {
|
||||
match &package.source {
|
||||
PackageSource::Hub => PackageSpecifier {
|
||||
name: package.manifest.name.to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
PackageSource::Git {
|
||||
repo_url,
|
||||
repo_branch,
|
||||
use_native_git,
|
||||
} => PackageSpecifier {
|
||||
name: package.manifest.name.to_string(),
|
||||
git_repo_url: Some(repo_url.to_string()),
|
||||
git_branch: repo_branch.clone(),
|
||||
use_native_git: *use_native_git,
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
48
espanso-package/src/archive/read.rs
Normal file
48
espanso-package/src/archive/read.rs
Normal file
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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 std::path::Path;
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
|
||||
use crate::{manifest::Manifest, ArchivedPackage};
|
||||
|
||||
use super::{PackageSource, PACKAGE_SOURCE_FILE};
|
||||
|
||||
pub fn read_archived_package(containing_dir: &Path) -> Result<ArchivedPackage> {
|
||||
let manifest_path = containing_dir.join("_manifest.yml");
|
||||
if !manifest_path.is_file() {
|
||||
bail!("missing _manifest.yml file");
|
||||
}
|
||||
|
||||
let source_path = containing_dir.join(PACKAGE_SOURCE_FILE);
|
||||
let source = if source_path.is_file() {
|
||||
let yaml = std::fs::read_to_string(&source_path)?;
|
||||
let source: PackageSource =
|
||||
serde_yaml::from_str(&yaml).context("unable to parse package source file.")?;
|
||||
source
|
||||
} else {
|
||||
// Fallback to hub installation
|
||||
PackageSource::Hub
|
||||
};
|
||||
|
||||
let manifest = Manifest::parse(&manifest_path).context("unable to parse manifest file")?;
|
||||
|
||||
Ok(ArchivedPackage { manifest, source })
|
||||
}
|
60
espanso-package/src/archive/util.rs
Normal file
60
espanso-package/src/archive/util.rs
Normal file
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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 std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use fs_extra::dir::CopyOptions;
|
||||
|
||||
use crate::PackageSpecifier;
|
||||
|
||||
use super::{PACKAGE_SOURCE_FILE, PackageSource};
|
||||
|
||||
// TODO: test
|
||||
pub fn copy_dir_without_dot_files(source_dir: &Path, inside_dir: &Path) -> Result<()> {
|
||||
fs_extra::dir::copy(
|
||||
source_dir,
|
||||
inside_dir,
|
||||
&CopyOptions {
|
||||
copy_inside: true,
|
||||
content_only: true,
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
|
||||
// Remove dot files and dirs (such as .git)
|
||||
let mut to_be_removed = Vec::new();
|
||||
for path in std::fs::read_dir(inside_dir)? {
|
||||
let path = path?.path();
|
||||
if path.starts_with(".") {
|
||||
to_be_removed.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
fs_extra::remove_items(&to_be_removed)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn create_package_source_file(specifier: &PackageSpecifier, target_dir: &Path) -> Result<()> {
|
||||
let source: PackageSource = specifier.into();
|
||||
let yaml = serde_yaml::to_string(&source)?;
|
||||
std::fs::write(target_dir.join(PACKAGE_SOURCE_FILE), yaml)?;
|
||||
Ok(())
|
||||
}
|
|
@ -19,53 +19,26 @@
|
|||
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
use thiserror::Error;
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
mod archive;
|
||||
mod manifest;
|
||||
mod package;
|
||||
mod provider;
|
||||
mod resolver;
|
||||
mod util;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct PackageSpecifier {
|
||||
pub name: String,
|
||||
pub version: Option<String>,
|
||||
|
||||
// Source information
|
||||
pub git_repo_url: Option<String>,
|
||||
pub git_branch: Option<String>,
|
||||
}
|
||||
|
||||
pub trait Package {
|
||||
// Manifest
|
||||
fn name(&self) -> &str;
|
||||
fn title(&self) -> &str;
|
||||
fn description(&self) -> &str;
|
||||
fn version(&self) -> &str;
|
||||
fn author(&self) -> &str;
|
||||
|
||||
// Directory containing the package files
|
||||
fn location(&self) -> &Path;
|
||||
}
|
||||
|
||||
pub trait PackageProvider {
|
||||
fn download(&self, package: &PackageSpecifier) -> Result<Box<dyn Package>>;
|
||||
// TODO: fn check update available? (probably should be only available in the hub)
|
||||
}
|
||||
pub use archive::{ArchivedPackage, Archiver, SaveOptions};
|
||||
pub use provider::{PackageSpecifier, PackageProvider};
|
||||
pub use package::Package;
|
||||
|
||||
// TODO: once the download is completed, avoid copying files beginning with "."
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum PackageResolutionError {
|
||||
#[error("package not found")]
|
||||
PackageNotFound,
|
||||
}
|
||||
|
||||
pub fn get_provider(package: &PackageSpecifier) -> Result<Box<dyn PackageProvider>> {
|
||||
if let Some(git_repo_url) = package.git_repo_url.as_deref() {
|
||||
let matches_known_hosts = if let Some(github_parts) = util::github::extract_github_url_parts(git_repo_url) {
|
||||
if !package.use_native_git {
|
||||
let matches_known_hosts =
|
||||
if let Some(github_parts) = util::github::extract_github_url_parts(git_repo_url) {
|
||||
if let Some(repo_scheme) =
|
||||
util::github::resolve_repo_scheme(github_parts, package.git_branch.as_deref())?
|
||||
{
|
||||
|
@ -93,6 +66,7 @@ pub fn get_provider(package: &PackageSpecifier) -> Result<Box<dyn PackageProvide
|
|||
if matches_known_hosts && !util::git::is_private_repo(git_repo_url) {
|
||||
bail!("could not access repository: {}, make sure it exists and that you have the necessary access rights.");
|
||||
}
|
||||
}
|
||||
|
||||
// Git repository is neither on Github or Gitlab
|
||||
// OR it's a private repository, which means we can't use the direct method
|
||||
|
@ -103,3 +77,8 @@ pub fn get_provider(package: &PackageSpecifier) -> Result<Box<dyn PackageProvide
|
|||
todo!();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn get_archiver(package_dir: &Path) -> Result<Box<dyn Archiver>> {
|
||||
Ok(Box::new(archive::default::DefaultArchiver::new(package_dir)))
|
||||
}
|
51
espanso-package/src/package/mod.rs
Normal file
51
espanso-package/src/package/mod.rs
Normal file
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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 std::path::Path;
|
||||
|
||||
pub mod default;
|
||||
|
||||
pub trait Package {
|
||||
// Manifest
|
||||
fn name(&self) -> &str;
|
||||
fn title(&self) -> &str;
|
||||
fn description(&self) -> &str;
|
||||
fn version(&self) -> &str;
|
||||
fn author(&self) -> &str;
|
||||
|
||||
// Directory containing the package files
|
||||
fn location(&self) -> &Path;
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for dyn Package {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
writeln!(
|
||||
f,
|
||||
"name: {}\nversion: {}\ntitle: {}\ndescription: {}\nauthor: {}\nlocation: {:?}",
|
||||
self.name(),
|
||||
self.version(),
|
||||
self.title(),
|
||||
self.description(),
|
||||
self.author(),
|
||||
self.location()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub use default::DefaultPackage;
|
|
@ -20,10 +20,11 @@
|
|||
use crate::{
|
||||
package::DefaultPackage,
|
||||
resolver::{resolve_package},
|
||||
Package, PackageProvider, PackageSpecifier,
|
||||
Package, PackageSpecifier,
|
||||
};
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use std::{path::Path, process::Command};
|
||||
use super::PackageProvider;
|
||||
|
||||
pub struct GitPackageProvider {}
|
||||
|
||||
|
@ -72,6 +73,10 @@ impl GitPackageProvider {
|
|||
}
|
||||
|
||||
impl PackageProvider for GitPackageProvider {
|
||||
fn name(&self) -> String {
|
||||
"git".to_string()
|
||||
}
|
||||
|
||||
fn download(&self, package: &PackageSpecifier) -> Result<Box<dyn Package>> {
|
||||
if !Self::is_git_installed() {
|
||||
bail!("unable to invoke 'git' command, please make sure it is installed and visible in PATH");
|
||||
|
|
|
@ -18,9 +18,10 @@
|
|||
*/
|
||||
|
||||
use crate::{
|
||||
package::DefaultPackage, resolver::resolve_package, Package, PackageProvider, PackageSpecifier,
|
||||
package::DefaultPackage, resolver::resolve_package, Package, PackageSpecifier,
|
||||
};
|
||||
use anyhow::{Result};
|
||||
use super::PackageProvider;
|
||||
|
||||
pub struct GitHubPackageProvider {
|
||||
repo_author: String,
|
||||
|
@ -39,6 +40,10 @@ impl GitHubPackageProvider {
|
|||
}
|
||||
|
||||
impl PackageProvider for GitHubPackageProvider {
|
||||
fn name(&self) -> String {
|
||||
"github".to_string()
|
||||
}
|
||||
|
||||
fn download(&self, package: &PackageSpecifier) -> Result<Box<dyn Package>> {
|
||||
let download_url = format!(
|
||||
"https://github.com/{}/{}/archive/refs/heads/{}.zip",
|
||||
|
@ -80,6 +85,7 @@ mod tests {
|
|||
version: None,
|
||||
git_repo_url: Some("https://github.com/espanso/dummy-package".to_string()),
|
||||
git_branch: None,
|
||||
..Default::default()
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
|
|
|
@ -17,5 +17,29 @@
|
|||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::Package;
|
||||
|
||||
pub(crate) mod git;
|
||||
pub(crate) mod github;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct PackageSpecifier {
|
||||
pub name: String,
|
||||
pub version: Option<String>,
|
||||
|
||||
// Source information
|
||||
pub git_repo_url: Option<String>,
|
||||
pub git_branch: Option<String>,
|
||||
|
||||
// Resolution options
|
||||
pub use_native_git: bool,
|
||||
}
|
||||
|
||||
pub trait PackageProvider {
|
||||
fn name(&self) -> String;
|
||||
fn download(&self, package: &PackageSpecifier) -> Result<Box<dyn Package>>;
|
||||
// TODO: fn check update available? (probably should be only available in the hub)
|
||||
}
|
||||
|
||||
|
|
|
@ -59,8 +59,8 @@ pub fn resolve_package(
|
|||
Ok(matching_package)
|
||||
} else {
|
||||
bail!(
|
||||
"unable to find version: {:?} for package: {}",
|
||||
version,
|
||||
"unable to find version: {} for package: {}",
|
||||
version.unwrap_or_default(),
|
||||
name
|
||||
);
|
||||
}
|
||||
|
|
|
@ -85,7 +85,7 @@ pub fn check_repo_with_branch(parts: &GitHubParts, branch: &str) -> Result<bool>
|
|||
let url = generate_github_download_url(parts, branch);
|
||||
let response = client.head(url).send()?;
|
||||
|
||||
Ok(response.status() == StatusCode::FOUND)
|
||||
Ok(response.status() == StatusCode::FOUND || response.status() == StatusCode::OK)
|
||||
}
|
||||
|
||||
fn generate_github_download_url(parts: &GitHubParts, branch: &str) -> String {
|
||||
|
|
Loading…
Reference in New Issue
Block a user