Feat/support s3 storage next (#6096)

* feat: change upload components

* feat: restore original component in schema, support useUploadProps register

* feat: s3 upload support relation fileds

* feat: export buildin storages

* chore: remove invalid file

* fix: change getStorage action name

* fix: change action name getDesensitizedStorage to getDesensitized

* fix: add storage get action

* fix: remove useStorageRules

* fix: change action get to getBasicInfo

* fix: adjust server test case
This commit is contained in:
Jiannx 2025-01-20 14:34:43 +08:00 committed by GitHub
parent f3c1761840
commit 7ccf088a06
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 149 additions and 38 deletions

View File

@ -85,9 +85,10 @@ const useTableSelectorProps = () => {
function FileSelector(props) {
const { disabled, multiple, value, onChange, action, onSelect, quickUpload, selectFile, ...other } = props;
const { wrapSSR, hashId, componentCls: prefixCls } = useStyles();
const { useFileCollectionStorageRules } = useExpressionScope();
const { useFileCollectionStorageRules, useAttachmentFieldProps } = useExpressionScope();
const { t } = useTranslation();
const rules = useFileCollectionStorageRules();
const attachmentFieldProps = useAttachmentFieldProps();
// 兼容旧版本
const showSelectButton = selectFile === undefined && quickUpload === undefined;
return wrapSSR(
@ -116,6 +117,7 @@ function FileSelector(props) {
) : null}
{quickUpload ? (
<Uploader
{...attachmentFieldProps}
value={value}
multiple={multiple}
// onRemove={handleRemove}

View File

@ -41,7 +41,13 @@ attachmentFileTypes.add({
return matchMimetype(file, 'image/*');
},
getThumbnailURL(file) {
return file.url ? `${file.url}${file.thumbnailRule || ''}` : URL.createObjectURL(file.originFileObj);
if (file.url) {
return `${file.url}${file.thumbnailRule || ''}`;
}
if (file.originFileObj) {
return URL.createObjectURL(file.originFileObj);
}
return null;
},
Previewer({ index, list, onSwitchIndex }) {
const onDownload = useCallback(

View File

@ -147,7 +147,6 @@ export function useUploadProps<T extends IUploadProps = UploadProps>(props: T) {
const api = useAPIClient();
return {
...props,
// in customRequest method can't modify form's status(e.g: form.disabled=true )
// that will be trigger Upload componentactual Underlying is AjaxUploader component 's componentWillUnmount method
// which will cause multiple files upload fail
@ -180,6 +179,7 @@ export function useUploadProps<T extends IUploadProps = UploadProps>(props: T) {
},
};
},
...props,
};
}

View File

@ -10,6 +10,7 @@
import { useEffect } from 'react';
import { useField } from '@formily/react';
import { useAPIClient, useCollectionField, useCollectionManager, useRequest } from '@nocobase/client';
import { useStorageUploadProps } from './useStorageUploadProps';
export function useStorageRules(storage) {
const name = storage ?? '';
@ -17,7 +18,7 @@ export function useStorageRules(storage) {
const field = useField<any>();
const { loading, data, run } = useRequest<any>(
{
url: `storages:getRules/${name}`,
url: `storages:getBasicInfo/${name}`,
},
{
manual: true,
@ -31,17 +32,16 @@ export function useStorageRules(storage) {
}
run();
}, [field.pattern, run]);
return (!loading && data?.data) || null;
return (!loading && data?.data?.rules) || null;
}
export function useAttachmentFieldProps() {
const field = useCollectionField();
const rules = useStorageRules(field?.storage);
return {
rules,
action: `${field.target}:create${field.storage ? `?attachmentField=${field.collectionName}.${field.name}` : ''}`,
};
const action = `${field.target}:create${
field.storage ? `?attachmentField=${field.collectionName}.${field.name}` : ''
}`;
const storageUploadProps = useStorageUploadProps({ action });
return { action, ...storageUploadProps };
}
export function useFileCollectionStorageRules() {

View File

@ -0,0 +1,67 @@
/**
* 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 {
Input,
Upload,
useCollection,
useCollectionField,
useCollectionManager,
useCollectionRecordData,
usePlugin,
useRequest,
withDynamicSchemaProps,
} from '@nocobase/client';
import React, { useEffect } from 'react';
import { connect, mapProps, mapReadPretty, useField } from '@formily/react';
import FileManagerPlugin from '../';
export function useStorage(storage) {
const name = storage ?? '';
const url = `storages:getBasicInfo/${name}`;
const { loading, data, run } = useRequest<any>(
{
url,
},
{
manual: true,
refreshDeps: [name],
cacheKey: url,
},
);
useEffect(() => {
run();
}, [run]);
return (!loading && data?.data) || null;
}
export function useStorageCfg() {
const field = useCollectionField();
const cm = useCollectionManager();
const targetCollection = cm.getCollection(field?.target);
const collection = useCollection();
const plugin = usePlugin(FileManagerPlugin);
const storage = useStorage(
field?.storage || collection?.getOption('storage') || targetCollection?.getOption('storage'),
);
const storageType = plugin.getStorageType(storage?.type);
return {
storage,
storageType,
};
}
export function useStorageUploadProps(props) {
const { storage, storageType } = useStorageCfg();
const useStorageTypeUploadProps = storageType?.useUploadProps;
const storageTypeUploadProps = useStorageTypeUploadProps?.({ storage, rules: storage.rules, ...props }) || {};
return {
rules: storage?.rules,
...storageTypeUploadProps,
};
}

View File

@ -16,7 +16,7 @@ import {
useSourceId,
} from '@nocobase/client';
import { useContext, useMemo } from 'react';
import { useStorageRules } from './useStorageRules';
import { useStorageUploadProps } from './useStorageUploadProps';
export const useUploadFiles = () => {
const { getDataBlockRequest } = useDataBlockRequestGetter();
@ -24,7 +24,6 @@ export const useUploadFiles = () => {
const { setVisible } = useActionContext();
const collection = useCollection();
const sourceId = useSourceId();
const rules = useStorageRules(collection?.getOption('storage'));
const action = useMemo(() => {
let action = `${collection.name}:create`;
if (association) {
@ -38,7 +37,7 @@ export const useUploadFiles = () => {
let pendingNumber = 0;
return {
const uploadProps = {
action,
onChange(fileList) {
fileList.forEach((file) => {
@ -62,6 +61,11 @@ export const useUploadFiles = () => {
setVisible(false);
}
},
rules,
};
const storageUploadProps = useStorageUploadProps(uploadProps);
return {
...uploadProps,
...storageUploadProps,
};
};

View File

@ -16,8 +16,11 @@ import { AttachmentFieldInterface } from './interfaces/attachment';
import { FileCollectionTemplate } from './templates';
import { useAttachmentFieldProps, useFileCollectionStorageRules } from './hooks';
import { FileSizeField } from './FileSizeField';
import { STORAGE_TYPE_ALI_OSS, STORAGE_TYPE_LOCAL, STORAGE_TYPE_S3, STORAGE_TYPE_TX_COS } from '../constants';
export class PluginFileManagerClient extends Plugin {
// refer by plugin-field-attachment-url
static buildInStorage = [STORAGE_TYPE_LOCAL, STORAGE_TYPE_ALI_OSS, STORAGE_TYPE_S3, STORAGE_TYPE_TX_COS];
storageTypes = new Map();
async load() {
@ -65,6 +68,10 @@ export class PluginFileManagerClient extends Plugin {
registerStorageType(name: string, options) {
this.storageTypes.set(name, options);
}
getStorageType(name: string) {
return this.storageTypes.get(name);
}
}
export default PluginFileManagerClient;

View File

@ -35,6 +35,7 @@ describe('action', () => {
local1 = await StorageRepo.create({
values: {
name: 'local1',
title: 'local1',
type: STORAGE_TYPE_LOCAL,
baseUrl: DEFAULT_LOCAL_BASE_URL,
rules: {
@ -478,34 +479,40 @@ describe('action', () => {
});
describe('storage actions', () => {
describe('getRules', () => {
it('get rules without key as default storage', async () => {
const { body, status } = await agent.resource('storages').getRules();
describe('getBasicInfo', () => {
it('get default storage', async () => {
const { body, status } = await agent.resource('storages').getBasicInfo();
expect(status).toBe(200);
expect(body.data).toEqual({ size: FILE_SIZE_LIMIT_DEFAULT });
expect(body.data).toMatchObject({ id: 1 });
});
it('get rules by storage id as default rules', async () => {
const { body, status } = await agent.resource('storages').getRules({ filterByTk: 1 });
expect(status).toBe(200);
expect(body.data).toEqual({ size: FILE_SIZE_LIMIT_DEFAULT });
});
it('get rules by unexisted id as 404', async () => {
const { body, status } = await agent.resource('storages').getRules({ filterByTk: -1 });
it('get storage by unexisted id as 404', async () => {
const { body, status } = await agent.resource('storages').getBasicInfo({ filterByTk: -1 });
expect(status).toBe(404);
});
it('get rules by storage id', async () => {
const { body, status } = await agent.resource('storages').getRules({ filterByTk: local1.id });
it('get by storage local id', async () => {
const { body, status } = await agent.resource('storages').getBasicInfo({ filterByTk: local1.id });
expect(status).toBe(200);
expect(body.data).toMatchObject({ size: 1024 });
expect(body.data).toMatchObject({
id: local1.id,
title: local1.title,
name: local1.name,
type: local1.type,
rules: local1.rules,
});
});
it('get rules by storage name', async () => {
const { body, status } = await agent.resource('storages').getRules({ filterByTk: local1.name });
it('get storage by name', async () => {
const { body, status } = await agent.resource('storages').getBasicInfo({ filterByTk: local1.name });
expect(status).toBe(200);
expect(body.data).toMatchObject({ size: 1024 });
expect(body.data).toMatchObject({
id: local1.id,
title: local1.title,
name: local1.name,
type: local1.type,
rules: local1.rules,
});
});
});
});

View File

@ -126,8 +126,11 @@ export async function createMiddleware(ctx: Context, next: Next) {
const storage = await StorageRepo.findOne({ filter: storageName ? { name: storageName } : { default: true } });
ctx.storage = storage;
await multipart(ctx, next);
if (ctx?.request.is('multipart/*')) {
await multipart(ctx, next);
} else {
await next();
}
}
export async function destroyMiddleware(ctx: Context, next: Next) {

View File

@ -1,6 +1,15 @@
/**
* 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 Plugin from '..';
export async function getRules(context, next) {
export async function getBasicInfo(context, next) {
const { storagesCache } = context.app.pm.get(Plugin) as Plugin;
let result;
const { filterByTk } = context.action.params;
@ -15,7 +24,13 @@ export async function getRules(context, next) {
if (!result) {
return context.throw(404);
}
context.body = result.rules;
context.body = {
id: result.id,
title: result.title,
name: result.name,
type: result.type,
rules: result.rules,
};
next();
}

View File

@ -232,7 +232,7 @@ export class PluginFileManagerServer extends Plugin {
this.app.acl.allow('attachments', 'upload', 'loggedIn');
this.app.acl.allow('attachments', 'create', 'loggedIn');
this.app.acl.allow('storages', 'getRules', 'loggedIn');
this.app.acl.allow('storages', 'getBasicInfo', 'loggedIn');
// this.app.resourcer.use(uploadMiddleware);
// this.app.resourcer.use(createAction);