diff --git a/src/application/use-cases/multipart-upload.ts b/src/application/use-cases/multipart-upload.ts index 856cc9c..4db78ea 100644 --- a/src/application/use-cases/multipart-upload.ts +++ b/src/application/use-cases/multipart-upload.ts @@ -1,4 +1,5 @@ import { nanoid } from 'nanoid'; +import { buildNewFile } from '../../domain/entities/file-factory'; import type { MultipartUpload } from '../../domain/entities/multipart'; import type { IBucketRepository } from '../../domain/ports/bucket-repository'; import type { IFileRepository } from '../../domain/ports/file-repository'; @@ -270,31 +271,23 @@ export function createCompleteMultipartUploadUseCase(deps: MultipartDeps) { const publicId = nanoid(); - await deps.fileRepo.create({ - publicId, - telegramFileId: firstPart.telegramFileId, - telegramFileUniqueId: firstPart.telegramFileUniqueId, - storageChatId: deps.config.storageChatId, - storageMessageId: firstPart.storageMessageId, - fileName: input.key.split('/').pop() || 'file', - mimeType: 'application/octet-stream', - sizeBytes: totalSize, - fileType: 'document', - uploaderId: 0, - fileHash: null, - archiveTelegramFileId: null, - archiveStorageMessageId: null, - archiveFileName: null, - archiveEntryName: null, - archiveMimeType: null, - archiveSizeBytes: null, - bucketId: multipart.bucketId, - s3Key: input.key, - storageBackend: 'telegram', - isDeleted: false, - multipartUploadId: input.uploadId, - partCount: null, - }); + await deps.fileRepo.create( + buildNewFile({ + publicId, + telegramFileId: firstPart.telegramFileId, + telegramFileUniqueId: firstPart.telegramFileUniqueId, + storageChatId: deps.config.storageChatId, + storageMessageId: firstPart.storageMessageId, + fileName: input.key.split('/').pop() || 'file', + mimeType: 'application/octet-stream', + sizeBytes: totalSize, + fileType: 'document', + storageBackend: 'telegram', + bucketId: multipart.bucketId, + s3Key: input.key, + multipartUploadId: input.uploadId, + }), + ); await deps.multipartRepo.complete(input.uploadId); diff --git a/src/application/use-cases/s3-object.ts b/src/application/use-cases/s3-object.ts index 5af1fb1..7cc411b 100644 --- a/src/application/use-cases/s3-object.ts +++ b/src/application/use-cases/s3-object.ts @@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto'; import { gzipSync } from 'node:zlib'; import { nanoid } from 'nanoid'; import type { File } from '../../domain/entities/file'; +import { buildNewFile } from '../../domain/entities/file-factory'; import type { NewFilePart } from '../../domain/entities/file-part'; import type { MultipartPart } from '../../domain/entities/multipart'; import type { IBucketRepository } from '../../domain/ports/bucket-repository'; @@ -482,31 +483,24 @@ export function createPutObjectUseCase(deps: S3ObjectDeps) { const fileId = randomUUID(); const publicId = nanoid(); - await deps.fileRepo.create({ - publicId, - telegramFileId: firstPart.telegramFileId, - telegramFileUniqueId: firstPart.telegramFileUniqueId, - storageChatId, - storageMessageId: firstPart.storageMessageId, - fileName: finalFileName, - mimeType, - sizeBytes: chunkResult.totalSizeBytes, - fileType: 'document', - uploaderId: 0, - fileHash: chunkResult.fileHash, - archiveTelegramFileId: null, - archiveStorageMessageId: null, - archiveFileName: null, - archiveEntryName: null, - archiveMimeType: null, - archiveSizeBytes: null, - bucketId: bucket.id, - s3Key: key, - storageBackend: 'chunked', - isDeleted: false, - multipartUploadId: null, - partCount: chunkResult.parts.length, - }); + await deps.fileRepo.create( + buildNewFile({ + publicId, + telegramFileId: firstPart.telegramFileId, + telegramFileUniqueId: firstPart.telegramFileUniqueId, + storageChatId, + storageMessageId: firstPart.storageMessageId, + fileName: finalFileName, + mimeType, + sizeBytes: chunkResult.totalSizeBytes, + fileType: 'document', + fileHash: chunkResult.fileHash, + bucketId: bucket.id, + s3Key: key, + storageBackend: 'chunked', + partCount: chunkResult.parts.length, + }), + ); const fileParts: NewFilePart[] = chunkResult.parts.map((part) => ({ fileId, @@ -535,31 +529,24 @@ export function createPutObjectUseCase(deps: S3ObjectDeps) { const publicId = nanoid(); - await deps.fileRepo.create({ - publicId, - telegramFileId: forwardResult.telegramFileId, - telegramFileUniqueId: forwardResult.telegramFileUniqueId, - storageChatId, - storageMessageId: forwardResult.storageMessageId, - fileName: finalFileName, - mimeType, - sizeBytes: body.byteLength, - fileType: 'document', - uploaderId: 0, - fileHash: hash, - archiveTelegramFileId: null, - archiveStorageMessageId: null, - archiveFileName: null, - archiveEntryName: null, - archiveMimeType: null, - archiveSizeBytes: null, - bucketId: bucket.id, - s3Key: key, - storageBackend: 'telegram', - isDeleted: false, - multipartUploadId: null, - partCount: null, - }); + await deps.fileRepo.create( + buildNewFile({ + publicId, + telegramFileId: forwardResult.telegramFileId, + telegramFileUniqueId: forwardResult.telegramFileUniqueId, + storageChatId, + storageMessageId: forwardResult.storageMessageId, + fileName: finalFileName, + mimeType, + sizeBytes: body.byteLength, + fileType: 'document', + fileHash: hash, + bucketId: bucket.id, + s3Key: key, + storageBackend: 'telegram', + partCount: null, + }), + ); return { etag: `"${hash}"` }; }; @@ -625,31 +612,30 @@ export function createCopyObjectUseCase(deps: S3ObjectDeps) { const publicId = nanoid(); - await deps.fileRepo.create({ - publicId, - telegramFileId: sourceFile.telegramFileId, - telegramFileUniqueId: sourceFile.telegramFileUniqueId, - storageChatId: sourceFile.storageChatId, - storageMessageId: sourceFile.storageMessageId, - fileName: sourceFile.fileName, - mimeType: sourceFile.mimeType, - sizeBytes: sourceFile.sizeBytes, - fileType: sourceFile.fileType, - uploaderId: 0, - fileHash: sourceFile.fileHash, - archiveTelegramFileId: sourceFile.archiveTelegramFileId, - archiveStorageMessageId: sourceFile.archiveStorageMessageId, - archiveFileName: sourceFile.archiveFileName, - archiveEntryName: sourceFile.archiveEntryName, - archiveMimeType: sourceFile.archiveMimeType, - archiveSizeBytes: sourceFile.archiveSizeBytes, - bucketId: input.destBucketId, - s3Key: input.destKey, - storageBackend: 'telegram', - isDeleted: false, - multipartUploadId: null, - partCount: null, - }); + await deps.fileRepo.create( + buildNewFile({ + publicId, + telegramFileId: sourceFile.telegramFileId, + telegramFileUniqueId: sourceFile.telegramFileUniqueId, + storageChatId: sourceFile.storageChatId, + storageMessageId: sourceFile.storageMessageId, + fileName: sourceFile.fileName, + mimeType: sourceFile.mimeType, + sizeBytes: sourceFile.sizeBytes, + fileType: sourceFile.fileType, + fileHash: sourceFile.fileHash, + archiveTelegramFileId: sourceFile.archiveTelegramFileId, + archiveStorageMessageId: sourceFile.archiveStorageMessageId, + archiveFileName: sourceFile.archiveFileName, + archiveEntryName: sourceFile.archiveEntryName, + archiveMimeType: sourceFile.archiveMimeType, + archiveSizeBytes: sourceFile.archiveSizeBytes, + bucketId: input.destBucketId, + s3Key: input.destKey, + storageBackend: 'telegram', + partCount: null, + }), + ); return { etag: sourceEtag || nanoid(16), diff --git a/src/application/use-cases/upload-file.ts b/src/application/use-cases/upload-file.ts index bcf3a94..f4ffcc8 100644 --- a/src/application/use-cases/upload-file.ts +++ b/src/application/use-cases/upload-file.ts @@ -1,6 +1,7 @@ import { createReadStream } from 'node:fs'; import { open } from 'node:fs/promises'; import { nanoid } from 'nanoid'; +import { buildNewFile } from '../../domain/entities/file-factory'; import type { NewFilePart } from '../../domain/entities/file-part'; import type { IFilePartRepository } from '../../domain/ports/file-part-repository'; import type { IFileRepository } from '../../domain/ports/file-repository'; @@ -231,31 +232,25 @@ export function createUploadFileUseCase(deps: UploadFileUseCaseDeps) { const fileId = nanoid(); const publicId = nanoid(); - const newFile = await deps.fileRepo.create({ - publicId, - telegramFileId: firstPart.telegramFileId, - telegramFileUniqueId: firstPart.telegramFileUniqueId, - storageChatId: deps.config.storageChatId, - storageMessageId: firstPart.storageMessageId, - fileName: finalFileName, - mimeType, - sizeBytes: chunkResult.totalSizeBytes, - fileType, - uploaderId: input.uploaderId ?? 0, - fileHash: chunkResult.fileHash, - archiveTelegramFileId: null, - archiveStorageMessageId: null, - archiveFileName: null, - archiveEntryName: null, - archiveMimeType: null, - archiveSizeBytes: null, - bucketId: input.bucketId ?? null, - s3Key: input.s3Key ?? null, - storageBackend: 'chunked', - isDeleted: false, - multipartUploadId: null, - partCount: chunkResult.parts.length, - }); + const newFile = await deps.fileRepo.create( + buildNewFile({ + publicId, + telegramFileId: firstPart.telegramFileId, + telegramFileUniqueId: firstPart.telegramFileUniqueId, + storageChatId: deps.config.storageChatId, + storageMessageId: firstPart.storageMessageId, + fileName: finalFileName, + mimeType, + sizeBytes: chunkResult.totalSizeBytes, + fileType, + storageBackend: 'chunked', + uploaderId: input.uploaderId, + fileHash: chunkResult.fileHash, + bucketId: input.bucketId, + s3Key: input.s3Key, + partCount: chunkResult.parts.length, + }), + ); const fileParts: NewFilePart[] = chunkResult.parts.map((part) => ({ fileId, @@ -292,31 +287,24 @@ export function createUploadFileUseCase(deps: UploadFileUseCaseDeps) { const singlePublicId = nanoid(); - const createdFile = await deps.fileRepo.create({ - publicId: singlePublicId, - telegramFileId: forwardResult.telegramFileId, - telegramFileUniqueId: forwardResult.telegramFileUniqueId, - storageChatId: deps.config.storageChatId, - storageMessageId: forwardResult.storageMessageId, - fileName: finalFileName, - mimeType, - sizeBytes: input.sizeBytes, - fileType, - uploaderId: input.uploaderId ?? 0, - fileHash: input.fileHash, - archiveTelegramFileId: null, - archiveStorageMessageId: null, - archiveFileName: null, - archiveEntryName: null, - archiveMimeType: null, - archiveSizeBytes: null, - bucketId: input.bucketId ?? null, - s3Key: input.s3Key ?? null, - storageBackend: 'telegram', - isDeleted: false, - multipartUploadId: null, - partCount: null, - }); + const createdFile = await deps.fileRepo.create( + buildNewFile({ + publicId: singlePublicId, + telegramFileId: forwardResult.telegramFileId, + telegramFileUniqueId: forwardResult.telegramFileUniqueId, + storageChatId: deps.config.storageChatId, + storageMessageId: forwardResult.storageMessageId, + fileName: finalFileName, + mimeType, + sizeBytes: input.sizeBytes, + fileType, + storageBackend: 'telegram', + uploaderId: input.uploaderId, + fileHash: input.fileHash, + bucketId: input.bucketId, + s3Key: input.s3Key, + }), + ); return { publicId: createdFile.publicId, diff --git a/src/domain/entities/file-factory.ts b/src/domain/entities/file-factory.ts new file mode 100644 index 0000000..59c4e2d --- /dev/null +++ b/src/domain/entities/file-factory.ts @@ -0,0 +1,85 @@ +/** + * Factory function for building NewFile records with sensible defaults. + * + * Most call sites set the same null defaults for archive/S3/soft-delete fields. + * This factory eliminates ~20 lines of boilerplate per call site (~27 sites). + */ +import type { NewFile } from './file'; + +/** + * Partial input for creating a file record. + * Only the required unique fields must be provided; optional fields default to null/0/false. + */ +export interface FileInput { + publicId: string; + telegramFileId: string; + telegramFileUniqueId: string; + storageChatId: number; + storageMessageId: number; + fileName: string; + mimeType: string; + sizeBytes: number; + fileType: string; + storageBackend: string | null; + /** Optional overrides */ + uploaderId?: number; + fileHash?: string | null; + bucketId?: string | null; + s3Key?: string | null; + partCount?: number | null; + multipartUploadId?: string | null; + /** Archive fields (for batch/zip archives) */ + archiveTelegramFileId?: string | null; + archiveStorageMessageId?: number | null; + archiveFileName?: string | null; + archiveEntryName?: string | null; + archiveMimeType?: string | null; + archiveSizeBytes?: number | null; +} + +/** + * Build a NewFile record, filling in null/zero defaults for omitted fields. + * + * @example + * ```ts + * await fileRepo.create(buildNewFile({ + * publicId, + * telegramFileId: result.telegramFileId, + * telegramFileUniqueId: result.telegramFileUniqueId, + * storageChatId, + * storageMessageId: result.storageMessageId, + * fileName: input.fileName, + * mimeType, + * sizeBytes: input.sizeBytes, + * fileType, + * storageBackend: 'telegram', + * uploaderId: input.uploaderId, + * fileHash: input.fileHash, + * })); + * ``` + */ +export const buildNewFile = (input: FileInput): NewFile => ({ + publicId: input.publicId, + telegramFileId: input.telegramFileId, + telegramFileUniqueId: input.telegramFileUniqueId, + storageChatId: input.storageChatId, + storageMessageId: input.storageMessageId, + fileName: input.fileName, + mimeType: input.mimeType, + sizeBytes: input.sizeBytes, + fileType: input.fileType, + uploaderId: input.uploaderId ?? 0, + fileHash: input.fileHash ?? null, + archiveTelegramFileId: input.archiveTelegramFileId ?? null, + archiveStorageMessageId: input.archiveStorageMessageId ?? null, + archiveFileName: input.archiveFileName ?? null, + archiveEntryName: input.archiveEntryName ?? null, + archiveMimeType: input.archiveMimeType ?? null, + archiveSizeBytes: input.archiveSizeBytes ?? null, + bucketId: input.bucketId ?? null, + s3Key: input.s3Key ?? null, + storageBackend: input.storageBackend, + isDeleted: false, + multipartUploadId: input.multipartUploadId ?? null, + partCount: input.partCount ?? null, +}); diff --git a/src/infrastructure/telegram/chunked-storage.ts b/src/infrastructure/telegram/chunked-storage.ts index b4d34fc..9a5faab 100644 --- a/src/infrastructure/telegram/chunked-storage.ts +++ b/src/infrastructure/telegram/chunked-storage.ts @@ -1,6 +1,7 @@ import { createReadStream } from 'node:fs'; import { nanoid } from 'nanoid'; import type { File as FileEntity } from '../../domain/entities/file'; +import { buildNewFile } from '../../domain/entities/file-factory'; import type { NewFilePart } from '../../domain/entities/file-part'; import type { IFilePartRepository } from '../../domain/ports/file-part-repository'; import type { IFileRepository } from '../../domain/ports/file-repository'; @@ -193,31 +194,25 @@ export class ChunkedStorage { const publicId = nanoid(); - const file = await this.fileRepository.create({ - publicId, - telegramFileId: firstPart.telegramFileId, - telegramFileUniqueId: firstPart.telegramFileUniqueId, - storageChatId: config.storageChatId, - storageMessageId: firstPart.storageMessageId, - fileName: input.fileName, - mimeType: input.mimeType, - sizeBytes: upload.totalSizeBytes, - fileType: input.fileType, - uploaderId: input.uploaderId, - fileHash: upload.fileHash, - archiveTelegramFileId: null, - archiveStorageMessageId: null, - archiveFileName: null, - archiveEntryName: null, - archiveMimeType: null, - archiveSizeBytes: null, - bucketId: input.bucketId ?? null, - s3Key: input.s3Key ?? null, - storageBackend: 'chunked', - isDeleted: false, - multipartUploadId: null, - partCount: upload.parts.length, - }); + const file = await this.fileRepository.create( + buildNewFile({ + publicId, + telegramFileId: firstPart.telegramFileId, + telegramFileUniqueId: firstPart.telegramFileUniqueId, + storageChatId: config.storageChatId, + storageMessageId: firstPart.storageMessageId, + fileName: input.fileName, + mimeType: input.mimeType, + sizeBytes: upload.totalSizeBytes, + fileType: input.fileType, + storageBackend: 'chunked', + uploaderId: input.uploaderId, + fileHash: upload.fileHash, + bucketId: input.bucketId, + s3Key: input.s3Key, + partCount: upload.parts.length, + }), + ); const fileParts: NewFilePart[] = upload.parts.map((part) => ({ fileId: file.id, diff --git a/src/infrastructure/telegram/upload-batcher.ts b/src/infrastructure/telegram/upload-batcher.ts index f208211..62f4692 100644 --- a/src/infrastructure/telegram/upload-batcher.ts +++ b/src/infrastructure/telegram/upload-batcher.ts @@ -1,6 +1,7 @@ import { createReadStream } from 'node:fs'; import { nanoid } from 'nanoid'; import type { File as FileEntity, NewFile } from '../../domain/entities/file'; +import { buildNewFile } from '../../domain/entities/file-factory'; import type { IFileRepository } from '../../domain/ports/file-repository'; import type { ITelegramService } from '../../domain/ports/telegram-service'; import { config } from '../../env'; @@ -92,7 +93,7 @@ export class UploadBatcher { sizeBytes: number; }, ): NewFile { - return { + return buildNewFile({ publicId: nanoid(), telegramFileId: archive.telegramFileId, telegramFileUniqueId: archive.telegramFileUniqueId, @@ -102,7 +103,7 @@ export class UploadBatcher { mimeType: item.mimeType || 'application/octet-stream', sizeBytes: item.prepared.sizeBytes, fileType: item.fileType, - uploaderId: 0, + storageBackend: null, fileHash: item.prepared.fileHash, archiveTelegramFileId: archive.telegramFileId, archiveStorageMessageId: archive.storageMessageId, @@ -110,13 +111,7 @@ export class UploadBatcher { archiveEntryName: entry.entryName, archiveMimeType: 'application/zip', archiveSizeBytes: archive.sizeBytes, - bucketId: null, - s3Key: null, - storageBackend: null, - isDeleted: null, - multipartUploadId: null, - partCount: null, - }; + }); } /** diff --git a/src/interfaces/bot/handler.ts b/src/interfaces/bot/handler.ts index 488dabd..69f5375 100644 --- a/src/interfaces/bot/handler.ts +++ b/src/interfaces/bot/handler.ts @@ -1,6 +1,6 @@ import { nanoid } from 'nanoid'; import { type Context, Telegraf } from 'telegraf'; -import type { NewFile } from '../../domain/entities/file'; +import { buildNewFile } from '../../domain/entities/file-factory'; import type { IFileRepository } from '../../domain/ports/file-repository'; import type { ITelegramService } from '../../domain/ports/telegram-service'; import { config } from '../../env'; @@ -8,6 +8,7 @@ import { DrizzleFileRepository } from '../../infrastructure/persistence/reposito import { botPool } from '../../infrastructure/telegram/bot-pool'; import logger from '../../shared/logger/index'; import { + checkFileSize, detectFileType, extractFileFromMessage, getErrorMessage, @@ -127,10 +128,10 @@ export async function startBot( ctx.message.voice?.file_name || 'file'; - const maxSize = getFileSizeLimit(fileType); - - if (fileSize > maxSize) { - return ctx.reply(`File size exceeds ${maxSize / (1024 * 1024)}MB limit`); + if (!checkFileSize(fileSize, fileType)) { + return ctx.reply( + `File size exceeds ${getFileSizeLimit(fileType) / (1024 * 1024)}MB limit`, + ); } const existing = await fileRepo.findByUniqueId(fileObj.file_unique_id); @@ -149,33 +150,21 @@ export async function startBot( const result = await telegramService.forwardToStorage(file_id, fileName, fileType); const publicId = nanoid(); - const uploaded: NewFile = { - publicId, - telegramFileId: result.telegramFileId, - telegramFileUniqueId: result.telegramFileUniqueId, - storageChatId: config.storageChatId, - storageMessageId: result.storageMessageId, - fileName, - mimeType: mime_type || 'application/octet-stream', - sizeBytes: fileSize, - fileType, - uploaderId: ctx.from.id, - fileHash: null, - archiveTelegramFileId: null, - archiveStorageMessageId: null, - archiveFileName: null, - archiveEntryName: null, - archiveMimeType: null, - archiveSizeBytes: null, - bucketId: null, - s3Key: null, - storageBackend: 'telegram', - isDeleted: false, - multipartUploadId: null, - partCount: null, - }; - - await fileRepo.create(uploaded); + await fileRepo.create( + buildNewFile({ + publicId, + telegramFileId: result.telegramFileId, + telegramFileUniqueId: result.telegramFileUniqueId, + storageChatId: config.storageChatId, + storageMessageId: result.storageMessageId, + fileName, + mimeType: mime_type || 'application/octet-stream', + sizeBytes: fileSize, + fileType, + uploaderId: ctx.from.id, + storageBackend: 'telegram', + }), + ); await replyWithDownloadUrl(ctx, publicId); diff --git a/src/interfaces/http/controllers/s3-controller.ts b/src/interfaces/http/controllers/s3-controller.ts index 2efed5b..903c30a 100644 --- a/src/interfaces/http/controllers/s3-controller.ts +++ b/src/interfaces/http/controllers/s3-controller.ts @@ -1,6 +1,7 @@ import { createReadStream } from 'node:fs'; import { nanoid } from 'nanoid'; import type { File as FileEntity } from '../../../domain/entities/file'; +import { buildNewFile } from '../../../domain/entities/file-factory'; import type { ForwardResult } from '../../../domain/ports/telegram-service'; import { config } from '../../../env'; import { @@ -9,7 +10,6 @@ import { fileRepository, multipartRepository, } from '../../../infrastructure/di'; -import { db, files as fileSchema } from '../../../infrastructure/persistence/drizzle/index'; import { botPool } from '../../../infrastructure/telegram/bot-pool'; import logger from '../../../shared/logger/index'; import { cleanupTempFile, ensureExtension, getErrorMessage } from '../../../shared/utils/file'; @@ -1070,25 +1070,24 @@ const storeFileFromTemp = async ( const publicId = nanoid(); - await db.insert(fileSchema).values({ - publicId, - telegramFileId: forwardResult.telegramFileId, - telegramFileUniqueId: forwardResult.telegramFileUniqueId, - storageChatId: config.storageChatId, - storageMessageId: forwardResult.storageMessageId, - fileName: finalFileName, - mimeType, - sizeBytes: streamed.sizeBytes, - fileType: 'document', - uploaderId: 0, - fileHash: streamed.fileHash, - bucketId, - s3Key: key, - storageBackend: 'telegram', - isDeleted: false, - createdAt: new Date(), - updatedAt: new Date(), - }); + await fileRepository.create( + buildNewFile({ + publicId, + telegramFileId: forwardResult.telegramFileId, + telegramFileUniqueId: forwardResult.telegramFileUniqueId, + storageChatId: config.storageChatId, + storageMessageId: forwardResult.storageMessageId, + fileName: finalFileName, + mimeType, + sizeBytes: streamed.sizeBytes, + fileType: 'document', + uploaderId: 0, + fileHash: streamed.fileHash, + bucketId, + s3Key: key, + storageBackend: 'telegram', + }), + ); await cleanupTempFile(streamed.tempPath); @@ -1181,25 +1180,24 @@ const handleCopyObject = async ( const publicId = nanoid(); - await db.insert(fileSchema).values({ - publicId, - telegramFileId: sourceFile.telegramFileId, - telegramFileUniqueId: sourceFile.telegramFileUniqueId, - storageChatId: sourceFile.storageChatId, - storageMessageId: sourceFile.storageMessageId, - fileName: sourceFile.fileName, - mimeType: sourceFile.mimeType, - sizeBytes: sourceFile.sizeBytes, - fileType: sourceFile.fileType, - uploaderId: 0, - fileHash: sourceFile.fileHash, - bucketId: destBucketId, - s3Key: destKey, - storageBackend: 'telegram', - isDeleted: false, - createdAt: new Date(), - updatedAt: new Date(), - }); + await fileRepository.create( + buildNewFile({ + publicId, + telegramFileId: sourceFile.telegramFileId, + telegramFileUniqueId: sourceFile.telegramFileUniqueId, + storageChatId: sourceFile.storageChatId, + storageMessageId: sourceFile.storageMessageId, + fileName: sourceFile.fileName, + mimeType: sourceFile.mimeType, + sizeBytes: sourceFile.sizeBytes, + fileType: sourceFile.fileType, + uploaderId: 0, + fileHash: sourceFile.fileHash, + bucketId: destBucketId, + s3Key: destKey, + storageBackend: 'telegram', + }), + ); const xml = copyObjectResultXml(sourceFile.fileHash || nanoid(16), new Date()); return s3Response(xml, 200, reqId, { 'content-type': 'application/xml' }); @@ -1337,13 +1335,7 @@ const handleListObjectsV1 = async ( const xml = listBucketResultXml( bucket, - displayObjects.map((o) => ({ - key: o.s3Key ?? '', - sizeBytes: o.sizeBytes, - etag: o.fileHash || nanoid(16), - lastModified: o.createdAt instanceof Date ? o.createdAt : new Date(), - mimeType: o.mimeType, - })), + displayObjects.map(mapFileToListEntry), commonPrefixes, isTruncated, marker, @@ -1358,6 +1350,29 @@ const handleListObjectsV1 = async ( return s3Response(xml, 200, reqId, { 'content-type': 'application/xml' }); }; +/** Shape of an S3 list entry object. */ +type S3ListEntry = { + key: string; + sizeBytes: number; + etag: string; + lastModified: Date; + mimeType: string; +}; + +/** + * Maps a File entity to an S3 list entry object. + * + * @param file - The file entity from the repository. + * @returns An S3 list entry with key, size, etag, last modified, and MIME type. + */ +const mapFileToListEntry = (file: FileEntity): S3ListEntry => ({ + key: file.s3Key ?? '', + sizeBytes: file.sizeBytes, + etag: file.fileHash || nanoid(16), + lastModified: file.createdAt instanceof Date ? file.createdAt : new Date(), + mimeType: file.mimeType, +}); + /** * Handles GET /{bucket}?list-type=2 (ListObjectsV2). * @@ -1405,13 +1420,7 @@ const handleListObjectsV2 = async ( const xml = listBucketV2ResultXml( bucket, - displayObjects.map((o) => ({ - key: o.s3Key ?? '', - sizeBytes: o.sizeBytes, - etag: o.fileHash || nanoid(16), - lastModified: o.createdAt instanceof Date ? o.createdAt : new Date(), - mimeType: o.mimeType, - })), + displayObjects.map(mapFileToListEntry), commonPrefixes, isTruncated, maxKeys, @@ -1668,25 +1677,24 @@ const handleCompleteMultipartUpload = async ( // M7: Use stored content-type from the multipart record if available const mimeType = multipart.contentType || 'application/octet-stream'; - await db.insert(fileSchema).values({ - publicId, - telegramFileId: storedParts[0]!.telegramFileId, - telegramFileUniqueId: storedParts[0]!.telegramFileUniqueId, - storageChatId: config.storageChatId, - storageMessageId: storedParts[0]!.storageMessageId, - fileName: key.split('/').pop() || 'file', - mimeType, - sizeBytes: totalSize, - fileType: 'document', - uploaderId: 0, - bucketId: multipart.bucketId, - s3Key: key, - storageBackend: 'telegram', - isDeleted: false, - multipartUploadId: uploadId, - createdAt: new Date(), - updatedAt: new Date(), - }); + await fileRepository.create( + buildNewFile({ + publicId, + telegramFileId: storedParts[0]!.telegramFileId, + telegramFileUniqueId: storedParts[0]!.telegramFileUniqueId, + storageChatId: config.storageChatId, + storageMessageId: storedParts[0]!.storageMessageId, + fileName: key.split('/').pop() || 'file', + mimeType, + sizeBytes: totalSize, + fileType: 'document', + uploaderId: 0, + bucketId: multipart.bucketId, + s3Key: key, + storageBackend: 'telegram', + multipartUploadId: uploadId, + }), + ); await multipartRepository.complete(uploadId); diff --git a/src/interfaces/http/controllers/web-api-controller.ts b/src/interfaces/http/controllers/web-api-controller.ts index 78b7515..d928e18 100644 --- a/src/interfaces/http/controllers/web-api-controller.ts +++ b/src/interfaces/http/controllers/web-api-controller.ts @@ -1,8 +1,8 @@ import { createReadStream } from 'node:fs'; import { nanoid } from 'nanoid'; +import { buildNewFile } from '../../../domain/entities/file-factory'; import { config } from '../../../env'; import { bucketRepository, chunkedStorage, fileRepository } from '../../../infrastructure/di'; -import { db, files as fileSchema } from '../../../infrastructure/persistence/drizzle/index'; import { botPool } from '../../../infrastructure/telegram/bot-pool'; import logger from '../../../shared/logger/index'; import { cleanupTempFile, ensureExtension, getErrorMessage } from '../../../shared/utils/file'; @@ -238,25 +238,24 @@ export const handleUploadObjectV1 = async ( const publicId = nanoid(); - await db.insert(fileSchema).values({ - publicId, - telegramFileId: forwardResult.telegramFileId, - telegramFileUniqueId: forwardResult.telegramFileUniqueId, - storageChatId: config.storageChatId, - storageMessageId: forwardResult.storageMessageId, - fileName: finalFileName, - mimeType, - sizeBytes, - fileType: 'document', - uploaderId: 0, - fileHash: hash, - bucketId: bucket.id, - s3Key: key, - storageBackend: 'telegram', - isDeleted: false, - createdAt: new Date(), - updatedAt: new Date(), - }); + await fileRepository.create( + buildNewFile({ + publicId, + telegramFileId: forwardResult.telegramFileId, + telegramFileUniqueId: forwardResult.telegramFileUniqueId, + storageChatId: config.storageChatId, + storageMessageId: forwardResult.storageMessageId, + fileName: finalFileName, + mimeType, + sizeBytes, + fileType: 'document', + uploaderId: 0, + fileHash: hash, + bucketId: bucket.id, + s3Key: key, + storageBackend: 'telegram', + }), + ); await cleanupTempFile(tempPath); @@ -352,25 +351,24 @@ export const handleCopyObjectV1 = async (req: Request, params: RouteParams): Pro const publicId = nanoid(); - await db.insert(fileSchema).values({ - publicId, - telegramFileId: sourceFile.telegramFileId, - telegramFileUniqueId: sourceFile.telegramFileUniqueId, - storageChatId: sourceFile.storageChatId, - storageMessageId: sourceFile.storageMessageId, - fileName: sourceFile.fileName, - mimeType: sourceFile.mimeType, - sizeBytes: sourceFile.sizeBytes, - fileType: sourceFile.fileType, - uploaderId: 0, - fileHash: sourceFile.fileHash, - bucketId: destBucket.id, - s3Key: body.destKey, - storageBackend: 'telegram', - isDeleted: false, - createdAt: new Date(), - updatedAt: new Date(), - }); + await fileRepository.create( + buildNewFile({ + publicId, + telegramFileId: sourceFile.telegramFileId, + telegramFileUniqueId: sourceFile.telegramFileUniqueId, + storageChatId: sourceFile.storageChatId, + storageMessageId: sourceFile.storageMessageId, + fileName: sourceFile.fileName, + mimeType: sourceFile.mimeType, + sizeBytes: Number(sourceFile.sizeBytes), + fileType: sourceFile.fileType, + uploaderId: 0, + fileHash: sourceFile.fileHash, + bucketId: destBucket.id, + s3Key: body.destKey, + storageBackend: 'telegram', + }), + ); return json({ sourceKey: body.sourceKey, destKey: body.destKey, destBucket: destBucketName }); };