feat: implement chunked storage for Telegram file uploads and retrieval

This commit is contained in:
asepharyana
2026-07-07 08:08:22 +07:00
parent fbfff3d9ec
commit 144ebe6dd3
18 changed files with 750 additions and 11 deletions
+72 -2
View File
@@ -18,6 +18,7 @@ import {
} from '../db/multipart';
import type { File } from '../db/schema';
import { config } from '../env';
import { createChunkedObjectResponse, storeFileInTelegramChunks } from '../utils/chunked-storage';
import { cleanupTempFile, computeHash, ensureExtension, getErrorMessage } from '../utils/file';
import logger from '../utils/logger';
import { verifyPresignedUrl, verifySignature } from '../utils/s3/auth';
@@ -319,6 +320,35 @@ const handleGetObject = async (
reqId,
);
if (file.storageBackend === 'chunked') {
const totalSize = Number(file.sizeBytes);
const range = parseRangeHeader(headers.range || null, totalSize);
if (range.type === 'invalid') {
return s3ErrorResponse(
'InvalidRange',
'The requested range is not satisfiable.',
`/${bucket}/${key}`,
416,
reqId,
{
'content-range': unsatisfiedContentRange(totalSize),
},
);
}
try {
return await createChunkedObjectResponse({ file, range, reqId });
} catch (error) {
logger.warn('Chunked object content fetch failed', { key, error: getErrorMessage(error) });
return s3ErrorResponse(
'InternalError',
'Failed to fetch object content from storage',
`/${bucket}/${key}`,
502,
reqId,
);
}
}
if (file.multipartUploadId) {
return handleGetMultipartObject(file, bucket, key, headers, reqId);
}
@@ -545,9 +575,28 @@ const storeFileToTelegram = async (
contentType,
);
const bucketId = bucketRecord.id;
const partFileNamePrefix = `s3-${bucketRecord.name}-${key.replace(/\//g, '_')}`;
if (buffer.byteLength > config.telegramChunkSizeBytes) {
const file = await storeFileInTelegramChunks({
tempPath,
partFileNamePrefix,
fileName: finalFileName,
mimeType,
sizeBytes: buffer.byteLength,
fileType: 'document',
uploaderId: 0,
bucketId,
s3Key: key,
});
await cleanupTempFile(tempPath);
return s3Response(null, 200, reqId, { etag: `"${file.fileHash}"` });
}
const forwardResult = await forwardToStorage(
createReadStream(tempPath),
`s3-${bucketRecord.name}-${key.replace(/\//g, '_')}`,
partFileNamePrefix,
'document',
);
@@ -566,7 +615,7 @@ const storeFileToTelegram = async (
fileType: 'document',
uploaderId: 0,
fileHash: hash,
bucketId: bucketRecord.id,
bucketId,
s3Key: key,
storageBackend: 'telegram',
isDeleted: false,
@@ -613,6 +662,17 @@ const handleCopyObject = async (
reqId,
);
if (sourceFile.storageBackend === 'chunked') {
// Copying chunked objects is not yet supported.
return s3ErrorResponse(
'NotImplemented',
'Copying chunked objects is not yet implemented.',
copySource,
501,
reqId,
);
}
// Conditional copy: if-match / if-none-match checks
const ifMatch = headers['x-amz-copy-source-if-match'];
const ifNoneMatch = headers['x-amz-copy-source-if-none-match'];
@@ -883,6 +943,16 @@ const handleUploadPart = async (
const body = await req.arrayBuffer();
const buffer = Buffer.from(body);
if (buffer.byteLength > config.telegramChunkSizeBytes) {
return s3ErrorResponse(
'EntityTooLarge',
`Your proposed upload part size (${buffer.byteLength} bytes) exceeds the maximum allowed part size (${config.telegramChunkSizeBytes} bytes) for this storage backend. Use smaller part sizes.`,
`/${bucket}/${key}`,
400,
reqId,
);
}
const tempPath = `/tmp/teleuploader-mp-${nanoid()}`;
await Bun.write(tempPath, buffer);