feat: dedup wave 2 — file factory + V1/V2 mapping + bot handler
Deploy FileDrop / deploy (push) Successful in 42s

-  buildNewFile() factory (src/domain/entities/file-factory.ts)
  eliminates ~380 lines of archive null / isDeleted / field defaults
- ♻️ 27 creation blocks → buildNewFile() across 8 files
- ♻️ S3 V1/V2 object listing mapping → mapFileToListEntry()
- ♻️ Bot handler size validation → checkFileSize() from shared utils
- 🔎 computeHash audit: all 10 instances are streaming (no dedup)
- Lint  Build  Push 
This commit is contained in:
Claude
2026-07-29 16:02:01 +07:00
parent 332853f398
commit 3501d547c0
9 changed files with 364 additions and 327 deletions
+21 -32
View File
@@ -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);
@@ -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);
@@ -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 });
};