Experiment to rebuild Diffuse using web applets.
1import { S3Client } from "@bradenmacdonald/s3-lite-client";
2import * as IDB from "idb-keyval";
3import * as URI from "uri-js";
4import QS from "query-string";
5
6import type { Track } from "@applets/core/types.d.ts";
7import { ENCODINGS, IDB_BUCKETS, SCHEME } from "./constants";
8import type { Bucket } from "./types";
9
10////////////////////////////////////////////
11// 🛠️
12////////////////////////////////////////////
13export function bucketsFromTracks(tracks: Track[]) {
14 return tracks.reduce((acc: Record<string, Bucket>, track: Track) => {
15 const parsed = parseURI(track.uri);
16 if (!parsed) return acc;
17
18 const id = bucketId(parsed.bucket);
19 if (acc[id]) return acc;
20
21 return { ...acc, [id]: parsed.bucket };
22 }, {});
23}
24
25export function bucketId(bucket: Bucket) {
26 return `${bucket.accessKey}:${bucket.secretKey}@${bucket.host}`;
27}
28
29export function buildURI(bucket: Bucket, path: string) {
30 return URI.serialize({
31 scheme: SCHEME,
32 userinfo: `${bucket.accessKey}:${bucket.secretKey}`,
33 host: bucket.host.replace(/^https?:\/\//, ""),
34 path: path,
35 query: QS.stringify({
36 bucketName: bucket.bucketName,
37 bucketPath: bucket.path,
38 region: bucket.region,
39 }),
40 });
41}
42
43export async function consultBucket(bucket: Bucket) {
44 const client = createClient(bucket);
45 return await client.bucketExists(bucket.bucketName);
46}
47
48export function createClient(bucket: Bucket) {
49 return new S3Client({
50 bucket: bucket.bucketName,
51 endPoint: `http${bucket.host.startsWith("localhost") ? "" : "s"}://${bucket.host}`,
52 region: bucket.region,
53 pathStyle: false,
54 accessKey: bucket.accessKey,
55 secretKey: bucket.secretKey,
56 });
57}
58
59export function encodeAwsUriComponent(a: string) {
60 return encodeURIComponent(a).replace(
61 /(\+|!|"|#|\$|&|'|\(|\)|\*|\+|,|:|;|=|\?|@)/gim,
62 (match) => (ENCODINGS as any)[match] ?? match,
63 );
64}
65
66export function groupTracksByBucket(tracks: Track[]) {
67 return tracks.reduce((acc: Record<string, { bucket: Bucket; tracks: Track[] }>, track: Track) => {
68 const parsed = parseURI(track.uri);
69 if (!parsed) return acc;
70
71 const id = bucketId(parsed.bucket);
72 const obj = { bucket: parsed.bucket, tracks: acc[id] ? [...acc[id].tracks, track] : [track] };
73
74 return { ...acc, [id]: obj };
75 }, {});
76}
77
78export async function loadBuckets(): Promise<Record<string, Bucket>> {
79 const i = await IDB.get(IDB_BUCKETS);
80 return i ? i : {};
81}
82
83export function parseURI(uriString: string): { bucket: Bucket; path: string } | undefined {
84 const uri = URI.parse(uriString);
85 if (uri.scheme !== SCHEME) return undefined;
86 if (!uri.host) return undefined;
87
88 const [accessKey, secretKey] = uri.userinfo?.split(":") ?? [];
89 if (!accessKey || !secretKey) return undefined;
90
91 const qs = QS.parse(uri.query || "");
92
93 const bucket = {
94 accessKey,
95 bucketName: typeof qs.bucketName === "string" ? qs.bucketName : "",
96 host: uri.host,
97 path: qs.bucketPath === "string" ? qs.bucketPath : "/",
98 region: typeof qs.region === "string" ? qs.region : "",
99 secretKey,
100 };
101
102 const path = (bucket.path.replace(/\/$/, "") + URI.unescapeComponent(uri.path || "")).replace(
103 /^\//,
104 "",
105 );
106
107 return { bucket, path };
108}
109
110export async function saveBuckets(items: Record<string, Bucket>) {
111 await IDB.set(IDB_BUCKETS, items);
112}