mirror of
https://github.com/Nezumi-2711/google-drive-s3.git
synced 2026-09-22 13:38:30 +00:00
fix: update the prefix for the variable
This commit is contained in:
@@ -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 -'
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
+4
-4
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
|
||||
@@ -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<void> {
|
||||
const contentType = file.type || "application/octet-stream";
|
||||
const create = await presign({ key, contentType, operation: "createMultipart" });
|
||||
|
||||
@@ -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`.
|
||||
|
||||
+3
-3
@@ -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
|
||||
|
||||
|
||||
+1
-1
@@ -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.
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ export function docsResponse(): Response {
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="dark light">
|
||||
<title>I.R.I.S. S3 API reference</title>
|
||||
<title>Google Drive S3 API reference</title>
|
||||
</head>
|
||||
<body>
|
||||
<script id="api-reference" data-url="${OPENAPI_PATH}"></script>
|
||||
|
||||
+1
-1
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user