@directus/api
Version:
Directus is a real-time API and App dashboard for managing SQL database content
117 lines (116 loc) • 4.29 kB
JavaScript
/**
* TUS implementation for resumable uploads
*
* https://tus.io/
*/
import { useEnv } from '@directus/env';
import { supportsTus } from '@directus/storage';
import { toArray } from '@directus/utils';
import { Server } from '@tus/server';
import { RESUMABLE_UPLOADS } from '../../constants.js';
import { getStorage } from '../../storage/index.js';
import { extractMetadata } from '../files/lib/extract-metadata.js';
import { ItemsService } from '../index.js';
import { TusDataStore } from './data-store.js';
import { getTusLocker } from './lockers.js';
import { pick } from 'lodash-es';
import emitter from '../../emitter.js';
import getDatabase from '../../database/index.js';
import { getSchema } from '../../utils/get-schema.js';
async function createTusStore(context) {
const env = useEnv();
const storage = await getStorage();
const location = toArray(env['STORAGE_LOCATIONS'])[0];
const driver = storage.location(location);
if (!supportsTus(driver)) {
throw new Error(`Storage location ${location} does not support the TUS protocol`);
}
return new TusDataStore({
constants: RESUMABLE_UPLOADS,
accountability: context.accountability,
schema: context.schema,
location,
driver,
});
}
export async function createTusServer(context) {
const env = useEnv();
const store = await createTusStore(context);
const server = new Server({
path: '/files/tus',
datastore: store,
locker: getTusLocker(),
...(RESUMABLE_UPLOADS.MAX_SIZE !== null && { maxSize: RESUMABLE_UPLOADS.MAX_SIZE }),
async onUploadFinish(_req, upload) {
const schema = await getSchema();
const service = new ItemsService('directus_files', {
schema,
});
const file = (await service.readByQuery({
filter: { tus_id: { _eq: upload.id } },
limit: 1,
}))[0];
if (!file)
return {};
let fileData;
// update metadata when file is replaced
if (file.tus_data?.['metadata']?.['replace_id']) {
const newFile = await service.readOne(file.tus_data['metadata']['replace_id']);
const updateFields = pick(file, ['filename_download', 'filesize', 'type']);
const metadata = await extractMetadata(newFile.storage, {
...newFile,
...updateFields,
});
await service.updateOne(file.tus_data['metadata']['replace_id'], {
...updateFields,
...metadata,
});
fileData = {
...newFile,
...updateFields,
...metadata,
id: file.tus_data['metadata']['replace_id'],
};
await service.deleteOne(file.id);
}
else {
const metadata = await extractMetadata(file.storage, file);
await service.updateOne(file.id, {
...metadata,
tus_id: null,
tus_data: null,
});
fileData = {
...file,
...metadata,
tus_id: null,
tus_data: null,
};
}
emitter.emitAction('files.upload', {
payload: fileData,
key: fileData.id,
collection: 'directus_files',
}, {
schema,
database: getDatabase(),
accountability: context.accountability ?? null,
});
return {
headers: {
'Directus-File-Id': upload.metadata['id'],
},
};
},
generateUrl(_req, opts) {
return env['PUBLIC_URL'] + '/files/tus/' + opts.id;
},
allowedHeaders: env['CORS_ALLOWED_HEADERS'],
exposedHeaders: env['CORS_EXPOSED_HEADERS'],
relativeLocation: String(env['PUBLIC_URL']).startsWith('http'),
});
return [server, cleanup];
function cleanup() {
server.removeAllListeners();
}
}