diff --git a/packages/client/src/jobs/file-download.ts b/packages/client/src/jobs/file-download.ts index 16876a9b..1568910b 100644 --- a/packages/client/src/jobs/file-download.ts +++ b/packages/client/src/jobs/file-download.ts @@ -54,13 +54,6 @@ export class FileDownloadJobHandler implements JobHandler { }; } - 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 { 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 { }; } + 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 { .selectFrom('nodes') .selectAll() .where('id', '=', fileId) - .executeTakeFirstOrThrow(); + .executeTakeFirst(); if (!node) { return undefined; @@ -262,14 +267,4 @@ export class FileDownloadJobHandler implements JobHandler { download: mapDownload(updatedDownload), }); } - - private async deleteDownload( - workspace: WorkspaceService, - downloadId: string - ): Promise { - await workspace.database - .deleteFrom('downloads') - .where('id', '=', downloadId) - .execute(); - } } diff --git a/packages/client/src/jobs/file-upload.ts b/packages/client/src/jobs/file-upload.ts index 8f8a14a2..f7b307a5 100644 --- a/packages/client/src/jobs/file-upload.ts +++ b/packages/client/src/jobs/file-upload.ts @@ -62,13 +62,6 @@ export class FileUploadJobHandler implements JobHandler { }; } - 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 { 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 { 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 { .selectFrom('nodes') .selectAll() .where('id', '=', fileId) - .executeTakeFirstOrThrow(); + .executeTakeFirst(); if (!node) { return undefined; @@ -380,7 +382,7 @@ export class FileUploadJobHandler implements JobHandler { .selectFrom('local_files') .selectAll() .where('id', '=', fileId) - .executeTakeFirstOrThrow(); + .executeTakeFirst(); } private async updateUpload( @@ -411,9 +413,19 @@ export class FileUploadJobHandler implements JobHandler { workspace: WorkspaceService, fileId: string ): Promise { - 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), + }); + } } } diff --git a/packages/client/src/services/workspaces/node-service.ts b/packages/client/src/services/workspaces/node-service.ts index 2ac96ab3..adf46ab1 100644 --- a/packages/client/src/services/workspaces/node-service.ts +++ b/packages/client/src/services/workspaces/node-service.ts @@ -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({ diff --git a/packages/ui/src/components/workspaces/downloads/workspace-download-file.tsx b/packages/ui/src/components/workspaces/downloads/workspace-download-file.tsx index 5c23e099..4eac9b1b 100644 --- a/packages/ui/src/components/workspaces/downloads/workspace-download-file.tsx +++ b/packages/ui/src/components/workspaces/downloads/workspace-download-file.tsx @@ -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 (
{ - layout.previewLeft(file.id, true); + if (file) { + layout.previewLeft(file.id, true); + } }} > - + {file ? ( + + ) : ( + + )}

{download.name}

diff --git a/packages/ui/src/components/workspaces/uploads/workspace-upload-file.tsx b/packages/ui/src/components/workspaces/uploads/workspace-upload-file.tsx index 99289001..145c6de1 100644 --- a/packages/ui/src/components/workspaces/uploads/workspace-upload-file.tsx +++ b/packages/ui/src/components/workspaces/uploads/workspace-upload-file.tsx @@ -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 ( +
+ + +
+

+ File not found or has been deleted +

+ {upload.errorMessage && ( +

{upload.errorMessage}

+ )} +
+
+
+ +
+
+
+ ); } return (