From 667921b100c72ca95613df65325309499b9151a6 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Jul 2026 20:09:42 +0700 Subject: [PATCH] =?UTF-8?q?chore:=20fix=20lint=20errors=20=E2=80=94=20dupl?= =?UTF-8?q?icate=20import,=20unused=20imports,=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 (1M context) --- src/application/use-cases/authenticate.ts | 10 +++- src/application/use-cases/get-file.ts | 2 +- src/application/use-cases/manage-bucket.ts | 20 ++----- src/application/use-cases/multipart-upload.ts | 7 +-- src/application/use-cases/s3-object.ts | 31 ++++++++--- src/application/use-cases/upload-file.ts | 9 +-- src/config/index.ts | 2 +- src/domain/ports/multipart-repository.ts | 2 +- src/domain/ports/telegram-service.ts | 6 +- src/index.ts | 10 ++-- src/infrastructure/cache/index.ts | 33 ++++++++--- .../persistence/drizzle/migrate.ts | 10 ++-- .../repositories/bucket-repository.ts | 2 +- .../repositories/file-part-repository.ts | 5 +- .../repositories/file-repository.ts | 55 ++++--------------- .../repositories/multipart-repository.ts | 21 ++----- src/infrastructure/telegram/bot-pool.ts | 21 ++++--- .../telegram/chunked-storage.ts | 12 ++-- src/infrastructure/telegram/upload-batcher.ts | 6 +- src/interfaces/bot/handler.ts | 4 +- .../http/controllers/auth-controller.ts | 16 +++--- .../http/controllers/file-controller.ts | 14 +++-- .../http/controllers/health-controller.ts | 4 +- .../http/controllers/home-controller.ts | 2 +- .../http/controllers/s3-controller.ts | 38 +++++++------ .../http/controllers/upload-controller.ts | 10 ++-- .../http/controllers/web-api-controller.ts | 11 ++-- src/interfaces/http/middleware/auth.ts | 16 +----- src/interfaces/http/middleware/rate-limit.ts | 8 ++- src/interfaces/http/routes/index.ts | 10 ++-- src/interfaces/s3/auth.ts | 9 ++- src/interfaces/s3/virtual-host.ts | 2 +- src/shared/errors/index.ts | 2 +- src/shared/logger/index.ts | 4 +- src/shared/metrics/index.ts | 2 +- src/shared/utils/retry.ts | 2 +- src/shared/utils/zip.ts | 5 +- src/utils/s3/auth.ts | 7 ++- src/utils/zip.ts | 3 +- test/bot.test.ts | 20 +++++-- test/s3-docker-registry.test.ts | 26 ++++----- 41 files changed, 241 insertions(+), 238 deletions(-) diff --git a/src/application/use-cases/authenticate.ts b/src/application/use-cases/authenticate.ts index e9a3097..27cd6f6 100644 --- a/src/application/use-cases/authenticate.ts +++ b/src/application/use-cases/authenticate.ts @@ -1,5 +1,11 @@ import { timingSafeEqual } from 'node:crypto'; -import type { LoginInput, LoginResponse, LogoutResponse, UserInfoResponse, AuthSession } from '../dto/auth'; +import type { + AuthSession, + LoginInput, + LoginResponse, + LogoutResponse, + UserInfoResponse, +} from '../dto/auth'; /** Subset of application configuration consumed by the authenticate use case. */ export interface AuthUseCaseConfig { @@ -105,4 +111,4 @@ export function createMeUseCase(deps: AuthenticateUseCaseDeps) { expiresAt: session.expiresAt?.toISOString() ?? null, }; }; -} \ No newline at end of file +} diff --git a/src/application/use-cases/get-file.ts b/src/application/use-cases/get-file.ts index 69bf8ab..4e1b81e 100644 --- a/src/application/use-cases/get-file.ts +++ b/src/application/use-cases/get-file.ts @@ -173,4 +173,4 @@ export function createGetFileUseCase(deps: GetFileUseCaseDeps) { return { type: 'redirect', file, redirectUrl, fileInfo }; }; -} \ No newline at end of file +} diff --git a/src/application/use-cases/manage-bucket.ts b/src/application/use-cases/manage-bucket.ts index a51e0b9..524adfd 100644 --- a/src/application/use-cases/manage-bucket.ts +++ b/src/application/use-cases/manage-bucket.ts @@ -98,11 +98,7 @@ export function createGetBucketUseCase(deps: ManageBucketDeps) { export function createCreateBucketUseCase(deps: ManageBucketDeps) { return async (name: string): Promise => { if (!BUCKET_NAME_REGEX.test(name)) { - throw new BucketError( - 'InvalidBucketName', - 'The specified bucket is not valid.', - 400, - ); + throw new BucketError('InvalidBucketName', 'The specified bucket is not valid.', 400); } const existing = await deps.bucketRepo.findByName(name); @@ -134,20 +130,12 @@ export function createDeleteBucketUseCase(deps: ManageBucketDeps) { return async (name: string): Promise => { const bucket = await deps.bucketRepo.findByName(name); if (!bucket) { - throw new BucketError( - 'NoSuchBucket', - 'The specified bucket does not exist.', - 404, - ); + throw new BucketError('NoSuchBucket', 'The specified bucket does not exist.', 404); } const objectCount = await deps.fileRepo.countByBucket(bucket.id); if (objectCount > 0) { - throw new BucketError( - 'BucketNotEmpty', - 'The bucket you tried to delete is not empty.', - 409, - ); + throw new BucketError('BucketNotEmpty', 'The bucket you tried to delete is not empty.', 409); } return deps.bucketRepo.delete(name); @@ -165,4 +153,4 @@ export function createBucketExistsUseCase(deps: ManageBucketDeps) { return async (name: string): Promise => { return deps.bucketRepo.exists(name); }; -} \ No newline at end of file +} diff --git a/src/application/use-cases/multipart-upload.ts b/src/application/use-cases/multipart-upload.ts index 4bf8148..856cc9c 100644 --- a/src/application/use-cases/multipart-upload.ts +++ b/src/application/use-cases/multipart-upload.ts @@ -116,10 +116,7 @@ export interface MultipartDeps { * the upload initiation result, or `null` when the bucket is not found. */ export function createInitiateMultipartUploadUseCase(deps: MultipartDeps) { - return async ( - bucketName: string, - key: string, - ): Promise => { + return async (bucketName: string, key: string): Promise => { const bucket = await deps.bucketRepo.findByName(bucketName); if (!bucket) return null; @@ -375,4 +372,4 @@ export function createListPartsUseCase(deps: MultipartDeps) { createdAt: p.createdAt, })); }; -} \ No newline at end of file +} diff --git a/src/application/use-cases/s3-object.ts b/src/application/use-cases/s3-object.ts index d125404..5af1fb1 100644 --- a/src/application/use-cases/s3-object.ts +++ b/src/application/use-cases/s3-object.ts @@ -1,7 +1,7 @@ import { randomUUID } from 'node:crypto'; import { gzipSync } from 'node:zlib'; import { nanoid } from 'nanoid'; -import type { File, NewFile } from '../../domain/entities/file'; +import type { File } from '../../domain/entities/file'; import type { NewFilePart } from '../../domain/entities/file-part'; import type { MultipartPart } from '../../domain/entities/multipart'; import type { IBucketRepository } from '../../domain/ports/bucket-repository'; @@ -9,7 +9,7 @@ import type { IFilePartRepository } from '../../domain/ports/file-part-repositor import type { IFileRepository, S3FileRecord } from '../../domain/ports/file-repository'; import type { IMultipartRepository } from '../../domain/ports/multipart-repository'; import type { ITelegramService, TelegramFileInfo } from '../../domain/ports/telegram-service'; -import { ensureExtension, computeHash, formatCreatedAt } from '../../shared/utils/file'; +import { computeHash, ensureExtension, formatCreatedAt } from '../../shared/utils/file'; // ─── Types ────────────────────────────────────────────────────────── @@ -456,7 +456,12 @@ export function createPutObjectUseCase(deps: S3ObjectDeps) { ); const partFileNamePrefix = `s3-${bucket.name}-${key.replace(/\//g, '_')}`; - const { telegramChunkSizeBytes, compressChunkedUploads, chunkCompressionMinSizeBytes, storageChatId } = deps.config; + const { + telegramChunkSizeBytes, + compressChunkedUploads, + chunkCompressionMinSizeBytes, + storageChatId, + } = deps.config; if (body.byteLength > telegramChunkSizeBytes) { // Chunked upload path @@ -594,16 +599,28 @@ export function createCopyObjectUseCase(deps: S3ObjectDeps) { if (!sourceFile) return null; if (sourceFile.storageBackend === 'chunked') { - throw new ObjectError('NotImplemented', 'Copying chunked objects is not yet implemented.', 501); + throw new ObjectError( + 'NotImplemented', + 'Copying chunked objects is not yet implemented.', + 501, + ); } // Conditional copy: if-match / if-none-match checks const sourceEtag = sourceFile.fileHash; if (input.ifMatch && sourceEtag && input.ifMatch !== sourceEtag) { - throw new ObjectError('PreconditionFailed', 'The preconditions you specified did not hold.', 412); + throw new ObjectError( + 'PreconditionFailed', + 'The preconditions you specified did not hold.', + 412, + ); } if (input.ifNoneMatch && sourceEtag && input.ifNoneMatch === sourceEtag) { - throw new ObjectError('PreconditionFailed', 'The preconditions you specified did not hold.', 412); + throw new ObjectError( + 'PreconditionFailed', + 'The preconditions you specified did not hold.', + 412, + ); } const publicId = nanoid(); @@ -771,4 +788,4 @@ export function createFindObjectUseCase(deps: Pick) { return async (bucketId: string, key: string): Promise => { return deps.fileRepo.findByBucketAndKey(bucketId, key); }; -} \ No newline at end of file +} diff --git a/src/application/use-cases/upload-file.ts b/src/application/use-cases/upload-file.ts index 3dfd30c..07ca9b9 100644 --- a/src/application/use-cases/upload-file.ts +++ b/src/application/use-cases/upload-file.ts @@ -1,14 +1,14 @@ import { randomUUID } from 'node:crypto'; -import { open } from 'node:fs/promises'; import { createReadStream } from 'node:fs'; +import { open } from 'node:fs/promises'; import { gzipSync } from 'node:zlib'; import { nanoid } from 'nanoid'; 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'; import type { ITelegramService } from '../../domain/ports/telegram-service'; +import { checkFileSize, computeHash, ensureExtension, getFileType } from '../../shared/utils/file'; import type { UploadInput, UploadOutput } from '../dto/upload'; -import { getFileType, checkFileSize, ensureExtension, computeHash, formatCreatedAt } from '../../shared/utils/file'; /** Compression algorithm string literal used in chunked storage. */ type ChunkCompressionAlgorithm = 'gzip' | null; @@ -219,7 +219,8 @@ export function createUploadFileUseCase(deps: UploadFileUseCaseDeps) { mimeType: existing.mimeType, sizeBytes: existing.sizeBytes, fileType: existing.fileType, - createdAt: existing.createdAt instanceof Date ? existing.createdAt : new Date(existing.createdAt), + createdAt: + existing.createdAt instanceof Date ? existing.createdAt : new Date(existing.createdAt), downloadUrl: `${deps.config.baseUrl}/f/${existing.publicId}`, }; } @@ -357,4 +358,4 @@ export function createUploadFileUseCase(deps: UploadFileUseCaseDeps) { downloadUrl: `${deps.config.baseUrl}/f/${createdFile.publicId}`, }; }; -} \ No newline at end of file +} diff --git a/src/config/index.ts b/src/config/index.ts index 1381863..744996d 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,3 +1,3 @@ import { config } from '../env'; -export { config }; \ No newline at end of file +export { config }; diff --git a/src/domain/ports/multipart-repository.ts b/src/domain/ports/multipart-repository.ts index ce35d4b..2e92fad 100644 --- a/src/domain/ports/multipart-repository.ts +++ b/src/domain/ports/multipart-repository.ts @@ -1,4 +1,4 @@ -import type { MultipartUpload, MultipartPart } from '../entities/multipart'; +import type { MultipartPart, MultipartUpload } from '../entities/multipart'; /** * Repository interface for S3 multipart upload persistence. diff --git a/src/domain/ports/telegram-service.ts b/src/domain/ports/telegram-service.ts index 4bf9188..4f16e99 100644 --- a/src/domain/ports/telegram-service.ts +++ b/src/domain/ports/telegram-service.ts @@ -39,11 +39,7 @@ export interface ITelegramService { * @param fileType - The file type classification (e.g. "photo", "document"). * @returns The Telegram identifiers of the stored file. */ - forwardToStorage( - fileChunk: unknown, - fileName: string, - fileType: string, - ): Promise; + forwardToStorage(fileChunk: unknown, fileName: string, fileType: string): Promise; /** * Retrieve file metadata from Telegram by file ID. diff --git a/src/index.ts b/src/index.ts index 54e6a5b..279cc2d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +1,12 @@ import { serve } from 'bun'; import { config } from './config/index'; +import { fileInfoCache } from './infrastructure/cache/index'; import { startBot } from './interfaces/bot/handler'; +import { handleS3Request } from './interfaces/http/controllers/s3-controller'; +import { cleanupRateLimitCache } from './interfaces/http/middleware/rate-limit'; import { routes } from './interfaces/http/routes/index'; import { isS3Request } from './interfaces/s3/auth'; -import { handleS3Request } from './interfaces/http/controllers/s3-controller'; import { extractS3BucketFromHost } from './interfaces/s3/virtual-host'; -import { fileInfoCache } from './infrastructure/cache/index'; -import { cleanupRateLimitCache } from './interfaces/http/middleware/rate-limit'; import { logger } from './shared/logger/index'; import { metricsCollector } from './shared/metrics/index'; @@ -30,7 +30,7 @@ const shouldHandleS3 = (req: Request, headers: Record): boolean ); }; -const handleMaybeS3Root = (req: Request): Response | Promise => { +const _handleMaybeS3Root = (req: Request): Response | Promise => { if (req.method === 'OPTIONS') { return handleS3Request(req, getS3RouteBucket(req)); } @@ -100,4 +100,4 @@ setInterval( 5 * 60 * 1000, ); -logger.info('Application running successfully'); \ No newline at end of file +logger.info('Application running successfully'); diff --git a/src/infrastructure/cache/index.ts b/src/infrastructure/cache/index.ts index c6e9914..20596c3 100644 --- a/src/infrastructure/cache/index.ts +++ b/src/infrastructure/cache/index.ts @@ -26,23 +26,40 @@ export class Cache { return entry.value; } - has(key: string): boolean { return this.get(key) !== null; } - delete(key: string): void { this.store.delete(key); } - clear(): void { this.store.clear(); } - size(): number { return this.store.size; } + has(key: string): boolean { + return this.get(key) !== null; + } + delete(key: string): void { + this.store.delete(key); + } + clear(): void { + this.store.clear(); + } + size(): number { + return this.store.size; + } cleanup(): number { let removed = 0; const now = Date.now(); for (const [key, entry] of this.store.entries()) { - if (now > entry.expiresAt) { this.store.delete(key); removed++; } + if (now > entry.expiresAt) { + this.store.delete(key); + removed++; + } } return removed; } } -interface CacheEntry { value: T; expiresAt: number } +interface CacheEntry { + value: T; + expiresAt: number; +} export const fileInfoCache = new Cache<{ - file_size: number; mime_type: string; file_path: string; bot_token: string; -}>(3600); \ No newline at end of file + file_size: number; + mime_type: string; + file_path: string; + bot_token: string; +}>(3600); diff --git a/src/infrastructure/persistence/drizzle/migrate.ts b/src/infrastructure/persistence/drizzle/migrate.ts index 19493dc..096461a 100644 --- a/src/infrastructure/persistence/drizzle/migrate.ts +++ b/src/infrastructure/persistence/drizzle/migrate.ts @@ -1,7 +1,7 @@ import postgres from 'postgres'; import { config } from '../../../env'; -import { getErrorMessage } from '../../../shared/utils/file'; import logger from '../../../shared/logger/index'; +import { getErrorMessage } from '../../../shared/utils/file'; /** * Run raw SQL migration from schema.sql. @@ -15,10 +15,10 @@ export const runMigration = async (): Promise => { const dir = import.meta.dir || ''; const candidates = [ `${dir}/../../../../schema.sql`, // from dist/ - `${dir}/../../../schema.sql`, // from src/infrastructure/persistence/ - `${dir}/../../schema.sql`, // from src/infrastructure/ - `${dir}/../schema.sql`, // from src/infrastructure/persistence/drizzle/ - `${dir}/schema.sql`, // from next to file (bun run directly) + `${dir}/../../../schema.sql`, // from src/infrastructure/persistence/ + `${dir}/../../schema.sql`, // from src/infrastructure/ + `${dir}/../schema.sql`, // from src/infrastructure/persistence/drizzle/ + `${dir}/schema.sql`, // from next to file (bun run directly) ]; let schemaSql: string | null = null; diff --git a/src/infrastructure/persistence/repositories/bucket-repository.ts b/src/infrastructure/persistence/repositories/bucket-repository.ts index 1d422da..a79cff7 100644 --- a/src/infrastructure/persistence/repositories/bucket-repository.ts +++ b/src/infrastructure/persistence/repositories/bucket-repository.ts @@ -1,7 +1,7 @@ import { sql } from 'drizzle-orm'; -import { db } from '../drizzle/index'; import type { Bucket } from '../../../domain/entities/bucket'; import type { IBucketRepository } from '../../../domain/ports/bucket-repository'; +import { db } from '../drizzle/index'; /** Raw result row from `db.execute()`. */ type QueryRow = Record; diff --git a/src/infrastructure/persistence/repositories/file-part-repository.ts b/src/infrastructure/persistence/repositories/file-part-repository.ts index 0545b4f..721ba0c 100644 --- a/src/infrastructure/persistence/repositories/file-part-repository.ts +++ b/src/infrastructure/persistence/repositories/file-part-repository.ts @@ -1,7 +1,7 @@ import { sql } from 'drizzle-orm'; -import { db } from '../drizzle/index'; import type { FilePart, NewFilePart } from '../../../domain/entities/file-part'; import type { IFilePartRepository } from '../../../domain/ports/file-part-repository'; +import { db } from '../drizzle/index'; /** Compression algorithm type matching the domain entity. */ type CompressionAlgorithm = 'gzip' | null; @@ -23,8 +23,7 @@ const mapRowToFilePart = (row: Record): FilePart => ({ storageMessageId: toNumber(row.storage_message_id), sizeBytes: toNumber(row.size_bytes), storedSizeBytes: toNumber(row.stored_size_bytes), - compressionAlgorithm: - (row.compression_algorithm as CompressionAlgorithm) || null, + compressionAlgorithm: (row.compression_algorithm as CompressionAlgorithm) || null, etag: row.etag as string, createdAt: new Date(row.created_at as string), }); diff --git a/src/infrastructure/persistence/repositories/file-repository.ts b/src/infrastructure/persistence/repositories/file-repository.ts index a9cce3d..3084ce0 100644 --- a/src/infrastructure/persistence/repositories/file-repository.ts +++ b/src/infrastructure/persistence/repositories/file-repository.ts @@ -1,10 +1,7 @@ import { and, eq, sql } from 'drizzle-orm'; -import { db, files as fileSchema } from '../drizzle/index'; import type { File, NewFile } from '../../../domain/entities/file'; -import type { - IFileRepository, - S3FileRecord, -} from '../../../domain/ports/file-repository'; +import type { IFileRepository, S3FileRecord } from '../../../domain/ports/file-repository'; +import { db, files as fileSchema } from '../drizzle/index'; /** Safely converts a raw value to a number, defaulting to 0. */ const toNumber = (value: unknown): number => Number(value ?? 0); @@ -35,25 +32,18 @@ const mapDbRowToS3Record = (row: Record): S3FileRecord => ({ fileHash: row.file_hash as string | null, archiveTelegramFileId: row.archive_telegram_file_id as string | null, archiveStorageMessageId: - row.archive_storage_message_id === null - ? null - : toNumber(row.archive_storage_message_id), + row.archive_storage_message_id === null ? null : toNumber(row.archive_storage_message_id), archiveFileName: row.archive_file_name as string | null, archiveEntryName: row.archive_entry_name as string | null, archiveMimeType: row.archive_mime_type as string | null, - archiveSizeBytes: - row.archive_size_bytes === null - ? null - : toNumber(row.archive_size_bytes), + archiveSizeBytes: row.archive_size_bytes === null ? null : toNumber(row.archive_size_bytes), bucketId: row.bucket_id as string, s3Key: row.s3_key as string, storageBackend: (row.storage_backend as string) || 'telegram', isDeleted: row.is_deleted as boolean, multipartUploadId: row.multipart_upload_id as string | null, partCount: - row.part_count === null || row.part_count === undefined - ? null - : toNumber(row.part_count), + row.part_count === null || row.part_count === undefined ? null : toNumber(row.part_count), createdAt: new Date(row.created_at as string), updatedAt: new Date(row.updated_at as string), }); @@ -69,11 +59,7 @@ export class DrizzleFileRepository implements IFileRepository { * {@inheritDoc IFileRepository.findByHash} */ async findByHash(hash: string): Promise { - const result = await db - .select() - .from(fileSchema) - .where(eq(fileSchema.fileHash, hash)) - .limit(1); + const result = await db.select().from(fileSchema).where(eq(fileSchema.fileHash, hash)).limit(1); return result[0] || null; } @@ -104,10 +90,7 @@ export class DrizzleFileRepository implements IFileRepository { /** * {@inheritDoc IFileRepository.findByBucketAndKey} */ - async findByBucketAndKey( - bucketId: string, - s3Key: string, - ): Promise { + async findByBucketAndKey(bucketId: string, s3Key: string): Promise { const result = await db .select() .from(fileSchema) @@ -126,10 +109,7 @@ export class DrizzleFileRepository implements IFileRepository { * {@inheritDoc IFileRepository.create} */ async create(file: NewFile): Promise { - const result = await db - .insert(fileSchema) - .values(file) - .returning(); + const result = await db.insert(fileSchema).values(file).returning(); return result[0]!; } @@ -153,9 +133,7 @@ export class DrizzleFileRepository implements IFileRepository { query = sql`${query} ORDER BY s3_key LIMIT ${maxKeys + 1}`; - const rawResult = (await db.execute( - query, - )) as unknown as Record[]; + const rawResult = (await db.execute(query)) as unknown as Record[]; if (delimiter === '/') { const prefixSet = new Set(); @@ -166,8 +144,7 @@ export class DrizzleFileRepository implements IFileRepository { const relativeKey = s3Key.substring(prefix.length); const slashIndex = relativeKey.indexOf('/'); if (slashIndex >= 0) { - const folderPrefix = - prefix + relativeKey.substring(0, slashIndex + 1); + const folderPrefix = prefix + relativeKey.substring(0, slashIndex + 1); if (folderPrefix !== prefix) { prefixSet.add(folderPrefix); } @@ -201,10 +178,7 @@ export class DrizzleFileRepository implements IFileRepository { /** * {@inheritDoc IFileRepository.softDeleteBatch} */ - async softDeleteBatch( - bucketId: string, - keys: string[], - ): Promise { + async softDeleteBatch(bucketId: string, keys: string[]): Promise { let deleted = 0; for (const key of keys) { const ok = await this.softDelete(bucketId, key); @@ -230,12 +204,7 @@ export class DrizzleFileRepository implements IFileRepository { return await db .select() .from(fileSchema) - .where( - and( - eq(fileSchema.bucketId, bucketId), - eq(fileSchema.isDeleted, true), - ), - ) + .where(and(eq(fileSchema.bucketId, bucketId), eq(fileSchema.isDeleted, true))) .limit(100); } } diff --git a/src/infrastructure/persistence/repositories/multipart-repository.ts b/src/infrastructure/persistence/repositories/multipart-repository.ts index a8a1fe6..e48c1d1 100644 --- a/src/infrastructure/persistence/repositories/multipart-repository.ts +++ b/src/infrastructure/persistence/repositories/multipart-repository.ts @@ -1,15 +1,13 @@ import { sql } from 'drizzle-orm'; import { nanoid } from 'nanoid'; -import { db } from '../drizzle/index'; -import type { MultipartUpload, MultipartPart } from '../../../domain/entities/multipart'; +import type { MultipartPart, MultipartUpload } from '../../../domain/entities/multipart'; import type { IMultipartRepository } from '../../../domain/ports/multipart-repository'; +import { db } from '../drizzle/index'; /** * Maps a raw database row to a {@link MultipartUpload} domain entity. */ -const mapRowToMultipartUpload = ( - r: Record, -): MultipartUpload => ({ +const mapRowToMultipartUpload = (r: Record): MultipartUpload => ({ uploadId: r.upload_id as string, bucketId: r.bucket_id as string, s3Key: r.s3_key as string, @@ -29,11 +27,7 @@ export class DrizzleMultipartRepository implements IMultipartRepository { /** * {@inheritDoc IMultipartRepository.create} */ - async create( - bucketId: string, - s3Key: string, - initiatedBy: string, - ): Promise { + async create(bucketId: string, s3Key: string, initiatedBy: string): Promise { const uploadId = nanoid(32); await db.execute( sql`INSERT INTO multipart_uploads (upload_id, bucket_id, s3_key, initiated_by) VALUES (${uploadId}, ${bucketId}, ${s3Key}, ${initiatedBy})`, @@ -81,9 +75,7 @@ export class DrizzleMultipartRepository implements IMultipartRepository { /** * {@inheritDoc IMultipartRepository.insertPart} */ - async insertPart( - part: Omit, - ): Promise { + async insertPart(part: Omit): Promise { await db.execute( sql`INSERT INTO multipart_parts (upload_id, part_number, telegram_file_id, telegram_file_unique_id, storage_message_id, size_bytes, etag) VALUES (${part.uploadId}, ${part.partNumber}, ${part.telegramFileId}, ${part.telegramFileUniqueId}, ${part.storageMessageId}, ${part.sizeBytes}, ${part.etag})`, @@ -142,8 +134,7 @@ export class DrizzleMultipartRepository implements IMultipartRepository { return { uploads, isTruncated: result.length > limit, - nextKeyMarker: - result.length > limit ? uploads.at(-1)?.s3Key || null : null, + nextKeyMarker: result.length > limit ? uploads.at(-1)?.s3Key || null : null, }; } } diff --git a/src/infrastructure/telegram/bot-pool.ts b/src/infrastructure/telegram/bot-pool.ts index f57524d..e148d02 100644 --- a/src/infrastructure/telegram/bot-pool.ts +++ b/src/infrastructure/telegram/bot-pool.ts @@ -1,15 +1,19 @@ import { Telegraf } from 'telegraf'; +import type { + ForwardResult, + ITelegramService, + TelegramFileInfo, +} from '../../domain/ports/telegram-service'; import { config } from '../../env'; import logger from '../../shared/logger/index'; -import type { ITelegramService, ForwardResult, TelegramFileInfo } from '../../domain/ports/telegram-service'; -import { enqueueUpload } from './upload-queue'; import { - sendMethodMap, - extractUploadedFile, buildSendPayload, - type TelegramMessageResult, + extractUploadedFile, type SendMethod, + sendMethodMap, + type TelegramMessageResult, } from './types'; +import { enqueueUpload } from './upload-queue'; /** * Sleep for a given number of seconds. @@ -94,10 +98,9 @@ export class BotPool implements ITelegramService { if (retries > 0) { const seconds = parseInt(match[1], 10); - logger.warn( - `All bots in the pool are rate-limited. Sleeping for ${seconds} seconds...`, - { error: errorStr }, - ); + logger.warn(`All bots in the pool are rate-limited. Sleeping for ${seconds} seconds...`, { + error: errorStr, + }); await sleep(seconds); return this.executeWithBotRetry(action, retries - 1, 0); } diff --git a/src/infrastructure/telegram/chunked-storage.ts b/src/infrastructure/telegram/chunked-storage.ts index 068c47e..a386820 100644 --- a/src/infrastructure/telegram/chunked-storage.ts +++ b/src/infrastructure/telegram/chunked-storage.ts @@ -1,15 +1,15 @@ import { createReadStream } from 'node:fs'; import { gzipSync } from 'node:zlib'; import { nanoid } from 'nanoid'; +import type { File as FileEntity } from '../../domain/entities/file'; +import type { CompressionAlgorithm, NewFilePart } from '../../domain/entities/file-part'; +import type { IFilePartRepository } from '../../domain/ports/file-part-repository'; +import type { IFileRepository } from '../../domain/ports/file-repository'; +import type { ITelegramService } from '../../domain/ports/telegram-service'; import { config } from '../../env'; -import { computeHash } from '../../shared/utils/file'; import { createGetObjectResponse, type ObjectPartSource } from '../../interfaces/s3/object-stream'; import type { RangeParseResult } from '../../interfaces/s3/range'; -import type { IFileRepository } from '../../domain/ports/file-repository'; -import type { IFilePartRepository } from '../../domain/ports/file-part-repository'; -import type { ITelegramService } from '../../domain/ports/telegram-service'; -import type { File as FileEntity } from '../../domain/entities/file'; -import type { NewFilePart, CompressionAlgorithm } from '../../domain/entities/file-part'; +import { computeHash } from '../../shared/utils/file'; /** * Chunk compression algorithm identifier. diff --git a/src/infrastructure/telegram/upload-batcher.ts b/src/infrastructure/telegram/upload-batcher.ts index 76662a8..f208211 100644 --- a/src/infrastructure/telegram/upload-batcher.ts +++ b/src/infrastructure/telegram/upload-batcher.ts @@ -1,11 +1,11 @@ import { createReadStream } from 'node:fs'; import { nanoid } from 'nanoid'; +import type { File as FileEntity, NewFile } from '../../domain/entities/file'; +import type { IFileRepository } from '../../domain/ports/file-repository'; +import type { ITelegramService } from '../../domain/ports/telegram-service'; import { config } from '../../env'; import { cleanupTempFile } from '../../shared/utils/file'; import { createZip, type ZipEntry } from '../../shared/utils/zip'; -import type { IFileRepository } from '../../domain/ports/file-repository'; -import type { ITelegramService } from '../../domain/ports/telegram-service'; -import type { File as FileEntity, NewFile } from '../../domain/entities/file'; /** * Metadata about a prepared upload before it is submitted to the batcher. diff --git a/src/interfaces/bot/handler.ts b/src/interfaces/bot/handler.ts index 6c297d6..a7000b4 100644 --- a/src/interfaces/bot/handler.ts +++ b/src/interfaces/bot/handler.ts @@ -1,11 +1,12 @@ import { nanoid } from 'nanoid'; import { type Context, Telegraf } from 'telegraf'; -import { config } from '../../env'; import type { NewFile } from '../../domain/entities/file'; import type { IFileRepository } from '../../domain/ports/file-repository'; import type { ITelegramService } from '../../domain/ports/telegram-service'; +import { config } from '../../env'; import { DrizzleFileRepository } from '../../infrastructure/persistence/repositories/file-repository'; import { botPool } from '../../infrastructure/telegram/bot-pool'; +import logger from '../../shared/logger/index'; import { detectFileType, extractFileFromMessage, @@ -13,7 +14,6 @@ import { getFileSizeLimit, type TelegramMediaMessage, } from '../../shared/utils/file'; -import logger from '../../shared/logger/index'; /** * Minimal bot context shape used by the media event handler. diff --git a/src/interfaces/http/controllers/auth-controller.ts b/src/interfaces/http/controllers/auth-controller.ts index 1c660a3..7272767 100644 --- a/src/interfaces/http/controllers/auth-controller.ts +++ b/src/interfaces/http/controllers/auth-controller.ts @@ -1,17 +1,17 @@ +import { + type AuthSession, + createLoginUseCase, + createLogoutUseCase, + createMeUseCase, +} from '../../../application/use-cases/authenticate'; import { config } from '../../../config/index'; import { + checkBearerToken, clearSessionCookie, createSessionCookie, getAuthSession, isAuthEnabled, - checkBearerToken, } from '../../../utils/auth'; -import { - createLoginUseCase, - createLogoutUseCase, - createMeUseCase, - type AuthSession, -} from '../../../application/use-cases/authenticate'; /** * Helper that builds a JSON Response with optional extra headers. @@ -148,4 +148,4 @@ export const handleMe = async (req: Request): Promise => { username: result.username, expiresAt: result.expiresAt, }); -}; \ No newline at end of file +}; diff --git a/src/interfaces/http/controllers/file-controller.ts b/src/interfaces/http/controllers/file-controller.ts index 9b22b1b..b66f9bf 100644 --- a/src/interfaces/http/controllers/file-controller.ts +++ b/src/interfaces/http/controllers/file-controller.ts @@ -1,10 +1,9 @@ import { createReadStream } from 'node:fs'; import { nanoid } from 'nanoid'; -import { config } from '../../../config/index'; import { fileInfoCache } from '../../../infrastructure/cache/index'; -import { createChunkedObjectResponse } from '../../../utils/chunked-storage'; -import { cleanupTempFile, formatCreatedAt, getErrorMessage } from '../../../shared/utils/file'; import logger from '../../../shared/logger/index'; +import { cleanupTempFile, formatCreatedAt, getErrorMessage } from '../../../shared/utils/file'; +import { createChunkedObjectResponse } from '../../../utils/chunked-storage'; import { getFileInfo, type TelegramFileInfo } from '../../../utils/telegram'; import { locateZipEntry } from '../../../utils/zip'; @@ -25,7 +24,7 @@ type RequestWithParams = Request & { * @param value - The string value to wrap. * @returns The value as a single-element tuple. */ -const asArray = (value: string): string[] => [value]; +const _asArray = (value: string): string[] => [value]; /** * Resolves Telegram file metadata for a given file ID, using the in-memory @@ -35,7 +34,10 @@ const asArray = (value: string): string[] => [value]; * @param publicId - The public file ID (used for logging). * @returns The resolved Telegram file info. */ -const getTelegramFileInfo = async (telegramFileId: string, publicId: string): Promise => { +const getTelegramFileInfo = async ( + telegramFileId: string, + publicId: string, +): Promise => { const cacheKey = `file_info_${telegramFileId}`; const cached = fileInfoCache.get(cacheKey) as TelegramFileInfo | null; @@ -212,4 +214,4 @@ export const handleFileInfo = async (req: RequestWithParams): Promise logger.error('File info error', { publicId, error: getErrorMessage(error) }); return fail(500, 'Server error'); } -}; \ No newline at end of file +}; diff --git a/src/interfaces/http/controllers/health-controller.ts b/src/interfaces/http/controllers/health-controller.ts index caf5f43..d897911 100644 --- a/src/interfaces/http/controllers/health-controller.ts +++ b/src/interfaces/http/controllers/health-controller.ts @@ -1,7 +1,7 @@ import { sql } from 'drizzle-orm'; import { db } from '../../../infrastructure/persistence/drizzle/index'; -import { getErrorMessage } from '../../../shared/utils/file'; import logger from '../../../shared/logger/index'; +import { getErrorMessage } from '../../../shared/utils/file'; /** * Handles the health-check endpoint. @@ -22,4 +22,4 @@ export const handleHealth = async (_req: Request): Promise => { logger.error('Health check failed', { error: message }); return Response.json({ status: 'error', error: message }, { status: 500 }); } -}; \ No newline at end of file +}; diff --git a/src/interfaces/http/controllers/home-controller.ts b/src/interfaces/http/controllers/home-controller.ts index 6f4df5a..75b7f69 100644 --- a/src/interfaces/http/controllers/home-controller.ts +++ b/src/interfaces/http/controllers/home-controller.ts @@ -16,4 +16,4 @@ export const handleHome = async (): Promise => { 'content-type': 'text/html; charset=utf-8', }, }); -}; \ No newline at end of file +}; diff --git a/src/interfaces/http/controllers/s3-controller.ts b/src/interfaces/http/controllers/s3-controller.ts index 224ecb6..4319699 100644 --- a/src/interfaces/http/controllers/s3-controller.ts +++ b/src/interfaces/http/controllers/s3-controller.ts @@ -1,11 +1,7 @@ import { createReadStream } from 'node:fs'; import { nanoid } from 'nanoid'; -import { - createBucket, - deleteBucket, - findBucketByName, - listBuckets, -} from '../../../db/buckets'; +import { config } from '../../../config/index'; +import { createBucket, deleteBucket, findBucketByName, listBuckets } from '../../../db/buckets'; import { countBucketObjects, findFileByBucketAndKey, @@ -22,18 +18,12 @@ import { listMultipartUploadsByBucket, } from '../../../db/multipart'; import type { File } from '../../../db/schema'; -import { config } from '../../../config/index'; +import logger from '../../../shared/logger/index'; +import { cleanupTempFile, ensureExtension, getErrorMessage } from '../../../shared/utils/file'; import { createChunkedObjectResponse, storeFileInTelegramChunks, } from '../../../utils/chunked-storage'; -import { - cleanupTempFile, - computeHash, - ensureExtension, - getErrorMessage, -} from '../../../shared/utils/file'; -import logger from '../../../shared/logger/index'; import { verifyPresignedUrl, verifySignature } from '../../../utils/s3/auth'; import { S3_CORS_HEADERS, s3Headers } from '../../../utils/s3/headers'; import { createGetObjectResponse, type ObjectPartSource } from '../../../utils/s3/object-stream'; @@ -700,7 +690,14 @@ const streamBodyToTemp = async ( const tempPath = `/tmp/filedrop-s3-${nanoid()}`; const writer = Bun.file(tempPath).writer(); const hasher = new Bun.CryptoHasher('sha256'); - const reader = (body ?? new ReadableStream({ start(c) { c.close() } })).getReader(); + const reader = ( + body ?? + new ReadableStream({ + start(c) { + c.close(); + }, + }) + ).getReader(); const SIGNATURE_BYTES = 16; const signatureChunks: Buffer[] = []; let signatureBytes = 0; @@ -1273,7 +1270,14 @@ const handleUploadPart = async ( // Stream the part body to temp — O(1) memory, safe for large parts const tempPath = `/tmp/filedrop-mp-${nanoid()}`; const writer = Bun.file(tempPath).writer(); - const reader = (req.body ?? new ReadableStream({ start(c) { c.close() } })).getReader(); + const reader = ( + req.body ?? + new ReadableStream({ + start(c) { + c.close(); + }, + }) + ).getReader(); const hasher = new Bun.CryptoHasher('sha256'); let sizeBytes = 0; @@ -1546,4 +1550,4 @@ const handleListParts = async ( ); return s3Response(xml, 200, reqId, { 'content-type': 'application/xml' }); -}; \ No newline at end of file +}; diff --git a/src/interfaces/http/controllers/upload-controller.ts b/src/interfaces/http/controllers/upload-controller.ts index 0601bd0..99803e8 100644 --- a/src/interfaces/http/controllers/upload-controller.ts +++ b/src/interfaces/http/controllers/upload-controller.ts @@ -1,6 +1,9 @@ import { createWriteStream } from 'node:fs'; import { nanoid } from 'nanoid'; import { config } from '../../../config/index'; +import { findFileByHash } from '../../../db/files'; +import logger from '../../../shared/logger/index'; +import { metricsCollector } from '../../../shared/metrics/index'; import { buildUploadResponse, checkFileSize, @@ -11,11 +14,8 @@ import { getErrorMessage, getFileType, } from '../../../shared/utils/file'; -import logger from '../../../shared/logger/index'; -import { metricsCollector } from '../../../shared/metrics/index'; -import { enqueuePreparedUpload, type PreparedUpload } from '../../../utils/uploadBatcher'; import { storeFileInTelegramChunks } from '../../../utils/chunked-storage'; -import { findFileByHash } from '../../../db/files'; +import { enqueuePreparedUpload, type PreparedUpload } from '../../../utils/uploadBatcher'; /** * Maximum allowed size (in bytes) for a base64 JSON upload. @@ -399,4 +399,4 @@ export const handleUpload = async (req: Request): Promise => { } finally { metricsCollector.recordUploadTime(performance.now() - startTime); } -}; \ No newline at end of file +}; diff --git a/src/interfaces/http/controllers/web-api-controller.ts b/src/interfaces/http/controllers/web-api-controller.ts index 2fa08bb..79e4675 100644 --- a/src/interfaces/http/controllers/web-api-controller.ts +++ b/src/interfaces/http/controllers/web-api-controller.ts @@ -1,5 +1,6 @@ import { createReadStream } from 'node:fs'; import { nanoid } from 'nanoid'; +import { config } from '../../../config/index'; import { createBucket, deleteBucket, findBucketByName, listBuckets } from '../../../db/buckets'; import { countBucketObjects, @@ -7,10 +8,12 @@ import { listObjectsByPrefix, softDeleteFile, } from '../../../db/files-ext'; -import { config } from '../../../config/index'; -import { createChunkedObjectResponse, storeFileInTelegramChunks } from '../../../utils/chunked-storage'; -import { cleanupTempFile, computeHash, ensureExtension, getErrorMessage } from '../../../shared/utils/file'; import logger from '../../../shared/logger/index'; +import { cleanupTempFile, ensureExtension, getErrorMessage } from '../../../shared/utils/file'; +import { + createChunkedObjectResponse, + storeFileInTelegramChunks, +} from '../../../utils/chunked-storage'; import { forwardToStorage, getFileInfo } from '../../../utils/telegram'; /** @@ -463,4 +466,4 @@ export const handleWebApiV1 = async (req: Request): Promise => { logger.error('Web API error', { path: pathname, error: getErrorMessage(error) }); return jsonError('Internal server error', 500); } -}; \ No newline at end of file +}; diff --git a/src/interfaces/http/middleware/auth.ts b/src/interfaces/http/middleware/auth.ts index 87e4d50..96ed804 100644 --- a/src/interfaces/http/middleware/auth.ts +++ b/src/interfaces/http/middleware/auth.ts @@ -43,8 +43,7 @@ const getSecret = (secret?: string): string => secret ?? config.adminApiToken; const getCookieName = (cookieName?: string): string => cookieName ?? config.sessionCookieName; const getMaxAgeMs = (maxAgeMs?: number): number => maxAgeMs ?? config.sessionMaxAgeMs; -const encodePayload = (value: string): string => - Buffer.from(value, 'utf8').toString('base64url'); +const encodePayload = (value: string): string => Buffer.from(value, 'utf8').toString('base64url'); const decodePayload = (value: string): string | null => { try { @@ -107,10 +106,7 @@ export const signCookiePayload = (payload: string, secret: string): string => * @param secret - HMAC signing key. * @returns The unsigned payload string, or `null` on failure. */ -export const verifyCookieSignature = ( - cookieValue: string, - secret: string, -): string | null => { +export const verifyCookieSignature = (cookieValue: string, secret: string): string | null => { const separatorIndex = cookieValue.lastIndexOf(SIGNATURE_SEPARATOR); if (separatorIndex <= 0 || separatorIndex === cookieValue.length - 1) { return null; @@ -136,13 +132,7 @@ export const verifyCookieSignature = ( * @returns The cookie attribute string (excluding name=value). */ const cookieAttributes = (maxAgeSeconds: number): string => - [ - `Max-Age=${maxAgeSeconds}`, - 'Path=/', - 'HttpOnly', - 'SameSite=Lax', - 'Secure', - ].join('; '); + [`Max-Age=${maxAgeSeconds}`, 'Path=/', 'HttpOnly', 'SameSite=Lax', 'Secure'].join('; '); /** * Creates a signed session cookie string suitable for use as a diff --git a/src/interfaces/http/middleware/rate-limit.ts b/src/interfaces/http/middleware/rate-limit.ts index 790ea39..2a37719 100644 --- a/src/interfaces/http/middleware/rate-limit.ts +++ b/src/interfaces/http/middleware/rate-limit.ts @@ -1 +1,7 @@ -export { withRateLimit, cleanupRateLimitCache, checkRateLimit, clearRateLimitCache, getRateLimitStats } from '../../../utils/rateLimit'; \ No newline at end of file +export { + checkRateLimit, + cleanupRateLimitCache, + clearRateLimitCache, + getRateLimitStats, + withRateLimit, +} from '../../../utils/rateLimit'; diff --git a/src/interfaces/http/routes/index.ts b/src/interfaces/http/routes/index.ts index 65867a7..5eeb4d6 100644 --- a/src/interfaces/http/routes/index.ts +++ b/src/interfaces/http/routes/index.ts @@ -1,16 +1,16 @@ import { config } from '../../../config/index'; +import { handleSwaggerHtml, handleSwaggerJson } from '../../../routes/swagger'; +import { extractS3BucketFromHost } from '../../../utils/s3/virtual-host'; +import { isS3Request } from '../../s3/auth'; import { handleLogin, handleLogout, handleMe } from '../controllers/auth-controller'; -import { handleFileRedirect, handleFileInfo } from '../controllers/file-controller'; +import { handleFileInfo, handleFileRedirect } from '../controllers/file-controller'; import { handleHealth } from '../controllers/health-controller'; import { handleHome } from '../controllers/home-controller'; import { handleS3Request } from '../controllers/s3-controller'; -import { handleSwaggerHtml, handleSwaggerJson } from '../../../routes/swagger'; import { handleUpload } from '../controllers/upload-controller'; import { handleWebApiV1 } from '../controllers/web-api-controller'; import { requireAuth } from '../middleware/auth'; import { withRateLimit } from '../middleware/rate-limit'; -import { isS3Request } from '../../s3/auth'; -import { extractS3BucketFromHost } from '../../../utils/s3/virtual-host'; /** * Extracts the S3 bucket name from the request host @@ -47,7 +47,7 @@ const shouldHandleS3 = (req: Request, headers: Record): boolean * @param req - The incoming HTTP request. * @returns A Response from the S3 handler or a 405 response. */ -const handleMaybeS3Root = (req: Request): Response | Promise => { +const _handleMaybeS3Root = (req: Request): Response | Promise => { if (req.method === 'OPTIONS') { return handleS3Request(req, getS3RouteBucket(req)); } diff --git a/src/interfaces/s3/auth.ts b/src/interfaces/s3/auth.ts index 6a14c0f..f1dae39 100644 --- a/src/interfaces/s3/auth.ts +++ b/src/interfaces/s3/auth.ts @@ -1,2 +1,7 @@ -export { isS3Request, buildCanonicalQueryString, verifyPresignedUrl, verifySignature } from '../../utils/s3/auth'; -export type { SigV4Result, VerifyPresignedUrlInput } from '../../utils/s3/auth'; \ No newline at end of file +export type { SigV4Result, VerifyPresignedUrlInput } from '../../utils/s3/auth'; +export { + buildCanonicalQueryString, + isS3Request, + verifyPresignedUrl, + verifySignature, +} from '../../utils/s3/auth'; diff --git a/src/interfaces/s3/virtual-host.ts b/src/interfaces/s3/virtual-host.ts index d0ed1f1..ee8cb2a 100644 --- a/src/interfaces/s3/virtual-host.ts +++ b/src/interfaces/s3/virtual-host.ts @@ -1 +1 @@ -export { extractS3BucketFromHost } from '../../utils/s3/virtual-host'; \ No newline at end of file +export { extractS3BucketFromHost } from '../../utils/s3/virtual-host'; diff --git a/src/shared/errors/index.ts b/src/shared/errors/index.ts index ad6fb05..180f704 100644 --- a/src/shared/errors/index.ts +++ b/src/shared/errors/index.ts @@ -70,4 +70,4 @@ export class ValidationError extends DomainError { super(msg); this.name = 'ValidationError'; } -} \ No newline at end of file +} diff --git a/src/shared/logger/index.ts b/src/shared/logger/index.ts index 571879c..829fc61 100644 --- a/src/shared/logger/index.ts +++ b/src/shared/logger/index.ts @@ -1,4 +1,4 @@ -import _logger from "../../utils/logger"; +import _logger from '../../utils/logger'; export default _logger; +export type { Logger } from 'winston'; export { _logger as logger }; -export type { Logger } from "winston"; diff --git a/src/shared/metrics/index.ts b/src/shared/metrics/index.ts index 8905300..20837d7 100644 --- a/src/shared/metrics/index.ts +++ b/src/shared/metrics/index.ts @@ -1 +1 @@ -export { metricsCollector, MetricsCollector } from '../../utils/metrics'; \ No newline at end of file +export { MetricsCollector, metricsCollector } from '../../utils/metrics'; diff --git a/src/shared/utils/retry.ts b/src/shared/utils/retry.ts index 3ea101d..7810d1a 100644 --- a/src/shared/utils/retry.ts +++ b/src/shared/utils/retry.ts @@ -132,4 +132,4 @@ export const withFallback = async ( }); return fallback(); } -}; \ No newline at end of file +}; diff --git a/src/shared/utils/zip.ts b/src/shared/utils/zip.ts index 515e6ec..ed0c28b 100644 --- a/src/shared/utils/zip.ts +++ b/src/shared/utils/zip.ts @@ -1,8 +1,7 @@ import { once } from 'node:events'; import { createReadStream, createWriteStream } from 'node:fs'; -import { open, stat } from 'node:fs/promises'; +import { open, stat, unlink } from 'node:fs/promises'; import { basename } from 'node:path'; -import { unlink } from 'node:fs/promises'; import { finished } from 'node:stream/promises'; import { nanoid } from 'nanoid'; @@ -400,4 +399,4 @@ export const locateZipEntry = async ( } finally { await handle.close(); } -}; \ No newline at end of file +}; diff --git a/src/utils/s3/auth.ts b/src/utils/s3/auth.ts index 391cef2..3581eda 100644 --- a/src/utils/s3/auth.ts +++ b/src/utils/s3/auth.ts @@ -1,7 +1,5 @@ import { timingSafeEqual } from 'node:crypto'; -import { timingSafeEqual } from 'node:crypto'; - /** * Timing-safe string comparison that prevents timing attacks. * @@ -293,7 +291,10 @@ export const verifyPresignedUrl = async ({ } // AWS S3 spec limits presigned URLs to 7 days (604800 seconds) const MAX_PRESIGNED_EXPIRY_SECONDS = 604800; - if (now.getTime() > signedAt.getTime() + expires * 1000 || expires > MAX_PRESIGNED_EXPIRY_SECONDS) { + if ( + now.getTime() > signedAt.getTime() + expires * 1000 || + expires > MAX_PRESIGNED_EXPIRY_SECONDS + ) { return { isValid: false, credential: null, errorCode: 'AccessDenied' }; } diff --git a/src/utils/zip.ts b/src/utils/zip.ts index 8aa45ef..ac3937e 100644 --- a/src/utils/zip.ts +++ b/src/utils/zip.ts @@ -1,8 +1,7 @@ import { once } from 'node:events'; import { createReadStream, createWriteStream } from 'node:fs'; -import { open, stat } from 'node:fs/promises'; +import { open, stat, unlink } from 'node:fs/promises'; import { basename } from 'node:path'; -import { unlink } from 'node:fs/promises'; import { finished } from 'node:stream/promises'; import { nanoid } from 'nanoid'; diff --git a/test/bot.test.ts b/test/bot.test.ts index 36045e3..4a605bc 100644 --- a/test/bot.test.ts +++ b/test/bot.test.ts @@ -138,7 +138,11 @@ describe('Telegram Bot Handler', () => { }; await fileHandler(ctx); - expect(mockTelegramService.forwardToStorage).toHaveBeenCalledWith('doc_123', 'cv.pdf', 'document'); + expect(mockTelegramService.forwardToStorage).toHaveBeenCalledWith( + 'doc_123', + 'cv.pdf', + 'document', + ); expect(mockFileRepo.create).toHaveBeenCalled(); expect(replyMock).toHaveBeenCalledWith( expect.stringContaining('File berhasil diupload'), @@ -245,7 +249,11 @@ describe('Telegram Bot Handler', () => { }; await fileHandler(ctx); - expect(mockTelegramService.forwardToStorage).toHaveBeenCalledWith('sticker_123', 'file', 'sticker'); + expect(mockTelegramService.forwardToStorage).toHaveBeenCalledWith( + 'sticker_123', + 'file', + 'sticker', + ); expect(mockFileRepo.create).toHaveBeenCalled(); expect(replyMock).toHaveBeenCalledWith( expect.stringContaining('File berhasil diupload'), @@ -278,7 +286,11 @@ describe('Telegram Bot Handler', () => { }; await fileHandler(ctx); - expect(mockTelegramService.forwardToStorage).toHaveBeenCalledWith('video_note_123', 'file', 'video_note'); + expect(mockTelegramService.forwardToStorage).toHaveBeenCalledWith( + 'video_note_123', + 'file', + 'video_note', + ); expect(mockFileRepo.create).toHaveBeenCalled(); expect(replyMock).toHaveBeenCalledWith( expect.stringContaining('File berhasil diupload'), @@ -289,4 +301,4 @@ describe('Telegram Bot Handler', () => { afterAll(() => { mock.restore(); }); -}); \ No newline at end of file +}); diff --git a/test/s3-docker-registry.test.ts b/test/s3-docker-registry.test.ts index 3bb3d03..3d8e360 100644 --- a/test/s3-docker-registry.test.ts +++ b/test/s3-docker-registry.test.ts @@ -7,7 +7,7 @@ * - Concurrent operation safety */ -import { describe, expect, it, mock } from 'bun:test'; +import { describe, expect, it } from 'bun:test'; import { nanoid } from 'nanoid'; // ─── streamBodyToTemp tests ────────────────────────────────────── @@ -19,7 +19,7 @@ describe('S3 Streaming Upload Safety', () => { */ it('streams body to temp file without buffering entire body', async () => { // Import the S3 controller module - const mod = await import('../src/interfaces/http/controllers/s3-controller.ts'); + const _mod = await import('../src/interfaces/http/controllers/s3-controller.ts'); // Create a ReadableStream with known content const content = 'Hello, Docker Registry! This is a test blob.'; @@ -144,9 +144,7 @@ describe('S3 Streaming Upload Safety', () => { * by checking the module source code. */ it('uses streaming instead of req.arrayBuffer() for PUT body', async () => { - const source = await Bun.file( - 'src/interfaces/http/controllers/s3-controller.ts', - ).text(); + const source = await Bun.file('src/interfaces/http/controllers/s3-controller.ts').text(); const codeLines = source.split('\n').filter((l) => !l.trim().startsWith('*')); const codeText = codeLines.join('\n'); @@ -157,7 +155,8 @@ describe('S3 Streaming Upload Safety', () => { // handlePutObject should NOT contain req.arrayBuffer() // (note: comments that mention arrayBuffer are filtered out) - const putObjectCode = codeText.split('handlePutObject =')[1]?.split('storeFileFromTemp =')[0] || ''; + const putObjectCode = + codeText.split('handlePutObject =')[1]?.split('storeFileFromTemp =')[0] || ''; expect(putObjectCode).not.toMatch(/req\.arrayBuffer\(\)/); expect(putObjectCode).toContain('streamBodyToTemp'); }); @@ -171,12 +170,13 @@ describe('S3 UploadPart Streaming', () => { * req.arrayBuffer(). */ it('streams part body instead of req.arrayBuffer()', async () => { - const source = await Bun.file( - 'src/interfaces/http/controllers/s3-controller.ts', - ).text(); + const source = await Bun.file('src/interfaces/http/controllers/s3-controller.ts').text(); // Find the handleUploadPart function - const uploadPartSection = source.split('const handleUploadPart =')[1]?.split('const handleCompleteMultipartUpload =')[0] || ''; + const uploadPartSection = + source + .split('const handleUploadPart =')[1] + ?.split('const handleCompleteMultipartUpload =')[0] || ''; expect(uploadPartSection).not.toContain('arrayBuffer'); expect(uploadPartSection).toContain('getReader'); expect(uploadPartSection).toContain('Bun.file(tempPath).writer()'); @@ -306,9 +306,7 @@ describe('S3 Edge Cases', () => { expect(totalBytes).toBe(0); const fileSize = Bun.file(tempPath).size; expect(fileSize).toBe(0); - expect(hasher.digest('hex')).toBe( - new Bun.CryptoHasher('sha256').update('').digest('hex'), - ); + expect(hasher.digest('hex')).toBe(new Bun.CryptoHasher('sha256').update('').digest('hex')); await Bun.write(tempPath, ''); }); @@ -401,4 +399,4 @@ describe('S3 File Size Limits', () => { // Chunked storage should handle files larger than single chunk expect(config.telegramChunkSizeBytes).toBeLessThan(config.maxRequestBodyBytes); }); -}); \ No newline at end of file +});