From 7db8fc21e5049d275e151fe5d411f47ffe85afee Mon Sep 17 00:00:00 2001 From: nexryai <61890205+nexryai@users.noreply.github.com> Date: Thu, 8 Jan 2026 10:26:36 +0000 Subject: [PATCH] Fix some errors --- src/index.ts | 102 +++++++++++++------- test/s3.test.ts | 252 +++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 314 insertions(+), 40 deletions(-) diff --git a/src/index.ts b/src/index.ts index 2c31a2f..d40e1fc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,6 @@ export default { async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { - const start = performance.now(); try { const isValid = await verifySignature(request, env); if (!isValid) { @@ -49,39 +48,63 @@ export default { }); } - const fileStream = await streamDownloadFromDrive(accessToken, bucket, objectKey, env); + try { + const fileStream = await streamDownloadFromDrive(accessToken, bucket, objectKey, env); - return new Response(fileStream.body, { - status: 200, - headers: { - "Content-Type": fileStream.contentType, - "Content-Length": fileStream.size.toString(), - ETag: `"${fileStream.id}"`, - }, - }); + return new Response(fileStream.body, { + status: 200, + headers: { + "Content-Type": fileStream.contentType, + "Content-Length": fileStream.size.toString(), + ETag: `"${fileStream.id}"`, + }, + }); + } catch (e) { + const error = e as Error; + if (error.message === "File not found") { + return new Response("NoSuchKey", { status: 404 }); + } + throw e; + } } else if (method === "DELETE") { // ファイル削除 if (!objectKey) { return new Response("Object key required", { status: 400 }); } - await deleteFromDrive(accessToken, bucket, objectKey, env); - return new Response(null, { status: 204 }); + try { + await deleteFromDrive(accessToken, bucket, objectKey, env); + return new Response(null, { status: 204 }); + } catch (e) { + const error = e as Error; + if (error.message === "File not found") { + return new Response(null, { status: 404 }); + } + throw e; + } } else if (method === "HEAD") { // メタデータ取得 if (!objectKey) { return new Response(null, { status: 400 }); } - const metadata = await getFileMetadata(accessToken, bucket, objectKey, env); - return new Response(null, { - status: 200, - headers: { - "Content-Type": metadata.mimeType, - "Content-Length": metadata.size.toString(), - ETag: `"${metadata.id}"`, - }, - }); + try { + const metadata = await getFileMetadata(accessToken, bucket, objectKey, env); + return new Response(null, { + status: 200, + headers: { + "Content-Type": metadata.mimeType, + "Content-Length": metadata.size.toString(), + ETag: `"${metadata.id}"`, + }, + }); + } catch (e) { + const error = e as Error; + if (error.message === "File not found") { + return new Response(null, { status: 404 }); + } + throw e; + } } return new Response("Method not allowed", { status: 405 }); @@ -89,11 +112,6 @@ export default { const error = e as Error; console.error("Error:", error); return new Response(error.message, { status: 500 }); - } finally { - const end = performance.now(); - const cpuUsed = end - start; - - console.log(`CPU time used: ${cpuUsed}ms`); } }, } satisfies ExportedHandler; @@ -106,7 +124,19 @@ interface Env { GOOGLE_CLIENT_SECRET: string; GOOGLE_REFRESH_TOKEN: string; AUTH_KV: KVNamespace; - FOLDER_CACHE: KVNamespace; // バケット名→フォルダIDのキャッシュ + FOLDER_CACHE: KVNamespace; +} + +interface GoogleDriveFile { + id: string; + name: string; + mimeType: string; + size: string; + modifiedTime?: string; +} + +interface GoogleDriveSearchResponse { + files?: GoogleDriveFile[]; } // ======================================== @@ -132,7 +162,7 @@ async function getAccessToken(env: Env): Promise { }), }); - const data = await response.json(); + const data: any = await response.json(); if (!response.ok) { throw new Error(`Token Error: ${data.error_description}`); } @@ -154,7 +184,7 @@ async function getOrCreateFolder(accessToken: string, folderName: string, env: E headers: { Authorization: `Bearer ${accessToken}` }, }); - const searchData = await searchRes.json(); + const searchData: GoogleDriveSearchResponse = await searchRes.json(); if (searchData.files && searchData.files.length > 0) { const folderId = searchData.files[0].id; @@ -175,7 +205,7 @@ async function getOrCreateFolder(accessToken: string, folderName: string, env: E }), }); - const createData = await createRes.json(); + const createData: any = await createRes.json(); await env.FOLDER_CACHE.put(folderName, createData.id, { expirationTtl: 3600 }); return createData.id; } @@ -214,8 +244,8 @@ async function streamUploadToDrive(accessToken: string, stream: ReadableStream | Authorization: `Bearer ${accessToken}`, }, body: stream, - duplex: "half" as any, - }); + duplex: "half", + } as RequestInit); if (!uploadRes.ok) { const errorText = await uploadRes.text(); @@ -225,12 +255,12 @@ async function streamUploadToDrive(accessToken: string, stream: ReadableStream | return await uploadRes.json(); } -async function findFileInFolder(accessToken: string, folderId: string, fileName: string): Promise { +async function findFileInFolder(accessToken: string, folderId: string, fileName: string): Promise { const searchRes = await fetch(`https://www.googleapis.com/drive/v3/files?q=name='${encodeURIComponent(fileName)}' and '${folderId}' in parents and trashed=false&fields=files(id,name,mimeType,size)`, { headers: { Authorization: `Bearer ${accessToken}` }, }); - const data = await searchRes.json(); + const data: GoogleDriveSearchResponse = await searchRes.json(); return data.files && data.files.length > 0 ? data.files[0] : null; } @@ -291,14 +321,14 @@ async function getFileMetadata(accessToken: string, bucket: string, objectKey: s }; } -async function listFiles(accessToken: string, bucket: string, env: Env): Promise { +async function listFiles(accessToken: string, bucket: string, env: Env): Promise { const folderId = await getOrCreateFolder(accessToken, bucket, env); const listRes = await fetch(`https://www.googleapis.com/drive/v3/files?q='${folderId}' in parents and trashed=false&fields=files(id,name,mimeType,size,modifiedTime)`, { headers: { Authorization: `Bearer ${accessToken}` }, }); - const data = await listRes.json(); + const data: GoogleDriveSearchResponse = await listRes.json(); return data.files || []; } diff --git a/test/s3.test.ts b/test/s3.test.ts index d9ac155..6a9599e 100644 --- a/test/s3.test.ts +++ b/test/s3.test.ts @@ -263,18 +263,41 @@ describe("S3 API Server with Google Drive Backend", () => { }); // ファイル一覧を返すようにモックを更新 - (global.fetch as any).mockImplementationOnce(async (url: string) => { - if (url.includes("drive/v3/files") && url.includes("in parents")) { + const originalFetch = global.fetch; + (global.fetch as any) = vi.fn(async (url: string, init?: any) => { + const urlStr = typeof url === "string" ? url : url.toString(); + + if (urlStr.includes("oauth2.googleapis.com/token")) { + return new Response( + JSON.stringify({ + access_token: "mock-access-token", + expires_in: 3600, + }), + { status: 200 }, + ); + } + + if (urlStr.includes("drive/v3/files") && urlStr.includes("mimeType='application/vnd.google-apps.folder'")) { + return new Response( + JSON.stringify({ + files: [{ id: "folder-id-123", name: "test-bucket" }], + }), + { status: 200 }, + ); + } + + if (urlStr.includes("drive/v3/files") && urlStr.includes("in parents")) { return new Response( JSON.stringify({ files: [ - { id: "1", name: "file1.txt", size: "100", modifiedTime: "2024-01-01T00:00:00Z" }, - { id: "2", name: "file2.txt", size: "200", modifiedTime: "2024-01-02T00:00:00Z" }, + { id: "1", name: "file1.txt", size: "100", mimeType: "text/plain", modifiedTime: "2024-01-01T00:00:00Z" }, + { id: "2", name: "file2.txt", size: "200", mimeType: "text/plain", modifiedTime: "2024-01-02T00:00:00Z" }, ], }), { status: 200 }, ); } + return new Response("Not Found", { status: 404 }); }); @@ -293,5 +316,226 @@ describe("S3 API Server with Google Drive Backend", () => { expect(xmlText).toContain(" { + const aws4 = new AwsClient({ + accessKeyId: ENV.ACCESS_KEY, + secretAccessKey: ENV.SECRET_KEY, + region: ENV.REGION, + service: "s3", + }); + + // ファイルが見つからないケース + const originalFetch = global.fetch; + (global.fetch as any) = vi.fn(async (url: string, init?: any) => { + const urlStr = typeof url === "string" ? url : url.toString(); + + if (urlStr.includes("oauth2.googleapis.com/token")) { + return new Response( + JSON.stringify({ + access_token: "mock-access-token", + expires_in: 3600, + }), + { status: 200 }, + ); + } + + if (urlStr.includes("drive/v3/files") && urlStr.includes("mimeType='application/vnd.google-apps.folder'")) { + return new Response( + JSON.stringify({ + files: [{ id: "folder-id-123", name: "test-bucket" }], + }), + { status: 200 }, + ); + } + + if (urlStr.includes("drive/v3/files") && urlStr.includes("in parents")) { + // ファイルが存在しない + return new Response(JSON.stringify({ files: [] }), { status: 200 }); + } + + return new Response("Not Found", { status: 404 }); + }); + + const requestUrl = `${endpoint}/test-bucket/non-existent.txt`; + const signedReq = await aws4.sign(requestUrl, { + method: "HEAD", + headers: { + "x-amz-content-sha256": "UNSIGNED-PAYLOAD", + }, + }); + + const response = await worker.fetch(signedReq, ENV, CTX); + expect(response.status).toBe(404); + + global.fetch = originalFetch; + }); + + // 9. 存在しないファイルのGETリクエスト (404) + it("should return 404 for GET on non-existent file", async () => { + const aws4 = new AwsClient({ + accessKeyId: ENV.ACCESS_KEY, + secretAccessKey: ENV.SECRET_KEY, + region: ENV.REGION, + service: "s3", + }); + + const originalFetch = global.fetch; + (global.fetch as any) = vi.fn(async (url: string, init?: any) => { + const urlStr = typeof url === "string" ? url : url.toString(); + + if (urlStr.includes("oauth2.googleapis.com/token")) { + return new Response( + JSON.stringify({ + access_token: "mock-access-token", + expires_in: 3600, + }), + { status: 200 }, + ); + } + + if (urlStr.includes("drive/v3/files") && urlStr.includes("mimeType='application/vnd.google-apps.folder'")) { + return new Response( + JSON.stringify({ + files: [{ id: "folder-id-123", name: "test-bucket" }], + }), + { status: 200 }, + ); + } + + if (urlStr.includes("drive/v3/files") && urlStr.includes("in parents")) { + return new Response(JSON.stringify({ files: [] }), { status: 200 }); + } + + return new Response("Not Found", { status: 404 }); + }); + + const requestUrl = `${endpoint}/test-bucket/non-existent.txt`; + const signedReq = await aws4.sign(requestUrl, { + method: "GET", + headers: { + "x-amz-content-sha256": "UNSIGNED-PAYLOAD", + }, + }); + + const response = await worker.fetch(signedReq, ENV, CTX); + expect(response.status).toBe(404); + expect(await response.text()).toBe("NoSuchKey"); + + global.fetch = originalFetch; + }); + + // 10. 存在しないファイルのDELETEリクエスト (404) + it("should return 404 for DELETE on non-existent file", async () => { + const aws4 = new AwsClient({ + accessKeyId: ENV.ACCESS_KEY, + secretAccessKey: ENV.SECRET_KEY, + region: ENV.REGION, + service: "s3", + }); + + const originalFetch = global.fetch; + (global.fetch as any) = vi.fn(async (url: string, init?: any) => { + const urlStr = typeof url === "string" ? url : url.toString(); + + if (urlStr.includes("oauth2.googleapis.com/token")) { + return new Response( + JSON.stringify({ + access_token: "mock-access-token", + expires_in: 3600, + }), + { status: 200 }, + ); + } + + if (urlStr.includes("drive/v3/files") && urlStr.includes("mimeType='application/vnd.google-apps.folder'")) { + return new Response( + JSON.stringify({ + files: [{ id: "folder-id-123", name: "test-bucket" }], + }), + { status: 200 }, + ); + } + + if (urlStr.includes("drive/v3/files") && urlStr.includes("in parents")) { + return new Response(JSON.stringify({ files: [] }), { status: 200 }); + } + + return new Response("Not Found", { status: 404 }); + }); + + const requestUrl = `${endpoint}/test-bucket/non-existent.txt`; + const signedReq = await aws4.sign(requestUrl, { + method: "DELETE", + headers: { + "x-amz-content-sha256": "UNSIGNED-PAYLOAD", + }, + }); + + const response = await worker.fetch(signedReq, ENV, CTX); + expect(response.status).toBe(404); + + global.fetch = originalFetch; + }); + + // 11. 空のバケット一覧 + it("should return empty list for empty bucket", async () => { + const aws4 = new AwsClient({ + accessKeyId: ENV.ACCESS_KEY, + secretAccessKey: ENV.SECRET_KEY, + region: ENV.REGION, + service: "s3", + }); + + const originalFetch = global.fetch; + (global.fetch as any) = vi.fn(async (url: string, init?: any) => { + const urlStr = typeof url === "string" ? url : url.toString(); + + if (urlStr.includes("oauth2.googleapis.com/token")) { + return new Response( + JSON.stringify({ + access_token: "mock-access-token", + expires_in: 3600, + }), + { status: 200 }, + ); + } + + if (urlStr.includes("drive/v3/files") && urlStr.includes("mimeType='application/vnd.google-apps.folder'")) { + return new Response( + JSON.stringify({ + files: [{ id: "folder-id-123", name: "empty-bucket" }], + }), + { status: 200 }, + ); + } + + if (urlStr.includes("drive/v3/files") && urlStr.includes("in parents")) { + return new Response(JSON.stringify({ files: [] }), { status: 200 }); + } + + return new Response("Not Found", { status: 404 }); + }); + + const requestUrl = `${endpoint}/empty-bucket/`; + const signedReq = await aws4.sign(requestUrl, { + method: "GET", + headers: { + "x-amz-content-sha256": "UNSIGNED-PAYLOAD", + }, + }); + + const response = await worker.fetch(signedReq, ENV, CTX); + expect(response.status).toBe(200); + + const xmlText = await response.text(); + expect(xmlText).toContain(""); + + global.fetch = originalFetch; }); });