refactor: full DDD + Clean Architecture refactor
Deploy FileDrop / deploy (push) Successful in 48s
Deploy FileDrop / deploy (push) Successful in 48s
- Hapus src/utils/ (17 files) + src/db/ (8 files) dead code - Absorb 7 re-export stubs → real impl di lokasi DDD - Buat src/infrastructure/di.ts (DI container) - Rewrite 5 controllers pakai repository/DI - Fix shared/utils imports, env.ts, routes, index.ts - Update package.json build path migrate - Lint clean, build clean
This commit is contained in:
@@ -11,7 +11,7 @@ import {
|
||||
createSessionCookie,
|
||||
getAuthSession,
|
||||
isAuthEnabled,
|
||||
} from '../../../utils/auth';
|
||||
} from '../middleware/auth';
|
||||
|
||||
/**
|
||||
* Helper that builds a JSON Response with optional extra headers.
|
||||
|
||||
@@ -2,11 +2,11 @@ import { createReadStream } from 'node:fs';
|
||||
import { nanoid } from 'nanoid';
|
||||
import type { TelegramFileInfo } from '../../../domain/ports/telegram-service';
|
||||
import { fileInfoCache } from '../../../infrastructure/cache/index';
|
||||
import { chunkedStorage } from '../../../infrastructure/di';
|
||||
import { botPool } from '../../../infrastructure/telegram/bot-pool';
|
||||
import logger from '../../../shared/logger/index';
|
||||
import { cleanupTempFile, formatCreatedAt, getErrorMessage } from '../../../shared/utils/file';
|
||||
import { createChunkedObjectResponse } from '../../../utils/chunked-storage';
|
||||
import { locateZipEntry } from '../../../utils/zip';
|
||||
import { locateZipEntry } from '../../../shared/utils/zip';
|
||||
|
||||
/**
|
||||
* Extended Request type that includes route parameter access.
|
||||
@@ -115,7 +115,7 @@ export const handleFileRedirect = async (req: RequestWithParams): Promise<Respon
|
||||
return fail(501, 'Archive entry extraction is not supported for chunked files');
|
||||
}
|
||||
const range = { type: 'none' as const };
|
||||
return createChunkedObjectResponse({ file, range, reqId: '' });
|
||||
return chunkedStorage.createChunkedObjectResponse({ file, range, reqId: '' });
|
||||
}
|
||||
|
||||
const archiveEntryName = file.archiveEntryName;
|
||||
|
||||
@@ -1,36 +1,22 @@
|
||||
import { createReadStream } from 'node:fs';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { config } from '../../../config/index';
|
||||
import { createBucket, deleteBucket, findBucketByName, listBuckets } from '../../../db/buckets';
|
||||
import {
|
||||
countBucketObjects,
|
||||
findFileByBucketAndKey,
|
||||
listObjectsByPrefix,
|
||||
softDeleteFile,
|
||||
} from '../../../db/files-ext';
|
||||
import { db, files as fileSchema } from '../../../db/index';
|
||||
import {
|
||||
abortMultipartUpload,
|
||||
completeMultipartUpload,
|
||||
createMultipartUpload,
|
||||
findMultipartUpload,
|
||||
insertMultipartPart,
|
||||
listMultipartParts,
|
||||
listMultipartUploadsByBucket,
|
||||
} from '../../../db/multipart';
|
||||
import type { File } from '../../../db/schema';
|
||||
import type { File as FileEntity } from '../../../domain/entities/file';
|
||||
import type { ForwardResult } from '../../../domain/ports/telegram-service';
|
||||
import {
|
||||
bucketRepository,
|
||||
chunkedStorage,
|
||||
fileRepository,
|
||||
multipartRepository,
|
||||
} from '../../../infrastructure/di';
|
||||
import { db, files as fileSchema } from '../../../infrastructure/persistence/drizzle/index';
|
||||
import { botPool } from '../../../infrastructure/telegram/bot-pool';
|
||||
import logger from '../../../shared/logger/index';
|
||||
import { cleanupTempFile, ensureExtension, getErrorMessage } from '../../../shared/utils/file';
|
||||
import {
|
||||
createChunkedObjectResponse,
|
||||
storeFileInTelegramChunks,
|
||||
} from '../../../utils/chunked-storage';
|
||||
import { verifyBodyHash, verifyPresignedUrl, verifySignature } from '../../../utils/s3/auth';
|
||||
import { S3_CORS_HEADERS, s3Headers } from '../../../utils/s3/headers';
|
||||
import { createGetObjectResponse, type ObjectPartSource } from '../../../utils/s3/object-stream';
|
||||
import { parseRangeHeader, unsatisfiedContentRange } from '../../../utils/s3/range';
|
||||
import { verifyBodyHash, verifyPresignedUrl, verifySignature } from '../../s3/auth';
|
||||
import { S3_CORS_HEADERS, s3Headers } from '../../s3/headers';
|
||||
import { createGetObjectResponse, type ObjectPartSource } from '../../s3/object-stream';
|
||||
import { parseRangeHeader, unsatisfiedContentRange } from '../../s3/range';
|
||||
import {
|
||||
bucketVersioningConfigurationXml,
|
||||
completeMultipartUploadXml,
|
||||
@@ -45,7 +31,7 @@ import {
|
||||
parseCompleteMultipartBody,
|
||||
parseDeleteObjectsBody,
|
||||
s3ErrorResponse,
|
||||
} from '../../../utils/s3/xml';
|
||||
} from '../../s3/xml';
|
||||
|
||||
/**
|
||||
* The default S3 region returned when no region is explicitly configured.
|
||||
@@ -309,7 +295,7 @@ export const handleS3Request = async (
|
||||
* @returns An S3 XML response with the bucket list.
|
||||
*/
|
||||
const handleListBuckets = async (reqId: string): Promise<Response> => {
|
||||
const buckets = await listBuckets();
|
||||
const buckets = await bucketRepository.list();
|
||||
const xml = listBucketsXml(buckets, reqId);
|
||||
return s3Response(xml, 200, reqId, { 'content-type': 'application/xml' });
|
||||
};
|
||||
@@ -339,7 +325,7 @@ const handleCreateBucket = async (bucketName: string, reqId: string): Promise<Re
|
||||
reqId,
|
||||
);
|
||||
}
|
||||
const existing = await findBucketByName(bucketName);
|
||||
const existing = await bucketRepository.findByName(bucketName);
|
||||
if (existing) {
|
||||
return s3ErrorResponse(
|
||||
'BucketAlreadyExists',
|
||||
@@ -349,7 +335,7 @@ const handleCreateBucket = async (bucketName: string, reqId: string): Promise<Re
|
||||
reqId,
|
||||
);
|
||||
}
|
||||
await createBucket(bucketName);
|
||||
await bucketRepository.create(bucketName);
|
||||
return s3Response(null, 200, reqId);
|
||||
};
|
||||
|
||||
@@ -361,7 +347,7 @@ const handleCreateBucket = async (bucketName: string, reqId: string): Promise<Re
|
||||
* @returns A 200 response when the bucket exists, or an S3 XML error.
|
||||
*/
|
||||
const handleHeadBucket = async (bucketName: string, reqId: string): Promise<Response> => {
|
||||
const bucket = await findBucketByName(bucketName);
|
||||
const bucket = await bucketRepository.findByName(bucketName);
|
||||
if (!bucket) {
|
||||
return s3ErrorResponse(
|
||||
'NoSuchBucket',
|
||||
@@ -384,7 +370,7 @@ const handleHeadBucket = async (bucketName: string, reqId: string): Promise<Resp
|
||||
* @returns A 204 response on success, or an S3 XML error.
|
||||
*/
|
||||
const handleDeleteBucket = async (bucketName: string, reqId: string): Promise<Response> => {
|
||||
const bucket = await findBucketByName(bucketName);
|
||||
const bucket = await bucketRepository.findByName(bucketName);
|
||||
if (!bucket) {
|
||||
return s3ErrorResponse(
|
||||
'NoSuchBucket',
|
||||
@@ -394,7 +380,7 @@ const handleDeleteBucket = async (bucketName: string, reqId: string): Promise<Re
|
||||
reqId,
|
||||
);
|
||||
}
|
||||
const objCount = await countBucketObjects(bucket.id);
|
||||
const objCount = await fileRepository.countByBucket(bucket.id);
|
||||
if (objCount > 0) {
|
||||
return s3ErrorResponse(
|
||||
'BucketNotEmpty',
|
||||
@@ -404,7 +390,7 @@ const handleDeleteBucket = async (bucketName: string, reqId: string): Promise<Re
|
||||
reqId,
|
||||
);
|
||||
}
|
||||
await deleteBucket(bucketName);
|
||||
await bucketRepository.delete(bucketName);
|
||||
return s3Response(null, 204, reqId);
|
||||
};
|
||||
|
||||
@@ -417,7 +403,7 @@ const handleDeleteBucket = async (bucketName: string, reqId: string): Promise<Re
|
||||
* @returns An S3 XML response with the versioning configuration.
|
||||
*/
|
||||
const handleGetBucketVersioning = async (bucketName: string, reqId: string): Promise<Response> => {
|
||||
const bucket = await findBucketByName(bucketName);
|
||||
const bucket = await bucketRepository.findByName(bucketName);
|
||||
if (!bucket) {
|
||||
return s3ErrorResponse(
|
||||
'NoSuchBucket',
|
||||
@@ -456,7 +442,7 @@ const handleGetObject = async (
|
||||
headers: Record<string, string>,
|
||||
reqId: string,
|
||||
): Promise<Response> => {
|
||||
const bucketRecord = await findBucketByName(bucket);
|
||||
const bucketRecord = await bucketRepository.findByName(bucket);
|
||||
if (!bucketRecord)
|
||||
return s3ErrorResponse(
|
||||
'NoSuchBucket',
|
||||
@@ -466,7 +452,7 @@ const handleGetObject = async (
|
||||
reqId,
|
||||
);
|
||||
|
||||
const file = await findFileByBucketAndKey(bucketRecord.id, key);
|
||||
const file = await fileRepository.findByBucketAndKey(bucketRecord.id, key);
|
||||
if (!file)
|
||||
return s3ErrorResponse(
|
||||
'NoSuchKey',
|
||||
@@ -551,7 +537,7 @@ const handleGetObject = async (
|
||||
);
|
||||
}
|
||||
try {
|
||||
return await createChunkedObjectResponse({ file, range, reqId });
|
||||
return await chunkedStorage.createChunkedObjectResponse({ file, range, reqId });
|
||||
} catch (error) {
|
||||
logger.warn('Chunked object content fetch failed', { key, error: getErrorMessage(error) });
|
||||
return s3ErrorResponse(
|
||||
@@ -638,14 +624,14 @@ const handleGetObject = async (
|
||||
* @returns An S3 response streaming the assembled object content.
|
||||
*/
|
||||
const handleGetMultipartObject = async (
|
||||
file: File,
|
||||
file: FileEntity,
|
||||
bucket: string,
|
||||
key: string,
|
||||
headers: Record<string, string>,
|
||||
reqId: string,
|
||||
): Promise<Response> => {
|
||||
const uploadId = file.multipartUploadId!;
|
||||
const parts = await listMultipartParts(uploadId);
|
||||
const parts = await multipartRepository.listParts(uploadId);
|
||||
|
||||
if (parts.length === 0) {
|
||||
return s3ErrorResponse(
|
||||
@@ -724,7 +710,7 @@ const handleHeadObject = async (
|
||||
headers: Record<string, string>,
|
||||
reqId: string,
|
||||
): Promise<Response> => {
|
||||
const bucketRecord = await findBucketByName(bucket);
|
||||
const bucketRecord = await bucketRepository.findByName(bucket);
|
||||
if (!bucketRecord)
|
||||
return s3ErrorResponse(
|
||||
'NoSuchBucket',
|
||||
@@ -734,7 +720,7 @@ const handleHeadObject = async (
|
||||
reqId,
|
||||
);
|
||||
|
||||
const file = await findFileByBucketAndKey(bucketRecord.id, key);
|
||||
const file = await fileRepository.findByBucketAndKey(bucketRecord.id, key);
|
||||
if (!file)
|
||||
return s3ErrorResponse(
|
||||
'NoSuchKey',
|
||||
@@ -930,7 +916,7 @@ const handlePutObject = async (
|
||||
req: Request,
|
||||
reqId: string,
|
||||
): Promise<Response> => {
|
||||
const bucketRecord = await findBucketByName(bucket);
|
||||
const bucketRecord = await bucketRepository.findByName(bucket);
|
||||
if (!bucketRecord)
|
||||
return s3ErrorResponse(
|
||||
'NoSuchBucket',
|
||||
@@ -1011,7 +997,7 @@ const handlePutObject = async (
|
||||
|
||||
// Idempotent PUT: if the object already exists, skip upload
|
||||
try {
|
||||
const existing = await findFileByBucketAndKey(bucketRecord.id, key);
|
||||
const existing = await fileRepository.findByBucketAndKey(bucketRecord.id, key);
|
||||
if (existing) {
|
||||
await cleanupTempFile(streamed.tempPath);
|
||||
return s3Response(null, 200, reqId, { etag: `"${streamed.fileHash}"` });
|
||||
@@ -1057,7 +1043,7 @@ const storeFileFromTemp = async (
|
||||
const partFileNamePrefix = `s3-${bucketRecord.name}-${key.replace(/\//g, '_')}`;
|
||||
|
||||
if (streamed.sizeBytes > config.telegramChunkSizeBytes) {
|
||||
const file = await storeFileInTelegramChunks({
|
||||
const file = await chunkedStorage.storeFileInTelegramChunks({
|
||||
tempPath: streamed.tempPath,
|
||||
partFileNamePrefix,
|
||||
fileName: finalFileName,
|
||||
@@ -1138,7 +1124,7 @@ const handleCopyObject = async (
|
||||
const sourceBucket = parts[0];
|
||||
const sourceKey = parts.slice(1).join('/');
|
||||
|
||||
const sourceBucketRecord = await findBucketByName(sourceBucket);
|
||||
const sourceBucketRecord = await bucketRepository.findByName(sourceBucket);
|
||||
if (!sourceBucketRecord)
|
||||
return s3ErrorResponse(
|
||||
'NoSuchBucket',
|
||||
@@ -1148,7 +1134,7 @@ const handleCopyObject = async (
|
||||
reqId,
|
||||
);
|
||||
|
||||
const sourceFile = await findFileByBucketAndKey(sourceBucketRecord.id, sourceKey);
|
||||
const sourceFile = await fileRepository.findByBucketAndKey(sourceBucketRecord.id, sourceKey);
|
||||
if (!sourceFile)
|
||||
return s3ErrorResponse(
|
||||
'NoSuchKey',
|
||||
@@ -1232,7 +1218,7 @@ const handleDeleteObject = async (
|
||||
key: string,
|
||||
reqId: string,
|
||||
): Promise<Response> => {
|
||||
const bucketRecord = await findBucketByName(bucket);
|
||||
const bucketRecord = await bucketRepository.findByName(bucket);
|
||||
if (!bucketRecord)
|
||||
return s3ErrorResponse(
|
||||
'NoSuchBucket',
|
||||
@@ -1242,7 +1228,7 @@ const handleDeleteObject = async (
|
||||
reqId,
|
||||
);
|
||||
|
||||
await softDeleteFile(bucketRecord.id, key);
|
||||
await fileRepository.softDelete(bucketRecord.id, key);
|
||||
return s3Response(null, 204, reqId);
|
||||
};
|
||||
|
||||
@@ -1262,7 +1248,7 @@ const handleDeleteObjects = async (
|
||||
body: string,
|
||||
reqId: string,
|
||||
): Promise<Response> => {
|
||||
const bucketRecord = await findBucketByName(bucket);
|
||||
const bucketRecord = await bucketRepository.findByName(bucket);
|
||||
if (!bucketRecord)
|
||||
return s3ErrorResponse(
|
||||
'NoSuchBucket',
|
||||
@@ -1288,7 +1274,7 @@ const handleDeleteObjects = async (
|
||||
const deletedKeys: string[] = [];
|
||||
const errors: Array<{ key: string; code: string; message: string }> = [];
|
||||
for (const key of keys) {
|
||||
const ok = await softDeleteFile(bucketRecord.id, key);
|
||||
const ok = await fileRepository.softDelete(bucketRecord.id, key);
|
||||
if (ok) {
|
||||
deletedKeys.push(key);
|
||||
} else {
|
||||
@@ -1316,7 +1302,7 @@ const handleListObjectsV1 = async (
|
||||
searchParams: URLSearchParams,
|
||||
reqId: string,
|
||||
): Promise<Response> => {
|
||||
const bucketRecord = await findBucketByName(bucket);
|
||||
const bucketRecord = await bucketRepository.findByName(bucket);
|
||||
if (!bucketRecord)
|
||||
return s3ErrorResponse(
|
||||
'NoSuchBucket',
|
||||
@@ -1335,7 +1321,7 @@ const handleListObjectsV1 = async (
|
||||
const marker = searchParams.get('marker') || null;
|
||||
const encodingType = searchParams.get('encoding-type') || null;
|
||||
|
||||
const { objects, prefixes: commonPrefixes } = await listObjectsByPrefix(
|
||||
const { objects, prefixes: commonPrefixes } = await fileRepository.listByPrefix(
|
||||
bucketRecord.id,
|
||||
prefix,
|
||||
delimiter,
|
||||
@@ -1386,7 +1372,7 @@ const handleListObjectsV2 = async (
|
||||
searchParams: URLSearchParams,
|
||||
reqId: string,
|
||||
): Promise<Response> => {
|
||||
const bucketRecord = await findBucketByName(bucket);
|
||||
const bucketRecord = await bucketRepository.findByName(bucket);
|
||||
if (!bucketRecord)
|
||||
return s3ErrorResponse(
|
||||
'NoSuchBucket',
|
||||
@@ -1403,7 +1389,7 @@ const handleListObjectsV2 = async (
|
||||
const startAfter = searchParams.get('start-after') || null;
|
||||
const encodingType = searchParams.get('encoding-type') || null;
|
||||
|
||||
const { objects, prefixes: commonPrefixes } = await listObjectsByPrefix(
|
||||
const { objects, prefixes: commonPrefixes } = await fileRepository.listByPrefix(
|
||||
bucketRecord.id,
|
||||
prefix,
|
||||
delimiter,
|
||||
@@ -1459,7 +1445,7 @@ const handleCreateMultipartUpload = async (
|
||||
headers: Record<string, string>,
|
||||
reqId: string,
|
||||
): Promise<Response> => {
|
||||
const bucketRecord = await findBucketByName(bucket);
|
||||
const bucketRecord = await bucketRepository.findByName(bucket);
|
||||
if (!bucketRecord)
|
||||
return s3ErrorResponse(
|
||||
'NoSuchBucket',
|
||||
@@ -1470,7 +1456,7 @@ const handleCreateMultipartUpload = async (
|
||||
);
|
||||
|
||||
const contentType = headers['content-type'] || null;
|
||||
const uploadId = await createMultipartUpload(bucketRecord.id, key, 's3', contentType);
|
||||
const uploadId = await multipartRepository.create(bucketRecord.id, key, 's3', contentType);
|
||||
|
||||
const xml = initiateMultipartUploadXml(bucket, key, uploadId);
|
||||
return s3Response(xml, 200, reqId, { 'content-type': 'application/xml' });
|
||||
@@ -1514,7 +1500,7 @@ const handleUploadPart = async (
|
||||
);
|
||||
}
|
||||
|
||||
const multipart = await findMultipartUpload(uploadId);
|
||||
const multipart = await multipartRepository.findById(uploadId);
|
||||
if (!multipart || multipart.s3Key !== key) {
|
||||
return s3ErrorResponse(
|
||||
'NoSuchUpload',
|
||||
@@ -1583,7 +1569,7 @@ const handleUploadPart = async (
|
||||
await cleanupTempFile(tempPath);
|
||||
|
||||
const etag = hasher.digest('hex');
|
||||
await insertMultipartPart({
|
||||
await multipartRepository.insertPart({
|
||||
uploadId,
|
||||
partNumber,
|
||||
telegramFileId: forwardResult.telegramFileId,
|
||||
@@ -1617,7 +1603,7 @@ const handleCompleteMultipartUpload = async (
|
||||
reqId: string,
|
||||
): Promise<Response> => {
|
||||
const uploadId = searchParams.get('uploadId')!;
|
||||
const multipart = await findMultipartUpload(uploadId);
|
||||
const multipart = await multipartRepository.findById(uploadId);
|
||||
// H5: Verify both upload exists AND key matches (consistent with handleUploadPart)
|
||||
if (!multipart || multipart.s3Key !== key) {
|
||||
return s3ErrorResponse(
|
||||
@@ -1630,7 +1616,7 @@ const handleCompleteMultipartUpload = async (
|
||||
}
|
||||
|
||||
const parts = parseCompleteMultipartBody(body);
|
||||
const storedParts = await listMultipartParts(uploadId);
|
||||
const storedParts = await multipartRepository.listParts(uploadId);
|
||||
|
||||
// Validate ascending part order
|
||||
const partNumbers = parts.map((p) => p.partNumber);
|
||||
@@ -1702,7 +1688,7 @@ const handleCompleteMultipartUpload = async (
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
await completeMultipartUpload(uploadId);
|
||||
await multipartRepository.complete(uploadId);
|
||||
|
||||
const location = `${config.baseUrl}/${bucket}/${key}`;
|
||||
const xml = completeMultipartUploadXml(bucket, key, combinedEtag, location);
|
||||
@@ -1723,7 +1709,7 @@ const handleListMultipartUploads = async (
|
||||
searchParams: URLSearchParams,
|
||||
reqId: string,
|
||||
): Promise<Response> => {
|
||||
const bucketRecord = await findBucketByName(bucket);
|
||||
const bucketRecord = await bucketRepository.findByName(bucket);
|
||||
if (!bucketRecord)
|
||||
return s3ErrorResponse(
|
||||
'NoSuchBucket',
|
||||
@@ -1735,7 +1721,7 @@ const handleListMultipartUploads = async (
|
||||
|
||||
const maxUploads = Math.min(Number.parseInt(searchParams.get('max-uploads') || '1000', 10), 1000);
|
||||
const keyMarker = searchParams.get('key-marker') || null;
|
||||
const { uploads, isTruncated, nextKeyMarker } = await listMultipartUploadsByBucket(
|
||||
const { uploads, isTruncated, nextKeyMarker } = await multipartRepository.listByBucket(
|
||||
bucketRecord.id,
|
||||
maxUploads,
|
||||
keyMarker,
|
||||
@@ -1774,7 +1760,7 @@ const handleAbortMultipartUpload = async (
|
||||
reqId: string,
|
||||
): Promise<Response> => {
|
||||
const uploadId = searchParams.get('uploadId')!;
|
||||
const multipart = await findMultipartUpload(uploadId);
|
||||
const multipart = await multipartRepository.findById(uploadId);
|
||||
if (!multipart) {
|
||||
return s3ErrorResponse(
|
||||
'NoSuchUpload',
|
||||
@@ -1785,7 +1771,7 @@ const handleAbortMultipartUpload = async (
|
||||
);
|
||||
}
|
||||
|
||||
await abortMultipartUpload(uploadId);
|
||||
await multipartRepository.abort(uploadId);
|
||||
return s3Response(null, 204, reqId);
|
||||
};
|
||||
|
||||
@@ -1807,7 +1793,7 @@ const handleListParts = async (
|
||||
reqId: string,
|
||||
): Promise<Response> => {
|
||||
const uploadId = searchParams.get('uploadId')!;
|
||||
const multipart = await findMultipartUpload(uploadId);
|
||||
const multipart = await multipartRepository.findById(uploadId);
|
||||
if (!multipart) {
|
||||
return s3ErrorResponse(
|
||||
'NoSuchUpload',
|
||||
@@ -1818,7 +1804,7 @@ const handleListParts = async (
|
||||
);
|
||||
}
|
||||
|
||||
const parts = await listMultipartParts(uploadId);
|
||||
const parts = await multipartRepository.listParts(uploadId);
|
||||
const maxParts = Math.min(Number.parseInt(searchParams.get('max-parts') || '1000', 10), 1000);
|
||||
|
||||
const xml = listPartsXml(
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createWriteStream } from 'node:fs';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { config } from '../../../config/index';
|
||||
import { findFileByHash } from '../../../db/files';
|
||||
import { chunkedStorage, fileRepository, uploadBatcher } from '../../../infrastructure/di';
|
||||
import type { PreparedUpload } from '../../../infrastructure/telegram/upload-batcher';
|
||||
import logger from '../../../shared/logger/index';
|
||||
import { metricsCollector } from '../../../shared/metrics/index';
|
||||
import {
|
||||
@@ -14,8 +15,6 @@ import {
|
||||
getErrorMessage,
|
||||
getFileType,
|
||||
} from '../../../shared/utils/file';
|
||||
import { storeFileInTelegramChunks } from '../../../utils/chunked-storage';
|
||||
import { enqueuePreparedUpload, type PreparedUpload } from '../../../utils/uploadBatcher';
|
||||
|
||||
/**
|
||||
* Maximum allowed size (in bytes) for a base64 JSON upload.
|
||||
@@ -223,7 +222,7 @@ const handleMultipartUpload = async (req: Request): Promise<Response> => {
|
||||
|
||||
const prepared = await streamFileToTemp(file, config.maxRequestBodyBytes);
|
||||
|
||||
const existingFile = await findFileByHash(prepared.fileHash);
|
||||
const existingFile = await fileRepository.findByHash(prepared.fileHash);
|
||||
if (existingFile) {
|
||||
await cleanupTempFile(prepared.tempPath);
|
||||
return Response.json(buildUploadResponse(existingFile, config.baseUrl), { status: 200 });
|
||||
@@ -243,7 +242,7 @@ const handleMultipartUpload = async (req: Request): Promise<Response> => {
|
||||
}
|
||||
|
||||
if (prepared.sizeBytes > config.telegramChunkSizeBytes) {
|
||||
const uploadedFile = await storeFileInTelegramChunks({
|
||||
const uploadedFile = await chunkedStorage.storeFileInTelegramChunks({
|
||||
tempPath: prepared.tempPath,
|
||||
partFileNamePrefix: `direct-${prepared.fileHash?.slice(0, 16) || 'upload'}`,
|
||||
fileName: finalFileName,
|
||||
@@ -256,7 +255,7 @@ const handleMultipartUpload = async (req: Request): Promise<Response> => {
|
||||
return Response.json(buildUploadResponse(uploadedFile, config.baseUrl), { status: 200 });
|
||||
}
|
||||
|
||||
const uploaded = await enqueuePreparedUpload({
|
||||
const uploaded = await uploadBatcher.enqueuePreparedUpload({
|
||||
prepared,
|
||||
fileName: finalFileName,
|
||||
mimeType,
|
||||
@@ -317,7 +316,7 @@ const handleJSONUpload = async (req: Request): Promise<Response> => {
|
||||
const fileBytes = Buffer.from(base64Data, 'base64');
|
||||
const hash = computeHash(fileBytes);
|
||||
|
||||
const existingFile = await findFileByHash(hash);
|
||||
const existingFile = await fileRepository.findByHash(hash);
|
||||
if (existingFile) {
|
||||
return Response.json(buildUploadResponse(existingFile, config.baseUrl), { status: 200 });
|
||||
}
|
||||
@@ -334,7 +333,7 @@ const handleJSONUpload = async (req: Request): Promise<Response> => {
|
||||
const prepared = await writeBufferToTemp(fileBytes, hash);
|
||||
|
||||
if (prepared.sizeBytes > config.telegramChunkSizeBytes) {
|
||||
const uploadedFile = await storeFileInTelegramChunks({
|
||||
const uploadedFile = await chunkedStorage.storeFileInTelegramChunks({
|
||||
tempPath: prepared.tempPath,
|
||||
partFileNamePrefix: `direct-${prepared.fileHash?.slice(0, 16) || 'json'}`,
|
||||
fileName: finalFileName,
|
||||
@@ -347,7 +346,7 @@ const handleJSONUpload = async (req: Request): Promise<Response> => {
|
||||
return Response.json(buildUploadResponse(uploadedFile, config.baseUrl), { status: 200 });
|
||||
}
|
||||
|
||||
const uploaded = await enqueuePreparedUpload({
|
||||
const uploaded = await uploadBatcher.enqueuePreparedUpload({
|
||||
prepared,
|
||||
fileName: finalFileName,
|
||||
mimeType,
|
||||
|
||||
@@ -1,21 +1,11 @@
|
||||
import { createReadStream } from 'node:fs';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { config } from '../../../config/index';
|
||||
import { createBucket, deleteBucket, findBucketByName, listBuckets } from '../../../db/buckets';
|
||||
import {
|
||||
countBucketObjects,
|
||||
findFileByBucketAndKey,
|
||||
listObjectsByPrefix,
|
||||
softDeleteFile,
|
||||
} from '../../../db/files-ext';
|
||||
import { db, files as fileSchema } from '../../../db/index';
|
||||
import { bucketRepository, chunkedStorage, fileRepository } from '../../../infrastructure/di';
|
||||
import { db, files as fileSchema } from '../../../infrastructure/persistence/drizzle/index';
|
||||
import { botPool } from '../../../infrastructure/telegram/bot-pool';
|
||||
import logger from '../../../shared/logger/index';
|
||||
import { cleanupTempFile, ensureExtension, getErrorMessage } from '../../../shared/utils/file';
|
||||
import {
|
||||
createChunkedObjectResponse,
|
||||
storeFileInTelegramChunks,
|
||||
} from '../../../utils/chunked-storage';
|
||||
|
||||
/**
|
||||
* Route parameters extracted from the URL path.
|
||||
@@ -48,13 +38,13 @@ const jsonError = (error: string, status: number): Response => Response.json({ e
|
||||
* @returns A JSON response with the bucket list.
|
||||
*/
|
||||
export const handleListBucketsV1 = async (): Promise<Response> => {
|
||||
const buckets = await listBuckets();
|
||||
const buckets = await bucketRepository.list();
|
||||
const result = await Promise.all(
|
||||
buckets.map(async (b) => ({
|
||||
id: b.id,
|
||||
name: b.name,
|
||||
createdAt: b.createdAt.toISOString(),
|
||||
objectCount: await countBucketObjects(b.id),
|
||||
objectCount: await fileRepository.countByBucket(b.id),
|
||||
})),
|
||||
);
|
||||
return json({ buckets: result });
|
||||
@@ -73,9 +63,9 @@ export const handleCreateBucketV1 = async (req: Request): Promise<Response> => {
|
||||
if (!body.name || !/^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/.test(body.name)) {
|
||||
return jsonError('Invalid bucket name. Use lowercase, 3-63 chars, no underscore', 400);
|
||||
}
|
||||
const existing = await findBucketByName(body.name);
|
||||
const existing = await bucketRepository.findByName(body.name);
|
||||
if (existing) return jsonError('Bucket already exists', 409);
|
||||
const bucket = await createBucket(body.name);
|
||||
const bucket = await bucketRepository.create(body.name);
|
||||
return json({ id: bucket.id, name: bucket.name }, 201);
|
||||
};
|
||||
|
||||
@@ -92,11 +82,11 @@ export const handleDeleteBucketV1 = async (
|
||||
_req: Request,
|
||||
params: RouteParams,
|
||||
): Promise<Response> => {
|
||||
const bucket = await findBucketByName(params.bucket!);
|
||||
const bucket = await bucketRepository.findByName(params.bucket!);
|
||||
if (!bucket) return jsonError('Bucket not found', 404);
|
||||
const count = await countBucketObjects(bucket.id);
|
||||
const count = await fileRepository.countByBucket(bucket.id);
|
||||
if (count > 0) return jsonError('Bucket is not empty', 409);
|
||||
await deleteBucket(params.bucket!);
|
||||
await bucketRepository.delete(params.bucket!);
|
||||
return json({ success: true });
|
||||
};
|
||||
|
||||
@@ -110,7 +100,7 @@ export const handleDeleteBucketV1 = async (
|
||||
* @returns A JSON response with the object list.
|
||||
*/
|
||||
export const handleListObjectsV1 = async (req: Request, params: RouteParams): Promise<Response> => {
|
||||
const bucket = await findBucketByName(params.bucket!);
|
||||
const bucket = await bucketRepository.findByName(params.bucket!);
|
||||
if (!bucket) return jsonError('Bucket not found', 404);
|
||||
|
||||
const url = new URL(req.url);
|
||||
@@ -119,7 +109,7 @@ export const handleListObjectsV1 = async (req: Request, params: RouteParams): Pr
|
||||
const maxKeys = Number.parseInt(url.searchParams.get('max-keys') || '1000', 10);
|
||||
const continuationToken = url.searchParams.get('continuation-token') || null;
|
||||
|
||||
const { objects, prefixes } = await listObjectsByPrefix(
|
||||
const { objects, prefixes } = await fileRepository.listByPrefix(
|
||||
bucket.id,
|
||||
prefix,
|
||||
delimiter,
|
||||
@@ -162,7 +152,7 @@ export const handleUploadObjectV1 = async (
|
||||
req: Request,
|
||||
params: RouteParams,
|
||||
): Promise<Response> => {
|
||||
const bucket = await findBucketByName(params.bucket!);
|
||||
const bucket = await bucketRepository.findByName(params.bucket!);
|
||||
if (!bucket) return jsonError('Bucket not found', 404);
|
||||
|
||||
const formData = await req.formData();
|
||||
@@ -217,7 +207,7 @@ export const handleUploadObjectV1 = async (
|
||||
const partFileNamePrefix = `s3-${bucket.name}-${key.replace(/\//g, '_')}`;
|
||||
|
||||
if (sizeBytes > config.telegramChunkSizeBytes) {
|
||||
const uploadedFile = await storeFileInTelegramChunks({
|
||||
const uploadedFile = await chunkedStorage.storeFileInTelegramChunks({
|
||||
tempPath,
|
||||
partFileNamePrefix,
|
||||
fileName: finalFileName,
|
||||
@@ -287,9 +277,9 @@ export const handleDeleteObjectV1 = async (
|
||||
_req: Request,
|
||||
params: RouteParams,
|
||||
): Promise<Response> => {
|
||||
const bucket = await findBucketByName(params.bucket!);
|
||||
const bucket = await bucketRepository.findByName(params.bucket!);
|
||||
if (!bucket) return jsonError('Bucket not found', 404);
|
||||
await softDeleteFile(bucket.id, params.key!);
|
||||
await fileRepository.softDelete(bucket.id, params.key!);
|
||||
return json({ success: true });
|
||||
};
|
||||
|
||||
@@ -307,15 +297,15 @@ export const handleDownloadObjectV1 = async (
|
||||
_req: Request,
|
||||
params: RouteParams,
|
||||
): Promise<Response> => {
|
||||
const bucket = await findBucketByName(params.bucket!);
|
||||
const bucket = await bucketRepository.findByName(params.bucket!);
|
||||
if (!bucket) return jsonError('Bucket not found', 404);
|
||||
|
||||
const file = await findFileByBucketAndKey(bucket.id, params.key!);
|
||||
const file = await fileRepository.findByBucketAndKey(bucket.id, params.key!);
|
||||
if (!file) return jsonError('Object not found', 404);
|
||||
|
||||
if (file.storageBackend === 'chunked') {
|
||||
const range = { type: 'none' as const };
|
||||
return createChunkedObjectResponse({ file, range, reqId: '' });
|
||||
return chunkedStorage.createChunkedObjectResponse({ file, range, reqId: '' });
|
||||
}
|
||||
|
||||
const fileInfo = await botPool.getFileInfo(file.telegramFileId);
|
||||
@@ -348,12 +338,12 @@ export const handleCopyObjectV1 = async (req: Request, params: RouteParams): Pro
|
||||
}
|
||||
|
||||
const destBucketName = body.destBucket || params.bucket!;
|
||||
const sourceBucket = await findBucketByName(params.bucket!);
|
||||
const destBucket = await findBucketByName(destBucketName);
|
||||
const sourceBucket = await bucketRepository.findByName(params.bucket!);
|
||||
const destBucket = await bucketRepository.findByName(destBucketName);
|
||||
|
||||
if (!sourceBucket || !destBucket) return jsonError('Bucket not found', 404);
|
||||
|
||||
const sourceFile = await findFileByBucketAndKey(sourceBucket.id, body.sourceKey);
|
||||
const sourceFile = await fileRepository.findByBucketAndKey(sourceBucket.id, body.sourceKey);
|
||||
if (!sourceFile) return jsonError('Source object not found', 404);
|
||||
|
||||
if (sourceFile.storageBackend === 'chunked') {
|
||||
|
||||
Reference in New Issue
Block a user