fix(web): server-side DELETE for failed/discarded uploads (no janitor wait)

This commit is contained in:
Jannis Braun
2026-05-02 18:22:45 +02:00
parent dd91f5f349
commit 4e5a440176
3 changed files with 35 additions and 11 deletions
+8 -3
View File
@@ -542,11 +542,16 @@ export function Message({ message, isCompact, isFirstInGroup, previousMessageId
const transfers = useTransferStore.getState().transfers; const transfers = useTransferStore.getState().transfers;
for (const tid of pending.transferIds) { for (const tid of pending.transferIds) {
const t = transfers.get(tid); const t = transfers.get(tid);
if (t && (t.state === 'active' || t.state === 'paused' || t.state === 'queued')) { if (!t) continue;
// Route anything with potential server-side state (active/paused/
// queued/failed) through abortUpload — it sends DELETE so the
// .tus session doesn't sit on disk for the janitor to sweep.
// 'aborted' already cleaned itself; 'completed' has a finalized
// attachment row that the unlinked-attachment janitor handles (1h grace).
if (t.state !== 'aborted' && t.state !== 'completed') {
useTransferStore.getState().abortUpload(tid); useTransferStore.getState().abortUpload(tid);
} else {
useTransferStore.getState().remove(tid);
} }
useTransferStore.getState().remove(tid);
} }
if (channelKey) { if (channelKey) {
usePendingMessageStore.getState().removeByClientId(channelKey, pending.clientId); usePendingMessageStore.getState().removeByClientId(channelKey, pending.clientId);
@@ -253,15 +253,17 @@ export function MessageInput({ channelId, channelName }: MessageInputProps) {
const t = useTransferStore.getState().transfers.get(transferId); const t = useTransferStore.getState().transfers.get(transferId);
if (t?.state === 'completed') { if (t?.state === 'completed') {
// User is discarding a fully-uploaded attachment. Drop it entirely so // Fully-uploaded attachment with a finalized DB row. Server-side bytes
// no orphan 'aborted'-with-attachmentId record persists. Server-side // get cleaned by the unlinked-attachment janitor (1h grace).
// bytes get cleaned by the storage janitor (per docs/systems/uploads.md). useTransferStore.getState().remove(transferId);
} else if (t && t.state !== 'aborted') {
// active/paused/queued/failed: abortUpload tears down any live tus
// instance AND sends DELETE for orphaned server-side .tus sessions.
// Then drop the transfer + free the retained File reference.
abortUpload(transferId);
useTransferStore.getState().remove(transferId); useTransferStore.getState().remove(transferId);
} else { } else {
// Tear down the live tus instance, then drop the transfer + free the // Already 'aborted' (server already cleaned); just drop the record.
// retained File reference. (abortUpload alone leaves the record in the
// store for retry-after-abort; here the user is fully discarding.)
abortUpload(transferId);
useTransferStore.getState().remove(transferId); useTransferStore.getState().remove(transferId);
} }
removeStaged(channelId, transferId); removeStaged(channelId, transferId);
+18 -1
View File
@@ -281,12 +281,29 @@ export const useTransferStore = create<TransferStore>()(
}, },
abortUpload: (id) => { abortUpload: (id) => {
const t = get().get(id);
const u = liveUploads.get(id); const u = liveUploads.get(id);
if (u) { if (u) {
// tus-js-client v4: abort(true) deletes server-side state via DELETE. // tus-js-client v4: abort(true) deletes server-side state via DELETE.
// We pass true so the user actually frees the slot, not just "pause".
void u.abort(true).catch(() => { /* server may be unreachable; that's OK */ }); void u.abort(true).catch(() => { /* server may be unreachable; that's OK */ });
liveUploads.delete(id); liveUploads.delete(id);
} else if (t?.tusUploadUrl && !t.attachmentId) {
// No live instance, but server-side .tus state still exists (e.g., this
// transfer failed mid-flight or was paused with a stored URL). Send DELETE
// directly so the partial bytes don't sit on disk until the janitor sweeps.
const token = useAuthStore.getState().token;
if (token) {
const fullUrl = t.tusUploadUrl.startsWith('http')
? t.tusUploadUrl
: `${t.origin ?? ''}${t.tusUploadUrl}`;
void fetch(fullUrl, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${token}`,
'Tus-Resumable': '1.0.0',
},
}).catch(() => { /* server unreachable — janitor will eventually clean */ });
}
} }
// Keep liveUploadFiles entry — needed for retry-after-abort. Cleared by remove(). // Keep liveUploadFiles entry — needed for retry-after-abort. Cleared by remove().
get().setState_(id, 'aborted'); get().setState_(id, 'aborted');