From d7df0121001ef5df4cb2f6081070a65cf53b37ec Mon Sep 17 00:00:00 2001 From: "Parsa Yazdani (Quix)" Date: Wed, 27 Nov 2024 02:48:35 +1100 Subject: [PATCH 01/26] Tidy up docs --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index aeaa6eb..cbbc7da 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@

Hydrafiles

The (P2P) web privacy layer.

-Quick Install

+ Quick Install +



Please submit ideas & feature requests as well as any problems you're facing as an issue. From 5115e28dfd8cfdb45202bd5bf57e31ad1627a30f Mon Sep 17 00:00:00 2001 From: "Parsa Yazdani (Quix)" Date: Wed, 27 Nov 2024 02:49:20 +1100 Subject: [PATCH 02/26] baseDir --- src/config.ts | 6 ++++++ src/database.ts | 2 +- src/filesystem/filesystem.ts | 16 +++++++++------- web/dashboard.ts | 2 +- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/config.ts b/src/config.ts index 2b36564..f4812f2 100644 --- a/src/config.ts +++ b/src/config.ts @@ -176,6 +176,11 @@ export interface Config { * @default "" */ deriveKey: string; + + /** + * @default "./" + */ + baseDir: `${string}/`; } // DO NOT CHANGE DEFAULT CONFIG - Check documentation on how to set custom config. @@ -208,6 +213,7 @@ const defaultConfig: Config = { announceSpeed: 30000, dontUseFileSystemAPI: false, deriveKey: "", + baseDir: "./", }; /** @internal */ diff --git a/src/database.ts b/src/database.ts index 8f0a264..55bbff4 100644 --- a/src/database.ts +++ b/src/database.ts @@ -66,7 +66,7 @@ export default class Database { if (typeof window === "undefined") { const SQLite = (await import("jsr:@db/sqlite")).Database; - const db: DatabaseWrapperSQLite = { type: "SQLITE", db: new SQLite(`${model.tableName}.db`) }; + const db: DatabaseWrapperSQLite = { type: "SQLITE", db: new SQLite(`${client.config.baseDir}${model.tableName}.db`) }; database.db = db; const columns = Object.entries(model.columns) diff --git a/src/filesystem/filesystem.ts b/src/filesystem/filesystem.ts index cf97bf9..feae43b 100644 --- a/src/filesystem/filesystem.ts +++ b/src/filesystem/filesystem.ts @@ -6,16 +6,18 @@ import StandardFileSystem from "./StandardFileSystem.ts"; export default class FileSystem { fs!: StandardFileSystem | DirectoryHandleFileSystem | IndexedDBFileSystem; + private _client: Hydrafiles; constructor(client: Hydrafiles) { const fs = typeof window === "undefined" ? StandardFileSystem : (typeof globalThis.window.showDirectoryPicker !== "undefined" && !client.config.dontUseFileSystemAPI ? DirectoryHandleFileSystem : IndexedDBFileSystem); this.fs = new fs(); if (this.fs instanceof IndexedDBFileSystem) this.fs.dbPromise = this.fs.initDB(); + this._client = client; } - exists = async (path: string) => this.fs ? await this.fs.exists(path) : new ErrorNotInitialised(); - mkdir = async (path: `${string}/`) => this.fs ? await this.fs.mkdir(path) : new ErrorNotInitialised(); - readDir = async (path: `${string}/`) => this.fs ? await this.fs.readDir(path) : new ErrorNotInitialised(); - readFile = async (path: string) => this.fs ? await this.fs.readFile(path) : new ErrorNotInitialised(); - writeFile = async (path: string, data: Uint8Array) => this.fs ? await this.fs.writeFile(path, data) : new ErrorNotInitialised(); - getFileSize = async (path: string) => this.fs ? await this.fs.getFileSize(path) : new ErrorNotInitialised(); - remove = async (path: string) => this.fs ? await this.fs.remove(path) : new ErrorNotInitialised(); + exists = async (path: string) => this.fs ? await this.fs.exists(`${this._client.config.baseDir}${path}`) : new ErrorNotInitialised(); + mkdir = async (path: `${string}/`) => this.fs ? await this.fs.mkdir(`${this._client.config.baseDir}${path}`) : new ErrorNotInitialised(); + readDir = async (path: `${string}/`) => this.fs ? await this.fs.readDir(`${this._client.config.baseDir}${path}`) : new ErrorNotInitialised(); + readFile = async (path: string) => this.fs ? await this.fs.readFile(`${this._client.config.baseDir}${path}`) : new ErrorNotInitialised(); + writeFile = async (path: string, data: Uint8Array) => this.fs ? await this.fs.writeFile(`${this._client.config.baseDir}${path}`, data) : new ErrorNotInitialised(); + getFileSize = async (path: string) => this.fs ? await this.fs.getFileSize(`${this._client.config.baseDir}${path}`) : new ErrorNotInitialised(); + remove = async (path: string) => this.fs ? await this.fs.remove(`${this._client.config.baseDir}${path}`) : new ErrorNotInitialised(); } diff --git a/web/dashboard.ts b/web/dashboard.ts index 180ffa8..395ddcd 100644 --- a/web/dashboard.ts +++ b/web/dashboard.ts @@ -91,7 +91,7 @@ document.getElementById("startHydrafilesButton")!.addEventListener("click", asyn const buffer = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`${email}:${password}`)); const deriveKey = Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join(""); - window.hydrafiles = new Hydrafiles({ deriveKey, customPeers: [`${window.location.protocol}//${window.location.hostname}`] }); + window.hydrafiles = new Hydrafiles({ deriveKey, customPeers: [`${window.location.protocol}//${window.location.hostname}`], baseDir: "./dashboard/" }); const webtorrent = new WebTorrent(); await window.hydrafiles.start({ onUpdateFileListProgress, webtorrent }); From 8ab27ad63aa63bd9e069a227dd740d4f90df2d0b Mon Sep 17 00:00:00 2001 From: "Parsa Yazdani (Quix)" Date: Wed, 27 Nov 2024 02:49:39 +1100 Subject: [PATCH 03/26] Change default config --- src/config.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/config.ts b/src/config.ts index f4812f2..b7af319 100644 --- a/src/config.ts +++ b/src/config.ts @@ -208,9 +208,9 @@ const defaultConfig: Config = { logLevel: "normal", summarySpeed: 300000, backfill: true, - comparePeersSpeed: 3600000, - compareFilesSpeed: 300000, - announceSpeed: 30000, + comparePeersSpeed: 300000, + compareFilesSpeed: 600000, + announceSpeed: 60000, dontUseFileSystemAPI: false, deriveKey: "", baseDir: "./", From a94407c2a3348240447647b3926dcbcfe41bb8ec Mon Sep 17 00:00:00 2001 From: "Parsa Yazdani (Quix)" Date: Wed, 27 Nov 2024 02:51:53 +1100 Subject: [PATCH 04/26] baseDir --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 695a488..13aecdf 100644 --- a/.gitignore +++ b/.gitignore @@ -110,4 +110,6 @@ build/ __sysdb__.sqlite public/docs/ -typedoc-theme/ \ No newline at end of file +typedoc-theme/ + +sandbox/ \ No newline at end of file From a51d1b65378973ad25a701133429896ac093959d Mon Sep 17 00:00:00 2001 From: "Parsa Yazdani (Quix)" Date: Wed, 27 Nov 2024 02:53:53 +1100 Subject: [PATCH 05/26] Better logging --- src/database.ts | 2 +- src/errors.ts | 33 +++++++++++++++++++++++++++++++++ src/file.ts | 2 +- src/services/services.ts | 2 +- src/start.ts | 2 +- 5 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/database.ts b/src/database.ts index 55bbff4..72c18f6 100644 --- a/src/database.ts +++ b/src/database.ts @@ -361,7 +361,7 @@ export default class Database { } as Partial>; for (const [key, def] of Object.entries(this.model.columns)) { - if (!def.default && !def.isNullable && result[key as keyof DatabaseModal] === undefined) return new ErrorMissingRequiredProperty(`Missing required property: ${key}`); + if (!def.default && !def.isNullable && result[key as keyof DatabaseModal] === undefined) return new ErrorMissingRequiredProperty(key); } return result as DatabaseModal; diff --git a/src/errors.ts b/src/errors.ts index 20c97f6..36f2303 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -5,15 +5,31 @@ export class ErrorNotFound extends Error { readonly brand = Symbol(); } export class ErrorMissingRequiredProperty extends Error { + constructor(msg?: string) { + super(msg); + console.error("ErrorMissingRequiredProperty", this.stack); + } readonly brand = Symbol(); } export class ErrorUnreachableCodeReached extends Error { + constructor() { + super("Error of type 'ErrorUnreachableCodeReached' thrown"); + console.error("ErrorUnreachableCodeReached", this.stack); + } readonly brand = Symbol(); } export class ErrorNotInitialised extends Error { + constructor() { + super("Error of type 'ErrorNotInitialised' thrown"); + console.error("ErrorNotInitialised", this.stack); + } readonly brand = Symbol(); } export class ErrorWrongDatabaseType extends Error { + constructor() { + super("Error of type 'ErrorWrongDatabaseType' thrown"); + console.error("ErrorWrongDatabaseType", this.stack); + } readonly brand = Symbol(); } export class ErrorChecksumMismatch extends Error { @@ -23,11 +39,28 @@ export class ErrorRequestFailed extends Error { readonly brand = Symbol(); } export class ErrorDownloadFailed extends Error { + constructor() { + super("Error of type 'ErrorDownloadFailed' thrown"); + console.error("ErrorDownloadFailed", this.stack); + } readonly brand = Symbol(); } export class ErrorFailedToReadFile extends Error { + constructor() { + super("Error of type 'ErrorFailedToReadFile' thrown"); + console.error("ErrorFailedToReadFile", this.stack); + } readonly brand = Symbol(); } + export class ErrorInsufficientBalance extends Error { + constructor() { + super("Error of type 'ErrorInsufficientBalance' thrown"); + console.error("ErrorInsufficientBalance", this.stack); + } + readonly brand = Symbol(); +} + +export class ErrorUnexpectedProtocol extends Error { readonly brand = Symbol(); } diff --git a/src/file.ts b/src/file.ts index 6962548..758f897 100644 --- a/src/file.ts +++ b/src/file.ts @@ -476,7 +476,7 @@ class Files { try { files = files.concat(JSON.parse(response.text()) as FileAttributes[]); } catch (e) { - if (Files._client.config.logLevel === "verbose") console.log(e); + if (!(e instanceof SyntaxError)) throw e; } } } diff --git a/src/services/services.ts b/src/services/services.ts index 777c573..b0a1300 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -40,7 +40,7 @@ export default class Services { const wallet = new Wallet(1000 + seed); const api = new Service(wallet, requestHandler); const hostname = encodeBase32(wallet.address()).toUpperCase(); - console.log("Added hostname", hostname, api); + console.log(`Hostname: ${hostname} added`); this.ownedServices[hostname] = api; return hostname; } diff --git a/src/start.ts b/src/start.ts index f99d26f..972f24b 100644 --- a/src/start.ts +++ b/src/start.ts @@ -4,7 +4,7 @@ import Hydrafiles from "./hydrafiles.ts"; const configPath = Deno.args[0] ?? "config.json"; const config = JSON.parse(existsSync(configPath) ? new TextDecoder().decode(Deno.readFileSync(configPath)) : "{}"); const hydrafiles = new Hydrafiles(config); -hydrafiles.start().then(() => console.log("Hydrafiles Started", hydrafiles)); +hydrafiles.start().then(() => console.log("Hydrafiles Started")); // (async () => { // // Example Search From 9153e0b5a6faa5fbaa360a2953c8d24573067c5c Mon Sep 17 00:00:00 2001 From: "Parsa Yazdani (Quix)" Date: Wed, 27 Nov 2024 02:55:51 +1100 Subject: [PATCH 06/26] Dont re-export --- src/database.ts | 3 ++- src/hydrafiles.ts | 28 ---------------------------- src/rpc/client.ts | 2 +- src/services/services.ts | 2 +- web/dashboard.ts | 3 ++- 5 files changed, 6 insertions(+), 32 deletions(-) diff --git a/src/database.ts b/src/database.ts index 72c18f6..1d6f627 100644 --- a/src/database.ts +++ b/src/database.ts @@ -1,7 +1,8 @@ import type { Database as SQLite } from "jsr:@db/sqlite"; import type { indexedDB } from "https://deno.land/x/indexeddb@v1.1.0/ponyfill.ts"; import { ErrorMissingRequiredProperty, ErrorNotFound, ErrorNotInitialised } from "./errors.ts"; -import Hydrafiles, { type NonEmptyString } from "./hydrafiles.ts"; +import Hydrafiles from "./hydrafiles.ts"; +import type { NonEmptyString } from "./utils.ts"; export interface ModelType { tableName: string; diff --git a/src/hydrafiles.ts b/src/hydrafiles.ts index 80df627..6ab1854 100644 --- a/src/hydrafiles.ts +++ b/src/hydrafiles.ts @@ -130,31 +130,3 @@ class Hydrafiles { } export default Hydrafiles; - -// // Re-export the main class as default -// export { default } from "./hydrafiles.ts"; - -// // Export types -// export type { FileAttributes } from "./types"; -// export type { HydrafilesConfig } from "./config"; -// export type { RpcClient } from "./rpc"; -// export type { FilesDB } from "./db"; - -// // Export utilities and constants -// export { Utils } from "./utils"; -// export { processingRequests } from "./processing"; - -export * from "./wallet.ts"; -export * from "./utils.ts"; -export * from "./file.ts"; -export * from "./events.ts"; -export * from "./errors.ts"; -export * from "./config.ts"; -export * from "./services/services.ts"; -export * from "./rpc/server.ts"; -export * from "./rpc/routes.ts"; -export * from "./rpc/client.ts"; -export * from "./rpc/peers/http.ts"; -export * from "./rpc/peers/rtc.ts"; -export * from "./rpc/peers/ws.ts"; -export * from "./filesystem/filesystem.ts"; diff --git a/src/rpc/client.ts b/src/rpc/client.ts index aa0918e..e265eff 100644 --- a/src/rpc/client.ts +++ b/src/rpc/client.ts @@ -1,10 +1,10 @@ import { ErrorRequestFailed, ErrorTimeout } from "../errors.ts"; -import type { DecodedResponse } from "../hydrafiles.ts"; import type Hydrafiles from "../hydrafiles.ts"; import type Wallet from "../wallet.ts"; import HTTPPeers from "./peers/http.ts"; import RTCPeers from "./peers/rtc.ts"; import WSPeers from "./peers/ws.ts"; +import type { DecodedResponse } from "./routes.ts"; export default class RPCClient { static _client: Hydrafiles; diff --git a/src/services/services.ts b/src/services/services.ts index b0a1300..5dfac63 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1,6 +1,6 @@ import { DecodedResponse } from "./../rpc/routes.ts"; import type Hydrafiles from "../hydrafiles.ts"; -import { ErrorNotFound, ErrorRequestFailed } from "../hydrafiles.ts"; +import { ErrorNotFound, ErrorRequestFailed } from "../errors.ts"; import type { EthAddress } from "../wallet.ts"; import Wallet from "../wallet.ts"; import { decodeBase32, encodeBase32 } from "https://deno.land/std@0.224.0/encoding/base32.ts"; diff --git a/web/dashboard.ts b/web/dashboard.ts index 395ddcd..7a1e978 100644 --- a/web/dashboard.ts +++ b/web/dashboard.ts @@ -1,5 +1,5 @@ import { EthAddress } from "./../src/wallet.ts"; -import Hydrafiles, { FileEvent, RTCEvent } from "../src/hydrafiles.ts"; +import Hydrafiles from "../src/hydrafiles.ts"; import { type FileAttributes } from "../src/file.ts"; import WebTorrent from "https://esm.sh/webtorrent@2.5.1"; import { Chart } from "https://esm.sh/chart.js@4.4.6/auto"; @@ -9,6 +9,7 @@ import { DataSet } from "npm:vis-data/esnext"; import { Network } from "npm:vis-network/esnext"; import Utils from "../src/utils.ts"; import { Edge, Node } from "npm:vis-network/esnext"; +import type { FileEvent, RTCEvent } from "../src/events.ts"; declare global { interface Window { From dd7fb612388cdc4e6fe29981dee33ac8fdc07303 Mon Sep 17 00:00:00 2001 From: "Parsa Yazdani (Quix)" Date: Wed, 27 Nov 2024 02:57:14 +1100 Subject: [PATCH 07/26] generic fetch default to https --- src/rpc/server.ts | 2 +- src/services/NameService.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rpc/server.ts b/src/rpc/server.ts index ce12625..b3ce846 100644 --- a/src/rpc/server.ts +++ b/src/rpc/server.ts @@ -83,7 +83,7 @@ class RPCServer { if (response instanceof ErrorDownloadFailed) console.error("Test: Failed to download file from self"); else { console.log("Announcing HTTP server to nodes"); - RPCServer._client.rpcClient.fetch(`http://localhost/announce?host=${RPCServer._client.config.publicHostname}`); + RPCServer._client.rpcClient.fetch(`https://localhost/announce?host=${RPCServer._client.config.publicHostname}`); } await RPCServer._client.rpcClient.http.add(RPCServer._client.config.publicHostname); } diff --git a/src/services/NameService.ts b/src/services/NameService.ts index 0c4872d..6279af3 100644 --- a/src/services/NameService.ts +++ b/src/services/NameService.ts @@ -113,7 +113,7 @@ export default class BlockchainNameService { async fetchBlocks(): Promise { console.log(`Blocks: Fetching blocks from peers`); - const responses = await Promise.all(await BlockchainNameService._client.rpcClient.fetch("http://localhost/blocks")); + const responses = await Promise.all(await BlockchainNameService._client.rpcClient.fetch("https://localhost/blocks")); for (let i = 0; i < responses.length; i++) { const response = responses[i]; if (response instanceof Error) continue; From 8b07bd23a2d4517ededa6261a6240eeaa0f66e88 Mon Sep 17 00:00:00 2001 From: "Parsa Yazdani (Quix)" Date: Wed, 27 Nov 2024 02:57:35 +1100 Subject: [PATCH 08/26] Init mkdir --- src/hydrafiles.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/hydrafiles.ts b/src/hydrafiles.ts index 6ab1854..c50a8cb 100644 --- a/src/hydrafiles.ts +++ b/src/hydrafiles.ts @@ -66,6 +66,9 @@ class Hydrafiles { } public async start(opts: { onUpdateFileListProgress?: (progress: number, total: number) => void; webtorrent?: WebTorrent } = {}): Promise { + if (!await this.fs.exists("/")) await this.fs.mkdir("/"); // In case of un-initiated base dir + if (!await this.fs.exists("/files/")) await this.fs.mkdir("/files/"); + console.log("Startup: Populating FileDB"); this.files = await Files.init(); console.log("Startup: Populating RPC Client & Server"); From 0790c1536e06641ccaf5bfa7a9ffcaf34e993443 Mon Sep 17 00:00:00 2001 From: "Parsa Yazdani (Quix)" Date: Wed, 27 Nov 2024 02:57:56 +1100 Subject: [PATCH 09/26] Shorten code --- src/hydrafiles.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/hydrafiles.ts b/src/hydrafiles.ts index c50a8cb..e5f3faa 100644 --- a/src/hydrafiles.ts +++ b/src/hydrafiles.ts @@ -54,10 +54,7 @@ class Hydrafiles { this.filesWallet = new Wallet(); this.rtcWallet = new Wallet(1); this.services = new Services(); - this.services.addHostname((req: Request) => { - console.log(req); - return new Response("Hello World!"); - }, 0); + this.services.addHostname((_req: Request) => new Response("Hello World!"), 0); if (this.config.s3Endpoint.length) { console.log("Startup: Populating S3"); From c4eca434686cf8fe4775d0619706810e4c9a8044 Mon Sep 17 00:00:00 2001 From: "Parsa Yazdani (Quix)" Date: Wed, 27 Nov 2024 02:59:39 +1100 Subject: [PATCH 10/26] Better error types --- src/filesystem/StandardFileSystem.ts | 6 +++--- src/rpc/peers/http.ts | 17 ++++++++++------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/filesystem/StandardFileSystem.ts b/src/filesystem/StandardFileSystem.ts index d5914c5..7135c1e 100644 --- a/src/filesystem/StandardFileSystem.ts +++ b/src/filesystem/StandardFileSystem.ts @@ -1,12 +1,12 @@ import { ErrorNotFound } from "../errors.ts"; export default class StandardFileSystem { - exists = async (path: string): Promise => { + exists = async (path: string): Promise => { try { await Deno.stat(path); return true; } catch (e) { - if (e instanceof Deno.errors.NotFound) return new ErrorNotFound(); + if (e instanceof Deno.errors.NotFound) return false; console.error((e as Error).message); throw e; } @@ -26,7 +26,7 @@ export default class StandardFileSystem { }; readFile = async (path: string): Promise => { - if (await this.exists(path) instanceof ErrorNotFound) return new ErrorNotFound(); + if (!await this.exists(path)) return new ErrorNotFound(); return await Deno.readFile(path); }; diff --git a/src/rpc/peers/http.ts b/src/rpc/peers/http.ts index c7f8ac6..c3a4783 100644 --- a/src/rpc/peers/http.ts +++ b/src/rpc/peers/http.ts @@ -3,7 +3,7 @@ import Database, { type DatabaseModal } from "../../database.ts"; import { File } from "../../file.ts"; import RPCClient from "../client.ts"; import type { EthAddress } from "../../wallet.ts"; -import { ErrorChecksumMismatch, ErrorDownloadFailed, ErrorMissingRequiredProperty, ErrorRequestFailed, ErrorTimeout } from "../../errors.ts"; +import { ErrorChecksumMismatch, ErrorDownloadFailed, ErrorMissingRequiredProperty, ErrorRequestFailed, ErrorTimeout, ErrorUnexpectedProtocol } from "../../errors.ts"; import { ErrorNotFound } from "../../errors.ts"; import { DecodedResponse } from "../routes.ts"; @@ -55,8 +55,9 @@ class HTTPPeer implements PeerAttributes { * @returns {HTTPPeer} A new instance of HTTPPeer. * @default */ - static async init(values: Partial>, db: Database): Promise { + static async init(values: Partial>, db: Database): Promise { if (values.host === undefined) return new ErrorMissingRequiredProperty(); + if (values.host.startsWith("hydra:")) return new ErrorUnexpectedProtocol(); const result = new URL(values.host); if (!result.protocol || !result.host || result.protocol === "hydra") throw new Error("Invalid URL"); @@ -87,7 +88,8 @@ class HTTPPeer implements PeerAttributes { response = await Utils.promiseWithTimeout(fetch(`${this.host}/download/${hash}`), RPCClient._client.config.timeout); } catch (e) { if (RPCClient._client.config.logLevel === "verbose") console.error(e); - return new ErrorRequestFailed(); + const message = e instanceof Error ? e.message : "Unknown error"; + return new ErrorRequestFailed(message); } if (response instanceof ErrorTimeout) return new ErrorTimeout(); const fileContent = new Uint8Array(await response.arrayBuffer()); @@ -150,7 +152,7 @@ export default class HTTPPeers { const httpPeers = new HTTPPeers(db); (await Promise.all((await db.select()).map((peer) => HTTPPeer.init(peer, db)))).forEach((peer) => { - if (!(peer instanceof ErrorMissingRequiredProperty)) httpPeers.peers.set(peer.host, peer); + if (!(peer instanceof Error)) httpPeers.peers.set(peer.host, peer); }); for (let i = 0; i < RPCClient._client.config.bootstrapPeers.length; i++) { @@ -163,9 +165,9 @@ export default class HTTPPeers { return httpPeers; } - async add(host: string): Promise { + async add(host: string): Promise { const peer = await HTTPPeer.init({ host }, this.db); - if (peer instanceof ErrorMissingRequiredProperty) return peer; + if (peer instanceof Error) return peer; if (host !== RPCClient._client.config.publicHostname) this.peers.set(peer.host, peer); return true; } @@ -212,7 +214,8 @@ export default class HTTPPeers { return await DecodedResponse.from(res); } catch (e) { if (RPCClient._client.config.logLevel === "verbose") console.error(e); - return new ErrorRequestFailed(); + const message = e instanceof Error ? e.message : "Unknown error"; + return new ErrorRequestFailed(message); } }); From 913e72c44be6ee72dabbad3e870c33261328d9ba Mon Sep 17 00:00:00 2001 From: "Parsa Yazdani (Quix)" Date: Wed, 27 Nov 2024 02:59:53 +1100 Subject: [PATCH 11/26] Remove unnecessary await --- src/file.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/file.ts b/src/file.ts index 758f897..6f0e714 100644 --- a/src/file.ts +++ b/src/file.ts @@ -386,7 +386,7 @@ export class File implements FileAttributes { const responses = Files._client.rpcClient.rtc.fetch(new URL(`http://localhost/download/${this.hash}`)); for (let i = 0; i < responses.length; i++) { const response = await responses[i]; - const fileContent = new Uint8Array(await response.arrayBuffer()); + const fileContent = new Uint8Array(response.arrayBuffer()); console.log(`File: ${this.hash} Validating hash`); const verifiedHash = await Utils.hashUint8Array(fileContent); console.log(`File: ${this.hash} Done Validating hash`); From dd283bc3f5ed8351c3f7dc0238ceceb94c8da03f Mon Sep 17 00:00:00 2001 From: "Parsa Yazdani (Quix)" Date: Wed, 27 Nov 2024 03:00:26 +1100 Subject: [PATCH 12/26] config.listen option --- src/config.ts | 8 ++++++++ src/rpc/server.ts | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/config.ts b/src/config.ts index b7af319..7d43947 100644 --- a/src/config.ts +++ b/src/config.ts @@ -178,9 +178,16 @@ export interface Config { deriveKey: string; /** + * Base directory to save files to. * @default "./" */ baseDir: `${string}/`; + + /** + * Whether or not to listen for & serve requests via HTTP, WebRTC, WS. Disabling this breaks many P2P functionalities such as lowing data availability and lowering privacy. Static files (i.e. GUI and docs) are still served. + * @default true + */ + listen: boolean; } // DO NOT CHANGE DEFAULT CONFIG - Check documentation on how to set custom config. @@ -214,6 +221,7 @@ const defaultConfig: Config = { dontUseFileSystemAPI: false, deriveKey: "", baseDir: "./", + listen: true, }; /** @internal */ diff --git a/src/rpc/server.ts b/src/rpc/server.ts index b3ce846..4d47b3d 100644 --- a/src/rpc/server.ts +++ b/src/rpc/server.ts @@ -111,7 +111,7 @@ class RPCServer { const filePath = `./public${url.pathname.endsWith("/") ? `${url.pathname}index.html` : url.pathname}`; return await serveFile(req, filePath); } catch (_) { - const routeHandler = req.headers.get("upgrade") === "websocket" ? router.get("WS") : router.get(`/${url.pathname.split("/")[1]}`); + if (!RPCServer._client.config.listen) return new Response("Peer has peering disabled"); if (routeHandler) { const response = await routeHandler(req, RPCServer._client); if (response instanceof Response) return response; From 100157fe04d93b69c5050517e10e1c71a3a3cb73 Mon Sep 17 00:00:00 2001 From: "Parsa Yazdani (Quix)" Date: Wed, 27 Nov 2024 03:03:00 +1100 Subject: [PATCH 13/26] Temporarily pause /upload endpoint - needs to be rethought --- src/rpc/routes.ts | 49 +++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/src/rpc/routes.ts b/src/rpc/routes.ts index 8df4361..d03a460 100644 --- a/src/rpc/routes.ts +++ b/src/rpc/routes.ts @@ -280,38 +280,37 @@ router.set("/infohash", async (req, client): Promise => { return response; }); -router.set("/upload", async (req, client) => { - const uploadSecret = req.headers.get("x-hydra-upload-secret"); - if (uploadSecret !== client.config.uploadSecret) { - return new DecodedResponse("401 Unauthorized\n", { status: 401 }); - } +// router.set("/upload", async (req, client) => { +// const uploadSecret = req.headers.get("x-hydra-upload-secret"); +// if (uploadSecret !== client.config.uploadSecret) { +// return new DecodedResponse("401 Unauthorized\n", { status: 401 }); +// } - const form = await req.formData(); - const formData = { - hash: form.get("hash")?.toString(), - file: form.get("file") as globalThis.File | null, - }; +// const form = await req.formData(); +// const formData = { +// hash: form.get("hash")?.toString(), +// file: form.get("file") as globalThis.File | null, +// }; - if (typeof formData.hash === "undefined" || typeof formData.file === "undefined" || formData.file === null) return new DecodedResponse("400 Bad Request\n", { status: 400 }); +// if (typeof formData.hash === "undefined" || typeof formData.file === "undefined" || formData.file === null) return new DecodedResponse("400 Bad Request\n", { status: 400 }); - const hash = Utils.sha256(formData.hash[0]); +// const hash = Utils.sha256(formData.hash[0]); - const file = await File.init({ hash }, true); - if (!file) throw new Error("Failed to build file"); - if (!file.name && formData.file.name !== null) { - file.name = formData.file.name; - file.cacheFile(new Uint8Array(await formData.file.arrayBuffer())); - file.save(); - } +// const file = await File.init({ hash }, true); +// if (!file) throw new Error("Failed to build file"); +// if (!file.name && formData.file.name !== null) { +// file.name = formData.file.name; +// file.cacheFile(new Uint8Array(await formData.file.arrayBuffer())); +// file.save(); +// } - console.log("Uploading", file.hash); +// console.log("Uploading", file.hash); - if (await client.fs.exists(join("files", file.hash))) return new DecodedResponse("200 OK\n"); +// if (await client.fs.exists(join("files", file.hash))) return new DecodedResponse("200 OK\n"); - if (!client.config.permaFiles.includes(hash)) client.config.permaFiles.push(hash); - await client.fs.writeFile("config.json", new TextEncoder().encode(JSON.stringify(client.config, null, 2))); - return new DecodedResponse("200 OK\n"); -}); +// if (!client.config.permaFiles.includes(hash)) client.config.permaFiles.push(hash); // TODO: Save this +// return new DecodedResponse("200 OK\n"); +// }); router.set("/files", (_, client) => { const rows = Array.from(client.files.getFiles()).map((row) => { From 8e5ae944729065befcbc46c875ec9afc6b80ce7a Mon Sep 17 00:00:00 2001 From: "Parsa Yazdani (Quix)" Date: Wed, 27 Nov 2024 03:05:02 +1100 Subject: [PATCH 14/26] Refactor websocksets to be bi-directional p2p instead of client-server --- src/rpc/client.ts | 4 ++-- src/rpc/peers/rtc.ts | 55 +++++++++++++++----------------------------- src/rpc/peers/ws.ts | 55 +++++++++++++++++++++++++++++++++++++++++--- src/rpc/routes.ts | 30 ------------------------ src/rpc/server.ts | 1 + 5 files changed, 73 insertions(+), 72 deletions(-) diff --git a/src/rpc/client.ts b/src/rpc/client.ts index e265eff..e1c311f 100644 --- a/src/rpc/client.ts +++ b/src/rpc/client.ts @@ -9,16 +9,16 @@ import type { DecodedResponse } from "./routes.ts"; export default class RPCClient { static _client: Hydrafiles; http!: HTTPPeers; - rtc!: RTCPeers; ws!: WSPeers; + rtc!: RTCPeers; private constructor() {} static async init(): Promise { const rpcClient = new RPCClient(); rpcClient.http = await HTTPPeers.init(); - rpcClient.rtc = new RTCPeers(rpcClient); rpcClient.ws = new WSPeers(rpcClient); + rpcClient.rtc = new RTCPeers(rpcClient); return rpcClient; } diff --git a/src/rpc/peers/rtc.ts b/src/rpc/peers/rtc.ts index 29902b9..48eafd4 100644 --- a/src/rpc/peers/rtc.ts +++ b/src/rpc/peers/rtc.ts @@ -45,36 +45,24 @@ function arrayBufferToUnicodeString(buffer: ArrayBuffer): string { const receivedPackets: Record = {}; class RTCPeers { + private _rpcClient: RPCClient; peerId: EthAddress; - websockets: WebSocket[]; peers: PeerConnections = {}; - messageQueue: WSMessage[] = []; seenMessages: Set = new Set(); constructor(rpcClient: RPCClient) { - this.websockets = [new WebSocket("wss://rooms.deno.dev/")]; - + this._rpcClient = rpcClient; this.peerId = RPCClient._client.rtcWallet.account.address; - const peers = rpcClient.http.getPeers(true); - for (let i = 0; i < peers.length; i++) { - try { - this.websockets.push(new WebSocket(peers[i].host.replace("https://", "wss://").replace("http://", "ws://"))); - } catch (e) { - if (RPCClient._client.config.logLevel === "verbose") console.error(e); - continue; - } - } - - for (let i = 0; i < this.websockets.length; i++) { - this.websockets[i].onopen = () => { - console.log(`WebRTC: Announcing to ${this.websockets[i].url}`); + for (let i = 0; i < rpcClient.ws.peers.length; i++) { + rpcClient.ws.peers[i].socket.onopen = () => { + console.log(`WebRTC: Announcing to ${rpcClient.ws.peers[i].socket.url}`); const message: WSMessage = { announce: true, from: this.peerId }; - this.wsMessage(message); - setInterval(() => this.wsMessage(message), RPCClient._client.config.announceSpeed); + rpcClient.ws.send(message); + setInterval(() => rpcClient.ws.send(message), RPCClient._client.config.announceSpeed); }; - this.websockets[i].onmessage = async (event) => { + rpcClient.ws.peers[i].socket.onmessage = async (event) => { const message = JSON.parse(event.data) as WSMessage; if (message === null || message.from === this.peerId || this.seenMessages.has(event.data) || ("to" in message && message.to !== this.peerId)) return; this.seenMessages.add(event.data); @@ -82,7 +70,7 @@ class RTCPeers { else if ("offer" in message) await this.handleOffer(message.from, message.offer); else if ("answer" in message) await this.handleAnswer(message.from, message.answer); else if ("iceCandidate" in message) this.handleIceCandidate(message.from, message.iceCandidate); - else if ("request" in message) this.handleWsRequest(this.websockets[i], message); + else if ("request" in message) this.handleWsRequest(rpcClient.ws.peers[i].socket, message); else if (!("response" in message)) console.warn("WebRTC: Unknown message type received", message); }; } @@ -109,7 +97,10 @@ class RTCPeers { channel.onmessage = async (e) => { console.log(`WebRTC: Received request`); const { id, url, ...data } = JSON.parse(e.data as string); - const req = new Request(url, data); + const newUrl = new URL(url); + newUrl.protocol = "rtc:"; + newUrl.hostname = "0.0.0.0"; + const req = new Request(newUrl, data); const response = await RPCClient._client.rpcServer.handleRequest(req); const headers: Record = {}; response.headers.forEach((value, key) => { @@ -148,7 +139,7 @@ class RTCPeers { conn.onicecandidate = (event) => { if (event.candidate) { if (RPCClient._client.config.logLevel === "verbose") console.log(`WebRTC: ${from} Sending ICE candidate`); - this.wsMessage({ iceCandidate: event.candidate, to: from, from: this.peerId }); + this._rpcClient.ws.send({ iceCandidate: event.candidate, to: from, from: this.peerId }); } }; conn.onnegotiationneeded = async () => { @@ -158,7 +149,7 @@ class RTCPeers { const offer = await conn.createOffer(); await conn.setLocalDescription(offer); console.log(`WebRTC: ${from} Sending offer from`, extractIPAddress(offer.sdp)); - this.wsMessage({ offer, to: from, from: this.peerId }); + this._rpcClient.ws.send({ offer, to: from, from: this.peerId }); } catch (e) { console.error(e); } @@ -192,18 +183,6 @@ class RTCPeers { } } - wsMessage(message: WSMessage): void { - this.messageQueue.push(message); - for (let i = 0; i < this.websockets.length; i++) { - if (this.websockets[i].readyState === 1) this.websockets[i].send(JSON.stringify(message)); - else { - this.websockets[i].addEventListener("open", () => { - this.websockets[i].send(JSON.stringify(message)); - }); - } - } - } - async handleAnnounce(from: EthAddress): Promise { RPCClient._client.events.log(RPCClient._client.events.rtcEvents.RTCAnnounce); console.log(`WebRTC: ${from} Received announce`); @@ -245,7 +224,7 @@ class RTCPeers { await this.peers[from].answered.conn.setLocalDescription(answer); console.log(`WebRTC: ${from} Sending answer from`, extractIPAddress(answer.sdp)); - this.wsMessage({ answer, to: from, from: this.peerId }); + this._rpcClient.ws.send({ answer, to: from, from: this.peerId }); } catch (e) { console.error(e); } @@ -288,6 +267,8 @@ class RTCPeers { } public fetch(url: URL, method = "GET", headers: { [key: string]: string } = {}, body: string | undefined = undefined): Promise[] { + url.protocol = "rtc:"; + url.hostname = "0.0.0.0"; const requestId = Math.random(); const request = { method, url, headers, body: method === "GET" ? null : body, id: requestId }; const connIDs = Object.keys(this.peers); diff --git a/src/rpc/peers/ws.ts b/src/rpc/peers/ws.ts index 6db9bf9..caacc69 100644 --- a/src/rpc/peers/ws.ts +++ b/src/rpc/peers/ws.ts @@ -1,23 +1,44 @@ import { ErrorTimeout } from "../../errors.ts"; import Utils from "../../utils.ts"; import RPCClient from "../client.ts"; -import { DecodedResponse, pendingWSRequests, sockets } from "../routes.ts"; +import { DecodedResponse, pendingWSRequests } from "../routes.ts"; import type { WSMessage } from "./rtc.ts"; export default class WSPeers { private _rpcClient: RPCClient; + peers: { id: string; socket: WebSocket }[] = [{ id: RPCClient._client.rtcWallet.account.address, socket: new WebSocket("wss://rooms.deno.dev/") }]; + messageQueue: WSMessage[] = []; constructor(rpcClient: RPCClient) { this._rpcClient = rpcClient; + + const peers = rpcClient.http.getPeers(true); + for (let i = 0; i < peers.length; i++) { + this.peers.push({ id: RPCClient._client.rtcWallet.account.address, socket: new WebSocket(peers[i].host.replace("https://", "wss://").replace("http://", "ws://")) }); + } + } + + send(message: WSMessage): void { + this.messageQueue.push(message); + for (let i = 0; i < this.peers.length; i++) { + if (this.peers[i].socket.readyState === 1) this.peers[i].socket.send(JSON.stringify(message)); + else { + this.peers[i].socket.addEventListener("open", () => { + this.peers[i].socket.send(JSON.stringify(message)); + }); + } + } } public fetch(url: URL, method = "GET", headers: { [key: string]: string } = {}, body: string | undefined = undefined): Promise[] { - if (!sockets.length) return []; + if (!this.peers.length) return []; + url.protocol = "wss:"; + url.hostname = "0.0.0.0"; const requestId = Math.random(); const request: WSMessage = { request: { method, url: url.toString(), headers, body: method === "GET" ? undefined : body }, id: requestId, from: this._rpcClient.rtc.peerId }; - const responses = sockets.map(async (socket) => { + const responses = this.peers.map(async (socket) => { return await Utils.promiseWithTimeout( new Promise((resolve) => { pendingWSRequests.set(requestId, resolve); @@ -29,4 +50,32 @@ export default class WSPeers { return responses; } + + handleConnection(req: Request): Response { + const { socket, response } = Deno.upgradeWebSocket(req); + this.peers.push({ socket, id: "" }); + + socket.addEventListener("message", ({ data }) => { + const message = JSON.parse(data) as WSMessage | null; + if (message === null) return; + if ("response" in message) { + const resolve = pendingWSRequests.get(message.id); + if (resolve) { + const { status, headers, body } = message.response; + resolve(new DecodedResponse(body, { status, headers })); + pendingWSRequests.delete(message.id); + } + } + for (let i = 0; i < this.peers.length; i++) { + if (this.peers[i].socket !== socket && (!("to" in message) || message.to === this.peers[i].id)) { + if (this.peers[i].socket.readyState === 1) this.peers[i].socket.send(data); + } else if ("from" in message) { + this.peers[i].id = message.from; + } + } + }); + + (response as Response & { ws: true }).ws = true; + return response as Response & { ws: true }; + } } diff --git a/src/rpc/routes.ts b/src/rpc/routes.ts index d03a460..893b9fc 100644 --- a/src/rpc/routes.ts +++ b/src/rpc/routes.ts @@ -1,5 +1,4 @@ import { join } from "https://deno.land/std@0.224.0/path/mod.ts"; -import { WSMessage } from "./peers/rtc.ts"; import { File } from "../file.ts"; import type { PeerAttributes } from "./peers/http.ts"; import Utils, { type Sha256 } from "../utils.ts"; @@ -46,39 +45,10 @@ export class DecodedResponse { } export const router = new Map Promise | DecodedResponse | (Response & { ws: true })>(); -export const sockets: { id: string; socket: WebSocket }[] = []; export const pendingWSRequests = new Map void>(); export const processingDownloads = new Map>(); -router.set("WS", (req) => { - const { socket, response } = Deno.upgradeWebSocket(req); - sockets.push({ socket, id: "" }); - - socket.addEventListener("message", ({ data }) => { - const message = JSON.parse(data) as WSMessage | null; - if (message === null) return; - if ("response" in message) { - const resolve = pendingWSRequests.get(message.id); - if (resolve) { - const { status, headers, body } = message.response; - resolve(new DecodedResponse(body, { status, headers })); - pendingWSRequests.delete(message.id); - } - } - for (let i = 0; i < sockets.length; i++) { - if (sockets[i].socket !== socket && (!("to" in message) || message.to === sockets[i].id)) { - if (sockets[i].socket.readyState === 1) sockets[i].socket.send(data); - } else if ("from" in message) { - sockets[i].id = message.from; - } - } - }); - - (response as Response & { ws: true }).ws = true; - return response as Response & { ws: true }; -}); - router.set("/status", () => { const headers = { "Content-Type": "application/json", diff --git a/src/rpc/server.ts b/src/rpc/server.ts index 4d47b3d..7d342c2 100644 --- a/src/rpc/server.ts +++ b/src/rpc/server.ts @@ -112,6 +112,7 @@ class RPCServer { return await serveFile(req, filePath); } catch (_) { if (!RPCServer._client.config.listen) return new Response("Peer has peering disabled"); + const routeHandler = req.headers.get("upgrade") === "websocket" ? RPCServer._client.rpcClient.ws.handleConnection : router.get(`/${url.pathname.split("/")[1]}`); if (routeHandler) { const response = await routeHandler(req, RPCServer._client); if (response instanceof Response) return response; From ff50971ddcd162cc4d9fe01ccc45550a8ea6aeff Mon Sep 17 00:00:00 2001 From: "Parsa Yazdani (Quix)" Date: Wed, 27 Nov 2024 03:56:01 +1100 Subject: [PATCH 15/26] baseDir --- src/config.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/config.ts b/src/config.ts index 7d43947..1d7ca6c 100644 --- a/src/config.ts +++ b/src/config.ts @@ -19,14 +19,14 @@ export interface Config { httpsPort: number; /** - * SSL Certificate Path. - * @default "./certs/ca/localhost/localhost.crt" + * SSL Certificate Path. Replace `../` with `./` if replacing the cert. + * @default "../certs/ca/localhost/localhost.crt" */ sslCertPath: string; /** - * SSL Key Path. - * @default "./certs/ca/localhost/localhost.key" + * SSL Key Path. Replace `../` with `./` if replacing the cert. + * @default "../certs/ca/localhost/localhost.key" */ sslKeyPath: string; @@ -195,8 +195,8 @@ const defaultConfig: Config = { hostname: "0.0.0.0", httpPort: 80, httpsPort: 443, - sslCertPath: "./certs/ca/localhost/localhost.crt", - sslKeyPath: "./certs/ca/localhost/localhost.key", + sslCertPath: "../certs/ca/localhost/localhost.crt", + sslKeyPath: "../certs/ca/localhost/localhost.key", publicHostname: "http://127.0.0.1:80", maxCache: -1, permaFiles: ["04aa07009174edc6f03224f003a435bcdc9033d2c52348f3a35fbb342ea82f6f"], From 83434fabe322cb1844b9f437dd1dbd7ec5ce1c2a Mon Sep 17 00:00:00 2001 From: "Parsa Yazdani (Quix)" Date: Wed, 27 Nov 2024 03:57:04 +1100 Subject: [PATCH 16/26] nicer logs --- src/database.ts | 10 +++++----- src/file.ts | 4 ++-- src/hydrafiles.ts | 9 +++++---- src/rpc/peers/http.ts | 2 +- src/rpc/server.ts | 16 ++++++++-------- src/start.ts | 2 +- 6 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/database.ts b/src/database.ts index 1d6f627..f4fc4ca 100644 --- a/src/database.ts +++ b/src/database.ts @@ -84,11 +84,11 @@ export default class Database { Object.entries(model.columns).forEach(([name, def]) => addColumnIfNotExists(db.db, model.tableName, name, def.type)); } else { const db = await new Promise((resolve, reject) => { - console.log(`Startup: ${model.tableName}DB: Opening IndexedDB Connection`); + console.log(`Database: ${model.tableName}DB: Opening IndexedDB Connection`); // @ts-expect-error: const request = indexedDB.open(model.tableName, 2); request.onupgradeneeded = (event): void => { - console.log(`Startup: ${model.tableName}DB: On Upgrade Needed`); + console.log(`Database: ${model.tableName}DB: On Upgrade Needed`); // @ts-expect-error: if (!event.target.result.objectStoreNames.contains(model.tableName)) { // @ts-expect-error: @@ -103,15 +103,15 @@ export default class Database { } }; request.onsuccess = () => { - console.log(`Startup: ${model.tableName}DB: On Success`); + console.log(`Database: ${model.tableName}DB: On Success`); resolve(request.result as unknown as IDBDatabase); }; request.onerror = () => { - console.error(`Startup: ${model.tableName}DB error:`, request.error); + console.error(`Database: ${model.tableName}DB error:`, request.error); reject(request.error); }; request.onblocked = () => { - console.error(`Startup: ${model.tableName}DB: Blocked. Close other tabs with this site open.`); + console.error(`Database: ${model.tableName}DB: Blocked. Close other tabs with this site open.`); }; }); database.db = { type: "INDEXEDDB", db: db }; diff --git a/src/file.ts b/src/file.ts index 6f0e714..612ea24 100644 --- a/src/file.ts +++ b/src/file.ts @@ -448,7 +448,7 @@ class Files { backfillFiles = (): void => { setTimeout(async () => { while (true) { - console.log("Backfilling file"); + console.log("Files: Backfilling file"); const keys = Array.from(this.filesHash.keys()); if (keys.length === 0) { await delay(500); @@ -467,7 +467,7 @@ class Files { // TODO: Compare list between all peers and give score based on how similar they are. 100% = all exactly the same, 0% = no items in list were shared. The lower the score, the lower the propagation times, the lower the decentralisation async updateFileList(onProgress?: (progress: number, total: number) => void): Promise { - console.log(`Comparing file list`); + console.log(`Files: Comparing file list`); let files: FileAttributes[] = []; const responses = await Promise.all(await Files._client.rpcClient.fetch("http://localhost/files")); for (let i = 0; i < responses.length; i++) { diff --git a/src/hydrafiles.ts b/src/hydrafiles.ts index e5f3faa..441f8f0 100644 --- a/src/hydrafiles.ts +++ b/src/hydrafiles.ts @@ -57,7 +57,7 @@ class Hydrafiles { this.services.addHostname((_req: Request) => new Response("Hello World!"), 0); if (this.config.s3Endpoint.length) { - console.log("Startup: Populating S3"); + console.log("Startup: Populating S3"); this.s3 = new S3Client({ endPoint: this.config.s3Endpoint, region: "us-east-1", bucket: "uploads", accessKey: this.config.s3AccessKeyId, secretKey: this.config.s3SecretAccessKey, pathStyle: false }); } } @@ -66,12 +66,13 @@ class Hydrafiles { if (!await this.fs.exists("/")) await this.fs.mkdir("/"); // In case of un-initiated base dir if (!await this.fs.exists("/files/")) await this.fs.mkdir("/files/"); - console.log("Startup: Populating FileDB"); + console.log("Startup: Populating FileDB"); this.files = await Files.init(); - console.log("Startup: Populating RPC Client & Server"); + console.log("Startup: Populating RPC Client & Server"); this.rpcClient = await RPCClient.init(); this.rpcServer = new RPCServer(); - console.log("Startup: Starting WebTorrent"); + console.log("Startup: Starting HTTP Server"); + console.log("Startup: Starting WebTorrent"); this.webtorrent = opts.webtorrent; NameService._client = this; this.nameService = await NameService.init(); diff --git a/src/rpc/peers/http.ts b/src/rpc/peers/http.ts index c3a4783..6b244fa 100644 --- a/src/rpc/peers/http.ts +++ b/src/rpc/peers/http.ts @@ -224,7 +224,7 @@ export default class HTTPPeers { // TODO: Compare list between all peers and give score based on how similar they are. 100% = all exactly the same, 0% = no items in list were shared. The lower the score, the lower the propagation times, the lower the decentralisation async updatePeers(): Promise { - console.log(`Fetching peers`); + console.log(`HTTP: Fetching peers`); const responses = await Promise.all(await RPCClient._client.rpcClient.fetch("http://localhost/peers")); for (let i = 0; i < responses.length; i++) { try { diff --git a/src/rpc/server.ts b/src/rpc/server.ts index 7d342c2..8bed6ff 100644 --- a/src/rpc/server.ts +++ b/src/rpc/server.ts @@ -64,25 +64,25 @@ class RPCServer { } private onListen = async (hostname: string, port: number): Promise => { - console.log(`Server started at ${hostname}:${port}`); - console.log("Testing network connection"); + console.log(`HTTP: Listening at ${hostname}:${port}`); + console.log("RPC: Testing network connectivity"); const file = RPCServer._client.files.filesHash.get("04aa07009174edc6f03224f003a435bcdc9033d2c52348f3a35fbb342ea82f6f"); if (!file) return; - if (!(await file.download())) console.error("Download test failed, cannot connect to network"); + if (!(await file.download())) console.error("RPC: Download test failed, cannot connect to network"); else { - console.log("Connected to network"); + console.log("RPC: Connected to network"); if (Utils.isIp(RPCServer._client.config.publicHostname) && Utils.isPrivateIP(RPCServer._client.config.publicHostname)) console.error("Public hostname is a private IP address, cannot announce to other nodes"); else { - console.log(`Testing downloads ${RPCServer._client.config.publicHostname}/download/04aa07009174edc6f03224f003a435bcdc9033d2c52348f3a35fbb342ea82f6f`); + console.log(`HTTP: Testing downloads ${RPCServer._client.config.publicHostname}/download/04aa07009174edc6f03224f003a435bcdc9033d2c52348f3a35fbb342ea82f6f`); if (!file) console.error("Failed to build file"); else { const self = RPCServer._client.rpcClient.http.getSelf(); - if (self instanceof ErrorNotFound) console.error("Failed to find self in peers"); + if (self instanceof ErrorNotFound) console.error("HTTP: Failed to find self in peers"); else { const response = await self.downloadFile(file); - if (response instanceof ErrorDownloadFailed) console.error("Test: Failed to download file from self"); + if (response instanceof ErrorDownloadFailed) console.error("HTTP: Failed to download file from self"); else { - console.log("Announcing HTTP server to nodes"); + console.log("HTTP: Announcing server to nodes"); RPCServer._client.rpcClient.fetch(`https://localhost/announce?host=${RPCServer._client.config.publicHostname}`); } await RPCServer._client.rpcClient.http.add(RPCServer._client.config.publicHostname); diff --git a/src/start.ts b/src/start.ts index 972f24b..fb25773 100644 --- a/src/start.ts +++ b/src/start.ts @@ -4,7 +4,7 @@ import Hydrafiles from "./hydrafiles.ts"; const configPath = Deno.args[0] ?? "config.json"; const config = JSON.parse(existsSync(configPath) ? new TextDecoder().decode(Deno.readFileSync(configPath)) : "{}"); const hydrafiles = new Hydrafiles(config); -hydrafiles.start().then(() => console.log("Hydrafiles Started")); +await hydrafiles.start() // (async () => { // // Example Search From 92dbf54286df8e3c56959e3df96bcafd9bcbd4b2 Mon Sep 17 00:00:00 2001 From: "Parsa Yazdani (Quix)" Date: Wed, 27 Nov 2024 03:57:08 +1100 Subject: [PATCH 17/26] base dir --- web/dashboard.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/dashboard.ts b/web/dashboard.ts index 7a1e978..46ed691 100644 --- a/web/dashboard.ts +++ b/web/dashboard.ts @@ -92,7 +92,7 @@ document.getElementById("startHydrafilesButton")!.addEventListener("click", asyn const buffer = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`${email}:${password}`)); const deriveKey = Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join(""); - window.hydrafiles = new Hydrafiles({ deriveKey, customPeers: [`${window.location.protocol}//${window.location.hostname}`], baseDir: "./dashboard/" }); + window.hydrafiles = new Hydrafiles({ deriveKey, customPeers: [`${window.location.protocol}//${window.location.hostname}`], baseDir: "dashboard/" }); const webtorrent = new WebTorrent(); await window.hydrafiles.start({ onUpdateFileListProgress, webtorrent }); From 23e8ca8948055a2a7735de5478b4598f1009a652 Mon Sep 17 00:00:00 2001 From: "Parsa Yazdani (Quix)" Date: Wed, 27 Nov 2024 03:57:37 +1100 Subject: [PATCH 18/26] listenHTTP --- src/hydrafiles.ts | 1 + src/rpc/server.ts | 30 ++++++++++++++---------------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/hydrafiles.ts b/src/hydrafiles.ts index 441f8f0..55f999b 100644 --- a/src/hydrafiles.ts +++ b/src/hydrafiles.ts @@ -72,6 +72,7 @@ class Hydrafiles { this.rpcClient = await RPCClient.init(); this.rpcServer = new RPCServer(); console.log("Startup: Starting HTTP Server"); + await this.rpcServer.listenHTTP(); console.log("Startup: Starting WebTorrent"); this.webtorrent = opts.webtorrent; NameService._client = this; diff --git a/src/rpc/server.ts b/src/rpc/server.ts index 8bed6ff..e4714bb 100644 --- a/src/rpc/server.ts +++ b/src/rpc/server.ts @@ -7,9 +7,13 @@ import { serveFile } from "https://deno.land/std@0.115.0/http/file_server.ts"; class RPCServer { static _client: Hydrafiles; - constructor() { + constructor() {} + + async listenHTTP(): Promise { + const rpcServer = new RPCServer(); + const onListen = ({ hostname, port }: { hostname: string; port: number }): void => { - this.onListen(hostname, port); + rpcServer.onListen(hostname, port); }; if (typeof window !== "undefined") return; @@ -30,21 +34,15 @@ class RPCServer { httpPort++; } } - (async () => { + const certFile = await RPCServer._client.fs.readFile(RPCServer._client.config.sslCertPath); + const keyFile = await RPCServer._client.fs.readFile(RPCServer._client.config.sslKeyPath); + if (certFile instanceof Error) console.error(certFile); + else if (keyFile instanceof Error) console.error(keyFile); + else { + const cert = new TextDecoder().decode(certFile); + const key = new TextDecoder().decode(keyFile); while (true) { try { - const certFile = await RPCServer._client.fs.readFile(RPCServer._client.config.sslCertPath); - if (certFile instanceof Error) { - console.error(certFile); - break; - } - const cert = new TextDecoder().decode(certFile); - const keyFile = await RPCServer._client.fs.readFile(RPCServer._client.config.sslKeyPath); - if (keyFile instanceof Error) { - console.error(keyFile); - break; - } - const key = new TextDecoder().decode(keyFile); Deno.serve({ port: httpsPort, cert, @@ -60,7 +58,7 @@ class RPCServer { httpsPort++; } } - })(); + } } private onListen = async (hostname: string, port: number): Promise => { From ede596267c63c5ea4679a875e77ddf86a4973d22 Mon Sep 17 00:00:00 2001 From: "Parsa Yazdani (Quix)" Date: Wed, 27 Nov 2024 03:59:01 +1100 Subject: [PATCH 19/26] build/* serveFile --- src/rpc/routes.ts | 41 ----------------------------------------- src/rpc/server.ts | 23 +++++++++++++---------- 2 files changed, 13 insertions(+), 51 deletions(-) diff --git a/src/rpc/routes.ts b/src/rpc/routes.ts index 893b9fc..cdb4bfe 100644 --- a/src/rpc/routes.ts +++ b/src/rpc/routes.ts @@ -1,4 +1,3 @@ -import { join } from "https://deno.land/std@0.224.0/path/mod.ts"; import { File } from "../file.ts"; import type { PeerAttributes } from "./peers/http.ts"; import Utils, { type Sha256 } from "../utils.ts"; @@ -56,46 +55,6 @@ router.set("/status", () => { return new DecodedResponse(JSON.stringify({ status: true }), { headers }); }); -router.set("/hydrafiles-web.esm.js", async (_, client) => { - const headers = { - "Content-Type": "application/javascript", - "Cache-Control": "public, max-age=300", - }; - const fileContent = await client.fs.readFile("build/hydrafiles-web.esm.js"); - if (fileContent instanceof Error) return new DecodedResponse("File gone", { status: 403 }); - return new DecodedResponse(fileContent, { headers }); -}); - -router.set("/dashboard.js", async (_, client) => { - const headers = { - "Content-Type": "application/javascript", - "Cache-Control": "public, max-age=300", - }; - const fileContent = await client.fs.readFile("build/dashboard.js"); - if (fileContent instanceof Error) return new DecodedResponse("File not found", { status: 404 }); - return new DecodedResponse(fileContent, { headers }); -}); - -router.set("/hydrafiles-web.esm.js.map", async (_, client) => { - const headers = { - "Content-Type": "application/json", - "Cache-Control": "public, max-age=300", - }; - const fileContent = await client.fs.readFile("build/hydrafiles-web.esm.js.map"); - if (fileContent instanceof Error) return new DecodedResponse("File not found", { status: 404 }); - return new DecodedResponse(fileContent, { headers }); -}); - -router.set("/dashboard.js.map", async (_, client) => { - const headers = { - "Content-Type": "application/json", - "Cache-Control": "public, max-age=300", - }; - const fileContent = await client.fs.readFile("build/dashboard.js.map"); - if (fileContent instanceof Error) return new DecodedResponse("File not found", { status: 404 }); - return new DecodedResponse(fileContent, { headers }); -}); - router.set("/peers", (_, client) => { const headers = { "Content-Type": "application/json", diff --git a/src/rpc/server.ts b/src/rpc/server.ts index e4714bb..5d04cc3 100644 --- a/src/rpc/server.ts +++ b/src/rpc/server.ts @@ -106,18 +106,21 @@ class RPCServer { try { const url = new URL(req.url); - const filePath = `./public${url.pathname.endsWith("/") ? `${url.pathname}index.html` : url.pathname}`; - return await serveFile(req, filePath); + return await serveFile(req, `./public${url.pathname.endsWith("/") ? `${url.pathname}index.html` : url.pathname}`); } catch (_) { - if (!RPCServer._client.config.listen) return new Response("Peer has peering disabled"); - const routeHandler = req.headers.get("upgrade") === "websocket" ? RPCServer._client.rpcClient.ws.handleConnection : router.get(`/${url.pathname.split("/")[1]}`); - if (routeHandler) { - const response = await routeHandler(req, RPCServer._client); - if (response instanceof Response) return response; - response.addHeaders(headers); - return response.response(); + try { + return await serveFile(req, `./build${(url).pathname}`); + } catch (_) { + if (!RPCServer._client.config.listen) return new Response("Peer has peering disabled"); + const routeHandler = req.headers.get("upgrade") === "websocket" ? RPCServer._client.rpcClient.ws.handleConnection : router.get(`/${url.pathname.split("/")[1]}`); + if (routeHandler) { + const response = await routeHandler(req, RPCServer._client); + if (response instanceof Response) return response; + response.addHeaders(headers); + return response.response(); + } + return new Response("404 Page Not Found\n", { status: 404, headers }); } - return new Response("404 Page Not Found\n", { status: 404, headers }); } } catch (e) { console.error(req.url, "Internal Server Error", e); From dac9e15a94d8da75a6e3bf77b98662ce1d6c8c56 Mon Sep 17 00:00:00 2001 From: "Parsa Yazdani (Quix)" Date: Wed, 27 Nov 2024 04:18:00 +1100 Subject: [PATCH 20/26] Better traces --- src/filesystem/DirectoryHandleFileSystem.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/filesystem/DirectoryHandleFileSystem.ts b/src/filesystem/DirectoryHandleFileSystem.ts index 5c418ca..83efd02 100644 --- a/src/filesystem/DirectoryHandleFileSystem.ts +++ b/src/filesystem/DirectoryHandleFileSystem.ts @@ -45,9 +45,8 @@ export default class DirectoryHandleFileSystem { else await (await this.directoryHandle()).getFileHandle(path, { create: false }); return true; } catch (e) { - const error = e as Error; - if (error.name === "NotFoundError") return false; - throw error; + if ((e as Error).name === "NotFoundError") return false; + throw e; } }; From 78c1a751dc98a8a99ff12bfd489225b2270bbd8b Mon Sep 17 00:00:00 2001 From: "Parsa Yazdani (Quix)" Date: Wed, 27 Nov 2024 04:18:08 +1100 Subject: [PATCH 21/26] Path join --- src/filesystem/filesystem.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/filesystem/filesystem.ts b/src/filesystem/filesystem.ts index feae43b..2e7903d 100644 --- a/src/filesystem/filesystem.ts +++ b/src/filesystem/filesystem.ts @@ -1,3 +1,4 @@ +import { join } from "https://deno.land/std@0.224.0/path/join.ts"; import { ErrorNotInitialised } from "../errors.ts"; import type Hydrafiles from "../hydrafiles.ts"; import DirectoryHandleFileSystem from "./DirectoryHandleFileSystem.ts"; @@ -13,11 +14,11 @@ export default class FileSystem { if (this.fs instanceof IndexedDBFileSystem) this.fs.dbPromise = this.fs.initDB(); this._client = client; } - exists = async (path: string) => this.fs ? await this.fs.exists(`${this._client.config.baseDir}${path}`) : new ErrorNotInitialised(); - mkdir = async (path: `${string}/`) => this.fs ? await this.fs.mkdir(`${this._client.config.baseDir}${path}`) : new ErrorNotInitialised(); - readDir = async (path: `${string}/`) => this.fs ? await this.fs.readDir(`${this._client.config.baseDir}${path}`) : new ErrorNotInitialised(); - readFile = async (path: string) => this.fs ? await this.fs.readFile(`${this._client.config.baseDir}${path}`) : new ErrorNotInitialised(); - writeFile = async (path: string, data: Uint8Array) => this.fs ? await this.fs.writeFile(`${this._client.config.baseDir}${path}`, data) : new ErrorNotInitialised(); - getFileSize = async (path: string) => this.fs ? await this.fs.getFileSize(`${this._client.config.baseDir}${path}`) : new ErrorNotInitialised(); - remove = async (path: string) => this.fs ? await this.fs.remove(`${this._client.config.baseDir}${path}`) : new ErrorNotInitialised(); + exists = async (path: string) => this.fs ? await this.fs.exists(`${join(this._client.config.baseDir, path)}`) : new ErrorNotInitialised(); + mkdir = async (path: `${string}/`) => this.fs ? await this.fs.mkdir(`${join(this._client.config.baseDir, path)}/`) : new ErrorNotInitialised(); + readDir = async (path: `${string}/`) => this.fs ? await this.fs.readDir(`${join(this._client.config.baseDir, path)}/`) : new ErrorNotInitialised(); + readFile = async (path: string) => this.fs ? await this.fs.readFile(`${join(this._client.config.baseDir, path)}`) : new ErrorNotInitialised(); + writeFile = async (path: string, data: Uint8Array) => this.fs ? await this.fs.writeFile(`${join(this._client.config.baseDir, path)}`, data) : new ErrorNotInitialised(); + getFileSize = async (path: string) => this.fs ? await this.fs.getFileSize(`${join(this._client.config.baseDir, path)}`) : new ErrorNotInitialised(); + remove = async (path: string) => this.fs ? await this.fs.remove(`${join(this._client.config.baseDir, path)}`) : new ErrorNotInitialised(); } From 242c11c327d10a5b2d3dc840fbcd8b00b02496a0 Mon Sep 17 00:00:00 2001 From: "Parsa Yazdani (Quix)" Date: Wed, 27 Nov 2024 04:18:22 +1100 Subject: [PATCH 22/26] better errs --- src/errors.ts | 4 ++-- src/filesystem/IndexedDBFileSystem.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/errors.ts b/src/errors.ts index 36f2303..ebe08c1 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -46,8 +46,8 @@ export class ErrorDownloadFailed extends Error { readonly brand = Symbol(); } export class ErrorFailedToReadFile extends Error { - constructor() { - super("Error of type 'ErrorFailedToReadFile' thrown"); + constructor(msg?: string) { + super(msg); console.error("ErrorFailedToReadFile", this.stack); } readonly brand = Symbol(); diff --git a/src/filesystem/IndexedDBFileSystem.ts b/src/filesystem/IndexedDBFileSystem.ts index ac9e62a..cdcfddc 100644 --- a/src/filesystem/IndexedDBFileSystem.ts +++ b/src/filesystem/IndexedDBFileSystem.ts @@ -70,7 +70,7 @@ export default class IndexedDBFileSystem { const transaction = db.transaction(this.storeName, "readonly"); const store = transaction.objectStore(this.storeName); const request = store.get(path); - request.onsuccess = () => resolve(request.result ? new Uint8Array(request.result.data) : new ErrorFailedToReadFile()); + request.onsuccess = () => resolve(request.result ? new Uint8Array(request.result.data) : new ErrorFailedToReadFile(path)); request.onerror = () => reject(new Error(`Error reading ${path}`)); }); } From c5a5228689d450f4a6186a1a84fd0a5735637498 Mon Sep 17 00:00:00 2001 From: "Parsa Yazdani (Quix)" Date: Wed, 27 Nov 2024 04:18:33 +1100 Subject: [PATCH 23/26] clearer log --- src/file.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/file.ts b/src/file.ts index 612ea24..a825c99 100644 --- a/src/file.ts +++ b/src/file.ts @@ -448,7 +448,7 @@ class Files { backfillFiles = (): void => { setTimeout(async () => { while (true) { - console.log("Files: Backfilling file"); + console.log("Files: Finding file to backfill"); const keys = Array.from(this.filesHash.keys()); if (keys.length === 0) { await delay(500); From 95c259c6196bac19d9e7f9059b4d6f36b2eefcd0 Mon Sep 17 00:00:00 2001 From: "Parsa Yazdani (Quix)" Date: Wed, 27 Nov 2024 04:18:43 +1100 Subject: [PATCH 24/26] clean code --- src/rpc/server.ts | 2 +- src/start.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rpc/server.ts b/src/rpc/server.ts index 5d04cc3..965fc4a 100644 --- a/src/rpc/server.ts +++ b/src/rpc/server.ts @@ -109,7 +109,7 @@ class RPCServer { return await serveFile(req, `./public${url.pathname.endsWith("/") ? `${url.pathname}index.html` : url.pathname}`); } catch (_) { try { - return await serveFile(req, `./build${(url).pathname}`); + return await serveFile(req, `./build${url.pathname}`); } catch (_) { if (!RPCServer._client.config.listen) return new Response("Peer has peering disabled"); const routeHandler = req.headers.get("upgrade") === "websocket" ? RPCServer._client.rpcClient.ws.handleConnection : router.get(`/${url.pathname.split("/")[1]}`); diff --git a/src/start.ts b/src/start.ts index fb25773..08e681f 100644 --- a/src/start.ts +++ b/src/start.ts @@ -4,7 +4,7 @@ import Hydrafiles from "./hydrafiles.ts"; const configPath = Deno.args[0] ?? "config.json"; const config = JSON.parse(existsSync(configPath) ? new TextDecoder().decode(Deno.readFileSync(configPath)) : "{}"); const hydrafiles = new Hydrafiles(config); -await hydrafiles.start() +await hydrafiles.start(); // (async () => { // // Example Search From 6daad0d38663bcd4866b71fdf7f7338ca1664d75 Mon Sep 17 00:00:00 2001 From: "Parsa Yazdani (Quix)" Date: Wed, 27 Nov 2024 04:42:12 +1100 Subject: [PATCH 25/26] subdir support --- build.ts | 14 +++-- src/filesystem/DirectoryHandleFileSystem.ts | 59 +++++++++++++-------- 2 files changed, 45 insertions(+), 28 deletions(-) diff --git a/build.ts b/build.ts index c9013d1..5f20698 100644 --- a/build.ts +++ b/build.ts @@ -10,9 +10,11 @@ console.log( format: "esm", platform: "browser", sourcemap: true, - minify: true, - treeShaking: true, keepNames: true, + minify: false, // TODO: Toggle for dev/prod + treeShaking: false, // TODO: Toggle for dev/prod + sourcesContent: true, // TODO: Toggle for dev/prod + metafile: true, }), await esbuild.build({ plugins: [...denoPlugins()], @@ -22,10 +24,12 @@ console.log( format: "esm", platform: "browser", sourcemap: true, - minify: true, - treeShaking: true, - external: ["https://esm.sh/webtorrent@2.5.1"], keepNames: true, + minify: false, // TODO: Toggle for dev/prod + treeShaking: false, // TODO: Toggle for dev/prod + sourcesContent: true, // TODO: Toggle for dev/prod + metafile: true, + external: ["https://esm.sh/webtorrent@2.5.1"], }), ); diff --git a/src/filesystem/DirectoryHandleFileSystem.ts b/src/filesystem/DirectoryHandleFileSystem.ts index 83efd02..aa607eb 100644 --- a/src/filesystem/DirectoryHandleFileSystem.ts +++ b/src/filesystem/DirectoryHandleFileSystem.ts @@ -1,13 +1,13 @@ import { ErrorNotFound, ErrorUnreachableCodeReached } from "../errors.ts"; interface FileHandle extends FileSystemFileHandle { - remove(): Promise; // Returns a Promise that resolves when the file is removed. + remove(): Promise; } interface DirectoryHandle extends FileSystemDirectoryHandle { - values(): IterableIterator; // Returns an iterator of FileSystemHandle objects for the directory's contents. - getDirectoryHandle(path: string, options: { create: boolean }): Promise; // Returns a Promise that resolves to a DirectoryHandle for the specified path, creating it if specified. - getFileHandle(path: string, opts?: { create: boolean }): Promise; // Returns a Promise that resolves to a FileHandle for the specified file path. + values(): IterableIterator; + getDirectoryHandle(path: string, options: { create: boolean }): Promise; + getFileHandle(path: string, opts?: { create: boolean }): Promise; } declare global { @@ -17,22 +17,30 @@ declare global { } } -async function getFileHandle(directoryHandle: DirectoryHandle, path: string, touch = false): Promise { - const parts = path.split("/"); - let currentHandle = directoryHandle; +async function getDirectoryFromPath(rootHandle: DirectoryHandle, path: string, create = false): Promise { + let currentHandle = rootHandle; + for (const part of path.split("/").filter((part) => part.length > 0)) { + currentHandle = await currentHandle.getDirectoryHandle(part, { create }); + } + return currentHandle; +} - for (let i = 0; i < parts.length; i++) { - const part = parts[i]; +async function getFileFromPath(rootHandle: DirectoryHandle, path: string, create = false): Promise { + const parts = path.split("/").filter((part) => part.length > 0); + if (parts.length === 0) return new ErrorUnreachableCodeReached(); - if (i === parts.length - 1) return await currentHandle.getFileHandle(part, { create: touch }); - else currentHandle = await currentHandle.getDirectoryHandle(part, { create: true }); + let currentHandle = rootHandle; + for (const part of parts) { + currentHandle = await currentHandle.getDirectoryHandle(part, { create }); } - return new ErrorUnreachableCodeReached(); + const fileName = parts.pop(); + if (typeof fileName === "undefined") return new ErrorNotFound(); + return await currentHandle.getFileHandle(fileName, { create }); } export default class DirectoryHandleFileSystem { - directoryHandle = async () => { + directoryHandle = async (): Promise => { if ("handle" in globalThis.window && typeof globalThis.window.handle !== "undefined") return globalThis.window.handle; const handle = await globalThis.window.showDirectoryPicker(); globalThis.window.handle = handle; @@ -41,8 +49,9 @@ export default class DirectoryHandleFileSystem { exists = async (path: string): Promise => { try { - if (path.endsWith("/")) await (await this.directoryHandle()).getDirectoryHandle(path.replace(/\/+$/, ""), { create: false }); - else await (await this.directoryHandle()).getFileHandle(path, { create: false }); + const rootHandle = await this.directoryHandle(); + if (path.endsWith("/")) await getDirectoryFromPath(rootHandle, path.replace(/\/+$/, ""), false); + else await getFileFromPath(rootHandle, path, false); return true; } catch (e) { if ((e as Error).name === "NotFoundError") return false; @@ -50,15 +59,14 @@ export default class DirectoryHandleFileSystem { } }; - mkdir = async (path: `${string}/`) => { + mkdir = async (path: `${string}/`): Promise => { if (await this.exists(path)) return; - await (await this.directoryHandle()).getDirectoryHandle(path.replace(/\/+$/, ""), { create: true }); + await getDirectoryFromPath(await this.directoryHandle(), path.replace(/\/+$/, ""), true); }; readDir = async (path: `${string}/`): Promise => { const entries: string[] = []; - const dirHandle = await (await this.directoryHandle()).getDirectoryHandle(path.replace(/\/+$/, ""), { create: false }); - for await (const entry of dirHandle.values()) { + for await (const entry of (await getDirectoryFromPath(await this.directoryHandle(), path.replace(/\/+$/, ""), false)).values()) { entries.push(entry.name); } return entries; @@ -66,15 +74,18 @@ export default class DirectoryHandleFileSystem { readFile = async (path: string): Promise => { if (!await this.exists(path)) return new ErrorNotFound(); - const fileHandle = await getFileHandle(await this.directoryHandle(), path); + + const fileHandle = await getFileFromPath(await this.directoryHandle(), path); if (fileHandle instanceof ErrorUnreachableCodeReached) return fileHandle; + const file = await fileHandle.getFile(); return new Uint8Array(await file.arrayBuffer()); }; writeFile = async (path: string, data: Uint8Array): Promise => { - const fileHandle = await getFileHandle(await this.directoryHandle(), path, true); + const fileHandle = await getFileFromPath(await this.directoryHandle(), path, true); if (fileHandle instanceof ErrorUnreachableCodeReached) return fileHandle; + const writable = await fileHandle.createWritable(); await writable.write(data); await writable.close(); @@ -82,15 +93,17 @@ export default class DirectoryHandleFileSystem { }; getFileSize = async (path: string): Promise => { - const fileHandle = await getFileHandle(await this.directoryHandle(), path); + const fileHandle = await getFileFromPath(await this.directoryHandle(), path); if (fileHandle instanceof ErrorUnreachableCodeReached) return fileHandle; + const file = await fileHandle.getFile(); return file.size; }; remove = async (path: string): Promise => { - const fileHandle = await getFileHandle(await this.directoryHandle(), path); + const fileHandle = await getFileFromPath(await this.directoryHandle(), path); if (fileHandle instanceof ErrorUnreachableCodeReached) return fileHandle; + await fileHandle.remove(); return true; }; From 5f83417f324e0b20cd050bbc88a4c58b5ce96fb9 Mon Sep 17 00:00:00 2001 From: "Parsa Yazdani (Quix)" Date: Wed, 27 Nov 2024 04:42:56 +1100 Subject: [PATCH 26/26] Bump version number --- deno.jsonc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deno.jsonc b/deno.jsonc index 98317e4..a5901f4 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -1,6 +1,6 @@ { "name": "@starfiles/hydrafiles", - "version": "0.10.7", + "version": "0.11", "description": "The (P2P) web privacy layer.", "main": "src/hydrafiles.ts", "exports": {