feature/insight-jobs #102

Merged
cameron merged 13 commits from feature/insight-jobs into master 2026-06-02 23:41:37 +00:00
Showing only changes of commit 16ae82ba70 - Show all commits
+77 -6
View File
@@ -96,13 +96,10 @@ pub async fn generate_video(
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 rel_path = full_path_str
.strip_prefix(&resolved_root)
.unwrap_or(full_path_str.as_str())
.trim_start_matches(['/', '\\'])
.to_string();
let rel_path = rel_path_for_lookup(&full_path_str, &resolved_root);
// DB lookup first. Cheap and avoids re-reading the file off disk for
// 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())
}
/// 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
/// files matching the `segment_NNN.ts` template that `PlaylistGenerator`
/// writes via `hls_paths::SEGMENT_TEMPLATE`. Anything else (including
@@ -570,6 +584,63 @@ mod tests {
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 {
let claims = Claims::valid_user("1".to_string());
jsonwebtoken::encode(