mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 13:39:24 +08:00
feat(file-manager): add getFileStream api (#6741)
This commit is contained in:
parent
33141d100e
commit
c2521a04c1
@ -20,6 +20,7 @@
|
|||||||
"@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",
|
||||||
|
"axios": "^1.7.0",
|
||||||
"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",
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import { Readable } from 'stream';
|
||||||
|
|
||||||
import { getApp } from '.';
|
import { getApp } from '.';
|
||||||
import PluginFileManagerServer from '../server';
|
import PluginFileManagerServer from '../server';
|
||||||
@ -177,5 +178,41 @@ describe('file manager > server', () => {
|
|||||||
process.env.APP_PUBLIC_PATH = originalPath;
|
process.env.APP_PUBLIC_PATH = originalPath;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getFileStream', () => {
|
||||||
|
it('should get file stream for local storage', async () => {
|
||||||
|
const { body } = await agent.resource('attachments').create({
|
||||||
|
[FILE_FIELD_NAME]: path.resolve(__dirname, './files/text.txt'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await plugin.getFileStream(body.data);
|
||||||
|
expect(result).toHaveProperty('stream');
|
||||||
|
expect(result.stream).toBeInstanceOf(Readable);
|
||||||
|
expect(result).toHaveProperty('contentType');
|
||||||
|
expect(result.contentType).toBe('text/plain');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error when file not found', async () => {
|
||||||
|
const { body } = await agent.resource('attachments').create({
|
||||||
|
[FILE_FIELD_NAME]: path.resolve(__dirname, './files/text.txt'),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Modify the file path to a non-existent one
|
||||||
|
body.data.path = 'non-existent-path';
|
||||||
|
|
||||||
|
await expect(plugin.getFileStream(body.data)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error when storage not found', async () => {
|
||||||
|
const { body } = await agent.resource('attachments').create({
|
||||||
|
[FILE_FIELD_NAME]: path.resolve(__dirname, './files/text.txt'),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove storageId to simulate storage not found
|
||||||
|
delete body.data.storageId;
|
||||||
|
|
||||||
|
await expect(plugin.getFileStream(body.data)).rejects.toThrow('File storageId not found');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -13,6 +13,7 @@ import { basename } from 'path';
|
|||||||
import { Collection, Model, Transactionable } from '@nocobase/database';
|
import { Collection, Model, Transactionable } from '@nocobase/database';
|
||||||
import { Plugin } from '@nocobase/server';
|
import { Plugin } from '@nocobase/server';
|
||||||
import { Registry } from '@nocobase/utils';
|
import { Registry } from '@nocobase/utils';
|
||||||
|
import { Readable } from 'stream';
|
||||||
import { STORAGE_TYPE_ALI_OSS, STORAGE_TYPE_LOCAL, STORAGE_TYPE_S3, STORAGE_TYPE_TX_COS } from '../constants';
|
import { STORAGE_TYPE_ALI_OSS, STORAGE_TYPE_LOCAL, STORAGE_TYPE_S3, STORAGE_TYPE_TX_COS } from '../constants';
|
||||||
import initActions from './actions';
|
import initActions from './actions';
|
||||||
import { AttachmentInterface } from './interfaces/attachment-interface';
|
import { AttachmentInterface } from './interfaces/attachment-interface';
|
||||||
@ -334,6 +335,27 @@ export class PluginFileManagerServer extends Plugin {
|
|||||||
}
|
}
|
||||||
return !!storage.options?.public;
|
return !!storage.options?.public;
|
||||||
}
|
}
|
||||||
|
async getFileStream(file: AttachmentModel): Promise<{ stream: Readable; contentType?: string }> {
|
||||||
|
if (!file.storageId) {
|
||||||
|
throw new Error('File storageId not found');
|
||||||
|
}
|
||||||
|
const storage = this.storagesCache.get(file.storageId);
|
||||||
|
if (!storage) {
|
||||||
|
throw new Error('[file-manager] no linked or default storage provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
const StorageType = this.storageTypes.get(storage.type);
|
||||||
|
if (!StorageType) {
|
||||||
|
throw new Error(`[file-manager] storage type "${storage.type}" is not defined`);
|
||||||
|
}
|
||||||
|
const storageInstance = new StorageType(storage);
|
||||||
|
|
||||||
|
if (!storageInstance) {
|
||||||
|
throw new Error(`[file-manager] storage type "${storage.type}" is not defined`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return storageInstance.getFileStream(file);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PluginFileManagerServer;
|
export default PluginFileManagerServer;
|
||||||
|
@ -7,12 +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 Path from 'path';
|
|
||||||
import { StorageEngine } from 'multer';
|
|
||||||
import urlJoin from 'url-join';
|
|
||||||
import { isURL } from '@nocobase/utils';
|
import { isURL } from '@nocobase/utils';
|
||||||
|
import axios, { AxiosRequestConfig } from 'axios';
|
||||||
|
import { StorageEngine } from 'multer';
|
||||||
|
import Path from 'path';
|
||||||
|
import type { Readable } from 'stream';
|
||||||
|
import urlJoin from 'url-join';
|
||||||
import { encodeURL, ensureUrlEncoded, getFileKey } from '../utils';
|
import { encodeURL, ensureUrlEncoded, getFileKey } from '../utils';
|
||||||
|
|
||||||
export interface StorageModel {
|
export interface StorageModel {
|
||||||
id?: number;
|
id?: number;
|
||||||
title: string;
|
title: string;
|
||||||
@ -32,6 +33,7 @@ export interface AttachmentModel {
|
|||||||
path: string;
|
path: string;
|
||||||
url: string;
|
url: string;
|
||||||
storageId: number;
|
storageId: number;
|
||||||
|
mimetype: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class StorageType {
|
export abstract class StorageType {
|
||||||
@ -85,6 +87,26 @@ export abstract class StorageType {
|
|||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
return urlJoin(keys);
|
return urlJoin(keys);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getFileStream(file: AttachmentModel): Promise<{ stream: Readable; contentType?: string }> {
|
||||||
|
try {
|
||||||
|
const fileURL = await this.getFileURL(file);
|
||||||
|
const requestOptions: AxiosRequestConfig = {
|
||||||
|
responseType: 'stream',
|
||||||
|
validateStatus: (status) => status === 200,
|
||||||
|
timeout: 30000, // 30 seconds timeout
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await axios.get(fileURL, requestOptions);
|
||||||
|
|
||||||
|
return {
|
||||||
|
stream: response.data,
|
||||||
|
contentType: response.headers['content-type'],
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`fetch file failed: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type StorageClassType = { new (storage: StorageModel): StorageType } & typeof StorageType;
|
export type StorageClassType = { new (storage: StorageModel): StorageType } & typeof StorageType;
|
||||||
|
@ -8,10 +8,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { isURL } from '@nocobase/utils';
|
import { isURL } from '@nocobase/utils';
|
||||||
|
import fsSync from 'fs';
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
import mkdirp from 'mkdirp';
|
import mkdirp from 'mkdirp';
|
||||||
import multer from 'multer';
|
import multer from 'multer';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import type { Readable } from 'stream';
|
||||||
import urlJoin from 'url-join';
|
import urlJoin from 'url-join';
|
||||||
import { AttachmentModel, StorageType } from '.';
|
import { AttachmentModel, StorageType } from '.';
|
||||||
import { FILE_SIZE_LIMIT_DEFAULT, STORAGE_TYPE_LOCAL } from '../../constants';
|
import { FILE_SIZE_LIMIT_DEFAULT, STORAGE_TYPE_LOCAL } from '../../constants';
|
||||||
@ -83,4 +85,16 @@ export default class extends StorageType {
|
|||||||
}
|
}
|
||||||
return urlJoin(process.env.APP_PUBLIC_PATH, url);
|
return urlJoin(process.env.APP_PUBLIC_PATH, url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getFileStream(file: AttachmentModel): Promise<{ stream: Readable; contentType?: string }> {
|
||||||
|
// compatible with windows path
|
||||||
|
const filePath = path.join(process.cwd(), 'storage', 'uploads', file.path || '', file.filename);
|
||||||
|
if (await fs.stat(filePath)) {
|
||||||
|
return {
|
||||||
|
stream: fsSync.createReadStream(filePath),
|
||||||
|
contentType: file.mimetype,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error(`File not found: ${filePath}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user