fix(api,web): movie delete always removes files
ci / web (push) Successful in 1m5s
ci / rust (push) Successful in 1m30s
e2e / e2e (push) Successful in 1m27s

Drop the delete_files choice: removing a movie now always unlinks
its §7.4 library folder. Removes the query flag from DELETE
/api/movies/{id} and the OpenAPI params, and the toggle from the UI
confirmation panel — the confirmation itself stays. Path-safety and
torrent-untouched behaviour are unchanged.

Closes #110
This commit is contained in:
Miguel Palhas
2026-08-23 10:21:03 +01:00
parent 54b3ea8a4d
commit d6345c5305
5 changed files with 26 additions and 145 deletions
+1 -2
View File
@@ -23,8 +23,7 @@ use utoipa_scalar::{Scalar, Servable};
pub use health::{Check, Health, HealthReport, Status};
pub use movies::{
Accepted, AttentionQueues, CreateMovie, DeleteMovieQuery, ErrorBody, Movie, MovieFile, Release,
UpdateMovie,
Accepted, AttentionQueues, CreateMovie, ErrorBody, Movie, MovieFile, Release, UpdateMovie,
};
pub use owners::{CreateOwner, Owner, UpdateOwner};
pub use roots::Root;
+5 -43
View File
@@ -343,18 +343,9 @@ pub async fn update(
Ok(Json(load_movie(&state, id).await?))
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct DeleteMovieQuery {
/// Also remove the title's folder under its root (§7.4). Off unless the
/// caller says otherwise: dropping the row is reversible, unlinking
/// 40 GB is not.
#[serde(default)]
pub delete_files: bool,
}
#[utoipa::path(
delete, path = "/api/movies/{movie_id}", tag = "movies",
params(("movie_id" = i64, Path, description = "Movie row id"), DeleteMovieQuery),
params(("movie_id" = i64, Path, description = "Movie row id")),
responses(
(status = 204),
(status = 404, body = ErrorBody),
@@ -366,14 +357,11 @@ pub struct DeleteMovieQuery {
pub async fn delete(
State(state): State<AppState>,
Path(id): Path<i64>,
Query(query): Query<DeleteMovieQuery>,
) -> Result<StatusCode, ApiError> {
// The row is loaded first so a missing movie is 404 before anything
// touches the disk.
load_movie(&state, id).await?;
if query.delete_files {
remove_library_files(&state, id).await?;
}
remove_library_files(&state, id).await?;
// `media_files.path` is UNIQUE and the owner is polymorphic, so nothing
// cascades: leaving the rows behind would block re-importing the same
// path after a re-add. Owner tags go with the title they tagged.
@@ -838,7 +826,7 @@ mod tests {
/// The §7.4 folder is the unit of deletion, so the sidecar goes with the
/// feature — and the root itself is never touched.
#[tokio::test]
async fn delete_files_removes_the_whole_title_folder() {
async fn deleting_a_movie_removes_the_whole_title_folder() {
let (_dir, state, base) = application().await;
let movie = add_movie(&base, 693_134, 1).await;
let id = movie["id"].as_i64().expect("id");
@@ -846,7 +834,7 @@ mod tests {
let folder = library_on_disk(&state, id, root.path()).await;
let response = reqwest::Client::new()
.delete(format!("{base}/api/movies/{id}?delete_files=true"))
.delete(format!("{base}/api/movies/{id}"))
.send()
.await
.expect("delete");
@@ -867,32 +855,6 @@ mod tests {
assert_eq!(orphans, 0, "the file rows go with the files");
}
/// Default off (§7.4 in the issue): removing a title from the library is
/// not the same decision as unlinking 40 GB.
#[tokio::test]
async fn delete_without_the_flag_leaves_the_files_on_disk() {
let (_dir, state, base) = application().await;
let movie = add_movie(&base, 693_134, 1).await;
let id = movie["id"].as_i64().expect("id");
let root = tempfile::tempdir().expect("root");
let folder = library_on_disk(&state, id, root.path()).await;
let response = reqwest::Client::new()
.delete(format!("{base}/api/movies/{id}"))
.send()
.await
.expect("delete");
assert_eq!(response.status(), StatusCode::NO_CONTENT);
assert!(folder.exists(), "the files stay until asked for");
assert_eq!(
reqwest::get(format!("{base}/api/movies/{id}"))
.await
.expect("get deleted")
.status(),
StatusCode::NOT_FOUND
);
}
/// The guard that keeps a delete inside the library: a path that is not
/// under the title's root is left alone, whatever the row says.
#[tokio::test]
@@ -921,7 +883,7 @@ mod tests {
.expect("media file");
let response = reqwest::Client::new()
.delete(format!("{base}/api/movies/{id}?delete_files=true"))
.delete(format!("{base}/api/movies/{id}"))
.send()
.await
.expect("delete");