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:
+15
-9
@@ -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
@@ -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
@@ -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
@@ -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
File diff suppressed because one or more lines are too long
+3
-3
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -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
@@ -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 };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user