Skip to content

Commit a77d81c

Browse files
committed
WC-2683 Provide documentation for asset upload
1 parent b50b5a0 commit a77d81c

File tree

1 file changed

+355
-0
lines changed

1 file changed

+355
-0
lines changed
Lines changed: 355 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,355 @@
1+
---
2+
pcx_content_type: concept
3+
title: Direct Uploads
4+
sidebar:
5+
order: 11
6+
head: []
7+
description: Upload assets through the Workers API.
8+
---
9+
10+
import {
11+
Badge,
12+
Description,
13+
FileTree,
14+
InlineBadge,
15+
Render,
16+
TabItem,
17+
Tabs,
18+
} from "~/components";
19+
20+
The Workers API empowers users to upload and serve static assets. Users can also fetch assets through an optional [assets binding](/workers/static-assets/binding/). This guide will describe the process for attaching assets to your Worker directly through the Workers API.
21+
22+
```mermaid
23+
sequenceDiagram
24+
participant User
25+
participant Workers API
26+
User<<->>Workers API: Submit manifest<br/>POST /client/v4/accounts/:accountId/workers/scripts/:scriptName/assets-upload-session
27+
User<<->>Workers API: Upload files<br/>POST /client/v4/accounts/:accountId/workers/assets/upload?base64=true
28+
User<<->>Workers API: Upload script version<br/>PUT /client/v4/accounts/:accountId/workers/scripts/:scriptName
29+
```
30+
31+
The asset upload flow can be distilled into three distinct phases: registration of a manifest, uploading of the assets, and deployment of the Worker.
32+
33+
## `Upload manifest`
34+
35+
The asset manifest is a ledger which keeps track of files we want to use in our Worker. This manifest is used to track assets associated with each Worker version, and eliminate the need to upload unchanged files prior to a new upload.
36+
37+
The manifest upload request describes each file which we intend to upload. Each file is its own key representing the file path and name, and is an object which contains metadata about the file.
38+
39+
`hash` represents the first 32 characters of the file’s hash, while `size` is the size (in bytes) of the file.
40+
41+
```bash title="Example manifest upload request"
42+
curl -X POST https://api.cloudflare.com/client/v4/accounts/:accountId/workers/scripts/:scriptName/assets-upload-session \
43+
--header 'content-type: application/json' \
44+
--header 'Authorization: Bearer API_TOKEN' \
45+
--data '{ "manifest": { "/filea.html": { "hash": "08f1dfda4574284ab3c21666d1", "size": 12 }, "/fileb.html": { "hash": "4f1c1af44620d531446ceef93f", "size": 23 } } }'
46+
```
47+
48+
The resulting response will contain a JWT, which provides authentication during file upload. The JWT is valid for one hour.
49+
50+
In addition to the JWT, the response instructs users how to most optimally batch upload their files. These instructions are encoded in the “buckets” field. Each array in buckets contains a list of file hashes which should be uploaded together.
51+
52+
```bash title="Example manifest upload response"
53+
{
54+
"result": {
55+
"jwt": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3N0YWdpbmcuYXBpLndvcmtlcnMuY2xvdWRmbGFyZS5jb20iLCJhdWQiOiJodHRwczovL3N0YWdpbmcuYXBpLndvcmtlcnMuY2xvdWRmbGFyZS5jb20iLCJleHAiOjE3MzA4MzY4MzksIm5iZiI6MTczMDgzMzIzOSwiaWF0IjoxNzMwODMzMjM5LCJtYW5pZmVzdF9pZCI6IjhkNzRmMzQ0LTcyNmQtNDY1Yi04MjE0LTJmYjMwMTE5NWE5YSJ9.Z3g0RNYshjMFHnM8o4X15PRpIAkgkuztIKOfmEW3ZfkmrDFykZFizclBGJKFoP0eZ1N9ODyFNUkfeikjUDvkAQ",
56+
"buckets": [
57+
[
58+
"08f1dfda4574284ab3c21666d1",
59+
"4f1c1af44620d531446ceef93f"
60+
]
61+
]
62+
},
63+
"success": true,
64+
"errors": null,
65+
"messages": null
66+
}
67+
```
68+
69+
### `Limitations`
70+
71+
- Each file must be under 25 MiB
72+
- The overall manifest must not contain more than 20,000 file entries
73+
74+
## `Upload Static Assets`
75+
76+
Users can upload assets by POSTing to https://api.cloudflare.com/client/v4/accounts/:accountID/workers/assets/upload?base64=true. This endpoint requires files be uploaded using multipart/form data. The contents of each file must be base64 encoded, and the `base64` query param must be set to `true`.
77+
78+
The authorization header must be provided as a bearer token, using the jwt from the aforementioned manifest upload call.
79+
80+
Once every file in the manifest has been uploaded, a status code of 201 will be returned, with the `jwt` field present, and containing a final "completion" token which can be used in a deployment of a Worker with assets. This token is valid for 1 hour.
81+
82+
## `Create/Deploy New Version`
83+
84+
[Script](https://developers.cloudflare.com/api/operations/worker-script-upload-worker-module) and [version](https://developers.cloudflare.com/api/operations/worker-versions-upload-version) upload endpoints require specifying a metadata part in the form data. If this is a new Worker, we can provide the completion token from the previous (upload assets) step. This token will be validated by the backend to ensure that all assets have been uploaded.
85+
86+
```bash title="Example Worker Metadata Specifying Completion Token"
87+
{
88+
"main_module": "main.js",
89+
"assets": {
90+
"jwt": "<completion_token>"
91+
},
92+
"compatibility_date": "2021-09-14"
93+
}
94+
```
95+
96+
If this is a worker which already exists with assets, and you wish to just re-use the existing set of assets, we do not have to specify the completion token again. Instead, we can provide the `keep_assets` metadata configuration.
97+
98+
```bash title="Example Worker Metadata Specifying keep_assets"
99+
{
100+
"main_module": "main.js",
101+
"keep_assets": true,
102+
"compatibility_date": "2021-09-14"
103+
}
104+
```
105+
106+
Asset [routing configuration](/workers/static-assets/routing/#routing-configuration) can be provided in the assets object, such as html_handling and not_found_handling.
107+
108+
```bash title="Example Worker Metadata Specifying Asset Configuration"
109+
{
110+
"main_module": "main.js",
111+
"assets": {
112+
"jwt": "<completion_token>",
113+
"config" {
114+
"html_handling": "auto-trailing-slash"
115+
}
116+
},
117+
"compatibility_date": "2021-09-14"
118+
}
119+
```
120+
121+
Optionally, an asset binding can be provided if you wish to fetch and serve assets from within your Worker.
122+
123+
```bash title="Example Worker Metadata Specifying Asset Binding"
124+
{
125+
"main_module": "main.js",
126+
"assets": {
127+
...
128+
},
129+
"bindings": [
130+
...
131+
{
132+
"name": "ASSETS",
133+
"type": "assets"
134+
}
135+
...
136+
]
137+
"compatibility_date": "2021-09-14"
138+
}
139+
```
140+
141+
## `Programmatic Example`
142+
143+
<Tabs> <TabItem label="TypeScript" icon="seti:typescript">
144+
145+
```ts
146+
import * as fs from "fs";
147+
import * as path from "path";
148+
import * as crypto from "crypto";
149+
import { FormData, fetch } from "undici";
150+
151+
const accountId: string = ""; // Replace with your actual account ID
152+
const authToken: string = ""; // Replace with your actual API token
153+
const filesDirectory: string = "assets"; // Adjust to your assets directory
154+
const scriptName: string = "my-new-script"; // Replace with desired script name
155+
156+
interface FileMetadata {
157+
hash: string;
158+
size: number;
159+
}
160+
161+
interface UploadSessionData {
162+
uploadToken: string;
163+
buckets: string[][];
164+
fileMetadata: Record<string, FileMetadata>;
165+
}
166+
167+
interface UploadResponse {
168+
result: {
169+
jwt: string;
170+
buckets: string[][];
171+
};
172+
success: boolean;
173+
errors: any;
174+
messages: any;
175+
}
176+
177+
// Function to calculate the SHA-256 hash of a file and truncate to 32 bits
178+
function calculateFileHash(filePath: string): {
179+
fileHash: string;
180+
fileSize: number;
181+
} {
182+
const hash = crypto.createHash("sha256");
183+
const fileBuffer = fs.readFileSync(filePath);
184+
hash.update(fileBuffer);
185+
const fileHash = hash.digest("hex").slice(0, 32); // Grab the first 32 characters
186+
const fileSize = fileBuffer.length;
187+
return { fileHash, fileSize };
188+
}
189+
190+
// Function to gather file metadata for all files in the directory
191+
function gatherFileMetadata(directory: string): Record<string, FileMetadata> {
192+
const files = fs.readdirSync(directory);
193+
const fileMetadata: Record<string, FileMetadata> = {};
194+
195+
files.forEach((file) => {
196+
const filePath = path.join(directory, file);
197+
const { fileHash, fileSize } = calculateFileHash(filePath);
198+
fileMetadata["/" + file] = {
199+
hash: fileHash,
200+
size: fileSize,
201+
};
202+
});
203+
204+
return fileMetadata;
205+
}
206+
207+
function findMatch(
208+
fileHash: string,
209+
fileMetadata: Record<string, FileMetadata>,
210+
): string {
211+
for (let prop in fileMetadata) {
212+
const file = fileMetadata[prop] as FileMetadata;
213+
if (file.hash === fileHash) {
214+
return prop;
215+
}
216+
}
217+
throw new Error("unknown fileHash");
218+
}
219+
220+
// Function to upload a batch of files using the JWT from the first response
221+
async function uploadFilesBatch(
222+
jwt: string,
223+
fileHashes: string[][],
224+
fileMetadata: Record<string, FileMetadata>,
225+
): Promise<string> {
226+
const form = new FormData();
227+
228+
fileHashes.forEach(async (bucket) => {
229+
bucket.forEach((fileHash) => {
230+
const fullPath = findMatch(fileHash, fileMetadata);
231+
const relPath = filesDirectory + "/" + path.basename(fullPath);
232+
const fileBuffer = fs.readFileSync(relPath);
233+
const base64Data = fileBuffer.toString("base64"); // Convert file to Base64
234+
235+
form.append(
236+
fileHash,
237+
new File([base64Data], fileHash, {
238+
type: "text/html", // Modify Content-Type header based on type of file
239+
}),
240+
fileHash,
241+
);
242+
});
243+
244+
const response = await fetch(
245+
`https://api.cloudflare.com/client/v4/accounts/${accountId}/workers/assets/upload?base64=true`,
246+
{
247+
method: "POST",
248+
headers: {
249+
Authorization: `Bearer ${jwt}`,
250+
},
251+
body: form,
252+
},
253+
);
254+
255+
const data = (await response.json()) as UploadResponse;
256+
if (data && data.result.jwt) {
257+
return { completionToken: data.result.jwt };
258+
}
259+
});
260+
261+
throw new Error("Should have received completion token");
262+
}
263+
264+
async function scriptUpload(completionToken: string): Promise<void> {
265+
const form = new FormData();
266+
267+
// Configure metadata
268+
form.append(
269+
"metadata",
270+
JSON.stringify({
271+
main_module: "index.mjs",
272+
compatibility_date: "2022-03-11",
273+
assets: {
274+
jwt: completionToken, // Provide the completion token from file uploads
275+
},
276+
bindings: [{ name: "ASSETS", type: "assets" }], // Optional assets binding to fetch from user worker
277+
}),
278+
);
279+
280+
// Configure (optional) user worker
281+
form.append(
282+
"@index.js",
283+
new File(
284+
[
285+
"export default {async fetch(request, env) { return new Response('Hello world from user worker!'); }}",
286+
],
287+
"index.mjs",
288+
{
289+
type: "application/javascript+module",
290+
},
291+
),
292+
);
293+
294+
const response = await fetch(
295+
`https://api.cloudflare.com/client/v4/accounts/${accountId}/workers/scripts/${scriptName}`,
296+
{
297+
method: "PUT",
298+
headers: {
299+
Authorization: `Bearer ${authToken}`,
300+
},
301+
body: form,
302+
},
303+
);
304+
305+
if (response.status != 200) {
306+
throw new Error("unexpected status code");
307+
}
308+
}
309+
310+
// Function to make the POST request to start the assets upload session
311+
async function startUploadSession(): Promise<UploadSessionData> {
312+
const fileMetadata = gatherFileMetadata(filesDirectory);
313+
314+
const requestBody = JSON.stringify({
315+
manifest: fileMetadata,
316+
});
317+
318+
const response = await fetch(
319+
`https://api.cloudflare.com/client/v4/accounts/${accountId}/workers/scripts/${scriptName}/assets-upload-session`,
320+
{
321+
method: "POST",
322+
headers: {
323+
Authorization: `Bearer ${authToken}`,
324+
"Content-Type": "application/json",
325+
"Content-Length": Buffer.byteLength(requestBody).toString(),
326+
},
327+
body: requestBody,
328+
},
329+
);
330+
331+
const data = (await response.json()) as UploadResponse;
332+
const jwt = data.result.jwt;
333+
334+
return {
335+
uploadToken: jwt,
336+
buckets: data.result.buckets,
337+
fileMetadata,
338+
};
339+
}
340+
341+
// Begin the upload session by uploading a new manifest
342+
const { uploadToken, buckets, fileMetadata } = await startUploadSession();
343+
344+
// If all files are already uploaded, a completion token will be immediately returned. Otherwise,
345+
// we should upload the missing files
346+
let completionToken = uploadToken;
347+
if (buckets.length > 0) {
348+
completionToken = await uploadFilesBatch(uploadToken, buckets, fileMetadata);
349+
}
350+
351+
// Once we have uploaded all of our files, we can upload a new script, and assets, with completion token
352+
scriptUpload(completionToken);
353+
```
354+
355+
</TabItem> </Tabs>

0 commit comments

Comments
 (0)