1
Fork 0
mirror of https://github.com/thegeneralist01/archivr synced 2026-05-30 08:36:47 +02:00

Handle archive command errors without panics

This commit is contained in:
TheGeneralist 2026-05-29 16:41:52 +02:00
parent ce3aaa8b76
commit 4d34ffaa32
Signed by: thegeneralist01
SSH key fingerprint: SHA256:pp9qddbCNmVNoSjevdvQvM5z0DHN7LTa8qBMbcMq/R4

View file

@ -57,17 +57,17 @@ enum Command {
}, },
} }
fn get_archive_path() -> Option<PathBuf> { fn get_archive_path() -> Result<Option<PathBuf>> {
let mut dir = env::current_dir().unwrap(); let mut dir = env::current_dir().context("failed to read current working directory")?;
loop { loop {
if dir.join(".archivr").is_dir() { if dir.join(".archivr").is_dir() {
return Some(dir.join(".archivr")); return Ok(Some(dir.join(".archivr")));
} }
if !dir.pop() { if !dir.pop() {
break; break;
} }
} }
None Ok(None)
} }
#[derive(Debug, PartialEq, Eq, Clone, Copy)] #[derive(Debug, PartialEq, Eq, Clone, Copy)]
@ -91,13 +91,9 @@ use crate::twitter::parse_tweet_id;
fn expand_shorthand_to_url(path: &str, source: &Source) -> String { fn expand_shorthand_to_url(path: &str, source: &Source) -> String {
if *source == Source::X && (path.starts_with("tweet:media:") || path.starts_with("x:media:")) { if *source == Source::X && (path.starts_with("tweet:media:") || path.starts_with("x:media:")) {
return format!( if let Some(tweet_id) = path.split(':').next_back().and_then(parse_tweet_id) {
"https://x.com/i/status/{}", return format!("https://x.com/i/status/{tweet_id}");
path.split(':') }
.next_back()
.and_then(parse_tweet_id)
.unwrap()
);
} }
if let Some(path) = path.strip_prefix("instagram:") { if let Some(path) = path.strip_prefix("instagram:") {
@ -294,52 +290,26 @@ fn determine_source(path: &str) -> Source {
Source::Other Source::Other
} }
fn hash_exists(filename: String, store_path: &Path) -> bool { fn hash_exists(hash: &str, file_extension: &str, store_path: &Path) -> Result<bool> {
let mut chars = filename.chars(); let path = store_path.join(raw_relative_path_from_hash(hash, file_extension)?);
let first_letter = chars.next().unwrap();
let second_letter = chars.next().unwrap();
let path = store_path
.join("raw")
.join(first_letter.to_string())
.join(second_letter.to_string())
.join(filename);
println!("Checking {}", path.display()); println!("Checking {}", path.display());
path.exists() Ok(path.exists())
} }
fn move_temp_to_raw(file: &Path, hash: &String, store_path: &Path) -> Result<()> { fn move_temp_to_raw(file: &Path, hash: &str, store_path: &Path) -> Result<()> {
let mut chars = hash.chars();
let first_letter = chars.next().unwrap().to_string();
let second_letter = chars.next().unwrap().to_string();
let file_extension = file let file_extension = file
.extension() .extension()
.map_or(String::new(), |ext| format!(".{}", ext.to_string_lossy())); .map_or(String::new(), |ext| format!(".{}", ext.to_string_lossy()));
let raw_relpath = raw_relative_path_from_hash(hash, &file_extension)?;
let destination = store_path.join(raw_relpath);
fs::create_dir_all( if let Some(parent) = destination.parent() {
store_path fs::create_dir_all(parent)?;
.join("raw")
.join(&first_letter)
.join(&second_letter),
)?;
fs::rename(
file,
store_path
.join("raw")
.join(&first_letter)
.join(&second_letter)
.join(format!(
"{hash}{}",
if file_extension.is_empty() {
""
} else {
&file_extension
} }
)),
)?; fs::rename(file, destination)?;
Ok(()) Ok(())
} }
@ -582,7 +552,7 @@ fn record_tweet_entry(
)?; )?;
let tweet_json = fs::read_to_string(store_path.join(&tweet_json_relpath))?; let tweet_json = fs::read_to_string(store_path.join(&tweet_json_relpath))?;
for (role, raw_relpath) in tweet_raw_artifacts(&tweet_json) { for (role, raw_relpath) in tweet_raw_artifacts(&tweet_json)? {
let raw_path = PathBuf::from(&raw_relpath); let raw_path = PathBuf::from(&raw_relpath);
let blob = blob_record_for_raw_relpath(store_path, &raw_path)?; let blob = blob_record_for_raw_relpath(store_path, &raw_path)?;
let blob_id = database::upsert_blob(conn, &blob)?; let blob_id = database::upsert_blob(conn, &blob)?;
@ -604,8 +574,8 @@ fn record_tweet_entry(
Ok(entry) Ok(entry)
} }
fn tweet_raw_artifacts(tweet_json: &str) -> Vec<(String, String)> { fn tweet_raw_artifacts(tweet_json: &str) -> Result<Vec<(String, String)>> {
let regex = regex::Regex::new(r#""(avatar_local_path|local_path)": "([^"\n]+)""#).unwrap(); let regex = regex::Regex::new(r#""(avatar_local_path|local_path)": "([^"\n]+)""#)?;
let mut seen = HashSet::new(); let mut seen = HashSet::new();
let mut artifacts = Vec::new(); let mut artifacts = Vec::new();
@ -623,7 +593,7 @@ fn tweet_raw_artifacts(tweet_json: &str) -> Vec<(String, String)> {
artifacts.push((role.to_string(), relpath)); artifacts.push((role.to_string(), relpath));
} }
artifacts Ok(artifacts)
} }
fn fail_archive_and_exit( fn fail_archive_and_exit(
@ -643,7 +613,7 @@ fn main() -> Result<()> {
match args.command { match args.command {
Command::Archive { ref path } => { Command::Archive { ref path } => {
let archive_path = match get_archive_path() { let archive_path = match get_archive_path()? {
Some(path) => path, Some(path) => path,
None => { None => {
eprintln!("Not in an archive. Use 'archivr init' to create one."); eprintln!("Not in an archive. Use 'archivr init' to create one.");
@ -811,7 +781,7 @@ fn main() -> Result<()> {
.with_context(|| format!("failed to stat staged file {}", temp_file.display()))? .with_context(|| format!("failed to stat staged file {}", temp_file.display()))?
.len() as i64; .len() as i64;
let hash_exists = hash_exists(format!("{hash}{file_extension}"), &store_path); let hash_exists = hash_exists(&hash, &file_extension, &store_path)?;
// TODO: check for repeated archives? // TODO: check for repeated archives?
// There could be one of the following: // There could be one of the following:
@ -869,7 +839,9 @@ fn main() -> Result<()> {
} => { } => {
let archive_path = Path::new(&archive_path_string).join(".archivr"); let archive_path = Path::new(&archive_path_string).join(".archivr");
let store_path = if Path::new(&store_path_string).is_relative() { let store_path = if Path::new(&store_path_string).is_relative() {
env::current_dir().unwrap().join(store_path_string) env::current_dir()
.context("failed to read current working directory")?
.join(store_path_string)
} else { } else {
Path::new(store_path_string).to_path_buf() Path::new(store_path_string).to_path_buf()
}; };
@ -899,14 +871,18 @@ fn main() -> Result<()> {
process::exit(1); process::exit(1);
} }
fs::create_dir_all(&archive_path).unwrap(); fs::create_dir_all(&archive_path)?;
fs::create_dir_all(&store_path).unwrap(); fs::create_dir_all(&store_path)?;
fs::write(archive_path.join("name"), archive_name).unwrap(); fs::write(archive_path.join("name"), archive_name)?;
let _ = fs::write( fs::write(
archive_path.join("store_path"), archive_path.join("store_path"),
store_path.canonicalize().unwrap().to_str().unwrap(), store_path
); .canonicalize()
initialize_store_directories(&store_path).unwrap(); .with_context(|| format!("failed to canonicalize {}", store_path.display()))?
.to_str()
.context("store path is not valid UTF-8")?,
)?;
initialize_store_directories(&store_path)?;
let conn = database::open_or_initialize(&archive_path)?; let conn = database::open_or_initialize(&archive_path)?;
let _ = database::ensure_default_user(&conn)?; let _ = database::ensure_default_user(&conn)?;