feat: add the openapi docs

This commit is contained in:
2026-08-16 16:43:21 +07:00
parent 92d4da14ed
commit a235040be7
27 changed files with 3558 additions and 2837 deletions
+29
View File
@@ -0,0 +1,29 @@
# I.R.I.S. 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).
## Start here
1. Configure the Worker with `CORS_ALLOWED_ORIGINS` for every browser origin that will upload or download objects.
2. Keep `SECRET_KEY` in a backend-for-frontend (BFF), not in browser code.
3. Let the BFF issue short-lived presigned URLs for a constrained bucket/key/method.
4. Upload or download through the presigned URL from the browser.
| 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. |
| [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. |
| [`examples/browser-multipart.ts`](./examples/browser-multipart.ts) | Strictly sequential browser multipart upload. |
## Browser security model
```text
Browser ── POST /presign ──> BFF (holds SECRET_KEY)
│ │
└──── presigned S3 request ────┴──> I.R.I.S. 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.
+39
View File
@@ -0,0 +1,39 @@
# Authentication and Signature V4
I.R.I.S. 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.
The `Credential` access-key ID must equal the Worker's `ACCESS_KEY`; the signing key is derived from `SECRET_KEY`, `REGION`, service `s3`, and the `YYYYMMDD` request date.
## Presigned URL expiry
Query-authenticated requests require `X-Amz-Expires` from 1 through 604800 seconds (seven days). The Worker returns `403 AccessDenied` with `Request has expired` when it is missing, invalid, out of range, or its signed timestamp plus expiry is in the past.
BFFs should normally use a much shorter period such as five minutes.
## Header-authenticated requests
The Worker requires a valid `x-amz-date` and rejects a request when the server time differs from it by more than 15 minutes. This returns `403 RequestTimeTooSkewed`.
## Canonical request behavior
The canonical request is built from:
1. method;
2. URL path;
3. sorted query string (excluding `X-Amz-Signature` for presigned URLs);
4. the signed-header list and normalized values;
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.
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.
## Public-read buckets
Buckets listed in `PUBLIC_READ_BUCKETS` permit unsigned `GET` and `HEAD` requests. All write operations still require valid Signature V4 authentication. `PUBLIC_READ_BUCKETS` must be a subset of `ALLOWED_BUCKETS`.
For a tested signing reference, see the `signed()` helper in [`test/s3.test.ts`](../test/s3.test.ts).
+95
View File
@@ -0,0 +1,95 @@
import { AbortMultipartUploadCommand, CompleteMultipartUploadCommand, CreateMultipartUploadCommand, PutObjectCommand, S3Client, UploadPartCommand } from "@aws-sdk/client-s3";
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;
}
interface PresignRequest {
key: string;
contentType?: string;
operation?: "put" | "createMultipart" | "uploadPart" | "completeMultipart" | "abortMultipart";
uploadId?: string;
partNumber?: number;
parts?: Array<{ ETag: string; PartNumber: number }>;
}
function escapeXml(value: string): string {
return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
}
function completionXml(parts: Array<{ ETag: string; PartNumber: number }> | undefined): string | null {
if (!parts?.length || parts.some((part, index) => !Number.isInteger(part.PartNumber) || part.PartNumber !== index + 1 || typeof part.ETag !== "string" || part.ETag.length === 0)) return null;
return `<CompleteMultipartUpload>${parts.map((part) => `<Part><PartNumber>${part.PartNumber}</PartNumber><ETag>${escapeXml(part.ETag)}</ETag></Part>`).join("")}</CompleteMultipartUpload>`;
}
function json(data: unknown, status = 200): Response {
return Response.json(data, { status, headers: { "Cache-Control": "no-store" } });
}
function s3(env: Env): S3Client {
return new S3Client({
endpoint: env.IRIS_ENDPOINT,
region: env.IRIS_REGION,
forcePathStyle: true,
credentials: { accessKeyId: env.IRIS_ACCESS_KEY, secretAccessKey: env.IRIS_SECRET_KEY },
});
}
function validKey(key: unknown): key is string {
return typeof key === "string" && key.length > 0 && key.length <= 1024 && !key.startsWith("/") && !key.split("/").includes("..");
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
if (request.method !== "POST" || new URL(request.url).pathname !== "/presign") return new Response("Not found", { status: 404 });
let input: PresignRequest;
try {
input = await request.json<PresignRequest>();
} catch {
return json({ error: "Expected JSON body" }, 400);
}
if (!validKey(input.key)) return json({ error: "Invalid object key" }, 400);
const client = s3(env);
const operation = input.operation ?? "put";
const expiresIn = 5 * 60;
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 });
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 });
return json({ url, method: "POST", headers: { "Content-Type": contentType }, expiresIn });
}
if (!input.uploadId) return json({ error: "uploadId is required" }, 400);
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 });
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 });
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 });
return json({ url, method: "DELETE", expiresIn });
}
return json({ error: "Unsupported operation" }, 400);
},
} satisfies ExportedHandler<Env>;
+48
View File
@@ -0,0 +1,48 @@
interface PresignResponse {
url: string;
method: string;
headers?: Record<string, string>;
body?: string;
}
async function presign(body: Record<string, unknown>): Promise<PresignResponse> {
const response = await fetch("/presign", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!response.ok) throw new Error(await response.text());
return response.json<PresignResponse>();
}
function uploadIdFromXml(xml: string): string {
const value = /<UploadId>([^<]+)<\/UploadId>/.exec(xml)?.[1];
if (!value) throw new Error("CreateMultipartUpload did not return UploadId");
return value;
}
/** Uploads parts in order. I.R.I.S. 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" });
const createResponse = await fetch(create.url, { method: create.method, headers: create.headers });
if (!createResponse.ok) throw new Error(await createResponse.text());
const uploadId = uploadIdFromXml(await createResponse.text());
const parts: Array<{ ETag: string; PartNumber: number }> = [];
for (let index = 0, offset = 0; offset < file.size; index++, offset += partSize) {
const partNumber = index + 1;
const body = file.slice(offset, Math.min(offset + partSize, file.size));
const signed = await presign({ key, uploadId, partNumber, operation: "uploadPart" });
const response = await fetch(signed.url, { method: signed.method, body });
if (!response.ok) throw new Error(`Part ${partNumber} failed: ${await response.text()}`);
const etag = response.headers.get("ETag");
if (!etag) throw new Error(`Part ${partNumber} did not return ETag; check CORS_ALLOWED_ORIGINS`);
parts.push({ ETag: etag, PartNumber: partNumber });
}
const complete = await presign({ key, uploadId, parts, operation: "completeMultipart" });
if (!complete.body) throw new Error("BFF did not return the completion XML body");
const completeResponse = await fetch(complete.url, { method: complete.method, headers: complete.headers, body: complete.body });
if (!completeResponse.ok) throw new Error(await completeResponse.text());
}
+29
View File
@@ -0,0 +1,29 @@
interface PresignedPut {
url: string;
method: "PUT";
headers: { "Content-Type": string };
expiresIn: number;
}
async function getPresignedPut(key: string, contentType: string): Promise<PresignedPut> {
const response = await fetch("/presign", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ key, contentType, operation: "put" }),
});
if (!response.ok) throw new Error(await response.text());
return response.json<PresignedPut>();
}
export async function uploadFile(file: File, key: string): Promise<string | null> {
const signed = await getPresignedPut(key, file.type || "application/octet-stream");
const response = await fetch(signed.url, {
method: signed.method,
headers: signed.headers,
body: file,
});
if (!response.ok) throw new Error(`Upload failed: ${await response.text()}`);
// Requires CORS_ALLOWED_ORIGINS to include this page's exact origin.
return response.headers.get("ETag");
}
+98
View File
@@ -0,0 +1,98 @@
# Frontend integration guide
## Architecture
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
```
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.
## Enable browser access
Configure the Worker with an explicit allow-list of web origins:
```ini
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.
### Server-to-server compatibility
CORS does not restrict non-browser clients. `Access-Control-*` headers are emitted only when a request has an allowed `Origin`; a CLI, Dokploy, rclone, aws-cli, or other service request with no `Origin` keeps its normal S3 status and body. With an exact origin allow-list, responses also include `Vary: Origin`, including requests without an `Origin`, so shared caches cannot replay a no-origin object response to a browser. S3 clients ignore this cache-control metadata.
An `OPTIONS` request with no `Origin`, or a preflight from a disallowed origin, receives the same bare `204` as a CORS-disabled Worker. A disallowed `Origin` never changes the underlying S3 result—it simply receives no `Access-Control-*` headers.
## 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.
```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,
credentials: {
accessKeyId: env.IRIS_ACCESS_KEY,
secretAccessKey: env.IRIS_SECRET_KEY,
},
forcePathStyle: true,
});
const url = await getSignedUrl(
client,
new PutObjectCommand({
Bucket: "assets",
Key: "uploads/avatar.png",
ContentType: "image/png",
}),
{ expiresIn: 300 },
);
```
Keep the `ContentType` in the command and send precisely that same `Content-Type` from the browser. It becomes the Drive `mimeType`. A non-simple content type causes a preflight request, which is why the Worker CORS configuration is necessary.
## Upload one object
Use `Blob`, `File`, `ArrayBuffer`, or a typed array as the body. This lets the browser provide a known body length. Do **not** use a `ReadableStream` unless the BFF signs `x-amz-decoded-content-length` and the browser sends it: multipart part uploads require `Content-Length` or `x-amz-decoded-content-length`.
```ts
const response = await fetch(presignedUrl, {
method: "PUT",
headers: { "Content-Type": file.type || "application/octet-stream" },
body: file,
});
if (!response.ok) throw new Error(await response.text());
const etag = response.headers.get("ETag");
```
`ETag` is readable only when the request `Origin` is configured in `CORS_ALLOWED_ORIGINS`.
## Multipart upload
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.
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. Repeat one part at a time for parts 2 through $n$.
4. BFF signs `POST /bucket/key?uploadId=…`; browser sends the `CompleteMultipartUpload` XML body containing the sequential part numbers and ETags.
See [`examples/browser-multipart.ts`](./examples/browser-multipart.ts) for a complete browser-side loop.
## Build a file browser
List with `prefix` and `delimiter=/`:
```text
GET /assets?prefix=photos/&delimiter=/
```
Files appear in `Contents`; immediate child folders appear as `CommonPrefixes`. Google Drive directories are physical folders, not zero-byte marker objects. Read [limitations](./limitations.md) before designing pagination or rename/move features.
+35
View File
@@ -0,0 +1,35 @@
# Limitations
I.R.I.S. 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.
Use narrow prefixes and `delimiter=/` for a file browser. Do not build an infinite-scrolling full-drive browser.
## No server-side rename or move
`CopyObject` and `UploadPartCopy` return `501 NotImplemented`. There is no server-side rename or move because both require copying. Implement rename/move as download, re-upload under the target key, then delete the original—subject to bandwidth, time, and error handling.
## Unsupported S3 features
- `ListBuckets`, `CreateBucket`, and `DeleteBucket`
- batch `DeleteObjects`
- object versioning, ACLs, object tags, lifecycle rules, and `x-amz-meta-*` user metadata
- CopyObject and UploadPartCopy
- discovering active multipart uploads: `ListMultipartUploads` always reports an empty list
Bucket `?acl`, `?versioning`, and `?location` requests are currently treated as regular bucket listing requests rather than errors. Do not rely on this accidental compatibility.
## 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.
## Multipart behavior
Parts must be uploaded strictly in consecutive order beginning at part 1. Parts are immutable after they have committed. An incomplete upload expires after 24 hours. `ETAG_STYLE=md5` returns Google Drive's file MD5 for completed multipart objects; `ETAG_STYLE=multipart` is available only for clients that require an S3-style composite ETag.
## Quotas and plan limits
Google Drive's free tier has 15 GB total storage and a 750 GB/day upload limit. Cloudflare applies request-size limits before a request reaches the Worker; use multipart uploads for large browser uploads.
+391
View File
@@ -0,0 +1,391 @@
openapi: 3.1.0
info:
title: I.R.I.S. Google Drive S3 API
version: 1.0.0
description: |
An intentionally small S3-compatible object-storage API backed by Google Drive.
Authenticate every non-public request with AWS Signature Version 4. Browser clients
should receive short-lived presigned URLs from a backend-for-frontend, never credentials.
servers:
- url: https://{worker-host}
variables:
worker-host:
default: your-worker.example.workers.dev
security:
- sigv4: []
tags:
- name: Objects
- name: Buckets
- name: Multipart uploads
paths:
/{bucket}:
parameters:
- $ref: '#/components/parameters/Bucket'
get:
tags: [Buckets]
operationId: listObjects
summary: List objects in a bucket
description: List objects recursively, or use `delimiter=/` to return immediate folders as `CommonPrefixes`.
parameters:
- $ref: '#/components/parameters/Prefix'
- $ref: '#/components/parameters/Delimiter'
responses:
'200':
description: Object listing.
content:
application/xml:
schema: { $ref: '#/components/schemas/ListBucketResult' }
'403': { $ref: '#/components/responses/AccessDenied' }
'405': { $ref: '#/components/responses/MethodNotAllowed' }
head:
tags: [Buckets]
operationId: headBucket
summary: Check bucket access
responses:
'200': { description: The bucket is configured and accessible. }
'403': { $ref: '#/components/responses/AccessDenied' }
'405': { $ref: '#/components/responses/MethodNotAllowed' }
/{bucket}?uploads:
parameters:
- $ref: '#/components/parameters/Bucket'
get:
tags: [Multipart uploads]
operationId: listMultipartUploads
summary: List multipart uploads
description: Always returns an empty list; active multipart uploads cannot be enumerated.
responses:
'200':
description: Empty multipart upload listing.
content:
application/xml:
schema: { $ref: '#/components/schemas/ListMultipartUploadsResult' }
'403': { $ref: '#/components/responses/AccessDenied' }
/{bucket}/{key}:
parameters:
- $ref: '#/components/parameters/Bucket'
- $ref: '#/components/parameters/Key'
put:
tags: [Objects]
operationId: putObject
summary: Create or replace an object
parameters:
- $ref: '#/components/parameters/ContentType'
requestBody:
required: true
content:
application/octet-stream:
schema: { type: string, format: binary }
'*/*':
schema: { type: string, format: binary }
responses:
'200':
description: Object uploaded.
headers:
ETag: { $ref: '#/components/headers/ETag' }
'403': { $ref: '#/components/responses/AccessDenied' }
'501': { $ref: '#/components/responses/NotImplemented' }
get:
tags: [Objects]
operationId: getObject
summary: Download an object
parameters:
- name: Range
in: header
schema: { type: string, example: bytes=0-1023 }
responses:
'200':
description: Full object data.
headers:
ETag: { $ref: '#/components/headers/ETag' }
Content-Length: { $ref: '#/components/headers/ContentLength' }
Last-Modified: { $ref: '#/components/headers/LastModified' }
Accept-Ranges: { schema: { type: string, example: bytes } }
content:
application/octet-stream:
schema: { type: string, format: binary }
'206':
description: Requested byte range.
headers:
Content-Range: { schema: { type: string, example: bytes 0-1023/4096 } }
Content-Length: { $ref: '#/components/headers/ContentLength' }
content:
application/octet-stream:
schema: { type: string, format: binary }
'403': { $ref: '#/components/responses/AccessDenied' }
'404': { $ref: '#/components/responses/NoSuchKey' }
head:
tags: [Objects]
operationId: headObject
summary: Get object metadata
responses:
'200':
description: Object metadata in headers.
headers:
ETag: { $ref: '#/components/headers/ETag' }
Content-Length: { $ref: '#/components/headers/ContentLength' }
Last-Modified: { $ref: '#/components/headers/LastModified' }
Accept-Ranges: { schema: { type: string, example: bytes } }
'403': { $ref: '#/components/responses/AccessDenied' }
'404': { $ref: '#/components/responses/NoSuchKey' }
delete:
tags: [Objects]
operationId: deleteObject
summary: Delete an object
responses:
'204': { description: Object deleted. }
'403': { $ref: '#/components/responses/AccessDenied' }
'404': { $ref: '#/components/responses/NoSuchKey' }
/{bucket}/{key}?uploads:
parameters:
- $ref: '#/components/parameters/Bucket'
- $ref: '#/components/parameters/Key'
post:
tags: [Multipart uploads]
operationId: createMultipartUpload
summary: Initiate a multipart upload
parameters:
- $ref: '#/components/parameters/ContentType'
responses:
'200':
description: Upload initiated.
content:
application/xml:
schema: { $ref: '#/components/schemas/InitiateMultipartUploadResult' }
'403': { $ref: '#/components/responses/AccessDenied' }
'501': { $ref: '#/components/responses/NotImplemented' }
/{bucket}/{key}?uploadId={uploadId}&partNumber={partNumber}:
parameters:
- $ref: '#/components/parameters/Bucket'
- $ref: '#/components/parameters/Key'
- $ref: '#/components/parameters/UploadId'
- $ref: '#/components/parameters/PartNumber'
put:
tags: [Multipart uploads]
operationId: uploadPart
summary: Upload one multipart part
description: Parts must arrive strictly sequentially beginning with part number 1.
requestBody:
required: true
content:
application/octet-stream:
schema: { type: string, format: binary }
responses:
'200':
description: Part accepted.
headers:
ETag: { $ref: '#/components/headers/ETag' }
'400': { $ref: '#/components/responses/InvalidArgument' }
'403': { $ref: '#/components/responses/AccessDenied' }
'404': { $ref: '#/components/responses/NoSuchUpload' }
'503': { $ref: '#/components/responses/SlowDown' }
/{bucket}/{key}?uploadId={uploadId}:
parameters:
- $ref: '#/components/parameters/Bucket'
- $ref: '#/components/parameters/Key'
- $ref: '#/components/parameters/UploadId'
post:
tags: [Multipart uploads]
operationId: completeMultipartUpload
summary: Complete a multipart upload
requestBody:
required: true
content:
application/xml:
schema: { $ref: '#/components/schemas/CompleteMultipartUpload' }
responses:
'200':
description: Upload completed.
content:
application/xml:
schema: { $ref: '#/components/schemas/CompleteMultipartUploadResult' }
'400': { $ref: '#/components/responses/InvalidArgument' }
'403': { $ref: '#/components/responses/AccessDenied' }
'404': { $ref: '#/components/responses/NoSuchUpload' }
get:
tags: [Multipart uploads]
operationId: listParts
summary: List uploaded parts
parameters:
- name: part-number-marker
in: query
schema: { type: integer, minimum: 0, default: 0 }
- name: max-parts
in: query
schema: { type: integer, minimum: 1, maximum: 1000, default: 1000 }
responses:
'200':
description: Current part list.
content:
application/xml:
schema: { $ref: '#/components/schemas/ListPartsResult' }
'400': { $ref: '#/components/responses/InvalidArgument' }
'403': { $ref: '#/components/responses/AccessDenied' }
'404': { $ref: '#/components/responses/NoSuchUpload' }
delete:
tags: [Multipart uploads]
operationId: abortMultipartUpload
summary: Abort a multipart upload
responses:
'204': { description: Upload aborted. }
'403': { $ref: '#/components/responses/AccessDenied' }
'404': { $ref: '#/components/responses/NoSuchUpload' }
components:
securitySchemes:
sigv4:
type: apiKey
in: header
name: Authorization
description: AWS Signature Version 4 header authentication or equivalent `X-Amz-*` presigned query parameters.
parameters:
Bucket:
name: bucket
in: path
required: true
schema: { type: string }
Key:
name: key
in: path
required: true
schema: { type: string }
Prefix:
name: prefix
in: query
schema: { type: string, default: '' }
Delimiter:
name: delimiter
in: query
schema: { type: string, example: / }
UploadId:
name: uploadId
in: query
required: true
schema: { type: string }
PartNumber:
name: partNumber
in: query
required: true
schema: { type: integer, minimum: 1, maximum: 10000 }
ContentType:
name: Content-Type
in: header
schema: { type: string, example: application/octet-stream }
headers:
ETag:
schema: { type: string, example: '"d41d8cd98f00b204e9800998ecf8427e"' }
ContentLength:
schema: { type: integer, minimum: 0 }
LastModified:
schema: { type: string, format: date-time }
responses:
AccessDenied:
description: Authentication failed, the access key is not recognized, the presigned URL expired, or bucket access is denied.
content: { application/xml: { schema: { $ref: '#/components/schemas/S3Error' } } }
NoSuchKey:
description: The object does not exist.
content: { application/xml: { schema: { $ref: '#/components/schemas/S3Error' } } }
NoSuchUpload:
description: The multipart upload does not exist.
content: { application/xml: { schema: { $ref: '#/components/schemas/S3Error' } } }
InvalidArgument:
description: A required parameter or body is invalid.
content: { application/xml: { schema: { $ref: '#/components/schemas/S3Error' } } }
NotImplemented:
description: This S3 operation is not supported.
content: { application/xml: { schema: { $ref: '#/components/schemas/S3Error' } } }
SlowDown:
description: Parts are out of sequence or the upload is busy; retry later.
content: { application/xml: { schema: { $ref: '#/components/schemas/S3Error' } } }
MethodNotAllowed:
description: The method/path combination is unsupported.
content: { application/xml: { schema: { $ref: '#/components/schemas/S3Error' } } }
schemas:
S3Error:
type: object
xml: { name: Error }
required: [Code, Message, RequestId]
properties:
Code: { type: string, xml: { name: Code } }
Message: { type: string, xml: { name: Message } }
Resource: { type: string, xml: { name: Resource } }
RequestId: { type: string, xml: { name: RequestId } }
Object:
type: object
xml: { name: Contents }
properties:
Key: { type: string }
LastModified: { type: string, format: date-time }
ETag: { type: string }
Size: { type: integer }
StorageClass: { type: string, example: STANDARD }
ListBucketResult:
type: object
xml: { name: ListBucketResult, namespace: 'http://s3.amazonaws.com/doc/2006-03-01/' }
properties:
Name: { type: string }
Prefix: { type: string }
Delimiter: { type: string }
MaxKeys: { type: integer, example: 1000 }
IsTruncated: { type: boolean }
Contents: { type: array, items: { $ref: '#/components/schemas/Object' }, xml: { wrapped: false } }
InitiateMultipartUploadResult:
type: object
xml: { name: InitiateMultipartUploadResult, namespace: 'http://s3.amazonaws.com/doc/2006-03-01/' }
required: [Bucket, Key, UploadId]
properties:
Bucket: { type: string }
Key: { type: string }
UploadId: { type: string }
CompleteMultipartUpload:
type: object
xml: { name: CompleteMultipartUpload }
required: [Part]
properties:
Part:
type: array
minItems: 1
xml: { wrapped: false }
items:
type: object
xml: { name: Part }
required: [PartNumber, ETag]
properties:
PartNumber: { type: integer }
ETag: { type: string }
CompleteMultipartUploadResult:
type: object
xml: { name: CompleteMultipartUploadResult, namespace: 'http://s3.amazonaws.com/doc/2006-03-01/' }
properties:
Location: { type: string }
Bucket: { type: string }
Key: { type: string }
ETag: { type: string }
ListPartsResult:
type: object
xml: { name: ListPartsResult, namespace: 'http://s3.amazonaws.com/doc/2006-03-01/' }
properties:
Bucket: { type: string }
Key: { type: string }
UploadId: { type: string }
NextPartNumberMarker: { type: integer }
IsTruncated: { type: boolean }
Part:
type: array
xml: { wrapped: false }
items:
type: object
properties:
PartNumber: { type: integer }
LastModified: { type: string, format: date-time }
ETag: { type: string }
Size: { type: integer }
ListMultipartUploadsResult:
type: object
xml: { name: ListMultipartUploadsResult, namespace: 'http://s3.amazonaws.com/doc/2006-03-01/' }
properties:
Bucket: { type: string }
KeyMarker: { type: string }
UploadIdMarker: { type: string }
NextKeyMarker: { type: string }
NextUploadIdMarker: { type: string }
MaxUploads: { type: integer, example: 1000 }
IsTruncated: { type: boolean, example: false }