mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 05:29:26 +08:00
* 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>
141 lines
3.8 KiB
TypeScript
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,
|
|
},
|
|
};
|
|
},
|
|
};
|