fix(plugin-file-manager): fix file issues (#6436)

* fix(plugin-file-manager): fix file issues

* fix(plugin-file-manager): fix special char in windows
This commit is contained in:
Junyi 2025-03-12 23:35:14 +08:00 committed by GitHub
parent c408c916d7
commit cc0e13dce0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 81 additions and 29 deletions

View File

@ -17,7 +17,7 @@ server {
server_name _; server_name _;
root /app/nocobase/packages/app/client/dist; root /app/nocobase/packages/app/client/dist;
index index.html; index index.html;
client_max_body_size 20M; client_max_body_size 0;
access_log /var/log/nginx/nocobase.log apm; access_log /var/log/nginx/nocobase.log apm;

View File

@ -17,7 +17,7 @@ server {
server_name _; server_name _;
root /app/nocobase/node_modules/@nocobase/app/dist/client; root /app/nocobase/node_modules/@nocobase/app/dist/client;
index index.html; index index.html;
client_max_body_size 1000M; client_max_body_size 0;
access_log /var/log/nginx/nocobase.log apm; access_log /var/log/nginx/nocobase.log apm;
gzip on; gzip on;

View File

@ -17,7 +17,7 @@ server {
server_name _; server_name _;
root {{cwd}}/node_modules/@nocobase/app/dist/client; root {{cwd}}/node_modules/@nocobase/app/dist/client;
index index.html; index index.html;
client_max_body_size 1000M; client_max_body_size 0;
access_log /var/log/nginx/nocobase.log apm; access_log /var/log/nginx/nocobase.log apm;
gzip on; gzip on;

View File

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

View File

@ -32,6 +32,7 @@ import {
toValueItem as toValueItemDefault, toValueItem as toValueItemDefault,
useBeforeUpload, useBeforeUpload,
useUploadProps, useUploadProps,
encodeFileURL,
} from './shared'; } from './shared';
import { useStyles } from './style'; import { useStyles } from './style';
import type { ComposedUpload, DraggerProps, DraggerV2Props, UploadProps } from './type'; import type { ComposedUpload, DraggerProps, DraggerV2Props, UploadProps } from './type';
@ -89,26 +90,27 @@ attachmentFileTypes.add({
}, },
}); });
const iframePreviewSupportedTypes = ['application/pdf', 'audio/*', 'image/*', 'video/*']; const iframePreviewSupportedTypes = ['application/pdf', 'audio/*', 'image/*', 'video/*', 'text/*'];
function IframePreviewer({ index, list, onSwitchIndex }) { function IframePreviewer({ index, list, onSwitchIndex }) {
const { t } = useTranslation(); const { t } = useTranslation();
const file = list[index]; const file = list[index];
const url = encodeFileURL(file.url);
const onOpen = useCallback( const onOpen = useCallback(
(e) => { (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
window.open(file.url); window.open(url);
}, },
[file], [url],
); );
const onDownload = useCallback( const onDownload = useCallback(
(e) => { (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
saveAs(file.url, `${file.title}${file.extname}`); saveAs(url, `${file.title}${file.extname}`);
}, },
[file], [file.extname, file.title, url],
); );
const onClose = useCallback(() => { const onClose = useCallback(() => {
onSwitchIndex(null); onSwitchIndex(null);
@ -148,7 +150,7 @@ function IframePreviewer({ index, list, onSwitchIndex }) {
> >
{iframePreviewSupportedTypes.some((type) => matchMimetype(file, type)) ? ( {iframePreviewSupportedTypes.some((type) => matchMimetype(file, type)) ? (
<iframe <iframe
src={file.url} src={url}
style={{ style={{
width: '100%', width: '100%',
maxHeight: '90vh', maxHeight: '90vh',
@ -390,7 +392,7 @@ export function Uploader({ rules, ...props }: UploadProps) {
} else { } else {
field.setFeedback({}); field.setFeedback({});
} }
}, [field, pendingList]); }, [field, pendingList, t]);
const onUploadChange = useCallback( const onUploadChange = useCallback(
(info) => { (info) => {

View File

@ -267,3 +267,15 @@ export function useBeforeUpload(rules) {
[rules], [rules],
); );
} }
export function encodeFileURL(url: string): string {
if (!url) {
return url;
}
const parts = url.split('/');
const filename = parts.pop();
parts.push(encodeURIComponent(filename));
const encodedURL = parts.join('/');
return encodedURL;
}

View File

@ -20,7 +20,6 @@
"@types/multer": "^1.4.5", "@types/multer": "^1.4.5",
"antd": "5.x", "antd": "5.x",
"cos-nodejs-sdk-v5": "^2.11.14", "cos-nodejs-sdk-v5": "^2.11.14",
"iconv-lite": "^0.6.3",
"koa-static": "^5.0.0", "koa-static": "^5.0.0",
"mime-match": "^1.0.2", "mime-match": "^1.0.2",
"mkdirp": "~0.5.4", "mkdirp": "~0.5.4",

View File

@ -10,7 +10,7 @@
export const FILE_FIELD_NAME = 'file'; export const FILE_FIELD_NAME = 'file';
export const LIMIT_FILES = 1; export const LIMIT_FILES = 1;
export const FILE_SIZE_LIMIT_MIN = 1; export const FILE_SIZE_LIMIT_MIN = 1;
export const FILE_SIZE_LIMIT_MAX = 1024 * 1024 * 1024; export const FILE_SIZE_LIMIT_MAX = Number.POSITIVE_INFINITY;
export const FILE_SIZE_LIMIT_DEFAULT = 1024 * 1024 * 20; export const FILE_SIZE_LIMIT_DEFAULT = 1024 * 1024 * 20;
export const STORAGE_TYPE_LOCAL = 'local'; export const STORAGE_TYPE_LOCAL = 'local';

View File

@ -9,9 +9,9 @@
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import path from 'path'; import path from 'path';
import querystring from 'querystring';
import { getApp } from '.'; import { getApp } from '.';
import { FILE_FIELD_NAME, FILE_SIZE_LIMIT_DEFAULT, STORAGE_TYPE_LOCAL } from '../../constants'; 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; const { LOCAL_STORAGE_BASE_URL, LOCAL_STORAGE_DEST = 'storage/uploads', APP_PORT = '13000' } = process.env;
@ -105,6 +105,35 @@ describe('action', () => {
const content = await agent.get(url); const content = await agent.get(url);
expect(content.text.includes('Hello world!')).toBeTruthy(); expect(content.text.includes('Hello world!')).toBeTruthy();
}); });
it('filename with special character (URL)', async () => {
const rawText = '[]中文报告! 1%~50.4% (123) {$#}';
const rawFilename = `${rawText}.txt`;
const { body } = await agent.resource('attachments').create({
[FILE_FIELD_NAME]: path.resolve(__dirname, `./files/${rawFilename}`),
});
const matcher = {
title: rawText,
extname: '.txt',
path: '',
mimetype: 'text/plain',
meta: {},
storageId: 1,
};
// 文件上传和解析是否正常
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(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}`);
// expect(res2.text).toBe(rawText);
});
}); });
describe('specific storage', () => { describe('specific storage', () => {

View File

@ -0,0 +1 @@
[]中文报告! 1%~50.4% (123) {$#}

View File

@ -12,13 +12,7 @@ import { koaMulter as multer } from '@nocobase/utils';
import Path from 'path'; import Path from 'path';
import Plugin from '..'; import Plugin from '..';
import { import { FILE_FIELD_NAME, FILE_SIZE_LIMIT_DEFAULT, FILE_SIZE_LIMIT_MIN, LIMIT_FILES } from '../../constants';
FILE_FIELD_NAME,
FILE_SIZE_LIMIT_DEFAULT,
FILE_SIZE_LIMIT_MAX,
FILE_SIZE_LIMIT_MIN,
LIMIT_FILES,
} from '../../constants';
import * as Rules from '../rules'; import * as Rules from '../rules';
import { StorageClassType } from '../storages'; import { StorageClassType } from '../storages';
@ -90,10 +84,7 @@ async function multipart(ctx: Context, next: Next) {
}, },
storage: storageInstance.make(), storage: storageInstance.make(),
}; };
multerOptions.limits['fileSize'] = Math.min( multerOptions.limits['fileSize'] = Math.max(FILE_SIZE_LIMIT_MIN, storage.rules.size ?? FILE_SIZE_LIMIT_DEFAULT);
Math.max(FILE_SIZE_LIMIT_MIN, storage.rules.size ?? FILE_SIZE_LIMIT_DEFAULT),
FILE_SIZE_LIMIT_MAX,
);
const upload = multer(multerOptions).single(FILE_FIELD_NAME); const upload = multer(multerOptions).single(FILE_FIELD_NAME);
try { try {

View File

@ -7,13 +7,13 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { uid } from '@nocobase/utils';
import iconv from 'iconv-lite';
import path from 'path'; import path from 'path';
import { uid } from '@nocobase/utils';
export function getFilename(req, file, cb) { export function getFilename(req, file, cb) {
const originalname = iconv.decode(Buffer.from(file.originalname, 'binary'), 'utf8'); const originalname = Buffer.from(file.originalname, 'binary').toString('utf8');
const baseName = path.basename(originalname, path.extname(originalname)); // Filename in Windows cannot contain the following characters: < > ? * | : " \ /
const baseName = path.basename(originalname.replace(/[<>?*|:"\\/]/g, '-'), path.extname(originalname));
cb(null, `${baseName}-${uid(6)}${path.extname(originalname)}`); cb(null, `${baseName}-${uid(6)}${path.extname(originalname)}`);
} }