nappup 1.8.3 → 1.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/services/blossom-upload.js +49 -48
package/package.json
CHANGED
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
import { sha256 } from '@noble/hashes/sha2.js'
|
|
2
|
-
import { BlossomClient } from 'nostr-tools/nipb7'
|
|
3
2
|
import nostrRelays from '#services/nostr-relays.js'
|
|
4
3
|
import { bytesToBase16 } from '#helpers/base16.js'
|
|
5
4
|
|
|
5
|
+
function normalizeServerUrl (url) {
|
|
6
|
+
if (!url.startsWith('http')) url = 'https://' + url
|
|
7
|
+
return url.replace(/\/$/, '') + '/'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function createAuthHeader (signer, modify) {
|
|
11
|
+
const now = Math.floor(Date.now() / 1000)
|
|
12
|
+
const event = {
|
|
13
|
+
created_at: now,
|
|
14
|
+
kind: 24242,
|
|
15
|
+
content: 'blossom stuff',
|
|
16
|
+
tags: [['expiration', String(now + 60)]]
|
|
17
|
+
}
|
|
18
|
+
if (modify) modify(event)
|
|
19
|
+
const signedEvent = await signer.signEvent(event)
|
|
20
|
+
return 'Nostr ' + btoa(JSON.stringify(signedEvent))
|
|
21
|
+
}
|
|
22
|
+
|
|
6
23
|
/**
|
|
7
24
|
* Fetches the user's blossom server list from their kind 10063 event.
|
|
8
25
|
* Returns an array of server URLs, or empty array if none configured.
|
|
@@ -27,31 +44,14 @@ export async function getBlossomServers (signer, writeRelays) {
|
|
|
27
44
|
}
|
|
28
45
|
|
|
29
46
|
/**
|
|
30
|
-
* Health-checks blossom servers
|
|
31
|
-
* A server is considered healthy if
|
|
32
|
-
*
|
|
33
|
-
* Returns the subset of servers that are reachable.
|
|
47
|
+
* Health-checks blossom servers with a simple HEAD request.
|
|
48
|
+
* A server is considered healthy if fetch resolves (any HTTP status).
|
|
49
|
+
* Only network-level errors mark a server as unreachable.
|
|
34
50
|
*/
|
|
35
51
|
export async function healthCheckServers (servers, signer, { log = () => {} } = {}) {
|
|
36
|
-
const randomBytes = crypto.getRandomValues(new Uint8Array(32))
|
|
37
|
-
const hashBuffer = await crypto.subtle.digest('SHA-256', randomBytes)
|
|
38
|
-
const randomHash = bytesToBase16(new Uint8Array(hashBuffer))
|
|
39
|
-
|
|
40
52
|
const results = await Promise.allSettled(
|
|
41
53
|
servers.map(async (serverUrl) => {
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
await client.check(randomHash)
|
|
45
|
-
} catch (err) {
|
|
46
|
-
// check() throws on non-2xx. A 404 means the server is up but blob doesn't exist — that's fine.
|
|
47
|
-
// We only want to filter out servers that are truly unreachable (network errors).
|
|
48
|
-
const message = err?.message ?? ''
|
|
49
|
-
if (message.includes('returned an error')) {
|
|
50
|
-
// Server responded with an HTTP error — it's reachable
|
|
51
|
-
return serverUrl
|
|
52
|
-
}
|
|
53
|
-
throw err
|
|
54
|
-
}
|
|
54
|
+
await fetch(normalizeServerUrl(serverUrl), { method: 'HEAD' })
|
|
55
55
|
return serverUrl
|
|
56
56
|
})
|
|
57
57
|
)
|
|
@@ -85,15 +85,12 @@ export async function computeFileHash (file) {
|
|
|
85
85
|
* Uploads a single file to a single blossom server with retry+backoff.
|
|
86
86
|
* Returns { success: true, descriptor } or { success: false, error }.
|
|
87
87
|
*/
|
|
88
|
-
async function uploadFileToServer (
|
|
88
|
+
async function uploadFileToServer (serverUrl, signer, file, fileHash, mimeType, { shouldReupload, log, maxRetries = 5 }) {
|
|
89
89
|
// Check if already uploaded
|
|
90
90
|
if (!shouldReupload) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
// File already exists on this server
|
|
91
|
+
const checkResponse = await fetch(serverUrl + fileHash, { method: 'HEAD' })
|
|
92
|
+
if (checkResponse.ok) {
|
|
94
93
|
return { success: true, alreadyExists: true }
|
|
95
|
-
} catch {
|
|
96
|
-
// Not found — proceed to upload
|
|
97
94
|
}
|
|
98
95
|
}
|
|
99
96
|
|
|
@@ -101,21 +98,25 @@ async function uploadFileToServer (client, file, fileHash, mimeType, { shouldReu
|
|
|
101
98
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
102
99
|
try {
|
|
103
100
|
if (attempt > 0) {
|
|
104
|
-
log(`Retrying upload to ${
|
|
101
|
+
log(`Retrying upload to ${serverUrl} (attempt ${attempt + 1}/${maxRetries + 1})`)
|
|
105
102
|
await new Promise(resolve => setTimeout(resolve, pause))
|
|
106
103
|
pause += 2000
|
|
107
104
|
}
|
|
108
|
-
const
|
|
109
|
-
'
|
|
110
|
-
'
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
)
|
|
105
|
+
const authorization = await createAuthHeader(signer, (evt) => {
|
|
106
|
+
evt.tags.push(['t', 'upload'])
|
|
107
|
+
evt.tags.push(['x', fileHash])
|
|
108
|
+
})
|
|
109
|
+
const response = await fetch(serverUrl + 'upload', {
|
|
110
|
+
method: 'PUT',
|
|
111
|
+
headers: { 'Content-Type': mimeType, Authorization: authorization },
|
|
112
|
+
body: file.stream(),
|
|
113
|
+
duplex: 'half'
|
|
114
|
+
})
|
|
115
|
+
if (response.status >= 300) {
|
|
116
|
+
const reason = response.headers.get('X-Reason') || response.statusText
|
|
117
|
+
throw new Error(`upload returned an error (${response.status}): ${reason}`)
|
|
118
|
+
}
|
|
119
|
+
const descriptor = await response.json()
|
|
119
120
|
return { success: true, descriptor }
|
|
120
121
|
} catch (err) {
|
|
121
122
|
if (attempt === maxRetries) {
|
|
@@ -160,24 +161,24 @@ export async function uploadFilesToBlossom ({
|
|
|
160
161
|
const fileServerResults = fileInfos.map(() => ({ successCount: 0, errors: [] }))
|
|
161
162
|
|
|
162
163
|
// Upload to each server in parallel, but within a server, upload files sequentially
|
|
163
|
-
const serverTasks = servers.map(async (
|
|
164
|
-
const
|
|
164
|
+
const serverTasks = servers.map(async (server) => {
|
|
165
|
+
const serverUrl = normalizeServerUrl(server)
|
|
165
166
|
|
|
166
167
|
for (let i = 0; i < fileInfos.length; i++) {
|
|
167
168
|
const info = fileInfos[i]
|
|
168
|
-
log(`Uploading ${info.filename} to ${
|
|
169
|
-
const result = await uploadFileToServer(
|
|
169
|
+
log(`Uploading ${info.filename} to ${server}`)
|
|
170
|
+
const result = await uploadFileToServer(serverUrl, signer, info.file, info.sha256, info.mimeType, { shouldReupload, log, maxRetries })
|
|
170
171
|
|
|
171
172
|
if (result.success) {
|
|
172
173
|
fileServerResults[i].successCount++
|
|
173
174
|
if (result.alreadyExists) {
|
|
174
|
-
log(`${info.filename}: Already exists on ${
|
|
175
|
+
log(`${info.filename}: Already exists on ${server}`)
|
|
175
176
|
} else {
|
|
176
|
-
log(`${info.filename}: Uploaded to ${
|
|
177
|
+
log(`${info.filename}: Uploaded to ${server}`)
|
|
177
178
|
}
|
|
178
179
|
} else {
|
|
179
|
-
fileServerResults[i].errors.push({ server
|
|
180
|
-
log(`${info.filename}: Failed to upload to ${
|
|
180
|
+
fileServerResults[i].errors.push({ server, error: result.error })
|
|
181
|
+
log(`${info.filename}: Failed to upload to ${server}: ${result.error?.message ?? result.error}`)
|
|
181
182
|
}
|
|
182
183
|
}
|
|
183
184
|
})
|