Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ability to provide a checksum for global cache #42

Merged
merged 3 commits into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions auth_tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export class AuthTokens {
tokens.push({ type: "bearer", host, token });
}
} else {
// todo(dsherret): feel like this should error?
// deno-lint-ignore no-console

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it better to just throw here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's what I added the todo about. It's weird this code does this. I opened #43 just now.

console.error("Badly formed auth token discarded.");
}
}
Expand Down
3 changes: 2 additions & 1 deletion cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ export class FetchCacher {
specifier: string,
_isDynamic?: boolean,
cacheSetting?: CacheSetting,
checksum?: string,
): Promise<LoadResponse | undefined> => {
const url = new URL(specifier);
return this.#fileFetcher.fetch(url, { cacheSetting });
return this.#fileFetcher.fetch(url, { cacheSetting, checksum });
};
}
7 changes: 7 additions & 0 deletions deno.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@
"build": "deno task wasmbuild",
"wasmbuild": "deno run -A https://deno.land/x/[email protected]/main.ts --sync --features wasm"
},
"lint": {
"rules": {
"include": [
"no-console"
]
}
},
"exclude": [
"target"
]
Expand Down
12 changes: 10 additions & 2 deletions deno_dir_test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.

import { assertEquals } from "./deps_test.ts";
import { assertEquals, assertRejects } from "./deps_test.ts";
import { DenoDir } from "./deno_dir.ts";
import { assert } from "./util.ts";

Expand All @@ -12,7 +12,7 @@ Deno.test({
const deps = denoDir.createHttpCache();
const headers = (await deps.getHeaders(url))!;
assert(Object.keys(headers).length > 10);
const text = new TextDecoder().decode(await deps.get(url));
const text = new TextDecoder().decode(await deps.get(url, undefined));
assertEquals(
text,
`// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
Expand Down Expand Up @@ -56,5 +56,13 @@ export * from "./_interface.ts";
export * from "./glob.ts";
`,
);

// ok
await deps.get(
url,
"d3e68d0abb393fb0bf94a6d07c46ec31dc755b544b13144dee931d8d5f06a52d",
);
// not ok
await assertRejects(async () => await deps.get(url, "invalid"));
},
});
48 changes: 37 additions & 11 deletions file_fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export class FileFetcher {
specifier: URL,
cacheSetting: CacheSetting,
): Promise<LoadResponse> {
const cached = await this.#fetchCached(specifier, 0);
const cached = await this.#fetchCached(specifier, undefined, 0);
if (cached) {
return cached;
}
Expand Down Expand Up @@ -153,6 +153,7 @@ export class FileFetcher {

async #fetchCached(
specifier: URL,
maybeChecksum: string | undefined,
redirectLimit: number,
): Promise<LoadResponse | undefined> {
if (redirectLimit < 0) {
Expand All @@ -168,9 +169,9 @@ export class FileFetcher {
const location = headers["location"];
if (location != null && location.length > 0) {
const redirect = new URL(location, specifier);
return this.#fetchCached(redirect, redirectLimit - 1);
return this.#fetchCached(redirect, maybeChecksum, redirectLimit - 1);
}
const content = await this.#httpCache.get(specifier);
const content = await this.#httpCache.get(specifier, maybeChecksum);
if (content == null) {
return undefined;
}
Expand All @@ -182,19 +183,27 @@ export class FileFetcher {
};
}

async #fetchRemote(
specifier: URL,
redirectLimit: number,
cacheSetting: CacheSetting,
): Promise<LoadResponse | undefined> {
async #fetchRemote(specifier: URL, {
redirectLimit,
cacheSetting,
checksum,
}: {
redirectLimit: number;
cacheSetting: CacheSetting;
checksum: string | undefined;
}): Promise<LoadResponse | undefined> {
if (redirectLimit < 0) {
throw new Deno.errors.Http(
`Too many redirects.\n Specifier: "${specifier.toString()}"`,
);
}

if (shouldUseCache(cacheSetting, specifier)) {
const response = await this.#fetchCached(specifier, redirectLimit);
const response = await this.#fetchCached(
specifier,
checksum,
redirectLimit,
);
if (response) {
return response;
}
Expand All @@ -218,6 +227,7 @@ export class FileFetcher {
if (authToken) {
requestHeaders.append("authorization", authToken);
}
// deno-lint-ignore no-console
console.error(`${colors.green("Download")} ${specifier.toString()}`);
const response = await fetchWithRetries(specifier.toString(), {
headers: requestHeaders,
Expand All @@ -242,6 +252,17 @@ export class FileFetcher {
headers[key.toLowerCase()] = value;
}
await this.#httpCache.set(url, headers, content);
if (checksum != null) {
const digest = await crypto.subtle.digest("SHA-256", content);
const actualChecksum = Array.from(new Uint8Array(digest))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
if (actualChecksum != checksum) {
throw new Error(
`Integrity check failed for: ${url}\n\nActual: ${actualChecksum}\nExpected: ${checksum}`,
);
}
}
return {
kind: "module",
specifier: response.url,
Expand All @@ -252,7 +273,7 @@ export class FileFetcher {

async fetch(
specifier: URL,
options?: { cacheSetting?: CacheSetting },
options?: { cacheSetting?: CacheSetting; checksum?: string },
): Promise<LoadResponse | undefined> {
const cacheSetting = options?.cacheSetting ?? this.#cacheSetting;
const scheme = getValidatedScheme(specifier);
Expand All @@ -271,7 +292,11 @@ export class FileFetcher {
`A remote specifier was requested: "${specifier.toString()}", but --no-remote is specified.`,
);
} else {
const response = await this.#fetchRemote(specifier, 10, cacheSetting);
const response = await this.#fetchRemote(specifier, {
redirectLimit: 10,
cacheSetting,
checksum: options?.checksum,
});
if (response) {
await this.#cache.set(specifier.toString(), response);
}
Expand Down Expand Up @@ -299,6 +324,7 @@ export async function fetchWithRetries(
throw err;
}
}
// deno-lint-ignore no-console
console.warn(
`${
colors.yellow("WARN")
Expand Down
62 changes: 60 additions & 2 deletions file_fetcher_test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.

import { DenoDir } from "./deno_dir.ts";
import { createGraph } from "./deps_test.ts";
import { assertRejects, createGraph } from "./deps_test.ts";
import { FileFetcher } from "./file_fetcher.ts";

Deno.test({
Expand All @@ -14,6 +14,64 @@ Deno.test({
return fileFetcher.fetch(new URL(specifier));
},
});
console.log(graph.toString());
// deno-lint-ignore no-console
console.log(graph);
},
});

Deno.test({
name: "FileFetcher - bad checksum no cache",
async fn() {
const denoDir = new DenoDir();
const fileFetcher = new FileFetcher(denoDir.createHttpCache());
{
// should error
await assertRejects(async () => {
await fileFetcher.fetch(
new URL("https://deno.land/x/[email protected]/mod.ts"),
{
checksum: "bad",
},
);
});
// ok for good checksum
await fileFetcher.fetch(
new URL("https://deno.land/x/[email protected]/mod.ts"),
{
checksum:
"7a1b5169ef702e96dd994168879dbcbd8af4f639578b6300cbe1c6995d7f3f32",
},
);
}
},
});

Deno.test({
name: "FileFetcher - bad checksum reload",
async fn() {
const denoDir = new DenoDir();
const fileFetcher = new FileFetcher(denoDir.createHttpCache());
await assertRejects(async () => {
await fileFetcher.fetch(
new URL("https://deno.land/x/[email protected]/mod.ts"),
{
cacheSetting: "reload",
checksum: "bad",
},
);
});
},
});

Deno.test({
name: "FileFetcher - good checksum reload",
async fn() {
const denoDir = new DenoDir();
const fileFetcher = new FileFetcher(denoDir.createHttpCache());
await fileFetcher.fetch(new URL("https://deno.land/x/[email protected]/mod.ts"), {
cacheSetting: "reload",
checksum:
"7a1b5169ef702e96dd994168879dbcbd8af4f639578b6300cbe1c6995d7f3f32",
});
},
});
6 changes: 5 additions & 1 deletion http_cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,12 @@ export class HttpCache {

async get(
url: URL,
maybeChecksum: string | undefined,
): Promise<Uint8Array | undefined> {
const data = (await this.#ensureCache()).getFileBytes(url.toString());
const data = (await this.#ensureCache()).getFileBytes(
url.toString(),
maybeChecksum,
);
return data == null ? undefined : data;
}

Expand Down
Loading