Compare commits
7 Commits
c8cdf22e0b
...
a2bb209e2c
Author | SHA1 | Date |
---|---|---|
Jean-Claude | a2bb209e2c | |
Jean-Claude | 6aae4afa06 | |
Jean-Claude | 747b34d51f | |
Jean-Claude | 027db6013c | |
Jean-Claude | f78831000b | |
Jean-Claude | 68295540bb | |
Jean-Claude | 2c2fae4e36 |
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)?)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
pub fn run() {
|
||||
use anyhow::Result;
|
||||
|
||||
pub fn run() -> Result<()> {
|
||||
println!("Run cull");
|
||||
todo!()
|
||||
}
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
pub fn run() {
|
||||
use anyhow::Result;
|
||||
|
||||
pub fn run() -> Result<()> {
|
||||
println!("Run init");
|
||||
todo!()
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 },
|
||||
}
|
||||
}
|
||||
}
|
129
src/git.rs
129
src/git.rs
|
@ -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)
|
||||
}
|
||||
|
|
16
src/main.rs
16
src/main.rs
|
@ -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)?;
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue