被雨水过滤的空气-Rairn 53d0c2dd23
feat: support file collection (#1636)
* feat: support to add File collection

* feat: support to upload files

* refactor: rename 'ReadPretty.Attachment' to 'ReadPretty.File'

* feat: support to associate the File collection

* refactor: add Preview and replace Upload.Selector

* fix(Preview): fix some problems in ReadPretty mode

* feat: use 'preview' as a default title field

* feat: support only local storage now

* fix: should not show 'Add new' button

* chore: add default value for file storage

* fix: fix preview field of file collection cannot be displayed normally

* fix: only Table and Details can display File collection

* chore: translate

* refactor: migration to plugin from core

* refactor: change 'preview' to 'url'

* fix: only 'belongsTo' and 'belongsToMany' can linked file collection

* fix: fix storage and add a field called storage in file collection

* feat: add 'deletable' to configure the visibility of the delete button

* fix: fix can't upload attachment problem

* fix: remove more option

* fix: can't use preview to filter

* fix: remove Import action option

* refactor: remove useless code

* chore: optimize condition

* chore: remove comment

* test: windows compatible

* refactor: optimize upload

* fix: upload action

* fix: createAction

* fix: uploads

* fix: file collection cannot be inherited by other collections

* fix: url should be editable

* fix: url is filterable

* fix: use input interface for url field

* fix: fix error

* fix: remove subform

* Revert "chore: translate"

This reverts commit 53cd346dab8cbee0c52a9da3cf83a99dff2def34.

* refactor: move translation to plugin

* fix: title is editable

* fix: collection?.template === 'file'

* fix: fix order of URL

* fix(collection-manager): allow collectionCategories:list

* chore: add translation

* fix(upload): should enable to use drawer

* refactor: move code to plugin

---------

Co-authored-by: chenos <chenlinxh@gmail.com>
2023-04-06 12:43:40 +08:00

170 lines
5.1 KiB
TypeScript

import multer from '@koa/multer';
import { Context, Next } from '@nocobase/actions';
import path from 'path';
import { FILE_FIELD_NAME, LIMIT_FILES, LIMIT_MAX_FILE_SIZE } from '../constants';
import * as Rules from '../rules';
import { getStorageConfig } from '../storages';
function getRules(ctx: Context) {
const { resourceField } = ctx;
if (!resourceField) {
return ctx.storage.rules;
}
const { rules = {} } = resourceField.options.attachment || {};
return Object.assign({}, ctx.storage.rules, rules);
}
// TODO(optimize): 需要优化错误处理,计算失败后需要抛出对应错误,以便程序处理
function getFileFilter(ctx: Context) {
return (req, file, cb) => {
// size 交给 limits 处理
const { size, ...rules } = getRules(ctx);
const ruleKeys = Object.keys(rules);
const result =
!ruleKeys.length ||
!ruleKeys.some((key) => typeof Rules[key] !== 'function' || !Rules[key](file, rules[key], ctx));
cb(null, result);
};
}
const isUploadAction = (ctx: Context) => {
const { resourceName, actionName } = ctx.action;
if (actionName === 'upload' && resourceName === 'attachments') {
return true;
}
const collection = ctx.db.getCollection(resourceName);
if (collection?.options?.template === 'file' && ['upload', 'create'].includes(actionName)) {
return true;
}
};
export async function middleware(ctx: Context, next: Next) {
const { resourceName } = ctx.action;
const collection = ctx.db.getCollection(resourceName);
if (!isUploadAction(ctx)) {
return next();
}
const Storage = ctx.db.getCollection('storages');
let storage;
if (collection.options.storage) {
storage = await Storage.repository.findOne({ filter: { name: collection.options.storage } });
} else {
storage = await Storage.repository.findOne({ filter: { default: true } });
}
if (!storage) {
console.error('[file-manager] no default or linked storage provided');
return ctx.throw(500);
}
// 传递已取得的存储引擎,避免重查
ctx.storage = storage;
const storageConfig = getStorageConfig(storage.type);
if (!storageConfig) {
console.error(`[file-manager] storage type "${storage.type}" is not defined`);
return ctx.throw(500);
}
const multerOptions = {
fileFilter: getFileFilter(ctx),
limits: {
fileSize: Math.min(getRules(ctx).size || LIMIT_MAX_FILE_SIZE, LIMIT_MAX_FILE_SIZE),
// 每次只允许提交一个文件
files: LIMIT_FILES,
},
storage: storageConfig.make(storage),
};
const upload = multer(multerOptions).single(FILE_FIELD_NAME);
return upload(ctx, next);
}
export async function createAction(ctx: Context, next: Next) {
if (!isUploadAction(ctx)) {
return next();
}
const { [FILE_FIELD_NAME]: file, storage } = ctx;
if (!file) {
return ctx.throw(400, 'file validation failed');
}
const storageConfig = getStorageConfig(storage.type);
const { [storageConfig.filenameKey || 'filename']: name } = file;
// make compatible filename across cloud service (with path)
const filename = path.basename(name);
const extname = path.extname(filename);
const urlPath = storage.path ? storage.path.replace(/^([^\/])/, '/$1') : '';
const values = {
title: file.originalname.replace(extname, ''),
filename,
extname,
// TODO(feature): 暂时两者相同,后面 storage.path 模版化以后,这里只是 file 实际的 path
path: storage.path,
size: file.size,
// 直接缓存起来
url: `${storage.baseUrl}${urlPath}/${filename}`,
mimetype: file.mimetype,
storageId: storage.id,
// @ts-ignore
meta: ctx.request.body,
...(storageConfig.getFileData ? storageConfig.getFileData(file) : {}),
};
ctx.action.mergeParams({
values,
});
await next();
}
export async function uploadAction(ctx: Context, next: Next) {
const { [FILE_FIELD_NAME]: file, storage } = ctx;
if (!file) {
return ctx.throw(400, 'file validation failed');
}
const storageConfig = getStorageConfig(storage.type);
const { [storageConfig.filenameKey || 'filename']: name } = file;
// make compatible filename across cloud service (with path)
const filename = path.basename(name);
const extname = path.extname(filename);
const urlPath = storage.path ? storage.path.replace(/^([^\/])/, '/$1') : '';
const data = {
title: file.originalname.replace(extname, ''),
filename,
extname,
// TODO(feature): 暂时两者相同,后面 storage.path 模版化以后,这里只是 file 实际的 path
path: storage.path,
size: file.size,
// 直接缓存起来
url: `${storage.baseUrl}${urlPath}/${filename}`,
mimetype: file.mimetype,
storageId: storage.id,
// @ts-ignore
meta: ctx.request.body,
...(storageConfig.getFileData ? storageConfig.getFileData(file) : {}),
};
const fileData = await ctx.db.sequelize.transaction(async (transaction) => {
const { resourceName } = ctx.action;
const repository = ctx.db.getRepository(resourceName);
const result = await repository.create({
values: {
...data,
},
transaction,
});
return result;
});
ctx.body = fileData;
await next();
}