fix: PutObject raw body, presigned URL auth guard, DeleteObjects tracking, home.ts path

This commit is contained in:
asepharyana
2026-07-06 21:38:17 +07:00
parent e175a27512
commit 0ad783b794
2 changed files with 22 additions and 28 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
export const handleHome = async (): Promise<Response> => { export const handleHome = async (): Promise<Response> => {
const html = await Bun.file('src/home.html').text(); const html = await Bun.file(import.meta.dir + '/../home.html').text();
return new Response(html, { return new Response(html, {
status: 200, status: 200,
headers: { headers: {
+21 -27
View File
@@ -10,7 +10,7 @@ import {
insertMultipartPart, listMultipartParts, insertMultipartPart, listMultipartParts,
} from '../db/multipart'; } from '../db/multipart';
import { import {
findFileByBucketAndKey, listObjectsByPrefix, softDeleteFile, softDeleteFilesBatch, countBucketObjects, findFileByBucketAndKey, listObjectsByPrefix, softDeleteFile, countBucketObjects,
} from '../db/files-ext'; } from '../db/files-ext';
import type { File } from '../db/schema'; import type { File } from '../db/schema';
import { config } from '../env'; import { config } from '../env';
@@ -49,8 +49,12 @@ export const handleS3Request = async (req: Request): Promise<Response> => {
const searchParams = url.searchParams; const searchParams = url.searchParams;
const reqId = REQUEST_ID(); const reqId = REQUEST_ID();
// Verify auth for regular requests; presigned URLs are verified per-operation // Presigned URLs: only supported for GET (verified in handleGetObject)
if (!searchParams.has('X-Amz-Signature')) { if (searchParams.has('X-Amz-Signature')) {
if (method !== 'GET') {
return s3ErrorResponse('AccessDenied', 'Presigned URLs only supported for GET', pathname, 403, reqId);
}
} else {
const authResult = await verifySignature(method, req.url, headers, null, config.s3AccessKey, config.s3SecretKey, REGION); const authResult = await verifySignature(method, req.url, headers, null, config.s3AccessKey, config.s3SecretKey, REGION);
if (!authResult.isValid) { if (!authResult.isValid) {
return s3ErrorResponse(authResult.errorCode || 'AccessDenied', 'Authentication required', pathname, 403, reqId); return s3ErrorResponse(authResult.errorCode || 'AccessDenied', 'Authentication required', pathname, 403, reqId);
@@ -250,41 +254,27 @@ const handlePutObject = async (bucket: string, key: string, searchParams: URLSea
return handleCopyObject(bucket, key, copySource, bucketRecord.id, reqId); return handleCopyObject(bucket, key, copySource, bucketRecord.id, reqId);
} }
const formData = await req.formData(); // Read raw body — S3 clients send raw binary, not multipart/form-data
const fileField = formData.get('file'); const body = await req.arrayBuffer();
const file = fileField instanceof File ? fileField : null; const fileBuffer = Buffer.from(body);
const contentType = headers['content-type'] || 'application/octet-stream';
if (!file) { const hash = computeHash(fileBuffer);
// Direct body upload
const body = await req.arrayBuffer();
const fileBuffer = Buffer.from(body);
const hash = computeHash(fileBuffer);
const existing = await findFileByBucketAndKey(bucketRecord.id, key);
if (existing) {
return new Response(null, { status: 200, headers: { 'etag': `"${hash}"`, 'x-amz-request-id': reqId } });
}
return await storeFileToTelegram(fileBuffer, hash, key, bucketRecord, reqId);
}
const buffer = Buffer.from(await file.arrayBuffer());
const hash = computeHash(buffer);
const existing = await findFileByBucketAndKey(bucketRecord.id, key); const existing = await findFileByBucketAndKey(bucketRecord.id, key);
if (existing) { if (existing) {
return new Response(null, { status: 200, headers: { 'etag': `"${hash}"`, 'x-amz-request-id': reqId } }); return new Response(null, { status: 200, headers: { 'etag': `"${hash}"`, 'x-amz-request-id': reqId } });
} }
return await storeFileToTelegram(buffer, hash, key, bucketRecord, reqId); return await storeFileToTelegram(fileBuffer, hash, key, bucketRecord, contentType, reqId);
}; };
const storeFileToTelegram = async (buffer: Buffer, hash: string, key: string, bucketRecord: { id: string; name: string }, reqId: string): Promise<Response> => { const storeFileToTelegram = async (buffer: Buffer, hash: string, key: string, bucketRecord: { id: string; name: string }, contentType: string, reqId: string): Promise<Response> => {
const tempPath = `/tmp/teleuploader-s3-${nanoid()}`; const tempPath = `/tmp/teleuploader-s3-${nanoid()}`;
await Bun.write(tempPath, buffer); await Bun.write(tempPath, buffer);
const signatureBuffer = buffer.subarray(0, 16); const signatureBuffer = buffer.subarray(0, 16);
const fileName = key.split('/').pop() || 'file'; const fileName = key.split('/').pop() || 'file';
const { fileName: finalFileName, mimeType } = ensureExtension(fileName, signatureBuffer, 'application/octet-stream'); const { fileName: finalFileName, mimeType } = ensureExtension(fileName, signatureBuffer, contentType);
const forwardResult = await forwardToStorage( const forwardResult = await forwardToStorage(
createReadStream(tempPath), createReadStream(tempPath),
@@ -378,8 +368,12 @@ const handleDeleteObjects = async (bucket: string, body: string, reqId: string):
if (!bucketRecord) return s3ErrorResponse('NoSuchBucket', 'The specified bucket does not exist.', `/${bucket}`, 404, reqId); if (!bucketRecord) return s3ErrorResponse('NoSuchBucket', 'The specified bucket does not exist.', `/${bucket}`, 404, reqId);
const { keys } = parseDeleteObjectsBody(body); const { keys } = parseDeleteObjectsBody(body);
const deleted = await softDeleteFilesBatch(bucketRecord.id, keys); const deletedKeys: string[] = [];
const xml = deleteResultXml(keys.slice(0, deleted), []); for (const key of keys) {
const ok = await softDeleteFile(bucketRecord.id, key);
if (ok) deletedKeys.push(key);
}
const xml = deleteResultXml(deletedKeys, []);
return new Response(xml, { return new Response(xml, {
status: 200, status: 200,
headers: { 'content-type': 'application/xml', 'x-amz-request-id': reqId }, headers: { 'content-type': 'application/xml', 'x-amz-request-id': reqId },