被雨水过滤的空气-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

141 lines
3.8 KiB
TypeScript

import Application from '@nocobase/server';
import serve from 'koa-static';
import mkdirp from 'mkdirp';
import multer from 'multer';
import path from 'path';
import { Transactionable } from 'sequelize/types';
import { URL } from 'url';
import { STORAGE_TYPE_LOCAL } from '../constants';
import { getFilename } from '../utils';
// use koa-mount match logic
function match(basePath: string, pathname: string): boolean {
if (!pathname.startsWith(basePath)) {
return false;
}
const newPath = pathname.replace(basePath, '') || '/';
if (basePath.slice(-1) === '/') {
return true;
}
return newPath[0] === '/';
}
async function refresh(app: Application, storages, options?: Transactionable) {
const Storage = app.db.getCollection('storages');
const items = await Storage.repository.find({
filter: {
type: STORAGE_TYPE_LOCAL,
},
transaction: options?.transaction,
});
const primaryKey = Storage.model.primaryKeyAttribute;
storages.clear();
for (const storage of items) {
storages.set(storage[primaryKey], storage);
}
}
function createLocalServerUpdateHook(app, storages) {
return async function (row, options) {
if (row.get('type') === STORAGE_TYPE_LOCAL) {
await refresh(app, storages, options);
}
};
}
function getDocumentRoot(storage): string {
const { documentRoot = 'uploads' } = storage.options || {};
// TODO(feature): 后面考虑以字符串模板的方式使用,可注入 req/action 相关变量,以便于区分文件夹
return path.resolve(path.isAbsolute(documentRoot) ? documentRoot : path.join(process.cwd(), documentRoot));
}
async function middleware(app: Application) {
const Storage = app.db.getCollection('storages');
const storages = new Map<string, any>();
const localServerUpdateHook = createLocalServerUpdateHook(app, storages);
Storage.model.addHook('afterSave', localServerUpdateHook);
Storage.model.addHook('afterDestroy', localServerUpdateHook);
app.on('beforeStart', async () => {
await refresh(app, storages);
});
app.use(async function (ctx, next) {
for (const storage of storages.values()) {
const baseUrl = storage.get('baseUrl').trim();
if (!baseUrl) {
console.error('"baseUrl" is not configured');
// return ctx.throw(500);
continue;
}
let url;
try {
url = new URL(baseUrl);
} catch (e) {
url = {
pathname: baseUrl,
};
}
// 以下情况才认为当前进程所应该提供静态服务
// 否则都忽略,交给其他 server 来提供(如 nginx/cdn 等)
if (url.origin && storage?.options?.serve === false) {
continue;
}
const basePath = url.pathname.startsWith('/') ? url.pathname : `/${url.pathname}`;
if (!match(basePath, ctx.path)) {
continue;
}
ctx.path = ctx.path.replace(basePath, '');
const documentRoot = getDocumentRoot(storage);
return serve(documentRoot)(ctx, async () => {
if (ctx.status == 404) {
return;
}
await next();
});
}
await next();
});
}
export default {
middleware,
make(storage) {
return multer.diskStorage({
destination: function (req, file, cb) {
const destPath = path.join(getDocumentRoot(storage), storage.path);
mkdirp(destPath, (err: Error | null) => cb(err, destPath));
},
filename: getFilename,
});
},
defaults() {
const { LOCAL_STORAGE_DEST, LOCAL_STORAGE_BASE_URL, APP_PORT } = process.env;
const documentRoot = LOCAL_STORAGE_DEST || 'uploads';
return {
title: '本地存储',
type: STORAGE_TYPE_LOCAL,
name: `local`,
baseUrl: LOCAL_STORAGE_BASE_URL || `http://localhost:${APP_PORT || '13000'}/${documentRoot}`,
options: {
documentRoot,
},
};
},
};