Files v2 - Add new workspace field file upload resolver (#17325)
This commit is contained in:
+21
-4
@@ -1,9 +1,11 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { Readable } from 'stream';
|
||||
|
||||
import { FileStorageDriverFactory } from 'src/engine/core-modules/file-storage/file-storage-driver.factory';
|
||||
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
||||
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
|
||||
|
||||
describe('FileStorageService', () => {
|
||||
let service: FileStorageService;
|
||||
@@ -13,6 +15,10 @@ describe('FileStorageService', () => {
|
||||
getCurrentDriver: jest.fn(),
|
||||
};
|
||||
|
||||
const mockFileRepository = {
|
||||
save: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
@@ -21,6 +27,10 @@ describe('FileStorageService', () => {
|
||||
provide: FileStorageDriverFactory,
|
||||
useValue: mockFileStorageDriverFactory,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(FileEntity),
|
||||
useValue: mockFileRepository,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
@@ -48,6 +58,9 @@ describe('FileStorageService', () => {
|
||||
copy: jest.fn(),
|
||||
download: jest.fn(),
|
||||
checkFileExists: jest.fn(),
|
||||
checkFolderExists: jest.fn(),
|
||||
writeFolder: jest.fn(),
|
||||
readFolder: jest.fn(),
|
||||
};
|
||||
|
||||
mockFileStorageDriverFactory.getCurrentDriver.mockReturnValue(mockDriver);
|
||||
@@ -67,7 +80,11 @@ describe('FileStorageService', () => {
|
||||
await service.write(writeParams);
|
||||
|
||||
expect(fileStorageDriverFactory.getCurrentDriver).toHaveBeenCalled();
|
||||
expect(mockDriver.write).toHaveBeenCalledWith(writeParams);
|
||||
expect(mockDriver.write).toHaveBeenCalledWith({
|
||||
filePath: 'documents/test.txt',
|
||||
sourceFile: writeParams.file,
|
||||
mimeType: 'text/plain',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle write errors', async () => {
|
||||
@@ -86,7 +103,6 @@ describe('FileStorageService', () => {
|
||||
'Write failed',
|
||||
);
|
||||
expect(fileStorageDriverFactory.getCurrentDriver).toHaveBeenCalled();
|
||||
expect(mockDriver.write).toHaveBeenCalledWith(writeParams);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -104,7 +120,9 @@ describe('FileStorageService', () => {
|
||||
const result = await service.read(readParams);
|
||||
|
||||
expect(fileStorageDriverFactory.getCurrentDriver).toHaveBeenCalled();
|
||||
expect(mockDriver.read).toHaveBeenCalledWith(readParams);
|
||||
expect(mockDriver.read).toHaveBeenCalledWith({
|
||||
filePath: 'documents/test.txt',
|
||||
});
|
||||
expect(result).toBe(mockStream);
|
||||
});
|
||||
|
||||
@@ -120,7 +138,6 @@ describe('FileStorageService', () => {
|
||||
|
||||
await expect(service.read(readParams)).rejects.toThrow('Read failed');
|
||||
expect(fileStorageDriverFactory.getCurrentDriver).toHaveBeenCalled();
|
||||
expect(mockDriver.read).toHaveBeenCalledWith(readParams);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+3
-4
@@ -4,12 +4,11 @@ import { type Sources } from 'twenty-shared/types';
|
||||
|
||||
export interface StorageDriver {
|
||||
delete(params: { folderPath: string; filename?: string }): Promise<void>;
|
||||
read(params: { folderPath: string; filename: string }): Promise<Readable>;
|
||||
read(params: { filePath: string }): Promise<Readable>;
|
||||
readFolder(folderPath: string): Promise<Sources>;
|
||||
write(params: {
|
||||
file: Buffer | Uint8Array | string;
|
||||
name: string;
|
||||
folder: string;
|
||||
filePath: string;
|
||||
sourceFile: Buffer | Uint8Array | string;
|
||||
mimeType: string | undefined;
|
||||
}): Promise<void>;
|
||||
writeFolder(sources: Sources, folderPath: string): Promise<void>;
|
||||
|
||||
+8
-21
@@ -28,21 +28,16 @@ export class LocalDriver implements StorageDriver {
|
||||
}
|
||||
|
||||
async write(params: {
|
||||
file: Buffer | Uint8Array | string;
|
||||
name: string;
|
||||
folder: string;
|
||||
filePath: string;
|
||||
sourceFile: Buffer | Uint8Array | string;
|
||||
mimeType: string | undefined;
|
||||
}): Promise<void> {
|
||||
const filePath = join(
|
||||
`${this.options.storagePath}/`,
|
||||
params.folder,
|
||||
params.name,
|
||||
);
|
||||
const filePath = `${this.options.storagePath}/${params.filePath}`;
|
||||
const folderPath = dirname(filePath);
|
||||
|
||||
await this.createFolder(folderPath);
|
||||
|
||||
await fs.writeFile(filePath, params.file);
|
||||
await fs.writeFile(filePath, params.sourceFile);
|
||||
}
|
||||
|
||||
async writeFolder(sources: Sources, folderPath: string) {
|
||||
@@ -52,10 +47,9 @@ export class LocalDriver implements StorageDriver {
|
||||
continue;
|
||||
}
|
||||
await this.write({
|
||||
file: sources[key],
|
||||
name: key,
|
||||
filePath: join(folderPath, key),
|
||||
sourceFile: sources[key],
|
||||
mimeType: undefined,
|
||||
folder: folderPath,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -73,15 +67,8 @@ export class LocalDriver implements StorageDriver {
|
||||
await fs.rm(filePath, { recursive: true });
|
||||
}
|
||||
|
||||
async read(params: {
|
||||
folderPath: string;
|
||||
filename: string;
|
||||
}): Promise<Readable> {
|
||||
const joinedPath = join(
|
||||
`${this.options.storagePath}/`,
|
||||
params.folderPath,
|
||||
params.filename,
|
||||
);
|
||||
async read(params: { filePath: string }): Promise<Readable> {
|
||||
const joinedPath = join(`${this.options.storagePath}/`, params.filePath);
|
||||
let filePath: string;
|
||||
|
||||
try {
|
||||
|
||||
@@ -20,8 +20,8 @@ import {
|
||||
S3,
|
||||
type S3ClientConfig,
|
||||
} from '@aws-sdk/client-s3';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { isObject } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type StorageDriver } from 'src/engine/core-modules/file-storage/drivers/interfaces/storage-driver.interface';
|
||||
import {
|
||||
@@ -61,14 +61,13 @@ export class S3Driver implements StorageDriver {
|
||||
}
|
||||
|
||||
async write(params: {
|
||||
file: Buffer | Uint8Array | string;
|
||||
name: string;
|
||||
folder: string;
|
||||
filePath: string;
|
||||
sourceFile: Buffer | Uint8Array | string;
|
||||
mimeType: string | undefined;
|
||||
}): Promise<void> {
|
||||
const command = new PutObjectCommand({
|
||||
Key: `${params.folder}/${params.name}`,
|
||||
Body: params.file,
|
||||
Key: params.filePath,
|
||||
Body: params.sourceFile,
|
||||
ContentType: params.mimeType,
|
||||
Bucket: this.bucketName,
|
||||
});
|
||||
@@ -83,10 +82,9 @@ export class S3Driver implements StorageDriver {
|
||||
continue;
|
||||
}
|
||||
await this.write({
|
||||
file: sources[key],
|
||||
name: key,
|
||||
filePath: `${folderPath}/${key}`,
|
||||
sourceFile: sources[key],
|
||||
mimeType: undefined,
|
||||
folder: folderPath,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -169,12 +167,9 @@ export class S3Driver implements StorageDriver {
|
||||
}
|
||||
}
|
||||
|
||||
async read(params: {
|
||||
folderPath: string;
|
||||
filename: string;
|
||||
}): Promise<Readable> {
|
||||
async read(params: { filePath: string }): Promise<Readable> {
|
||||
const command = new GetObjectCommand({
|
||||
Key: `${params.folderPath}/${params.filename}`,
|
||||
Key: params.filePath,
|
||||
Bucket: this.bucketName,
|
||||
});
|
||||
|
||||
@@ -222,7 +217,7 @@ export class S3Driver implements StorageDriver {
|
||||
const { fromFolderPath, filename } = folderAndFilePaths;
|
||||
|
||||
const fileContent = await readFileContent(
|
||||
await this.read({ folderPath: fromFolderPath, filename }),
|
||||
await this.read({ filePath: `${fromFolderPath}/${filename}` }),
|
||||
);
|
||||
|
||||
const formattedObjectKey = object.Key.replace(
|
||||
@@ -441,8 +436,7 @@ export class S3Driver implements StorageDriver {
|
||||
await mkdir(dir, { recursive: true });
|
||||
|
||||
const fileStream = await this.read({
|
||||
folderPath: params.from.folderPath,
|
||||
filename: params.from.filename,
|
||||
filePath: `${params.from.folderPath}/${params.from.filename}`,
|
||||
});
|
||||
|
||||
const toPath = join(
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { type DynamicModule, Global } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { FileStorageDriverFactory } from 'src/engine/core-modules/file-storage/file-storage-driver.factory';
|
||||
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
||||
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
|
||||
import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module';
|
||||
|
||||
@Global()
|
||||
@@ -9,7 +11,7 @@ export class FileStorageModule {
|
||||
static forRoot(): DynamicModule {
|
||||
return {
|
||||
module: FileStorageModule,
|
||||
imports: [TwentyConfigModule],
|
||||
imports: [TwentyConfigModule, TypeOrmModule.forFeature([FileEntity])],
|
||||
providers: [FileStorageDriverFactory, FileStorageService],
|
||||
exports: [FileStorageService],
|
||||
};
|
||||
|
||||
+88
-11
@@ -1,28 +1,111 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { type Readable } from 'stream';
|
||||
|
||||
import { Sources } from 'twenty-shared/types';
|
||||
|
||||
import { type StorageDriver } from 'src/engine/core-modules/file-storage/drivers/interfaces/storage-driver.interface';
|
||||
import { FileFolder, Sources } from 'twenty-shared/types';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { FileStorageDriverFactory } from 'src/engine/core-modules/file-storage/file-storage-driver.factory';
|
||||
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
|
||||
|
||||
@Injectable()
|
||||
export class FileStorageService implements StorageDriver {
|
||||
//TODO: Implement storage driver interface when removing v1
|
||||
//export class FileStorageService implements StorageDriver {
|
||||
export class FileStorageService {
|
||||
constructor(
|
||||
private readonly fileStorageDriverFactory: FileStorageDriverFactory,
|
||||
@InjectRepository(FileEntity)
|
||||
private readonly fileRepository: Repository<FileEntity>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @deprecated Use write_v2 instead
|
||||
*/
|
||||
write(params: {
|
||||
file: string | Buffer | Uint8Array;
|
||||
name: string;
|
||||
folder: string;
|
||||
mimeType: string | undefined;
|
||||
}): Promise<void> {
|
||||
const { file, name, folder, mimeType } = params;
|
||||
|
||||
const driver = this.fileStorageDriverFactory.getCurrentDriver();
|
||||
|
||||
return driver.write(params);
|
||||
return driver.write({
|
||||
filePath: `${folder}/${name}`,
|
||||
sourceFile: file,
|
||||
mimeType,
|
||||
});
|
||||
}
|
||||
|
||||
async write_v2({
|
||||
sourceFile,
|
||||
destinationPath,
|
||||
mimeType,
|
||||
fileFolder,
|
||||
applicationId,
|
||||
workspaceId,
|
||||
fileId,
|
||||
}: {
|
||||
sourceFile: string | Buffer | Uint8Array;
|
||||
destinationPath: string;
|
||||
mimeType: string | undefined;
|
||||
fileFolder: FileFolder;
|
||||
applicationId: string;
|
||||
workspaceId: string;
|
||||
fileId?: string;
|
||||
}): Promise<FileEntity> {
|
||||
const driver = this.fileStorageDriverFactory.getCurrentDriver();
|
||||
|
||||
const driverParams = {
|
||||
filePath: `${workspaceId}/${applicationId}/${fileFolder}/${destinationPath}`,
|
||||
mimeType,
|
||||
sourceFile,
|
||||
};
|
||||
|
||||
await driver.write(driverParams);
|
||||
|
||||
const fileEntity = await this.fileRepository.save({
|
||||
path: `${fileFolder}/${destinationPath}`,
|
||||
workspaceId,
|
||||
applicationId,
|
||||
id: fileId,
|
||||
size:
|
||||
typeof sourceFile === 'string'
|
||||
? Buffer.byteLength(sourceFile)
|
||||
: sourceFile.length,
|
||||
});
|
||||
|
||||
return fileEntity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use read_v2 instead
|
||||
*/
|
||||
read(params: { folderPath: string; filename: string }): Promise<Readable> {
|
||||
const driver = this.fileStorageDriverFactory.getCurrentDriver();
|
||||
const { folderPath, filename } = params;
|
||||
|
||||
return driver.read({ filePath: `${folderPath}/${filename}` });
|
||||
}
|
||||
|
||||
read_v2({
|
||||
destinationPath,
|
||||
fileFolder,
|
||||
applicationId,
|
||||
workspaceId,
|
||||
}: {
|
||||
destinationPath: string;
|
||||
fileFolder: FileFolder;
|
||||
applicationId: string;
|
||||
workspaceId: string;
|
||||
}): Promise<Readable> {
|
||||
const driver = this.fileStorageDriverFactory.getCurrentDriver();
|
||||
|
||||
const folderPath = `${workspaceId}/${applicationId}/${fileFolder}/${destinationPath}`;
|
||||
|
||||
return driver.read({ filePath: folderPath });
|
||||
}
|
||||
|
||||
writeFolder(sources: Sources, folderPath: string): Promise<void> {
|
||||
@@ -31,12 +114,6 @@ export class FileStorageService implements StorageDriver {
|
||||
return driver.writeFolder(sources, folderPath);
|
||||
}
|
||||
|
||||
read(params: { folderPath: string; filename: string }): Promise<Readable> {
|
||||
const driver = this.fileStorageDriverFactory.getCurrentDriver();
|
||||
|
||||
return driver.read(params);
|
||||
}
|
||||
|
||||
readFolder(folderPath: string): Promise<Sources> {
|
||||
const driver = this.fileStorageDriverFactory.getCurrentDriver();
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
@@ -17,7 +15,6 @@ import { ApplicationEntity } from 'src/engine/core-modules/application/applicati
|
||||
import { WorkspaceRelatedEntity } from 'src/engine/workspace-manager/types/workspace-related-entity';
|
||||
|
||||
@Entity('file')
|
||||
@ObjectType('File')
|
||||
@Index('IDX_FILE_WORKSPACE_ID', ['workspaceId'])
|
||||
export class FileEntity extends WorkspaceRelatedEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
|
||||
+26
-1
@@ -7,6 +7,7 @@ import { FileFolder } from 'twenty-shared/types';
|
||||
|
||||
import type { FileUpload } from 'graphql-upload/processRequest.mjs';
|
||||
|
||||
import { FileDTO } from 'src/engine/core-modules/file/dtos/file.dto';
|
||||
import { SignedFileDTO } from 'src/engine/core-modules/file/file-upload/dtos/signed-file.dto';
|
||||
import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service';
|
||||
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
|
||||
@@ -24,7 +25,9 @@ import { streamToBuffer } from 'src/utils/stream-to-buffer';
|
||||
export class FileUploadResolver {
|
||||
constructor(private readonly fileUploadService: FileUploadService) {}
|
||||
|
||||
@Mutation(() => SignedFileDTO)
|
||||
@Mutation(() => SignedFileDTO, {
|
||||
deprecationReason: 'Use uploadFilesFieldFile instead',
|
||||
})
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.UPLOAD_FILE))
|
||||
async uploadFile(
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
@@ -51,6 +54,28 @@ export class FileUploadResolver {
|
||||
return files[0];
|
||||
}
|
||||
|
||||
@Mutation(() => FileDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.UPLOAD_FILE))
|
||||
async uploadFilesFieldFile(
|
||||
@AuthWorkspace()
|
||||
{ id: workspaceId, workspaceCustomApplicationId }: WorkspaceEntity,
|
||||
@Args({ name: 'file', type: () => GraphQLUpload })
|
||||
{ createReadStream, filename, mimetype }: FileUpload,
|
||||
): Promise<FileDTO> {
|
||||
const stream = createReadStream();
|
||||
const buffer = await streamToBuffer(stream);
|
||||
|
||||
const fileEntity = await this.fileUploadService.uploadFilesFieldFile({
|
||||
file: buffer,
|
||||
filename,
|
||||
declaredMimeType: mimetype,
|
||||
workspaceId,
|
||||
applicationId: workspaceCustomApplicationId,
|
||||
});
|
||||
|
||||
return fileEntity;
|
||||
}
|
||||
|
||||
@Mutation(() => SignedFileDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.UPLOAD_FILE))
|
||||
async uploadImage(
|
||||
|
||||
+41
-1
@@ -4,13 +4,15 @@ import { Injectable } from '@nestjs/common';
|
||||
import DOMPurify from 'dompurify';
|
||||
import FileType from 'file-type';
|
||||
import sharp from 'sharp';
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
import { v4 } from 'uuid';
|
||||
import { type FileFolder } from 'twenty-shared/types';
|
||||
|
||||
import { settings } from 'src/engine/constants/settings';
|
||||
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
||||
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
|
||||
import { FileService } from 'src/engine/core-modules/file/services/file.service';
|
||||
import { buildFileInfo } from 'src/engine/core-modules/file/utils/build-file-info.utils';
|
||||
import { extractFileInfo } from 'src/engine/core-modules/file/utils/extract-file-info.utils';
|
||||
import { getCropSize, getImageBufferFromUrl } from 'src/utils/image';
|
||||
|
||||
export type SignedFile = { path: string; token: string };
|
||||
@@ -68,6 +70,9 @@ export class FileUploadService {
|
||||
return file;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use uploadWorkspaceRecordFile if uploading workspace records-scoped files. Or create your dedicated upload file service.
|
||||
*/
|
||||
async uploadFile({
|
||||
file,
|
||||
filename,
|
||||
@@ -206,4 +211,39 @@ export class FileUploadService {
|
||||
private getWorkspaceFolderName(workspaceId: string, fileFolder: FileFolder) {
|
||||
return `workspace-${workspaceId}/${fileFolder}`;
|
||||
}
|
||||
|
||||
async uploadFilesFieldFile({
|
||||
file,
|
||||
filename,
|
||||
declaredMimeType,
|
||||
workspaceId,
|
||||
applicationId,
|
||||
}: {
|
||||
file: Buffer;
|
||||
filename: string;
|
||||
declaredMimeType: string | undefined;
|
||||
workspaceId: string;
|
||||
applicationId: string;
|
||||
}): Promise<FileEntity> {
|
||||
const { mimeType, ext } = await extractFileInfo({
|
||||
file,
|
||||
declaredMimeType,
|
||||
filename,
|
||||
});
|
||||
|
||||
const sanitizedFile = this._sanitizeFile({ file, ext, mimeType });
|
||||
|
||||
const fileId = v4();
|
||||
const name = `${fileId}${ext ? `.${ext}` : ''}`;
|
||||
|
||||
return await this.fileStorage.write_v2({
|
||||
sourceFile: sanitizedFile,
|
||||
destinationPath: name,
|
||||
mimeType,
|
||||
fileFolder: FileFolder.FilesField,
|
||||
applicationId,
|
||||
workspaceId,
|
||||
fileId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+7
-1
@@ -1,7 +1,7 @@
|
||||
import { registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
import { type KebabCase } from 'type-fest';
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
import { type KebabCase } from 'type-fest';
|
||||
|
||||
registerEnumType(FileFolder, {
|
||||
name: 'FileFolder',
|
||||
@@ -48,6 +48,12 @@ export const fileFolderConfigs: Record<FileFolder, FileFolderConfig> = {
|
||||
[FileFolder.Source]: {
|
||||
ignoreExpirationToken: false,
|
||||
},
|
||||
[FileFolder.FilesField]: {
|
||||
ignoreExpirationToken: false,
|
||||
},
|
||||
[FileFolder.TemporaryFilesField]: {
|
||||
ignoreExpirationToken: false,
|
||||
},
|
||||
};
|
||||
|
||||
export type AllowedFolders = KebabCase<keyof typeof FileFolder>;
|
||||
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
import FileType from 'file-type';
|
||||
|
||||
import { extractFileInfo } from 'src/engine/core-modules/file/utils/extract-file-info.utils';
|
||||
|
||||
jest.mock('file-type', () => ({
|
||||
fromBuffer: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('extractFileInfo', () => {
|
||||
const mockBuffer = Buffer.from('test content');
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should use detected file type when available', async () => {
|
||||
(FileType.fromBuffer as jest.Mock).mockResolvedValue({
|
||||
mime: 'image/png',
|
||||
ext: 'png',
|
||||
});
|
||||
|
||||
const result = await extractFileInfo({
|
||||
file: mockBuffer,
|
||||
declaredMimeType: 'text/plain',
|
||||
filename: 'test.txt',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
mimeType: 'image/png',
|
||||
ext: 'png',
|
||||
});
|
||||
});
|
||||
|
||||
it('should fall back to declared values when file type is not detected', async () => {
|
||||
(FileType.fromBuffer as jest.Mock).mockResolvedValue(undefined);
|
||||
|
||||
const result = await extractFileInfo({
|
||||
file: mockBuffer,
|
||||
declaredMimeType: 'text/plain',
|
||||
filename: 'test.txt',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
mimeType: 'text/plain',
|
||||
ext: 'txt',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle missing declared mime type when file type is detected', async () => {
|
||||
(FileType.fromBuffer as jest.Mock).mockResolvedValue({
|
||||
mime: 'application/pdf',
|
||||
ext: 'pdf',
|
||||
});
|
||||
|
||||
const result = await extractFileInfo({
|
||||
file: mockBuffer,
|
||||
declaredMimeType: undefined,
|
||||
filename: 'document',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
mimeType: 'application/pdf',
|
||||
ext: 'pdf',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle both mime type and extension being undefined', async () => {
|
||||
(FileType.fromBuffer as jest.Mock).mockResolvedValue(undefined);
|
||||
|
||||
const result = await extractFileInfo({
|
||||
file: mockBuffer,
|
||||
declaredMimeType: undefined,
|
||||
filename: 'file-without-extension',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
mimeType: undefined,
|
||||
ext: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle filenames with multiple dots', async () => {
|
||||
(FileType.fromBuffer as jest.Mock).mockResolvedValue(undefined);
|
||||
|
||||
const result = await extractFileInfo({
|
||||
file: mockBuffer,
|
||||
declaredMimeType: 'application/gzip',
|
||||
filename: 'archive.tar.gz',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
mimeType: 'application/gzip',
|
||||
ext: 'gz',
|
||||
});
|
||||
});
|
||||
|
||||
it('should call FileType.fromBuffer with the provided buffer', async () => {
|
||||
(FileType.fromBuffer as jest.Mock).mockResolvedValue(undefined);
|
||||
|
||||
await extractFileInfo({
|
||||
file: mockBuffer,
|
||||
declaredMimeType: 'image/png',
|
||||
filename: 'image.png',
|
||||
});
|
||||
|
||||
expect(FileType.fromBuffer).toHaveBeenCalledWith(mockBuffer);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import FileType from 'file-type';
|
||||
|
||||
import { buildFileInfo } from 'src/engine/core-modules/file/utils/build-file-info.utils';
|
||||
|
||||
export const extractFileInfo = async ({
|
||||
file,
|
||||
declaredMimeType,
|
||||
filename,
|
||||
}: {
|
||||
file: Buffer;
|
||||
declaredMimeType: string | undefined;
|
||||
filename: string;
|
||||
}) => {
|
||||
const { ext: declaredExt } = buildFileInfo(filename);
|
||||
|
||||
const detectedFileType = await FileType.fromBuffer(file);
|
||||
|
||||
const mimeType = detectedFileType?.mime ?? declaredMimeType;
|
||||
|
||||
const ext = detectedFileType?.ext ?? declaredExt;
|
||||
|
||||
return {
|
||||
mimeType,
|
||||
ext,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user