feat: dedup wave 2 — file factory + V1/V2 mapping + bot handler
Deploy FileDrop / deploy (push) Successful in 42s
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:
@@ -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 });
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user