Skip to content

Commit 3211ff9

Browse files
authoredNov 9, 2024··
fix: documentation site build (#1042)
1 parent 6350b9d commit 3211ff9

File tree

6 files changed

+370
-8
lines changed

6 files changed

+370
-8
lines changed
 

‎compute-file-server-cli/src/main.rs

+12-4
Original file line numberDiff line numberDiff line change
@@ -585,7 +585,10 @@ async fn clone_version_of_service(
585585
..Default::default()
586586
};
587587

588-
Ok(clone_service_version(&mut cfg, params).await?.number.unwrap())
588+
Ok(clone_service_version(&mut cfg, params)
589+
.await?
590+
.number
591+
.unwrap())
589592
}
590593

591594
async fn activate_version_of_service(
@@ -607,7 +610,10 @@ async fn activate_version_of_service(
607610
..Default::default()
608611
};
609612

610-
Ok(activate_service_version(&mut cfg, params).await?.number.unwrap())
613+
Ok(activate_service_version(&mut cfg, params)
614+
.await?
615+
.number
616+
.unwrap())
611617
}
612618

613619
fn cli() -> Command {
@@ -735,7 +741,8 @@ async fn upload(sub_matches: &clap::ArgMatches) -> Result<(), Box<dyn std::error
735741
.get_one::<PathBuf>("path")
736742
.expect("required in clap");
737743

738-
let entries = WalkDir::new(path).follow_links(true)
744+
let entries = WalkDir::new(path)
745+
.follow_links(true)
739746
.into_iter()
740747
.filter_map(Result::ok)
741748
.filter(|e| !e.file_type().is_dir())
@@ -868,7 +875,8 @@ async fn local(sub_matches: &clap::ArgMatches) -> Result<(), Box<dyn std::error:
868875
.get_one::<PathBuf>("toml")
869876
.expect("required in clap");
870877

871-
let entries = WalkDir::new(path).follow_links(true)
878+
let entries = WalkDir::new(path)
879+
.follow_links(true)
872880
.into_iter()
873881
.filter_map(Result::ok)
874882
.filter(|e| !e.file_type().is_dir())
+342
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,342 @@
1+
import parseRange from 'range-parser'
2+
3+
/**
4+
* Attempt to locate the requested resource from a Fastly Object-Store,
5+
* If the request is a GET or HEAD request and a resource was found in the Object-Store, this will return a `Response`.
6+
* If request is not GET or HEAD, or no resource was found in the Object-Store, this will return `null`
7+
* @param {string} store_name The name of the Fastly Object-Store to search within.
8+
* @param {Request} request The request to attempt to match against a resource within the Object-Store.
9+
* @returns {Promise<Response | null>} Returns a `Response` if a resource was found, else returns `null`.
10+
*/
11+
export async function get(store_name, request) {
12+
const isHeadRequest = request.method === 'HEAD'
13+
// static files should only respond on HEAD and GET requests
14+
if (!isHeadRequest && request.method !== 'GET') {
15+
return null
16+
}
17+
18+
// if path ends in / or does not have an extension
19+
// then append /index.html to the end so we can serve a page
20+
let path = new URL(request.url).pathname
21+
if (path.endsWith('/')) {
22+
path += 'index.html'
23+
} else if (!path.includes('.')) {
24+
path += '/index.html'
25+
}
26+
27+
const metadataPath = path + '__metadata__'
28+
29+
let metadata = await (new KVStore(store_name)).get(metadataPath)
30+
if (metadata == null) {
31+
return null
32+
}
33+
metadata = await metadata.json();
34+
const responseHeaders = metadata;
35+
responseHeaders['accept-ranges'] = 'bytes'
36+
37+
const response = checkPreconditions(request, responseHeaders);
38+
if (response) {
39+
return response;
40+
}
41+
42+
const item = await (new KVStore(store_name)).get(path)
43+
44+
if (item == null) {
45+
return null
46+
}
47+
48+
let range = request.headers.get("range");
49+
if (range == null) {
50+
return new Response(isHeadRequest ? null : item.body, { status: 200, headers: responseHeaders })
51+
} else {
52+
return handleRangeRequest(item, range, responseHeaders, isHeadRequest)
53+
}
54+
}
55+
56+
async function handleRangeRequest(item, range, headers, isHeadRequest) {
57+
/**
58+
* @type {Uint8Array}
59+
*/
60+
const itemBuffer = new Uint8Array(await item.arrayBuffer())
61+
const total = itemBuffer.byteLength
62+
const subranges = parseRange(total, range)
63+
64+
// -1 signals an unsatisfiable range
65+
if (subranges == -1) {
66+
headers['content-range'] = `bytes */${total}`
67+
return new Response(null, { status: 416, headers })
68+
}
69+
// -2 signals a malformed header string
70+
if (subranges == -2) {
71+
headers['content-length'] = String(total)
72+
return new Response(isHeadRequest ? null : itemBuffer, { status: 200, headers })
73+
}
74+
75+
if (subranges.length == 1) {
76+
const { start, end } = subranges[0]
77+
headers['content-range'] = `bytes ${start}-${end}/${total}`
78+
headers['content-length'] = String(end - start + 1)
79+
80+
return new Response(isHeadRequest ? null : itemBuffer.slice(start, end), { status: 206, headers })
81+
} else {
82+
const mime = headers['Content-Type']
83+
headers['Content-Type'] = 'multipart/byteranges; boundary=3d6b6a416f9b5'
84+
const enc = new TextEncoder();
85+
const boundaryString = '--3d6b6a416f9b5';
86+
const type = mime ? enc.encode(`Content-Type: ${mime}\n`) : null
87+
const results = []
88+
let bufferLength = 0
89+
let boundary = enc.encode(`\n${boundaryString}\n`)
90+
subranges.forEach(function ({ start, end }) {
91+
{
92+
bufferLength += boundary.byteLength
93+
results.push(boundary)
94+
}
95+
if (type) {
96+
results.push(type)
97+
bufferLength += type.byteLength
98+
}
99+
{
100+
let content_range = enc.encode(`Content-Range: bytes ${start}-${end}/${total}\n\n`)
101+
bufferLength += content_range.byteLength
102+
results.push(content_range)
103+
}
104+
{
105+
let content = itemBuffer.slice(start, end)
106+
bufferLength += content.byteLength
107+
results.push(content)
108+
}
109+
})
110+
{
111+
results.push(boundary)
112+
bufferLength += boundary.byteLength
113+
}
114+
const body = concat(results, bufferLength)
115+
const length = body.byteLength
116+
headers['content-length'] = String(length)
117+
return new Response(isHeadRequest ? null : body, { status: 206, headers })
118+
}
119+
}
120+
121+
function concat(views, length) {
122+
console.log({length})
123+
const buf = new Uint8Array(length)
124+
let offset = 0
125+
for (const v of views) {
126+
const uint8view = new Uint8Array(v.buffer, v.byteOffset, v.byteLength)
127+
buf.set(uint8view, offset)
128+
offset += uint8view.byteLength
129+
}
130+
131+
return buf
132+
}
133+
134+
function checkPreconditions(request, responseHeaders) {
135+
// https://httpwg.org/specs/rfc9110.html#rfc.section.13.2.2
136+
// A recipient cache or origin server MUST evaluate the request preconditions defined by this specification in the following order:
137+
// 1. When recipient is the origin server and If-Match is present, evaluate the If-Match precondition:
138+
// - if true, continue to step 3
139+
// - if false, respond 412 (Precondition Failed) unless it can be determined that the state-changing request has already succeeded (see Section 13.1.1)
140+
let header = request.headers.get("if-match");
141+
if (typeof header === 'string') {
142+
console.log("!ifMatch(responseHeaders, header)", !ifMatch(responseHeaders, header));
143+
if (!ifMatch(responseHeaders, header)) {
144+
return new Response(null, { status: 412 });
145+
}
146+
// } else {
147+
// // 2. When recipient is the origin server, If-Match is not present, and If-Unmodified-Since is present, evaluate the If-Unmodified-Since precondition:
148+
// // - if true, continue to step 3
149+
// // - if false, respond 412 (Precondition Failed) unless it can be determined that the state-changing request has already succeeded (see Section 13.1.4)
150+
// header = request.headers.get("if-unmodified-since");
151+
// if (typeof header === 'string') {
152+
// // console.log("!ifUnmodifiedSince(responseHeaders, header)", !ifUnmodifiedSince(responseHeaders, header));
153+
// if (!ifUnmodifiedSince(responseHeaders, header)) {
154+
// return new Response(null, { status: 412 });
155+
// }
156+
// }
157+
}
158+
159+
// 3. When If-None-Match is present, evaluate the If-None-Match precondition:
160+
// - if true, continue to step 5
161+
// - if false for GET/HEAD, respond 304 (Not Modified)
162+
// - if false for other methods, respond 412 (Precondition Failed)
163+
header = request.headers.get("if-none-match");
164+
const method = request.method;
165+
const get = "GET";
166+
const head = "HEAD";
167+
if (typeof header === 'string') {
168+
// console.log("!ifNoneMatch(responseHeaders, header)", !ifNoneMatch(responseHeaders, header));
169+
if (!ifNoneMatch(responseHeaders, header)) {
170+
if (method === get || method === head) {
171+
return new Response(null, { status: 304, headers: responseHeaders })
172+
}
173+
return new Response(null, { status: 412 });
174+
}
175+
} else {
176+
// 4. When the method is GET or HEAD, If-None-Match is not present, and If-Modified-Since is present, evaluate the If-Modified-Since precondition:
177+
// - if true, continue to step 5
178+
// - if false, respond 304 (Not Modified)
179+
if (method === get || method === head) {
180+
header = request.headers.get("if-modified-since");
181+
if (typeof header === 'string') {
182+
// console.log("!ifModifiedSince(responseHeaders, header)", !ifModifiedSince(responseHeaders, header));
183+
if (!ifModifiedSince(responseHeaders, header)) {
184+
return new Response(null, { status: 304, headers: responseHeaders })
185+
}
186+
}
187+
}
188+
}
189+
190+
// 5. When the method is GET and both Range and If-Range are present, evaluate the If-Range precondition:
191+
// - if true and the Range is applicable to the selected representation, respond 206 (Partial Content)
192+
// - otherwise, ignore the Range header field and respond 200 (OK)
193+
if (method === get) {
194+
if (request.headers.get("range")) {
195+
header = request.headers.get("if-range");
196+
if (typeof header === 'string') {
197+
// console.log("!ifRange(responseHeaders, header)", !ifRange(responseHeaders, header));
198+
if (!ifRange(responseHeaders, header)) {
199+
// We delete the range headers so that the `get` function will return the full body
200+
request.headers.delete("range")
201+
}
202+
}
203+
}
204+
}
205+
206+
// 6. Otherwise,
207+
// - perform the requested method and respond according to its success or failure.
208+
return null;
209+
}
210+
211+
function isWeak(etag) {
212+
return etag.startsWith("W/\"");
213+
}
214+
215+
function isStrong(etag) {
216+
return etag.startsWith("\"");
217+
}
218+
219+
function opaqueTag(etag) {
220+
if (isWeak(etag)) {
221+
return etag.substring(2);
222+
}
223+
return etag;
224+
}
225+
function weakMatch(a, b) {
226+
// https://httpwg.org/specs/rfc9110.html#entity.tag.comparison
227+
// two entity tags are equivalent if their opaque-tags match character-by-character, regardless of either or both being tagged as "weak".
228+
return opaqueTag(a) === opaqueTag(b);
229+
}
230+
231+
function strongMatch(a, b) {
232+
// https://httpwg.org/specs/rfc9110.html#entity.tag.comparison
233+
// two entity tags are equivalent if both are not weak and their opaque-tags match character-by-character.
234+
return isStrong(a) && isStrong(b) && a === b;
235+
}
236+
237+
function splitList(value) {
238+
return value.split(",").map(s => s.trim());
239+
}
240+
241+
// https://httpwg.org/specs/rfc9110.html#field.if-match
242+
function ifMatch(validationFields, header) {
243+
if (validationFields.ETag === undefined) {
244+
return true;
245+
}
246+
247+
// 1. If the field value is "*", the condition is true if the origin server has a current representation for the target resource.
248+
if (header === "*") {
249+
if (validationFields.ETag !== undefined) {
250+
return true;
251+
}
252+
} else {
253+
// 2. If the field value is a list of entity tags, the condition is true if any of the listed tags match the entity tag of the selected representation.
254+
// An origin server MUST use the strong comparison function when comparing entity tags for If-Match (Section 8.8.3.2),
255+
// since the client intends this precondition to prevent the method from being applied if there have been any changes to the representation data.
256+
if (splitList(header).some(etag => {
257+
console.log(`strongMatch(${etag}, ${validationFields.ETag}) -- ${strongMatch(etag, validationFields.ETag)}`);
258+
return strongMatch(etag, validationFields.ETag)
259+
})) {
260+
return true;
261+
}
262+
}
263+
264+
// 3. Otherwise, the condition is false.
265+
return false;
266+
}
267+
268+
// https://httpwg.org/specs/rfc9110.html#field.if-none-match
269+
function ifNoneMatch(validationFields, header) {
270+
// 1. If the field value is "*", the condition is false if the origin server has a current representation for the target resource.
271+
if (header === "*") {
272+
if (validationFields.ETag !== undefined) {
273+
return false;
274+
}
275+
} else {
276+
// 2. If the field value is a list of entity tags, the condition is false if one of the listed tags matches the entity tag of the selected representation.
277+
// A recipient MUST use the weak comparison function when comparing entity tags for If-None-Match (Section 8.8.3.2), since weak entity tags can be used for cache validation even if there have been changes to the representation data.
278+
if (splitList(header).some(etag => weakMatch(etag, validationFields.ETag))) {
279+
return false;
280+
}
281+
}
282+
283+
// 3. Otherwise, the condition is true.
284+
return true;
285+
}
286+
287+
// https://httpwg.org/specs/rfc9110.html#field.if-modified-since
288+
function ifModifiedSince(validationFields, header) {
289+
// A recipient MUST ignore the If-Modified-Since header field if the received field value is not a valid HTTP-date, the field value has more than one member, or if the request method is neither GET nor HEAD.
290+
const date = new Date(header);
291+
if (isNaN(date)) {
292+
return true;
293+
}
294+
295+
// 1. If the selected representation's last modification date is earlier or equal to the date provided in the field value, the condition is false.
296+
if (new Date(validationFields["Last-Modified"]) <= date) {
297+
return false;
298+
}
299+
// 2. Otherwise, the condition is true.
300+
return true;
301+
}
302+
303+
// https://httpwg.org/specs/rfc9110.html#field.if-unmodified-since
304+
// function ifUnmodifiedSince(req, validationFields, header) {
305+
// // A recipient MUST ignore the If-Unmodified-Since header field if the received field value is not a valid HTTP-date (including when the field value appears to be a list of dates).
306+
// const date = new Date(header);
307+
// if (isNaN(date)) {
308+
// return true;
309+
// }
310+
311+
// // 1. If the selected representation's last modification date is earlier than or equal to the date provided in the field value, the condition is true.
312+
// if (new Date(validationFields["Last-Modified"]) <= date) {
313+
// return true;
314+
// }
315+
// // 2. Otherwise, the condition is false.
316+
// return false;
317+
// }
318+
319+
// https://httpwg.org/specs/rfc9110.html#field.if-range
320+
function ifRange(validationFields, header) {
321+
const date = new Date(header);
322+
console.log(new Date(validationFields["Last-Modified"]), date);
323+
console.log(new Date(validationFields["Last-Modified"]).getTime() === date.getTime());
324+
if (!isNaN(date)) {
325+
// To evaluate a received If-Range header field containing an HTTP-date:
326+
// 1. If the HTTP-date validator provided is not a strong validator in the sense defined by Section 8.8.2.2, the condition is false.
327+
// 2. If the HTTP-date validator provided exactly matches the Last-Modified field value for the selected representation, the condition is true.
328+
if (new Date(validationFields["Last-Modified"]).getTime() === date.getTime()) {
329+
return true;
330+
}
331+
// 3. Otherwise, the condition is false.
332+
return false;
333+
} else {
334+
// To evaluate a received If-Range header field containing an entity-tag:
335+
// 1. If the entity-tag validator provided exactly matches the ETag field value for the selected representation using the strong comparison function (Section 8.8.3.2), the condition is true.
336+
if (strongMatch(header, validationFields.ETag)) {
337+
return true;
338+
}
339+
// 2. Otherwise, the condition is false.
340+
return false;
341+
}
342+
}

‎documentation/app/package-lock.json

+12-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎documentation/app/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
{
22
"license": "MIT",
33
"devDependencies": {
4-
"@fastly/js-compute": "^3"
4+
"@fastly/js-compute": "^3",
5+
"range-parser": "^1.2.1"
56
},
67
"type": "module",
78
"scripts": {

‎documentation/app/src/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/// <reference types="@fastly/js-compute" />
22
/* eslint-env serviceworker */
33

4-
import { get } from "@jakechampion/c-at-e-file-server";
4+
import { get } from "../c-at-e-file-server.js";
55
import { env } from "fastly:env";
66
import { KVStore } from "fastly:kv-store";
77

‎types/kv-store.d.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ declare module 'fastly:kv-store' {
121121
list: string[];
122122
/**
123123
* Pass this base64 cursor into a subsequent list call to obtain the next listing.
124-
*
124+
*
125125
* The cursor is *undefined* when the end of the list is reached.
126126
*/
127127
cursor: string | undefined;

0 commit comments

Comments
 (0)
Please sign in to comment.