Deletion on and offline restored.

This commit is contained in:
2026-02-17 00:52:02 -06:00
parent 9ce31a6e09
commit 1242999b35
3 changed files with 186 additions and 2 deletions
+135
View File
@@ -1227,3 +1227,138 @@ pub fn local_media_path_for_item_id(item_id: &str) -> Option<PathBuf> {
let root = downloads_root().ok()?;
walk(&root, item_id)
}
// Delete ALL local copies of an item by scanning *.cj-jftui.json markers recursively.
// Returns how many media files we attempted to remove (count of matched markers).
pub fn delete_local_by_item_id(item_id: &str) -> Result<usize, String> {
let root = downloads_root()?;
let mut deleted = 0usize;
delete_in_dir_by_item_id(&root, item_id, &mut deleted)?;
Ok(deleted)
}
// Delete local content based on a concrete local file path.
// If there is a sibling marker, we read item_id and delete ALL copies (same as online behavior).
// If there is no marker, we delete just that file + sidecars.
pub fn delete_local_by_path(path: &Path) -> Result<usize, String> {
// If there is a sibling marker, prefer item_id-based deletion (removes hardlinks elsewhere too).
if let Some(item_id) = item_id_for_media_path(path)? {
return delete_local_by_item_id(&item_id);
}
// No marker => delete just this file + any obvious sidecars.
delete_file_and_sidecars(path)?;
Ok(1)
}
// --------------------
// Internal helpers
// --------------------
fn delete_in_dir_by_item_id(dir: &Path, item_id: &str, deleted: &mut usize) -> Result<(), String> {
if !dir.exists() {
return Ok(());
}
for ent in fs::read_dir(dir).map_err(|e| format!("read_dir failed: {e}"))? {
let p = ent.map_err(|e| format!("dir entry failed: {e}"))?.path();
if p.is_dir() {
delete_in_dir_by_item_id(&p, item_id, deleted)?;
continue;
}
// marker files only
let name = p.file_name().and_then(|s| s.to_str()).unwrap_or("");
if !name.ends_with(DOWNLOAD_MARKER_EXT) {
continue;
}
let raw = match fs::read_to_string(&p) {
Ok(s) => s,
Err(_) => continue,
};
let v: Value = match serde_json::from_str(raw.trim()) {
Ok(v) => v,
Err(_) => continue,
};
let mid = match v.get("item_id").and_then(|x| x.as_str()) {
Some(s) => s.trim(),
None => continue,
};
if mid != item_id {
continue;
}
// If we matched the item_id, delete:
// - media file referenced by marker (if present)
// - marker itself
// - media sidecars (.aria2/.part)
if let Some(media_name) = v.get("media_file").and_then(|x| x.as_str()) {
let media_path = p
.parent()
.unwrap_or(dir)
.join(media_name);
let _ = delete_file_and_sidecars(&media_path);
}
let _ = fs::remove_file(&p); // remove marker
*deleted += 1;
}
Ok(())
}
// Reads sibling marker "<mediafilename>.cj-jftui.json" and returns item_id if present.
fn item_id_for_media_path(media_path: &Path) -> Result<Option<String>, String> {
let fname = match media_path.file_name().and_then(|s| s.to_str()) {
Some(s) => s,
None => return Ok(None),
};
let marker = media_path
.parent()
.unwrap_or_else(|| Path::new("."))
.join(format!("{fname}.{DOWNLOAD_MARKER_EXT}"));
if !marker.exists() {
return Ok(None);
}
let raw = fs::read_to_string(&marker).map_err(|e| format!("read marker failed: {e}"))?;
let v: Value = serde_json::from_str(raw.trim()).map_err(|e| format!("marker json parse failed: {e}"))?;
Ok(v.get("item_id").and_then(|x| x.as_str()).map(|s| s.to_string()))
}
fn delete_file_and_sidecars(path: &Path) -> Result<(), String> {
// delete main file
let _ = fs::remove_file(path);
// delete sibling marker (if any)
if let Some(fname) = path.file_name().and_then(|s| s.to_str()) {
let marker = path
.parent()
.unwrap_or_else(|| Path::new("."))
.join(format!("{fname}.{DOWNLOAD_MARKER_EXT}"));
let _ = fs::remove_file(marker);
}
// delete obvious aria2 artifacts
if let Some(fname) = path.file_name().and_then(|s| s.to_str()) {
let parent = path.parent().unwrap_or_else(|| Path::new("."));
let aria2 = parent.join(format!("{fname}.aria2"));
let part = parent.join(format!("{fname}.part"));
let _ = fs::remove_file(aria2);
let _ = fs::remove_file(part);
}
Ok(())
}