From 92d4da14ed1b4eff506c52fe2ea644c8448fe023 Mon Sep 17 00:00:00 2001 From: Nezumi-2711 Date: Sun, 16 Aug 2026 15:40:42 +0700 Subject: [PATCH] fix: add modified value for object --- src/google-drive.ts | 4 +++- src/router.ts | 8 ++++---- src/types.ts | 2 ++ test/s3.test.ts | 18 +++++++++++++++++- 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/google-drive.ts b/src/google-drive.ts index 614a682..1cbaf93 100644 --- a/src/google-drive.ts +++ b/src/google-drive.ts @@ -11,7 +11,7 @@ interface GoogleDriveCreateResponse { id: string; } -const DRIVE_FIELDS = "id,name,size,mimeType,md5Checksum"; +const DRIVE_FIELDS = "id,name,size,mimeType,md5Checksum,modifiedTime"; const FOLDER_MIME_TYPE = "application/vnd.google-apps.folder"; const LIST_NODE_CAP = 5000; @@ -227,6 +227,7 @@ export async function streamDownloadFromDrive(accessToken: string, bucket: strin size: parseInt(file.size || "0", 10), id: file.id, md5Checksum: file.md5Checksum, + modifiedTime: file.modifiedTime, status: downloadRes.status, contentRange: downloadRes.headers.get("Content-Range") ?? undefined, contentLength: downloadRes.headers.get("Content-Length") ?? undefined, @@ -264,6 +265,7 @@ export async function getFileMetadata(accessToken: string, bucket: string, objec mimeType: file.mimeType || "application/octet-stream", size: parseInt(file.size || "0", 10), md5Checksum: file.md5Checksum, + modifiedTime: file.modifiedTime, }; } diff --git a/src/router.ts b/src/router.ts index da81f69..e498bdd 100644 --- a/src/router.ts +++ b/src/router.ts @@ -207,6 +207,7 @@ export async function dispatch(request: Request, env: Env, accessToken: string, ETag: `"${etag(file)}"`, }); if (file.contentRange) headers.set("Content-Range", file.contentRange); + if (file.modifiedTime) headers.set("Last-Modified", new Date(file.modifiedTime).toUTCString()); return new Response(file.body, { status: file.status, headers }); } catch (error) { if (error instanceof Error && error.message === "File not found") return s3Error("NoSuchKey", 404, undefined, resource); @@ -218,10 +219,9 @@ export async function dispatch(request: Request, env: Env, accessToken: string, if (!key) return new Response(null, { status: 200 }); try { const metadata = await getFileMetadata(accessToken, bucket, key, env); - return new Response(null, { - status: 200, - headers: { "Content-Type": metadata.mimeType, "Content-Length": metadata.size.toString(), "Accept-Ranges": "bytes", ETag: `"${etag(metadata)}"` }, - }); + const headers = new Headers({ "Content-Type": metadata.mimeType, "Content-Length": metadata.size.toString(), "Accept-Ranges": "bytes", ETag: `"${etag(metadata)}"` }); + if (metadata.modifiedTime) headers.set("Last-Modified", new Date(metadata.modifiedTime).toUTCString()); + return new Response(null, { status: 200, headers }); } catch (error) { if (error instanceof Error && error.message === "File not found") return s3Error("NoSuchKey", 404, undefined, resource, true); throw error; diff --git a/src/types.ts b/src/types.ts index 54ae13f..44772fc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -41,6 +41,7 @@ export interface DriveDownloadResult { size: number; id: string; md5Checksum?: string; + modifiedTime?: string; status: number; contentRange?: string; contentLength?: string; @@ -51,4 +52,5 @@ export interface DriveFileMetadata { mimeType: string; size: number; md5Checksum?: string; + modifiedTime?: string; } diff --git a/test/s3.test.ts b/test/s3.test.ts index 36905d3..c5127b4 100644 --- a/test/s3.test.ts +++ b/test/s3.test.ts @@ -18,8 +18,11 @@ interface StoredFile { mimeType: string; data: Uint8Array; md5Checksum: string; + modifiedTime: string; } +const FAKE_MODIFIED_TIME = "2026-08-16T08:00:00.000Z"; + interface StoredFolder { id: string; name: string; @@ -95,6 +98,7 @@ class FakeDrive { mimeType, data: new Uint8Array(), md5Checksum: "d41d8cd98f00b204e9800998ecf8427e", + modifiedTime: FAKE_MODIFIED_TIME, }; this.files.set(id, file); return Response.json({ ...file, size: "0", data: undefined }); @@ -148,7 +152,7 @@ class FakeDrive { private finalize(session: UploadSession, data: Uint8Array): Response { const id = session.fileId ?? `file-${this.nextId++}`; - const file: StoredFile = { id, name: session.name, parent: session.parent, mimeType: session.mimeType, data, md5Checksum: fakeMd5(data) }; + const file: StoredFile = { id, name: session.name, parent: session.parent, mimeType: session.mimeType, data, md5Checksum: fakeMd5(data), modifiedTime: FAKE_MODIFIED_TIME }; this.files.set(id, file); this.sessions.delete(session.id); return Response.json({ ...file, size: String(data.byteLength), data: undefined }); @@ -225,6 +229,18 @@ describe("S3 compatibility", () => { expect(new TextDecoder().decode([...drive.files.values()].find((file) => file.name === "file.txt")!.data)).toBe("second"); }); + it("sets Last-Modified on GET and HEAD from Drive's modifiedTime", async () => { + await worker.fetch(await signed("/test-bucket/dated.txt", { method: "PUT", body: "hi" }), ENV, CTX); + + const expected = new Date(FAKE_MODIFIED_TIME).toUTCString(); + + const get = await worker.fetch(await signed("/test-bucket/dated.txt", { method: "GET" }), ENV, CTX); + expect(get.headers.get("Last-Modified")).toBe(expected); + + const head = await worker.fetch(await signed("/test-bucket/dated.txt", { method: "HEAD" }), ENV, CTX); + expect(head.headers.get("Last-Modified")).toBe(expected); + }); + it("forwards Range and returns a standard XML NoSuchKey", async () => { await worker.fetch(await signed("/test-bucket/range.bin", { method: "PUT", body: "0123456789" }), ENV, CTX); const response = await worker.fetch(await signed("/test-bucket/range.bin", { method: "GET", headers: { Range: "bytes=2-5" } }), ENV, CTX);