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 const acc: Record<string, Bucket> = {};
15
16 tracks.forEach((track: Track) => {
17 const parsed = parseURI(track.uri);
18 if (!parsed) return;
19
20 const id = bucketId(parsed.bucket);
21 if (acc[id]) return;
22
23 acc[id] = parsed.bucket;
24 });
25
26 return acc;
27}
28
29export function bucketId(bucket: Bucket) {
30 return `${bucket.accessKey}:${bucket.secretKey}@${bucket.host}`;
31}
32
33export function buildURI(bucket: Bucket, path: string) {
34 return URI.serialize({
35 scheme: SCHEME,
36 userinfo: `${bucket.accessKey}:${bucket.secretKey}`,
37 host: bucket.host.replace(/^https?:\/\//, ""),
38 path: path,
39 query: QS.stringify({
40 bucketName: bucket.bucketName,
41 bucketPath: bucket.path,
42 region: bucket.region,
43 }),
44 });
45}
46
47export async function consultBucket(bucket: Bucket) {
48 const client = createClient(bucket);
49 return await client.bucketExists(bucket.bucketName);
50}
51
52export function createClient(bucket: Bucket) {
53 return new S3Client({
54 bucket: bucket.bucketName,
55 endPoint: `http${bucket.host.startsWith("localhost") ? "" : "s"}://${bucket.host}`,
56 region: bucket.region,
57 pathStyle: false,
58 accessKey: bucket.accessKey,
59 secretKey: bucket.secretKey,
60 });
61}
62
63export function encodeAwsUriComponent(a: string) {
64 return encodeURIComponent(a).replace(
65 /(\+|!|"|#|\$|&|'|\(|\)|\*|\+|,|:|;|=|\?|@)/gim,
66 (match) => (ENCODINGS as any)[match] ?? match,
67 );
68}
69
70export function groupTracksByBucket(tracks: Track[]) {
71 const acc: Record<string, { bucket: Bucket; tracks: Track[] }> = {};
72
73 tracks.forEach((track: Track) => {
74 const parsed = parseURI(track.uri);
75 if (!parsed) return acc;
76
77 const id = bucketId(parsed.bucket);
78
79 if (acc[id]) {
80 acc[id].tracks.push(track);
81 } else {
82 acc[id] = { bucket: parsed.bucket, tracks: [track] };
83 }
84 });
85
86 return acc;
87}
88
89export async function loadBuckets(): Promise<Record<string, Bucket>> {
90 const i = await IDB.get(IDB_BUCKETS);
91 return i ? i : {};
92}
93
94export function parseURI(uriString: string): { bucket: Bucket; path: string } | undefined {
95 const uri = URI.parse(uriString);
96 if (uri.scheme !== SCHEME) return undefined;
97 if (!uri.host) return undefined;
98
99 const [accessKey, secretKey] = uri.userinfo?.split(":") ?? [];
100 if (!accessKey || !secretKey) return undefined;
101
102 const qs = QS.parse(uri.query || "");
103
104 const bucket = {
105 accessKey,
106 bucketName: typeof qs.bucketName === "string" ? qs.bucketName : "",
107 host: uri.host,
108 path: qs.bucketPath === "string" ? qs.bucketPath : "/",
109 region: typeof qs.region === "string" ? qs.region : "",
110 secretKey,
111 };
112
113 const path = (bucket.path.replace(/\/$/, "") + URI.unescapeComponent(uri.path || "")).replace(
114 /^\//,
115 "",
116 );
117
118 return { bucket, path };
119}
120
121export async function saveBuckets(items: Record<string, Bucket>) {
122 await IDB.set(IDB_BUCKETS, items);
123}