mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-09-24 18:26:51 +03:00
show disk space reclaimed by media sync (#24189)
This commit is contained in:
committed by
Nicolas Mowen
parent
d6b93301e2
commit
6d33b31bc6
@@ -499,7 +499,7 @@ Media files (event snapshots, event thumbnails, review thumbnails, previews, exp
|
||||
|
||||
Normal operation may leave small numbers of orphaned files until Frigate's scheduled cleanup, but crashes, configuration changes, or upgrades may cause more orphaned files that Frigate does not clean up. This feature checks the file system for media files and removes any that are not referenced in the database.
|
||||
|
||||
The Maintenance pane in the Frigate UI or an API endpoint `POST /api/media/sync` can be used to trigger a media sync. When using the API, a job ID is returned and the operation continues on the server. Status can be checked with the `/api/media/sync/status/{job_id}` endpoint.
|
||||
The Maintenance pane in the Frigate UI or an API endpoint `POST /api/media/sync` can be used to trigger a media sync. When using the API, a job ID is returned and the operation continues on the server. Status can be checked with the `/api/media/sync/status/{job_id}` endpoint. Results include the disk space reclaimed, or with `dry_run: true`, the space that would be reclaimed.
|
||||
|
||||
Setting `verbose: true` writes a detailed report of every orphaned file and database entry to `/config/media_sync/<job_id>.txt`. For recordings, the report separates orphaned database entries (DB records whose files are missing from disk) from orphaned files (files on disk with no corresponding database record).
|
||||
|
||||
|
||||
+65
-7
@@ -41,6 +41,14 @@ FFPROBE_PATH = (
|
||||
)
|
||||
|
||||
|
||||
def _file_size(path: str) -> int:
|
||||
"""Return the size of a file in bytes, or 0 if it cannot be read."""
|
||||
try:
|
||||
return os.path.getsize(path)
|
||||
except OSError:
|
||||
return 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class SyncResult:
|
||||
"""Result of a sync operation."""
|
||||
@@ -49,6 +57,7 @@ class SyncResult:
|
||||
files_checked: int = 0
|
||||
orphans_found: int = 0
|
||||
orphans_deleted: int = 0
|
||||
bytes_reclaimed: int = 0
|
||||
orphan_paths: list[str] = field(default_factory=list)
|
||||
orphan_db_paths: list[str] = field(default_factory=list)
|
||||
aborted: bool = False
|
||||
@@ -60,6 +69,7 @@ class SyncResult:
|
||||
"files_checked": self.files_checked,
|
||||
"orphans_found": self.orphans_found,
|
||||
"orphans_deleted": self.orphans_deleted,
|
||||
"bytes_reclaimed": self.bytes_reclaimed,
|
||||
"aborted": self.aborted,
|
||||
"error": self.error,
|
||||
}
|
||||
@@ -235,6 +245,7 @@ def sync_recordings(
|
||||
return result
|
||||
|
||||
if dry_run:
|
||||
result.bytes_reclaimed = sum(_file_size(f) for f in files_to_delete)
|
||||
logger.info(
|
||||
f"Recordings sync (dry run): Found {len(files_to_delete)} orphaned files"
|
||||
)
|
||||
@@ -243,11 +254,15 @@ def sync_recordings(
|
||||
# Delete orphans
|
||||
logger.info(f"Deleting {len(files_to_delete)} orphaned recordings files")
|
||||
for file in files_to_delete:
|
||||
size = _file_size(file)
|
||||
try:
|
||||
os.unlink(file)
|
||||
result.orphans_deleted += 1
|
||||
except OSError as e:
|
||||
logger.error(f"Failed to delete {file}: {e}")
|
||||
continue
|
||||
|
||||
result.orphans_deleted += 1
|
||||
result.bytes_reclaimed += size
|
||||
|
||||
logger.debug("End sync recordings.")
|
||||
|
||||
@@ -325,6 +340,7 @@ def sync_event_snapshots(dry_run: bool = False, force: bool = False) -> SyncResu
|
||||
return result
|
||||
|
||||
if dry_run:
|
||||
result.bytes_reclaimed = sum(_file_size(p) for p in orphans)
|
||||
logger.info(
|
||||
f"Event snapshots sync (dry run): Found {len(orphans)} orphaned files"
|
||||
)
|
||||
@@ -333,11 +349,15 @@ def sync_event_snapshots(dry_run: bool = False, force: bool = False) -> SyncResu
|
||||
# Delete orphans
|
||||
logger.info(f"Deleting {len(orphans)} orphaned event snapshot files")
|
||||
for file_path in orphans:
|
||||
size = _file_size(file_path)
|
||||
try:
|
||||
os.unlink(file_path)
|
||||
result.orphans_deleted += 1
|
||||
except OSError as e:
|
||||
logger.error(f"Failed to delete {file_path}: {e}")
|
||||
continue
|
||||
|
||||
result.orphans_deleted += 1
|
||||
result.bytes_reclaimed += size
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error syncing event snapshots: {e}")
|
||||
@@ -421,6 +441,7 @@ def sync_event_thumbnails(dry_run: bool = False, force: bool = False) -> SyncRes
|
||||
return result
|
||||
|
||||
if dry_run:
|
||||
result.bytes_reclaimed = sum(_file_size(p) for p in orphans)
|
||||
logger.info(
|
||||
f"Event thumbnails sync (dry run): Found {len(orphans)} orphaned files"
|
||||
)
|
||||
@@ -429,11 +450,15 @@ def sync_event_thumbnails(dry_run: bool = False, force: bool = False) -> SyncRes
|
||||
# Delete orphans
|
||||
logger.info(f"Deleting {len(orphans)} orphaned event thumbnail files")
|
||||
for file_path in orphans:
|
||||
size = _file_size(file_path)
|
||||
try:
|
||||
os.unlink(file_path)
|
||||
result.orphans_deleted += 1
|
||||
except OSError as e:
|
||||
logger.error(f"Failed to delete {file_path}: {e}")
|
||||
continue
|
||||
|
||||
result.orphans_deleted += 1
|
||||
result.bytes_reclaimed += size
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error syncing event thumbnails: {e}")
|
||||
@@ -501,6 +526,7 @@ def sync_review_thumbnails(dry_run: bool = False, force: bool = False) -> SyncRe
|
||||
return result
|
||||
|
||||
if dry_run:
|
||||
result.bytes_reclaimed = sum(_file_size(p) for p in orphans)
|
||||
logger.info(
|
||||
f"Review thumbnails sync (dry run): Found {len(orphans)} orphaned files"
|
||||
)
|
||||
@@ -509,11 +535,15 @@ def sync_review_thumbnails(dry_run: bool = False, force: bool = False) -> SyncRe
|
||||
# Delete orphans
|
||||
logger.info(f"Deleting {len(orphans)} orphaned review thumbnail files")
|
||||
for file_path in orphans:
|
||||
size = _file_size(file_path)
|
||||
try:
|
||||
os.unlink(file_path)
|
||||
result.orphans_deleted += 1
|
||||
except OSError as e:
|
||||
logger.error(f"Failed to delete {file_path}: {e}")
|
||||
continue
|
||||
|
||||
result.orphans_deleted += 1
|
||||
result.bytes_reclaimed += size
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error syncing review thumbnails: {e}")
|
||||
@@ -581,17 +611,22 @@ def sync_previews(dry_run: bool = False, force: bool = False) -> SyncResult:
|
||||
return result
|
||||
|
||||
if dry_run:
|
||||
result.bytes_reclaimed = sum(_file_size(p) for p in orphans)
|
||||
logger.info(f"Previews sync (dry run): Found {len(orphans)} orphaned files")
|
||||
return result
|
||||
|
||||
# Delete orphans
|
||||
logger.info(f"Deleting {len(orphans)} orphaned preview files")
|
||||
for file_path in orphans:
|
||||
size = _file_size(file_path)
|
||||
try:
|
||||
os.unlink(file_path)
|
||||
result.orphans_deleted += 1
|
||||
except OSError as e:
|
||||
logger.error(f"Failed to delete {file_path}: {e}")
|
||||
continue
|
||||
|
||||
result.orphans_deleted += 1
|
||||
result.bytes_reclaimed += size
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error syncing previews: {e}")
|
||||
@@ -673,17 +708,22 @@ def sync_exports(dry_run: bool = False, force: bool = False) -> SyncResult:
|
||||
return result
|
||||
|
||||
if dry_run:
|
||||
result.bytes_reclaimed = sum(_file_size(p) for p in orphans)
|
||||
logger.info(f"Exports sync (dry run): Found {len(orphans)} orphaned files")
|
||||
return result
|
||||
|
||||
# Delete orphans
|
||||
logger.info(f"Deleting {len(orphans)} orphaned export files")
|
||||
for file_path in orphans:
|
||||
size = _file_size(file_path)
|
||||
try:
|
||||
os.unlink(file_path)
|
||||
result.orphans_deleted += 1
|
||||
except OSError as e:
|
||||
logger.error(f"Failed to delete {file_path}: {e}")
|
||||
continue
|
||||
|
||||
result.orphans_deleted += 1
|
||||
result.bytes_reclaimed += size
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error syncing exports: {e}")
|
||||
@@ -733,6 +773,21 @@ class MediaSyncResults:
|
||||
total += result.orphans_found
|
||||
return total
|
||||
|
||||
@property
|
||||
def total_bytes_reclaimed(self) -> int:
|
||||
total = 0
|
||||
for result in [
|
||||
self.event_snapshots,
|
||||
self.event_thumbnails,
|
||||
self.review_thumbnails,
|
||||
self.previews,
|
||||
self.exports,
|
||||
self.recordings,
|
||||
]:
|
||||
if result:
|
||||
total += result.bytes_reclaimed
|
||||
return total
|
||||
|
||||
@property
|
||||
def total_orphans_deleted(self) -> int:
|
||||
total = 0
|
||||
@@ -764,6 +819,7 @@ class MediaSyncResults:
|
||||
"files_checked": result.files_checked,
|
||||
"orphans_found": result.orphans_found,
|
||||
"orphans_deleted": result.orphans_deleted,
|
||||
"bytes_reclaimed": result.bytes_reclaimed,
|
||||
"aborted": result.aborted,
|
||||
"error": result.error,
|
||||
}
|
||||
@@ -771,6 +827,7 @@ class MediaSyncResults:
|
||||
"files_checked": self.total_files_checked,
|
||||
"orphans_found": self.total_orphans_found,
|
||||
"orphans_deleted": self.total_orphans_deleted,
|
||||
"bytes_reclaimed": self.total_bytes_reclaimed,
|
||||
}
|
||||
return results
|
||||
|
||||
@@ -874,7 +931,8 @@ def sync_all_media(
|
||||
logger.info(
|
||||
f"Media sync complete: checked {results.total_files_checked} files, "
|
||||
f"found {results.total_orphans_found} orphans, "
|
||||
f"deleted {results.total_orphans_deleted}"
|
||||
f"deleted {results.total_orphans_deleted}, "
|
||||
f"reclaimed {results.total_bytes_reclaimed} bytes"
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
@@ -1469,6 +1469,8 @@
|
||||
"filesChecked": "Files Checked",
|
||||
"orphansFound": "Orphans Found",
|
||||
"orphansDeleted": "Orphans Deleted",
|
||||
"spaceToReclaim": "Space To Reclaim",
|
||||
"spaceReclaimed": "Space Reclaimed",
|
||||
"aborted": "Aborted. Deletion would exceed safety threshold.",
|
||||
"error": "Error",
|
||||
"totals": "Totals"
|
||||
|
||||
@@ -131,6 +131,7 @@ export type MediaSyncStats = {
|
||||
files_checked: number;
|
||||
orphans_found: number;
|
||||
orphans_deleted: number;
|
||||
bytes_reclaimed: number;
|
||||
aborted: boolean;
|
||||
error: string | null;
|
||||
};
|
||||
@@ -139,6 +140,7 @@ export type MediaSyncTotals = {
|
||||
files_checked: number;
|
||||
orphans_found: number;
|
||||
orphans_deleted: number;
|
||||
bytes_reclaimed: number;
|
||||
};
|
||||
|
||||
export type MediaSyncResults = {
|
||||
@@ -154,4 +156,5 @@ export type Job<TResults = unknown> = {
|
||||
start_time?: number;
|
||||
end_time?: number;
|
||||
error_message?: string;
|
||||
dry_run?: boolean;
|
||||
};
|
||||
|
||||
@@ -5,3 +5,11 @@ export const getUnitSize = (MB: number) => {
|
||||
|
||||
return `${(MB / 1048576).toFixed(2)} TiB`;
|
||||
};
|
||||
|
||||
export const getUnitSizeFromBytes = (bytes: number) => {
|
||||
if (bytes === null || isNaN(bytes) || bytes < 0) return "Invalid number";
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1048576) return `${(bytes / 1024).toFixed(2)} KiB`;
|
||||
|
||||
return getUnitSize(bytes / 1048576);
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@ import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
|
||||
import { MediaSyncResults, MediaSyncStats } from "@/types/ws";
|
||||
import { useDocDomain } from "@/hooks/use-doc-domain";
|
||||
import { Link } from "react-router-dom";
|
||||
import { getUnitSizeFromBytes } from "@/utils/storageUtil";
|
||||
|
||||
export default function MediaSyncSettingsView() {
|
||||
const { t } = useTranslation("views/settings");
|
||||
@@ -376,6 +377,22 @@ export default function MediaSyncSettingsView() {
|
||||
{mediaStats.orphans_deleted}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">
|
||||
{currentJob?.dry_run
|
||||
? t(
|
||||
"maintenance.sync.resultsFields.spaceToReclaim",
|
||||
)
|
||||
: t(
|
||||
"maintenance.sync.resultsFields.spaceReclaimed",
|
||||
)}
|
||||
</span>
|
||||
<span>
|
||||
{getUnitSizeFromBytes(
|
||||
mediaStats.bytes_reclaimed,
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{mediaStats.aborted && (
|
||||
<div className="flex items-center gap-2 text-destructive">
|
||||
<LuX className="size-4" />
|
||||
@@ -449,6 +466,22 @@ export default function MediaSyncSettingsView() {
|
||||
{mediaSyncResults.totals.orphans_deleted}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">
|
||||
{currentJob?.dry_run
|
||||
? t(
|
||||
"maintenance.sync.resultsFields.spaceToReclaim",
|
||||
)
|
||||
: t(
|
||||
"maintenance.sync.resultsFields.spaceReclaimed",
|
||||
)}
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
{getUnitSizeFromBytes(
|
||||
mediaSyncResults.totals.bytes_reclaimed,
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user