Files
lislgosms/api/src/files/files.service.ts
T

112 lines
3.5 KiB
TypeScript

import { Injectable, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { randomUUID } from 'node:crypto';
import { PrismaService } from '../prisma/prisma.service';
import { ObjectStorageService } from './object-storage.service';
export interface CreateFileObjectDto {
tenantId?: string;
bucket: string;
objectKey: string;
fileName: string;
contentType: string;
sizeBytes: number | string;
checksum?: string;
purpose: string;
}
export interface CreatePresignedUploadDto {
objectKey: string;
expiresInSeconds?: number;
}
export interface UploadFileDto {
tenantId?: string;
purpose: string;
prefix?: string;
}
@Injectable()
export class FilesService {
constructor(
private readonly prisma: PrismaService,
private readonly objectStorage: ObjectStorageService,
) {}
list(tenantId?: string) {
return this.prisma.fileObject.findMany({
where: tenantId ? { tenantId } : undefined,
orderBy: { createdAt: 'desc' },
take: 100,
}).then((items) => items.map(serializeFileObject));
}
create(data: CreateFileObjectDto) {
const createData: Prisma.FileObjectUncheckedCreateInput = {
tenantId: data.tenantId,
bucket: data.bucket,
objectKey: data.objectKey,
fileName: data.fileName,
contentType: data.contentType,
sizeBytes: BigInt(data.sizeBytes),
checksum: data.checksum,
purpose: data.purpose,
};
return this.prisma.fileObject.create({ data: createData }).then(serializeFileObject);
}
async createPresignedUpload(data: CreatePresignedUploadDto) {
const uploadUrl = await this.objectStorage.presignedPutObject(data.objectKey, data.expiresInSeconds ?? 3600);
return {
bucket: this.objectStorage.getBucket(),
objectKey: data.objectKey,
uploadUrl,
expiresInSeconds: data.expiresInSeconds ?? 3600,
};
}
async upload(data: UploadFileDto, file: { originalname: string; mimetype: string; size: number; buffer: Buffer }) {
const fileName = normalizeMultipartFileName(file.originalname);
const safeName = fileName.replace(/[^\w.\-\u4e00-\u9fa5]/g, '_');
const objectKey = `${data.prefix ?? data.purpose}/${Date.now()}-${randomUUID()}-${safeName}`;
await this.objectStorage.putObject(objectKey, file.buffer, file.size, file.mimetype || 'application/octet-stream');
return this.create({
tenantId: data.tenantId,
bucket: this.objectStorage.getBucket(),
objectKey,
fileName,
contentType: file.mimetype || 'application/octet-stream',
sizeBytes: file.size,
purpose: data.purpose,
});
}
async getDownload(id: string) {
const fileObject = await this.prisma.fileObject.findUnique({ where: { id } });
if (!fileObject) {
throw new NotFoundException('File object not found');
}
const content = await this.objectStorage.getObject(fileObject.objectKey);
return {
fileObject: serializeFileObject(fileObject),
content,
};
}
}
function normalizeMultipartFileName(value: string) {
if (![...value].some((character) => character.charCodeAt(0) > 0x7f) || [...value].some((character) => character.charCodeAt(0) > 0xff)) {
return value;
}
const decoded = Buffer.from(value, 'latin1').toString('utf8');
return decoded.includes('\uFFFD') ? value : decoded;
}
function serializeFileObject<T extends { sizeBytes: bigint | number | string }>(fileObject: T) {
return {
...fileObject,
sizeBytes: fileObject.sizeBytes.toString(),
};
}