@nocobase/plugin-file-manager
Version:
Provides files storage services with files collection template and attachment field.
284 lines (282 loc) • 9.52 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 __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
});