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,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);
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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