diff --git a/.changeset/shy-chairs-return.md b/.changeset/shy-chairs-return.md
new file mode 100644
index 0000000000..54e7ced1bc
--- /dev/null
+++ b/.changeset/shy-chairs-return.md
@@ -0,0 +1,6 @@
+---
+'@directus/api': patch
+'@directus/app': patch
+---
+
+Fixed TUS file upload logic to include the file UUID to enable file info fetching
diff --git a/api/src/controllers/tus.ts b/api/src/controllers/tus.ts
index 07b327fbd5..22ac71f22c 100644
--- a/api/src/controllers/tus.ts
+++ b/api/src/controllers/tus.ts
@@ -31,7 +31,7 @@ const checkFileAccess = asyncHandler(async (req, _res, next) => {
const action = mapAction(req.method);
if (action === 'create') {
- // checkAccess doesnt seem to work as expected for "create" actions
+ // checkAccess doesn't seem to work as expected for "create" actions
const hasPermission = Boolean(
req.accountability?.permissions?.find((permission) => {
return permission.collection === 'directus_files' && permission.action === action;
@@ -52,12 +52,14 @@ const checkFileAccess = asyncHandler(async (req, _res, next) => {
});
const handler = asyncHandler(async (req, res) => {
- const tusServer = await createTusServer({
+ const [tusServer, cleanupServer] = await createTusServer({
schema: req.schema,
accountability: req.accountability,
});
await tusServer.handle(req, res);
+
+ cleanupServer();
});
export function scheduleTusCleanup() {
@@ -65,11 +67,13 @@ export function scheduleTusCleanup() {
if (validateCron(RESUMABLE_UPLOADS.SCHEDULE)) {
scheduleSynchronizedJob('tus-cleanup', RESUMABLE_UPLOADS.SCHEDULE, async () => {
- const tusServer = await createTusServer({
+ const [tusServer, cleanupServer] = await createTusServer({
schema: await getSchema(),
});
await tusServer.cleanUpExpiredUploads();
+
+ cleanupServer();
});
}
}
diff --git a/api/src/services/tus/data-store.ts b/api/src/services/tus/data-store.ts
index 4a03f52b42..a218f26575 100644
--- a/api/src/services/tus/data-store.ts
+++ b/api/src/services/tus/data-store.ts
@@ -116,6 +116,11 @@ export class TusDataStore extends DataStore {
// If this is a new file upload, we need to generate a new primary key and DB record
const primaryKey = await itemsService.createOne(fileData, { emitEvents: false });
+ // Set the file id, so it is available to be sent as a header on upload creation / resume
+ if (!upload.metadata['id']) {
+ upload.metadata['id'] = primaryKey as string;
+ }
+
const fileExtension =
extname(upload.metadata['filename_download']) ||
(upload.metadata['type'] && '.' + extension(upload.metadata['type'])) ||
@@ -124,13 +129,6 @@ export class TusDataStore extends DataStore {
// The filename_disk is the FINAL filename on disk
fileData.filename_disk ||= primaryKey + (fileExtension || '');
- // Temp filename is used for replacements
- // const tempFilenameDisk = fileData.tus_id! + (fileExtension || '');
-
- // if (isReplacement) {
- // upload.metadata['temp_file'] = tempFilenameDisk;
- // }
-
try {
// If this is a replacement, we'll write the file to a temp location first to ensure we don't overwrite the existing file if something goes wrong
upload = (await this.storageDriver.createChunkedUpload(fileData.filename_disk, upload)) as Upload;
diff --git a/api/src/services/tus/server.ts b/api/src/services/tus/server.ts
index 05a67be8cf..bbf3536c90 100644
--- a/api/src/services/tus/server.ts
+++ b/api/src/services/tus/server.ts
@@ -8,7 +8,7 @@ import type { Driver, TusDriver } from '@directus/storage';
import { supportsTus } from '@directus/storage';
import type { Accountability, File, SchemaOverview } from '@directus/types';
import { toArray } from '@directus/utils';
-import { Server } from '@tus/server';
+import { Server, EVENTS } from '@tus/server';
import { RESUMABLE_UPLOADS } from '../../constants.js';
import { getStorage } from '../../storage/index.js';
import { extractMetadata } from '../files/lib/extract-metadata.js';
@@ -41,11 +41,11 @@ async function createTusStore(context: Context) {
});
}
-export async function createTusServer(context: Context) {
+export async function createTusServer(context: Context): Promise<[Server, () => void]> {
const env = useEnv();
const store = await createTusStore(context);
- return new Server({
+ const server = new Server({
path: '/files/tus',
datastore: store,
locker: getTusLocker(),
@@ -97,4 +97,14 @@ export async function createTusServer(context: Context) {
},
relativeLocation: String(env['PUBLIC_URL']).startsWith('http'),
});
+
+ server.on(EVENTS.POST_CREATE, async (_req, res, upload) => {
+ res.setHeader('Directus-File-Id', upload.metadata!['id']!);
+ });
+
+ return [server, cleanup];
+
+ function cleanup() {
+ server.removeAllListeners();
+ }
}
diff --git a/app/src/components/v-upload.vue b/app/src/components/v-upload.vue
index f4a279eea5..63e406b9fe 100644
--- a/app/src/components/v-upload.vue
+++ b/app/src/components/v-upload.vue
@@ -1,12 +1,13 @@
@@ -27,7 +24,7 @@ function close() {
{{ t('add_file') }}
-
+
{{ t('done') }}
diff --git a/app/src/utils/upload-file.ts b/app/src/utils/upload-file.ts
index 2125f15027..2cc47be9aa 100644
--- a/app/src/utils/upload-file.ts
+++ b/app/src/utils/upload-file.ts
@@ -1,4 +1,5 @@
import api from '@/api';
+import type { File } from '@directus/types';
import { emitter, Events } from '@/events';
import { i18n } from '@/lang';
import { useServerStore } from '@/stores/server';
@@ -10,29 +11,23 @@ import type { PreviousUpload } from 'tus-js-client';
import { Upload } from 'tus-js-client';
export async function uploadFile(
- file: File,
+ file: globalThis.File,
options?: {
onProgressChange?: (percentage: number) => void;
onChunkedUpload?: (controller: Upload) => void;
notifications?: boolean;
- preset?: Record;
+ preset?: Partial;
fileId?: string;
requirePreviousUpload?: boolean;
},
-): Promise {
+): Promise {
const progressHandler = options?.onProgressChange || (() => undefined);
const server = useServerStore();
let notified = false;
if (server.info.uploads) {
- const fileInfo: Record = {};
-
- if (options?.preset) {
- for (const [key, value] of Object.entries(options.preset)) {
- fileInfo[key] = value;
- }
- }
+ const fileInfo: Partial = { ...(options?.preset ?? {}) };
if (options?.fileId) {
fileInfo.id = options?.fileId;
@@ -45,7 +40,7 @@ export async function uploadFile(
const upload = new Upload(file, {
endpoint: getRootPath() + `files/tus`,
chunkSize: server.info.uploads?.chunkSize ?? 10_000_000,
- metadata: fileInfo,
+ metadata: fileInfo as Record,
// Allow user to re-upload of the same file
// https://github.com/tus/tus-js-client/blob/main/docs/api.md#removefingerprintonsuccess
removeFingerprintOnSuccess: true,
@@ -66,7 +61,7 @@ export async function uploadFile(
notified = true;
}
},
- onSuccess() {
+ async onSuccess() {
if (options?.notifications) {
notify({
title: i18n.global.t('upload_file_success'),
@@ -76,11 +71,20 @@ export async function uploadFile(
emitter.emit(Events.upload);
emitter.emit(Events.tusResumableUploadsChanged);
- resolve(fileInfo);
+ const response = await api.get(`files/${fileInfo.id}`);
+
+ if (response) {
+ resolve(response.data.data);
+ } else {
+ resolve(fileInfo as File);
+ }
},
onShouldRetry() {
return false;
},
+ onAfterResponse(_req, res) {
+ fileInfo.id ??= res.getHeader('Directus-File-Id');
+ },
});
options?.onChunkedUpload?.({
@@ -99,6 +103,7 @@ export async function uploadFile(
// Found previous uploads so we select the first one.
if (previousUploads.length > 0) {
upload.resumeFromPreviousUpload(previousUploads[0]!);
+ fileInfo.id = previousUploads[0]!.metadata['id'];
}
// Start the upload
@@ -141,6 +146,8 @@ export async function uploadFile(
} catch (error) {
unexpectedError(error);
}
+
+ return;
}
function onUploadProgress(progressEvent: AxiosProgressEvent) {
diff --git a/app/src/utils/upload-files.ts b/app/src/utils/upload-files.ts
index 08e59dbb52..507c366767 100644
--- a/app/src/utils/upload-files.ts
+++ b/app/src/utils/upload-files.ts
@@ -2,10 +2,11 @@ import { i18n } from '@/lang';
import { notify } from '@/utils/notify';
import { uploadFile } from '@/utils/upload-file';
import { unexpectedError } from './unexpected-error';
+import type { File } from '@directus/types';
import type { Upload } from 'tus-js-client';
export async function uploadFiles(
- files: File[],
+ files: globalThis.File[],
options?: {
onProgressChange?: (percentages: number[]) => void;
onChunkedUpload?: (controllers: (Upload | null)[]) => void;
@@ -13,7 +14,7 @@ export async function uploadFiles(
preset?: Record;
folder?: string;
},
-): Promise {
+): Promise<(File | undefined)[]> {
const progressHandler = options?.onProgressChange || (() => undefined);
const progressForFiles = files.map(() => 0);
const uploadControllers: (Upload | null)[] = Array(files.length).fill(null);
@@ -48,5 +49,5 @@ export async function uploadFiles(
unexpectedError(error);
}
- return;
+ return [];
}
diff --git a/app/src/views/private/components/file-preview-replace.vue b/app/src/views/private/components/file-preview-replace.vue
index cfc27b3753..1bbdfdd75d 100644
--- a/app/src/views/private/components/file-preview-replace.vue
+++ b/app/src/views/private/components/file-preview-replace.vue
@@ -13,7 +13,6 @@ const emit = defineEmits<{
const { t } = useI18n();
const dialogActive = ref(false);
-const uploadRef = ref | null>(null);
function onInput() {
dialogActive.value = false;
@@ -22,7 +21,6 @@ function onInput() {
function close() {
dialogActive.value = false;
- uploadRef.value?.abort();
}
@@ -38,7 +36,7 @@ function close() {
{{ t('replace_file') }}
-
+
{{ t('done') }}