import { html, LitElement } from "./lib/lit.min.js"; import { formatBytes, newExpirationValue, unixNow } from "./utils.js"; export class MirrorBlobs extends LitElement { static properties = { remoteBlobs: { state: true }, localBlobs: { state: true }, status: { state: true, type: String }, progress: { state: true }, server: { state: true, type: String }, selected: { state: true }, }; createRenderRoot() { return this; } constructor() { super(); this.selected = []; } async fetchRemoteBlobs() { if (!this.server) return; const pubkey = await window.nostr.getPublicKey(); this.status = "Signing..."; const auth = await window.nostr.signEvent({ kind: 24242, content: "List Blobs", created_at: unixNow(), tags: [ ["t", "list"], ["expiration", newExpirationValue()], ["server", new URL("/", this.server).toString()], ], }); this.status = "Fetching..."; this.remoteBlobs = await fetch(new URL("/list/" + pubkey, this.server), { headers: { authorization: "Nostr " + btoa(JSON.stringify(auth)) }, }).then((res) => res.json()); this.status = undefined; } localAuth = null; async fetchLocalBlobs() { this.pubkey = await window.nostr?.getPublicKey(); if (!this.pubkey) return; this.status = "Signing..."; this.localAuth = await window.nostr.signEvent({ kind: 24242, content: "List Blobs", created_at: unixNow(), tags: [ ["t", "list"], ["expiration", newExpirationValue()], ["server", new URL(location.protocol + "//" + location.hostname).toString()], ], }); this.status = "Fetching..."; this.localBlobs = await fetch("/list/" + this.pubkey, { headers: { authorization: "Nostr " + btoa(JSON.stringify(this.localAuth)) }, }).then((res) => res.json()); this.status = undefined; } async submit(e) { e.preventDefault(); await this.fetchLocalBlobs(); await this.fetchRemoteBlobs(); } serverChange(e) { this.server = e.target.value; } renderForm() { return html`
`; } selectAll() { const missingBlobs = this.remoteBlobs.filter((blob) => !this.localBlobs.some((b) => b.sha256 === blob.sha256)); if (this.selected.length === missingBlobs.length) { this.selected = []; } else this.selected = missingBlobs.map((b) => b.sha256); } toggleSelection(sha256) { if (this.selected.includes(sha256)) { this.selected = this.selected.filter((s) => s !== sha256); } else this.selected = [...this.selected, sha256]; } async mirrorBlobs() { const blobs = this.remoteBlobs.filter((blob) => this.selected.includes(blob.sha256)); for (const blob of blobs) { this.progress = blobs.indexOf(blob) + 1; this.status = `Signing ${blob.sha256}`; // create auth event const auth = await window.nostr.signEvent({ kind: 24242, content: "Mirror Blob", created_at: unixNow(), tags: [ ["t", "upload"], ["x", blob.sha256], ["expiration", newExpirationValue()], ], }); this.status = `Mirroring ${blob.sha256}`; await fetch("/mirror", { method: "PUT", body: JSON.stringify({ url: blob.url }), headers: { authorization: "Nostr " + btoa(JSON.stringify(auth)), "Content-Type": "application/json" }, }); } this.progress = undefined; this.status = undefined; this.selected = []; await this.fetchLocalBlobs(); } renderContent() { if (!window.nostr) return html`

Missing window.nostr extension

`; if (this.progress !== undefined) { return html`

${this.progress}/${this.selected.length} - ${this.status}

${((this.selected.length / this.progress) * 100).toFixed(2)}% `; } else if (this.status) { return html`

${this.status}

`; } else if (this.remoteBlobs && this.localBlobs) { const missingBlobs = this.remoteBlobs.filter((blob) => !this.localBlobs.some((b) => b.sha256 === blob.sha256)); if (missingBlobs.length === 0) { return html`

All blobs synced ✅

`; } return html`
${this.renderBlobs(missingBlobs)} `; } return html`

Select Blossom Server

`; } renderBlobs(blobs = []) { return html` ${blobs.map( (blob) => html` `, )}
sha256 Type Size
${blob.sha256} ${blob.type} ${formatBytes(blob.size)}
`; } render() { return html`

Mirror blobs

${window.nostr && this.renderForm()}
${this.renderContent()}
back to upload 🌸 Blossom Spec
`; } } customElements.define("mirror-blobs", MirrorBlobs);