fix: satisfy deploy lint gate for S3 compatibility work

- Apply Biome organize-import/formatting fixes across changed S3 files
- Replace remaining string concatenations with template literals for lint
- Make home page inline handlers explicit via window.* and add button types
- Clean S3 auth lint issues with dot-property access and optional chaining
- Keep GetObject proxy and production/S3 SDK tests passing

Verification:
- bun run lint (0 errors, 1 CSS specificity warning)
- S3_SECRET_KEY=<env> bun test test/production-e2e.test.ts (29 pass)
- S3_SECRET_KEY=<env> bun test test/s3-sdk.test.ts (20 pass)
- bun test test/s3-auth.test.ts (5 pass)
This commit is contained in:
asepharyana
2026-07-07 03:07:51 +07:00
parent 06f93e30f6
commit 9a48fbf227
14 changed files with 161 additions and 124 deletions
+15 -9
View File
@@ -52,15 +52,21 @@ export const listBuckets = async (): Promise<Bucket[]> => {
export const deleteBucket = async (name: string): Promise<boolean> => {
// Cascade-delete rows that hold FK references to the bucket
await db.execute(
sql`DELETE FROM multipart_parts WHERE upload_id IN (SELECT upload_id FROM multipart_uploads WHERE bucket_id IN (SELECT id FROM buckets WHERE name = ${name}))`,
).catch(() => {});
await db.execute(
sql`DELETE FROM multipart_uploads WHERE bucket_id IN (SELECT id FROM buckets WHERE name = ${name})`,
).catch(() => {});
await db.execute(
sql`DELETE FROM files WHERE bucket_id IN (SELECT id FROM buckets WHERE name = ${name})`,
).catch(() => {});
await db
.execute(
sql`DELETE FROM multipart_parts WHERE upload_id IN (SELECT upload_id FROM multipart_uploads WHERE bucket_id IN (SELECT id FROM buckets WHERE name = ${name}))`,
)
.catch(() => {});
await db
.execute(
sql`DELETE FROM multipart_uploads WHERE bucket_id IN (SELECT id FROM buckets WHERE name = ${name})`,
)
.catch(() => {});
await db
.execute(
sql`DELETE FROM files WHERE bucket_id IN (SELECT id FROM buckets WHERE name = ${name})`,
)
.catch(() => {});
const result = (await db.execute(
sql`DELETE FROM buckets WHERE name = ${name}`,
)) as unknown as QueryResult;
+2 -2
View File
@@ -1,4 +1,4 @@
import { eq, and, sql } from 'drizzle-orm';
import { and, eq, sql } from 'drizzle-orm';
import { db, files as fileSchema } from './index';
import type { File } from './schema';
@@ -62,7 +62,7 @@ export const listObjectsByPrefix = async (
maxKeys: number,
startAfter: string | null,
): Promise<{ objects: S3FileRecord[]; prefixes: string[] }> => {
let query = sql`SELECT * FROM files WHERE bucket_id = ${bucketId}::uuid AND is_deleted = false AND s3_key LIKE ${prefix + '%'}`;
let query = sql`SELECT * FROM files WHERE bucket_id = ${bucketId}::uuid AND is_deleted = false AND s3_key LIKE ${`${prefix}%`}`;
if (startAfter) {
query = sql`${query} AND s3_key > ${startAfter}`;
+5 -5
View File
@@ -12,10 +12,10 @@ export const runMigration = async (): Promise<void> => {
// In source via bun --hot: import.meta.dir = .../src/db/
const dir = import.meta.dir || '';
const candidates = [
dir + '/../../schema.sql', // from dist/
dir + '/../schema.sql', // from src/ (bun --hot src/index.ts)
dir + '/../schema.sql', // from src/db/ (bun --hot src/db/migrate.ts)
dir + '/schema.sql', // from src/ (bun run db:migrate)
`${dir}/../../schema.sql`, // from dist/
`${dir}/../schema.sql`, // from src/ (bun --hot src/index.ts)
`${dir}/../schema.sql`, // from src/db/ (bun --hot src/db/migrate.ts)
`${dir}/schema.sql`, // from src/ (bun run db:migrate)
];
let schemaSql: string | null = null;
@@ -29,7 +29,7 @@ export const runMigration = async (): Promise<void> => {
}
if (!schemaSql) {
logger.error('Migration failed: schema.sql not found (tried ' + candidates.join(', ') + ')');
logger.error(`Migration failed: schema.sql not found (tried ${candidates.join(', ')})`);
process.exitCode = 1;
return;
}
+1 -1
View File
@@ -1,6 +1,6 @@
import { sql } from 'drizzle-orm';
import { db } from './index';
import { nanoid } from 'nanoid';
import { db } from './index';
export interface MultipartUpload {
uploadId: string;
+11 -10
View File
File diff suppressed because one or more lines are too long
+3 -3
View File
@@ -3,16 +3,16 @@ import { startBot } from './bot';
import { config } from './env';
import { handleFileInfo, handleFileRedirect } from './routes/files';
import { handleHealth } from './routes/health';
import { handleHome } from './routes/home';
import { handleS3Request } from './routes/s3';
import { handleSwaggerHtml, handleSwaggerJson } from './routes/swagger';
import { handleUpload } from './routes/upload';
import { handleHome } from './routes/home';
import { handleWebApiV1 } from './routes/web-api';
import { handleS3Request } from './routes/s3';
import { isS3Request } from './utils/s3/auth';
import { fileInfoCache } from './utils/cache';
import logger from './utils/logger';
import { metricsCollector } from './utils/metrics';
import { cleanupRateLimitCache, withRateLimit } from './utils/rateLimit';
import { isS3Request } from './utils/s3/auth';
// ─── Auto-run migration at startup ──────────────────────────────────────────
try {
+1 -1
View File
@@ -1,5 +1,5 @@
export const handleHome = async (): Promise<Response> => {
const html = await Bun.file(import.meta.dir + '/../home.html').text();
const html = await Bun.file(`${import.meta.dir}/../home.html`).text();
return new Response(html, {
status: 200,
headers: {
+28 -28
View File
@@ -1,39 +1,39 @@
import { verifySignature, verifyPresignedUrl } from '../utils/s3/auth';
import {
listBucketsXml,
s3ErrorResponse,
listBucketResultXml,
listBucketV2ResultXml,
initiateMultipartUploadXml,
listPartsXml,
completeMultipartUploadXml,
deleteResultXml,
copyObjectResultXml,
parseDeleteObjectsBody,
parseCompleteMultipartBody,
} from '../utils/s3/xml';
import { createBucket, findBucketByName, listBuckets, deleteBucket } from '../db/buckets';
import {
createMultipartUpload,
findMultipartUpload,
completeMultipartUpload,
abortMultipartUpload,
insertMultipartPart,
listMultipartParts,
} from '../db/multipart';
import { createReadStream } from 'node:fs';
import { nanoid } from 'nanoid';
import { createBucket, deleteBucket, findBucketByName, listBuckets } from '../db/buckets';
import {
countBucketObjects,
findFileByBucketAndKey,
listObjectsByPrefix,
softDeleteFile,
countBucketObjects,
} from '../db/files-ext';
import {
abortMultipartUpload,
completeMultipartUpload,
createMultipartUpload,
findMultipartUpload,
insertMultipartPart,
listMultipartParts,
} from '../db/multipart';
import type { File } from '../db/schema';
import { config } from '../env';
import { forwardToStorage, getFileInfo } from '../utils/telegram';
import { computeHash, ensureExtension, getErrorMessage, cleanupTempFile } from '../utils/file';
import { nanoid } from 'nanoid';
import { createReadStream } from 'node:fs';
import { cleanupTempFile, computeHash, ensureExtension, getErrorMessage } from '../utils/file';
import logger from '../utils/logger';
import { verifyPresignedUrl, verifySignature } from '../utils/s3/auth';
import {
completeMultipartUploadXml,
copyObjectResultXml,
deleteResultXml,
initiateMultipartUploadXml,
listBucketResultXml,
listBucketsXml,
listBucketV2ResultXml,
listPartsXml,
parseCompleteMultipartBody,
parseDeleteObjectsBody,
s3ErrorResponse,
} from '../utils/s3/xml';
import { forwardToStorage, getFileInfo } from '../utils/telegram';
const REGION = config.s3DefaultRegion || 'us-east-1';
const REQUEST_ID = () => nanoid(16);
+6 -6
View File
@@ -1,16 +1,16 @@
import { createBucket, findBucketByName, listBuckets, deleteBucket } from '../db/buckets';
import { createReadStream } from 'node:fs';
import { nanoid } from 'nanoid';
import { createBucket, deleteBucket, findBucketByName, listBuckets } from '../db/buckets';
import {
countBucketObjects,
findFileByBucketAndKey,
listObjectsByPrefix,
softDeleteFile,
countBucketObjects,
} from '../db/files-ext';
import { createReadStream } from 'node:fs';
import { config } from '../env';
import { forwardToStorage, getFileInfo } from '../utils/telegram';
import { computeHash, ensureExtension, getErrorMessage, cleanupTempFile } from '../utils/file';
import { nanoid } from 'nanoid';
import { cleanupTempFile, computeHash, ensureExtension, getErrorMessage } from '../utils/file';
import logger from '../utils/logger';
import { forwardToStorage, getFileInfo } from '../utils/telegram';
type RouteParams = { bucket?: string; key?: string };
+3 -3
View File
@@ -131,8 +131,8 @@ export const verifySignature = async (
s3SecretKey: string,
region: string,
): Promise<SigV4Result> => {
const authHeader = headers['authorization'];
if (!authHeader || !authHeader.startsWith('AWS4-HMAC-SHA256')) {
const authHeader = headers.authorization;
if (!authHeader?.startsWith('AWS4-HMAC-SHA256')) {
return { isValid: false, credential: null, errorCode: 'AccessDenied' };
}
@@ -276,6 +276,6 @@ export const verifyPresignedUrl = async (
};
export const isS3Request = (headers: Record<string, string>): boolean => {
const auth = headers['authorization'] || '';
const auth = headers.authorization || '';
return auth.startsWith('AWS4-HMAC-SHA256');
};
+5 -10
View File
@@ -12,7 +12,7 @@ const isoDate = (d: Date): string => d.toISOString().replace(/\.\d{3}Z$/, 'Z');
export const listBucketsXml = (
buckets: { name: string; createdAt: Date }[],
requestId: string,
_requestId: string,
): string => `<?xml version="1.0" encoding="UTF-8"?>
<ListAllMyBucketsResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<Buckets>
@@ -39,7 +39,7 @@ export const listBucketResultXml = (
prefix: string,
delimiter: string | null,
nextMarker: string | null,
requestId: string,
_requestId: string,
): string => `<?xml version="1.0" encoding="UTF-8"?>
<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<Name>${escapeXml(bucketName)}</Name>
@@ -80,7 +80,7 @@ export const listBucketV2ResultXml = (
continuationToken: string | null,
nextContinuationToken: string | null,
keyCount: number,
requestId: string,
_requestId: string,
): string => `<?xml version="1.0" encoding="UTF-8"?>
<ListBucketResultV2 xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<Name>${escapeXml(bucketName)}</Name>
@@ -131,7 +131,7 @@ export const listPartsXml = (
parts: { partNumber: number; etag: string; sizeBytes: number; createdAt: Date }[],
maxParts: number,
isTruncated: boolean,
requestId: string,
_requestId: string,
): string => `<?xml version="1.0" encoding="UTF-8"?>
<ListPartsResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<Bucket>${escapeXml(bucketName)}</Bucket>
@@ -233,12 +233,7 @@ export const s3ErrorResponse = (
// ─────── DeleteObjects XML parser ───────
export const parseDeleteObjectsBody = (body: string): { keys: string[]; quiet: boolean } => {
const keys: string[] = [];
const keyRegex = /<Key>([^<]+)<\/Key>/g;
let match;
while ((match = keyRegex.exec(body)) !== null) {
keys.push(match[1]);
}
const keys = Array.from(body.matchAll(/<Key>([^<]+)<\/Key>/g), (match) => match[1]);
const quiet = body.includes('<Quiet>true</Quiet>') || body.includes('<Quiet>true ');
return { keys, quiet };
};