···11-import { type FileSystemDirectoryHandle } from "native-file-system-adapter";
21import * as IDB from "idb-keyval";
32import * as URI from "uri-js";
43import QS from "query-string";
-2
src/scripts/input/native-fs/types.d.ts
···11-import type { FileSystemDirectoryHandle } from "native-file-system-adapter";
22-31export type Handles = Record<string, FileSystemDirectoryHandle>;
+5-3
src/scripts/input/native-fs/ui.ts
···11import { computed, effect, type Signal } from "spellcaster";
22import { repeat, tags, text } from "spellcaster/hyperscript.js";
33-import { type FileSystemDirectoryHandle } from "native-file-system-adapter";
4355-import { IDB_HANDLES } from "./constants";
64import { mount, mounts, unmount } from "./mounting";
55+import { isSupported } from "./common";
7687////////////////////////////////////////////
98// SIGNALS
109////////////////////////////////////////////
11101211// Mount button
1313-document.getElementById("mount")?.addEventListener("click", () => mount());
1212+document.getElementById("mount")?.addEventListener("click", () => {
1313+ if (isSupported()) mount();
1414+ else alert("The File System Access API is not supported on this platform.");
1515+});
14161517// Directories
1618const dirList = computed(() => {
+2-18
src/scripts/input/native-fs/worker.ts
···11-import { type FileSystemDirectoryHandle } from "native-file-system-adapter";
21import * as URI from "uri-js";
3243import type { Track } from "@applets/core/types.d.ts";
54import { SCHEME } from "./constants";
66-import {
77- fetchHandles,
88- fetchHandlesList,
99- isSupported,
1010- recursiveList,
1111- trackHandleId,
1212-} from "./common";
55+import { fetchHandles, fetchHandlesList, recursiveList, trackHandleId } from "./common";
136import { expose } from "@scripts/common";
147158////////////////////////////////////////////
···2720// Actions
28212922export async function consult(fileUriOrScheme: string) {
3030- if (!isSupported()) {
2323+ if (!self.FileSystemDirectoryHandle) {
3124 return { supported: false, reason: "File System Access API is not supported" };
3225 }
3326···4538export async function contextualize(cachedTracks: Track[]) {}
46394740export async function list(cachedTracks: Track[] = []) {
4848- if (!isSupported()) {
4949- return cachedTracks;
5050- }
5151-5252- // Continue if supported
5341 const handles = await fetchHandlesList();
54425543 // Recursive listing of all tracks of available handles
···98869987export async function resolve(args: { uri: string }) {
10088 const fileUri = args.uri;
101101-102102- if (!isSupported()) {
103103- return undefined;
104104- }
1058910690 const uri = URI.parse(fileUri);
10791 if (uri.scheme !== SCHEME) return undefined;
+88
src/scripts/input/s3/common.ts
···11+import { S3Client } from "@bradenmacdonald/s3-lite-client";
22+import * as IDB from "idb-keyval";
33+import * as URI from "uri-js";
44+import QS from "query-string";
55+66+import type { Track } from "@applets/core/types.d.ts";
77+import { ENCODINGS, IDB_BUCKETS, SCHEME } from "./constants";
88+import type { Bucket } from "./types";
99+1010+////////////////////////////////////////////
1111+// 🛠️
1212+////////////////////////////////////////////
1313+export function bucketsFromTracks(tracks: Track[]) {
1414+ return tracks.reduce((acc: Record<string, Bucket>, track: Track) => {
1515+ const bucket = parseURI(track.uri);
1616+ if (!bucket) return acc;
1717+1818+ const id = bucketId(bucket);
1919+ if (acc[id]) return acc;
2020+2121+ return { ...acc, [id]: bucket };
2222+ }, {});
2323+}
2424+2525+export function bucketId(bucket: Bucket) {
2626+ return `${bucket.accessKey}:${bucket.secretKey}@${bucket.host}`;
2727+}
2828+2929+export function buildURI(bucket: Bucket, path: string) {
3030+ return URI.serialize({
3131+ scheme: SCHEME,
3232+ userinfo: `${bucket.accessKey}:${bucket.secretKey}`,
3333+ host: bucket.host.replace(/^https?:\/\//, ""),
3434+ path: path,
3535+ query: QS.stringify({
3636+ bucketName: bucket.bucketName,
3737+ bucketPath: bucket.path,
3838+ region: bucket.region,
3939+ }),
4040+ });
4141+}
4242+4343+export function createClient(bucket: Bucket) {
4444+ return new S3Client({
4545+ bucket: bucket.bucketName,
4646+ endPoint: `http${bucket.host.startsWith("localhost") ? "" : "s"}://${bucket.host}`,
4747+ region: bucket.region,
4848+ pathStyle: false,
4949+ accessKey: bucket.accessKey,
5050+ secretKey: bucket.secretKey,
5151+ });
5252+}
5353+5454+export function encodeAwsUriComponent(a: string) {
5555+ return encodeURIComponent(a).replace(
5656+ /(\+|!|"|#|\$|&|'|\(|\)|\*|\+|,|:|;|=|\?|@)/gim,
5757+ (match) => (ENCODINGS as any)[match] ?? match,
5858+ );
5959+}
6060+6161+export async function loadBuckets(): Promise<Record<string, Bucket>> {
6262+ const i = await IDB.get(IDB_BUCKETS);
6363+ return i ? i : {};
6464+}
6565+6666+export function parseURI(uriString: string): Bucket | undefined {
6767+ const uri = URI.parse(uriString);
6868+ if (uri.scheme !== SCHEME) return undefined;
6969+ if (!uri.host) return undefined;
7070+7171+ const [accessKey, secretKey] = uri.userinfo?.split(":") ?? [];
7272+ if (!accessKey || !secretKey) return undefined;
7373+7474+ const qs = QS.parse(uri.query || "");
7575+7676+ return {
7777+ accessKey,
7878+ bucketName: typeof qs.bucketName === "string" ? qs.bucketName : "",
7979+ host: uri.host,
8080+ path: qs.bucketPath === "string" ? qs.bucketPath : "/",
8181+ region: typeof qs.region === "string" ? qs.region : "",
8282+ secretKey,
8383+ };
8484+}
8585+8686+export async function saveBuckets(items: Record<string, Bucket>) {
8787+ await IDB.set(IDB_BUCKETS, items);
8888+}