@nocobase/plugin-file-manager
Version:
Provides files storage services with files collection template and attachment field.
384 lines (382 loc) • 14.6 kB
JavaScript
/**
* 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
});