chore: fix lint errors — duplicate import, unused imports, formatting
Deploy FileDrop / deploy (push) Successful in 45s

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude committed 2026-07-28 20:09:42 +07:00
1 parent 002492626b
commit 667921b100
41 files changed
+241 -238

No files matched your search

+25 -8
View File
@@ -26,23 +26,40 @@ export class Cache<T> {
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<T> { value: T; expiresAt: number }
interface CacheEntry<T> {
value: T;
expiresAt: number;
}
export const fileInfoCache = new Cache<{
file_size: number; mime_type: string; file_path: string; bot_token: string;
}>(3600);
file_size: number;
mime_type: string;
file_path: string;
bot_token: string;
}>(3600);
@@ -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<void> => {
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;
@@ -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<string, unknown>;
@@ -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<string, unknown>): 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),
});
@@ -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<string, unknown>): 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<File | null> {
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<File | null> {
async findByBucketAndKey(bucketId: string, s3Key: string): Promise<File | null> {
const result = await db
.select()
.from(fileSchema)
@@ -126,10 +109,7 @@ export class DrizzleFileRepository implements IFileRepository {
* {@inheritDoc IFileRepository.create}
*/
async create(file: NewFile): Promise<File> {
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<string, unknown>[];
const rawResult = (await db.execute(query)) as unknown as Record<string, unknown>[];
if (delimiter === '/') {
const prefixSet = new Set<string>();
@@ -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<number> {
async softDeleteBatch(bucketId: string, keys: string[]): Promise<number> {
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);
}
}
@@ -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<string, unknown>,
): MultipartUpload => ({
const mapRowToMultipartUpload = (r: Record<string, unknown>): 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<string> {
async create(bucketId: string, s3Key: string, initiatedBy: string): Promise<string> {
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<MultipartPart, 'id' | 'createdAt'>,
): Promise<void> {
async insertPart(part: Omit<MultipartPart, 'id' | 'createdAt'>): Promise<void> {
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,
};
}
}
+12 -9
View File
@@ -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);
}
@@ -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.
@@ -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.