UNPKG

@nocobase/plugin-file-manager

Version:

Provides files storage services with files collection template and attachment field.

384 lines (382 loc) • 14.6 kB
/** * This file is part of the NocoBase (R) project. * Copyright (c) 2020-2024 NocoBase Co., Ltd. * Authors: NocoBase Team. * * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. * For more information, please refer to: https://www.nocobase.com/agreement. */ var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var server_exports = {}; __export(server_exports, { PluginFileManagerServer: () => PluginFileManagerServer, default: () => server_default }); module.exports = __toCommonJS(server_exports); var import_fs = __toESM(require("fs")); var import_path = require("path"); var import_mime_match = __toESM(require("mime-match")); var import_database = require("@nocobase/database"); var import_server = require("@nocobase/server"); var import_utils = require("@nocobase/utils"); var import_constants = require("../constants"); var import_actions = __toESM(require("./actions")); var import_attachment_interface = require("./interfaces/attachment-interface"); var import_ali_oss = __toESM(require("./storages/ali-oss")); var import_local = __toESM(require("./storages/local")); var import_s3 = __toESM(require("./storages/s3")); var import_tx_cos = __toESM(require("./storages/tx-cos")); var import_utils2 = require("./utils"); var import_repair_filenames = require("./commands/repair-filenames"); const DEFAULT_STORAGE_TYPE = import_constants.STORAGE_TYPE_LOCAL; class FileDeleteError extends Error { data; constructor(message, data) { super(message); this.name = "FileDeleteError"; this.data = data; } } function supportsFileStream(dataSource) { return Boolean( dataSource && typeof dataSource === "object" && typeof dataSource.getFileStream === "function" ); } class PluginFileManagerServer extends import_server.Plugin { storageTypes = new import_utils.Registry(); storagesCache = /* @__PURE__ */ new Map(); static async staticImport() { import_server.Application.addCommand(import_repair_filenames.registerRepairFilenamesCommand); } afterDestroy = async (record, options) => { var _a; const { collection } = record.constructor; if (((_a = collection == null ? void 0 : collection.options) == null ? void 0 : _a.template) !== "file" && collection.name !== "attachments") { return; } if (!record.get("storageId")) { return; } const storage = this.storagesCache.get(record.get("storageId")); if (!storage) { return; } if (storage == null ? void 0 : storage.paranoid) { return; } const Type = this.storageTypes.get(storage.type); if (!Type) { return; } const storageConfig = new Type(storage); const result = await storageConfig.delete([record]); if (!result[0]) { throw new FileDeleteError("Failed to delete file", record); } }; registerStorageType(type, Type) { this.storageTypes.register(type, Type); } async createFileRecord(options) { const { values, storageName, subPath, collectionName, filePath, transaction } = options; const collection = this.db.getCollection(collectionName); if (!collection) { throw new Error(`collection does not exist`); } const collectionRepository = this.db.getRepository(collectionName); const name = storageName || collection.options.storage; const data = await this.uploadFile({ storageName: name, subPath, filePath }); return await collectionRepository.create({ values: { ...data, ...values }, transaction }); } parseStorage(instance) { return this.app.environment.renderJsonTemplate(instance.toJSON()); } async uploadFile(options) { const { storageName, subPath, filePath, documentRoot } = options; if (!this.storagesCache.size) { await this.loadStorages(); } const storages = Array.from(this.storagesCache.values()); const cachedStorage = storages.find((item) => item.name === storageName) || storages.find((item) => item.default); if (!cachedStorage) { throw new Error("[file-manager] no linked or default storage provided"); } const storage = { ...cachedStorage, options: { ...cachedStorage.options || {} }, path: (0, import_utils2.resolveStoragePath)(cachedStorage.path, subPath) }; const fileStream = import_fs.default.createReadStream(filePath); if (documentRoot) { storage.options["documentRoot"] = documentRoot; } const StorageType = this.storageTypes.get(storage.type); const storageInstance = new StorageType(storage); if (!storageInstance) { throw new Error(`[file-manager] storage type "${storage.type}" is not defined`); } const engine = storageInstance.make(); const file = { originalname: (0, import_path.basename)(filePath), path: filePath, stream: fileStream }; await new Promise((resolve, reject) => { engine._handleFile({}, file, (error, info) => { if (error) { reject(error); } Object.assign(file, info); resolve(info); }); }); return storageInstance.getFileData(file, {}); } async loadStorages(options) { const repository = this.db.getRepository("storages"); const storages = await repository.find({ transaction: options == null ? void 0 : options.transaction }); this.storagesCache = /* @__PURE__ */ new Map(); for (const storage of storages) { this.storagesCache.set(storage.get("id"), this.parseStorage(storage)); } } async install() { const defaultStorageType = this.storageTypes.get(DEFAULT_STORAGE_TYPE); if (defaultStorageType) { const Storage = this.db.getCollection("storages"); if (await Storage.repository.findOne({ filter: { name: defaultStorageType.defaults().name } })) { return; } await Storage.repository.create({ values: { ...defaultStorageType.defaults(), type: DEFAULT_STORAGE_TYPE, default: true } }); } } async handleSyncMessage(message) { if (message.type === "reloadStorages") { await this.loadStorages(); } } async beforeLoad() { this.db.registerModels({ FileModel: import_database.Model }); this.db.on("beforeDefineCollection", (options) => { if (options.template === "file") { options.model = "FileModel"; } }); this.db.on("afterDefineCollection", (collection) => { if (collection.options.template !== "file") { return; } collection.model.beforeUpdate((model) => { if (!model.changed("url") || !model.changed("preview")) { return; } model.set("url", model.previous("url")); model.set("preview", model.previous("preview")); model.changed("url", false); model.changed("preview", false); }); }); this.app.on("afterStart", async () => { await this.loadStorages(); }); } async load() { this.db.on("afterDestroy", this.afterDestroy); this.storageTypes.register(import_constants.STORAGE_TYPE_LOCAL, import_local.default); this.storageTypes.register(import_constants.STORAGE_TYPE_ALI_OSS, import_ali_oss.default); this.storageTypes.register(import_constants.STORAGE_TYPE_S3, import_s3.default); this.storageTypes.register(import_constants.STORAGE_TYPE_TX_COS, import_tx_cos.default); const Storage = this.db.getModel("storages"); Storage.beforeSave((m) => { (0, import_local.validateLocalStorageConfig)(m.toJSON()); }); Storage.afterSave(async (m, { transaction }) => { await this.loadStorages({ transaction }); this.sendSyncMessage({ type: "reloadStorages" }, { transaction }); }); Storage.afterDestroy(async (m, { transaction }) => { var _a, _b; for (const collection of this.db.collections.values()) { if (((_a = collection == null ? void 0 : collection.options) == null ? void 0 : _a.template) === "file" && ((_b = collection == null ? void 0 : collection.options) == null ? void 0 : _b.storage) === m.name) { throw new Error( this.t( `The storage "${m.name}" is in use in collection "${collection.name}" and cannot be deleted.` ) ); } } await this.loadStorages({ transaction }); this.sendSyncMessage({ type: "reloadStorages" }, { transaction }); }); this.db.on("afterDefineCollection", (collection) => { const { template } = collection.options; if (template === "file") { collection.setField("storageId", { type: "bigInt", createOnly: true, visible: true, index: true }); } }); this.app.acl.registerSnippet({ name: `pm.${this.name}.storages`, actions: ["storages:*"] }); (0, import_actions.default)(this); this.app.acl.allow("attachments", ["upload", "create"], "loggedIn"); this.app.acl.allow("storages", "getBasicInfo", "loggedIn"); this.app.acl.allow("storages", "check", "loggedIn"); this.app.acl.appendStrategyResource("attachments"); const defaultStorageName = this.storageTypes.get(DEFAULT_STORAGE_TYPE).defaults().name; this.app.acl.addFixedParams("storages", "destroy", () => { return { filter: { "name.$ne": defaultStorageName } }; }); const ownMerger = () => { return { filter: { createdById: "{{ctx.state.currentUser.id}}" } }; }; this.app.acl.addFixedParams("attachments", "update", ownMerger); this.app.acl.addFixedParams("attachments", "create", ownMerger); this.app.acl.addFixedParams("attachments", "destroy", ownMerger); this.app.resourcer.define({ name: "attachments", actions: { list(ctx) { ctx.throw(404); } } }); this.app.db.interfaceManager.registerInterfaceType("attachment", import_attachment_interface.AttachmentInterface); this.db.on("afterFind", async (instances) => { var _a, _b, _c; if (!instances) { return; } const records = Array.isArray(instances) ? instances : [instances]; const name = (_b = (_a = records[0]) == null ? void 0 : _a.constructor) == null ? void 0 : _b.name; if (name) { const collection = this.db.getCollection(name); if ((collection == null ? void 0 : collection.name) === "attachments" || ((_c = collection == null ? void 0 : collection.options) == null ? void 0 : _c.template) === "file") { for (const record of records) { const url = await this.getFileURL(record); const previewUrl = await this.getFileURL(record, true); record.set("url", url); record.set("preview", previewUrl); record.dataValues.preview = previewUrl; } } } }); } async getFileURL(file, preview = false) { if (!file.storageId) { return (0, import_utils2.encodeURL)(file.url); } const storage = this.storagesCache.get(file.storageId); if (!storage) { return (0, import_utils2.encodeURL)(file.url); } const storageType = this.storageTypes.get(storage.type); return new storageType(storage).getFileURL( file, Boolean(file.mimetype && (0, import_mime_match.default)(file.mimetype, "image/*") && preview && storage.options.thumbnailRule) ); } async isPublicAccessStorage(storageName) { var _a; const storageRepository = this.db.getRepository("storages"); const storages = await storageRepository.findOne({ filter: { default: true } }); let storage; if (!storageName) { storage = storages; } else { storage = await storageRepository.findOne({ filter: { name: storageName } }); } storage = this.parseStorage(storage); if (["local", "ali-oss", "s3", "tx-cos"].includes(storage.type)) { return true; } return !!((_a = storage.options) == null ? void 0 : _a.public); } async getFileStream(file, options) { var _a; const dataSourceKey = (_a = file.source) == null ? void 0 : _a.dataSourceKey; if (dataSourceKey && dataSourceKey !== "main") { const dataSource = this.app.dataSourceManager.get(dataSourceKey); if (supportsFileStream(dataSource)) { return dataSource.getFileStream(file, options); } } if (!file.storageId) { throw new Error("File storageId not found"); } const storage = this.storagesCache.get(file.storageId); if (!storage) { throw new Error("[file-manager] no linked or default storage provided"); } const StorageType = this.storageTypes.get(storage.type); if (!StorageType) { throw new Error(`[file-manager] storage type "${storage.type}" is not defined`); } const storageInstance = new StorageType(storage); if (!storageInstance) { throw new Error(`[file-manager] storage type "${storage.type}" is not defined`); } return storageInstance.getFileStream(file, options); } } var server_default = PluginFileManagerServer; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { PluginFileManagerServer });