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);
if (!workspace) {
return {
@@ -77,7 +70,12 @@ export class FileDownloadJobHandler implements JobHandler<FileDownloadInput> {
const file = await this.fetchNode(workspace, download.file_id);
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 {
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);
}
@@ -230,7 +235,7 @@ export class FileDownloadJobHandler implements JobHandler<FileDownloadInput> {
.selectFrom('nodes')
.selectAll()
.where('id', '=', fileId)
.executeTakeFirstOrThrow();
.executeTakeFirst();
if (!node) {
return undefined;
@@ -262,14 +267,4 @@ export class FileDownloadJobHandler implements JobHandler<FileDownloadInput> {
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);
if (!workspace) {
return {
@@ -86,6 +79,7 @@ export class FileUploadJobHandler implements JobHandler<FileUploadInput> {
const file = await this.fetchNode(workspace, upload.file_id);
if (!file) {
await this.deleteUpload(workspace, upload.file_id);
return {
type: 'cancel',
};
@@ -94,11 +88,19 @@ export class FileUploadJobHandler implements JobHandler<FileUploadInput> {
const localFile = await this.fetchLocalFile(workspace, file.id);
if (!localFile) {
await this.deleteUpload(workspace, upload.file_id);
return {
type: 'cancel',
};
}
if (!account.server.isAvailable) {
return {
type: 'retry',
delay: ms('2 seconds'),
};
}
if (!isNodeSynced(file)) {
return {
type: 'retry',
@@ -363,7 +365,7 @@ export class FileUploadJobHandler implements JobHandler<FileUploadInput> {
.selectFrom('nodes')
.selectAll()
.where('id', '=', fileId)
.executeTakeFirstOrThrow();
.executeTakeFirst();
if (!node) {
return undefined;
@@ -380,7 +382,7 @@ export class FileUploadJobHandler implements JobHandler<FileUploadInput> {
.selectFrom('local_files')
.selectAll()
.where('id', '=', fileId)
.executeTakeFirstOrThrow();
.executeTakeFirst();
}
private async updateUpload(
@@ -411,9 +413,19 @@ export class FileUploadJobHandler implements JobHandler<FileUploadInput> {
workspace: WorkspaceService,
fileId: string
): Promise<void> {
await workspace.database
const deletedUpload = await workspace.database
.deleteFrom('uploads')
.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,
} from '@colanode/client/databases/workspace';
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 {
applyMentionUpdates,
checkMentionChanges,
} from '@colanode/client/lib/mentions';
import { deleteNodeRelations, fetchNodeTree } from '@colanode/client/lib/utils';
import { WorkspaceService } from '@colanode/client/services/workspaces/workspace-service';
import { DownloadStatus } from '@colanode/client/types';
import {
generateId,
IdType,
@@ -488,22 +494,63 @@ export class NodeService {
return { deletedNode, createdMutation };
});
if (deletedNode) {
debug(`Deleted node ${deletedNode.id} with type ${deletedNode.type}`);
eventBus.publish({
type: 'node.deleted',
accountId: this.workspace.accountId,
workspaceId: this.workspace.id,
node: mapNode(deletedNode),
});
} else {
debug(`Failed to delete node ${nodeId}`);
if (!deletedNode || !createdMutation) {
return;
}
if (createdMutation) {
this.workspace.mutations.scheduleSync();
debug(`Deleted node ${deletedNode.id} with type ${deletedNode.type}`);
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) {
@@ -821,7 +868,48 @@ export class NodeService {
await this.workspace.nodeCounters.checkCountersForDeletedNode(deletedNode);
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({

View File

@@ -2,6 +2,7 @@ import { Folder } from 'lucide-react';
import { LocalFileNode, Download } from '@colanode/client/types';
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 {
Tooltip,
@@ -30,19 +31,22 @@ export const WorkspaceDownloadFile = ({
nodeId: download.fileId,
});
const file = fileQuery.data as LocalFileNode;
if (!file) {
return null;
}
const file = fileQuery.data as LocalFileNode | undefined;
return (
<div
className="border rounded-lg p-4 bg-card hover:bg-accent/50 transition-colors flex items-center gap-6 cursor-pointer"
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">
<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 { formatBytes, timeAgo } from '@colanode/core';
import { FileThumbnail } from '@colanode/ui/components/files/file-thumbnail';
@@ -24,7 +26,28 @@ export const WorkspaceUploadFile = ({ upload }: WorkspaceUploadFileProps) => {
const file = fileQuery.data as LocalFileNode;
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 (