UNPKG

@nocobase/plugin-file-manager

Version:

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

284 lines (282 loc) • 9.52 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 __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; 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 __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var repair_filenames_exports = {}; __export(repair_filenames_exports, { getRepairedAttachmentValues: () => getRepairedAttachmentValues, registerRepairFilenamesCommand: () => registerRepairFilenamesCommand, repairAttachmentFilenames: () => repairAttachmentFilenames, replaceInvisibleChars: () => replaceInvisibleChars }); module.exports = __toCommonJS(repair_filenames_exports); var import_sequelize = require("sequelize"); var import_utils = require("../utils"); function containsInvisibleChars(value) { return Array.from(value || "").some((char) => { const code = char.charCodeAt(0); return code <= 31 || code >= 127 && code <= 159; }); } function replaceInvisibleChars(value) { return Array.from(value || "").map((char) => { const code = char.charCodeAt(0); return code <= 31 || code >= 127 && code <= 159 ? "-" : char; }).join(""); } function getRepairedAttachmentValues(record) { return { path: replaceInvisibleChars(record.path), filename: replaceInvisibleChars(record.filename) }; } function updateUrlValue(url, oldKey, newKey) { if (!url) { return url; } const encodedOldKey = oldKey.split("/").map((segment) => encodeURIComponent(segment)).join("/"); const encodedNewKey = newKey.split("/").map((segment) => encodeURIComponent(segment)).join("/"); return url.split(oldKey).join(newKey).split(encodedOldKey).join(encodedNewKey); } function formatItem(item) { return { collectionName: item.collectionName, id: item.id, storageId: item.storageId, storageType: item.storageType, oldKey: item.oldKey, newKey: item.newKey, sourceExists: item.sourceExists, targetExists: item.targetExists, status: item.status, reason: item.reason }; } async function getFileCollectionNames(app) { var _a; const repository = app.db.getRepository("collections") || ((_a = app.db.getCollection("collections")) == null ? void 0 : _a.repository); if (repository) { const collections = await repository.find({ filter: { "options.template": "file" } }); return Array.from(/* @__PURE__ */ new Set(["attachments", ...collections.map((collection) => collection.get("name"))])); } return Array.from( /* @__PURE__ */ new Set([ "attachments", ...Array.from(app.db.collections.values()).filter((collection) => collection.options.template === "file").map((collection) => collection.name) ]) ); } async function loadFileCollections(app) { var _a, _b; const collection = app.db.getCollection("collections"); if (!collection) { return; } const repository = collection.repository; await ((_a = repository.setApp) == null ? void 0 : _a.call(repository, app)); await ((_b = repository.load) == null ? void 0 : _b.call(repository)); } async function repairCollectionAttachmentFilenames({ app, fileManager, collectionName, result, apply, batchSize, limit }) { const collection = app.db.getCollection(collectionName); if (!collection) { return; } const Model = collection.model; let lastId = null; while (result.scanned < limit) { const rows = await Model.findAll({ attributes: ["id", "path", "filename", "storageId", "url"], where: lastId ? { id: { [import_sequelize.Op.gt]: lastId } } : void 0, order: [["id", "ASC"]], limit: Math.min(batchSize, limit - result.scanned) }); if (!rows.length) { break; } for (const row of rows) { result.scanned += 1; lastId = row.get("id"); const record = row.toJSON(); if (!containsInvisibleChars(record.path) && !containsInvisibleChars(record.filename)) { continue; } const storage = fileManager.storagesCache.get(record.storageId); const { path: newPath, filename: newFilename } = getRepairedAttachmentValues(record); const oldKey = (0, import_utils.getFileKey)(record); const newRecord = { ...record, path: newPath, filename: newFilename }; const newKey = (0, import_utils.getFileKey)(newRecord); const item = { collectionName, id: lastId, storageId: record.storageId, storageType: (storage == null ? void 0 : storage.type) || "", oldPath: record.path || "", oldFilename: record.filename, newPath, newFilename, oldKey, newKey, status: "pending" }; result.candidates.push(item); if (!storage) { item.status = "skipped"; item.reason = "storage_not_found"; result.skipped.push(item); continue; } if (oldKey === newKey) { item.status = "skipped"; item.reason = "unchanged"; result.skipped.push(item); continue; } try { const StorageClass = fileManager.storageTypes.get(storage.type); if (!StorageClass) { item.status = "skipped"; item.reason = "storage_type_not_found"; result.skipped.push(item); continue; } const storageInstance = new StorageClass(storage); item.sourceExists = await storageInstance.exists(record); if (!item.sourceExists) { item.status = "skipped"; item.reason = "source_not_found"; result.skipped.push(item); continue; } item.targetExists = await storageInstance.exists(newRecord); if (item.targetExists) { item.status = "skipped"; item.reason = "target_exists"; result.skipped.push(item); continue; } if (apply) { await storageInstance.copy(record, newRecord); await row.update({ path: newPath, filename: newFilename, url: updateUrlValue(record.url, oldKey, newKey) }); try { const [deleted] = await storageInstance.delete([record]); if (!deleted) { item.reason = "delete_old_failed"; } } catch (error) { item.reason = `delete_old_failed: ${error.message}`; } item.status = "repaired"; result.repaired.push(item); } } catch (error) { item.status = "failed"; item.reason = error.message; result.failed.push(item); } } } } async function repairAttachmentFilenames(app, fileManager, options = {}) { const apply = !!options.apply; const batchSize = options.batchSize || 500; const limit = options.limit || Number.POSITIVE_INFINITY; const result = { dryRun: !apply, scanned: 0, candidates: [], repaired: [], skipped: [], failed: [] }; if (!fileManager.storagesCache.size) { await fileManager.loadStorages(); } await loadFileCollections(app); for (const collectionName of await getFileCollectionNames(app)) { if (result.scanned >= limit) { break; } await repairCollectionAttachmentFilenames({ app, fileManager, collectionName, result, apply, batchSize, limit }); } return result; } function registerRepairFilenamesCommand(app) { const command = app.findCommand("file-manager") || app.command("file-manager"); command.command("repair-filenames").preload().option("--apply", "rename objects and update attachment records").option("--batch-size [batchSize]", "batch size for scanning attachments").option("--limit [limit]", "maximum number of attachment records to scan").action(async (options) => { const fileManager = app.pm.get("file-manager"); const result = await repairAttachmentFilenames(app, fileManager, { apply: !!options.apply, batchSize: options.batchSize ? Number(options.batchSize) : void 0, limit: options.limit ? Number(options.limit) : void 0 }); console.log( JSON.stringify( { dryRun: result.dryRun, scanned: result.scanned, candidates: result.candidates.length, repaired: result.repaired.length, skipped: result.skipped.length, failed: result.failed.length }, null, 2 ) ); if (result.candidates.length) { console.table(result.candidates.map(formatItem)); } }); } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { getRepairedAttachmentValues, registerRepairFilenamesCommand, repairAttachmentFilenames, replaceInvisibleChars });