Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 44 additions & 8 deletions src/files/files.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,34 @@ import { AppException } from 'omniboxd/common/exceptions/app.exception';
import { I18nService } from 'nestjs-i18n';
import { PresignedPost } from '@aws-sdk/s3-presigned-post';
import { formatFileSize } from '../utils/format-file-size';
import { S3Service } from 'omniboxd/s3/s3.service';
import { ObjectMeta, S3Service } from 'omniboxd/s3/s3.service';
import { extname } from 'path';
import * as mime from 'mime-types';

const s3Prefix = 'uploaded-files';

const ALLOWED_FILE_EXTENSIONS = new Set([
'.md',
'.doc',
'.ppt',
'.docx',
'.pptx',
'.txt',
'.pdf',
'.wav',
'.mp3',
'.m4a',
'.pcm',
'.opus',
'.webm',
'.mp4',
'.avi',
'.mov',
'.mkv',
'.flv',
'.wmv',
]);

@Injectable()
export class FilesService {
private readonly s3MaxFileSize: number;
Expand All @@ -33,8 +57,18 @@ export class FilesService {
userId: string,
namespaceId: string,
filename: string,
mimetype: string,
mimetype?: string,
): Promise<File> {
const extension = extname(filename).toLowerCase();
if (!ALLOWED_FILE_EXTENSIONS.has(extension)) {
const message = this.i18n.t('resource.errors.fileTypeNotSupported');
throw new AppException(
message,
'FILE_TYPE_NOT_SUPPORTED',
HttpStatus.BAD_REQUEST,
);
}
mimetype = mimetype || mime.lookup(filename) || 'application/octet-stream';
return await this.fileRepo.save(
this.fileRepo.create({
namespaceId,
Expand All @@ -53,7 +87,6 @@ export class FilesService {
fileId: string,
fileSize: number | undefined,
filename: string,
mimetype: string,
): Promise<PresignedPost> {
if (fileSize && fileSize > this.s3MaxFileSize) {
const message = this.i18n.t('resource.errors.fileTooLarge', {
Expand All @@ -68,25 +101,24 @@ export class FilesService {
return await this.s3Service.generateUploadForm(
`${s3Prefix}/${fileId}`,
true,
mimetype,
disposition,
this.s3MaxFileSize,
);
}

async uploadFile(file: File, data: Express.Multer.File): Promise<void> {
if (data.size > this.s3MaxFileSize) {
async uploadFile(file: File, buffer: Buffer): Promise<void> {
if (buffer.length > this.s3MaxFileSize) {
const message = this.i18n.t('resource.errors.fileTooLarge', {
args: {
userSize: formatFileSize(data.size),
userSize: formatFileSize(buffer.length),
limitSize: formatFileSize(this.s3MaxFileSize),
},
});
throw new AppException(message, 'FILE_TOO_LARGE', HttpStatus.BAD_REQUEST);
}
await this.s3Service.putObject(
`${s3Prefix}/${file.id}`,
data.stream,
buffer,
file.mimetype,
);
}
Expand All @@ -109,4 +141,8 @@ export class FilesService {
disposition,
);
}

async headFile(fileId: string): Promise<ObjectMeta | null> {
return await this.s3Service.headObject(`${s3Prefix}/${fileId}`);
}
}
1 change: 1 addition & 0 deletions src/i18n/en/resource.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"parentOrResourceIdRequired": "parent_id or resource_id is required",
"resourceNotFound": "Resource not found",
"fileNotFound": "File not found",
"fileTypeNotSupported": "This file type is not supported",
"fileTooLarge": "Your file ({userSize}) exceeds the maximum limit of {limitSize}",
"cannotDeleteRoot": "Cannot delete root resource",
"cannotDuplicateRoot": "Cannot duplicate root resource",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/zh/resource.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"parentOrResourceIdRequired": "需要 parent_id 或 resource_id",
"resourceNotFound": "资源未找到",
"fileNotFound": "文件未找到",
"fileTypeNotSupported": "不支持的文件类型",
"fileTooLarge": "您的文件({userSize})超过最大限制 {limitSize}",
"cannotDeleteRoot": "无法删除根资源",
"cannotDuplicateRoot": "无法复制根资源",
Expand Down
11 changes: 2 additions & 9 deletions src/namespace-resources/namespace-resources.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { InjectRepository } from '@nestjs/typeorm';
import duplicateName from 'omniboxd/utils/duplicate-name';
import * as mime from 'mime-types';
import {
DataSource,
EntityManager,
Expand Down Expand Up @@ -47,7 +46,6 @@ import {
import { getOriginalFileName } from 'omniboxd/utils/encode-filename';

const TASK_PRIORITY = 5;

@Injectable()
export class NamespaceResourcesService {
constructor(
Expand Down Expand Up @@ -720,15 +718,11 @@ export class NamespaceResourcesService {
const message = this.i18n.t('auth.errors.notAuthorized');
throw new AppException(message, 'NOT_AUTHORIZED', HttpStatus.FORBIDDEN);
}
const mimetype =
createReq.mimetype ||
mime.lookup(createReq.name) ||
'application/octet-stream';
return await this.filesService.createFile(
userId,
namespaceId,
createReq.name,
mimetype,
createReq.mimetype,
);
}

Expand All @@ -742,7 +736,6 @@ export class NamespaceResourcesService {
file.id,
createReq.size,
file.name,
file.mimetype,
);
return UploadFileInfoDto.new(file.id, postReq.url, postReq.fields);
}
Expand Down Expand Up @@ -835,7 +828,7 @@ export class NamespaceResourcesService {
name: originalFilename,
mimetype: file.mimetype,
});
await this.filesService.uploadFile(resourceFile, file);
await this.filesService.uploadFile(resourceFile, file.buffer);
return await this.create(
userId,
namespaceId,
Expand Down
3 changes: 2 additions & 1 deletion src/resources/resources.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { Resource } from './entities/resource.entity';
import { ResourcesService } from './resources.service';
import { TasksModule } from 'omniboxd/tasks/tasks.module';
import { FilesModule } from 'omniboxd/files/files.module';

@Module({
imports: [TypeOrmModule.forFeature([Resource]), TasksModule],
imports: [TypeOrmModule.forFeature([Resource]), TasksModule, FilesModule],
providers: [ResourcesService],
exports: [ResourcesService],
})
Expand Down
14 changes: 14 additions & 0 deletions src/resources/resources.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { DataSource, EntityManager, In, Repository } from 'typeorm';
import { ResourceMetaDto } from './dto/resource-meta.dto';
import { WizardTaskService } from 'omniboxd/tasks/wizard-task.service';
import { Task } from 'omniboxd/tasks/tasks.entity';
import { FilesService } from 'omniboxd/files/files.service';

const TASK_PRIORITY = 5;

Expand All @@ -18,6 +19,7 @@ export class ResourcesService {
private readonly resourceRepository: Repository<Resource>,
private readonly wizardTaskService: WizardTaskService,
private readonly i18n: I18nService,
private readonly filesService: FilesService,
) {}

async getParentResourcesOrFail(
Expand Down Expand Up @@ -345,6 +347,18 @@ export class ResourcesService {
);
}

if (props.fileId) {
const fileMeta = await this.filesService.headFile(props.fileId);
if (!fileMeta) {
const message = this.i18n.t('resource.errors.fileNotFound');
throw new AppException(
message,
'FILE_NOT_FOUND',
HttpStatus.UNPROCESSABLE_ENTITY,
);
}
}

// Create the resource
const repo = entityManager.getRepository(Resource);
const resource = await repo.save(repo.create(props));
Expand Down
5 changes: 0 additions & 5 deletions src/s3/s3.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,17 +211,12 @@ export class S3Service implements OnModuleInit {
async generateUploadForm(
key: string,
isPublic: boolean,
contentType?: string,
contentDisposition?: string,
maxSize?: number,
): Promise<PresignedPost> {
const s3Client = isPublic ? this.s3PublicClient : this.s3Client;
const conditions: Conditions[] = [];
const fields: Record<string, string> = {};
if (contentType) {
conditions.push({ 'content-type': contentType });
fields['content-type'] = contentType;
}
if (contentDisposition) {
conditions.push({ 'content-disposition': contentDisposition });
fields['content-disposition'] = contentDisposition;
Expand Down