From 27d8fae0f0910f7df14bfa67e45adca31df2933d Mon Sep 17 00:00:00 2001 From: Nezumi-2711 Date: Sun, 16 Aug 2026 18:34:59 +0700 Subject: [PATCH] fix: update the prefix for the variable --- .devcontainer/Dockerfile | 5 ---- .devcontainer/devcontainer.json | 44 ------------------------------ .env.example | 23 ++++++++++++++++ README.md | 3 +- docs/README.md | 8 +++--- docs/authentication.md | 4 +-- docs/examples/bff-presign.ts | 26 +++++++++--------- docs/examples/browser-multipart.ts | 2 +- docs/integration-guide.md | 16 +++++------ docs/limitations.md | 6 ++-- docs/openapi.yaml | 2 +- src/docs.ts | 2 +- wrangler.jsonc | 2 +- 13 files changed, 58 insertions(+), 85 deletions(-) delete mode 100644 .devcontainer/Dockerfile delete mode 100644 .devcontainer/devcontainer.json create mode 100644 .env.example diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index b5fb2c4..0000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,5 +0,0 @@ -FROM ubuntu:rolling - -RUN apt-get update && \ - apt-get install -y git curl && \ - su ubuntu sh -c 'curl -fsSL https://get.pnpm.io/install.sh | ENV="$HOME/.bashrc" SHELL="$(which bash)" bash -' diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 931ef17..0000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "name": "google-drive-s3", - "build": { - "dockerfile": "./Dockerfile" - }, - "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind", - "workspaceFolder": "/workspace", - "remoteUser": "ubuntu", - "overrideCommand": true, - "postCreateCommand": "git config --global pull.rebase false && pnpm env use --global 24", - "customizations": { - "vscode": { - "extensions": ["biomejs.biome"], - "settings": { - "editor.linkedEditing": true, - "editor.formatOnPaste": true, - "editor.formatOnSave": true, - - "editor.codeActionsOnSave": { - "source.organizeImports.biome": "explicit", - "source.fixAll.biome": "explicit" - }, - - "[typescript]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[typescriptreact]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[json]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[javascript]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[javascriptreact]": { - "editor.defaultFormatter": "biomejs.biome" - }, - - "biome.lsp.bin": "pnpm biome" - } - } - } -} diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..44ae38d --- /dev/null +++ b/.env.example @@ -0,0 +1,23 @@ +# S3 Signature V4 credentials held by CLI clients or your BFF +ACCESS_KEY=replace-with-access-key-id +SECRET_KEY=replace-with-long-random-secret +REGION=auto + +# Google OAuth credentials with Drive API access +GOOGLE_CLIENT_ID=replace-with-google-oauth-client-id +GOOGLE_CLIENT_SECRET=replace-with-google-oauth-client-secret +GOOGLE_REFRESH_TOKEN=replace-with-google-refresh-token + +# Comma-separated, exact bucket names. Unset denies every bucket. +ALLOWED_BUCKETS=assets +# Optional comma-separated subset of ALLOWED_BUCKETS that permits unsigned GET/HEAD. +PUBLIC_READ_BUCKETS= + +# Durable Object multipart uploads and ETag result behavior +ALLOW_MULTIPART=true +ETAG_STYLE=md5 + +# Browser CORS: comma-separated exact origins or *. Leave empty to disable CORS. +CORS_ALLOWED_ORIGINS=http://localhost:5173 +# Set false to disable /docs and /openapi.yaml +ENABLE_DOCS=true diff --git a/README.md b/README.md index 3603d6d..2171475 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ # google-drive-s3 -I.R.I.S. (Integrated Reliable Interop Storage) Use Cloudflare Workers to turn your Google Drive into S3 object storage at no extra cost. @@ -101,7 +100,7 @@ Google Drive's free tier has 15 GB total storage, and Google applies a 750 GB da ### 5. CORS Configuration -I.R.I.S. provides native, deny-by-default CORS handling. Set `CORS_ALLOWED_ORIGINS` to a comma-separated list of exact origins: +The Worker provides native, deny-by-default CORS handling. Set `CORS_ALLOWED_ORIGINS` to a comma-separated list of exact origins: ```ini CORS_ALLOWED_ORIGINS=https://app.example.com,http://localhost:5173 diff --git a/docs/README.md b/docs/README.md index a89e4e5..716d83f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ -# I.R.I.S. frontend integration docs +# Google Drive S3 frontend integration docs -I.R.I.S. exposes a focused S3-compatible API on top of Google Drive. The live API reference is available from every enabled Worker deployment at [`/docs`](/docs), with the source OpenAPI document at [`/openapi.yaml`](/openapi.yaml). +This project exposes a focused S3-compatible API on top of Google Drive. The live API reference is available from every enabled Worker deployment at [`/docs`](/docs), with the source OpenAPI document at [`/openapi.yaml`](/openapi.yaml). ## Start here @@ -12,7 +12,7 @@ I.R.I.S. exposes a focused S3-compatible API on top of Google Drive. The live AP | Document | Purpose | | --- | --- | | [Integration guide](./integration-guide.md) | Browser upload, multipart upload, listing, CORS, and recommended architecture. | -| [Authentication](./authentication.md) | The precise AWS Signature V4 behavior enforced by I.R.I.S. | +| [Authentication](./authentication.md) | The precise AWS Signature V4 behavior enforced by the Worker. | | [Limitations](./limitations.md) | Unsupported S3 features and Google Drive operational limits. | | [`examples/bff-presign.ts`](./examples/bff-presign.ts) | A Workers BFF endpoint that creates short-lived PUT URLs. | | [`examples/browser-upload.ts`](./examples/browser-upload.ts) | Single object upload from a browser. | @@ -23,7 +23,7 @@ I.R.I.S. exposes a focused S3-compatible API on top of Google Drive. The live AP ```text Browser ── POST /presign ──> BFF (holds SECRET_KEY) │ │ - └──── presigned S3 request ────┴──> I.R.I.S. Worker ──> Google Drive + └──── presigned S3 request ────┴──> S3 Google Drive Worker ──> Google Drive ``` The browser never receives `SECRET_KEY`. Presigned URLs must be short lived and scoped to the single object operation the browser needs. diff --git a/docs/authentication.md b/docs/authentication.md index 046fb1d..99662f9 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -1,6 +1,6 @@ # Authentication and Signature V4 -I.R.I.S. accepts AWS Signature Version 4 in either form: +The Worker accepts AWS Signature Version 4 in either form: - an `Authorization: AWS4-HMAC-SHA256 …` header with `x-amz-date`; or - a presigned request with `X-Amz-Algorithm`, `X-Amz-Credential`, `X-Amz-Date`, `X-Amz-SignedHeaders`, and `X-Amz-Signature` query parameters. @@ -28,7 +28,7 @@ The canonical request is built from: 5. the signed-header list; and 6. `x-amz-content-sha256`, defaulting to `UNSIGNED-PAYLOAD`. -I.R.I.S. deliberately canonicalizes a signed `accept-encoding` header to `identity`. Cloudflare can rewrite the received value at the edge; S3 SDKs that sign this header use `identity` for this reason. Browser code must not sign `accept-encoding` because browser networking controls it. +The Worker deliberately canonicalizes a signed `accept-encoding` header to `identity`. Cloudflare can rewrite the received value at the edge; S3 SDKs that sign this header use `identity` for this reason. Browser code must not sign `accept-encoding` because browser networking controls it. Payload hashes are **not** verified. Browser and BFF clients should use `x-amz-content-sha256: UNSIGNED-PAYLOAD`; this is a deliberate streaming limitation, not an integrity guarantee. diff --git a/docs/examples/bff-presign.ts b/docs/examples/bff-presign.ts index 9a27134..c20eb00 100644 --- a/docs/examples/bff-presign.ts +++ b/docs/examples/bff-presign.ts @@ -2,11 +2,11 @@ import { AbortMultipartUploadCommand, CompleteMultipartUploadCommand, CreateMult import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; interface Env { - IRIS_ENDPOINT: string; - IRIS_ACCESS_KEY: string; - IRIS_SECRET_KEY: string; - IRIS_REGION: string; - IRIS_BUCKET: string; + S3_ENDPOINT: string; + S3_ACCESS_KEY: string; + S3_SECRET_KEY: string; + S3_REGION: string; + S3_BUCKET: string; } interface PresignRequest { @@ -33,10 +33,10 @@ function json(data: unknown, status = 200): Response { function s3(env: Env): S3Client { return new S3Client({ - endpoint: env.IRIS_ENDPOINT, - region: env.IRIS_REGION, + endpoint: env.S3_ENDPOINT, + region: env.S3_REGION, forcePathStyle: true, - credentials: { accessKeyId: env.IRIS_ACCESS_KEY, secretAccessKey: env.IRIS_SECRET_KEY }, + credentials: { accessKeyId: env.S3_ACCESS_KEY, secretAccessKey: env.S3_SECRET_KEY }, }); } @@ -62,13 +62,13 @@ export default { if (operation === "put") { const contentType = input.contentType || "application/octet-stream"; - const url = await getSignedUrl(client, new PutObjectCommand({ Bucket: env.IRIS_BUCKET, Key: input.key, ContentType: contentType }), { expiresIn }); + const url = await getSignedUrl(client, new PutObjectCommand({ Bucket: env.S3_BUCKET, Key: input.key, ContentType: contentType }), { expiresIn }); return json({ url, method: "PUT", headers: { "Content-Type": contentType }, expiresIn }); } if (operation === "createMultipart") { const contentType = input.contentType || "application/octet-stream"; - const url = await getSignedUrl(client, new CreateMultipartUploadCommand({ Bucket: env.IRIS_BUCKET, Key: input.key, ContentType: contentType }), { expiresIn }); + const url = await getSignedUrl(client, new CreateMultipartUploadCommand({ Bucket: env.S3_BUCKET, Key: input.key, ContentType: contentType }), { expiresIn }); return json({ url, method: "POST", headers: { "Content-Type": contentType }, expiresIn }); } @@ -76,17 +76,17 @@ export default { if (operation === "uploadPart") { const partNumber = input.partNumber; if (!Number.isInteger(partNumber) || partNumber === undefined || partNumber < 1 || partNumber > 10_000) return json({ error: "partNumber must be 1 through 10000" }, 400); - const url = await getSignedUrl(client, new UploadPartCommand({ Bucket: env.IRIS_BUCKET, Key: input.key, UploadId: input.uploadId, PartNumber: partNumber }), { expiresIn }); + const url = await getSignedUrl(client, new UploadPartCommand({ Bucket: env.S3_BUCKET, Key: input.key, UploadId: input.uploadId, PartNumber: partNumber }), { expiresIn }); return json({ url, method: "PUT", expiresIn }); } if (operation === "completeMultipart") { const body = completionXml(input.parts); if (!body) return json({ error: "parts must have sequential PartNumber values beginning at 1 and non-empty ETag values" }, 400); - const url = await getSignedUrl(client, new CompleteMultipartUploadCommand({ Bucket: env.IRIS_BUCKET, Key: input.key, UploadId: input.uploadId, MultipartUpload: { Parts: input.parts } }), { expiresIn }); + const url = await getSignedUrl(client, new CompleteMultipartUploadCommand({ Bucket: env.S3_BUCKET, Key: input.key, UploadId: input.uploadId, MultipartUpload: { Parts: input.parts } }), { expiresIn }); return json({ url, method: "POST", headers: { "Content-Type": "application/xml" }, body, expiresIn }); } if (operation === "abortMultipart") { - const url = await getSignedUrl(client, new AbortMultipartUploadCommand({ Bucket: env.IRIS_BUCKET, Key: input.key, UploadId: input.uploadId }), { expiresIn }); + const url = await getSignedUrl(client, new AbortMultipartUploadCommand({ Bucket: env.S3_BUCKET, Key: input.key, UploadId: input.uploadId }), { expiresIn }); return json({ url, method: "DELETE", expiresIn }); } diff --git a/docs/examples/browser-multipart.ts b/docs/examples/browser-multipart.ts index 56ff2f0..f865ca5 100644 --- a/docs/examples/browser-multipart.ts +++ b/docs/examples/browser-multipart.ts @@ -21,7 +21,7 @@ function uploadIdFromXml(xml: string): string { return value; } -/** Uploads parts in order. I.R.I.S. does not allow parallel or out-of-order part uploads. */ +/** Uploads parts in order. The Worker does not allow parallel or out-of-order part uploads. */ export async function uploadLargeFile(file: File, key: string, partSize = 16 * 1024 * 1024): Promise { const contentType = file.type || "application/octet-stream"; const create = await presign({ key, contentType, operation: "createMultipart" }); diff --git a/docs/integration-guide.md b/docs/integration-guide.md index 8ea42ae..8af9dea 100644 --- a/docs/integration-guide.md +++ b/docs/integration-guide.md @@ -5,7 +5,7 @@ Use a backend-for-frontend (BFF) as the trust boundary: ```text -Browser → BFF (holds ACCESS_KEY + SECRET_KEY, issues presigned URLs) → I.R.I.S. Worker → Google Drive +Browser → BFF (holds ACCESS_KEY + SECRET_KEY, issues presigned URLs) → S3 Google Drive Worker → Google Drive ``` Never put `SECRET_KEY` in browser code, a static site, or a public environment variable. The browser receives only a short-lived presigned URL for one method and object key. @@ -18,7 +18,7 @@ Configure the Worker with an explicit allow-list of web origins: CORS_ALLOWED_ORIGINS=https://app.example.com,http://localhost:5173 ``` -Set it to `*` only for a genuinely public, credential-free integration. Leaving it unset emits no CORS headers. I.R.I.S. exposes `ETag`, `Content-Range`, `Content-Length`, `Last-Modified`, `Accept-Ranges`, and `x-amz-request-id`, so browser code can inspect the headers that form the effective S3 response payload. +Set it to `*` only for a genuinely public, credential-free integration. Leaving it unset emits no CORS headers. The Worker exposes `ETag`, `Content-Range`, `Content-Length`, `Last-Modified`, `Accept-Ranges`, and `x-amz-request-id`, so browser code can inspect the headers that form the effective S3 response payload. ### Server-to-server compatibility @@ -28,18 +28,18 @@ An `OPTIONS` request with no `Origin`, or a preflight from a disallowed origin, ## Presign in the BFF -Use `@aws-sdk/s3-request-presigner` and send a limited expiry. I.R.I.S. validates `X-Amz-Expires` and rejects URLs after their signing time plus that value. +Use `@aws-sdk/s3-request-presigner` and send a limited expiry. The Worker validates `X-Amz-Expires` and rejects URLs after their signing time plus that value. ```ts import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; const client = new S3Client({ - endpoint: env.IRIS_ENDPOINT, - region: env.IRIS_REGION, + endpoint: env.S3_ENDPOINT, + region: env.S3_REGION, credentials: { - accessKeyId: env.IRIS_ACCESS_KEY, - secretAccessKey: env.IRIS_SECRET_KEY, + accessKeyId: env.S3_ACCESS_KEY, + secretAccessKey: env.S3_SECRET_KEY, }, forcePathStyle: true, }); @@ -78,7 +78,7 @@ const etag = response.headers.get("ETag"); Cloudflare request limits mean a single PUT is best kept below roughly 100 MB. For larger files, have the BFF initiate the upload, issue a presigned URL for each part, and complete the upload after the browser returns all ETags. -Use 16 MB parts as a practical baseline. Upload parts **strictly sequentially starting at 1**. I.R.I.S. waits up to 20 seconds for an earlier part; out-of-order work then receives `503 SlowDown`. Requests more than 64 part numbers ahead are rejected immediately. +Use 16 MB parts as a practical baseline. Upload parts **strictly sequentially starting at 1**. The Worker waits up to 20 seconds for an earlier part; out-of-order work then receives `503 SlowDown`. Requests more than 64 part numbers ahead are rejected immediately. 1. BFF signs `POST /bucket/key?uploads` and starts the multipart upload. 2. BFF issues a signed `PUT` URL for part 1; browser uploads it and records the `ETag`. diff --git a/docs/limitations.md b/docs/limitations.md index d60ca7e..df53fa0 100644 --- a/docs/limitations.md +++ b/docs/limitations.md @@ -1,10 +1,10 @@ # Limitations -I.R.I.S. is a narrow S3 compatibility layer. Design frontend features around the behavior below. +This project is a narrow S3 compatibility layer. Design frontend features around the behavior below. ## Listing is not pageable -`max-keys`, `continuation-token`, and `marker` are ignored. I.R.I.S. scans at most 5000 Google Drive nodes and then sets `IsTruncated=true`; it does not provide a continuation token, so additional results cannot be requested. The XML value `MaxKeys=1000` is a fixed compatibility value, not the real scan limit. +`max-keys`, `continuation-token`, and `marker` are ignored. The Worker scans at most 5000 Google Drive nodes and then sets `IsTruncated=true`; it does not provide a continuation token, so additional results cannot be requested. The XML value `MaxKeys=1000` is a fixed compatibility value, not the real scan limit. Use narrow prefixes and `delimiter=/` for a file browser. Do not build an infinite-scrolling full-drive browser. @@ -24,7 +24,7 @@ Bucket `?acl`, `?versioning`, and `?location` requests are currently treated as ## Directories and objects -Directories are physical Google Drive folders created from object-key path segments. I.R.I.S. does not create zero-byte directory marker objects. An empty prefix can therefore exist as a Drive folder without an S3 marker object. +Directories are physical Google Drive folders created from object-key path segments. The Worker does not create zero-byte directory marker objects. An empty prefix can therefore exist as a Drive folder without an S3 marker object. ## Multipart behavior diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 0722319..b277897 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -1,6 +1,6 @@ openapi: 3.1.0 info: - title: I.R.I.S. Google Drive S3 API + title: Google Drive S3 API version: 1.0.0 description: | An intentionally small S3-compatible object-storage API backed by Google Drive. diff --git a/src/docs.ts b/src/docs.ts index de29098..ebbf918 100644 --- a/src/docs.ts +++ b/src/docs.ts @@ -19,7 +19,7 @@ export function docsResponse(): Response { - I.R.I.S. S3 API reference + Google Drive S3 API reference diff --git a/wrangler.jsonc b/wrangler.jsonc index 1c188c9..2d375fc 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -4,7 +4,7 @@ */ { "$schema": "node_modules/wrangler/config-schema.json", - "name": "iris", + "name": "s3-google-drive", "main": "src/index.ts", "compatibility_date": "2025-09-27", "vars": {