mirror of
https://github.com/colanode/colanode.git
synced 2025-12-16 11:47:47 +01:00
Improve upload and download handling on file delete (#176)
This commit is contained in:
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,7 +494,10 @@ export class NodeService {
|
|||||||
return { deletedNode, createdMutation };
|
return { deletedNode, createdMutation };
|
||||||
});
|
});
|
||||||
|
|
||||||
if (deletedNode) {
|
if (!deletedNode || !createdMutation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
debug(`Deleted node ${deletedNode.id} with type ${deletedNode.type}`);
|
debug(`Deleted node ${deletedNode.id} with type ${deletedNode.type}`);
|
||||||
|
|
||||||
eventBus.publish({
|
eventBus.publish({
|
||||||
@@ -497,14 +506,52 @@ export class NodeService {
|
|||||||
workspaceId: this.workspace.id,
|
workspaceId: this.workspace.id,
|
||||||
node: mapNode(deletedNode),
|
node: mapNode(deletedNode),
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
debug(`Failed to delete node ${nodeId}`);
|
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),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (createdMutation) {
|
const updatedDownloads = await this.workspace.database
|
||||||
this.workspace.mutations.scheduleSync();
|
.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) {
|
||||||
for (let count = 0; count < UPDATE_RETRIES_LIMIT; count++) {
|
for (let count = 0; count < UPDATE_RETRIES_LIMIT; count++) {
|
||||||
@@ -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({
|
||||||
|
|||||||
@@ -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={() => {
|
||||||
|
if (file) {
|
||||||
layout.previewLeft(file.id, true);
|
layout.previewLeft(file.id, true);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{file ? (
|
||||||
<FileThumbnail file={file} className="size-10 text-muted-foreground" />
|
<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>
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
Reference in New Issue
Block a user