Compare commits

...

7 Commits

Author SHA1 Message Date
Jean-Claude a2bb209e2c
Fix library returned path to .git instead of root
It seems like git2::Repository::discover_path returns the path to `.git`
and no longer to the actual root of the repository.
2023-08-22 16:45:07 +02:00
Jean-Claude 6aae4afa06
Add error handling to application
No new Error enums were created but `whatever` was used. At some places
an explicit error would probably be better.
2023-08-22 16:19:07 +02:00
Jean-Claude 747b34d51f
Add proper error handling for metadata
Only patches the library, but not the application yet
2023-08-22 09:27:41 +02:00
Jean-Claude 027db6013c
Add proper error handling for archive
Only patches the library, but not the application yet
2023-08-22 09:27:41 +02:00
Jean-Claude f78831000b
Add helper function to run git commands 2023-08-22 09:27:41 +02:00
Jean-Claude 68295540bb
Add proper error handling for git
Only patches the library, but not the application yet
2023-08-22 09:27:41 +02:00
Jean-Claude 2c2fae4e36
Add proper error handling for config
Only patches the library, but not the application yet
2023-08-22 09:27:37 +02:00
16 changed files with 477 additions and 269 deletions

28
Cargo.lock generated
View File

@ -69,6 +69,12 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "anyhow"
version = "1.0.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
[[package]]
name = "autocfg"
version = "1.1.0"
@ -436,12 +442,14 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
name = "mianex"
version = "0.0.1"
dependencies = [
"anyhow",
"chrono",
"clap",
"git2",
"kamadak-exif",
"regex",
"rust-ini",
"thiserror",
]
[[package]]
@ -597,6 +605,26 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "time"
version = "0.1.45"

View File

@ -8,9 +8,11 @@ description = "A manager for your photography archive."
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.71"
chrono = "0.4.24"
clap = { version = "4.2.7", features = ["derive"] }
git2 = "0.17.1"
kamadak-exif = "0.5.5"
regex = "1.8.1"
rust-ini = "0.19.0"
thiserror = "1.0.40"

View File

@ -1,9 +1,9 @@
//! Provides archive related functionalities
use std::path::PathBuf;
use std::process::Command;
use super::config::{Config, RELATIVE_CONFIG_FILE};
use super::error::Archive as ArchiveError;
use super::git;
/// Represents an archive
@ -15,89 +15,72 @@ pub struct Archive {
impl Archive {
/// Get the archive based on the CWD
pub fn new() -> Option<Self> {
let path = Archive::get_root();
///
/// May fail if e.g. the archive does not exist.
pub fn new() -> Result<Self, ArchiveError> {
let path = Archive::get_root()?;
match path {
Some(path) => {
let repo = git2::Repository::init(&path).unwrap();
let config = Archive::get_config_from_main_branch();
Some(Self { path, repo, config })
}
None => None,
}
let repo = git2::Repository::init(&path)?;
let config = Archive::get_config_from_main_branch()?;
Ok(Self { path, repo, config })
}
/// Get all open imports
pub fn get_imports(&self) -> Vec<git2::Branch> {
match self.repo.branches(Some(git2::BranchType::Local)) {
Ok(branches) => branches
.filter_map(|b| b.ok())
.map(|(branch, _)| branch)
.collect(),
Err(_) => panic!("failed to get branches"),
}
pub fn get_imports(&self) -> Result<Vec<git2::Branch>, ArchiveError> {
let branches = git::branch(&self.repo)?;
Ok(branches
.filter_map(|b| b.ok())
.map(|(branch, _)| branch)
.collect())
}
/// Switch to branch `branch_name`
pub fn switch_branch(&self, branch_name: &String) -> () {
git::switch_branch(&self.repo, branch_name)
pub fn switch_branch(&self, branch_name: &String) -> Result<(), ArchiveError> {
Ok(git::switch_branch(&self.repo, branch_name)?)
}
pub fn get_current_branch(&self) -> Option<String> {
git::get_current_branch(&self.repo)
/// Get the name of the currently checkout branch.
pub fn get_current_branch(&self) -> Result<String, ArchiveError> {
Ok(git::get_current_branch(&self.repo)?)
}
/// Checks if a given branch exists locally.
pub fn exists_branch(&self, branch_name: &String) -> bool {
git::exists_branch(&self.repo, branch_name)
}
/// Indicate if the archive is in a dirty state.
pub fn is_dirty(&self) -> bool {
git::is_dirty(&self.repo)
pub fn is_dirty(&self) -> Result<bool, ArchiveError> {
Ok(git::is_dirty(&self.repo)?)
}
/// Get the root of the current archive based on the CWD or None if we are in no archive
pub fn get_root() -> Option<PathBuf> {
let git_root = git::get_root();
/// Iteratively search archive root starting at CWD going up to /.
///
/// Returns an error if CWD cannot be read or if not archive can be found.
pub fn get_root() -> Result<PathBuf, ArchiveError> {
let root = git::get_root()?;
let mut annex_root = root.as_path().to_owned();
annex_root.push(".git");
annex_root.push("annex");
match git_root {
None => None,
Some(root) => {
let mut a: PathBuf = root.as_path().to_owned();
a.push(".git");
a.push("annex");
if a.exists() {
return Some(root);
}
None
}
if annex_root.exists() {
Ok(root)
} else {
Err(ArchiveError::NoArchive)
}
}
/// Get config by reading file.
fn get_config_from_current_branch(&self) -> Config {
let config_path = Archive::get_root()
.expect("Invalid archive")
.join(RELATIVE_CONFIG_FILE);
Config::from_file(&config_path)
fn get_config_from_current_branch(&self) -> Result<Config, ArchiveError> {
let config_path = Archive::get_root()?;
let config_path = config_path.join(RELATIVE_CONFIG_FILE);
Ok(Config::from_file(&config_path)?)
}
/// Get config by reading file from main branch.
fn get_config_from_main_branch() -> Config {
fn get_config_from_main_branch() -> Result<Config, ArchiveError> {
let main_branch = "main"; // TODO: How to figure out which main branch is, without config? :D
let out = Command::new("git")
.arg("show")
.arg(format!("{}:{}", main_branch, RELATIVE_CONFIG_FILE))
.output()
.expect("Failed to run git");
if out.status.success() {
return Config::from_string(String::from_utf8_lossy(&out.stdout).to_string().trim());
}
panic!("Failed to read config from branch {}", main_branch);
let out = git::show(vec![format!("{}:{}", main_branch, RELATIVE_CONFIG_FILE)])?;
Ok(Config::from_string(&out)?)
}
}

View File

@ -1,17 +1,19 @@
use anyhow::{Context, Result};
use std::path::PathBuf;
use crate::archive::Archive;
use crate::config::{Config, RELATIVE_CONFIG_FILE};
/// Crude function to dump config to default location.
pub fn run() {
if let Some(p) = Archive::get_root() {
let mut config_path = p;
config_path.push(RELATIVE_CONFIG_FILE);
if config_path.exists() {
panic!("Config already exists. Cannot write default config.")
}
Config::dump_default(&config_path);
println!("Wrote config to {}", config_path.to_str().unwrap());
pub fn run() -> Result<()> {
let mut config_path: PathBuf = Archive::get_root().context("Archive not found")?.to_owned();
config_path.push(RELATIVE_CONFIG_FILE);
if config_path.exists() {
println!("Config already exists. Cannot write default config.");
Ok(())
} else {
panic!("Archive not found. Cannot write default config.");
Config::dump_default(&config_path).context("Unable to write default config")?;
Ok(())
}
}

View File

@ -1,4 +1,6 @@
pub fn run() {
use anyhow::Result;
pub fn run() -> Result<()> {
println!("Run cull");
todo!()
}

View File

@ -8,35 +8,28 @@ use deactivate::deactivate;
use list::list;
use new::new;
use anyhow::{Context, Result};
use clap::{Args, Subcommand};
use std::path::{self, PathBuf};
use std::process;
use crate::archive::Archive;
pub fn run(import: Import) {
pub fn run(import: Import) -> Result<()> {
println!("Run import with {:?}", import);
let archive = Archive::new();
let archive = Archive::new().context("Failed to initiate archive")?;
if archive.is_none() {
eprintln!("Your are not inside a valid archive.");
process::exit(1)
}
let archive = archive.unwrap();
match import.command {
ImportCommands::Activate { import_name } => activate(archive, import_name),
Ok(match import.command {
ImportCommands::Activate { import_name } => activate(archive, import_name)?,
ImportCommands::Add => todo!(),
ImportCommands::Deactivate => deactivate(archive),
ImportCommands::List => list(archive),
ImportCommands::Deactivate => deactivate(archive)?,
ImportCommands::List => list(archive)?,
ImportCommands::New {
identifier,
schema,
images,
} => new(archive, identifier, schema, images),
}
} => new(archive, identifier, schema, images)?,
})
}
#[derive(Args, Debug)]

View File

@ -1,10 +1,12 @@
/// Provides all functionality to activate an import.
use crate::archive::Archive;
use anyhow::{bail, Result};
/// Activates an import
pub fn activate(archive: Archive, import_name: String) -> () {
if archive.is_dirty() {
panic!("Archive is dirty.")
pub fn activate(archive: Archive, import_name: String) -> Result<()> {
if archive.is_dirty()? {
bail!("Archive is dirty. Activate not possible");
}
let import_branch_name = format!(
@ -12,7 +14,8 @@ pub fn activate(archive: Archive, import_name: String) -> () {
&archive.config.general.import_branch_prefix, import_name
);
archive.switch_branch(&import_branch_name);
archive.switch_branch(&import_branch_name)?;
println!("Active import is {}", import_name);
Ok(())
}

View File

@ -1,11 +1,14 @@
/// Provides all functionality to deactivate an import.
use crate::archive::Archive;
use anyhow::{bail, Result};
/// Go back to the main branch
pub fn deactivate(archive: Archive) -> () {
if archive.is_dirty() {
panic!("Archive is dirty.")
pub fn deactivate(archive: Archive) -> Result<()> {
if archive.is_dirty()? {
bail!("Archive is dirty. Deactivate not possible");
}
archive.switch_branch(&archive.config.general.main_branch);
archive.switch_branch(&archive.config.general.main_branch)?;
Ok(())
}

View File

@ -1,36 +1,46 @@
/// Provides all functionality to list all open imports.
use crate::archive::Archive;
use anyhow::Result;
use crate::error::Archive as ArchiveError;
/// List all open imports
pub fn list(archive: Archive) -> () {
let import_branches: Vec<String> = archive
.get_imports()
pub fn list(archive: Archive) -> Result<(), ArchiveError> {
// let import_branches: Result<Vec<String>, ArchiveError> = archive
let import_branches: Result<Vec<String>, ArchiveError> = archive
.get_imports()?
.into_iter()
.filter(|branch| {
branch
.name()
.unwrap()
.unwrap()
.starts_with(&archive.config.general.import_branch_prefix)
})
.map(|branch| {
String::from(
branch
.name()
.unwrap()
.unwrap()
.strip_prefix(&archive.config.general.import_branch_prefix)
.unwrap(),
)
.filter_map(|branch| -> Option<Result<String, ArchiveError>> {
// Returns:
// None to filter out element
// Some(Ok(elemem)) to include element
// Some(Err) to make the overall variable Err
let branch_name = branch.name();
// TODO: cleanup match
match branch_name {
Ok(b) => {
let b2 = b.expect("Branch name contains invalid utf-8 chars");
let a = b2.strip_prefix(&archive.config.general.import_branch_prefix);
match a {
Some(x) => Some(Ok(x.to_string())),
None => None,
}
}
Err(e) => Some(Err(ArchiveError::from(e))),
}
})
.collect();
let import_branches = import_branches?;
if import_branches.is_empty() {
println!("There are not open imports!");
println!("There are no open imports!");
Ok(())
} else {
println!("Open imports:");
for b in import_branches {
println!("{}", b);
}
Ok(())
}
}

View File

@ -1,4 +1,5 @@
/// Provides all functionality for creating a new import.
use anyhow::{bail, Result};
use chrono::prelude::NaiveDateTime;
use std::collections::HashSet;
use std::fs::{copy, create_dir_all};
@ -10,8 +11,10 @@ use std::time::Duration;
use exif::Tag;
use crate::archive::Archive;
use crate::error::Metadata as MetadataError;
use crate::format_string::branch::BranchFS;
use crate::format_string::image::ImageFS;
use crate::git;
use crate::metadata::Metadata;
/// Represents a single image which is to be imported
@ -33,28 +36,24 @@ impl ImportImage {
source: PathBuf,
schema: &Option<String>,
identifier: Option<String>,
) -> Self {
) -> Result<Self, MetadataError> {
debug_assert!(source.is_absolute());
let metadata = Metadata::init(&source);
let dt_raw = metadata.get_formatted(Tag::DateTimeOriginal);
let dt;
if let Some(dt_raw) = dt_raw {
dt = NaiveDateTime::parse_from_str(&dt_raw, "%Y-%m-%d %H:%M:%S").expect("Extracted DateTime has an invalid format.");
} else {
panic!("Import not possible. Image has not DateTime.");
}
let metadata = Metadata::init(&source)?;
let dt_raw = metadata.get_formatted(Tag::DateTimeOriginal)?;
let dt = NaiveDateTime::parse_from_str(&dt_raw, "%Y-%m-%d %H:%M:%S")
.map_err(|_| MetadataError::DateTimeInvalidFormat)?;
let extension = source
.extension()
.and_then(|e| Some(String::from(e.to_str().unwrap())));
Self {
Ok(Self {
source,
destination: ImageFS::init(&archive, schema, Some(dt), identifier, extension),
datetime: dt,
metadata,
}
})
}
}
@ -75,13 +74,13 @@ impl ImportState {
archive: &Archive,
schema: Option<String>,
identifier: Option<String>,
) -> Self {
) -> Result<Self, MetadataError> {
assert!(!img_paths.is_empty());
let mut images: Vec<ImportImage> = Vec::new();
for e in img_paths {
debug_assert!(e.is_absolute());
images.push(ImportImage::init(archive, e, &schema, identifier.clone()));
images.push(ImportImage::init(archive, e, &schema, identifier.clone())?);
}
// Sort images by capture time
@ -103,10 +102,10 @@ impl ImportState {
}
assert!(dest.len() == images.len());
ImportState {
Ok(ImportState {
images,
branch: BranchFS::init(&archive.config, start_dt, end_dt, identifier),
}
})
}
}
@ -116,9 +115,9 @@ pub fn new(
identifier: Option<String>,
schema: Option<String>,
images: Vec<PathBuf>,
) -> () {
if archive.is_dirty() {
panic!("Archive is dirty.")
) -> Result<()> {
if archive.is_dirty()? {
bail!("Archive is dirty. New import not possible");
}
println!(
@ -129,9 +128,9 @@ pub fn new(
schema.as_ref().unwrap_or(&String::from("not provided")),
);
let state: ImportState = ImportState::init(images, &archive, schema, identifier);
let state: ImportState = ImportState::init(images, &archive, schema, identifier)?;
prepare_branch_for_new_import(&archive, &state);
prepare_branch_for_new_import(&archive, &state)?;
// TODO: Actual import can be multi threaded as there (should be) no collisions in destination.
@ -140,9 +139,13 @@ pub fn new(
for i in state.images.iter() {
let formatted_dest = i.destination.format();
let destination = Path::new(&formatted_dest);
create_dir_all(destination.parent().unwrap()).unwrap();
copy(&i.source, destination).expect("Failed to copy image to destination");
// TODO: get permission error without timeout
create_dir_all(
destination
.parent()
.expect("There should always be a parent"),
)?;
copy(&i.source, destination)?;
// TODO: somehow we get a permission error without timeout
thread::sleep(Duration::from_millis(100));
}
@ -153,25 +156,24 @@ pub fn new(
Command::new("git-annex")
.arg("add")
.arg(formatted_dest)
.output()
.expect("Failed to run git-annex");
.output()?;
}
// TODO: use git2 library
let message = format!("Import {} images", state.images.len());
Command::new("git")
.arg("commit")
.arg("--amend")
.arg("--message")
.arg(message)
.output()
.expect("Failed to run git");
let args = vec![
String::from("commit"),
String::from("--amend"),
String::from("--message"),
format!("Import {} images", state.images.len()),
];
git::run(args)?;
println!("Done importing {} images", state.images.len());
Ok(())
}
/// Ensure the import branch is unique, create it, and add an initial commit.
fn prepare_branch_for_new_import(archive: &Archive, state: &ImportState) -> () {
fn prepare_branch_for_new_import(archive: &Archive, state: &ImportState) -> Result<()> {
// Ensure `branch_name` is unique by adding count if required
let mut unique_branch = state.branch.format();
while archive.exists_branch(&unique_branch) {
@ -186,28 +188,26 @@ fn prepare_branch_for_new_import(archive: &Archive, state: &ImportState) -> () {
// Checkout new branch
// TODO: use git2 library
Command::new("git")
.arg("switch")
.arg("--orphan")
.arg(&unique_branch)
.output()
.expect("Failed to execute command");
let args = vec![
String::from("switch"),
String::from("--orphan"),
String::from(&unique_branch),
];
git::run(args)?;
// TODO: use git2 library
Command::new("git")
.arg("clean")
.arg("--force")
.output()
.expect("Failed to execute command");
let args = vec![String::from("clean"), String::from("--force")];
git::run(args)?;
// TODO: use git2 library
Command::new("git")
.arg("commit")
.arg("--allow-empty")
.arg("--message")
.arg("NEW EMPTY IMPORT")
.output()
.expect("Failed to execute command");
let args = vec![
String::from("commit"),
String::from("--allow-empty"),
String::from("--message"),
String::from("NEW EMPTY IMPORT"),
];
git::run(args)?;
assert!(archive.get_current_branch().unwrap() == unique_branch);
Ok(())
}

View File

@ -1,4 +1,6 @@
pub fn run() {
use anyhow::Result;
pub fn run() -> Result<()> {
println!("Run init");
todo!()
}

View File

@ -4,19 +4,24 @@ pub use ini::Ini;
use std::collections::HashMap;
use std::path::PathBuf;
use crate::error::{Config as ConfigError, ConfigFormat as ConfigFormatError};
pub static RELATIVE_CONFIG_FILE: &str = "mia.ini";
/// List all sections of the config
pub struct Config {
pub general: General,
pub schema: Schema,
}
/// List all options of the 'General' section
pub struct General {
pub import_branch_prefix: String,
pub import_branch_schema: String,
pub main_branch: String,
}
/// List all options of the 'Schema' section
pub struct Schema {
pub default: String,
pub custom: HashMap<String, String>,
@ -24,57 +29,65 @@ pub struct Schema {
impl Config {
/// Initialize `Config` by reading it from a valid Ini config file at `config_path`.
pub fn from_file(config_path: &PathBuf) -> Self {
let ini_conf: Ini = Ini::load_from_file(&config_path).unwrap_or_else(|_| {
panic!(
"Expected configuration file at {}",
&config_path.to_str().unwrap()
)
});
pub fn from_file(config_path: &PathBuf) -> Result<Self, ConfigError> {
let ini_conf = Ini::load_from_file(&config_path).map_err(|source| match source {
ini::Error::Io(e) => ConfigError::ReadFile { source: e },
ini::Error::Parse(e) => ConfigError::from(e),
})?;
Config::from_ini(ini_conf)
}
/// Initialize `Config` from valid ini string `dump`.
pub fn from_string(dump: &str) -> Self {
let ini_conf = Ini::load_from_str(dump).unwrap();
pub fn from_string(dump: &str) -> Result<Self, ConfigError> {
let ini_conf = Ini::load_from_str(dump)?;
Config::from_ini(ini_conf)
}
/// Initialize `Config` from a `Ini` config.
fn from_ini(ini_conf: Ini) -> Self {
let mut schemas = HashMap::from_iter(Self::read_section(&ini_conf, "Schema"));
let schema_default = schemas
.remove("Default")
.expect("Field 'Default' is missing in section 'Schema'");
fn from_ini(ini_conf: Ini) -> Result<Self, ConfigError> {
let mut schemas = HashMap::from_iter(Self::read_section(&ini_conf, "Schema")?);
let schema_default = schemas.remove("Default").ok_or(ConfigError::InvalidFormat(
ConfigFormatError::Custom {
message: format!("Field 'Default' is missing in section 'Schema'"),
},
))?;
Self {
Ok(Self {
general: General {
import_branch_prefix: Self::read_property(
&ini_conf,
"General",
"ImportBranchPrefix",
),
)?,
import_branch_schema: Self::read_property(
&ini_conf,
"General",
"ImportBranchSchema",
),
main_branch: Self::read_property(&ini_conf, "General", "MainBranch"),
)?,
main_branch: Self::read_property(&ini_conf, "General", "MainBranch")?,
},
schema: Schema {
default: schema_default,
custom: schemas,
},
}
})
}
/// Dumps the default config to `file`.
/// `file` must not exists.
pub fn dump_default(file: &PathBuf) -> () {
assert!(!file.exists());
pub fn dump_default(file: &PathBuf) -> Result<(), ConfigError> {
assert!(file.is_absolute());
// TODO: Add flag to overwrite config in case it exists
if file.exists() {
return Err(ConfigError::WriteFile {
source: std::io::Error::new(
std::io::ErrorKind::Other,
format!(": File {} already exists", file.display()),
),
});
}
let def_config = Config::default();
let mut conf = Ini::new();
@ -91,27 +104,45 @@ impl Config {
conf.with_section(Some("Schema"))
.set("Default", def_config.schema.default)
.set("Event", def_config.schema.custom.get("Event").unwrap());
.set(
"Event",
def_config
.schema
.custom
.get("Event")
.expect("Default trait is supposed to add an 'Event' schema"),
);
conf.write_to_file(file).expect("Failed to dump config");
conf.write_to_file(file)
.map_err(|source| ConfigError::WriteFile { source })?;
Ok(())
}
/// Return value of key within section
fn read_property(file: &Ini, section: &str, key: &str) -> String {
file.section(Some(section))
.unwrap_or_else(|| panic!("Section '{}' is missing", section))
fn read_property(file: &Ini, section: &str, key: &str) -> Result<String, ConfigFormatError> {
Ok(file
.section(Some(section))
.ok_or(ConfigFormatError::Custom {
message: format!("Section '{}' is missing", section),
})?
.get(key)
.unwrap_or_else(|| panic!("Field '{}' is missing in section '{}'", key, section))
.to_string()
.ok_or(ConfigFormatError::Custom {
message: format!("Field '{}' is missing in section '{}'", key, section),
})?
.to_string())
}
/// Return (key, value) pair for given section
fn read_section(file: &Ini, section: &str) -> Vec<(String, String)> {
file.section(Some(section))
.unwrap_or_else(|| panic!("Section '{}' is missing", section))
fn read_section(file: &Ini, section: &str) -> Result<Vec<(String, String)>, ConfigFormatError> {
Ok(file
.section(Some(section))
.ok_or(ConfigFormatError::Custom {
message: format!("Section '{}' is missing", section),
})?
.iter()
.map(|(s, k)| (s.to_string(), k.to_string()))
.collect()
.collect())
}
}

106
src/error.rs Normal file
View File

@ -0,0 +1,106 @@
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Archive {
/// No archive could be found.
#[error("No archive found")]
NoArchive,
/// Error coming from the underlying git library.
#[error("General Git error")]
Git(#[from] Git),
/// Error coming from the config
#[error("General Config error")]
Config(#[from] Config),
}
impl From<git2::Error> for Archive {
fn from(value: git2::Error) -> Self {
Archive::Git(Git::Generic(value))
}
}
/// Error related to Git
#[derive(Error, Debug)]
pub enum Git {
/// Error related to IO
#[error("IO Error: {0}")]
Io(#[from] std::io::Error),
/// Error coming from Git2
/// TODO: split into more meaningful types
#[error("Generic: {}", .0.message())]
Generic(#[from] git2::Error),
/// Head points to invalid reference for this operation
#[error("Invalid reference: {0}")]
InvalidReference(String),
}
/// Error related to the config
#[derive(Error, Debug)]
pub enum Config {
/// Error reading from config
#[error("Could not read from config file")]
ReadFile { source: std::io::Error },
/// Error writing to config
#[error("Could not write to config file")]
WriteFile { source: std::io::Error },
/// Error parsing config due to invalid format
#[error("Config has invalid format")]
InvalidFormat(ConfigFormat),
}
impl From<ini::ParseError> for Config {
fn from(value: ini::ParseError) -> Self {
Self::InvalidFormat(ConfigFormat::Ini { source: value })
}
}
impl From<ConfigFormat> for Config {
fn from(value: ConfigFormat) -> Self {
Self::InvalidFormat(value)
}
}
/// Error related to an invalid config format
#[derive(Error, Debug)]
pub enum ConfigFormat {
/// Format error detected by the own library
#[error("{message}")]
Custom { message: String },
/// Format error detected by the underlying `ini` library
#[error("{}", .source.msg)]
Ini { source: ini::ParseError },
}
/// Errors related to metadata
#[derive(Error, Debug)]
pub enum Metadata {
/// Error related to IO
#[error("IO Error: {0}")]
Io(#[from] std::io::Error),
/// Cannot read metadata
#[error("Cannot read metadata: {msg}")]
InvalidMetadata { msg: String },
/// Queries for invalid field
#[error("Invalid field: {msg}")]
InvalidField { msg: String },
/// Other metadata errors from kamadak-exif
#[error("Other exif errors")]
ExifError { source: exif::Error },
/// Extracted DateTime has invalid format
#[error("Extracted DateTime has invalid format")]
DateTimeInvalidFormat,
}
impl From<exif::Error> for Metadata {
fn from(value: exif::Error) -> Self {
match value {
exif::Error::Io(source) => Metadata::Io(source),
exif::Error::NotFound(msg) | exif::Error::InvalidFormat(msg) => {
Metadata::InvalidMetadata {
msg: msg.to_string(),
}
}
_ => Metadata::ExifError { source: value },
}
}
}

View File

@ -1,83 +1,116 @@
//! Provided git related functionalities
//! Wraps around git2
use std::path::PathBuf;
use std::env::current_dir;
use std::path::{Path, PathBuf};
use std::process::Command;
pub use git2;
use git2::{Repository, BranchType};
use git2::{BranchType, Branches, Repository};
/// Get the root of the current git repo based on the CWD or None if we are in no repo
pub fn get_root() -> Option<PathBuf> {
let out = Command::new("git")
.arg("rev-parse")
.arg("--show-toplevel")
.output()
.expect("Fails to run git");
use crate::error::Git as GitError;
if out.status.success() {
let path = PathBuf::from(String::from_utf8_lossy(&out.stdout).to_string().trim());
assert!(path.as_path().exists(), "Repo root is supposed to exist");
return Some(path);
}
/// Iteratively search git repository starting at CWD going up to /.
///
/// Returns an error if CWD cannot be read or if not repository can be found.
pub fn get_root() -> Result<PathBuf, GitError> {
let cwd = current_dir()?;
let ceiling_dir = Path::new("/");
// TODO: Improve error type of `discover_path` to indicate if no repo found or other error
let repo = Repository::discover_path(cwd, ceiling_dir)?;
let repo = repo.parent().expect("There should always be a parent");
return None;
Ok(PathBuf::from(repo))
}
/// Convenience wrapper to switch to a specific branch
/// Convenience wrapper to switch to a specific branch.
///
/// Branch `branch_name` must exists.
pub fn switch_branch(repo: &Repository, branch_name: &String) -> () {
let branch = repo
.find_branch(branch_name, BranchType::Local)
.unwrap_or_else(|_| panic!("Provided branch does not exists."));
let reference = branch.into_reference();
pub fn switch_branch(repo: &Repository, branch_name: &String) -> Result<(), GitError> {
// No need to change branch, if we are already on the right branch
if let Some(branch) = get_current_branch(repo) && &branch == branch_name {
if let Ok(branch) = get_current_branch(repo) && &branch == branch_name {
println!("Your are already on the right target. No need to switch.");
return ()
return Ok(());
}
let object = reference.peel(git2::ObjectType::Any).unwrap();
let reference = repo
.find_branch(branch_name, BranchType::Local)?
.into_reference();
repo.checkout_tree(&object, None).unwrap();
repo.set_head(reference.name().unwrap()).unwrap();
let object = reference.peel(git2::ObjectType::Any)?;
repo.checkout_tree(&object, None)?;
repo.set_head(
reference
.name()
.expect("Branch name contains invalid utf-8 chars"),
)?;
println!("Switch to branch {}", branch_name);
Ok(())
}
pub fn get_current_branch(repo: &Repository) -> Option<String> {
let head_ref = repo.head().ok().unwrap();
/// Get the name of the currently checkout branch.
pub fn get_current_branch(repo: &Repository) -> Result<String, GitError> {
let head_ref = repo.head()?;
if !head_ref.is_branch() {
return None;
}
match head_ref.shorthand() {
Some(name) => Some(String::from(name)),
None => None,
Err(GitError::InvalidReference(String::from(
"Current reference is not a branch",
)))
} else {
Ok(String::from(
head_ref
.shorthand()
.expect("Branch name contains invalid utf-8 chars"),
))
}
}
/// Checks if a given branch exists locally
/// Git list of all local branches.
pub fn branch(repo: &Repository) -> Result<Branches, GitError> {
Ok(repo.branches(Some(git2::BranchType::Local))?)
}
/// Checks if a given branch exists locally.
/// TODO: May return false even if branch exists
pub fn exists_branch(repo: &Repository, branch_name: &str) -> bool {
repo.find_branch(branch_name, BranchType::Local)
.is_ok()
repo.find_branch(branch_name, BranchType::Local).is_ok()
}
/// Run git command
pub fn run(args: Vec<String>) -> Result<String, GitError> {
let out = Command::new("git").args(&args).output()?;
if out.status.success() {
Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
} else {
Err(GitError::Io(std::io::Error::new(
std::io::ErrorKind::Other,
format!(
"Failed to run `{}`",
args.iter()
.fold(String::from("git"), |accum, e| format!("{} {}", accum, e))
),
)))
}
}
/// Indicate if the repository is dirty.
///
/// Dirty is interpreted as having uncommitted changes and/or having untracked files.
/// TODO: use git2 library
pub fn is_dirty(_repo: &Repository) -> bool {
let out = Command::new("git")
.arg("status")
.arg("--porcelain")
.output()
.expect("Failed to run git");
!String::from_utf8_lossy(&out.stdout).is_empty()
pub fn is_dirty(_repo: &Repository) -> Result<bool, GitError> {
let args = vec![String::from("status"), String::from("--porcelain")];
let out = run(args)?;
Ok(!out.is_empty())
}
/// Run `git show ARGS` command.
///
/// TODO: use git2 library
pub fn show(args: Vec<String>) -> Result<String, GitError> {
let mut args_complete = args.to_owned();
args_complete.insert(0, String::from("show"));
run(args_complete)
}

View File

@ -3,33 +3,35 @@
mod archive;
mod commands;
mod config;
mod error;
mod format_string;
mod git;
mod metadata;
use anyhow::Result;
use clap::Parser;
use commands::{Cli, Commands};
fn main() {
fn main() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Ok(match cli.command {
Commands::Config => {
println!("Command Config");
commands::config::run();
commands::config::run()?;
}
Commands::Cull => {
println!("Command Cull");
commands::cull::run();
commands::cull::run()?;
}
Commands::Init => {
println!("Command Initialize");
commands::init::run();
commands::init::run()?;
}
Commands::Import(arg) => {
println!("Command Import");
commands::import::run(arg);
commands::import::run(arg)?;
}
}
})
}

View File

@ -3,7 +3,9 @@ use std::fs::File;
use std::io::BufReader;
use std::path::PathBuf;
use exif::{Exif, Reader, Tag, Value};
use exif::{Exif, Field, Reader, Tag, Value};
use super::error::Metadata as MetadataError;
/// Holds `Exif` which is use interact with an images metadata.
pub struct Metadata {
@ -12,28 +14,34 @@ pub struct Metadata {
impl Metadata {
/// Initialize `Metadata` by creating an instance of `Exif`.
pub fn init(path: &PathBuf) -> Self {
let file = File::open(path).expect("Error reading file");
pub fn init(path: &PathBuf) -> Result<Self, MetadataError> {
let file = File::open(path)?;
let mut bufreader = BufReader::new(&file);
let exifreader = Reader::new();
let exif = exifreader
.read_from_container(&mut bufreader)
.expect("Error reading exif");
let exif = exifreader.read_from_container(&mut bufreader)?;
Self { exif }
Ok(Self { exif })
}
fn get_field(&self, tag: Tag) -> Result<&Field, MetadataError> {
self.exif
.get_field(tag, exif::In::PRIMARY)
.ok_or(MetadataError::InvalidField {
msg: format!("{}", tag),
})
}
/// Returns the raw metadata value associated to `tag`.
pub fn get_raw(&self, tag: Tag) -> Option<&Value> {
self.exif
.get_field(tag, exif::In::PRIMARY)
.and_then(|e| Some(&e.value))
pub fn get_raw(&self, tag: Tag) -> Result<&Value, MetadataError> {
Ok(&self.get_field(tag)?.value)
}
/// Returns a nicely formatted metadata value associated to `tag`.
pub fn get_formatted(&self, tag: Tag) -> Option<String> {
self.exif
.get_field(tag, exif::In::PRIMARY)
.and_then(|e| Some(e.display_value().with_unit(&self.exif).to_string()))
pub fn get_formatted(&self, tag: Tag) -> Result<String, MetadataError> {
Ok(self
.get_field(tag)?
.display_value()
.with_unit(&self.exif)
.to_string())
}
}