forked from
tokono.ma/diffuse
A music player that connects to your cloud/distributed storage.
1import { S3Client } from "@bradenmacdonald/s3-lite-client";
2import * as IDB from "idb-keyval";
3import * as URI from "fast-uri";
4import QS from "query-string";
5
6import { cachedConsult } from "~/components/input/common.js";
7import { safeDecodeURIComponent } from "~/common/utils.js";
8import { ENCODINGS, IDB_BUCKETS, SCHEME } from "./constants.js";
9
10/**
11 * @import { Track } from "~/definitions/types.d.ts";
12 * @import { Bucket } from "./types.d.ts";
13 */
14
15////////////////////////////////////////////
16// 🛠️
17////////////////////////////////////////////
18
19/**
20 * @param {Track[]} tracks
21 */
22export function bucketsFromTracks(tracks) {
23 /** @type {Record<string, Bucket>} */
24 const acc = {};
25
26 tracks.forEach((track) => {
27 const parsed = parseURI(track.uri);
28 if (!parsed) return;
29
30 const id = bucketId(parsed.bucket);
31 if (acc[id]) return;
32
33 acc[id] = parsed.bucket;
34 });
35
36 return acc;
37}
38
39/**
40 * @param {Bucket} bucket
41 */
42export function bucketId(bucket) {
43 return `${bucket.accessKey}:${bucket.secretKey}@${bucket.host}`;
44}
45
46/**
47 * @param {Bucket} bucket
48 * @param {string} [path]
49 */
50export function buildURI(bucket, path) {
51 return URI.serialize({
52 scheme: SCHEME,
53 userinfo: `${bucket.accessKey}:${bucket.secretKey}`,
54 host: bucket.host.replace(/^\w+:\/\//, ""),
55 path: path,
56 query: QS.stringify({
57 bucketName: bucket.bucketName,
58 bucketPath: bucket.path,
59 region: bucket.region,
60 }),
61 });
62}
63
64/**
65 * @param {Bucket} bucket
66 */
67export async function consultBucket(bucket) {
68 const client = createClient(bucket);
69 return await client.bucketExists(bucket.bucketName);
70}
71
72export const consultBucketCached = cachedConsult(consultBucket, bucketId);
73
74/**
75 * @param {Bucket} bucket
76 */
77export function createClient(bucket) {
78 return new S3Client({
79 bucket: bucket.bucketName,
80 endPoint: `http${
81 bucket.host.startsWith("localhost") ? "" : "s"
82 }://${bucket.host}`,
83 region: bucket.region,
84 pathStyle: false,
85 accessKey: bucket.accessKey,
86 secretKey: bucket.secretKey,
87 });
88}
89
90/**
91 * @param {string} a
92 */
93export function encodeAwsUriComponent(a) {
94 return encodeURIComponent(a).replace(
95 /(\+|!|"|#|\$|&|'|\(|\)|\*|\+|,|:|;|=|\?|@)/gim,
96 (match) => /** @type {any} */ (ENCODINGS)[match] ?? match,
97 );
98}
99
100/**
101 * @param {Track[]} tracks
102 */
103export function groupTracksByBucket(tracks) {
104 /** @type {Record<string, { bucket: Bucket; tracks: Track[] }>} */
105 const acc = {};
106
107 tracks.forEach((track) => {
108 const parsed = parseURI(track.uri);
109 if (!parsed) return acc;
110
111 const id = bucketId(parsed.bucket);
112
113 if (acc[id]) {
114 acc[id].tracks.push(track);
115 } else {
116 acc[id] = { bucket: parsed.bucket, tracks: [track] };
117 }
118 });
119
120 return acc;
121}
122
123/**
124 * @param {string[]} uris
125 */
126export function groupUrisByBucket(uris) {
127 /** @type {Record<string, { bucket: Bucket; uris: string[] }>} */
128 const acc = {};
129
130 uris.forEach((uri) => {
131 const parsed = parseURI(uri);
132 if (!parsed) return acc;
133
134 const id = bucketId(parsed.bucket);
135
136 if (acc[id]) {
137 acc[id].uris.push(uri);
138 } else {
139 acc[id] = { bucket: parsed.bucket, uris: [uri] };
140 }
141 });
142
143 return acc;
144}
145
146/**
147 * @returns {Promise<Record<string, Bucket>>}
148 */
149export async function loadBuckets() {
150 const i = await IDB.get(IDB_BUCKETS);
151 return i ? i : {};
152}
153
154/**
155 * @param {string} uriString
156 * @returns {{ bucket: Bucket; path: string } | undefined}
157 */
158export function parseURI(uriString) {
159 const uri = URI.parse(uriString);
160 if (uri.scheme !== SCHEME) return undefined;
161 if (!uri.host) return undefined;
162
163 const [accessKey, secretKey] = uri.userinfo?.split(":") ?? [];
164 if (!accessKey || !secretKey) return undefined;
165
166 const qs = QS.parse(uri.query || "");
167
168 const bucket = {
169 accessKey,
170 bucketName: typeof qs.bucketName === "string" ? qs.bucketName : "",
171 host: uri.host,
172 path: qs.bucketPath === "string" ? qs.bucketPath : "/",
173 region: typeof qs.region === "string" ? qs.region : "",
174 secretKey,
175 };
176
177 const path =
178 (bucket.path.replace(/\/$/, "") + safeDecodeURIComponent(uri.path || ""))
179 .replace(
180 /^\//,
181 "",
182 );
183
184 return { bucket, path };
185}
186
187/**
188 * @param {Record<string, Bucket>} items
189 */
190export async function saveBuckets(items) {
191 await IDB.set(IDB_BUCKETS, items);
192}