Normalize video rel_path lookup to forward slashes on Windows
generate_video built the rel_path for its image_exif lookup by stripping
the library root from the absolute path, leaving backslashes on Windows
(Melissa\clip.mp4). file_scan stores rel_paths forward-slash and
get_exif_batch matches exactly with no normalization, so the lookup
missed and the handler re-hashed the entire video file on every request.
Extract rel_path_for_lookup and normalize separators with replace('\\',
'/'). Adds tests for Windows/Unix separators, file-at-root, leading
separator stripping, and the no-match fallback.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+77
-6
@@ -96,13 +96,10 @@ pub async fn generate_video(
|
|||||||
return HttpResponse::BadRequest().finish();
|
return HttpResponse::BadRequest().finish();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build the rel_path used to look up the row.
|
// Build the rel_path used to look up the row. Forward-slash normalized
|
||||||
|
// so the lookup matches DB rows on Windows — see `rel_path_for_lookup`.
|
||||||
let full_path_str = full_path.to_string_lossy().to_string();
|
let full_path_str = full_path.to_string_lossy().to_string();
|
||||||
let rel_path = full_path_str
|
let rel_path = rel_path_for_lookup(&full_path_str, &resolved_root);
|
||||||
.strip_prefix(&resolved_root)
|
|
||||||
.unwrap_or(full_path_str.as_str())
|
|
||||||
.trim_start_matches(['/', '\\'])
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
// DB lookup first. Cheap and avoids re-reading the file off disk for
|
// DB lookup first. Cheap and avoids re-reading the file off disk for
|
||||||
// already-ingested videos.
|
// already-ingested videos.
|
||||||
@@ -284,6 +281,23 @@ fn is_valid_hash(s: &str) -> bool {
|
|||||||
s.len() == 64 && s.bytes().all(|b| b.is_ascii_hexdigit())
|
s.len() == 64 && s.bytes().all(|b| b.is_ascii_hexdigit())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Compute the forward-slash `rel_path` used to look up a video's
|
||||||
|
/// `image_exif` row, from its absolute path string and the library root.
|
||||||
|
///
|
||||||
|
/// Normalizing to forward slashes is essential on Windows: `file_scan`
|
||||||
|
/// stores rel_paths forward-slash regardless of OS, but a raw strip of a
|
||||||
|
/// backslash Windows path (`Z:\...\pic\Melissa\clip.mp4`) yields
|
||||||
|
/// `Melissa\clip.mp4`. `get_exif_batch` does an exact match with no
|
||||||
|
/// normalization, so the backslash form misses and the handler falls back
|
||||||
|
/// to re-hashing the entire file on every request.
|
||||||
|
fn rel_path_for_lookup(full_path_str: &str, resolved_root: &str) -> String {
|
||||||
|
full_path_str
|
||||||
|
.strip_prefix(resolved_root)
|
||||||
|
.unwrap_or(full_path_str)
|
||||||
|
.trim_start_matches(['/', '\\'])
|
||||||
|
.replace('\\', "/")
|
||||||
|
}
|
||||||
|
|
||||||
/// Allowed file names inside a hash dir. `playlist.m3u8` plus segment
|
/// Allowed file names inside a hash dir. `playlist.m3u8` plus segment
|
||||||
/// files matching the `segment_NNN.ts` template that `PlaylistGenerator`
|
/// files matching the `segment_NNN.ts` template that `PlaylistGenerator`
|
||||||
/// writes via `hls_paths::SEGMENT_TEMPLATE`. Anything else (including
|
/// writes via `hls_paths::SEGMENT_TEMPLATE`. Anything else (including
|
||||||
@@ -570,6 +584,63 @@ mod tests {
|
|||||||
assert!(!is_allowed_hls_filename(""));
|
assert!(!is_allowed_hls_filename(""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rel_path_for_lookup_normalizes_windows_separators() {
|
||||||
|
// Windows: backslash root + backslash full path. The stored row is
|
||||||
|
// forward-slash (`Melissa/clip.mp4`), so without normalization the
|
||||||
|
// lookup misses and the handler re-hashes the whole file.
|
||||||
|
assert_eq!(
|
||||||
|
rel_path_for_lookup(r"Z:\Media\pic\Melissa\clip.mp4", r"Z:\Media\pic"),
|
||||||
|
"Melissa/clip.mp4"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rel_path_for_lookup_handles_unix_separators() {
|
||||||
|
assert_eq!(
|
||||||
|
rel_path_for_lookup("/media/pic/Melissa/clip.mp4", "/media/pic"),
|
||||||
|
"Melissa/clip.mp4"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rel_path_for_lookup_file_at_root_has_no_separator() {
|
||||||
|
// A file directly in the library root has no internal separator, so
|
||||||
|
// the bug never manifested here — guard against a regression anyway.
|
||||||
|
assert_eq!(
|
||||||
|
rel_path_for_lookup(r"Z:\Media\pic\clip.mp4", r"Z:\Media\pic"),
|
||||||
|
"clip.mp4"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
rel_path_for_lookup("/media/pic/clip.mp4", "/media/pic"),
|
||||||
|
"clip.mp4"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rel_path_for_lookup_strips_leading_separators() {
|
||||||
|
// Both separator styles are trimmed from the front after the root
|
||||||
|
// is stripped, regardless of which form the join produced.
|
||||||
|
assert_eq!(
|
||||||
|
rel_path_for_lookup(r"Z:\Media\pic\sub\a.mp4", r"Z:\Media\pic"),
|
||||||
|
"sub/a.mp4"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
rel_path_for_lookup("/media/pic//sub/a.mp4", "/media/pic"),
|
||||||
|
"sub/a.mp4"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rel_path_for_lookup_falls_back_when_root_does_not_match() {
|
||||||
|
// If the root doesn't prefix the path (e.g. a stale mount), we keep
|
||||||
|
// the whole path but still normalize separators rather than panic.
|
||||||
|
assert_eq!(
|
||||||
|
rel_path_for_lookup(r"D:\other\Melissa\clip.mp4", r"Z:\Media\pic"),
|
||||||
|
"D:/other/Melissa/clip.mp4"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn make_token() -> String {
|
fn make_token() -> String {
|
||||||
let claims = Claims::valid_user("1".to_string());
|
let claims = Claims::valid_user("1".to_string());
|
||||||
jsonwebtoken::encode(
|
jsonwebtoken::encode(
|
||||||
|
|||||||
Reference in New Issue
Block a user