@directus/api
Version:
Directus is a real-time API and App dashboard for managing SQL database content
164 lines (162 loc) • 6.95 kB
JavaScript
import { useLogger } from "../../logger/index.js";
import database_default from "../../database/index.js";
import { assertUniqueFilename } from "../files/lib/assert-unique-filename.js";
import { sanitizeFilepath } from "../files/lib/sanitize-filepath.js";
import { assertValidStoragePath } from "../files/lib/assert-valid-storage-path.js";
import { isMimeTypeAllowed } from "../files/lib/is-mime-type-allowed.js";
import { ItemsService } from "../items.js";
import { useEnv } from "@directus/env";
import { UnsupportedMediaTypeError } from "@directus/errors";
import { omit } from "lodash-es";
import { extname } from "node:path";
import { extension } from "mime-types";
import formatTitle from "@directus/format-title";
import { DataStore, ERRORS, Upload } from "@tus/utils";
//#region src/services/tus/data-store.ts
var TusDataStore = class extends DataStore {
chunkSize;
expirationTime;
location;
storageDriver;
schema;
accountability;
constructor(config) {
super();
if (config.constants.CHUNK_SIZE !== null) this.chunkSize = config.constants.CHUNK_SIZE;
this.expirationTime = config.constants.EXPIRATION_TIME;
this.location = config.location;
this.storageDriver = config.driver;
this.extensions = this.storageDriver.tusExtensions;
this.schema = config.schema;
this.accountability = config.accountability;
}
async create(upload) {
const logger = useLogger();
const knex = database_default();
const filesItemsService = new ItemsService("directus_files", {
accountability: this.accountability,
schema: this.schema,
knex
});
upload.creation_date = (/* @__PURE__ */ new Date()).toISOString();
if (!upload.size || !upload.metadata || !upload.metadata["filename_download"]) throw ERRORS.INVALID_METADATA;
if (!upload.metadata["type"]) upload.metadata["type"] = "application/octet-stream";
const mimeType = upload.metadata["type"];
if (isMimeTypeAllowed(mimeType, useEnv()["FILES_MIME_TYPE_ALLOW_LIST"]) === false) throw new UnsupportedMediaTypeError({
mediaType: mimeType,
where: "tus upload"
});
if (!upload.metadata["title"]) upload.metadata["title"] = formatTitle(upload.metadata["filename_download"]);
let existingFile;
if (upload.metadata["id"]) {
existingFile = await knex.select("tus_id").from("directus_files").andWhere({ id: upload.metadata["id"] }).first();
if (existingFile && existingFile["tus_id"] !== null) throw ERRORS.INVALID_METADATA;
}
const fileData = {
...omit(upload.metadata, ["id"]),
tus_id: upload.id,
tus_data: upload,
filesize: upload.size,
storage: this.location
};
if (fileData.filename_disk) {
fileData.filename_disk = sanitizeFilepath(fileData.filename_disk);
assertValidStoragePath(fileData.filename_disk, this.location);
await assertUniqueFilename(knex, fileData.filename_disk, upload.metadata["id"]);
}
if ("folder" in fileData === false) {
const settings = await knex.select("storage_default_folder").from("directus_settings").first();
if (settings?.storage_default_folder) fileData.folder = settings.storage_default_folder;
}
const primaryKey = await filesItemsService.createOne(fileData, { emitEvents: false });
if (!existingFile) upload.metadata["id"] = primaryKey;
const fileExtension = extname(upload.metadata["filename_download"]) || upload.metadata["type"] && "." + extension(upload.metadata["type"]) || "";
fileData.filename_disk ||= primaryKey + (fileExtension || "");
try {
upload = await this.storageDriver.createChunkedUpload(fileData.filename_disk, upload);
fileData.tus_data = upload;
await filesItemsService.updateOne(primaryKey, fileData, { emitEvents: false });
return upload;
} catch (err) {
logger.warn(`Couldn't create chunked upload for ${fileData.filename_disk}`);
logger.warn(err);
await filesItemsService.deleteOne(primaryKey, { emitEvents: false });
throw ERRORS.UNKNOWN_ERROR;
}
}
async write(readable, tus_id, offset) {
const logger = useLogger();
const fileData = await this.getFileById(tus_id);
const filePath = fileData.filename_disk;
const sudoFilesItemsService = new ItemsService("directus_files", { schema: this.schema });
try {
const newOffset = await this.storageDriver.writeChunk(filePath, readable, offset, fileData.tus_data);
await sudoFilesItemsService.updateOne(fileData.id, { tus_data: {
...fileData.tus_data,
offset: newOffset
} });
if (Number(fileData.filesize) === newOffset) {
try {
await this.storageDriver.finishChunkedUpload(filePath, fileData.tus_data);
} catch (err) {
await this.remove(fileData.tus_id);
throw err;
}
const targetId = fileData.tus_data?.["metadata"]?.["id"];
if (targetId && targetId !== fileData.id) {
const replaceData = await sudoFilesItemsService.readOne(targetId, { fields: ["filename_disk"] });
for await (const partPath of this.storageDriver.list(targetId)) await this.storageDriver.delete(partPath);
await this.storageDriver.move(filePath, replaceData.filename_disk);
}
}
return newOffset;
} catch (err) {
logger.error(err, "Error writing chunk for upload \"%s\" at offset %d", tus_id, offset);
if ("status_code" in err && err.status_code === 500) throw err;
throw ERRORS.FILE_WRITE_ERROR;
}
}
async remove(tus_id) {
const sudoFilesItemsService = new ItemsService("directus_files", { schema: this.schema });
const fileData = await this.getFileById(tus_id);
await this.storageDriver.deleteChunkedUpload(fileData.filename_disk, fileData.tus_data);
await sudoFilesItemsService.deleteOne(fileData.id);
}
async deleteExpired() {
const sudoFilesItemsService = new ItemsService("directus_files", { schema: this.schema });
const now = /* @__PURE__ */ new Date();
const toDelete = [];
const uploadFiles = await sudoFilesItemsService.readByQuery({
fields: [
"modified_on",
"tus_id",
"tus_data"
],
filter: { tus_id: { _nnull: true } }
});
if (!uploadFiles) return 0;
for (const fileData of uploadFiles) if (fileData && fileData.tus_data && this.getExpiration() > 0 && fileData.tus_data["size"] !== fileData.tus_data["offset"] && fileData.modified_on) {
const modified = new Date(fileData.modified_on);
if (now > new Date(modified.getTime() + this.getExpiration())) toDelete.push(this.remove(fileData.tus_id));
}
await Promise.allSettled(toDelete);
return toDelete.length;
}
getExpiration() {
return this.expirationTime;
}
async getUpload(id) {
return new Upload((await this.getFileById(id)).tus_data);
}
async getFileById(tus_id) {
const results = await new ItemsService("directus_files", { schema: this.schema }).readByQuery({ filter: {
tus_id: { _eq: tus_id },
storage: { _eq: this.location },
...this.accountability?.user ? { uploaded_by: { _eq: this.accountability.user } } : {}
} });
if (!results || !results[0]) throw ERRORS.FILE_NOT_FOUND;
return results[0];
}
};
//#endregion
export { TusDataStore };