refactor(plugin-file-manager): refactor file url generating (#6472)

* refactor(plugin-file-manager): change file url generation to a better way

* fix(plugin-file-manager): use url field first

* fix(client): remove unused function call

* fix(plugin-file-manager): fix test cases

* fix: local getURL

* fix: md5

* fix: getFileURL

* fix(plugin-file-manager): fix FileModel class

* fix: use DeleteObjectCommand

* fix(plugin-file-manager): fix package version and require

* test(plugin-file-manager): fix test cases

* fix: storageInstance

* fix: test case error

* fix: url-join

* fix: autoGenId false

* fix: url

* fix: file.url

* fix: file preview

* fix: test error

* fix: isURL

* fix: only

---------

Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
Junyi 2025-03-17 08:54:26 +08:00 committed by GitHub
parent ec618b06c8
commit 5144d47eb9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 247 additions and 146 deletions

View File

@ -200,32 +200,11 @@ export interface URLReadPrettyProps {
const ellipsisStyle = { textOverflow: 'ellipsis', overflow: 'hidden', whiteSpace: 'nowrap', display: 'block' };
function encodeFileURL(url: string): string {
if (!url) {
return url;
}
if (url.includes('X-Amz-Content-Sha256')) {
return url;
}
const parts = url.split('/');
const filename = parts.pop();
parts.push(encodeURIComponent(filename));
const encodedURL = parts.join('/');
return encodedURL;
}
ReadPretty.URL = (props: URLReadPrettyProps) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const prefixCls = usePrefixCls('description-url', props);
const content = props.value && (
<a
style={props.ellipsis ? ellipsisStyle : undefined}
target="_blank"
rel="noopener noreferrer"
href={encodeFileURL(props.value)}
>
<a style={props.ellipsis ? ellipsisStyle : undefined} target="_blank" rel="noopener noreferrer" href={props.value}>
{props.value}
</a>
);

View File

@ -32,7 +32,6 @@ import {
toValueItem as toValueItemDefault,
useBeforeUpload,
useUploadProps,
encodeFileURL,
} from './shared';
import { useStyles } from './style';
import type { ComposedUpload, DraggerProps, DraggerV2Props, UploadProps } from './type';
@ -43,10 +42,10 @@ attachmentFileTypes.add({
},
getThumbnailURL(file) {
if (file.preview) {
return encodeFileURL(file.preview);
return file.preview;
}
if (file.url) {
return encodeFileURL(`${file.url}${file.thumbnailRule || ''}`);
return file.url;
}
if (file.originFileObj) {
return URL.createObjectURL(file.originFileObj);
@ -65,9 +64,9 @@ attachmentFileTypes.add({
return (
<LightBox
// discourageDownloads={true}
mainSrc={encodeFileURL(list[index]?.url)}
nextSrc={encodeFileURL(list[(index + 1) % list.length]?.url)}
prevSrc={encodeFileURL(list[(index + list.length - 1) % list.length]?.url)}
mainSrc={list[index]?.url}
nextSrc={list[(index + 1) % list.length]?.url}
prevSrc={list[(index + list.length - 1) % list.length]?.url}
onCloseRequest={() => onSwitchIndex(null)}
onMovePrevRequest={() => onSwitchIndex((index + list.length - 1) % list.length)}
onMoveNextRequest={() => onSwitchIndex((index + 1) % list.length)}
@ -95,7 +94,7 @@ const iframePreviewSupportedTypes = ['application/pdf', 'audio/*', 'image/*', 'v
function IframePreviewer({ index, list, onSwitchIndex }) {
const { t } = useTranslation();
const file = list[index];
const url = encodeFileURL(file.url);
const url = file.url;
const onOpen = useCallback(
(e) => {
e.preventDefault();
@ -264,7 +263,7 @@ function AttachmentListItem(props) {
) : null,
];
const wrappedItem = file.url ? (
<a target="_blank" rel="noopener noreferrer" href={encodeFileURL(file.url)} onClick={handleClick}>
<a target="_blank" rel="noopener noreferrer" href={file.url} onClick={handleClick}>
{item}
</a>
) : (

View File

@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
export { attachmentFileTypes, encodeFileURL } from './shared';
export { attachmentFileTypes } from './shared';
export { useUploadStyles } from './style';
export * from './Upload';

View File

@ -267,20 +267,3 @@ export function useBeforeUpload(rules) {
[rules],
);
}
export function encodeFileURL(url: string): string {
if (!url) {
return url;
}
if (url.includes('X-Amz-Content-Sha256')) {
return url;
}
const [base, search = ''] = url.split('?');
const parts = base.split('/');
const filename = parts.pop();
parts.push(encodeURIComponent(filename));
const encodedURL = `${parts.join('/')}${search ? `?${search}` : ''}`;
return encodedURL;
}

View File

@ -7,14 +7,14 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { BaseInterface } from '@nocobase/database';
import { createMockServer, MockServer } from '@nocobase/test';
import { uid } from '@nocobase/utils';
import { XlsxExporter } from '../services/xlsx-exporter';
import XLSX from 'xlsx';
import fs from 'fs';
import path from 'path';
import { BaseInterface } from '@nocobase/database';
import moment from 'moment';
import path from 'path';
import XLSX from 'xlsx';
import { XlsxExporter } from '../services/xlsx-exporter';
XLSX.set_fs(fs);
@ -391,13 +391,14 @@ describe('export to xlsx with preset', () => {
extname: '.png',
mimetype: 'image/png',
url: 'https://nocobase.oss-cn-beijing.aliyuncs.com/test1.png',
storageId: 1,
},
{
title: 'nocobase-logo2',
filename: '682e5ad037dd02a0fe4800a3e91c283b.png',
extname: '.png',
mimetype: 'image/png',
url: 'https://nocobase.oss-cn-beijing.aliyuncs.com/test2.png',
storageId: 1,
},
],
},
@ -432,7 +433,7 @@ describe('export to xlsx with preset', () => {
const firstUser = sheetData[1];
expect(firstUser[1]).toEqual(
'https://nocobase.oss-cn-beijing.aliyuncs.com/test1.png,https://nocobase.oss-cn-beijing.aliyuncs.com/test2.png',
'https://nocobase.oss-cn-beijing.aliyuncs.com/test1.png,/storage/uploads/682e5ad037dd02a0fe4800a3e91c283b.png',
);
} finally {
fs.unlinkSync(xlsxFilePath);

View File

@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { encodeFileURL, useAPIClient, useApp, withDynamicSchemaProps } from '@nocobase/client';
import { useAPIClient, useApp, withDynamicSchemaProps } from '@nocobase/client';
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import Vditor from 'vditor';
import { defaultToolbar } from '../interfaces/markdown-vditor';
@ -81,7 +81,7 @@ export const Edit = withDynamicSchemaProps((props) => {
data: {
errFiles: [],
succMap: {
[response.data.filename]: encodeFileURL(response.data.url),
[response.data.filename]: response.data.url,
},
},
};

View File

@ -10,7 +10,7 @@
"homepage": "https://docs.nocobase.com/handbook/file-manager",
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/file-manager",
"devDependencies": {
"@aws-sdk/client-s3": "^3.750.0",
"@aws-sdk/client-s3": "3.750.0",
"@formily/antd-v5": "1.x",
"@formily/core": "2.x",
"@formily/react": "2.x",
@ -29,7 +29,8 @@
"multer-s3": "^3.0.1",
"react": "^18.2.0",
"react-i18next": "^11.15.1",
"supertest": "^6.1.6"
"supertest": "^6.1.6",
"url-join": "4.0.1"
},
"peerDependencies": {
"@nocobase/actions": "1.x",

View File

@ -1,34 +0,0 @@
/**
* 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.
*/
import { Model } from '@nocobase/database';
import { STORAGE_TYPE_ALI_OSS, STORAGE_TYPE_LOCAL, STORAGE_TYPE_S3, STORAGE_TYPE_TX_COS } from '../constants';
const currentStorage = [STORAGE_TYPE_LOCAL, STORAGE_TYPE_ALI_OSS, STORAGE_TYPE_S3, STORAGE_TYPE_TX_COS];
export class FileModel extends Model {
public toJSON() {
const json = super.toJSON();
const fileStorages = this.constructor['database']?.['_fileStorages'];
if (json.storageId && fileStorages && fileStorages.has(json.storageId)) {
const storage = fileStorages.get(json.storageId);
// 当前文件管理器内的存储类型拼接生成预览链接,其他文件存储自行处理
if (currentStorage.includes(storage?.type) && storage?.options?.thumbnailRule) {
json['preview'] = `${json['url']}${storage?.options?.thumbnailRule || ''}`;
}
if (storage?.options?.thumbnailRule) {
json['thumbnailRule'] = storage?.options?.thumbnailRule;
}
if (storage?.type === 'local' && process.env.APP_PUBLIC_PATH) {
json['url'] = process.env.APP_PUBLIC_PATH.replace(/\/$/g, '') + json.url;
}
}
return json;
}
}

View File

@ -12,6 +12,7 @@ import path from 'path';
import querystring from 'querystring';
import { getApp } from '.';
import { FILE_FIELD_NAME, FILE_SIZE_LIMIT_DEFAULT, STORAGE_TYPE_LOCAL } from '../../constants';
import PluginFileManagerServer from '../server';
const { LOCAL_STORAGE_BASE_URL, LOCAL_STORAGE_DEST = 'storage/uploads', APP_PORT = '13000' } = process.env;
@ -52,6 +53,88 @@ describe('action', () => {
describe('create / upload', () => {
describe('default storage', () => {
it('should be create file record', async () => {
const Plugin = app.pm.get(PluginFileManagerServer) as PluginFileManagerServer;
const model = await Plugin.createFileRecord({
collectionName: 'attachments',
filePath: path.resolve(__dirname, './files/text.txt'),
});
const matcher = {
title: 'text',
extname: '.txt',
path: '',
// size: 13,
meta: {},
storageId: 1,
};
expect(model.toJSON()).toMatchObject(matcher);
});
it('should be local2 storage', async () => {
const storage = await StorageRepo.create({
values: {
name: 'local2',
type: STORAGE_TYPE_LOCAL,
baseUrl: DEFAULT_LOCAL_BASE_URL,
rules: {
size: 1024,
},
paranoid: true,
},
});
const Plugin = app.pm.get(PluginFileManagerServer) as PluginFileManagerServer;
const model = await Plugin.createFileRecord({
collectionName: 'attachments',
storageName: 'local2',
filePath: path.resolve(__dirname, './files/text.txt'),
});
const matcher = {
title: 'text',
extname: '.txt',
path: '',
// size: 13,
meta: {},
storageId: storage.id,
};
expect(model.toJSON()).toMatchObject(matcher);
});
it('should be custom values', async () => {
const Plugin = app.pm.get(PluginFileManagerServer) as PluginFileManagerServer;
const model = await Plugin.createFileRecord({
collectionName: 'attachments',
filePath: path.resolve(__dirname, './files/text.txt'),
values: {
size: 22,
},
});
const matcher = {
title: 'text',
extname: '.txt',
path: '',
size: 22,
meta: {},
storageId: 1,
};
expect(model.toJSON()).toMatchObject(matcher);
});
it('should be upload file', async () => {
const Plugin = app.pm.get(PluginFileManagerServer) as PluginFileManagerServer;
const data = await Plugin.uploadFile({
filePath: path.resolve(__dirname, './files/text.txt'),
documentRoot: 'storage/backups/test',
});
const matcher = {
title: 'text',
extname: '.txt',
path: '',
meta: {},
storageId: 1,
};
expect(data).toMatchObject(matcher);
});
it('upload file should be ok', async () => {
const { body } = await agent.resource('attachments').create({
[FILE_FIELD_NAME]: path.resolve(__dirname, './files/text.txt'),
@ -125,10 +208,9 @@ describe('action', () => {
// 文件上传和解析是否正常
expect(body.data).toMatchObject(matcher);
// 文件的 url 是否正常生成
expect(body.data.url).toBe(`${DEFAULT_LOCAL_BASE_URL}${body.data.path}/${body.data.filename}`);
const encodedFilename = querystring.escape(rawText);
expect(body.data.url).toContain(`${DEFAULT_LOCAL_BASE_URL}${body.data.path}/${encodedFilename}`);
const encodedFilename = querystring.escape(rawFilename);
// console.log('-----------', body.data, encodedFilename);
// 文件的 url 是否正常访问
// TODO: mock-server is not start within gateway, static url can not be accessed
// const res2 = await agent.get(`${DEFAULT_LOCAL_BASE_URL}${body.data.path}/${encodedFilename}`);
@ -192,7 +274,7 @@ describe('action', () => {
});
it('upload to storage which is not default', async () => {
const BASE_URL = `http://localhost:${APP_PORT}/storage/uploads/another`;
const BASE_URL = `/storage/uploads/another`;
const urlPath = 'test/path';
// 动态添加 storage
@ -236,7 +318,7 @@ describe('action', () => {
});
it('path longer than 255', async () => {
const BASE_URL = `http://localhost:${APP_PORT}/storage/uploads/another`;
const BASE_URL = `/storage/uploads/another`;
const urlPath =
'extreme-test/max-long-path-1234567890-1234567890-1234567890-1234567890-1234567890-1234567890-1234567890-1234567890-1234567890-1234567890-1234567890-1234567890-1234567890-1234567890-1234567890-1234567890-1234567890-1234567890-1234567890-1234567890-1234567890-1234567890';

View File

@ -12,7 +12,7 @@ import path from 'path';
import { getApp } from '.';
import PluginFileManagerServer from '../server';
import { STORAGE_TYPE_LOCAL, FILE_FIELD_NAME } from '../../constants';
import { FILE_FIELD_NAME, STORAGE_TYPE_LOCAL } from '../../constants';
const { LOCAL_STORAGE_BASE_URL, LOCAL_STORAGE_DEST = 'storage/uploads', APP_PORT = '13000' } = process.env;
const DEFAULT_LOCAL_BASE_URL = LOCAL_STORAGE_BASE_URL || `/storage/uploads`;
@ -131,23 +131,48 @@ describe('file manager > server', () => {
describe('getFileURL', () => {
it('local attachment without env', async () => {
const { body } = await agent.resource('attachments').create({
[FILE_FIELD_NAME]: path.resolve(__dirname, './files/text.txt'),
[FILE_FIELD_NAME]: path.resolve(__dirname, './files/[]中文报告! 1%~50.4% (123) {$#}.txt'),
});
const url = await plugin.getFileURL(body.data);
expect(url).toBe(`${process.env.APP_PUBLIC_PATH?.replace(/\/$/g, '') || ''}${body.data.url}`);
});
it('local attachment with env', async () => {
it('local (default with base url) attachment with env', async () => {
const originalPath = process.env.APP_PUBLIC_PATH;
process.env.APP_PUBLIC_PATH = 'http://localhost';
process.env.APP_PUBLIC_PATH = '/app';
const { body } = await agent.resource('attachments').create({
[FILE_FIELD_NAME]: path.resolve(__dirname, './files/text.txt'),
});
const url = await plugin.getFileURL(body.data);
expect(url).toBe(`http://localhost${body.data.url}`);
expect(url).toBe(`${process.env.APP_PUBLIC_PATH}/storage/uploads/${body.data.filename}`);
process.env.APP_PUBLIC_PATH = originalPath;
});
it('local (default without base url) attachment with env', async () => {
const storage = await StorageRepo.create({
values: {
name: 'local2',
type: STORAGE_TYPE_LOCAL,
rules: {
size: 1024,
},
default: true,
},
});
const originalPath = process.env.APP_PUBLIC_PATH;
process.env.APP_PUBLIC_PATH = '/nocobase/';
const { body } = await agent.resource('attachments').create({
[FILE_FIELD_NAME]: path.resolve(__dirname, './files/text.txt'),
});
const url = await plugin.getFileURL(body.data);
expect(url).toBe(`/nocobase/${body.data.filename}`);
process.env.APP_PUBLIC_PATH = originalPath;
});

View File

@ -28,38 +28,41 @@ function getFileFilter(storage) {
};
}
export function getFileData(ctx: Context) {
export async function getFileData(ctx: Context) {
const { [FILE_FIELD_NAME]: file, storage } = ctx;
if (!file) {
return ctx.throw(400, 'file validation failed');
}
const StorageType = ctx.app.pm.get(Plugin).storageTypes.get(storage.type) as StorageClassType;
const plugin = ctx.app.pm.get(Plugin);
const StorageType = plugin.storageTypes.get(storage.type) as StorageClassType;
const { [StorageType.filenameKey || 'filename']: name } = file;
// make compatible filename across cloud service (with path)
const filename = Path.basename(name);
const extname = Path.extname(filename);
const path = (storage.path || '').replace(/^\/|\/$/g, '');
const baseUrl = storage.baseUrl.replace(/\/+$/, '');
const pathname = [path, filename].filter(Boolean).join('/');
const storageInstance = new StorageType(storage);
let storageInstance = plugin.storagesCache.get(storage.id);
return {
if (!storageInstance) {
await plugin.loadStorages();
storageInstance = plugin.storagesCache.get(storage.id);
}
const data = {
title: Buffer.from(file.originalname, 'latin1').toString('utf8').replace(extname, ''),
filename,
extname,
// TODO(feature): 暂时两者相同,后面 storage.path 模版化以后,这里只是 file 实际的 path
path,
size: file.size,
// 直接缓存起来
url: `${baseUrl}/${pathname}`,
mimetype: file.mimetype,
// @ts-ignore
meta: ctx.request.body,
storageId: storage.id,
...(storageInstance.getFileData ? storageInstance.getFileData(file) : {}),
};
return data;
}
async function multipart(ctx: Context, next: Next) {
@ -98,7 +101,7 @@ async function multipart(ctx: Context, next: Next) {
return ctx.throw(500, err);
}
const values = getFileData(ctx);
const values = await getFileData(ctx);
ctx.action.mergeParams({
values,

View File

@ -18,4 +18,22 @@ export default function ({ app }) {
});
app.resourcer.use(createMiddleware, { tag: 'createMiddleware', after: 'auth' });
app.resourcer.registerActionHandler('upload', actions.create);
app.resourcer.use(
async (ctx, next) => {
await next();
try {
const { resourceName, actionName } = ctx.action;
const collection = ctx.db.getCollection(resourceName);
if (collection?.options?.template !== 'file') {
return;
}
if (actionName === 'create' || actionName === 'upload') {
ctx.body = await ctx.body.reload();
}
} catch (error) {
console.log(error);
}
},
{ before: 'dataWrapping' },
);
}

View File

@ -8,14 +8,13 @@
*/
import { Plugin } from '@nocobase/server';
import { Registry } from '@nocobase/utils';
import { isURL, Registry } from '@nocobase/utils';
import { basename } from 'path';
import { Model, Transactionable } from '@nocobase/database';
import fs from 'fs';
import { STORAGE_TYPE_ALI_OSS, STORAGE_TYPE_LOCAL, STORAGE_TYPE_S3, STORAGE_TYPE_TX_COS } from '../constants';
import { FileModel } from './FileModel';
import initActions from './actions';
import { getFileData } from './actions/attachments';
import { AttachmentInterface } from './interfaces/attachment-interface';
@ -158,7 +157,6 @@ export class PluginFileManagerServer extends Plugin {
for (const storage of storages) {
this.storagesCache.set(storage.get('id'), this.parseStorage(storage));
}
this.db['_fileStorages'] = this.storagesCache;
}
async install() {
@ -201,7 +199,7 @@ export class PluginFileManagerServer extends Plugin {
}
async beforeLoad() {
this.db.registerModels({ FileModel });
this.db.registerModels({ FileModel: Model });
this.db.on('beforeDefineCollection', (options) => {
if (options.template === 'file') {
options.model = 'FileModel';
@ -279,12 +277,38 @@ export class PluginFileManagerServer extends Plugin {
this.app.acl.addFixedParams('attachments', 'destroy', ownMerger);
this.app.db.interfaceManager.registerInterfaceType('attachment', AttachmentInterface);
this.db.on('afterFind', async (instances) => {
if (!instances) {
return;
}
const records = Array.isArray(instances) ? instances : [instances];
const name = records[0]?.constructor?.name;
if (name) {
const collection = this.db.getCollection(name);
if (collection?.name === 'attachments' || collection?.options?.template === 'file') {
for (const record of records) {
const url = await this.getFileURL(record);
const previewUrl = await this.getFileURL(record, true);
record.set('url', url);
record.set('preview', previewUrl);
record.dataValues.preview = previewUrl; // 强制添加preview在附件字段时通过set设置无效
}
}
}
});
}
getFileURL(file: AttachmentModel) {
async getFileURL(file: AttachmentModel, preview = false) {
if (!file.storageId) {
return file.url;
}
const storage = this.storagesCache.get(file.storageId);
if (!storage) {
return file.url;
}
const storageType = this.storageTypes.get(storage.type);
return new storageType(storage).getFileURL(file);
return new storageType(storage).getFileURL(file, preview ? storage.options.thumbnailRule : '');
}
}

View File

@ -7,8 +7,9 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import Application from '@nocobase/server';
import { isURL } from '@nocobase/utils';
import { StorageEngine } from 'multer';
import urlJoin from 'url-join';
export interface StorageModel {
id?: number;
@ -41,9 +42,22 @@ export abstract class StorageType {
abstract delete(records: AttachmentModel[]): [number, AttachmentModel[]] | Promise<[number, AttachmentModel[]]>;
getFileData?(file: { [key: string]: any }): { [key: string]: any };
getFileURL(file: AttachmentModel): string | Promise<string> {
getFileURL(file: AttachmentModel, preview?: boolean): string | Promise<string> {
// 兼容历史数据
if (file.url && isURL(file.url)) {
if (preview) {
return file.url + (this.storage.options.thumbnailRule || '');
}
return file.url;
}
const keys = [
this.storage.baseUrl,
file.path && encodeURI(file.path),
encodeURIComponent(file.filename),
preview && this.storage.options.thumbnailRule,
].filter(Boolean);
return urlJoin(keys);
}
}
export type StorageClassType = { new (storage: StorageModel): StorageType } & typeof StorageType;

View File

@ -7,14 +7,18 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { isURL } from '@nocobase/utils';
import fs from 'fs/promises';
import mkdirp from 'mkdirp';
import multer from 'multer';
import path from 'path';
import urlJoin from 'url-join';
import { AttachmentModel, StorageType } from '.';
import { FILE_SIZE_LIMIT_DEFAULT, STORAGE_TYPE_LOCAL } from '../../constants';
import { getFilename } from '../utils';
const DEFAULT_BASE_URL = '/storage/uploads';
function getDocumentRoot(storage): string {
const { documentRoot = process.env.LOCAL_STORAGE_DEST || 'storage/uploads' } = storage.options || {};
// TODO(feature): 后面考虑以字符串模板的方式使用,可注入 req/action 相关变量,以便于区分文件夹
@ -27,7 +31,7 @@ export default class extends StorageType {
title: 'Local storage',
type: STORAGE_TYPE_LOCAL,
name: `local`,
baseUrl: '/storage/uploads',
baseUrl: DEFAULT_BASE_URL,
options: {
documentRoot: 'storage/uploads',
},
@ -72,7 +76,11 @@ export default class extends StorageType {
return [count, undeleted];
}
getFileURL(file: AttachmentModel) {
return process.env.APP_PUBLIC_PATH ? `${process.env.APP_PUBLIC_PATH.replace(/\/$/g, '')}${file.url}` : file.url;
async getFileURL(file: AttachmentModel, preview = false) {
const url = await super.getFileURL(file, preview);
if (isURL(url)) {
return url;
}
return urlJoin(process.env.APP_PUBLIC_PATH, url);
}
}

View File

@ -7,8 +7,8 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { DeleteObjectsCommand } from '@aws-sdk/client-s3';
import { md5 } from '@nocobase/utils';
import { S3Client, DeleteObjectCommand } from '@aws-sdk/client-s3';
import crypto from 'crypto';
import { AttachmentModel, StorageType } from '.';
import { STORAGE_TYPE_S3 } from '../../constants';
import { cloudFilenameGetter, getFileKey } from '../utils';
@ -32,7 +32,6 @@ export default class extends StorageType {
static filenameKey = 'key';
make() {
const { S3Client } = require('@aws-sdk/client-s3');
const multerS3 = require('multer-s3');
const { accessKeyId, secretAccessKey, bucket, acl = 'public-read', ...options } = this.storage.options;
if (options.endpoint) {
@ -64,27 +63,25 @@ export default class extends StorageType {
});
}
calculateContentMD5(body) {
const hash = crypto.createHash('md5').update(body).digest('base64');
return hash;
}
async deleteS3Objects(bucketName: string, objects: string[]) {
const deleteBody = JSON.stringify({
Objects: objects.map((objectKey) => ({ Key: objectKey })),
});
const contentMD5 = md5(deleteBody);
const deleteCommand = new DeleteObjectsCommand({
Bucket: bucketName,
Delete: JSON.parse(deleteBody),
});
deleteCommand.middlewareStack.add(
(next, context) => async (args) => {
args.request['headers']['Content-Md5'] = contentMD5;
return next(args);
},
{ step: 'build' },
);
const { s3 } = this.make();
return await s3.send(deleteCommand);
const Deleted = [];
for (const Key of objects) {
const deleteCommand = new DeleteObjectCommand({
Bucket: bucketName,
Key,
});
await s3.send(deleteCommand);
Deleted.push({ Key });
}
return {
Deleted,
};
}
async delete(records: AttachmentModel[]): Promise<[number, AttachmentModel[]]> {

View File

@ -12,5 +12,6 @@ import { defineCollection } from '@nocobase/database';
export default defineCollection({
name: 'uiButtonSchemasRoles',
dumpRules: 'required',
autoGenId: false,
migrationRules: ['overwrite', 'schema-only'],
});