Improve upload and download handling on file delete (#176)

This commit is contained in:
Hakan Shehu
2025-08-02 14:49:15 +02:00
committed by GitHub
parent e117770e20
commit 2a9fea972f
5 changed files with 174 additions and 52 deletions

View File

@@ -54,13 +54,6 @@ export class FileDownloadJobHandler implements JobHandler<FileDownloadInput> {
}; };
} }
if (!account.server.isAvailable) {
return {
type: 'retry',
delay: ms('5 seconds'),
};
}
const workspace = account.getWorkspace(input.workspaceId); const workspace = account.getWorkspace(input.workspaceId);
if (!workspace) { if (!workspace) {
return { return {
@@ -77,7 +70,12 @@ export class FileDownloadJobHandler implements JobHandler<FileDownloadInput> {
const file = await this.fetchNode(workspace, download.file_id); const file = await this.fetchNode(workspace, download.file_id);
if (!file) { if (!file) {
await this.deleteDownload(workspace, download.id); await this.updateDownload(workspace, download.id, {
status: DownloadStatus.Failed,
error_code: 'file_deleted',
error_message: 'File has been deleted',
});
return { return {
type: 'cancel', type: 'cancel',
}; };
@@ -90,6 +88,13 @@ export class FileDownloadJobHandler implements JobHandler<FileDownloadInput> {
}; };
} }
if (!account.server.isAvailable) {
return {
type: 'retry',
delay: ms('5 seconds'),
};
}
return this.performDownload(workspace, download, file); return this.performDownload(workspace, download, file);
} }
@@ -230,7 +235,7 @@ export class FileDownloadJobHandler implements JobHandler<FileDownloadInput> {
.selectFrom('nodes') .selectFrom('nodes')
.selectAll() .selectAll()
.where('id', '=', fileId) .where('id', '=', fileId)
.executeTakeFirstOrThrow(); .executeTakeFirst();
if (!node) { if (!node) {
return undefined; return undefined;
@@ -262,14 +267,4 @@ export class FileDownloadJobHandler implements JobHandler<FileDownloadInput> {
download: mapDownload(updatedDownload), download: mapDownload(updatedDownload),
}); });
} }
private async deleteDownload(
workspace: WorkspaceService,
downloadId: string
): Promise<void> {
await workspace.database
.deleteFrom('downloads')
.where('id', '=', downloadId)
.execute();
}
} }

View File

@@ -62,13 +62,6 @@ export class FileUploadJobHandler implements JobHandler<FileUploadInput> {
}; };
} }
if (!account.server.isAvailable) {
return {
type: 'retry',
delay: ms('2 seconds'),
};
}
const workspace = account.getWorkspace(input.workspaceId); const workspace = account.getWorkspace(input.workspaceId);
if (!workspace) { if (!workspace) {
return { return {
@@ -86,6 +79,7 @@ export class FileUploadJobHandler implements JobHandler<FileUploadInput> {
const file = await this.fetchNode(workspace, upload.file_id); const file = await this.fetchNode(workspace, upload.file_id);
if (!file) { if (!file) {
await this.deleteUpload(workspace, upload.file_id); await this.deleteUpload(workspace, upload.file_id);
return { return {
type: 'cancel', type: 'cancel',
}; };
@@ -94,11 +88,19 @@ export class FileUploadJobHandler implements JobHandler<FileUploadInput> {
const localFile = await this.fetchLocalFile(workspace, file.id); const localFile = await this.fetchLocalFile(workspace, file.id);
if (!localFile) { if (!localFile) {
await this.deleteUpload(workspace, upload.file_id); await this.deleteUpload(workspace, upload.file_id);
return { return {
type: 'cancel', type: 'cancel',
}; };
} }
if (!account.server.isAvailable) {
return {
type: 'retry',
delay: ms('2 seconds'),
};
}
if (!isNodeSynced(file)) { if (!isNodeSynced(file)) {
return { return {
type: 'retry', type: 'retry',
@@ -363,7 +365,7 @@ export class FileUploadJobHandler implements JobHandler<FileUploadInput> {
.selectFrom('nodes') .selectFrom('nodes')
.selectAll() .selectAll()
.where('id', '=', fileId) .where('id', '=', fileId)
.executeTakeFirstOrThrow(); .executeTakeFirst();
if (!node) { if (!node) {
return undefined; return undefined;
@@ -380,7 +382,7 @@ export class FileUploadJobHandler implements JobHandler<FileUploadInput> {
.selectFrom('local_files') .selectFrom('local_files')
.selectAll() .selectAll()
.where('id', '=', fileId) .where('id', '=', fileId)
.executeTakeFirstOrThrow(); .executeTakeFirst();
} }
private async updateUpload( private async updateUpload(
@@ -411,9 +413,19 @@ export class FileUploadJobHandler implements JobHandler<FileUploadInput> {
workspace: WorkspaceService, workspace: WorkspaceService,
fileId: string fileId: string
): Promise<void> { ): Promise<void> {
await workspace.database const deletedUpload = await workspace.database
.deleteFrom('uploads') .deleteFrom('uploads')
.where('file_id', '=', fileId) .where('file_id', '=', fileId)
.execute(); .returningAll()
.executeTakeFirst();
if (deletedUpload) {
eventBus.publish({
type: 'upload.deleted',
accountId: workspace.accountId,
workspaceId: workspace.id,
upload: mapUpload(deletedUpload),
});
}
} }
} }

View File

@@ -4,13 +4,19 @@ import {
SelectNodeReference, SelectNodeReference,
} from '@colanode/client/databases/workspace'; } from '@colanode/client/databases/workspace';
import { eventBus } from '@colanode/client/lib/event-bus'; import { eventBus } from '@colanode/client/lib/event-bus';
import { mapNode, mapNodeReference } from '@colanode/client/lib/mappers'; import {
mapDownload,
mapNode,
mapNodeReference,
mapUpload,
} from '@colanode/client/lib/mappers';
import { import {
applyMentionUpdates, applyMentionUpdates,
checkMentionChanges, checkMentionChanges,
} from '@colanode/client/lib/mentions'; } from '@colanode/client/lib/mentions';
import { deleteNodeRelations, fetchNodeTree } from '@colanode/client/lib/utils'; import { deleteNodeRelations, fetchNodeTree } from '@colanode/client/lib/utils';
import { WorkspaceService } from '@colanode/client/services/workspaces/workspace-service'; import { WorkspaceService } from '@colanode/client/services/workspaces/workspace-service';
import { DownloadStatus } from '@colanode/client/types';
import { import {
generateId, generateId,
IdType, IdType,
@@ -488,22 +494,63 @@ export class NodeService {
return { deletedNode, createdMutation }; return { deletedNode, createdMutation };
}); });
if (deletedNode) { if (!deletedNode || !createdMutation) {
debug(`Deleted node ${deletedNode.id} with type ${deletedNode.type}`); return;
eventBus.publish({
type: 'node.deleted',
accountId: this.workspace.accountId,
workspaceId: this.workspace.id,
node: mapNode(deletedNode),
});
} else {
debug(`Failed to delete node ${nodeId}`);
} }
if (createdMutation) { debug(`Deleted node ${deletedNode.id} with type ${deletedNode.type}`);
this.workspace.mutations.scheduleSync();
eventBus.publish({
type: 'node.deleted',
accountId: this.workspace.accountId,
workspaceId: this.workspace.id,
node: mapNode(deletedNode),
});
if (deletedNode.type === 'file') {
const deletedUpload = await this.workspace.database
.deleteFrom('uploads')
.where('file_id', '=', deletedNode.id)
.returningAll()
.executeTakeFirst();
if (deletedUpload) {
eventBus.publish({
type: 'upload.deleted',
accountId: this.workspace.accountId,
workspaceId: this.workspace.id,
upload: mapUpload(deletedUpload),
});
}
const updatedDownloads = await this.workspace.database
.updateTable('downloads')
.set({
status: DownloadStatus.Failed,
error_code: 'file_deleted',
error_message: 'File has been deleted',
})
.where('file_id', '=', deletedNode.id)
.where('status', 'in', [
DownloadStatus.Pending,
DownloadStatus.Downloading,
])
.returningAll()
.execute();
if (updatedDownloads.length > 0) {
for (const updatedDownload of updatedDownloads) {
eventBus.publish({
type: 'download.updated',
accountId: this.workspace.accountId,
workspaceId: this.workspace.id,
download: mapDownload(updatedDownload),
});
}
}
} }
this.workspace.mutations.scheduleSync();
} }
public async syncServerNodeUpdate(update: SyncNodeUpdateData) { public async syncServerNodeUpdate(update: SyncNodeUpdateData) {
@@ -821,7 +868,48 @@ export class NodeService {
await this.workspace.nodeCounters.checkCountersForDeletedNode(deletedNode); await this.workspace.nodeCounters.checkCountersForDeletedNode(deletedNode);
if (deletedNode.type === 'file') { if (deletedNode.type === 'file') {
this.workspace.files.deleteFile(deletedNode); await this.workspace.files.deleteFile(deletedNode);
const deletedUpload = await this.workspace.database
.deleteFrom('uploads')
.where('file_id', '=', deletedNode.id)
.returningAll()
.executeTakeFirst();
if (deletedUpload) {
eventBus.publish({
type: 'upload.deleted',
accountId: this.workspace.accountId,
workspaceId: this.workspace.id,
upload: mapUpload(deletedUpload),
});
}
const updatedDownloads = await this.workspace.database
.updateTable('downloads')
.set({
status: DownloadStatus.Failed,
error_code: 'file_deleted',
error_message: 'File has been deleted',
})
.where('file_id', '=', deletedNode.id)
.where('status', 'in', [
DownloadStatus.Pending,
DownloadStatus.Downloading,
])
.returningAll()
.execute();
if (updatedDownloads.length > 0) {
for (const updatedDownload of updatedDownloads) {
eventBus.publish({
type: 'download.updated',
accountId: this.workspace.accountId,
workspaceId: this.workspace.id,
download: mapDownload(updatedDownload),
});
}
}
} }
eventBus.publish({ eventBus.publish({

View File

@@ -2,6 +2,7 @@ import { Folder } from 'lucide-react';
import { LocalFileNode, Download } from '@colanode/client/types'; import { LocalFileNode, Download } from '@colanode/client/types';
import { formatBytes, timeAgo } from '@colanode/core'; import { formatBytes, timeAgo } from '@colanode/core';
import { FileIcon } from '@colanode/ui/components/files/file-icon';
import { FileThumbnail } from '@colanode/ui/components/files/file-thumbnail'; import { FileThumbnail } from '@colanode/ui/components/files/file-thumbnail';
import { import {
Tooltip, Tooltip,
@@ -30,19 +31,22 @@ export const WorkspaceDownloadFile = ({
nodeId: download.fileId, nodeId: download.fileId,
}); });
const file = fileQuery.data as LocalFileNode; const file = fileQuery.data as LocalFileNode | undefined;
if (!file) {
return null;
}
return ( return (
<div <div
className="border rounded-lg p-4 bg-card hover:bg-accent/50 transition-colors flex items-center gap-6 cursor-pointer" className="border rounded-lg p-4 bg-card hover:bg-accent/50 transition-colors flex items-center gap-6 cursor-pointer"
onClick={() => { onClick={() => {
layout.previewLeft(file.id, true); if (file) {
layout.previewLeft(file.id, true);
}
}} }}
> >
<FileThumbnail file={file} className="size-10 text-muted-foreground" /> {file ? (
<FileThumbnail file={file} className="size-10 text-muted-foreground" />
) : (
<FileIcon mimeType={download.mimeType} className="size-10" />
)}
<div className="flex-grow flex flex-col gap-2 justify-center items-start min-w-0"> <div className="flex-grow flex flex-col gap-2 justify-center items-start min-w-0">
<p className="font-medium text-sm truncate">{download.name}</p> <p className="font-medium text-sm truncate">{download.name}</p>

View File

@@ -1,3 +1,5 @@
import { BadgeAlert } from 'lucide-react';
import { Upload, LocalFileNode } from '@colanode/client/types'; import { Upload, LocalFileNode } from '@colanode/client/types';
import { formatBytes, timeAgo } from '@colanode/core'; import { formatBytes, timeAgo } from '@colanode/core';
import { FileThumbnail } from '@colanode/ui/components/files/file-thumbnail'; import { FileThumbnail } from '@colanode/ui/components/files/file-thumbnail';
@@ -24,7 +26,28 @@ export const WorkspaceUploadFile = ({ upload }: WorkspaceUploadFileProps) => {
const file = fileQuery.data as LocalFileNode; const file = fileQuery.data as LocalFileNode;
if (!file) { if (!file) {
return null; return (
<div className="border rounded-lg p-4 bg-card hover:bg-accent/50 transition-colors flex items-center gap-6 cursor-pointer">
<BadgeAlert className="size-10 text-muted-foreground" />
<div className="flex-grow flex flex-col gap-2 justify-center items-start min-w-0">
<p className="font-medium text-sm truncate w-full">
File not found or has been deleted
</p>
{upload.errorMessage && (
<p className="text-xs text-red-500">{upload.errorMessage}</p>
)}
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<div className="w-10 flex items-center justify-center">
<WorkspaceUploadStatus
status={upload.status}
progress={upload.progress}
/>
</div>
</div>
</div>
);
} }
return ( return (