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 { nanoid } from 'nanoid';
|
||||||
|
import { buildNewFile } from '../../domain/entities/file-factory';
|
||||||
import type { MultipartUpload } from '../../domain/entities/multipart';
|
import type { MultipartUpload } from '../../domain/entities/multipart';
|
||||||
import type { IBucketRepository } from '../../domain/ports/bucket-repository';
|
import type { IBucketRepository } from '../../domain/ports/bucket-repository';
|
||||||
import type { IFileRepository } from '../../domain/ports/file-repository';
|
import type { IFileRepository } from '../../domain/ports/file-repository';
|
||||||
@@ -270,31 +271,23 @@ export function createCompleteMultipartUploadUseCase(deps: MultipartDeps) {
|
|||||||
|
|
||||||
const publicId = nanoid();
|
const publicId = nanoid();
|
||||||
|
|
||||||
await deps.fileRepo.create({
|
await deps.fileRepo.create(
|
||||||
publicId,
|
buildNewFile({
|
||||||
telegramFileId: firstPart.telegramFileId,
|
publicId,
|
||||||
telegramFileUniqueId: firstPart.telegramFileUniqueId,
|
telegramFileId: firstPart.telegramFileId,
|
||||||
storageChatId: deps.config.storageChatId,
|
telegramFileUniqueId: firstPart.telegramFileUniqueId,
|
||||||
storageMessageId: firstPart.storageMessageId,
|
storageChatId: deps.config.storageChatId,
|
||||||
fileName: input.key.split('/').pop() || 'file',
|
storageMessageId: firstPart.storageMessageId,
|
||||||
mimeType: 'application/octet-stream',
|
fileName: input.key.split('/').pop() || 'file',
|
||||||
sizeBytes: totalSize,
|
mimeType: 'application/octet-stream',
|
||||||
fileType: 'document',
|
sizeBytes: totalSize,
|
||||||
uploaderId: 0,
|
fileType: 'document',
|
||||||
fileHash: null,
|
storageBackend: 'telegram',
|
||||||
archiveTelegramFileId: null,
|
bucketId: multipart.bucketId,
|
||||||
archiveStorageMessageId: null,
|
s3Key: input.key,
|
||||||
archiveFileName: null,
|
multipartUploadId: input.uploadId,
|
||||||
archiveEntryName: null,
|
}),
|
||||||
archiveMimeType: null,
|
);
|
||||||
archiveSizeBytes: null,
|
|
||||||
bucketId: multipart.bucketId,
|
|
||||||
s3Key: input.key,
|
|
||||||
storageBackend: 'telegram',
|
|
||||||
isDeleted: false,
|
|
||||||
multipartUploadId: input.uploadId,
|
|
||||||
partCount: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
await deps.multipartRepo.complete(input.uploadId);
|
await deps.multipartRepo.complete(input.uploadId);
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto';
|
|||||||
import { gzipSync } from 'node:zlib';
|
import { gzipSync } from 'node:zlib';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import type { File } from '../../domain/entities/file';
|
import type { File } from '../../domain/entities/file';
|
||||||
|
import { buildNewFile } from '../../domain/entities/file-factory';
|
||||||
import type { NewFilePart } from '../../domain/entities/file-part';
|
import type { NewFilePart } from '../../domain/entities/file-part';
|
||||||
import type { MultipartPart } from '../../domain/entities/multipart';
|
import type { MultipartPart } from '../../domain/entities/multipart';
|
||||||
import type { IBucketRepository } from '../../domain/ports/bucket-repository';
|
import type { IBucketRepository } from '../../domain/ports/bucket-repository';
|
||||||
@@ -482,31 +483,24 @@ export function createPutObjectUseCase(deps: S3ObjectDeps) {
|
|||||||
const fileId = randomUUID();
|
const fileId = randomUUID();
|
||||||
const publicId = nanoid();
|
const publicId = nanoid();
|
||||||
|
|
||||||
await deps.fileRepo.create({
|
await deps.fileRepo.create(
|
||||||
publicId,
|
buildNewFile({
|
||||||
telegramFileId: firstPart.telegramFileId,
|
publicId,
|
||||||
telegramFileUniqueId: firstPart.telegramFileUniqueId,
|
telegramFileId: firstPart.telegramFileId,
|
||||||
storageChatId,
|
telegramFileUniqueId: firstPart.telegramFileUniqueId,
|
||||||
storageMessageId: firstPart.storageMessageId,
|
storageChatId,
|
||||||
fileName: finalFileName,
|
storageMessageId: firstPart.storageMessageId,
|
||||||
mimeType,
|
fileName: finalFileName,
|
||||||
sizeBytes: chunkResult.totalSizeBytes,
|
mimeType,
|
||||||
fileType: 'document',
|
sizeBytes: chunkResult.totalSizeBytes,
|
||||||
uploaderId: 0,
|
fileType: 'document',
|
||||||
fileHash: chunkResult.fileHash,
|
fileHash: chunkResult.fileHash,
|
||||||
archiveTelegramFileId: null,
|
bucketId: bucket.id,
|
||||||
archiveStorageMessageId: null,
|
s3Key: key,
|
||||||
archiveFileName: null,
|
storageBackend: 'chunked',
|
||||||
archiveEntryName: null,
|
partCount: chunkResult.parts.length,
|
||||||
archiveMimeType: null,
|
}),
|
||||||
archiveSizeBytes: null,
|
);
|
||||||
bucketId: bucket.id,
|
|
||||||
s3Key: key,
|
|
||||||
storageBackend: 'chunked',
|
|
||||||
isDeleted: false,
|
|
||||||
multipartUploadId: null,
|
|
||||||
partCount: chunkResult.parts.length,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fileParts: NewFilePart[] = chunkResult.parts.map((part) => ({
|
const fileParts: NewFilePart[] = chunkResult.parts.map((part) => ({
|
||||||
fileId,
|
fileId,
|
||||||
@@ -535,31 +529,24 @@ export function createPutObjectUseCase(deps: S3ObjectDeps) {
|
|||||||
|
|
||||||
const publicId = nanoid();
|
const publicId = nanoid();
|
||||||
|
|
||||||
await deps.fileRepo.create({
|
await deps.fileRepo.create(
|
||||||
publicId,
|
buildNewFile({
|
||||||
telegramFileId: forwardResult.telegramFileId,
|
publicId,
|
||||||
telegramFileUniqueId: forwardResult.telegramFileUniqueId,
|
telegramFileId: forwardResult.telegramFileId,
|
||||||
storageChatId,
|
telegramFileUniqueId: forwardResult.telegramFileUniqueId,
|
||||||
storageMessageId: forwardResult.storageMessageId,
|
storageChatId,
|
||||||
fileName: finalFileName,
|
storageMessageId: forwardResult.storageMessageId,
|
||||||
mimeType,
|
fileName: finalFileName,
|
||||||
sizeBytes: body.byteLength,
|
mimeType,
|
||||||
fileType: 'document',
|
sizeBytes: body.byteLength,
|
||||||
uploaderId: 0,
|
fileType: 'document',
|
||||||
fileHash: hash,
|
fileHash: hash,
|
||||||
archiveTelegramFileId: null,
|
bucketId: bucket.id,
|
||||||
archiveStorageMessageId: null,
|
s3Key: key,
|
||||||
archiveFileName: null,
|
storageBackend: 'telegram',
|
||||||
archiveEntryName: null,
|
partCount: null,
|
||||||
archiveMimeType: null,
|
}),
|
||||||
archiveSizeBytes: null,
|
);
|
||||||
bucketId: bucket.id,
|
|
||||||
s3Key: key,
|
|
||||||
storageBackend: 'telegram',
|
|
||||||
isDeleted: false,
|
|
||||||
multipartUploadId: null,
|
|
||||||
partCount: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
return { etag: `"${hash}"` };
|
return { etag: `"${hash}"` };
|
||||||
};
|
};
|
||||||
@@ -625,31 +612,30 @@ export function createCopyObjectUseCase(deps: S3ObjectDeps) {
|
|||||||
|
|
||||||
const publicId = nanoid();
|
const publicId = nanoid();
|
||||||
|
|
||||||
await deps.fileRepo.create({
|
await deps.fileRepo.create(
|
||||||
publicId,
|
buildNewFile({
|
||||||
telegramFileId: sourceFile.telegramFileId,
|
publicId,
|
||||||
telegramFileUniqueId: sourceFile.telegramFileUniqueId,
|
telegramFileId: sourceFile.telegramFileId,
|
||||||
storageChatId: sourceFile.storageChatId,
|
telegramFileUniqueId: sourceFile.telegramFileUniqueId,
|
||||||
storageMessageId: sourceFile.storageMessageId,
|
storageChatId: sourceFile.storageChatId,
|
||||||
fileName: sourceFile.fileName,
|
storageMessageId: sourceFile.storageMessageId,
|
||||||
mimeType: sourceFile.mimeType,
|
fileName: sourceFile.fileName,
|
||||||
sizeBytes: sourceFile.sizeBytes,
|
mimeType: sourceFile.mimeType,
|
||||||
fileType: sourceFile.fileType,
|
sizeBytes: sourceFile.sizeBytes,
|
||||||
uploaderId: 0,
|
fileType: sourceFile.fileType,
|
||||||
fileHash: sourceFile.fileHash,
|
fileHash: sourceFile.fileHash,
|
||||||
archiveTelegramFileId: sourceFile.archiveTelegramFileId,
|
archiveTelegramFileId: sourceFile.archiveTelegramFileId,
|
||||||
archiveStorageMessageId: sourceFile.archiveStorageMessageId,
|
archiveStorageMessageId: sourceFile.archiveStorageMessageId,
|
||||||
archiveFileName: sourceFile.archiveFileName,
|
archiveFileName: sourceFile.archiveFileName,
|
||||||
archiveEntryName: sourceFile.archiveEntryName,
|
archiveEntryName: sourceFile.archiveEntryName,
|
||||||
archiveMimeType: sourceFile.archiveMimeType,
|
archiveMimeType: sourceFile.archiveMimeType,
|
||||||
archiveSizeBytes: sourceFile.archiveSizeBytes,
|
archiveSizeBytes: sourceFile.archiveSizeBytes,
|
||||||
bucketId: input.destBucketId,
|
bucketId: input.destBucketId,
|
||||||
s3Key: input.destKey,
|
s3Key: input.destKey,
|
||||||
storageBackend: 'telegram',
|
storageBackend: 'telegram',
|
||||||
isDeleted: false,
|
partCount: null,
|
||||||
multipartUploadId: null,
|
}),
|
||||||
partCount: null,
|
);
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
etag: sourceEtag || nanoid(16),
|
etag: sourceEtag || nanoid(16),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { createReadStream } from 'node:fs';
|
import { createReadStream } from 'node:fs';
|
||||||
import { open } from 'node:fs/promises';
|
import { open } from 'node:fs/promises';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
|
import { buildNewFile } from '../../domain/entities/file-factory';
|
||||||
import type { NewFilePart } from '../../domain/entities/file-part';
|
import type { NewFilePart } from '../../domain/entities/file-part';
|
||||||
import type { IFilePartRepository } from '../../domain/ports/file-part-repository';
|
import type { IFilePartRepository } from '../../domain/ports/file-part-repository';
|
||||||
import type { IFileRepository } from '../../domain/ports/file-repository';
|
import type { IFileRepository } from '../../domain/ports/file-repository';
|
||||||
@@ -231,31 +232,25 @@ export function createUploadFileUseCase(deps: UploadFileUseCaseDeps) {
|
|||||||
const fileId = nanoid();
|
const fileId = nanoid();
|
||||||
const publicId = nanoid();
|
const publicId = nanoid();
|
||||||
|
|
||||||
const newFile = await deps.fileRepo.create({
|
const newFile = await deps.fileRepo.create(
|
||||||
publicId,
|
buildNewFile({
|
||||||
telegramFileId: firstPart.telegramFileId,
|
publicId,
|
||||||
telegramFileUniqueId: firstPart.telegramFileUniqueId,
|
telegramFileId: firstPart.telegramFileId,
|
||||||
storageChatId: deps.config.storageChatId,
|
telegramFileUniqueId: firstPart.telegramFileUniqueId,
|
||||||
storageMessageId: firstPart.storageMessageId,
|
storageChatId: deps.config.storageChatId,
|
||||||
fileName: finalFileName,
|
storageMessageId: firstPart.storageMessageId,
|
||||||
mimeType,
|
fileName: finalFileName,
|
||||||
sizeBytes: chunkResult.totalSizeBytes,
|
mimeType,
|
||||||
fileType,
|
sizeBytes: chunkResult.totalSizeBytes,
|
||||||
uploaderId: input.uploaderId ?? 0,
|
fileType,
|
||||||
fileHash: chunkResult.fileHash,
|
storageBackend: 'chunked',
|
||||||
archiveTelegramFileId: null,
|
uploaderId: input.uploaderId,
|
||||||
archiveStorageMessageId: null,
|
fileHash: chunkResult.fileHash,
|
||||||
archiveFileName: null,
|
bucketId: input.bucketId,
|
||||||
archiveEntryName: null,
|
s3Key: input.s3Key,
|
||||||
archiveMimeType: null,
|
partCount: chunkResult.parts.length,
|
||||||
archiveSizeBytes: null,
|
}),
|
||||||
bucketId: input.bucketId ?? null,
|
);
|
||||||
s3Key: input.s3Key ?? null,
|
|
||||||
storageBackend: 'chunked',
|
|
||||||
isDeleted: false,
|
|
||||||
multipartUploadId: null,
|
|
||||||
partCount: chunkResult.parts.length,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fileParts: NewFilePart[] = chunkResult.parts.map((part) => ({
|
const fileParts: NewFilePart[] = chunkResult.parts.map((part) => ({
|
||||||
fileId,
|
fileId,
|
||||||
@@ -292,31 +287,24 @@ export function createUploadFileUseCase(deps: UploadFileUseCaseDeps) {
|
|||||||
|
|
||||||
const singlePublicId = nanoid();
|
const singlePublicId = nanoid();
|
||||||
|
|
||||||
const createdFile = await deps.fileRepo.create({
|
const createdFile = await deps.fileRepo.create(
|
||||||
publicId: singlePublicId,
|
buildNewFile({
|
||||||
telegramFileId: forwardResult.telegramFileId,
|
publicId: singlePublicId,
|
||||||
telegramFileUniqueId: forwardResult.telegramFileUniqueId,
|
telegramFileId: forwardResult.telegramFileId,
|
||||||
storageChatId: deps.config.storageChatId,
|
telegramFileUniqueId: forwardResult.telegramFileUniqueId,
|
||||||
storageMessageId: forwardResult.storageMessageId,
|
storageChatId: deps.config.storageChatId,
|
||||||
fileName: finalFileName,
|
storageMessageId: forwardResult.storageMessageId,
|
||||||
mimeType,
|
fileName: finalFileName,
|
||||||
sizeBytes: input.sizeBytes,
|
mimeType,
|
||||||
fileType,
|
sizeBytes: input.sizeBytes,
|
||||||
uploaderId: input.uploaderId ?? 0,
|
fileType,
|
||||||
fileHash: input.fileHash,
|
storageBackend: 'telegram',
|
||||||
archiveTelegramFileId: null,
|
uploaderId: input.uploaderId,
|
||||||
archiveStorageMessageId: null,
|
fileHash: input.fileHash,
|
||||||
archiveFileName: null,
|
bucketId: input.bucketId,
|
||||||
archiveEntryName: null,
|
s3Key: input.s3Key,
|
||||||
archiveMimeType: null,
|
}),
|
||||||
archiveSizeBytes: null,
|
);
|
||||||
bucketId: input.bucketId ?? null,
|
|
||||||
s3Key: input.s3Key ?? null,
|
|
||||||
storageBackend: 'telegram',
|
|
||||||
isDeleted: false,
|
|
||||||
multipartUploadId: null,
|
|
||||||
partCount: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
publicId: createdFile.publicId,
|
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 { createReadStream } from 'node:fs';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import type { File as FileEntity } from '../../domain/entities/file';
|
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 { NewFilePart } from '../../domain/entities/file-part';
|
||||||
import type { IFilePartRepository } from '../../domain/ports/file-part-repository';
|
import type { IFilePartRepository } from '../../domain/ports/file-part-repository';
|
||||||
import type { IFileRepository } from '../../domain/ports/file-repository';
|
import type { IFileRepository } from '../../domain/ports/file-repository';
|
||||||
@@ -193,31 +194,25 @@ export class ChunkedStorage {
|
|||||||
|
|
||||||
const publicId = nanoid();
|
const publicId = nanoid();
|
||||||
|
|
||||||
const file = await this.fileRepository.create({
|
const file = await this.fileRepository.create(
|
||||||
publicId,
|
buildNewFile({
|
||||||
telegramFileId: firstPart.telegramFileId,
|
publicId,
|
||||||
telegramFileUniqueId: firstPart.telegramFileUniqueId,
|
telegramFileId: firstPart.telegramFileId,
|
||||||
storageChatId: config.storageChatId,
|
telegramFileUniqueId: firstPart.telegramFileUniqueId,
|
||||||
storageMessageId: firstPart.storageMessageId,
|
storageChatId: config.storageChatId,
|
||||||
fileName: input.fileName,
|
storageMessageId: firstPart.storageMessageId,
|
||||||
mimeType: input.mimeType,
|
fileName: input.fileName,
|
||||||
sizeBytes: upload.totalSizeBytes,
|
mimeType: input.mimeType,
|
||||||
fileType: input.fileType,
|
sizeBytes: upload.totalSizeBytes,
|
||||||
uploaderId: input.uploaderId,
|
fileType: input.fileType,
|
||||||
fileHash: upload.fileHash,
|
storageBackend: 'chunked',
|
||||||
archiveTelegramFileId: null,
|
uploaderId: input.uploaderId,
|
||||||
archiveStorageMessageId: null,
|
fileHash: upload.fileHash,
|
||||||
archiveFileName: null,
|
bucketId: input.bucketId,
|
||||||
archiveEntryName: null,
|
s3Key: input.s3Key,
|
||||||
archiveMimeType: null,
|
partCount: upload.parts.length,
|
||||||
archiveSizeBytes: null,
|
}),
|
||||||
bucketId: input.bucketId ?? null,
|
);
|
||||||
s3Key: input.s3Key ?? null,
|
|
||||||
storageBackend: 'chunked',
|
|
||||||
isDeleted: false,
|
|
||||||
multipartUploadId: null,
|
|
||||||
partCount: upload.parts.length,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fileParts: NewFilePart[] = upload.parts.map((part) => ({
|
const fileParts: NewFilePart[] = upload.parts.map((part) => ({
|
||||||
fileId: file.id,
|
fileId: file.id,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { createReadStream } from 'node:fs';
|
import { createReadStream } from 'node:fs';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import type { File as FileEntity, NewFile } from '../../domain/entities/file';
|
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 { IFileRepository } from '../../domain/ports/file-repository';
|
||||||
import type { ITelegramService } from '../../domain/ports/telegram-service';
|
import type { ITelegramService } from '../../domain/ports/telegram-service';
|
||||||
import { config } from '../../env';
|
import { config } from '../../env';
|
||||||
@@ -92,7 +93,7 @@ export class UploadBatcher {
|
|||||||
sizeBytes: number;
|
sizeBytes: number;
|
||||||
},
|
},
|
||||||
): NewFile {
|
): NewFile {
|
||||||
return {
|
return buildNewFile({
|
||||||
publicId: nanoid(),
|
publicId: nanoid(),
|
||||||
telegramFileId: archive.telegramFileId,
|
telegramFileId: archive.telegramFileId,
|
||||||
telegramFileUniqueId: archive.telegramFileUniqueId,
|
telegramFileUniqueId: archive.telegramFileUniqueId,
|
||||||
@@ -102,7 +103,7 @@ export class UploadBatcher {
|
|||||||
mimeType: item.mimeType || 'application/octet-stream',
|
mimeType: item.mimeType || 'application/octet-stream',
|
||||||
sizeBytes: item.prepared.sizeBytes,
|
sizeBytes: item.prepared.sizeBytes,
|
||||||
fileType: item.fileType,
|
fileType: item.fileType,
|
||||||
uploaderId: 0,
|
storageBackend: null,
|
||||||
fileHash: item.prepared.fileHash,
|
fileHash: item.prepared.fileHash,
|
||||||
archiveTelegramFileId: archive.telegramFileId,
|
archiveTelegramFileId: archive.telegramFileId,
|
||||||
archiveStorageMessageId: archive.storageMessageId,
|
archiveStorageMessageId: archive.storageMessageId,
|
||||||
@@ -110,13 +111,7 @@ export class UploadBatcher {
|
|||||||
archiveEntryName: entry.entryName,
|
archiveEntryName: entry.entryName,
|
||||||
archiveMimeType: 'application/zip',
|
archiveMimeType: 'application/zip',
|
||||||
archiveSizeBytes: archive.sizeBytes,
|
archiveSizeBytes: archive.sizeBytes,
|
||||||
bucketId: null,
|
});
|
||||||
s3Key: null,
|
|
||||||
storageBackend: null,
|
|
||||||
isDeleted: null,
|
|
||||||
multipartUploadId: null,
|
|
||||||
partCount: null,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import { type Context, Telegraf } from 'telegraf';
|
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 { IFileRepository } from '../../domain/ports/file-repository';
|
||||||
import type { ITelegramService } from '../../domain/ports/telegram-service';
|
import type { ITelegramService } from '../../domain/ports/telegram-service';
|
||||||
import { config } from '../../env';
|
import { config } from '../../env';
|
||||||
@@ -8,6 +8,7 @@ import { DrizzleFileRepository } from '../../infrastructure/persistence/reposito
|
|||||||
import { botPool } from '../../infrastructure/telegram/bot-pool';
|
import { botPool } from '../../infrastructure/telegram/bot-pool';
|
||||||
import logger from '../../shared/logger/index';
|
import logger from '../../shared/logger/index';
|
||||||
import {
|
import {
|
||||||
|
checkFileSize,
|
||||||
detectFileType,
|
detectFileType,
|
||||||
extractFileFromMessage,
|
extractFileFromMessage,
|
||||||
getErrorMessage,
|
getErrorMessage,
|
||||||
@@ -127,10 +128,10 @@ export async function startBot(
|
|||||||
ctx.message.voice?.file_name ||
|
ctx.message.voice?.file_name ||
|
||||||
'file';
|
'file';
|
||||||
|
|
||||||
const maxSize = getFileSizeLimit(fileType);
|
if (!checkFileSize(fileSize, fileType)) {
|
||||||
|
return ctx.reply(
|
||||||
if (fileSize > maxSize) {
|
`File size exceeds ${getFileSizeLimit(fileType) / (1024 * 1024)}MB limit`,
|
||||||
return ctx.reply(`File size exceeds ${maxSize / (1024 * 1024)}MB limit`);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const existing = await fileRepo.findByUniqueId(fileObj.file_unique_id);
|
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 result = await telegramService.forwardToStorage(file_id, fileName, fileType);
|
||||||
const publicId = nanoid();
|
const publicId = nanoid();
|
||||||
|
|
||||||
const uploaded: NewFile = {
|
await fileRepo.create(
|
||||||
publicId,
|
buildNewFile({
|
||||||
telegramFileId: result.telegramFileId,
|
publicId,
|
||||||
telegramFileUniqueId: result.telegramFileUniqueId,
|
telegramFileId: result.telegramFileId,
|
||||||
storageChatId: config.storageChatId,
|
telegramFileUniqueId: result.telegramFileUniqueId,
|
||||||
storageMessageId: result.storageMessageId,
|
storageChatId: config.storageChatId,
|
||||||
fileName,
|
storageMessageId: result.storageMessageId,
|
||||||
mimeType: mime_type || 'application/octet-stream',
|
fileName,
|
||||||
sizeBytes: fileSize,
|
mimeType: mime_type || 'application/octet-stream',
|
||||||
fileType,
|
sizeBytes: fileSize,
|
||||||
uploaderId: ctx.from.id,
|
fileType,
|
||||||
fileHash: null,
|
uploaderId: ctx.from.id,
|
||||||
archiveTelegramFileId: null,
|
storageBackend: 'telegram',
|
||||||
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 replyWithDownloadUrl(ctx, publicId);
|
await replyWithDownloadUrl(ctx, publicId);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { createReadStream } from 'node:fs';
|
import { createReadStream } from 'node:fs';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import type { File as FileEntity } from '../../../domain/entities/file';
|
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 type { ForwardResult } from '../../../domain/ports/telegram-service';
|
||||||
import { config } from '../../../env';
|
import { config } from '../../../env';
|
||||||
import {
|
import {
|
||||||
@@ -9,7 +10,6 @@ import {
|
|||||||
fileRepository,
|
fileRepository,
|
||||||
multipartRepository,
|
multipartRepository,
|
||||||
} from '../../../infrastructure/di';
|
} from '../../../infrastructure/di';
|
||||||
import { db, files as fileSchema } from '../../../infrastructure/persistence/drizzle/index';
|
|
||||||
import { botPool } from '../../../infrastructure/telegram/bot-pool';
|
import { botPool } from '../../../infrastructure/telegram/bot-pool';
|
||||||
import logger from '../../../shared/logger/index';
|
import logger from '../../../shared/logger/index';
|
||||||
import { cleanupTempFile, ensureExtension, getErrorMessage } from '../../../shared/utils/file';
|
import { cleanupTempFile, ensureExtension, getErrorMessage } from '../../../shared/utils/file';
|
||||||
@@ -1070,25 +1070,24 @@ const storeFileFromTemp = async (
|
|||||||
|
|
||||||
const publicId = nanoid();
|
const publicId = nanoid();
|
||||||
|
|
||||||
await db.insert(fileSchema).values({
|
await fileRepository.create(
|
||||||
publicId,
|
buildNewFile({
|
||||||
telegramFileId: forwardResult.telegramFileId,
|
publicId,
|
||||||
telegramFileUniqueId: forwardResult.telegramFileUniqueId,
|
telegramFileId: forwardResult.telegramFileId,
|
||||||
storageChatId: config.storageChatId,
|
telegramFileUniqueId: forwardResult.telegramFileUniqueId,
|
||||||
storageMessageId: forwardResult.storageMessageId,
|
storageChatId: config.storageChatId,
|
||||||
fileName: finalFileName,
|
storageMessageId: forwardResult.storageMessageId,
|
||||||
mimeType,
|
fileName: finalFileName,
|
||||||
sizeBytes: streamed.sizeBytes,
|
mimeType,
|
||||||
fileType: 'document',
|
sizeBytes: streamed.sizeBytes,
|
||||||
uploaderId: 0,
|
fileType: 'document',
|
||||||
fileHash: streamed.fileHash,
|
uploaderId: 0,
|
||||||
bucketId,
|
fileHash: streamed.fileHash,
|
||||||
s3Key: key,
|
bucketId,
|
||||||
storageBackend: 'telegram',
|
s3Key: key,
|
||||||
isDeleted: false,
|
storageBackend: 'telegram',
|
||||||
createdAt: new Date(),
|
}),
|
||||||
updatedAt: new Date(),
|
);
|
||||||
});
|
|
||||||
|
|
||||||
await cleanupTempFile(streamed.tempPath);
|
await cleanupTempFile(streamed.tempPath);
|
||||||
|
|
||||||
@@ -1181,25 +1180,24 @@ const handleCopyObject = async (
|
|||||||
|
|
||||||
const publicId = nanoid();
|
const publicId = nanoid();
|
||||||
|
|
||||||
await db.insert(fileSchema).values({
|
await fileRepository.create(
|
||||||
publicId,
|
buildNewFile({
|
||||||
telegramFileId: sourceFile.telegramFileId,
|
publicId,
|
||||||
telegramFileUniqueId: sourceFile.telegramFileUniqueId,
|
telegramFileId: sourceFile.telegramFileId,
|
||||||
storageChatId: sourceFile.storageChatId,
|
telegramFileUniqueId: sourceFile.telegramFileUniqueId,
|
||||||
storageMessageId: sourceFile.storageMessageId,
|
storageChatId: sourceFile.storageChatId,
|
||||||
fileName: sourceFile.fileName,
|
storageMessageId: sourceFile.storageMessageId,
|
||||||
mimeType: sourceFile.mimeType,
|
fileName: sourceFile.fileName,
|
||||||
sizeBytes: sourceFile.sizeBytes,
|
mimeType: sourceFile.mimeType,
|
||||||
fileType: sourceFile.fileType,
|
sizeBytes: sourceFile.sizeBytes,
|
||||||
uploaderId: 0,
|
fileType: sourceFile.fileType,
|
||||||
fileHash: sourceFile.fileHash,
|
uploaderId: 0,
|
||||||
bucketId: destBucketId,
|
fileHash: sourceFile.fileHash,
|
||||||
s3Key: destKey,
|
bucketId: destBucketId,
|
||||||
storageBackend: 'telegram',
|
s3Key: destKey,
|
||||||
isDeleted: false,
|
storageBackend: 'telegram',
|
||||||
createdAt: new Date(),
|
}),
|
||||||
updatedAt: new Date(),
|
);
|
||||||
});
|
|
||||||
|
|
||||||
const xml = copyObjectResultXml(sourceFile.fileHash || nanoid(16), new Date());
|
const xml = copyObjectResultXml(sourceFile.fileHash || nanoid(16), new Date());
|
||||||
return s3Response(xml, 200, reqId, { 'content-type': 'application/xml' });
|
return s3Response(xml, 200, reqId, { 'content-type': 'application/xml' });
|
||||||
@@ -1337,13 +1335,7 @@ const handleListObjectsV1 = async (
|
|||||||
|
|
||||||
const xml = listBucketResultXml(
|
const xml = listBucketResultXml(
|
||||||
bucket,
|
bucket,
|
||||||
displayObjects.map((o) => ({
|
displayObjects.map(mapFileToListEntry),
|
||||||
key: o.s3Key ?? '',
|
|
||||||
sizeBytes: o.sizeBytes,
|
|
||||||
etag: o.fileHash || nanoid(16),
|
|
||||||
lastModified: o.createdAt instanceof Date ? o.createdAt : new Date(),
|
|
||||||
mimeType: o.mimeType,
|
|
||||||
})),
|
|
||||||
commonPrefixes,
|
commonPrefixes,
|
||||||
isTruncated,
|
isTruncated,
|
||||||
marker,
|
marker,
|
||||||
@@ -1358,6 +1350,29 @@ const handleListObjectsV1 = async (
|
|||||||
return s3Response(xml, 200, reqId, { 'content-type': 'application/xml' });
|
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).
|
* Handles GET /{bucket}?list-type=2 (ListObjectsV2).
|
||||||
*
|
*
|
||||||
@@ -1405,13 +1420,7 @@ const handleListObjectsV2 = async (
|
|||||||
|
|
||||||
const xml = listBucketV2ResultXml(
|
const xml = listBucketV2ResultXml(
|
||||||
bucket,
|
bucket,
|
||||||
displayObjects.map((o) => ({
|
displayObjects.map(mapFileToListEntry),
|
||||||
key: o.s3Key ?? '',
|
|
||||||
sizeBytes: o.sizeBytes,
|
|
||||||
etag: o.fileHash || nanoid(16),
|
|
||||||
lastModified: o.createdAt instanceof Date ? o.createdAt : new Date(),
|
|
||||||
mimeType: o.mimeType,
|
|
||||||
})),
|
|
||||||
commonPrefixes,
|
commonPrefixes,
|
||||||
isTruncated,
|
isTruncated,
|
||||||
maxKeys,
|
maxKeys,
|
||||||
@@ -1668,25 +1677,24 @@ const handleCompleteMultipartUpload = async (
|
|||||||
// M7: Use stored content-type from the multipart record if available
|
// M7: Use stored content-type from the multipart record if available
|
||||||
const mimeType = multipart.contentType || 'application/octet-stream';
|
const mimeType = multipart.contentType || 'application/octet-stream';
|
||||||
|
|
||||||
await db.insert(fileSchema).values({
|
await fileRepository.create(
|
||||||
publicId,
|
buildNewFile({
|
||||||
telegramFileId: storedParts[0]!.telegramFileId,
|
publicId,
|
||||||
telegramFileUniqueId: storedParts[0]!.telegramFileUniqueId,
|
telegramFileId: storedParts[0]!.telegramFileId,
|
||||||
storageChatId: config.storageChatId,
|
telegramFileUniqueId: storedParts[0]!.telegramFileUniqueId,
|
||||||
storageMessageId: storedParts[0]!.storageMessageId,
|
storageChatId: config.storageChatId,
|
||||||
fileName: key.split('/').pop() || 'file',
|
storageMessageId: storedParts[0]!.storageMessageId,
|
||||||
mimeType,
|
fileName: key.split('/').pop() || 'file',
|
||||||
sizeBytes: totalSize,
|
mimeType,
|
||||||
fileType: 'document',
|
sizeBytes: totalSize,
|
||||||
uploaderId: 0,
|
fileType: 'document',
|
||||||
bucketId: multipart.bucketId,
|
uploaderId: 0,
|
||||||
s3Key: key,
|
bucketId: multipart.bucketId,
|
||||||
storageBackend: 'telegram',
|
s3Key: key,
|
||||||
isDeleted: false,
|
storageBackend: 'telegram',
|
||||||
multipartUploadId: uploadId,
|
multipartUploadId: uploadId,
|
||||||
createdAt: new Date(),
|
}),
|
||||||
updatedAt: new Date(),
|
);
|
||||||
});
|
|
||||||
|
|
||||||
await multipartRepository.complete(uploadId);
|
await multipartRepository.complete(uploadId);
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { createReadStream } from 'node:fs';
|
import { createReadStream } from 'node:fs';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
|
import { buildNewFile } from '../../../domain/entities/file-factory';
|
||||||
import { config } from '../../../env';
|
import { config } from '../../../env';
|
||||||
import { bucketRepository, chunkedStorage, fileRepository } from '../../../infrastructure/di';
|
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 { botPool } from '../../../infrastructure/telegram/bot-pool';
|
||||||
import logger from '../../../shared/logger/index';
|
import logger from '../../../shared/logger/index';
|
||||||
import { cleanupTempFile, ensureExtension, getErrorMessage } from '../../../shared/utils/file';
|
import { cleanupTempFile, ensureExtension, getErrorMessage } from '../../../shared/utils/file';
|
||||||
@@ -238,25 +238,24 @@ export const handleUploadObjectV1 = async (
|
|||||||
|
|
||||||
const publicId = nanoid();
|
const publicId = nanoid();
|
||||||
|
|
||||||
await db.insert(fileSchema).values({
|
await fileRepository.create(
|
||||||
publicId,
|
buildNewFile({
|
||||||
telegramFileId: forwardResult.telegramFileId,
|
publicId,
|
||||||
telegramFileUniqueId: forwardResult.telegramFileUniqueId,
|
telegramFileId: forwardResult.telegramFileId,
|
||||||
storageChatId: config.storageChatId,
|
telegramFileUniqueId: forwardResult.telegramFileUniqueId,
|
||||||
storageMessageId: forwardResult.storageMessageId,
|
storageChatId: config.storageChatId,
|
||||||
fileName: finalFileName,
|
storageMessageId: forwardResult.storageMessageId,
|
||||||
mimeType,
|
fileName: finalFileName,
|
||||||
sizeBytes,
|
mimeType,
|
||||||
fileType: 'document',
|
sizeBytes,
|
||||||
uploaderId: 0,
|
fileType: 'document',
|
||||||
fileHash: hash,
|
uploaderId: 0,
|
||||||
bucketId: bucket.id,
|
fileHash: hash,
|
||||||
s3Key: key,
|
bucketId: bucket.id,
|
||||||
storageBackend: 'telegram',
|
s3Key: key,
|
||||||
isDeleted: false,
|
storageBackend: 'telegram',
|
||||||
createdAt: new Date(),
|
}),
|
||||||
updatedAt: new Date(),
|
);
|
||||||
});
|
|
||||||
|
|
||||||
await cleanupTempFile(tempPath);
|
await cleanupTempFile(tempPath);
|
||||||
|
|
||||||
@@ -352,25 +351,24 @@ export const handleCopyObjectV1 = async (req: Request, params: RouteParams): Pro
|
|||||||
|
|
||||||
const publicId = nanoid();
|
const publicId = nanoid();
|
||||||
|
|
||||||
await db.insert(fileSchema).values({
|
await fileRepository.create(
|
||||||
publicId,
|
buildNewFile({
|
||||||
telegramFileId: sourceFile.telegramFileId,
|
publicId,
|
||||||
telegramFileUniqueId: sourceFile.telegramFileUniqueId,
|
telegramFileId: sourceFile.telegramFileId,
|
||||||
storageChatId: sourceFile.storageChatId,
|
telegramFileUniqueId: sourceFile.telegramFileUniqueId,
|
||||||
storageMessageId: sourceFile.storageMessageId,
|
storageChatId: sourceFile.storageChatId,
|
||||||
fileName: sourceFile.fileName,
|
storageMessageId: sourceFile.storageMessageId,
|
||||||
mimeType: sourceFile.mimeType,
|
fileName: sourceFile.fileName,
|
||||||
sizeBytes: sourceFile.sizeBytes,
|
mimeType: sourceFile.mimeType,
|
||||||
fileType: sourceFile.fileType,
|
sizeBytes: Number(sourceFile.sizeBytes),
|
||||||
uploaderId: 0,
|
fileType: sourceFile.fileType,
|
||||||
fileHash: sourceFile.fileHash,
|
uploaderId: 0,
|
||||||
bucketId: destBucket.id,
|
fileHash: sourceFile.fileHash,
|
||||||
s3Key: body.destKey,
|
bucketId: destBucket.id,
|
||||||
storageBackend: 'telegram',
|
s3Key: body.destKey,
|
||||||
isDeleted: false,
|
storageBackend: 'telegram',
|
||||||
createdAt: new Date(),
|
}),
|
||||||
updatedAt: new Date(),
|
);
|
||||||
});
|
|
||||||
|
|
||||||
return json({ sourceKey: body.sourceKey, destKey: body.destKey, destBucket: destBucketName });
|
return json({ sourceKey: body.sourceKey, destKey: body.destKey, destBucket: destBucketName });
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user