Experiment to rebuild Diffuse using web applets.
at main 123 lines 3.3 kB view raw
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}