Fixes #90: Re-enables full text search

pull/109/head
Zef Hemel 2022-10-19 09:52:29 +02:00
parent e225c926f5
commit 70501bc3e4
13 changed files with 128 additions and 80 deletions

View File

@ -6,8 +6,8 @@ import { FileData, FileEncoding, SpacePrimitives } from "./space_primitives.ts";
import { Plug } from "../../plugos/plug.ts"; import { Plug } from "../../plugos/plug.ts";
import { mime } from "https://deno.land/x/mimetypes@v1.0.0/mod.ts"; import { mime } from "https://deno.land/x/mimetypes@v1.0.0/mod.ts";
import { import {
base64Decode, base64DecodeDataUrl,
base64Encode, base64EncodedDataUrl,
} from "../../plugos/asset_bundle/base64.ts"; } from "../../plugos/asset_bundle/base64.ts";
function lookupContentType(path: string): string { function lookupContentType(path: string): string {
@ -53,10 +53,10 @@ export class DiskSpacePrimitives implements SpacePrimitives {
case "dataurl": case "dataurl":
{ {
const f = await Deno.open(localPath, { read: true }); const f = await Deno.open(localPath, { read: true });
const buf = base64Encode(await readAll(f)); const buf = await readAll(f);
Deno.close(f.rid); Deno.close(f.rid);
data = `data:${contentType};base64,${buf}`; data = base64EncodedDataUrl(contentType, buf);
} }
break; break;
case "arraybuffer": case "arraybuffer":
@ -103,7 +103,7 @@ export class DiskSpacePrimitives implements SpacePrimitives {
case "dataurl": case "dataurl":
await Deno.writeFile( await Deno.writeFile(
localPath, localPath,
base64Decode((data as string).split(",")[1]), base64DecodeDataUrl(data as string),
); );
break; break;
case "arraybuffer": case "arraybuffer":

View File

@ -1,6 +1,11 @@
import { FileMeta } from "../types.ts"; import { FileMeta } from "../types.ts";
import { Plug } from "../../plugos/plug.ts"; import { Plug } from "../../plugos/plug.ts";
import { FileData, FileEncoding, SpacePrimitives } from "./space_primitives.ts"; import { FileData, FileEncoding, SpacePrimitives } from "./space_primitives.ts";
import {
base64DecodeDataUrl,
base64EncodedDataUrl,
} from "../../plugos/asset_bundle/base64.ts";
import { mime } from "../../plugos/deps.ts";
export class HttpSpacePrimitives implements SpacePrimitives { export class HttpSpacePrimitives implements SpacePrimitives {
fsUrl: string; fsUrl: string;
@ -50,14 +55,16 @@ export class HttpSpacePrimitives implements SpacePrimitives {
switch (encoding) { switch (encoding) {
case "arraybuffer": case "arraybuffer":
{ {
const abBlob = await res.blob(); data = await res.arrayBuffer();
data = await abBlob.arrayBuffer(); // data = await abBlob.arrayBuffer();
} }
break; break;
case "dataurl": case "dataurl":
{ {
const dUBlob = await res.blob(); data = base64EncodedDataUrl(
data = arrayBufferToDataUrl(await dUBlob.arrayBuffer()); mime.getType(name) || "application/octet-stream",
new Uint8Array(await res.arrayBuffer()),
);
} }
break; break;
case "string": case "string":
@ -83,7 +90,7 @@ export class HttpSpacePrimitives implements SpacePrimitives {
body = data; body = data;
break; break;
case "dataurl": case "dataurl":
data = dataUrlToArrayBuffer(data as string); data = base64DecodeDataUrl(data as string);
break; break;
} }
const res = await this.authenticatedFetch(`${this.fsUrl}/${name}`, { const res = await this.authenticatedFetch(`${this.fsUrl}/${name}`, {
@ -184,23 +191,3 @@ export class HttpSpacePrimitives implements SpacePrimitives {
} }
} }
} }
function dataUrlToArrayBuffer(dataUrl: string): ArrayBuffer {
const binary_string = atob(dataUrl.split(",")[1]);
const len = binary_string.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i);
}
return bytes.buffer;
}
function arrayBufferToDataUrl(buffer: ArrayBuffer): string {
let binary = "";
const bytes = new Uint8Array(buffer);
const len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return `data:application/octet-stream,${btoa(binary)}`;
}

View File

@ -1,4 +1,4 @@
import { base64Decode } from "../../plugos/asset_bundle/base64.ts"; import { base64DecodeDataUrl } from "../../plugos/asset_bundle/base64.ts";
import { syscall } from "./syscall.ts"; import { syscall } from "./syscall.ts";
export async function readAsset( export async function readAsset(
@ -8,7 +8,7 @@ export async function readAsset(
const dataUrl = await syscall("asset.readAsset", name) as string; const dataUrl = await syscall("asset.readAsset", name) as string;
switch (encoding) { switch (encoding) {
case "utf8": case "utf8":
return new TextDecoder().decode(base64Decode(dataUrl.split(",")[1])); return new TextDecoder().decode(base64DecodeDataUrl(dataUrl));
case "dataurl": case "dataurl":
return dataUrl; return dataUrl;
} }

View File

@ -1,5 +1,9 @@
import { assertEquals } from "../../test_deps.ts"; import { assertEquals } from "../../test_deps.ts";
import { base64Decode } from "./base64.ts"; import {
base64Decode,
base64DecodeDataUrl,
base64EncodedDataUrl,
} from "./base64.ts";
import { base64Encode } from "./base64.ts"; import { base64Encode } from "./base64.ts";
Deno.test("Base 64 encoding", () => { Deno.test("Base 64 encoding", () => {
@ -9,4 +13,9 @@ Deno.test("Base 64 encoding", () => {
buf[2] = 3; buf[2] = 3;
assertEquals(buf, base64Decode(base64Encode(buf))); assertEquals(buf, base64Decode(base64Encode(buf)));
assertEquals(
buf,
base64DecodeDataUrl(base64EncodedDataUrl("application/octet-stream", buf)),
);
}); });

View File

@ -16,3 +16,15 @@ export function base64Encode(buffer: Uint8Array): string {
} }
return btoa(binary); return btoa(binary);
} }
export function base64EncodedDataUrl(
mimeType: string,
buffer: Uint8Array,
): string {
return `data:${mimeType};base64,${base64Encode(buffer)}`;
}
export function base64DecodeDataUrl(dataUrl: string): Uint8Array {
const b64Encoded = dataUrl.split(",", 2)[1];
return base64Decode(b64Encoded);
}

View File

@ -1,4 +1,4 @@
import { base64Decode, base64Encode } from "./base64.ts"; import { base64Decode, base64EncodedDataUrl } from "./base64.ts";
import { mime } from "../deps.ts"; import { mime } from "../deps.ts";
type DataUrl = string; type DataUrl = string;
@ -57,9 +57,8 @@ export class AssetBundle {
} }
writeFileSync(path: string, data: Uint8Array) { writeFileSync(path: string, data: Uint8Array) {
const encoded = base64Encode(data); const mimeType = mime.getType(path) || "application/octet-stream";
const mimeType = mime.getType(path); this.bundle[path] = base64EncodedDataUrl(mimeType, data);
this.bundle[path] = `data:${mimeType};base64,${encoded}`;
} }
writeTextFileSync(path: string, s: string) { writeTextFileSync(path: string, s: string) {

View File

@ -1,6 +1,6 @@
import type { SysCallMapping } from "../system.ts"; import type { SysCallMapping } from "../system.ts";
import { mime, path } from "../deps.ts"; import { mime, path } from "../deps.ts";
import { base64Decode, base64Encode } from "../asset_bundle/base64.ts"; import { base64DecodeDataUrl, base64Encode } from "../asset_bundle/base64.ts";
import { FileMeta } from "../../common/types.ts"; import { FileMeta } from "../../common/types.ts";
export default function fileSystemSyscalls(root = "/"): SysCallMapping { export default function fileSystemSyscalls(root = "/"): SysCallMapping {
@ -51,7 +51,7 @@ export default function fileSystemSyscalls(root = "/"): SysCallMapping {
if (encoding === "utf8") { if (encoding === "utf8") {
await Deno.writeTextFile(p, text); await Deno.writeTextFile(p, text);
} else { } else {
await Deno.writeFile(p, base64Decode(text.split(",")[1])); await Deno.writeFile(p, base64DecodeDataUrl(text));
} }
const s = await Deno.stat(p); const s = await Deno.stat(p);
return { return {

View File

@ -2,27 +2,22 @@ import { SQLite } from "../../server/deps.ts";
import { SysCallMapping } from "../system.ts"; import { SysCallMapping } from "../system.ts";
import { asyncExecute, asyncQuery } from "./store.deno.ts"; import { asyncExecute, asyncQuery } from "./store.deno.ts";
type Item = {
key: string;
value: string;
};
export function ensureFTSTable( export function ensureFTSTable(
db: SQLite, db: SQLite,
tableName: string, tableName: string,
) { ) {
// const stmt = db.prepare( const result = db.query(
// `SELECT name FROM sqlite_master WHERE type='table' AND name=?`, `SELECT name FROM sqlite_master WHERE type='table' AND name=?`,
// ); [tableName],
// const result = stmt.all(tableName); );
// if (result.length === 0) { if (result.length === 0) {
// asyncExecute( asyncExecute(
// db, db,
// `CREATE VIRTUAL TABLE ${tableName} USING fts5(key, value);`, `CREATE VIRTUAL TABLE ${tableName} USING fts5(key, value);`,
// ); );
// console.log(`Created fts5 table ${tableName}`); console.log(`Created fts5 table ${tableName}`);
// } }
return Promise.resolve(); return Promise.resolve();
} }
@ -44,6 +39,7 @@ export function fullTextSearchSyscalls(
await asyncExecute(db, `DELETE FROM ${tableName} WHERE key = ?`, key); await asyncExecute(db, `DELETE FROM ${tableName} WHERE key = ?`, key);
}, },
"fulltext.search": async (_ctx, phrase: string, limit: number) => { "fulltext.search": async (_ctx, phrase: string, limit: number) => {
console.log("Got search query", phrase);
return ( return (
await asyncQuery<any>( await asyncQuery<any>(
db, db,

View File

@ -138,15 +138,15 @@ functions:
- page:complete - page:complete
# Full text search # Full text search
# searchIndex: searchIndex:
# path: ./search.ts:pageIndex path: ./search.ts:pageIndex
# events: events:
# - page:index - page:index
# searchUnindex: searchUnindex:
# path: "./search.ts:pageUnindex" path: "./search.ts:pageUnindex"
# env: server env: server
# events: events:
# - page:deleted - page:deleted
searchQueryProvider: searchQueryProvider:
path: ./search.ts:queryProvider path: ./search.ts:queryProvider
events: events:
@ -158,12 +158,12 @@ functions:
key: Ctrl-Shift-f key: Ctrl-Shift-f
mac: Cmd-Shift-f mac: Cmd-Shift-f
readPageSearch: readPageSearch:
path: ./search.ts:readPageSearch path: ./search.ts:readFileSearch
pageNamespace: pageNamespace:
pattern: "🔍 .+" pattern: "🔍 .+"
operation: readFile operation: readFile
getPageMetaSearch: getPageMetaSearch:
path: ./search.ts:getPageMetaSearch path: ./search.ts:getFileMetaSearch
pageNamespace: pageNamespace:
pattern: "🔍 .+" pattern: "🔍 .+"
operation: getFileMeta operation: getFileMeta

View File

@ -1,9 +1,18 @@
import { fulltext } from "$sb/plugos-syscall/mod.ts"; import { fulltext } from "$sb/plugos-syscall/mod.ts";
import { renderToText } from "$sb/lib/tree.ts"; import { renderToText } from "$sb/lib/tree.ts";
import type { PageMeta } from "../../common/types.ts"; import type { FileMeta, PageMeta } from "../../common/types.ts";
import { editor, index } from "$sb/silverbullet-syscall/mod.ts"; import { editor, index } from "$sb/silverbullet-syscall/mod.ts";
import { IndexTreeEvent, QueryProviderEvent } from "$sb/app_event.ts"; import { IndexTreeEvent, QueryProviderEvent } from "$sb/app_event.ts";
import { applyQuery, removeQueries } from "$sb/lib/query.ts"; import { applyQuery, removeQueries } from "$sb/lib/query.ts";
import {
FileData,
FileEncoding,
} from "../../common/spaces/space_primitives.ts";
import {
base64DecodeDataUrl,
base64Encode,
base64EncodedDataUrl,
} from "../../plugos/asset_bundle/base64.ts";
const searchPrefix = "🔍 "; const searchPrefix = "🔍 ";
@ -46,16 +55,20 @@ export async function queryProvider({
} }
export async function searchCommand() { export async function searchCommand() {
const phrase = await prompt("Search for: "); const phrase = await editor.prompt("Search for: ");
if (phrase) { if (phrase) {
await editor.navigate(`${searchPrefix}${phrase}`); await editor.navigate(`${searchPrefix}${phrase}`);
} }
} }
export async function readPageSearch( export async function readFileSearch(
name: string, name: string,
): Promise<{ text: string; meta: PageMeta }> { encoding: FileEncoding,
const phrase = name.substring(searchPrefix.length); ): Promise<{ data: FileData; meta: FileMeta }> {
const phrase = name.substring(
searchPrefix.length,
name.length - ".md".length,
);
const results = await fulltext.fullTextSearch(phrase, 100); const results = await fulltext.fullTextSearch(phrase, 100);
const text = `# Search results for "${phrase}"\n${ const text = `# Search results for "${phrase}"\n${
results results
@ -63,19 +76,28 @@ export async function readPageSearch(
.join("\n") .join("\n")
} }
`; `;
return { return {
text: text, // encoding === "arraybuffer" is not an option, so either it's "string" or "dataurl"
data: encoding === "string" ? text : base64EncodedDataUrl(
"text/markdown",
new TextEncoder().encode(text),
),
meta: { meta: {
name, name,
contentType: "text/markdown",
size: text.length,
lastModified: 0, lastModified: 0,
perm: "ro", perm: "ro",
}, },
}; };
} }
export function getPageMetaSearch(name: string): PageMeta { export function getFileMetaSearch(name: string): FileMeta {
return { return {
name, name,
contentType: "text/markdown",
size: -1,
lastModified: 0, lastModified: 0,
perm: "ro", perm: "ro",
}; };

View File

@ -1,3 +1,4 @@
export * from "../common/deps.ts"; export * from "../common/deps.ts";
export { DB as SQLite } from "https://deno.land/x/sqlite@v3.5.0/mod.ts"; export { DB as SQLite } from "../plugos/forked/deno-sqlite/mod.ts";
export { Application, Router } from "https://deno.land/x/oak@v11.1.0/mod.ts"; export { Application, Router } from "https://deno.land/x/oak@v11.1.0/mod.ts";
export * as etag from "https://deno.land/x/oak@v11.1.0/etag.ts";

View File

@ -6,6 +6,7 @@ import {
} from "../../common/spaces/space_primitives.ts"; } from "../../common/spaces/space_primitives.ts";
import { FileMeta } from "../../common/types.ts"; import { FileMeta } from "../../common/types.ts";
import { NamespaceOperation, PageNamespaceHook } from "./page_namespace.ts"; import { NamespaceOperation, PageNamespaceHook } from "./page_namespace.ts";
import { base64DecodeDataUrl } from "../../plugos/asset_bundle/base64.ts";
export class PlugSpacePrimitives implements SpacePrimitives { export class PlugSpacePrimitives implements SpacePrimitives {
constructor( constructor(
@ -46,14 +47,27 @@ export class PlugSpacePrimitives implements SpacePrimitives {
return allFiles; return allFiles;
} }
readFile( async readFile(
name: string, name: string,
encoding: FileEncoding, encoding: FileEncoding,
): Promise<{ data: FileData; meta: FileMeta }> { ): Promise<{ data: FileData; meta: FileMeta }> {
const result = this.performOperation("readFile", name); const wantArrayBuffer = encoding === "arraybuffer";
const result: { data: FileData; meta: FileMeta } | false = await this
.performOperation(
"readFile",
name,
wantArrayBuffer ? "dataurl" : encoding,
);
if (result) { if (result) {
if (wantArrayBuffer) {
return {
data: base64DecodeDataUrl(result.data as string),
meta: result.meta,
};
} else {
return result; return result;
} }
}
return this.wrapped.readFile(name, encoding); return this.wrapped.readFile(name, encoding);
} }

View File

@ -1,4 +1,4 @@
import { Application, path, Router, SQLite } from "./deps.ts"; import { Application, etag, path, Router, SQLite } from "./deps.ts";
import { Manifest, SilverBulletHooks } from "../common/manifest.ts"; import { Manifest, SilverBulletHooks } from "../common/manifest.ts";
import { loadMarkdownExtensions } from "../common/markdown_ext.ts"; import { loadMarkdownExtensions } from "../common/markdown_ext.ts";
import buildMarkdown from "../common/parser.ts"; import buildMarkdown from "../common/parser.ts";
@ -15,7 +15,10 @@ import { DenoCronHook } from "../plugos/hooks/cron.deno.ts";
import { esbuildSyscalls } from "../plugos/syscalls/esbuild.ts"; import { esbuildSyscalls } from "../plugos/syscalls/esbuild.ts";
import { eventSyscalls } from "../plugos/syscalls/event.ts"; import { eventSyscalls } from "../plugos/syscalls/event.ts";
import fileSystemSyscalls from "../plugos/syscalls/fs.deno.ts"; import fileSystemSyscalls from "../plugos/syscalls/fs.deno.ts";
import { fullTextSearchSyscalls } from "../plugos/syscalls/fulltext.knex_sqlite.ts"; import {
ensureFTSTable,
fullTextSearchSyscalls,
} from "../plugos/syscalls/fulltext.knex_sqlite.ts";
import sandboxSyscalls from "../plugos/syscalls/sandbox.ts"; import sandboxSyscalls from "../plugos/syscalls/sandbox.ts";
import shellSyscalls from "../plugos/syscalls/shell.node.ts"; import shellSyscalls from "../plugos/syscalls/shell.node.ts";
import { import {
@ -43,6 +46,7 @@ export type ServerOptions = {
}; };
const indexRequiredKey = "$spaceIndexed"; const indexRequiredKey = "$spaceIndexed";
const staticLastModified = new Date().toUTCString();
export class HttpServer { export class HttpServer {
app: Application; app: Application;
@ -198,7 +202,7 @@ export class HttpServer {
async start() { async start() {
await ensureIndexTable(this.db); await ensureIndexTable(this.db);
await ensureStoreTable(this.db, "store"); await ensureStoreTable(this.db, "store");
// await ensureFTSTable(this.db, "fts"); await ensureFTSTable(this.db, "fts");
await this.ensureAndLoadSettings(); await this.ensureAndLoadSettings();
// Load plugs // Load plugs
@ -211,6 +215,8 @@ export class HttpServer {
response.body = this.assetBundle.readTextFileSync( response.body = this.assetBundle.readTextFileSync(
"web/index.html", "web/index.html",
); );
response.headers.set("Last-Modified", staticLastModified);
response.headers.set("ETag", await etag.calculate(response.body));
return; return;
} }
try { try {
@ -224,6 +230,8 @@ export class HttpServer {
assetName, assetName,
); );
response.headers.set("Content-length", "" + data.length); response.headers.set("Content-length", "" + data.length);
response.headers.set("Last-Modified", staticLastModified);
response.headers.set("ETag", await etag.calculate(data));
if (request.method === "GET") { if (request.method === "GET") {
response.body = data; response.body = data;