feat(jobs): purge file binaries when a job is delivered, keep metadata
Active scan + image traffic was going to bloat Storage fast — every
delivered case has tens of MB of STL hanging around forever. Now closing
a case via 'Teslim Aldım' fires a background archive sweep that deletes
the binary from the bucket but keeps the job_files row, so audit
('kim, ne, ne zaman yükledi') is preserved.
- DB: job_files.archivedAt datetime (nullable).
- archiveJobFiles(jobId) (lib/appwrite/job-file-archive.ts):
lists rows, storage.deleteFile each, stamps archivedAt on the row.
All in try/catch so partial Storage failures don't roll back the
'delivered' transition.
- markDeliveredAction fires it as 'void archiveJobFiles(jobId)' — same
fire-and-forget pattern as audit/notifications/finance sync.
UI / API
- Job detail file row dims to 60% opacity, shows 'Arşivlendi
{tarih}' inline, and disables both the download dialog trigger and
the STL viewer button.
- /api/jobs/[jobId]/files/[fileId]/download returns 410 Gone with a
Turkish message when archivedAt is set — direct-URL hot links can't
fish the file back either.
This commit is contained in:
@@ -254,7 +254,8 @@ function FileRow({ file }: { file: JobFileWithUrl }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [downloadOpen, setDownloadOpen] = useState(false);
|
||||
const [viewerOpen, setViewerOpen] = useState(false);
|
||||
const isViewable = VIEWABLE_RE.test(file.name);
|
||||
const isArchived = Boolean(file.archivedAt);
|
||||
const isViewable = !isArchived && VIEWABLE_RE.test(file.name);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.ok) {
|
||||
@@ -281,12 +282,20 @@ function FileRow({ file }: { file: JobFileWithUrl }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<li className="flex items-center gap-3 px-3 py-2">
|
||||
<li className={`flex items-center gap-3 px-3 py-2 ${isArchived ? "opacity-60" : ""}`}>
|
||||
<span className="text-muted-foreground">{kindIcon(file.kind)}</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="truncate text-sm font-medium">{file.name}</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{JOB_FILE_KIND_LABELS[file.kind] ?? file.kind} · {formatSize(file.size)}
|
||||
{isArchived && (
|
||||
<>
|
||||
{" · "}
|
||||
<span className="text-amber-600 dark:text-amber-400">
|
||||
Arşivlendi {new Date(file.archivedAt!).toLocaleDateString("tr-TR")}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant="outline" className="hidden sm:inline-flex">
|
||||
@@ -315,7 +324,13 @@ function FileRow({ file }: { file: JobFileWithUrl }) {
|
||||
</Dialog>
|
||||
)}
|
||||
<Dialog open={downloadOpen} onOpenChange={setDownloadOpen}>
|
||||
<Button size="sm" variant="outline" onClick={() => setDownloadOpen(true)}>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setDownloadOpen(true)}
|
||||
disabled={isArchived}
|
||||
title={isArchived ? "Bu dosya arşivlendi; indirilebilir kopyası yok." : undefined}
|
||||
>
|
||||
<Download className="size-4" />
|
||||
</Button>
|
||||
<DialogContent>
|
||||
|
||||
Reference in New Issue
Block a user