diff --git a/packages/plugins/@nocobase/plugin-file-manager/package.json b/packages/plugins/@nocobase/plugin-file-manager/package.json index ac1a8d5c72..94758e623a 100644 --- a/packages/plugins/@nocobase/plugin-file-manager/package.json +++ b/packages/plugins/@nocobase/plugin-file-manager/package.json @@ -20,6 +20,7 @@ "@types/multer": "^1.4.5", "antd": "5.x", "cos-nodejs-sdk-v5": "^2.11.14", + "axios": "^1.7.0", "koa-static": "^5.0.0", "mime-match": "^1.0.2", "mkdirp": "~0.5.4", diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/server/__tests__/server.test.ts b/packages/plugins/@nocobase/plugin-file-manager/src/server/__tests__/server.test.ts index 4866763b75..7a102d687d 100644 --- a/packages/plugins/@nocobase/plugin-file-manager/src/server/__tests__/server.test.ts +++ b/packages/plugins/@nocobase/plugin-file-manager/src/server/__tests__/server.test.ts @@ -8,6 +8,7 @@ */ import path from 'path'; +import { Readable } from 'stream'; import { getApp } from '.'; import PluginFileManagerServer from '../server'; @@ -177,5 +178,41 @@ describe('file manager > server', () => { 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'); + }); + }); }); }); diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/server/server.ts b/packages/plugins/@nocobase/plugin-file-manager/src/server/server.ts index f7903a5db9..871459f65f 100644 --- a/packages/plugins/@nocobase/plugin-file-manager/src/server/server.ts +++ b/packages/plugins/@nocobase/plugin-file-manager/src/server/server.ts @@ -13,6 +13,7 @@ import { basename } from 'path'; import { Collection, Model, Transactionable } from '@nocobase/database'; import { Plugin } from '@nocobase/server'; 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 initActions from './actions'; import { AttachmentInterface } from './interfaces/attachment-interface'; @@ -334,6 +335,27 @@ export class PluginFileManagerServer extends Plugin { } 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; diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/server/storages/index.ts b/packages/plugins/@nocobase/plugin-file-manager/src/server/storages/index.ts index 6c6cc825d9..47f79dcfe5 100644 --- a/packages/plugins/@nocobase/plugin-file-manager/src/server/storages/index.ts +++ b/packages/plugins/@nocobase/plugin-file-manager/src/server/storages/index.ts @@ -7,12 +7,13 @@ * 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 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'; - export interface StorageModel { id?: number; title: string; @@ -32,6 +33,7 @@ export interface AttachmentModel { path: string; url: string; storageId: number; + mimetype: string; } export abstract class StorageType { @@ -85,6 +87,26 @@ export abstract class StorageType { ].filter(Boolean); 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; diff --git a/packages/plugins/@nocobase/plugin-file-manager/src/server/storages/local.ts b/packages/plugins/@nocobase/plugin-file-manager/src/server/storages/local.ts index 0a35462962..c6fe750904 100644 --- a/packages/plugins/@nocobase/plugin-file-manager/src/server/storages/local.ts +++ b/packages/plugins/@nocobase/plugin-file-manager/src/server/storages/local.ts @@ -8,10 +8,12 @@ */ import { isURL } from '@nocobase/utils'; +import fsSync from 'fs'; import fs from 'fs/promises'; import mkdirp from 'mkdirp'; import multer from 'multer'; import path from 'path'; +import type { Readable } from 'stream'; import urlJoin from 'url-join'; import { AttachmentModel, StorageType } from '.'; 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); } + + 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}`); + } }