openapi: 3.1.0 info: title: 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 - name: Dashboard Auth - name: Dashboard Status paths: /auth/login: post: tags: [Dashboard Auth] operationId: authLogin summary: Login to dashboard description: Authenticate with SHA-256 hashed dashboard password. security: [] requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/LoginRequest' } responses: '200': description: Login successful. content: application/json: schema: { $ref: '#/components/schemas/LoginResponse' } '400': description: Invalid request. content: application/json: schema: { $ref: '#/components/schemas/AuthError' } '401': description: Invalid password. content: application/json: schema: { $ref: '#/components/schemas/AuthError' } '429': description: Too many failed attempts. content: application/json: schema: { $ref: '#/components/schemas/AuthError' } '503': description: Dashboard authentication not configured. content: application/json: schema: { $ref: '#/components/schemas/AuthError' } /auth/session: get: tags: [Dashboard Auth] operationId: authSession summary: Verify session token security: - bearerAuth: [] responses: '200': description: Session is valid. content: application/json: schema: type: object properties: valid: { type: boolean, example: true } '401': description: Invalid or expired session. content: application/json: schema: { $ref: '#/components/schemas/AuthError' } '503': description: Dashboard authentication not configured. content: application/json: schema: { $ref: '#/components/schemas/AuthError' } /auth/logout: post: tags: [Dashboard Auth] operationId: authLogout summary: Logout of dashboard session security: - bearerAuth: [] responses: '204': description: Successfully logged out. '503': description: Dashboard authentication not configured. content: application/json: schema: { $ref: '#/components/schemas/AuthError' } /api/status: get: tags: [Dashboard Status] operationId: getGatewayStatus summary: Get gateway and storage health status security: - bearerAuth: [] responses: '200': description: Gateway status and Drive account/quota overview. content: application/json: schema: { $ref: '#/components/schemas/GatewayStatus' } '401': description: Invalid or expired session. content: application/json: schema: { $ref: '#/components/schemas/AuthError' } '503': description: Dashboard authentication not configured. content: application/json: schema: { $ref: '#/components/schemas/AuthError' } /api/buckets: get: tags: [Dashboard Status] operationId: getBucketStats summary: Get bucket list and object metrics security: - bearerAuth: [] parameters: - name: refresh in: query description: Set to 1 to bypass KV cache. required: false schema: { type: string, example: "1" } responses: '200': description: Bucket statistics and aggregate totals. content: application/json: schema: { $ref: '#/components/schemas/BucketStats' } '401': description: Invalid or expired session. content: application/json: schema: { $ref: '#/components/schemas/AuthError' } '503': description: Storage root folder or authentication not configured. content: application/json: schema: { $ref: '#/components/schemas/AuthError' } post: tags: [Dashboard Status] operationId: createBucket summary: Create a new bucket security: - bearerAuth: [] requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/CreateBucketRequest' } responses: '201': description: Bucket created. content: application/json: schema: { $ref: '#/components/schemas/BucketRecord' } '400': description: Invalid bucket name. content: application/json: schema: { $ref: '#/components/schemas/AuthError' } '401': description: Invalid or expired session. content: application/json: schema: { $ref: '#/components/schemas/AuthError' } '409': description: Bucket already exists. content: application/json: schema: { $ref: '#/components/schemas/AuthError' } '503': description: Storage root folder or authentication not configured. content: application/json: schema: { $ref: '#/components/schemas/AuthError' } /api/buckets/{name}: parameters: - name: name in: path required: true schema: { type: string } patch: tags: [Dashboard Status] operationId: updateBucket summary: Update bucket settings (toggle public read or rename) security: - bearerAuth: [] requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/UpdateBucketRequest' } responses: '200': description: Bucket updated. content: application/json: schema: { $ref: '#/components/schemas/BucketRecord' } '400': description: Invalid request. content: application/json: schema: { $ref: '#/components/schemas/AuthError' } '401': description: Invalid or expired session. content: application/json: schema: { $ref: '#/components/schemas/AuthError' } '404': description: Bucket not found. content: application/json: schema: { $ref: '#/components/schemas/AuthError' } '409': description: Target bucket name conflict. content: application/json: schema: { $ref: '#/components/schemas/AuthError' } delete: tags: [Dashboard Status] operationId: deleteBucket summary: Delete an empty bucket security: - bearerAuth: [] responses: '204': description: Bucket deleted. '401': description: Invalid or expired session. content: application/json: schema: { $ref: '#/components/schemas/AuthError' } '404': description: Bucket not found. content: application/json: schema: { $ref: '#/components/schemas/AuthError' } '409': description: Bucket is not empty. content: application/json: schema: { $ref: '#/components/schemas/AuthError' } /api/import-candidates: get: tags: [Dashboard Status] operationId: listImportCandidates summary: List Drive folders available for import security: - bearerAuth: [] responses: '200': description: List of folders under Drive root. content: application/json: schema: { $ref: '#/components/schemas/ImportCandidatesResponse' } '401': description: Invalid or expired session. content: application/json: schema: { $ref: '#/components/schemas/AuthError' } /api/import: post: tags: [Dashboard Status] operationId: importBuckets summary: Import folders from Drive root into storage root folder security: - bearerAuth: [] requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/ImportBucketsRequest' } responses: '200': description: Import results. content: application/json: schema: { $ref: '#/components/schemas/ImportResult' } '400': description: Invalid body. content: application/json: schema: { $ref: '#/components/schemas/AuthError' } '401': description: Invalid or expired session. content: application/json: schema: { $ref: '#/components/schemas/AuthError' } /{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. bearerAuth: type: http scheme: bearer description: Session token returned from `/auth/login`. 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: LoginRequest: type: object required: [passwordHash] properties: passwordHash: type: string description: SHA-256 hash of the dashboard password in lowercase hex. example: ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f LoginResponse: type: object required: [token, expiresIn] properties: token: type: string description: Session token. expiresIn: type: integer description: Token TTL in seconds. example: 43200 GatewayStatus: type: object required: [gateway, drive, checkedAt] properties: gateway: type: object required: [status, region, multipartEnabled, etagStyle, docsEnabled, buckets, publicReadBuckets, rootFolder, corsOrigins, credentials] properties: status: { type: string, enum: [ok, degraded] } region: { type: string, example: auto } multipartEnabled: { type: boolean } etagStyle: { type: string, enum: [md5, multipart] } docsEnabled: { type: boolean } buckets: { type: array, items: { type: string } } publicReadBuckets: { type: array, items: { type: string } } rootFolder: type: object required: [name, id, configured] properties: name: { type: string, nullable: true } id: { type: string, nullable: true } configured: { type: boolean } corsOrigins: { type: array, items: { type: string } } credentials: type: object required: [s3Keys, googleOAuth, dashboardPassword] properties: s3Keys: { type: boolean } googleOAuth: { type: boolean } dashboardPassword: { type: boolean } drive: type: object required: [connected, account, quota, error] properties: connected: { type: boolean } account: type: object nullable: true properties: email: { type: string, nullable: true } displayName: { type: string, nullable: true } quota: type: object nullable: true properties: limit: { type: integer, nullable: true } usage: { type: integer } usageInDrive: { type: integer } usageInDriveTrash: { type: integer } free: { type: integer, nullable: true } percentUsed: { type: number, nullable: true } error: { type: string, nullable: true } checkedAt: type: string format: date-time BucketStats: type: object required: [buckets, totals, cachedAt] properties: buckets: type: array items: type: object required: [name, objectCount, totalSize, lastModified, truncated, publicRead, error] properties: name: { type: string } objectCount: { type: integer } totalSize: { type: integer } lastModified: { type: string, format: date-time, nullable: true } truncated: { type: boolean } publicRead: { type: boolean } error: { type: string, nullable: true } totals: type: object required: [buckets, objectCount, totalSize] properties: buckets: { type: integer } objectCount: { type: integer } totalSize: { type: integer } cachedAt: type: string format: date-time BucketRecord: type: object required: [name, folderId, publicRead, createdTime] properties: name: { type: string, example: assets } folderId: { type: string, example: 1A2b3C4d5E6f } publicRead: { type: boolean, example: false } createdTime: { type: string, format: date-time, nullable: true } CreateBucketRequest: type: object required: [name] properties: name: { type: string, example: assets } publicRead: { type: boolean, default: false } UpdateBucketRequest: type: object properties: publicRead: { type: boolean } name: { type: string } ImportCandidate: type: object required: [name, folderId, objectCount] properties: name: { type: string, example: legacy-photos } folderId: { type: string, example: 1A2b3C4d5E6f } objectCount: { type: integer, example: 42 } ImportCandidatesResponse: type: object required: [candidates] properties: candidates: type: array items: { $ref: '#/components/schemas/ImportCandidate' } ImportBucketsRequest: type: object required: [names] properties: names: type: array items: { type: string } ImportResult: type: object required: [imported, failed] properties: imported: type: array items: { type: string } failed: type: array items: type: object required: [name, error] properties: name: { type: string } error: { type: string } AuthError: type: object required: [message] properties: message: type: string example: Invalid password 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 }