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
|
# 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.
|
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
|
### 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
|
```ini
|
||||||
CORS_ALLOWED_ORIGINS=https://app.example.com,http://localhost:5173
|
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
|
## 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 |
|
| Document | Purpose |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| [Integration guide](./integration-guide.md) | Browser upload, multipart upload, listing, CORS, and recommended architecture. |
|
| [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. |
|
| [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/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. |
|
| [`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
|
```text
|
||||||
Browser ── POST /presign ──> BFF (holds SECRET_KEY)
|
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.
|
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
|
# 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
|
- 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.
|
- 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
|
5. the signed-header list; and
|
||||||
6. `x-amz-content-sha256`, defaulting to `UNSIGNED-PAYLOAD`.
|
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.
|
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";
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
||||||
|
|
||||||
interface Env {
|
interface Env {
|
||||||
IRIS_ENDPOINT: string;
|
S3_ENDPOINT: string;
|
||||||
IRIS_ACCESS_KEY: string;
|
S3_ACCESS_KEY: string;
|
||||||
IRIS_SECRET_KEY: string;
|
S3_SECRET_KEY: string;
|
||||||
IRIS_REGION: string;
|
S3_REGION: string;
|
||||||
IRIS_BUCKET: string;
|
S3_BUCKET: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PresignRequest {
|
interface PresignRequest {
|
||||||
@@ -33,10 +33,10 @@ function json(data: unknown, status = 200): Response {
|
|||||||
|
|
||||||
function s3(env: Env): S3Client {
|
function s3(env: Env): S3Client {
|
||||||
return new S3Client({
|
return new S3Client({
|
||||||
endpoint: env.IRIS_ENDPOINT,
|
endpoint: env.S3_ENDPOINT,
|
||||||
region: env.IRIS_REGION,
|
region: env.S3_REGION,
|
||||||
forcePathStyle: true,
|
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") {
|
if (operation === "put") {
|
||||||
const contentType = input.contentType || "application/octet-stream";
|
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 });
|
return json({ url, method: "PUT", headers: { "Content-Type": contentType }, expiresIn });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (operation === "createMultipart") {
|
if (operation === "createMultipart") {
|
||||||
const contentType = input.contentType || "application/octet-stream";
|
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 });
|
return json({ url, method: "POST", headers: { "Content-Type": contentType }, expiresIn });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,17 +76,17 @@ export default {
|
|||||||
if (operation === "uploadPart") {
|
if (operation === "uploadPart") {
|
||||||
const partNumber = input.partNumber;
|
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);
|
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 });
|
return json({ url, method: "PUT", expiresIn });
|
||||||
}
|
}
|
||||||
if (operation === "completeMultipart") {
|
if (operation === "completeMultipart") {
|
||||||
const body = completionXml(input.parts);
|
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);
|
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 });
|
return json({ url, method: "POST", headers: { "Content-Type": "application/xml" }, body, expiresIn });
|
||||||
}
|
}
|
||||||
if (operation === "abortMultipart") {
|
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 });
|
return json({ url, method: "DELETE", expiresIn });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ function uploadIdFromXml(xml: string): string {
|
|||||||
return value;
|
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> {
|
export async function uploadLargeFile(file: File, key: string, partSize = 16 * 1024 * 1024): Promise<void> {
|
||||||
const contentType = file.type || "application/octet-stream";
|
const contentType = file.type || "application/octet-stream";
|
||||||
const create = await presign({ key, contentType, operation: "createMultipart" });
|
const create = await presign({ key, contentType, operation: "createMultipart" });
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
Use a backend-for-frontend (BFF) as the trust boundary:
|
Use a backend-for-frontend (BFF) as the trust boundary:
|
||||||
|
|
||||||
```text
|
```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.
|
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
|
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
|
### 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
|
## 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
|
```ts
|
||||||
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
||||||
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
||||||
|
|
||||||
const client = new S3Client({
|
const client = new S3Client({
|
||||||
endpoint: env.IRIS_ENDPOINT,
|
endpoint: env.S3_ENDPOINT,
|
||||||
region: env.IRIS_REGION,
|
region: env.S3_REGION,
|
||||||
credentials: {
|
credentials: {
|
||||||
accessKeyId: env.IRIS_ACCESS_KEY,
|
accessKeyId: env.S3_ACCESS_KEY,
|
||||||
secretAccessKey: env.IRIS_SECRET_KEY,
|
secretAccessKey: env.S3_SECRET_KEY,
|
||||||
},
|
},
|
||||||
forcePathStyle: true,
|
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.
|
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.
|
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`.
|
2. BFF issues a signed `PUT` URL for part 1; browser uploads it and records the `ETag`.
|
||||||
|
|||||||
+3
-3
@@ -1,10 +1,10 @@
|
|||||||
# Limitations
|
# 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
|
## 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.
|
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 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
|
## Multipart behavior
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
openapi: 3.1.0
|
openapi: 3.1.0
|
||||||
info:
|
info:
|
||||||
title: I.R.I.S. Google Drive S3 API
|
title: Google Drive S3 API
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
description: |
|
description: |
|
||||||
An intentionally small S3-compatible object-storage API backed by Google Drive.
|
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 charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta name="color-scheme" content="dark light">
|
<meta name="color-scheme" content="dark light">
|
||||||
<title>I.R.I.S. S3 API reference</title>
|
<title>Google Drive S3 API reference</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script id="api-reference" data-url="${OPENAPI_PATH}"></script>
|
<script id="api-reference" data-url="${OPENAPI_PATH}"></script>
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
{
|
{
|
||||||
"$schema": "node_modules/wrangler/config-schema.json",
|
"$schema": "node_modules/wrangler/config-schema.json",
|
||||||
"name": "iris",
|
"name": "s3-google-drive",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"compatibility_date": "2025-09-27",
|
"compatibility_date": "2025-09-27",
|
||||||
"vars": {
|
"vars": {
|
||||||
|
|||||||
Reference in New Issue
Block a user