Files

263 lines
6.4 KiB
Markdown

# File API
The file API creates workflow-scoped metadata records and issues upload and download URLs. The
workflow-visible handle is `conductor://file/<file-id>`; route path variables use the bare
`file-id`.
All routes below are relative to the Conductor server URL.
## Handles and workflow scope
Every file has an owning workflow.
- Creating a file and every upload mutation require the exact owning `workflowId`.
- Metadata and downloads accept a workflow in the owner's workflow family, including parent and
child workflows.
- A file does not become downloadable until upload completion records it as `UPLOADED`.
## Single-request upload
### 1. Create a file record
```http
POST /api/files
Content-Type: application/json
{
"workflowId": "3a5b8c2d-1234-5678-9abc-def012345678",
"taskId": "optional-task-id",
"fileName": "report.pdf",
"contentType": "application/pdf"
}
```
`workflowId` is required. `fileName`, `contentType`, and `taskId` are optional metadata.
The response is `201 Created` with this shape:
```json
{
"fileHandleId": "conductor://file/<file-id>",
"fileName": "report.pdf",
"contentType": "application/pdf",
"storageType": "CONDUCTOR",
"uploadStatus": "UPLOADING",
"uploadUrl": "<backend upload URL>",
"uploadUrlExpiresAt": 0,
"createdAt": 0
}
```
`uploadUrlExpiresAt` and `createdAt` are epoch milliseconds. The `storageType` value reflects the
configured backend; it is `CONDUCTOR` for Conductor-managed storage.
### 2. Upload the bytes
Send the bytes to `uploadUrl`. The transfer protocol depends on the backend:
- `conductor`: the URL points to Conductor's raw content endpoint. See
[Conductor-managed raw content](#conductor-managed-raw-content).
- `s3`, `azure-blob`, and `gcs`: the URL is a provider URL and the client transfers directly to
that object store.
### 3. Refresh an expired upload URL
```http
GET /api/files/{workflowId}/{fileId}/upload-url
```
The response is `200 OK`:
```json
{
"fileHandleId": "conductor://file/<file-id>",
"uploadUrl": "<backend upload URL>",
"expiresAt": 0
}
```
`expiresAt` is epoch milliseconds.
### 4. Confirm the upload
After the bytes are stored, confirm the upload:
```http
POST /api/files/{workflowId}/{fileId}/upload-complete
```
The response is `200 OK`:
```json
{
"fileHandleId": "conductor://file/<file-id>",
"uploadStatus": "UPLOADED",
"contentHash": "<backend content hash>"
}
```
## Conductor-managed raw content
When `conductor.file-storage.type=conductor`, the upload and download URL use Conductor's raw
content routes. The request and response bodies are raw file bytes, not JSON.
### Upload content
```http
PUT /api/files/content/{workflowId}/{fileId}
<raw file bytes>
```
The exact response is:
```http
204 No Content
```
There is no response body. The owning workflow ID is required; a different workflow cannot upload
or overwrite the file.
### Download content
```http
GET /api/files/content/{workflowId}/{fileId}
```
The exact successful response shape is:
```http
200 OK
Content-Type: <the stored file content type>
Content-Length: <the stored file size>
<raw file bytes>
```
The controller streams the response body from storage. It sets `Content-Type` from the file
metadata and `Content-Length` from the stored file size. The file must already be `UPLOADED`.
If content URL signing is enabled, use the signed URL returned by the create, refresh, or download
URL endpoint. The signed content URL contains `op`, `exp`, `kid`, and `sig`; do not log it.
## Read metadata
```http
GET /api/files/{workflowId}/{fileId}
```
The response is `200 OK`:
```json
{
"fileHandleId": "conductor://file/<file-id>",
"fileName": "report.pdf",
"contentType": "application/pdf",
"fileSize": 0,
"contentHash": "<backend content hash>",
"storageType": "CONDUCTOR",
"uploadStatus": "UPLOADED",
"workflowId": "<owning workflow id>",
"taskId": "optional-task-id",
"createdAt": 0,
"updatedAt": 0
}
```
`fileSize`, `createdAt`, and `updatedAt` are numeric values; timestamps are epoch milliseconds.
## Download
### 1. Get a download URL
```http
GET /api/files/{workflowId}/{fileId}/download-url
```
The response is `200 OK`:
```json
{
"fileHandleId": "conductor://file/<file-id>",
"downloadUrl": "<backend download URL>",
"expiresAt": 0
}
```
For the `conductor` backend, `downloadUrl` is the raw `GET /api/files/content/{workflowId}/{fileId}`
endpoint. For object-store backends, it is a provider URL.
### 2. Download the bytes
Issue a `GET` to `downloadUrl`. For Conductor-managed storage, the response is the raw-stream shape
shown above. For object-store backends, follow the provider's signed-URL contract.
## Multipart upload
Multipart is available only for backends that implement it. S3 and Azure Blob Storage support the
multipart routes; `conductor` and GCS do not. Do not start a multipart session for the `conductor`
backend: Conductor-managed uploads are single-request and bounded by
`conductor.file-storage.conductor.max-size`.
### 1. Initiate
```http
POST /api/files/{workflowId}/{fileId}/multipart
```
The response is `200 OK`:
```json
{
"fileHandleId": "conductor://file/<file-id>",
"uploadId": "<backend multipart upload id>"
}
```
### 2. Get a URL for each part
```http
GET /api/files/{workflowId}/{fileId}/multipart/{uploadId}/part/{partNumber}
```
The response uses the upload URL shape:
```json
{
"fileHandleId": "conductor://file/<file-id>",
"uploadUrl": "<provider part upload URL>",
"expiresAt": 0
}
```
### 3. Complete
```http
POST /api/files/{workflowId}/{fileId}/multipart/{uploadId}/complete
Content-Type: application/json
{
"partETags": ["<part 1 token>", "<part 2 token>"]
}
```
The response uses the upload-complete shape shown above.
### Abort a failed session
```http
DELETE /api/files/{workflowId}/{fileId}/multipart/{uploadId}
```
The response is `204 No Content`.
## Errors
| Status | Meaning |
|---|---|
| `400 Bad Request` | Invalid request, an upload is not complete, or multipart is unsupported by the selected backend. |
| `403 Forbidden` | The workflow does not have the required owner or family access; a signed content URL is invalid or expired when signing is enabled. |
| `404 Not Found` | The file ID does not exist, or file storage is not enabled. |
| `405 Method Not Allowed` | A signed URL was used with the wrong operation, such as a download URL for a `PUT`. |
| `413 Payload Too Large` | A Conductor-managed upload exceeds `conductor.file-storage.conductor.max-size`. |