1
0
silverbullet/common/spaces/http_space_primitives.ts

193 lines
5.6 KiB
TypeScript
Raw Normal View History

import { SpacePrimitives } from "./space_primitives.ts";
import { flushCachesAndUnregisterServiceWorker } from "../sw_util.ts";
2023-08-20 15:51:00 +00:00
import { FileMeta } from "$sb/types.ts";
2022-04-07 13:21:30 +00:00
export class HttpSpacePrimitives implements SpacePrimitives {
2023-01-13 14:41:29 +00:00
constructor(
readonly url: string,
readonly expectedSpacePath?: string,
private bearerToken?: string,
2023-01-13 14:41:29 +00:00
) {
2022-04-29 16:54:27 +00:00
}
public async authenticatedFetch(
2022-04-29 16:54:27 +00:00
url: string,
options: RequestInit,
2022-04-29 16:54:27 +00:00
): Promise<Response> {
if (!options.headers) {
options.headers = {};
}
2023-07-28 13:34:12 +00:00
options.headers = {
...options.headers,
"X-Sync-Mode": "true",
};
if (this.bearerToken) {
options.headers = {
...options.headers,
"Authorization": `Bearer ${this.bearerToken}`,
};
}
2023-01-13 14:41:29 +00:00
2023-07-27 09:41:44 +00:00
try {
const result = await fetch(url, options);
if (result.status === 503) {
throw new Error("Offline");
}
if (result.redirected) {
// Got a redirect, we'll assume this is due to invalid credentials and redirecting to an auth page
console.log(
"Got a redirect via the API so will redirect to URL",
result.url,
);
2023-07-27 15:02:53 +00:00
alert("You are not authenticated, redirecting to login page...");
2023-07-27 09:41:44 +00:00
location.href = result.url;
2023-07-27 10:37:39 +00:00
throw new Error("Not authenticated");
2023-07-27 09:41:44 +00:00
}
return result;
} catch (e: any) {
2023-07-27 15:02:53 +00:00
// Errors when there is no internet connection:
//
// * Firefox: NetworkError when attempting to fetch resource (with SW and without)
// * Safari (service worker enabled): FetchEvent.respondWith received an error: TypeError: Load failed
// * Safari (no service worker): Load failed
// * Chrome: Failed to fetch
//
2023-07-27 09:41:44 +00:00
// Common substrings: "fetch" "load failed"
const errorMessage = e.message.toLowerCase();
if (
errorMessage.includes("fetch") || errorMessage.includes("load failed")
) {
throw new Error("Offline");
}
throw e;
2022-04-29 16:54:27 +00:00
}
}
2023-01-13 14:41:29 +00:00
async fetchFileList(): Promise<FileMeta[]> {
const resp = await this.authenticatedFetch(`${this.url}/index.json`, {
method: "GET",
headers: {
Accept: "application/json",
},
});
if (
resp.status === 200 &&
this.expectedSpacePath &&
2023-07-30 09:30:01 +00:00
resp.headers.get("X-Space-Path") &&
resp.headers.get("X-Space-Path") !== this.expectedSpacePath
) {
console.log("Expected space path", this.expectedSpacePath);
console.log("Got space path", resp.headers.get("X-Space-Path"));
await flushCachesAndUnregisterServiceWorker();
alert("Space folder path different on server, reloading the page");
location.reload();
}
return resp.json();
}
2022-09-12 12:50:37 +00:00
async readFile(
name: string,
): Promise<{ data: Uint8Array; meta: FileMeta }> {
2023-01-13 14:41:29 +00:00
const res = await this.authenticatedFetch(
`${this.url}/${encodeURI(name)}`,
2023-01-13 14:41:29 +00:00
{
method: "GET",
},
);
if (res.status === 404) {
throw new Error(`Not found`);
2022-09-12 12:50:37 +00:00
}
return {
data: new Uint8Array(await res.arrayBuffer()),
2022-09-12 12:50:37 +00:00
meta: this.responseToMeta(name, res),
};
}
2022-09-12 12:50:37 +00:00
async writeFile(
name: string,
data: Uint8Array,
_selfUpdate?: boolean,
meta?: FileMeta,
2022-09-12 12:50:37 +00:00
): Promise<FileMeta> {
2023-01-13 14:41:29 +00:00
const headers: Record<string, string> = {
"Content-Type": "application/octet-stream",
};
if (meta) {
headers["X-Created"] = "" + meta.created;
headers["X-Last-Modified"] = "" + meta.lastModified;
headers["X-Perm"] = "" + meta.perm;
2023-01-13 14:41:29 +00:00
}
const res = await this.authenticatedFetch(
`${this.url}/${encodeURI(name)}`,
2023-01-13 14:41:29 +00:00
{
method: "PUT",
headers,
body: data,
2022-09-12 12:50:37 +00:00
},
2023-01-13 14:41:29 +00:00
);
2022-09-12 12:50:37 +00:00
const newMeta = this.responseToMeta(name, res);
return newMeta;
}
2022-09-12 12:50:37 +00:00
async deleteFile(name: string): Promise<void> {
2023-01-13 14:41:29 +00:00
const req = await this.authenticatedFetch(
`${this.url}/${encodeURI(name)}`,
2023-01-13 14:41:29 +00:00
{
method: "DELETE",
},
);
if (req.status !== 200) {
2022-09-12 12:50:37 +00:00
throw Error(`Failed to delete file: ${req.statusText}`);
}
}
2022-09-12 12:50:37 +00:00
async getFileMeta(name: string): Promise<FileMeta> {
2023-01-13 14:41:29 +00:00
const res = await this.authenticatedFetch(
`${this.url}/${encodeURI(name)}`,
// This used to use HEAD, but it seems that Safari on iOS is blocking cookies/credentials to be sent along with HEAD requests
2023-08-08 13:00:18 +00:00
// so we'll use GET instead with a magic header which the server may or may not use to omit the body.
2023-01-13 14:41:29 +00:00
{
method: "GET",
2023-08-08 13:00:18 +00:00
headers: {
"X-Get-Meta": "true",
},
2023-01-13 14:41:29 +00:00
},
);
if (res.status === 404) {
throw new Error(`Not found`);
2022-09-12 12:50:37 +00:00
}
2023-10-05 16:24:12 +00:00
if (!res.ok) {
throw new Error(`Failed to get file meta: ${res.statusText}`);
}
2022-09-12 12:50:37 +00:00
return this.responseToMeta(name, res);
}
private responseToMeta(name: string, res: Response): FileMeta {
return {
name,
2023-08-08 13:00:18 +00:00
// The server may set a custom X-Content-Length header in case a GET request was sent with X-Get-Meta, in which case the body may be omitted
size: res.headers.has("X-Content-Length")
? +res.headers.get("X-Content-Length")!
: +res.headers.get("Content-Length")!,
2022-09-12 12:50:37 +00:00
contentType: res.headers.get("Content-type")!,
created: +(res.headers.get("X-Created") || "0"),
lastModified: +(res.headers.get("X-Last-Modified") || "0"),
2023-08-15 18:15:27 +00:00
perm: (res.headers.get("X-Permission") as "rw" | "ro") || "ro",
2022-09-12 12:50:37 +00:00
};
}
2023-07-27 10:37:39 +00:00
// Used to check if the server is reachable and the user is authenticated
// If not: throws an error or invokes a redirect
async ping() {
2023-07-28 13:34:12 +00:00
await this.authenticatedFetch(`${this.url}/index.json`, {
method: "GET",
headers: {
Accept: "application/json",
},
});
2023-07-27 10:37:39 +00:00
}
}