Federation progress

pull/503/head
Zef Hemel 2023-07-29 23:41:37 +02:00
parent 272bd90ee6
commit fe4887dc78
11 changed files with 112 additions and 49 deletions

View File

@ -22,10 +22,10 @@ export class FallbackSpacePrimitives implements SpacePrimitives {
try { try {
return await this.primary.readFile(name); return await this.primary.readFile(name);
} catch (e) { } catch (e) {
console.info( // console.info(
`Could not read file ${name} from primary, trying fallback, primary read error:`, // `Could not read file ${name} from primary, trying fallback, primary read error:`,
e.message, // e.message,
); // );
try { try {
const result = await this.fallback.readFile(name); const result = await this.fallback.readFile(name);
return { return {
@ -43,10 +43,10 @@ export class FallbackSpacePrimitives implements SpacePrimitives {
try { try {
return await this.primary.getFileMeta(name); return await this.primary.getFileMeta(name);
} catch (e) { } catch (e) {
console.info( // console.info(
`Could not fetch file ${name} metadata from primary, trying fallback, primary read error`, // `Could not fetch file ${name} metadata from primary, trying fallback, primary read error`,
e.message, // e.message,
); // );
try { try {
const meta = await this.fallback.getFileMeta(name); const meta = await this.fallback.getFileMeta(name);
return { ...meta, neverSync: true }; return { ...meta, neverSync: true };

View File

@ -0,0 +1,20 @@
import { resolvePath } from "$sb/lib/resolve.ts";
import { assertEquals } from "../../test_deps.ts";
Deno.test("Test URL resolver", () => {
assertEquals(resolvePath("test", "some page"), "some page");
assertEquals(
resolvePath("!silverbullet.md", "some page"),
"!silverbullet.md/some page",
);
assertEquals(
resolvePath("!silverbullet.md/some/deep/path", "some page"),
"!silverbullet.md/some page",
);
assertEquals(resolvePath("!bla/bla", "!bla/bla2"), "!bla/bla2");
assertEquals(
resolvePath("!silverbullet.md", "test/image.png", true),
"https://silverbullet.md/test/image.png",
);
});

20
plug-api/lib/resolve.ts Normal file
View File

@ -0,0 +1,20 @@
export function resolvePath(
currentPage: string,
pathToResolve: string,
fullUrl = false,
): string {
if (currentPage.startsWith("!") && !pathToResolve.startsWith("!")) {
let domainPart = currentPage.split("/")[0];
if (fullUrl) {
domainPart = domainPart.substring(1);
if (domainPart.startsWith("localhost")) {
domainPart = "http://" + domainPart;
} else {
domainPart = "https://" + domainPart;
}
}
return `${domainPart}/${pathToResolve}`;
} else {
return pathToResolve;
}
}

View File

@ -7,6 +7,7 @@ import {
nodeAtPos, nodeAtPos,
ParseTree, ParseTree,
} from "$sb/lib/tree.ts"; } from "$sb/lib/tree.ts";
import { resolvePath } from "$sb/lib/resolve.ts";
async function actionClickOrActionEnter( async function actionClickOrActionEnter(
mdTree: ParseTree | null, mdTree: ParseTree | null,
@ -46,6 +47,7 @@ async function actionClickOrActionEnter(
pos = +pos; pos = +pos;
} }
} }
pageLink = resolvePath(currentPage, pageLink);
if (!pageLink) { if (!pageLink) {
pageLink = currentPage; pageLink = currentPage;
} }
@ -77,7 +79,7 @@ async function actionClickOrActionEnter(
return editor.flashNotification("Empty link, ignoring", "error"); return editor.flashNotification("Empty link, ignoring", "error");
} }
if (url.indexOf("://") === -1 && !url.startsWith("mailto:")) { if (url.indexOf("://") === -1 && !url.startsWith("mailto:")) {
return editor.openUrl(decodeURI(url)); return editor.openUrl(resolvePath(currentPage, decodeURI(url)));
} else { } else {
await editor.openUrl(url); await editor.openUrl(url);
} }

View File

@ -17,28 +17,28 @@ async function responseToFileMeta(
r: Response, r: Response,
name: string, name: string,
): Promise<FileMeta> { ): Promise<FileMeta> {
let perm = r.headers.get("X-Permission") as any || "ro"; // const perm = r.headers.get("X-Permission") as any || "ro";
const federationConfigs = await readFederationConfigs(); // const federationConfigs = await readFederationConfigs();
const federationConfig = federationConfigs.find((config) => // const federationConfig = federationConfigs.find((config) =>
name.startsWith(config.uri) // name.startsWith(config.uri)
); // );
if (federationConfig?.perm) { // if (federationConfig?.perm) {
perm = federationConfig.perm; // perm = federationConfig.perm;
} // }
return { return {
name: name, name: name,
size: r.headers.get("Content-length") size: r.headers.get("Content-length")
? +r.headers.get("Content-length")! ? +r.headers.get("Content-length")!
: 0, : 0,
contentType: r.headers.get("Content-type")!, contentType: r.headers.get("Content-type")!,
perm: perm, perm: "ro",
lastModified: +(r.headers.get("X-Last-Modified") || "0"), lastModified: +(r.headers.get("X-Last-Modified") || "0"),
}; };
} }
type FederationConfig = { type FederationConfig = {
uri: string; uri: string;
perm?: "ro" | "rw"; // perm?: "ro" | "rw";
}; };
let federationConfigs: FederationConfig[] = []; let federationConfigs: FederationConfig[] = [];
let lastFederationUrlFetch = 0; let lastFederationUrlFetch = 0;
@ -71,7 +71,7 @@ export async function listFiles(): Promise<FileMeta[]> {
(await r.json()).filter((meta: FileMeta) => meta.name.startsWith(prefix)) (await r.json()).filter((meta: FileMeta) => meta.name.startsWith(prefix))
.map((meta: FileMeta) => ({ .map((meta: FileMeta) => ({
...meta, ...meta,
perm: config.perm || meta.perm, perm: "ro", //config.perm || meta.perm,
name: `${rootUri}/${meta.name}`, name: `${rootUri}/${meta.name}`,
})), })),
); );
@ -121,30 +121,33 @@ export async function writeFile(
name: string, name: string,
data: Uint8Array, data: Uint8Array,
): Promise<FileMeta> { ): Promise<FileMeta> {
const url = resolveFederated(name); throw new Error("Writing federation file, not yet supported");
console.log("Writing federation file", url); // const url = resolveFederated(name);
// console.log("Writing federation file", url);
const r = await nativeFetch(url, { // const r = await nativeFetch(url, {
method: "PUT", // method: "PUT",
body: data, // body: data,
}); // });
const fileMeta = await responseToFileMeta(r, name); // const fileMeta = await responseToFileMeta(r, name);
if (!r.ok) { // if (!r.ok) {
throw new Error("Could not write file"); // throw new Error("Could not write file");
} // }
return fileMeta; // return fileMeta;
} }
export async function deleteFile( export async function deleteFile(
name: string, name: string,
): Promise<void> { ): Promise<void> {
console.log("Deleting federation file", name); throw new Error("Writing federation file, not yet supported");
const url = resolveFederated(name);
const r = await nativeFetch(url, { method: "DELETE" }); // console.log("Deleting federation file", name);
if (!r.ok) { // const url = resolveFederated(name);
throw Error("Failed to delete file"); // const r = await nativeFetch(url, { method: "DELETE" });
} // if (!r.ok) {
// throw Error("Failed to delete file");
// }
} }
export async function getFileMeta(name: string): Promise<FileMeta> { export async function getFileMeta(name: string): Promise<FileMeta> {

View File

@ -2,11 +2,13 @@ import { editor, system } from "$sb/silverbullet-syscall/mod.ts";
import { asset, store } from "$sb/plugos-syscall/mod.ts"; import { asset, store } from "$sb/plugos-syscall/mod.ts";
import { parseMarkdown } from "$sb/silverbullet-syscall/markdown.ts"; import { parseMarkdown } from "$sb/silverbullet-syscall/markdown.ts";
import { renderMarkdownToHtml } from "./markdown_render.ts"; import { renderMarkdownToHtml } from "./markdown_render.ts";
import { resolvePath } from "$sb/lib/resolve.ts";
export async function updateMarkdownPreview() { export async function updateMarkdownPreview() {
if (!(await store.get("enableMarkdownPreview"))) { if (!(await store.get("enableMarkdownPreview"))) {
return; return;
} }
const currentPage = await editor.getCurrentPage();
const text = await editor.getText(); const text = await editor.getText();
const mdTree = await parseMarkdown(text); const mdTree = await parseMarkdown(text);
// const cleanMd = await cleanMarkdown(text); // const cleanMd = await cleanMarkdown(text);
@ -17,7 +19,7 @@ export async function updateMarkdownPreview() {
annotationPositions: true, annotationPositions: true,
translateUrls: (url) => { translateUrls: (url) => {
if (!url.includes("://")) { if (!url.includes("://")) {
return decodeURI(url); url = resolvePath(currentPage, decodeURI(url), true);
} }
return url; return url;
}, },

View File

@ -352,7 +352,17 @@ export class HttpServer {
} else { } else {
url = `https://${url}`; url = `https://${url}`;
} }
response.redirect(url); try {
const req = await fetch(url);
response.status = req.status;
response.headers = req.headers;
response.body = req.body;
} catch (e: any) {
console.error("Error fetching federated link", e);
response.status = 500;
response.body = e.message;
}
// response.redirect(url);
return; return;
} }
try { try {

View File

@ -159,8 +159,8 @@ export class Client {
); );
} }
this.initNavigator();
await this.loadPlugs(); await this.loadPlugs();
this.initNavigator();
this.initSync(); this.initSync();
this.loadCustomStyles().catch(console.error); this.loadCustomStyles().catch(console.error);

View File

@ -7,14 +7,14 @@ import {
} from "../deps.ts"; } from "../deps.ts";
import { decoratorStateField } from "./util.ts"; import { decoratorStateField } from "./util.ts";
import type { Space } from "../space.ts";
import type { Client } from "../client.ts"; import type { Client } from "../client.ts";
import { resolvePath } from "$sb/lib/resolve.ts";
class InlineImageWidget extends WidgetType { class InlineImageWidget extends WidgetType {
constructor( constructor(
readonly url: string, readonly url: string,
readonly title: string, readonly title: string,
readonly space: Space, readonly client: Client,
) { ) {
super(); super();
// console.log("Creating widget", url); // console.log("Creating widget", url);
@ -25,22 +25,25 @@ class InlineImageWidget extends WidgetType {
} }
get estimatedHeight(): number { get estimatedHeight(): number {
const cachedHeight = this.space.getCachedImageHeight(this.url); const cachedHeight = this.client.space.getCachedImageHeight(this.url);
// console.log("Estimated height requested", this.url, cachedHeight); // console.log("Estimated height requested", this.url, cachedHeight);
return cachedHeight; return cachedHeight;
} }
toDOM() { toDOM() {
const img = document.createElement("img"); const img = document.createElement("img");
let url = this.url;
url = resolvePath(this.client.currentPage!, url, true);
console.log("Resolving image to", url);
// console.log("Creating DOM", this.url); // console.log("Creating DOM", this.url);
const cachedImageHeight = this.space.getCachedImageHeight(this.url); const cachedImageHeight = this.client.space.getCachedImageHeight(url);
img.onload = () => { img.onload = () => {
// console.log("Loaded", this.url, "with height", img.height); // console.log("Loaded", this.url, "with height", img.height);
if (img.height !== cachedImageHeight) { if (img.height !== cachedImageHeight) {
this.space.setCachedImageHeight(this.url, img.height); this.client.space.setCachedImageHeight(url, img.height);
} }
}; };
img.src = this.url; img.src = url;
img.alt = this.title; img.alt = this.title;
img.title = this.title; img.title = this.title;
img.style.display = "block"; img.style.display = "block";
@ -53,7 +56,7 @@ class InlineImageWidget extends WidgetType {
} }
} }
export function inlineImagesPlugin(editor: Client) { export function inlineImagesPlugin(client: Client) {
return decoratorStateField((state: EditorState) => { return decoratorStateField((state: EditorState) => {
const widgets: Range<Decoration>[] = []; const widgets: Range<Decoration>[] = [];
const imageRegex = /!\[(?<title>[^\]]*)\]\((?<url>.+)\)/; const imageRegex = /!\[(?<title>[^\]]*)\]\((?<url>.+)\)/;
@ -78,7 +81,7 @@ export function inlineImagesPlugin(editor: Client) {
} }
widgets.push( widgets.push(
Decoration.widget({ Decoration.widget({
widget: new InlineImageWidget(url, title, editor.space), widget: new InlineImageWidget(url, title, client),
block: true, block: true,
}).range(node.to), }).range(node.to),
); );

View File

@ -9,6 +9,7 @@ import { renderMarkdownToHtml } from "../../plugs/markdown/markdown_render.ts";
import { ParseTree } from "$sb/lib/tree.ts"; import { ParseTree } from "$sb/lib/tree.ts";
import { lezerToParseTree } from "../../common/markdown_parser/parse_tree.ts"; import { lezerToParseTree } from "../../common/markdown_parser/parse_tree.ts";
import type { Client } from "../client.ts"; import type { Client } from "../client.ts";
import { resolvePath } from "$sb/lib/resolve.ts";
class TableViewWidget extends WidgetType { class TableViewWidget extends WidgetType {
constructor( constructor(
@ -39,7 +40,7 @@ class TableViewWidget extends WidgetType {
annotationPositions: true, annotationPositions: true,
translateUrls: (url) => { translateUrls: (url) => {
if (!url.includes("://")) { if (!url.includes("://")) {
return `/${url}`; url = resolvePath(this.editor.currentPage!, decodeURI(url), true);
} }
return url; return url;
}, },

View File

@ -8,6 +8,7 @@ import {
isCursorInRange, isCursorInRange,
LinkWidget, LinkWidget,
} from "./util.ts"; } from "./util.ts";
import { resolvePath } from "$sb/lib/resolve.ts";
/** /**
* Plugin to hide path prefix when the cursor is not inside. * Plugin to hide path prefix when the cursor is not inside.
@ -33,6 +34,7 @@ export function cleanWikiLinkPlugin(editor: Client) {
if (page.includes("@")) { if (page.includes("@")) {
cleanPage = page.split("@")[0]; cleanPage = page.split("@")[0];
} }
cleanPage = resolvePath(editor.currentPage!, cleanPage);
// console.log("Resolved page", resolvedPage); // console.log("Resolved page", resolvedPage);
for (const pageMeta of allPages) { for (const pageMeta of allPages) {
if (pageMeta.name === cleanPage) { if (pageMeta.name === cleanPage) {