refactor: dedup dead code, shared utils, conditional headers helper
- Extract shared utils: asSafeChunkSize (validation.ts), S3 detection (s3-detection.ts) - Remove dead _handleMaybeS3Root from index.ts and routes/index.ts - Remove dead _asArray from file-controller.ts - Consolidate maybeCompressChunk into shared compress.ts - Extract checkConditionalHeaders helper, remove ~120 lines dupe in s3-controller - Remove 500+ lines dead code from s3-object.ts (unused use cases + helpers) - Delegate upload-file.ts chunked path to ChunkedStorage, remove dupe - Fix broken dynamic import in file-controller.ts → proper DI - Fix test/files.test.ts import path and mocks [skip ci]
This commit is contained in:
@@ -2,7 +2,7 @@ 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 { chunkedStorage, fileRepository } from '../../../infrastructure/di';
|
||||
import { botPool } from '../../../infrastructure/telegram/bot-pool';
|
||||
import logger from '../../../shared/logger/index';
|
||||
import { cleanupTempFile, formatCreatedAt, getErrorMessage } from '../../../shared/utils/file';
|
||||
@@ -19,14 +19,6 @@ type RequestWithParams = Request & {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps a string into a `string | string[]` for cookie append operations.
|
||||
*
|
||||
* @param value - The string value to wrap.
|
||||
* @returns The value as a single-element tuple.
|
||||
*/
|
||||
const _asArray = (value: string): string[] => [value];
|
||||
|
||||
/**
|
||||
* Resolves Telegram file metadata for a given file ID, using the in-memory
|
||||
* cache to avoid repeated API calls to Telegram.
|
||||
@@ -103,8 +95,7 @@ export const handleFileRedirect = async (req: RequestWithParams): Promise<Respon
|
||||
return fail(400, 'Missing file id');
|
||||
}
|
||||
|
||||
const { findFileByPublicId } = await import('../../../db/files');
|
||||
const file = await findFileByPublicId(publicId);
|
||||
const file = await fileRepository.findByPublicId(publicId);
|
||||
if (!file) {
|
||||
logger.warn('File not found', { publicId });
|
||||
return fail(404, 'File not found');
|
||||
@@ -193,8 +184,7 @@ export const handleFileInfo = async (req: RequestWithParams): Promise<Response>
|
||||
return fail(400, 'Missing file id');
|
||||
}
|
||||
|
||||
const { findFileByPublicId } = await import('../../../db/files');
|
||||
const file = await findFileByPublicId(publicId);
|
||||
const file = await fileRepository.findByPublicId(publicId);
|
||||
if (!file) {
|
||||
logger.warn('File not found', { publicId });
|
||||
return fail(404, 'File not found');
|
||||
|
||||
@@ -424,6 +424,95 @@ const handleGetBucketVersioning = async (bucketName: string, reqId: string): Pro
|
||||
});
|
||||
};
|
||||
|
||||
// ─────── Conditional Headers Helper ──────
|
||||
|
||||
/**
|
||||
* S3-compatible response for 304 Not Modified.
|
||||
*/
|
||||
const notModifiedResponse = (
|
||||
reqId: string,
|
||||
etag: string,
|
||||
mimeType: string,
|
||||
sizeBytes: number,
|
||||
lastModified: Date,
|
||||
): Response =>
|
||||
new Response(null, {
|
||||
status: 304,
|
||||
headers: s3Headers(reqId, {
|
||||
etag,
|
||||
'content-type': mimeType,
|
||||
'content-length': String(sizeBytes),
|
||||
'last-modified': lastModified.toUTCString(),
|
||||
'x-amz-version-id': 'null',
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* S3-compatible response for 412 Precondition Failed.
|
||||
*/
|
||||
const preconditionFailedResponse = (path: string, reqId: string): Response =>
|
||||
s3ErrorResponse(
|
||||
'PreconditionFailed',
|
||||
'At least one of the pre-conditions you specified did not hold.',
|
||||
path,
|
||||
412,
|
||||
reqId,
|
||||
);
|
||||
|
||||
/**
|
||||
* Checks conditional headers (If-Match, If-None-Match, If-Modified-Since,
|
||||
* If-Unmodified-Since) and returns a prepared Response if the condition
|
||||
* is not satisfied, or `null` to let the request proceed.
|
||||
*
|
||||
* @returns A 304 / 412 Response when a condition fails, or `null` to continue.
|
||||
*/
|
||||
const checkConditionalHeaders = (
|
||||
headers: Record<string, string>,
|
||||
file: {
|
||||
mimeType: string;
|
||||
sizeBytes: number;
|
||||
fileHash: string | null;
|
||||
createdAt: Date | string | number;
|
||||
},
|
||||
path: string,
|
||||
reqId: string,
|
||||
): Response | null => {
|
||||
const etag = `"${file.fileHash || nanoid(16)}"`;
|
||||
const lastModified = file.createdAt instanceof Date ? file.createdAt : new Date(file.createdAt);
|
||||
|
||||
// If-Match
|
||||
const ifMatch = headers['if-match'];
|
||||
if (ifMatch && ifMatch !== '*' && ifMatch !== etag) {
|
||||
return preconditionFailedResponse(path, reqId);
|
||||
}
|
||||
|
||||
// If-None-Match
|
||||
const ifNoneMatch = headers['if-none-match'];
|
||||
if (ifNoneMatch && ifNoneMatch === etag) {
|
||||
return notModifiedResponse(reqId, etag, file.mimeType, file.sizeBytes, lastModified);
|
||||
}
|
||||
|
||||
// If-Modified-Since
|
||||
const ifModifiedSince = headers['if-modified-since'];
|
||||
if (ifModifiedSince) {
|
||||
const since = new Date(ifModifiedSince);
|
||||
if (!Number.isNaN(since.getTime()) && lastModified.getTime() <= since.getTime()) {
|
||||
return notModifiedResponse(reqId, etag, file.mimeType, file.sizeBytes, lastModified);
|
||||
}
|
||||
}
|
||||
|
||||
// If-Unmodified-Since
|
||||
const ifUnmodifiedSince = headers['if-unmodified-since'];
|
||||
if (ifUnmodifiedSince) {
|
||||
const since = new Date(ifUnmodifiedSince);
|
||||
if (!Number.isNaN(since.getTime()) && lastModified.getTime() > since.getTime()) {
|
||||
return preconditionFailedResponse(path, reqId);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// ─────── Object Operations ───────
|
||||
|
||||
/**
|
||||
@@ -468,62 +557,10 @@ const handleGetObject = async (
|
||||
reqId,
|
||||
);
|
||||
|
||||
// H3: Conditional headers — If-Match / If-None-Match
|
||||
const etag = `"${file.fileHash || nanoid(16)}"`;
|
||||
const lastModified = file.createdAt instanceof Date ? file.createdAt : new Date(file.createdAt);
|
||||
const ifMatch = headers['if-match'];
|
||||
if (ifMatch && ifMatch !== '*' && ifMatch !== etag) {
|
||||
return s3ErrorResponse(
|
||||
'PreconditionFailed',
|
||||
'At least one of the pre-conditions you specified did not hold.',
|
||||
`/${bucket}/${key}`,
|
||||
412,
|
||||
reqId,
|
||||
);
|
||||
}
|
||||
const ifNoneMatch = headers['if-none-match'];
|
||||
if (ifNoneMatch && ifNoneMatch === etag) {
|
||||
return new Response(null, {
|
||||
status: 304,
|
||||
headers: s3Headers(reqId, {
|
||||
etag,
|
||||
'content-type': file.mimeType,
|
||||
'content-length': String(file.sizeBytes),
|
||||
'last-modified': lastModified.toUTCString(),
|
||||
'x-amz-version-id': 'null',
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
// H3: Conditional headers — If-Modified-Since / If-Unmodified-Since
|
||||
const ifModifiedSince = headers['if-modified-since'];
|
||||
if (ifModifiedSince) {
|
||||
const since = new Date(ifModifiedSince);
|
||||
if (!Number.isNaN(since.getTime()) && lastModified.getTime() <= since.getTime()) {
|
||||
return new Response(null, {
|
||||
status: 304,
|
||||
headers: s3Headers(reqId, {
|
||||
etag,
|
||||
'content-type': file.mimeType,
|
||||
'content-length': String(file.sizeBytes),
|
||||
'last-modified': lastModified.toUTCString(),
|
||||
'x-amz-version-id': 'null',
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
const ifUnmodifiedSince = headers['if-unmodified-since'];
|
||||
if (ifUnmodifiedSince) {
|
||||
const since = new Date(ifUnmodifiedSince);
|
||||
if (!Number.isNaN(since.getTime()) && lastModified.getTime() > since.getTime()) {
|
||||
return s3ErrorResponse(
|
||||
'PreconditionFailed',
|
||||
'At least one of the pre-conditions you specified did not hold.',
|
||||
`/${bucket}/${key}`,
|
||||
412,
|
||||
reqId,
|
||||
);
|
||||
}
|
||||
// H3: Conditional headers — If-Match / If-None-Match / If-Modified-Since / If-Unmodified-Since
|
||||
const conditionResult = checkConditionalHeaders(headers, file, `/${bucket}/${key}`, reqId);
|
||||
if (conditionResult) {
|
||||
return conditionResult;
|
||||
}
|
||||
|
||||
// Chunked storage object
|
||||
@@ -736,62 +773,10 @@ const handleHeadObject = async (
|
||||
reqId,
|
||||
);
|
||||
|
||||
// H3: Conditional headers for HEAD — If-Match / If-None-Match
|
||||
const etag = `"${file.fileHash || nanoid(16)}"`;
|
||||
const lastModified = file.createdAt instanceof Date ? file.createdAt : new Date(file.createdAt);
|
||||
const ifMatch = headers['if-match'];
|
||||
if (ifMatch && ifMatch !== '*' && ifMatch !== etag) {
|
||||
return s3ErrorResponse(
|
||||
'PreconditionFailed',
|
||||
'At least one of the pre-conditions you specified did not hold.',
|
||||
`/${bucket}/${key}`,
|
||||
412,
|
||||
reqId,
|
||||
);
|
||||
}
|
||||
const ifNoneMatch = headers['if-none-match'];
|
||||
if (ifNoneMatch && ifNoneMatch === etag) {
|
||||
return new Response(null, {
|
||||
status: 304,
|
||||
headers: s3Headers(reqId, {
|
||||
etag,
|
||||
'content-type': file.mimeType,
|
||||
'content-length': String(file.sizeBytes),
|
||||
'last-modified': lastModified.toUTCString(),
|
||||
'x-amz-version-id': 'null',
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
// H3: Conditional headers for HEAD — If-Modified-Since / If-Unmodified-Since
|
||||
const ifModifiedSince = headers['if-modified-since'];
|
||||
if (ifModifiedSince) {
|
||||
const since = new Date(ifModifiedSince);
|
||||
if (!Number.isNaN(since.getTime()) && lastModified.getTime() <= since.getTime()) {
|
||||
return new Response(null, {
|
||||
status: 304,
|
||||
headers: s3Headers(reqId, {
|
||||
etag,
|
||||
'content-type': file.mimeType,
|
||||
'content-length': String(file.sizeBytes),
|
||||
'last-modified': lastModified.toUTCString(),
|
||||
'x-amz-version-id': 'null',
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
const ifUnmodifiedSince = headers['if-unmodified-since'];
|
||||
if (ifUnmodifiedSince) {
|
||||
const since = new Date(ifUnmodifiedSince);
|
||||
if (!Number.isNaN(since.getTime()) && lastModified.getTime() > since.getTime()) {
|
||||
return s3ErrorResponse(
|
||||
'PreconditionFailed',
|
||||
'At least one of the pre-conditions you specified did not hold.',
|
||||
`/${bucket}/${key}`,
|
||||
412,
|
||||
reqId,
|
||||
);
|
||||
}
|
||||
// H3: Conditional headers for HEAD — If-Match / If-None-Match / If-Modified-Since / If-Unmodified-Since
|
||||
const headConditionResult = checkConditionalHeaders(headers, file, `/${bucket}/${key}`, reqId);
|
||||
if (headConditionResult) {
|
||||
return headConditionResult;
|
||||
}
|
||||
|
||||
return s3Response(null, 200, reqId, {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { config } from '../../../env';
|
||||
import { handleSwaggerHtml, handleSwaggerJson } from '../../../routes/swagger';
|
||||
import { isS3Request } from '../../s3/auth';
|
||||
import { extractS3BucketFromHost } from '../../s3/virtual-host';
|
||||
import { getS3RouteBucket, shouldHandleS3 } from '../../../shared/utils/s3-detection';
|
||||
import { handleLogin, handleLogout, handleMe } from '../controllers/auth-controller';
|
||||
import { handleFileInfo, handleFileRedirect } from '../controllers/file-controller';
|
||||
import { handleHealth } from '../controllers/health-controller';
|
||||
@@ -12,52 +10,6 @@ import { handleWebApiV1 } from '../controllers/web-api-controller';
|
||||
import { requireAuth } from '../middleware/auth';
|
||||
import { withRateLimit } from '../middleware/rate-limit';
|
||||
|
||||
/**
|
||||
* Extracts the S3 bucket name from the request host
|
||||
* if it matches a virtual-hosted-style domain.
|
||||
*
|
||||
* @param req - The incoming HTTP request.
|
||||
* @returns The bucket name if found, or null.
|
||||
*/
|
||||
const getS3RouteBucket = (req: Request): string | null => {
|
||||
const host = req.headers.get('host') || '';
|
||||
return extractS3BucketFromHost(host, config.s3VhostDomains);
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines whether the incoming request appears to be an S3 API request
|
||||
* based on host headers, authorization headers, or query parameters.
|
||||
*
|
||||
* @param req - The incoming HTTP request.
|
||||
* @param headers - A record of parsed request headers.
|
||||
* @returns True if the request should be handled by the S3 handler.
|
||||
*/
|
||||
const shouldHandleS3 = (req: Request, headers: Record<string, string>): boolean => {
|
||||
const url = new URL(req.url);
|
||||
return Boolean(
|
||||
getS3RouteBucket(req) || isS3Request(headers) || url.searchParams.has('X-Amz-Signature'),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles non-GET requests to the root path by dispatching to the S3 handler
|
||||
* if the request matches S3 patterns (virtual-hosted bucket, S3 auth headers,
|
||||
* or presigned URL signature), or returning a 405 Method Not Allowed otherwise.
|
||||
*
|
||||
* @param req - The incoming HTTP request.
|
||||
* @returns A Response from the S3 handler or a 405 response.
|
||||
*/
|
||||
const _handleMaybeS3Root = (req: Request): Response | Promise<Response> => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
return handleS3Request(req, getS3RouteBucket(req));
|
||||
}
|
||||
const headers = Object.fromEntries(req.headers);
|
||||
if (shouldHandleS3(req, headers)) {
|
||||
return handleS3Request(req, getS3RouteBucket(req));
|
||||
}
|
||||
return new Response('Not Allowed', { status: 405 });
|
||||
};
|
||||
|
||||
/**
|
||||
* Dispatches an S3 request directly, bypassing rate limiting.
|
||||
*
|
||||
@@ -106,20 +58,12 @@ export const routes = {
|
||||
},
|
||||
'/': {
|
||||
GET: (req: Request): Promise<Response> => {
|
||||
const headers = Object.fromEntries(req.headers);
|
||||
if (shouldHandleS3(req, headers)) {
|
||||
return handleS3Direct(req);
|
||||
}
|
||||
if (shouldHandleS3(req)) return handleS3Direct(req);
|
||||
return handleHome();
|
||||
},
|
||||
PUT: (req: Request): Promise<Response> => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
return handleS3Request(req, getS3RouteBucket(req));
|
||||
}
|
||||
const headers = Object.fromEntries(req.headers);
|
||||
if (shouldHandleS3(req, headers)) {
|
||||
return handleS3Direct(req);
|
||||
}
|
||||
if (shouldHandleS3(req, headers)) return handleS3Direct(req);
|
||||
return Promise.resolve(new Response('Not Allowed', { status: 405 }));
|
||||
},
|
||||
HEAD: handleS3Direct,
|
||||
|
||||
Reference in New Issue
Block a user