Gate arr orphan deletion on ctime and stop the nightly permissions pass from restamping it — imports preserve the release's original mtime, so the age gate never actually fired for real content

This commit is contained in:
Gmer4Lfe
2026-07-27 18:38:18 -04:00
parent 3084546b32
commit d680bd0549
5 changed files with 80 additions and 23 deletions
Executable → Regular
+16 -6
View File
@@ -29,7 +29,7 @@
# The filesystem is walked once per run, not twice — classification records which paths are
# eligible for deletion as it goes, and the delete pass (once the size-threshold check below
# passes) just acts on that list instead of re-walking and re-classifying the whole tree.
# That single walk also gets size+mtime straight from find -printf instead of a separate stat
# That single walk also gets size+ctime straight from find -printf instead of a separate stat
# fork per file — find already has to stat() every entry to know it's -type f, so this is
# free by comparison. Measured ~130x faster per file (0.033ms vs 4.3ms).
#
@@ -62,6 +62,9 @@
# Files under LIDARR_ORPHAN_AGE are left alone regardless of tracked status.
# Lidarr's import pipeline writes files before registering them — acting
# immediately would delete files mid-import.
# Age is measured from ctime, not mtime — an import preserves the release's original
# mtime, so a file that landed today can read as years old and skip this gate. Depends
# on media_shares_permissions.sh touching only entries that are actually wrong.
#
# Seven-Gate Safety Model
# Multiple independent sanity checks must all pass before any file is touched.
@@ -455,9 +458,9 @@ NOW=$(date +%s)
TO_DELETE_FILE="$TMP_DIR/to_delete_paths.txt"
> "$TO_DELETE_FILE"
while read -r FILE_SIZE FILE_MTIME filepath; do
while read -r FILE_SIZE FILE_CTIME filepath; do
[[ -z "$filepath" ]] && continue
FILE_MTIME="${FILE_MTIME%%.*}"
FILE_CTIME="${FILE_CTIME%%.*}"
# Tracked — leave alone
if [[ -n "${TRACKED_MAP[$filepath]:-}" ]]; then
@@ -473,7 +476,14 @@ while read -r FILE_SIZE FILE_MTIME filepath; do
fi
if has_extension "$filepath" "${LIDARR_EXTENSIONS[@]}"; then
FILE_AGE=$(( NOW - FILE_MTIME ))
# ctime, not mtime — an import preserves the release's original mtime, so a file
# Lidarr moved in today can read as years old and skip this gate entirely.
# Measured 2026-07-27: 400 of 400 files imported that week had mtimes over 7
# days, one of them 9613 days. ctime is stamped when the file lands on this
# filesystem and cannot be carried in from an archive. This only holds because
# media_shares_permissions.sh applies owner/mode conditionally — a blanket
# chown/chmod restamps every inode nightly and would peg every file at age 0.
FILE_AGE=$(( NOW - FILE_CTIME ))
if [[ "$FILE_AGE" -lt "$AGE_SECONDS" ]] && [[ "$SKIP_AGE_CHECK" != true ]]; then
log "RECENT (skipping): $filepath"
@@ -495,7 +505,7 @@ while read -r FILE_SIZE FILE_MTIME filepath; do
# -printf gets size + mtime directly from find's own stat() during the walk, instead of a
# separate stat fork per file (2026-07-17) — measured ~130x faster per file (0.033ms vs
# 4.3ms), since find already has to stat() every entry anyway to know it's -type f.
done < <(find "$LIDARR_MUSIC_ROOT" -type f -printf '%s %T@ %p\n' 2>/dev/null)
done < <(find "$LIDARR_MUSIC_ROOT" -type f -printf '%s %C@ %p\n' 2>/dev/null)
TOTAL_DELETE_BYTES=$(( ORPHAN_BYTES + JUNK_BYTES ))
TOTAL_REMOVED=$(( ORPHAN_COUNT + JUNK_COUNT ))
@@ -558,4 +568,4 @@ if [[ "$DRY_RUN" == false ]] && [[ -n "${ARR_CLEANUP_STATS:-}" ]]; then
>> "$ARR_CLEANUP_STATS" 2>/dev/null || true
fi
exit 0
exit 0
Executable → Regular
+16 -6
View File
@@ -35,7 +35,7 @@
# once per run, not twice — classification records which paths are eligible for deletion as
# it goes, and the delete pass (once the size-threshold check below passes) just acts on that
# list instead of re-walking and re-classifying the whole tree. That single walk also gets
# size+mtime straight from find -printf instead of a separate stat fork per file — find
# size+ctime straight from find -printf instead of a separate stat fork per file — find
# already has to stat() every entry to know it's -type f, so this is free by comparison.
# Measured ~130x faster per file (0.033ms vs 4.3ms).
#
@@ -72,6 +72,9 @@
# Files under RADARR_ORPHAN_AGE are left alone regardless of tracked status.
# Radarr's import pipeline writes files before registering them — acting
# immediately would delete files mid-import.
# Age is measured from ctime, not mtime — an import preserves the release's original
# mtime, so a file that landed today can read as years old and skip this gate. Depends
# on media_shares_permissions.sh touching only entries that are actually wrong.
#
# Emby Cleanup Is Part of the Job
# Deleting a file without telling Emby leaves ghost entries that show as
@@ -451,9 +454,9 @@ NOW=$(date +%s)
TO_DELETE_FILE="$TMP_DIR/to_delete_paths.txt"
> "$TO_DELETE_FILE"
while read -r FILE_SIZE FILE_MTIME filepath; do
while read -r FILE_SIZE FILE_CTIME filepath; do
[[ -z "$filepath" ]] && continue
FILE_MTIME="${FILE_MTIME%%.*}"
FILE_CTIME="${FILE_CTIME%%.*}"
if [[ -n "${TRACKED_MAP[$filepath]:-}" ]]; then
log "TRACKED: $filepath"
@@ -467,7 +470,14 @@ while read -r FILE_SIZE FILE_MTIME filepath; do
fi
if has_extension "$filepath" "${RADARR_EXTENSIONS[@]}"; then
FILE_AGE=$(( NOW - FILE_MTIME ))
# ctime, not mtime — an import preserves the release's original mtime, so a file
# Radarr moved in today can read as years old and skip this gate entirely.
# Measured 2026-07-27: 400 of 400 files imported that week had mtimes over 7
# days, one of them 9613 days. ctime is stamped when the file lands on this
# filesystem and cannot be carried in from an archive. This only holds because
# media_shares_permissions.sh applies owner/mode conditionally — a blanket
# chown/chmod restamps every inode nightly and would peg every file at age 0.
FILE_AGE=$(( NOW - FILE_CTIME ))
if [[ "$FILE_AGE" -lt "$AGE_SECONDS" ]] && [[ "$SKIP_AGE_CHECK" != true ]]; then
log "RECENT (skipping): $filepath"
@@ -491,7 +501,7 @@ while read -r FILE_SIZE FILE_MTIME filepath; do
# 4.3ms), since find already has to stat() every entry anyway to know it's -type f.
done < <(
for host_path in "${SCAN_ROOTS[@]}"; do
[[ -d "$host_path" ]] && find "$host_path" -type f -printf '%s %T@ %p\n' 2>/dev/null
[[ -d "$host_path" ]] && find "$host_path" -type f -printf '%s %C@ %p\n' 2>/dev/null
done | sort -u
)
@@ -558,4 +568,4 @@ if [[ "$DRY_RUN" == false ]] && [[ -n "${ARR_CLEANUP_STATS:-}" ]]; then
>> "$ARR_CLEANUP_STATS" 2>/dev/null || true
fi
exit 0
exit 0
Executable → Regular
+16 -6
View File
@@ -29,7 +29,7 @@
# per run, not twice — classification records which paths are eligible for deletion as it
# goes, and the delete pass (once the size-threshold check below passes) just acts on that
# list instead of re-walking and re-classifying the whole tree. That single walk also gets
# size+mtime straight from find -printf instead of a separate stat fork per file — find
# size+ctime straight from find -printf instead of a separate stat fork per file — find
# already has to stat() every entry to know it's -type f, so this is free by comparison.
# Measured ~130x faster per file (0.033ms vs 4.3ms).
#
@@ -66,6 +66,9 @@
# Files under SONARR_ORPHAN_AGE are left alone regardless of tracked status.
# Sonarr's import pipeline writes files before registering them — acting
# immediately would delete files mid-import.
# Age is measured from ctime, not mtime — an import preserves the release's original
# mtime, so a file that landed today can read as years old and skip this gate. Depends
# on media_shares_permissions.sh touching only entries that are actually wrong.
#
# Emby Cleanup Is Part of the Job
# Deleting a file without telling Emby leaves ghost entries that show as
@@ -438,9 +441,9 @@ NOW=$(date +%s)
TO_DELETE_FILE="$TMP_DIR/to_delete_paths.txt"
> "$TO_DELETE_FILE"
while read -r FILE_SIZE FILE_MTIME filepath; do
while read -r FILE_SIZE FILE_CTIME filepath; do
[[ -z "$filepath" ]] && continue
FILE_MTIME="${FILE_MTIME%%.*}"
FILE_CTIME="${FILE_CTIME%%.*}"
if [[ -n "${TRACKED_MAP[$filepath]:-}" ]]; then
log "TRACKED: $filepath"
@@ -454,7 +457,14 @@ while read -r FILE_SIZE FILE_MTIME filepath; do
fi
if has_extension "$filepath" "${SONARR_EXTENSIONS[@]}"; then
FILE_AGE=$(( NOW - FILE_MTIME ))
# ctime, not mtime — an import preserves the release's original mtime, so a file
# Sonarr moved in today can read as years old and skip this gate entirely.
# Measured 2026-07-27: 400 of 400 files imported that week had mtimes over 7
# days, one of them 9613 days. ctime is stamped when the file lands on this
# filesystem and cannot be carried in from an archive. This only holds because
# media_shares_permissions.sh applies owner/mode conditionally — a blanket
# chown/chmod restamps every inode nightly and would peg every file at age 0.
FILE_AGE=$(( NOW - FILE_CTIME ))
if [[ "$FILE_AGE" -lt "$AGE_SECONDS" ]] && [[ "$SKIP_AGE_CHECK" != true ]]; then
log "RECENT (skipping): $filepath"
@@ -478,7 +488,7 @@ while read -r FILE_SIZE FILE_MTIME filepath; do
# 4.3ms), since find already has to stat() every entry anyway to know it's -type f.
done < <(
for host_path in "${SCAN_ROOTS[@]}"; do
[[ -d "$host_path" ]] && find "$host_path" -type f -printf '%s %T@ %p\n' 2>/dev/null
[[ -d "$host_path" ]] && find "$host_path" -type f -printf '%s %C@ %p\n' 2>/dev/null
done | sort -u
)
@@ -545,4 +555,4 @@ if [[ "$DRY_RUN" == false ]] && [[ -n "${ARR_CLEANUP_STATS:-}" ]]; then
>> "$ARR_CLEANUP_STATS" 2>/dev/null || true
fi
exit 0
exit 0