Compare commits
8 commits
v0.2.0-pre
...
main
Author | SHA1 | Date | |
---|---|---|---|
9ae778bb79 | |||
5ff3338b30 | |||
b1d66d7e9f | |||
aba9fa4025 | |||
37581ee6a0 | |||
aa57c0d092 | |||
4ebaee95cc | |||
9945b9eb7f |
2
.gitignore
vendored
|
@ -7,6 +7,6 @@ dist
|
|||
/target
|
||||
|
||||
# Project specific files
|
||||
test_album*
|
||||
/test_album*
|
||||
DESIGN.md
|
||||
TODO.md
|
||||
|
|
115
Cargo.lock
generated
|
@ -145,9 +145,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
|||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.9.0"
|
||||
version = "2.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
|
||||
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
|
||||
|
||||
[[package]]
|
||||
name = "bitstream-io"
|
||||
|
@ -200,9 +200,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
|||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.21"
|
||||
version = "1.2.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8691782945451c1c383942c4874dbe63814f61cb57ef773cda2972682b7bb3c0"
|
||||
checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
"libc",
|
||||
|
@ -227,9 +227,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
|||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.37"
|
||||
version = "4.5.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071"
|
||||
checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
|
@ -237,9 +237,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.37"
|
||||
version = "4.5.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2"
|
||||
checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
|
@ -349,6 +349,15 @@ dependencies = [
|
|||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
|
@ -472,9 +481,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.2"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0"
|
||||
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
|
@ -511,7 +520,7 @@ version = "0.9.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"bitflags 2.9.1",
|
||||
"ignore",
|
||||
"walkdir",
|
||||
]
|
||||
|
@ -528,9 +537,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.2"
|
||||
version = "0.15.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
||||
checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
|
@ -650,9 +659,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
|||
|
||||
[[package]]
|
||||
name = "jiff"
|
||||
version = "0.2.12"
|
||||
version = "0.2.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d07d8d955d798e7a4d6f9c58cd1f1916e790b42b092758a9ef6e16fef9f1b3fd"
|
||||
checksum = "f02000660d30638906021176af16b17498bd0d12813dbfe7b276d8bc7f3c0806"
|
||||
dependencies = [
|
||||
"jiff-static",
|
||||
"log",
|
||||
|
@ -663,9 +672,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "jiff-static"
|
||||
version = "0.2.12"
|
||||
version = "0.2.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f244cfe006d98d26f859c7abd1318d85327e1882dc9cef80f62daeeb0adcf300"
|
||||
checksum = "f3c30758ddd7188629c6713fc45d1188af4f44c90582311d0c8d8c9907f60c48"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -678,7 +687,7 @@ version = "0.1.33"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
|
||||
dependencies = [
|
||||
"getrandom 0.3.2",
|
||||
"getrandom 0.3.3",
|
||||
"libc",
|
||||
]
|
||||
|
||||
|
@ -698,6 +707,15 @@ dependencies = [
|
|||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kamadak-exif"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1130d80c7374efad55a117d715a3af9368f0fa7a2c54573afc15a188cd984837"
|
||||
dependencies = [
|
||||
"mutate_once",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
|
@ -792,6 +810,12 @@ dependencies = [
|
|||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mutate_once"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16cf681a23b4d0a43fc35024c176437f9dcd818db34e0f42ab456a0ee5ad497b"
|
||||
|
||||
[[package]]
|
||||
name = "new_debug_unreachable"
|
||||
version = "1.0.6"
|
||||
|
@ -824,6 +848,12 @@ dependencies = [
|
|||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||
|
||||
[[package]]
|
||||
name = "num-derive"
|
||||
version = "0.4.2"
|
||||
|
@ -929,7 +959,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "photojawn"
|
||||
version = "0.2.0-pre.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
|
@ -937,6 +967,7 @@ dependencies = [
|
|||
"fs_extra",
|
||||
"image",
|
||||
"indicatif",
|
||||
"kamadak-exif",
|
||||
"log",
|
||||
"mktemp",
|
||||
"pulldown-cmark",
|
||||
|
@ -945,6 +976,7 @@ dependencies = [
|
|||
"serde_yml",
|
||||
"tera",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -981,6 +1013,12 @@ dependencies = [
|
|||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
|
@ -1024,7 +1062,7 @@ version = "0.13.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"bitflags 2.9.1",
|
||||
"getopts",
|
||||
"memchr",
|
||||
"pulldown-cmark-escape",
|
||||
|
@ -1420,6 +1458,37 @@ dependencies = [
|
|||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
"num-conv",
|
||||
"powerfmt",
|
||||
"serde",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
|
||||
dependencies = [
|
||||
"num-conv",
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.22"
|
||||
|
@ -1761,9 +1830,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
|||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.9"
|
||||
version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9fb597c990f03753e08d3c29efbfcf2019a003b4bf4ba19225c158e1549f0f3"
|
||||
checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
@ -1774,7 +1843,7 @@ version = "0.39.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"bitflags 2.9.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
32
Cargo.toml
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "photojawn"
|
||||
version = "0.2.0-pre.1"
|
||||
version = "0.2.0"
|
||||
description = "A static site generator for photo albums"
|
||||
authors = ["Nick Pegg <nick@nickpegg.com>"]
|
||||
license = "MIT"
|
||||
|
@ -8,19 +8,21 @@ repository = "https://github.com/nickpegg/photojawn"
|
|||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "^1.0"
|
||||
clap = { version = "^4.5", features = ["derive"] }
|
||||
env_logger = "^0.11.8"
|
||||
fs_extra = "^1.3.0"
|
||||
image = "^0.25.6"
|
||||
indicatif = "^0.17.11"
|
||||
log = "^0.4.27"
|
||||
pulldown-cmark = "^0.13.0"
|
||||
rayon = "^1.10.0"
|
||||
serde = { version = "^1.0", features = ["derive"] }
|
||||
serde_yml = "^0.0.12"
|
||||
tera = { version = "^1.20", default-features = false }
|
||||
thiserror = "^2.0"
|
||||
anyhow = "1.0"
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
env_logger = "0.11.8"
|
||||
fs_extra = "1.3"
|
||||
image = "0.25.6"
|
||||
indicatif = "0.17.11"
|
||||
kamadak-exif = "0.6.1"
|
||||
log = "0.4.27"
|
||||
pulldown-cmark = "0.13.0"
|
||||
rayon = "1.10"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_yml = "0.0.12"
|
||||
tera = { version = "1.20", default-features = false }
|
||||
thiserror = "2.0"
|
||||
time = { version = "0.3.41", features = ["formatting", "macros", "parsing"] }
|
||||
|
||||
[dev-dependencies]
|
||||
mktemp = "^0.5.1"
|
||||
mktemp = "0.5.1"
|
||||
|
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
BIN
resources/test_album/nested1/moon.jpg
Normal file
After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
245
src/generate.rs
|
@ -1,9 +1,10 @@
|
|||
pub mod album_dir;
|
||||
mod image;
|
||||
|
||||
use crate::config::Config;
|
||||
use album_dir::{AlbumDir, Image};
|
||||
use crate::generate::image::Image;
|
||||
use album_dir::AlbumDir;
|
||||
use anyhow::{Context, anyhow};
|
||||
use image::imageops::FilterType;
|
||||
use indicatif::ProgressBar;
|
||||
use rayon::prelude::*;
|
||||
use serde::Serialize;
|
||||
|
@ -16,112 +17,7 @@ use std::path::{Path, PathBuf};
|
|||
use std::sync::Mutex;
|
||||
use tera::Tera;
|
||||
|
||||
const IMG_RESIZE_FILTER: FilterType = FilterType::Lanczos3;
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
struct Breadcrumb {
|
||||
path: PathBuf,
|
||||
name: String,
|
||||
}
|
||||
|
||||
/// A Tera context for album pages
|
||||
#[derive(Serialize, Debug)]
|
||||
struct AlbumContext {
|
||||
// Path required to get back to the root album
|
||||
root_path: PathBuf,
|
||||
|
||||
name: String,
|
||||
description: String,
|
||||
|
||||
images: Vec<Image>,
|
||||
|
||||
// Path to the cover image thumbnail within /slides/, relative to the album dir. Used when
|
||||
// linking to an album from a parent album
|
||||
cover_thumbnail_path: PathBuf,
|
||||
|
||||
// list of:
|
||||
// - relative dir to walk up to root, e.g. "../../.."
|
||||
// - dir name
|
||||
breadcrumbs: Vec<Breadcrumb>,
|
||||
|
||||
// Immediate children of this album
|
||||
children: Vec<AlbumContext>,
|
||||
}
|
||||
|
||||
impl TryFrom<&AlbumDir> for AlbumContext {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(album: &AlbumDir) -> anyhow::Result<Self> {
|
||||
let name: String = match album.path.file_name() {
|
||||
Some(n) => n.to_string_lossy().to_string(),
|
||||
None => String::new(),
|
||||
};
|
||||
|
||||
// Build breadcrumbs
|
||||
let mut breadcrumbs: Vec<Breadcrumb> = Vec::new();
|
||||
{
|
||||
let mut album_path = album.path.clone();
|
||||
let mut relpath: PathBuf = PathBuf::new();
|
||||
while album_path.pop() {
|
||||
let filename: &OsStr = album_path.file_name().unwrap_or(OsStr::new("Home"));
|
||||
relpath.push("..");
|
||||
breadcrumbs.push(Breadcrumb {
|
||||
path: relpath.clone(),
|
||||
name: filename.to_string_lossy().to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
breadcrumbs.reverse();
|
||||
log::debug!("Crumbs for {}: {breadcrumbs:?}", album.path.display());
|
||||
|
||||
// The first breadcrumb path is the relative path to the root album
|
||||
let root_path = if !breadcrumbs.is_empty() {
|
||||
breadcrumbs[0].path.clone()
|
||||
} else {
|
||||
PathBuf::new()
|
||||
};
|
||||
|
||||
// Make the path to the thumbnail relative to the album that we're in
|
||||
let cover_thumbnail_path: PathBuf;
|
||||
if let Some(parent) = album.path.parent() {
|
||||
cover_thumbnail_path = album.cover.thumb_path.strip_prefix(parent)?.to_path_buf()
|
||||
} else {
|
||||
cover_thumbnail_path = album
|
||||
.cover
|
||||
.thumb_path
|
||||
.strip_prefix(&album.path)?
|
||||
.to_path_buf()
|
||||
};
|
||||
|
||||
let children: Vec<AlbumContext> = album
|
||||
.children
|
||||
.iter()
|
||||
.map(AlbumContext::try_from)
|
||||
.collect::<anyhow::Result<Vec<AlbumContext>>>()?;
|
||||
let images: Vec<Image> = album.images.clone();
|
||||
|
||||
Ok(AlbumContext {
|
||||
root_path,
|
||||
name,
|
||||
description: album.description.clone(),
|
||||
breadcrumbs,
|
||||
images,
|
||||
children,
|
||||
cover_thumbnail_path,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A Tera context for slide (individual image) pages
|
||||
#[derive(Serialize, Debug)]
|
||||
struct SlideContext {
|
||||
// Path required to get back to the root album
|
||||
root_path: PathBuf,
|
||||
image: Image,
|
||||
prev_image: Option<Image>,
|
||||
next_image: Option<Image>,
|
||||
}
|
||||
|
||||
const IMG_RESIZE_FILTER: ::image::imageops::FilterType = ::image::imageops::FilterType::Lanczos3;
|
||||
/// Generate an album
|
||||
///
|
||||
/// `root_path` is a path to the root directory of the album. `full` if true will regenerate
|
||||
|
@ -202,7 +98,8 @@ fn generate_images(config: &Config, album: &AlbumDir, full: bool) -> anyhow::Res
|
|||
fs::hard_link(&img.path, &full_size_path)
|
||||
.with_context(|| format!("Error creating hard link at {}", full_size_path.display()))?;
|
||||
|
||||
let orig_image = image::open(&img.path)?;
|
||||
let orig_image = ::image::open(&img.path)
|
||||
.with_context(|| format!("Failed to read image {}", &img.path.display()))?;
|
||||
let thumb_path = output_path.join(&img.thumb_path);
|
||||
log::info!(
|
||||
"Resizing {} -> {}",
|
||||
|
@ -246,7 +143,7 @@ fn generate_html(config: &Config, album: &AlbumDir) -> anyhow::Result<()> {
|
|||
.path
|
||||
.join("_templates/*.html")
|
||||
.to_str()
|
||||
.ok_or(anyhow!("Missing _templates dir in album dir"))?,
|
||||
.ok_or(anyhow!("Album path {} is invalid", album.path.display()))?,
|
||||
)?;
|
||||
|
||||
println!("Generating HTML...");
|
||||
|
@ -301,15 +198,118 @@ fn generate_html(config: &Config, album: &AlbumDir) -> anyhow::Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
struct Breadcrumb {
|
||||
path: PathBuf,
|
||||
name: String,
|
||||
}
|
||||
|
||||
/// A Tera context for album pages
|
||||
#[derive(Serialize, Debug)]
|
||||
struct AlbumContext {
|
||||
/// Path required to get back to the root album
|
||||
root_path: PathBuf,
|
||||
|
||||
name: String,
|
||||
description: String,
|
||||
|
||||
images: Vec<Image>,
|
||||
|
||||
/// Path to the cover image thumbnail within /slides/, relative to the album dir. Used when
|
||||
/// linking to an album from a parent album
|
||||
cover_thumbnail_path: PathBuf,
|
||||
|
||||
/// list of:
|
||||
/// - relative dir to walk up to root, e.g. "../../.."
|
||||
/// - dir name
|
||||
breadcrumbs: Vec<Breadcrumb>,
|
||||
|
||||
/// Immediate children of this album
|
||||
children: Vec<AlbumContext>,
|
||||
}
|
||||
|
||||
impl TryFrom<&AlbumDir> for AlbumContext {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(album: &AlbumDir) -> anyhow::Result<Self> {
|
||||
let name: String = match album.path.file_name() {
|
||||
Some(n) => n.to_string_lossy().to_string(),
|
||||
None => String::new(),
|
||||
};
|
||||
|
||||
// Build breadcrumbs
|
||||
let mut breadcrumbs: Vec<Breadcrumb> = Vec::new();
|
||||
{
|
||||
let mut album_path = album.path.clone();
|
||||
let mut relpath: PathBuf = PathBuf::new();
|
||||
while album_path.pop() {
|
||||
let filename: &OsStr = album_path.file_name().unwrap_or(OsStr::new("Home"));
|
||||
relpath.push("..");
|
||||
breadcrumbs.push(Breadcrumb {
|
||||
path: relpath.clone(),
|
||||
name: filename.to_string_lossy().to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
breadcrumbs.reverse();
|
||||
log::debug!("Crumbs for {}: {breadcrumbs:?}", album.path.display());
|
||||
|
||||
// The first breadcrumb path is the relative path to the root album
|
||||
let root_path = match breadcrumbs.is_empty() {
|
||||
false => breadcrumbs[0].path.clone(),
|
||||
true => PathBuf::new(),
|
||||
};
|
||||
|
||||
// Make the path to the thumbnail relative to the album that we're in
|
||||
let cover_thumbnail_path: PathBuf;
|
||||
if let Some(parent) = album.path.parent() {
|
||||
cover_thumbnail_path = album.cover.thumb_path.strip_prefix(parent)?.to_path_buf()
|
||||
} else {
|
||||
cover_thumbnail_path = album
|
||||
.cover
|
||||
.thumb_path
|
||||
.strip_prefix(&album.path)?
|
||||
.to_path_buf()
|
||||
};
|
||||
|
||||
let children: Vec<AlbumContext> = album
|
||||
.children
|
||||
.iter()
|
||||
.map(AlbumContext::try_from)
|
||||
.collect::<anyhow::Result<Vec<AlbumContext>>>()?;
|
||||
let images: Vec<Image> = album.images.clone();
|
||||
|
||||
Ok(AlbumContext {
|
||||
root_path,
|
||||
name,
|
||||
description: album.description.clone(),
|
||||
breadcrumbs,
|
||||
images,
|
||||
children,
|
||||
cover_thumbnail_path,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A Tera context for slide (individual image) pages
|
||||
#[derive(Serialize, Debug)]
|
||||
struct SlideContext {
|
||||
// Path required to get back to the root album
|
||||
root_path: PathBuf,
|
||||
image: Image,
|
||||
prev_image: Option<Image>,
|
||||
next_image: Option<Image>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::generate;
|
||||
use crate::skel::make_skeleton;
|
||||
use mktemp::Temp;
|
||||
use std::collections::{HashSet, VecDeque};
|
||||
use std::ffi::OsStr;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::test_util::{init, make_test_album};
|
||||
|
||||
#[test]
|
||||
/// Test that the generate function creates a rendered site as we expect it
|
||||
fn test_generate() {
|
||||
|
@ -320,27 +320,6 @@ mod tests {
|
|||
check_album(output_path).unwrap();
|
||||
}
|
||||
|
||||
fn init() {
|
||||
let _ = env_logger::builder().is_test(true).try_init();
|
||||
}
|
||||
|
||||
/// Copies the test album to a tempdir and returns the path to it
|
||||
fn make_test_album() -> Temp {
|
||||
let tmpdir = Temp::new_dir().unwrap();
|
||||
let source_path = Path::new("resources/test_album");
|
||||
|
||||
log::info!("Creating test album in {}", tmpdir.display());
|
||||
make_skeleton(&tmpdir.to_path_buf()).unwrap();
|
||||
fs_extra::dir::copy(
|
||||
&source_path,
|
||||
&tmpdir,
|
||||
&fs_extra::dir::CopyOptions::new().content_only(true),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
tmpdir
|
||||
}
|
||||
|
||||
/// Does basic sanity checks on an output album
|
||||
fn check_album(root_path: PathBuf) -> anyhow::Result<()> {
|
||||
log::debug!("Checking album dir {}", root_path.display());
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::generate::image::Image;
|
||||
use anyhow::anyhow;
|
||||
use image::ImageReader;
|
||||
use serde::Serialize;
|
||||
use std::ffi::OsString;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::slice::Iter;
|
||||
|
@ -169,93 +169,6 @@ impl<'a> Iterator for AlbumImageIter<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize)]
|
||||
pub struct Image {
|
||||
/// Path to the image, relative to the root album
|
||||
pub path: PathBuf,
|
||||
pub filename: String,
|
||||
|
||||
/// Text description of the image which is displayed below it on the HTML page
|
||||
pub description: String,
|
||||
|
||||
pub thumb_filename: String,
|
||||
pub thumb_path: PathBuf,
|
||||
pub screen_filename: String,
|
||||
pub screen_path: PathBuf,
|
||||
pub html_filename: String,
|
||||
pub html_path: PathBuf,
|
||||
}
|
||||
|
||||
impl Image {
|
||||
pub fn new(path: PathBuf, description: String) -> anyhow::Result<Self> {
|
||||
let filename = path
|
||||
.file_name()
|
||||
.ok_or(anyhow!(
|
||||
"Image path {} is missing a filename",
|
||||
path.display()
|
||||
))?
|
||||
.to_str()
|
||||
.ok_or(anyhow!("Cannot convert {} to a string", path.display()))?
|
||||
.to_string();
|
||||
let thumb_filename = Self::slide_filename(&path, "thumb", true)?;
|
||||
let thumb_path = Self::slide_path(&path, &thumb_filename);
|
||||
let screen_filename = Self::slide_filename(&path, "screen", true)?;
|
||||
let screen_path = Self::slide_path(&path, &screen_filename);
|
||||
let html_filename = Self::slide_filename(&path, "html", false)?;
|
||||
let html_path = Self::slide_path(&path, &html_filename);
|
||||
|
||||
Ok(Image {
|
||||
path,
|
||||
description,
|
||||
filename,
|
||||
thumb_filename,
|
||||
thumb_path,
|
||||
screen_filename,
|
||||
screen_path,
|
||||
html_filename,
|
||||
html_path,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the filename for a given slide type. For example if ext = "thumb" and the current
|
||||
/// filename is "blah.jpg" this will return "blah.thumb.jpg". If keep_ext if false, it would
|
||||
/// return "blah.thumb"
|
||||
fn slide_filename(path: &Path, ext: &str, keep_ext: bool) -> anyhow::Result<String> {
|
||||
let mut new_ext: OsString = ext.into();
|
||||
if keep_ext {
|
||||
if let Some(e) = path.extension() {
|
||||
new_ext = OsString::from(
|
||||
ext.to_string()
|
||||
+ "."
|
||||
+ e.to_str().ok_or(anyhow!(
|
||||
"Image {} extension is not valid UTF-8",
|
||||
path.display()
|
||||
))?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let new_path = path.with_extension(new_ext);
|
||||
let new_name = new_path
|
||||
.file_name()
|
||||
.ok_or(anyhow!("Image {} missing a file name", path.display()))?
|
||||
.to_str()
|
||||
.ok_or(anyhow!("Unable to convert {} to a string", path.display()))?
|
||||
.to_string();
|
||||
|
||||
Ok(new_name)
|
||||
}
|
||||
|
||||
/// Returns the path to the file in the slides dir given the path to the original image
|
||||
fn slide_path(path: &Path, file_name: &str) -> PathBuf {
|
||||
let mut new_path = path.to_path_buf();
|
||||
new_path.pop();
|
||||
new_path.push("slides");
|
||||
new_path.push(file_name);
|
||||
new_path
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
@ -314,17 +227,4 @@ mod tests {
|
|||
]);
|
||||
assert_eq!(imgs, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_paths() {
|
||||
let img = Image::new(PathBuf::from("foo/bar/image.jpg"), String::new()).unwrap();
|
||||
assert_eq!(
|
||||
img.thumb_path,
|
||||
PathBuf::from("foo/bar/slides/image.thumb.jpg")
|
||||
);
|
||||
assert_eq!(
|
||||
img.screen_path,
|
||||
PathBuf::from("foo/bar/slides/image.screen.jpg")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
108
src/generate/image.rs
Normal file
|
@ -0,0 +1,108 @@
|
|||
use anyhow::anyhow;
|
||||
use serde::Serialize;
|
||||
use std::ffi::OsString;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize)]
|
||||
pub struct Image {
|
||||
/// Path to the image, relative to the root album
|
||||
pub path: PathBuf,
|
||||
pub filename: String,
|
||||
|
||||
/// Text description of the image which is displayed below it on the HTML page
|
||||
pub description: String,
|
||||
|
||||
pub thumb_filename: String,
|
||||
pub thumb_path: PathBuf,
|
||||
pub screen_filename: String,
|
||||
pub screen_path: PathBuf,
|
||||
pub html_filename: String,
|
||||
pub html_path: PathBuf,
|
||||
}
|
||||
impl Image {
|
||||
pub fn new(path: PathBuf, description: String) -> anyhow::Result<Self> {
|
||||
let filename = path
|
||||
.file_name()
|
||||
.ok_or(anyhow!(
|
||||
"Image path {} is missing a filename",
|
||||
path.display()
|
||||
))?
|
||||
.to_str()
|
||||
.ok_or(anyhow!("Cannot convert {} to a string", path.display()))?
|
||||
.to_string();
|
||||
let thumb_filename = Self::slide_filename(&path, "thumb", true)?;
|
||||
let thumb_path = Self::slide_path(&path, &thumb_filename);
|
||||
let screen_filename = Self::slide_filename(&path, "screen", true)?;
|
||||
let screen_path = Self::slide_path(&path, &screen_filename);
|
||||
let html_filename = Self::slide_filename(&path, "html", false)?;
|
||||
let html_path = Self::slide_path(&path, &html_filename);
|
||||
|
||||
Ok(Image {
|
||||
path,
|
||||
description,
|
||||
filename,
|
||||
thumb_filename,
|
||||
thumb_path,
|
||||
screen_filename,
|
||||
screen_path,
|
||||
html_filename,
|
||||
html_path,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the filename for a given slide type. For example if ext = "thumb" and the current
|
||||
/// filename is "blah.jpg" this will return "blah.thumb.jpg". If keep_ext if false, it would
|
||||
/// return "blah.thumb"
|
||||
fn slide_filename(path: &Path, ext: &str, keep_ext: bool) -> anyhow::Result<String> {
|
||||
let mut new_ext: OsString = ext.into();
|
||||
if keep_ext {
|
||||
if let Some(e) = path.extension() {
|
||||
new_ext = OsString::from(
|
||||
ext.to_string()
|
||||
+ "."
|
||||
+ e.to_str().ok_or(anyhow!(
|
||||
"Image {} extension is not valid UTF-8",
|
||||
path.display()
|
||||
))?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let new_path = path.with_extension(new_ext);
|
||||
let new_name = new_path
|
||||
.file_name()
|
||||
.ok_or(anyhow!("Image {} missing a file name", path.display()))?
|
||||
.to_str()
|
||||
.ok_or(anyhow!("Unable to convert {} to a string", path.display()))?
|
||||
.to_string();
|
||||
|
||||
Ok(new_name)
|
||||
}
|
||||
|
||||
/// Returns the path to the file in the slides dir given the path to the original image
|
||||
fn slide_path(path: &Path, file_name: &str) -> PathBuf {
|
||||
let mut new_path = path.to_path_buf();
|
||||
new_path.pop();
|
||||
new_path.push("slides");
|
||||
new_path.push(file_name);
|
||||
new_path
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn image_paths() {
|
||||
let img = Image::new(PathBuf::from("foo/bar/image.jpg"), String::new()).unwrap();
|
||||
assert_eq!(
|
||||
img.thumb_path,
|
||||
PathBuf::from("foo/bar/slides/image.thumb.jpg")
|
||||
);
|
||||
assert_eq!(
|
||||
img.screen_path,
|
||||
PathBuf::from("foo/bar/slides/image.screen.jpg")
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,3 +1,7 @@
|
|||
pub mod config;
|
||||
pub(crate) mod config;
|
||||
pub mod generate;
|
||||
pub mod reorganize;
|
||||
pub mod skel;
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod test_util;
|
||||
|
|
17
src/main.rs
|
@ -1,5 +1,6 @@
|
|||
use clap::{Parser, Subcommand};
|
||||
use photojawn::generate::generate;
|
||||
use photojawn::reorganize::reorganize;
|
||||
use photojawn::skel::make_skeleton;
|
||||
use std::path::Path;
|
||||
|
||||
|
@ -18,6 +19,9 @@ fn main() -> anyhow::Result<()> {
|
|||
let path = generate(&album_path.to_path_buf(), full)?;
|
||||
println!("Album site generated in {}", path.display());
|
||||
}
|
||||
Commands::Reorganize { path, dry_run } => {
|
||||
reorganize(Path::new(&path), dry_run)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -44,4 +48,17 @@ enum Commands {
|
|||
#[arg(long)]
|
||||
full: bool,
|
||||
},
|
||||
/// Reorganize photos in an album by date
|
||||
Reorganize {
|
||||
/// Directory of images you want to reorganize. Only image files will be moved.
|
||||
///
|
||||
/// The new image filenames will be the date and time taken, followed by the original
|
||||
/// filename. For example:
|
||||
/// original_filename.jpg -> YYYYMMDD_HHSS_original_filename.jpg
|
||||
#[arg()]
|
||||
path: String,
|
||||
/// Don't actually reorganize, just say what renames would happen
|
||||
#[arg(long)]
|
||||
dry_run: bool,
|
||||
},
|
||||
}
|
||||
|
|
243
src/reorganize.rs
Normal file
|
@ -0,0 +1,243 @@
|
|||
use anyhow::{anyhow, Context};
|
||||
use image::ImageReader;
|
||||
use std::ffi::OsStr;
|
||||
use std::fs::{rename, File};
|
||||
use std::io::BufReader;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::from_utf8;
|
||||
use thiserror::Error;
|
||||
use time::macros::format_description;
|
||||
use time::{OffsetDateTime, PrimitiveDateTime, UtcDateTime};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum OrganizeError {
|
||||
#[error("These files are not supported, unable to parse EXIF data: {0:?}")]
|
||||
ExifNotSupported(Vec<PathBuf>),
|
||||
#[error("File {0} is missing an EXIF DateTimeOriginal field")]
|
||||
ExifNoDateTime(PathBuf),
|
||||
}
|
||||
|
||||
pub fn reorganize(dir: &Path, dry_run: bool) -> anyhow::Result<()> {
|
||||
let renames = get_renames(dir)?;
|
||||
|
||||
if renames.is_empty() {
|
||||
println!("Nothing to rename");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Either do the renames, or if dry-run print what the names would be
|
||||
if dry_run {
|
||||
for (src, dst) in renames {
|
||||
println!("{} -> {}", src.display(), dst.display());
|
||||
}
|
||||
println!("Would have renamed the above files");
|
||||
} else {
|
||||
for (src, dst) in renames {
|
||||
println!("{} -> {}", src.display(), dst.display());
|
||||
rename(&src, &dst).with_context(|| {
|
||||
format!("Failed to rename {} to {}", src.display(), dst.display())
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns a vec of tuples of all the renames that need to happen in a directory
|
||||
fn get_renames(dir: &Path) -> anyhow::Result<Vec<(PathBuf, PathBuf)>> {
|
||||
let mut renames: Vec<(PathBuf, PathBuf)> = Vec::new();
|
||||
|
||||
// Run through all the images and figure out new names for them
|
||||
for entry in dir.read_dir()? {
|
||||
let entry = entry?;
|
||||
|
||||
if !entry.path().is_file() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only bother with image files, because those are the only hope for EXIF
|
||||
let is_image: bool = ImageReader::open(entry.path())?
|
||||
.with_guessed_format()?
|
||||
.format()
|
||||
.is_some();
|
||||
|
||||
let is_cover: bool = entry
|
||||
.path()
|
||||
.file_name()
|
||||
.is_some_and(|n| n.to_string_lossy().starts_with("cover"));
|
||||
|
||||
if is_image && !is_cover {
|
||||
// TODO: Should we just skip over images with no EXIF data? Find datetime some other
|
||||
// way?
|
||||
let Ok(dt) = get_exif_datetime(entry.path()) else {
|
||||
log::warn!(
|
||||
"Unable to read datetime from EXIF for {}",
|
||||
entry.path().display()
|
||||
);
|
||||
continue;
|
||||
};
|
||||
let orig_filename = entry
|
||||
.path()
|
||||
.file_name()
|
||||
.unwrap_or(OsStr::new(""))
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
|
||||
let ext = entry
|
||||
.path()
|
||||
.extension()
|
||||
.ok_or(anyhow!(
|
||||
"{} is missing an extension",
|
||||
entry.path().display()
|
||||
))?
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
let new_filename_base = dt.format(format_description!(
|
||||
"[year][month][day]_[hour][minute][second]_"
|
||||
))?;
|
||||
|
||||
// Renaming an already-renamed file should be a no-op
|
||||
if orig_filename.starts_with(&new_filename_base) {
|
||||
log::info!("{orig_filename} looks like it was already renamed, skiping");
|
||||
continue;
|
||||
}
|
||||
|
||||
let new_path = entry
|
||||
.path()
|
||||
.with_file_name(new_filename_base + &orig_filename)
|
||||
.with_extension(ext);
|
||||
|
||||
renames.push((entry.path(), new_path.clone()));
|
||||
|
||||
// Check for files associated with this image and set them up to be renamed too, like
|
||||
// description files that end with .txt or .md
|
||||
for ext in ["txt", "md"] {
|
||||
let side_file_path = entry.path().with_extension(ext);
|
||||
if side_file_path.exists() {
|
||||
let new_side_file_path = new_path.with_extension(ext);
|
||||
renames.push((side_file_path, new_side_file_path));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort renames by the destination
|
||||
renames.sort_by_key(|(_, dst)| dst.clone());
|
||||
|
||||
Ok(renames)
|
||||
}
|
||||
|
||||
/// Tries to figure out the datetime that the image was created from EXIF metadata
|
||||
fn get_exif_datetime(path: PathBuf) -> anyhow::Result<UtcDateTime> {
|
||||
let format_with_offset = format_description!(
|
||||
"[year]:[month]:[day] [hour]:[minute]:[second][offset_hour]:[offset_minute]"
|
||||
);
|
||||
let format_without_offset =
|
||||
format_description!(version = 2, "[year]:[month]:[day] [hour]:[minute]:[second]");
|
||||
|
||||
let file = File::open(&path).with_context(|| format!("Couldn't open {}", path.display()))?;
|
||||
let mut bufreader = BufReader::new(file);
|
||||
let exif = exif::Reader::new()
|
||||
.read_from_container(&mut bufreader)
|
||||
.with_context(|| format!("Couldn't read EXIF data from {}", path.display()))?;
|
||||
let field = exif
|
||||
.get_field(exif::Tag::DateTimeOriginal, exif::In::PRIMARY)
|
||||
.ok_or(OrganizeError::ExifNoDateTime(path.clone()))?;
|
||||
|
||||
let dt: UtcDateTime = match &field.value {
|
||||
exif::Value::Ascii(v) => {
|
||||
let s = from_utf8(&v[0])?;
|
||||
log::debug!("Date string from file: {s}");
|
||||
|
||||
match OffsetDateTime::parse(s, format_with_offset) {
|
||||
Ok(v) => v.to_utc(),
|
||||
Err(_) => PrimitiveDateTime::parse(s, format_without_offset)?.as_utc(),
|
||||
}
|
||||
}
|
||||
_ => return Err(OrganizeError::ExifNoDateTime(path).into()),
|
||||
};
|
||||
|
||||
Ok(dt)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_util::{init, make_test_album};
|
||||
use time::{Date, Month, Time};
|
||||
|
||||
#[test]
|
||||
/// Make sure we can get the datetime from one of our test photos
|
||||
fn basic_datetime_read() {
|
||||
init();
|
||||
let dt = get_exif_datetime("resources/test_album/moon.jpg".into()).unwrap();
|
||||
log::info!("Got dt: {dt}");
|
||||
assert_eq!(
|
||||
dt,
|
||||
UtcDateTime::new(
|
||||
Date::from_calendar_date(1970, Month::January, 1).unwrap(),
|
||||
Time::from_hms(13, 37, 0).unwrap(),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exif_datetime_missing() {
|
||||
init();
|
||||
let result = get_exif_datetime("resources/test_album/mountains.jpg".into());
|
||||
assert!(result.is_err());
|
||||
//result.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_renames() {
|
||||
init();
|
||||
let tmp_album_dir = make_test_album();
|
||||
let dir = tmp_album_dir.join("with_description");
|
||||
|
||||
log::debug!("Getting renames for {}", dir.display());
|
||||
let renames = get_renames(&dir).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
renames,
|
||||
vec![
|
||||
(dir.join("moon.jpg"), dir.join("19700102_133700_moon.jpg")),
|
||||
(dir.join("moon.txt"), dir.join("19700102_133700_moon.txt")),
|
||||
(
|
||||
dir.join("mountains.jpg"),
|
||||
dir.join("19700103_133700_mountains.jpg")
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// get_renames() should ignore other stuff in the directory
|
||||
fn test_other_junk() {
|
||||
init();
|
||||
let tmp_album_dir = make_test_album();
|
||||
|
||||
let renames = get_renames(&tmp_album_dir).unwrap();
|
||||
// No mountain.jpg since it doesn't have EXIF data
|
||||
assert_eq!(
|
||||
renames,
|
||||
vec![(
|
||||
tmp_album_dir.join("moon.jpg"),
|
||||
tmp_album_dir.join("19700101_133700_moon.jpg")
|
||||
)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// The rename function will prepend date and time to the original filenames. If we do it a
|
||||
/// second time, it should be a no-op instead of continuing to prepend date and time.
|
||||
fn test_rerename() {
|
||||
let tmp_album_dir = make_test_album();
|
||||
let dir = tmp_album_dir.join("with_description");
|
||||
reorganize(&dir, false).unwrap();
|
||||
|
||||
let renames = get_renames(&dir).unwrap();
|
||||
assert_eq!(renames, Vec::new());
|
||||
}
|
||||
}
|
25
src/test_util.rs
Normal file
|
@ -0,0 +1,25 @@
|
|||
use crate::skel::make_skeleton;
|
||||
use mktemp::Temp;
|
||||
use std::path::Path;
|
||||
|
||||
pub fn init() {
|
||||
let _ = env_logger::builder().is_test(true).try_init();
|
||||
}
|
||||
|
||||
/// Copies the test album to a tempdir and returns the path to it. Returns a Temp object which
|
||||
/// cleans up the directory on drop, so make sure to persist the variable until you're done with it
|
||||
pub fn make_test_album() -> Temp {
|
||||
let tmpdir = Temp::new_dir().unwrap();
|
||||
let source_path = Path::new("resources/test_album").canonicalize().unwrap();
|
||||
|
||||
log::info!("Creating test album in {}", tmpdir.display());
|
||||
make_skeleton(&tmpdir.to_path_buf()).unwrap();
|
||||
fs_extra::dir::copy(
|
||||
&source_path,
|
||||
&tmpdir,
|
||||
&fs_extra::dir::CopyOptions::new().content_only(true),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
tmpdir
|
||||
}
|