filepizza-client 2.0.1 → 2.1.1
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/README.md +50 -4
- package/dist/filepizza-downloader.d.ts +18 -100
- package/dist/filepizza-downloader.js +88 -215
- package/dist/filepizza-downloader.js.map +1 -1
- package/dist/filepizza-uploader.d.ts +14 -95
- package/dist/filepizza-uploader.js +79 -249
- package/dist/filepizza-uploader.js.map +1 -1
- package/dist/peerjs.d.ts +12 -0
- package/dist/peerjs.js +102 -0
- package/dist/peerjs.js.map +1 -0
- package/dist/types.d.ts +6 -2
- package/dist/types.js +4 -2
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/filepizza-downloader.ts +373 -459
- package/src/filepizza-uploader.ts +238 -392
- package/src/peerjs.ts +147 -0
- package/src/types.ts +37 -28
|
@@ -1,807 +1,721 @@
|
|
|
1
1
|
// src/filepizza-downloader.ts
|
|
2
2
|
import Peer, { DataConnection } from 'peerjs'
|
|
3
3
|
import { EventEmitter } from './event-emitter'
|
|
4
|
-
import { DownloadHelper } from './download-helper'
|
|
5
|
-
import {
|
|
4
|
+
import { DownloadHelper } from './download-helper'
|
|
5
|
+
import {
|
|
6
|
+
FileInfo,
|
|
7
|
+
ProgressInfo,
|
|
8
|
+
ConnectionStatus,
|
|
9
|
+
CompletedFile,
|
|
10
|
+
MessageType,
|
|
11
|
+
PeerJSSignalingServer,
|
|
12
|
+
} from './types'
|
|
13
|
+
import { createPeer } from './peerjs'
|
|
14
|
+
|
|
15
|
+
type DownloaderOptions = {
|
|
16
|
+
filePizzaServerUrl?: string
|
|
17
|
+
peerJSSignalingServer?: PeerJSSignalingServer
|
|
18
|
+
discoverPeerJSSignalingServer?: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type ChannelLookupResponse = {
|
|
22
|
+
uploaderPeerID: string
|
|
23
|
+
additionalUploaders?: string[]
|
|
24
|
+
}
|
|
6
25
|
|
|
7
26
|
/**
|
|
8
|
-
* FilePizza Downloader - connects to FilePizza uploads
|
|
27
|
+
* FilePizza Downloader - connects to FilePizza uploads.
|
|
9
28
|
*/
|
|
10
29
|
export class FilePizzaDownloader extends EventEmitter {
|
|
11
|
-
private peer?: Peer
|
|
12
|
-
private connection?: DataConnection
|
|
13
|
-
private filePizzaServerUrl: string
|
|
14
|
-
private filesInfo: FileInfo[] = []
|
|
15
|
-
private currentFileIndex = 0
|
|
16
|
-
private currentFileBytesReceived = 0
|
|
17
|
-
private totalBytesReceived = 0
|
|
18
|
-
private totalBytes = 0
|
|
19
|
-
private status = ConnectionStatus.Pending
|
|
20
|
-
private fileStreams: Map<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
private
|
|
29
|
-
private
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
constructor(options: {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
this.
|
|
30
|
+
private peer?: Peer
|
|
31
|
+
private connection?: DataConnection
|
|
32
|
+
private filePizzaServerUrl: string
|
|
33
|
+
private filesInfo: FileInfo[] = []
|
|
34
|
+
private currentFileIndex = 0
|
|
35
|
+
private currentFileBytesReceived = 0
|
|
36
|
+
private totalBytesReceived = 0
|
|
37
|
+
private totalBytes = 0
|
|
38
|
+
private status = ConnectionStatus.Pending
|
|
39
|
+
private fileStreams: Map<
|
|
40
|
+
string,
|
|
41
|
+
{
|
|
42
|
+
stream: ReadableStream<Uint8Array>
|
|
43
|
+
enqueue: (chunk: Uint8Array) => void
|
|
44
|
+
close: () => void
|
|
45
|
+
}
|
|
46
|
+
> = new Map()
|
|
47
|
+
private isPasswordRequired = false
|
|
48
|
+
private isPasswordInvalid = false
|
|
49
|
+
private errorMessage?: string
|
|
50
|
+
private completedFiles: CompletedFile[] = []
|
|
51
|
+
private peerJSSignalingServer?: PeerJSSignalingServer
|
|
52
|
+
private discoverPeerJSSignalingServer: boolean
|
|
53
|
+
|
|
54
|
+
constructor(options: DownloaderOptions = {}) {
|
|
55
|
+
super()
|
|
56
|
+
this.filePizzaServerUrl = options.filePizzaServerUrl || 'http://localhost:8081'
|
|
57
|
+
this.peerJSSignalingServer = options.peerJSSignalingServer
|
|
58
|
+
this.discoverPeerJSSignalingServer =
|
|
59
|
+
options.discoverPeerJSSignalingServer ?? true
|
|
40
60
|
}
|
|
41
61
|
|
|
42
|
-
/**
|
|
43
|
-
* Initialize the downloader
|
|
44
|
-
*/
|
|
45
62
|
async initialize(): Promise<void> {
|
|
46
63
|
if (this.peer) {
|
|
47
|
-
return
|
|
64
|
+
return
|
|
48
65
|
}
|
|
49
66
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
config: {
|
|
56
|
-
iceServers: this.iceServers || [{ urls: 'stun:stun.l.google.com:19302' }],
|
|
57
|
-
},
|
|
58
|
-
debug: 2,
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
// Wait for peer to be ready
|
|
62
|
-
if (!this.peer.id) {
|
|
63
|
-
await new Promise<void>((resolve) => {
|
|
64
|
-
const onOpen = () => {
|
|
65
|
-
this.peer?.off('open', onOpen);
|
|
66
|
-
resolve();
|
|
67
|
-
};
|
|
68
|
-
this.peer?.on('open', onOpen);
|
|
69
|
-
});
|
|
70
|
-
}
|
|
67
|
+
this.peer = await createPeer({
|
|
68
|
+
filePizzaServerUrl: this.filePizzaServerUrl,
|
|
69
|
+
peerJSSignalingServer: this.peerJSSignalingServer,
|
|
70
|
+
discoverPeerJSSignalingServer: this.discoverPeerJSSignalingServer,
|
|
71
|
+
})
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
/**
|
|
74
|
-
* Connect to an uploader using a FilePizza URL or slug
|
|
75
|
-
*/
|
|
76
74
|
async connect(urlOrSlug: string): Promise<boolean> {
|
|
77
|
-
this.resetConnectionState()
|
|
75
|
+
this.resetConnectionState()
|
|
78
76
|
|
|
79
|
-
const slug = this.extractSlug(urlOrSlug)
|
|
77
|
+
const slug = this.extractSlug(urlOrSlug)
|
|
80
78
|
|
|
81
79
|
try {
|
|
82
|
-
const uploaderPeerID = await this.lookupUploaderPeerID(slug)
|
|
83
|
-
return this.connectToPeer(uploaderPeerID)
|
|
80
|
+
const uploaderPeerID = await this.lookupUploaderPeerID(slug)
|
|
81
|
+
return this.connectToPeer(uploaderPeerID)
|
|
84
82
|
} catch (error) {
|
|
85
|
-
this.errorMessage = `Failed to connect: ${error instanceof Error ? error.message : String(error)
|
|
86
|
-
|
|
87
|
-
|
|
83
|
+
this.errorMessage = `Failed to connect: ${error instanceof Error ? error.message : String(error)
|
|
84
|
+
}`
|
|
85
|
+
this.emit('error', this.errorMessage)
|
|
86
|
+
return false
|
|
88
87
|
}
|
|
89
88
|
}
|
|
90
89
|
|
|
91
|
-
/**
|
|
92
|
-
* Reset the connection state to begin new download
|
|
93
|
-
*/
|
|
94
90
|
private resetConnectionState(): void {
|
|
95
|
-
this.cleanupFileStreams()
|
|
96
|
-
|
|
97
|
-
this.filesInfo = []
|
|
98
|
-
this.currentFileIndex = 0
|
|
99
|
-
this.currentFileBytesReceived = 0
|
|
100
|
-
this.totalBytesReceived = 0
|
|
101
|
-
this.totalBytes = 0
|
|
102
|
-
this.completedFiles = []
|
|
103
|
-
this.isPasswordRequired = false
|
|
104
|
-
this.isPasswordInvalid = false
|
|
105
|
-
this.errorMessage = undefined
|
|
91
|
+
this.cleanupFileStreams()
|
|
92
|
+
|
|
93
|
+
this.filesInfo = []
|
|
94
|
+
this.currentFileIndex = 0
|
|
95
|
+
this.currentFileBytesReceived = 0
|
|
96
|
+
this.totalBytesReceived = 0
|
|
97
|
+
this.totalBytes = 0
|
|
98
|
+
this.completedFiles = []
|
|
99
|
+
this.isPasswordRequired = false
|
|
100
|
+
this.isPasswordInvalid = false
|
|
101
|
+
this.errorMessage = undefined
|
|
102
|
+
this.status = ConnectionStatus.Pending
|
|
106
103
|
|
|
107
104
|
if (this.connection && this.connection.open) {
|
|
108
|
-
this.connection.close()
|
|
105
|
+
this.connection.close()
|
|
109
106
|
}
|
|
110
|
-
|
|
107
|
+
|
|
108
|
+
this.connection = undefined
|
|
111
109
|
}
|
|
112
110
|
|
|
113
|
-
/**
|
|
114
|
-
* Submit password for protected download
|
|
115
|
-
*/
|
|
116
111
|
submitPassword(password: string): void {
|
|
117
112
|
if (!this.connection || this.status !== ConnectionStatus.Authenticating) {
|
|
118
|
-
throw new Error('Not in authentication state')
|
|
113
|
+
throw new Error('Not in authentication state')
|
|
119
114
|
}
|
|
120
115
|
|
|
121
116
|
this.connection.send({
|
|
122
117
|
type: MessageType.UsePassword,
|
|
123
118
|
password,
|
|
124
|
-
})
|
|
119
|
+
})
|
|
125
120
|
}
|
|
126
121
|
|
|
127
|
-
/**
|
|
128
|
-
* Start downloading the files
|
|
129
|
-
*/
|
|
130
122
|
async startDownload(): Promise<void> {
|
|
131
123
|
if (!this.connection) {
|
|
132
|
-
throw new Error('Not connected')
|
|
124
|
+
throw new Error('Not connected')
|
|
133
125
|
}
|
|
134
126
|
|
|
135
127
|
if (this.filesInfo.length === 0) {
|
|
136
|
-
throw new Error('No files available')
|
|
128
|
+
throw new Error('No files available')
|
|
137
129
|
}
|
|
138
130
|
|
|
139
131
|
if (this.status !== ConnectionStatus.Ready) {
|
|
140
|
-
throw new Error(`Cannot start download in current state: ${this.status}`)
|
|
132
|
+
throw new Error(`Cannot start download in current state: ${this.status}`)
|
|
141
133
|
}
|
|
142
134
|
|
|
143
|
-
this.status = ConnectionStatus.Downloading
|
|
144
|
-
this.currentFileIndex = 0
|
|
145
|
-
this.currentFileBytesReceived = 0
|
|
146
|
-
this.totalBytesReceived = 0
|
|
147
|
-
|
|
148
|
-
// Initialize file streams
|
|
149
|
-
this.initializeFileStreams();
|
|
135
|
+
this.status = ConnectionStatus.Downloading
|
|
136
|
+
this.currentFileIndex = 0
|
|
137
|
+
this.currentFileBytesReceived = 0
|
|
138
|
+
this.totalBytesReceived = 0
|
|
150
139
|
|
|
151
|
-
|
|
152
|
-
this.requestNextFile()
|
|
140
|
+
this.initializeFileStreams()
|
|
141
|
+
this.requestNextFile()
|
|
153
142
|
}
|
|
154
143
|
|
|
155
|
-
/**
|
|
156
|
-
* Pause the download
|
|
157
|
-
*/
|
|
158
144
|
pauseDownload(): void {
|
|
159
145
|
if (!this.connection || this.status !== ConnectionStatus.Downloading) {
|
|
160
|
-
return
|
|
146
|
+
return
|
|
161
147
|
}
|
|
162
148
|
|
|
163
|
-
this.connection.send({ type: MessageType.Pause })
|
|
164
|
-
this.status = ConnectionStatus.Paused
|
|
165
|
-
this.emit('paused')
|
|
149
|
+
this.connection.send({ type: MessageType.Pause })
|
|
150
|
+
this.status = ConnectionStatus.Paused
|
|
151
|
+
this.emit('paused')
|
|
166
152
|
}
|
|
167
153
|
|
|
168
154
|
/**
|
|
169
|
-
*
|
|
155
|
+
* Kept for public API compatibility.
|
|
156
|
+
*
|
|
157
|
+
* The server frontend protocol currently supports pause/stop, not resume.
|
|
158
|
+
* A paused transfer should be restarted by creating a new connection.
|
|
170
159
|
*/
|
|
171
160
|
resumeDownload(): void {
|
|
172
|
-
if (
|
|
173
|
-
return
|
|
161
|
+
if (this.status !== ConnectionStatus.Paused) {
|
|
162
|
+
return
|
|
174
163
|
}
|
|
175
164
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
this.
|
|
179
|
-
|
|
180
|
-
fileName: currentFile.fileName,
|
|
181
|
-
offset: this.currentFileBytesReceived,
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
this.status = ConnectionStatus.Downloading;
|
|
185
|
-
this.emit('resumed');
|
|
165
|
+
this.errorMessage =
|
|
166
|
+
'Resume is not supported by the current FilePizza server protocol. Please reconnect and restart the download.'
|
|
167
|
+
this.status = ConnectionStatus.Error
|
|
168
|
+
this.emit('error', this.errorMessage)
|
|
186
169
|
}
|
|
187
170
|
|
|
188
|
-
/**
|
|
189
|
-
* Cancel the download
|
|
190
|
-
*/
|
|
191
171
|
cancelDownload(): void {
|
|
192
|
-
this.cleanupFileStreams()
|
|
172
|
+
this.cleanupFileStreams()
|
|
193
173
|
|
|
194
174
|
if (this.connection) {
|
|
195
175
|
if (this.connection.open) {
|
|
196
|
-
this.connection.close()
|
|
176
|
+
this.connection.close()
|
|
197
177
|
}
|
|
198
|
-
this.connection = undefined
|
|
178
|
+
this.connection = undefined
|
|
199
179
|
}
|
|
200
180
|
|
|
201
|
-
this.status = ConnectionStatus.Closed
|
|
202
|
-
this.emit('cancelled')
|
|
181
|
+
this.status = ConnectionStatus.Closed
|
|
182
|
+
this.emit('cancelled')
|
|
203
183
|
}
|
|
204
184
|
|
|
205
|
-
/**
|
|
206
|
-
* Cleanup resources when done
|
|
207
|
-
*/
|
|
208
185
|
private cleanupFileStreams(): void {
|
|
209
186
|
for (const [fileName, fileStreamData] of this.fileStreams.entries()) {
|
|
210
187
|
try {
|
|
211
188
|
if (fileStreamData.stream.locked === false) {
|
|
212
|
-
fileStreamData.close()
|
|
189
|
+
fileStreamData.close()
|
|
213
190
|
}
|
|
214
191
|
} catch (error) {
|
|
215
|
-
console.warn(`Error closing stream for ${fileName}:`, error)
|
|
192
|
+
console.warn(`Error closing stream for ${fileName}:`, error)
|
|
216
193
|
}
|
|
217
194
|
}
|
|
218
|
-
|
|
195
|
+
|
|
196
|
+
this.fileStreams.clear()
|
|
219
197
|
}
|
|
220
198
|
|
|
221
|
-
/**
|
|
222
|
-
* Get file information
|
|
223
|
-
*/
|
|
224
199
|
getFileInfo(): FileInfo[] {
|
|
225
|
-
return this.filesInfo
|
|
200
|
+
return this.filesInfo
|
|
226
201
|
}
|
|
227
202
|
|
|
228
|
-
/**
|
|
229
|
-
* Get download status
|
|
230
|
-
*/
|
|
231
203
|
getStatus(): {
|
|
232
|
-
status: ConnectionStatus
|
|
233
|
-
isPasswordRequired: boolean
|
|
234
|
-
isPasswordInvalid: boolean
|
|
235
|
-
errorMessage?: string
|
|
204
|
+
status: ConnectionStatus
|
|
205
|
+
isPasswordRequired: boolean
|
|
206
|
+
isPasswordInvalid: boolean
|
|
207
|
+
errorMessage?: string
|
|
236
208
|
} {
|
|
237
209
|
return {
|
|
238
210
|
status: this.status,
|
|
239
211
|
isPasswordRequired: this.isPasswordRequired,
|
|
240
212
|
isPasswordInvalid: this.isPasswordInvalid,
|
|
241
213
|
errorMessage: this.errorMessage,
|
|
242
|
-
}
|
|
214
|
+
}
|
|
243
215
|
}
|
|
244
216
|
|
|
245
|
-
/**
|
|
246
|
-
* Get progress information
|
|
247
|
-
*/
|
|
248
217
|
getProgress(): ProgressInfo {
|
|
249
218
|
return {
|
|
250
219
|
fileIndex: this.currentFileIndex,
|
|
251
220
|
fileName: this.filesInfo[this.currentFileIndex]?.fileName || '',
|
|
252
221
|
totalFiles: this.filesInfo.length,
|
|
253
|
-
currentFileProgress:
|
|
222
|
+
currentFileProgress:
|
|
223
|
+
this.currentFileBytesReceived /
|
|
254
224
|
(this.filesInfo[this.currentFileIndex]?.size || 1),
|
|
255
225
|
overallProgress: this.totalBytesReceived / (this.totalBytes || 1),
|
|
256
226
|
bytesTransferred: this.totalBytesReceived,
|
|
257
227
|
totalBytes: this.totalBytes,
|
|
258
|
-
}
|
|
228
|
+
}
|
|
259
229
|
}
|
|
260
230
|
|
|
261
|
-
/**
|
|
262
|
-
* Extract slug from URL or use directly
|
|
263
|
-
*/
|
|
264
231
|
private extractSlug(urlOrSlug: string): string {
|
|
265
|
-
// Check if it's a URL
|
|
266
232
|
if (urlOrSlug.startsWith('http')) {
|
|
267
|
-
const url = new URL(urlOrSlug)
|
|
268
|
-
const pathParts = url.pathname.split('/').filter(Boolean)
|
|
233
|
+
const url = new URL(urlOrSlug)
|
|
234
|
+
const pathParts = url.pathname.split('/').filter(Boolean)
|
|
269
235
|
|
|
270
|
-
// Extract the download slug
|
|
271
236
|
if (pathParts[0] === 'download' && pathParts.length > 1) {
|
|
272
|
-
return pathParts.slice(1).join('/')
|
|
273
|
-
} else {
|
|
274
|
-
throw new Error('Invalid FilePizza URL');
|
|
237
|
+
return pathParts.slice(1).join('/')
|
|
275
238
|
}
|
|
239
|
+
|
|
240
|
+
throw new Error('Invalid FilePizza URL')
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return urlOrSlug
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private async fetchChannel(slug: string): Promise<ChannelLookupResponse> {
|
|
247
|
+
const response = await fetch(`${this.filePizzaServerUrl}/api/channel/${slug}`)
|
|
248
|
+
|
|
249
|
+
if (!response.ok) {
|
|
250
|
+
throw new Error(`FilePizza server returned ${response.status}`)
|
|
276
251
|
}
|
|
277
252
|
|
|
278
|
-
|
|
279
|
-
return urlOrSlug;
|
|
253
|
+
return response.json()
|
|
280
254
|
}
|
|
281
255
|
|
|
282
256
|
/**
|
|
283
|
-
*
|
|
257
|
+
* Fallback for older FilePizza servers that do not expose /api/channel/:slug.
|
|
284
258
|
*/
|
|
285
|
-
private async
|
|
286
|
-
|
|
287
|
-
const response = await fetch(`${this.filePizzaServerUrl}/download/${slug}`);
|
|
259
|
+
private async extractUploaderPeerIDsFromHtml(slug: string): Promise<string[]> {
|
|
260
|
+
const response = await fetch(`${this.filePizzaServerUrl}/download/${slug}`)
|
|
288
261
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
262
|
+
if (!response.ok) {
|
|
263
|
+
throw new Error(`FilePizza server returned ${response.status}`)
|
|
264
|
+
}
|
|
292
265
|
|
|
293
|
-
|
|
266
|
+
const html = await response.text()
|
|
294
267
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
// Match either "uploaderPeerID" (for single uploader) or "primaryUploaderID" and "additionalUploaders" (for multiple)
|
|
300
|
-
const primaryMatch = html.match(/\\"primaryUploaderID\\":\\"([^\\]+)\\"/);
|
|
301
|
-
const singleMatch = html.match(/\\"uploaderPeerID\\":\\"([^\\]+)\\"/);
|
|
268
|
+
if (!html || html.trim() === '') {
|
|
269
|
+
throw new Error('Received empty response from server')
|
|
270
|
+
}
|
|
302
271
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const uploaderIDs = [primaryMatch[1]];
|
|
272
|
+
const primaryMatch = html.match(/\\"primaryUploaderID\\":\\"([^\\]+)\\"/)
|
|
273
|
+
const singleMatch = html.match(/\\"uploaderPeerID\\":\\"([^\\]+)\\"/)
|
|
306
274
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
if (additionalMatch && additionalMatch[1]) {
|
|
310
|
-
// Parse additional uploader IDs from JSON string
|
|
311
|
-
const additionalIDs = additionalMatch[1].split(',')
|
|
312
|
-
.map(id => id.trim().replace(/\\"|"/g, ''))
|
|
313
|
-
.filter(id => id.length > 0);
|
|
275
|
+
if (primaryMatch && primaryMatch[1]) {
|
|
276
|
+
const uploaderIDs = [primaryMatch[1]]
|
|
314
277
|
|
|
315
|
-
|
|
316
|
-
|
|
278
|
+
const additionalMatch = html.match(/\\"additionalUploaders\\":\[([^\]]+)\]/)
|
|
279
|
+
if (additionalMatch && additionalMatch[1]) {
|
|
280
|
+
const additionalIDs = additionalMatch[1]
|
|
281
|
+
.split(',')
|
|
282
|
+
.map((id) => id.trim().replace(/\\"|"/g, ''))
|
|
283
|
+
.filter((id) => id.length > 0)
|
|
317
284
|
|
|
318
|
-
|
|
319
|
-
} else if (singleMatch && singleMatch[1]) {
|
|
320
|
-
// Handle single uploader case
|
|
321
|
-
return [singleMatch[1]];
|
|
285
|
+
uploaderIDs.push(...additionalIDs)
|
|
322
286
|
}
|
|
323
287
|
|
|
324
|
-
|
|
325
|
-
} catch (error) {
|
|
326
|
-
console.error('Error extracting uploader peer IDs:', error);
|
|
327
|
-
throw new Error('Failed to look up uploader information');
|
|
288
|
+
return uploaderIDs
|
|
328
289
|
}
|
|
290
|
+
|
|
291
|
+
if (singleMatch && singleMatch[1]) {
|
|
292
|
+
return [singleMatch[1]]
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
throw new Error('Could not find uploader peer ID')
|
|
329
296
|
}
|
|
330
297
|
|
|
331
|
-
|
|
332
|
-
* Look up the uploader's peer ID from the FilePizza server
|
|
333
|
-
*/
|
|
334
|
-
private async lookupUploaderPeerID(slug: string): Promise<string> {
|
|
298
|
+
private async extractUploaderPeerIDs(slug: string): Promise<string[]> {
|
|
335
299
|
try {
|
|
336
|
-
const
|
|
300
|
+
const channel = await this.fetchChannel(slug)
|
|
301
|
+
return [
|
|
302
|
+
channel.uploaderPeerID,
|
|
303
|
+
...(channel.additionalUploaders || []),
|
|
304
|
+
].filter(Boolean)
|
|
305
|
+
} catch (error) {
|
|
306
|
+
console.warn(
|
|
307
|
+
'Falling back to HTML parsing because /api/channel lookup failed:',
|
|
308
|
+
error,
|
|
309
|
+
)
|
|
337
310
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
311
|
+
return this.extractUploaderPeerIDsFromHtml(slug)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
341
314
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
throw new Error('
|
|
315
|
+
private async lookupUploaderPeerID(slug: string): Promise<string> {
|
|
316
|
+
const uploaderIDs = await this.extractUploaderPeerIDs(slug)
|
|
317
|
+
|
|
318
|
+
if (uploaderIDs.length === 0) {
|
|
319
|
+
throw new Error('No uploader peer IDs found')
|
|
347
320
|
}
|
|
321
|
+
|
|
322
|
+
return uploaderIDs[0]
|
|
348
323
|
}
|
|
349
324
|
|
|
350
|
-
/**
|
|
351
|
-
* Connect to a specific uploader by ID
|
|
352
|
-
*/
|
|
353
325
|
public async getAvailableUploaders(urlOrSlug: string): Promise<string[]> {
|
|
354
|
-
const slug = this.extractSlug(urlOrSlug)
|
|
355
|
-
return this.extractUploaderPeerIDs(slug)
|
|
326
|
+
const slug = this.extractSlug(urlOrSlug)
|
|
327
|
+
return this.extractUploaderPeerIDs(slug)
|
|
356
328
|
}
|
|
357
329
|
|
|
358
|
-
/**
|
|
359
|
-
* Connect to a specific uploader by ID
|
|
360
|
-
*/
|
|
361
330
|
public async connectToUploader(uploaderId: string): Promise<boolean> {
|
|
362
331
|
try {
|
|
363
|
-
return this.connectToPeer(uploaderId)
|
|
332
|
+
return this.connectToPeer(uploaderId)
|
|
364
333
|
} catch (error) {
|
|
365
|
-
this.errorMessage = `Failed to connect to uploader: ${error instanceof Error ? error.message : String(error)
|
|
366
|
-
|
|
367
|
-
|
|
334
|
+
this.errorMessage = `Failed to connect to uploader: ${error instanceof Error ? error.message : String(error)
|
|
335
|
+
}`
|
|
336
|
+
this.emit('error', this.errorMessage)
|
|
337
|
+
return false
|
|
368
338
|
}
|
|
369
339
|
}
|
|
370
340
|
|
|
371
|
-
/**
|
|
372
|
-
* Get ICE servers from the FilePizza server
|
|
373
|
-
*/
|
|
374
|
-
private async getIceServers(): Promise<RTCIceServer[]> {
|
|
375
|
-
try {
|
|
376
|
-
const response = await fetch(`${this.filePizzaServerUrl}/api/ice`, {
|
|
377
|
-
method: 'POST',
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
if (!response.ok) {
|
|
381
|
-
throw new Error(`Failed to get ICE servers: ${response.status}`);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
const data = await response.json();
|
|
385
|
-
this.iceServers = data.iceServers;
|
|
386
|
-
return data.iceServers;
|
|
387
|
-
} catch (error) {
|
|
388
|
-
console.error('Error getting ICE servers:', error);
|
|
389
|
-
return [{ urls: 'stun:stun.l.google.com:19302' }];
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
/**
|
|
394
|
-
* Connect directly to a peer
|
|
395
|
-
*/
|
|
396
341
|
private async connectToPeer(peerId: string): Promise<boolean> {
|
|
397
|
-
|
|
398
|
-
await this.initialize();
|
|
342
|
+
await this.initialize()
|
|
399
343
|
|
|
400
344
|
if (!this.peer) {
|
|
401
|
-
throw new Error('Peer not initialized')
|
|
345
|
+
throw new Error('Peer not initialized')
|
|
402
346
|
}
|
|
403
347
|
|
|
404
348
|
try {
|
|
405
|
-
// Close existing connection if any
|
|
406
349
|
if (this.connection) {
|
|
407
|
-
this.connection.close()
|
|
350
|
+
this.connection.close()
|
|
408
351
|
}
|
|
409
352
|
|
|
410
|
-
|
|
411
|
-
this.
|
|
412
|
-
this.status = ConnectionStatus.Pending;
|
|
353
|
+
this.connection = this.peer.connect(peerId, { reliable: true })
|
|
354
|
+
this.status = ConnectionStatus.Pending
|
|
413
355
|
|
|
414
|
-
// Set up connection event handlers
|
|
415
356
|
return new Promise<boolean>((resolve) => {
|
|
416
357
|
if (!this.connection) {
|
|
417
|
-
resolve(false)
|
|
418
|
-
return
|
|
358
|
+
resolve(false)
|
|
359
|
+
return
|
|
419
360
|
}
|
|
420
361
|
|
|
421
362
|
this.connection.on('open', () => {
|
|
422
|
-
this.status = ConnectionStatus.Ready
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
this.
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
mobileModel: this.getMobileModel(),
|
|
434
|
-
});
|
|
435
|
-
}
|
|
363
|
+
this.status = ConnectionStatus.Ready
|
|
364
|
+
|
|
365
|
+
this.connection?.send({
|
|
366
|
+
type: MessageType.RequestInfo,
|
|
367
|
+
browserName: this.getBrowserName(),
|
|
368
|
+
browserVersion: this.getBrowserVersion(),
|
|
369
|
+
osName: this.getOSName(),
|
|
370
|
+
osVersion: this.getOSVersion(),
|
|
371
|
+
mobileVendor: this.getMobileVendor(),
|
|
372
|
+
mobileModel: this.getMobileModel(),
|
|
373
|
+
})
|
|
436
374
|
|
|
437
|
-
this.emit('connected')
|
|
438
|
-
resolve(true)
|
|
439
|
-
})
|
|
375
|
+
this.emit('connected')
|
|
376
|
+
resolve(true)
|
|
377
|
+
})
|
|
440
378
|
|
|
441
|
-
this.connection.on('data', this.handleData.bind(this))
|
|
379
|
+
this.connection.on('data', this.handleData.bind(this))
|
|
442
380
|
|
|
443
381
|
this.connection.on('close', () => {
|
|
444
|
-
this.status = ConnectionStatus.Closed
|
|
445
|
-
this.emit('disconnected')
|
|
446
|
-
})
|
|
382
|
+
this.status = ConnectionStatus.Closed
|
|
383
|
+
this.emit('disconnected')
|
|
384
|
+
})
|
|
447
385
|
|
|
448
386
|
this.connection.on('error', (error) => {
|
|
449
|
-
this.errorMessage = `Connection error: ${error.message}
|
|
450
|
-
this.status = ConnectionStatus.Error
|
|
451
|
-
this.emit('error', this.errorMessage)
|
|
452
|
-
})
|
|
453
|
-
})
|
|
387
|
+
this.errorMessage = `Connection error: ${error.message}`
|
|
388
|
+
this.status = ConnectionStatus.Error
|
|
389
|
+
this.emit('error', this.errorMessage)
|
|
390
|
+
})
|
|
391
|
+
})
|
|
454
392
|
} catch (error) {
|
|
455
|
-
this.errorMessage = `Failed to connect: ${error instanceof Error ? error.message : String(error)
|
|
456
|
-
|
|
457
|
-
|
|
393
|
+
this.errorMessage = `Failed to connect: ${error instanceof Error ? error.message : String(error)
|
|
394
|
+
}`
|
|
395
|
+
this.emit('error', this.errorMessage)
|
|
396
|
+
return false
|
|
458
397
|
}
|
|
459
398
|
}
|
|
460
399
|
|
|
461
|
-
/**
|
|
462
|
-
* Handle incoming data from uploader
|
|
463
|
-
*/
|
|
464
400
|
private handleData(data: unknown): void {
|
|
465
401
|
try {
|
|
466
|
-
|
|
467
|
-
const message = data as any;
|
|
402
|
+
const message = data as any
|
|
468
403
|
|
|
469
404
|
switch (message.type) {
|
|
470
405
|
case MessageType.PasswordRequired:
|
|
471
|
-
this.handlePasswordRequired(message)
|
|
472
|
-
break
|
|
406
|
+
this.handlePasswordRequired(message)
|
|
407
|
+
break
|
|
473
408
|
|
|
474
409
|
case MessageType.Info:
|
|
475
|
-
this.handleInfo(message)
|
|
476
|
-
break
|
|
410
|
+
this.handleInfo(message)
|
|
411
|
+
break
|
|
477
412
|
|
|
478
413
|
case MessageType.Chunk:
|
|
479
|
-
this.handleChunk(message)
|
|
480
|
-
break
|
|
414
|
+
this.handleChunk(message)
|
|
415
|
+
break
|
|
481
416
|
|
|
482
417
|
case MessageType.Error:
|
|
483
|
-
this.handleError(message)
|
|
484
|
-
break
|
|
418
|
+
this.handleError(message)
|
|
419
|
+
break
|
|
485
420
|
|
|
486
421
|
case MessageType.Report:
|
|
487
|
-
this.handleReport()
|
|
488
|
-
break
|
|
422
|
+
this.handleReport()
|
|
423
|
+
break
|
|
489
424
|
}
|
|
490
425
|
} catch (error) {
|
|
491
|
-
console.error('Error handling message:', error)
|
|
492
|
-
this.errorMessage = `Error processing data: ${error instanceof Error ? error.message : String(error)
|
|
493
|
-
|
|
426
|
+
console.error('Error handling message:', error)
|
|
427
|
+
this.errorMessage = `Error processing data: ${error instanceof Error ? error.message : String(error)
|
|
428
|
+
}`
|
|
429
|
+
this.emit('error', this.errorMessage)
|
|
494
430
|
}
|
|
495
431
|
}
|
|
496
432
|
|
|
497
|
-
/**
|
|
498
|
-
* Handle password required message
|
|
499
|
-
*/
|
|
500
433
|
private handlePasswordRequired(message: any): void {
|
|
501
|
-
this.isPasswordRequired = true
|
|
502
|
-
this.status = ConnectionStatus.Authenticating
|
|
434
|
+
this.isPasswordRequired = true
|
|
435
|
+
this.status = ConnectionStatus.Authenticating
|
|
503
436
|
|
|
504
437
|
if (message.errorMessage) {
|
|
505
|
-
this.errorMessage = message.errorMessage
|
|
506
|
-
this.isPasswordInvalid = true
|
|
507
|
-
this.emit('passwordInvalid', message.errorMessage)
|
|
438
|
+
this.errorMessage = message.errorMessage
|
|
439
|
+
this.isPasswordInvalid = true
|
|
440
|
+
this.emit('passwordInvalid', message.errorMessage)
|
|
508
441
|
} else {
|
|
509
|
-
this.emit('passwordRequired')
|
|
442
|
+
this.emit('passwordRequired')
|
|
510
443
|
}
|
|
511
444
|
}
|
|
512
445
|
|
|
513
|
-
/**
|
|
514
|
-
* Handle file info message
|
|
515
|
-
*/
|
|
516
446
|
private handleInfo(message: any): void {
|
|
517
|
-
this.filesInfo = message.files
|
|
518
|
-
this.totalBytes = this.filesInfo.reduce(
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
447
|
+
this.filesInfo = message.files
|
|
448
|
+
this.totalBytes = this.filesInfo.reduce(
|
|
449
|
+
(sum: number, file: FileInfo) => sum + file.size,
|
|
450
|
+
0,
|
|
451
|
+
)
|
|
452
|
+
this.isPasswordRequired = false
|
|
453
|
+
this.isPasswordInvalid = false
|
|
454
|
+
this.errorMessage = undefined
|
|
455
|
+
this.status = ConnectionStatus.Ready
|
|
522
456
|
|
|
523
|
-
this.emit('info', this.filesInfo)
|
|
457
|
+
this.emit('info', this.filesInfo)
|
|
524
458
|
}
|
|
525
459
|
|
|
526
|
-
/**
|
|
527
|
-
* Handle error message
|
|
528
|
-
*/
|
|
529
460
|
private handleError(message: any): void {
|
|
530
|
-
this.errorMessage = message.error
|
|
531
|
-
this.status = ConnectionStatus.Error
|
|
532
|
-
this.emit('error', this.errorMessage)
|
|
461
|
+
this.errorMessage = message.error
|
|
462
|
+
this.status = ConnectionStatus.Error
|
|
463
|
+
this.emit('error', this.errorMessage)
|
|
533
464
|
}
|
|
534
465
|
|
|
535
|
-
/**
|
|
536
|
-
* Handle report message (channel reported for violation)
|
|
537
|
-
*/
|
|
538
466
|
private handleReport(): void {
|
|
539
|
-
this.emit('reported')
|
|
467
|
+
this.emit('reported')
|
|
540
468
|
|
|
541
|
-
// Redirect to reported page if in browser
|
|
542
469
|
if (typeof window !== 'undefined') {
|
|
543
|
-
window.location.href = `${this.filePizzaServerUrl}/reported
|
|
470
|
+
window.location.href = `${this.filePizzaServerUrl}/reported`
|
|
544
471
|
}
|
|
545
472
|
}
|
|
546
473
|
|
|
547
|
-
/**
|
|
548
|
-
* Handle chunk message
|
|
549
|
-
*/
|
|
550
474
|
private handleChunk(message: any): void {
|
|
551
|
-
const { fileName, bytes, final } = message
|
|
552
|
-
const fileStream = this.fileStreams.get(fileName)
|
|
475
|
+
const { fileName, bytes, final, offset } = message
|
|
476
|
+
const fileStream = this.fileStreams.get(fileName)
|
|
553
477
|
|
|
554
478
|
if (!fileStream) {
|
|
555
|
-
console.error(`No stream found for file: ${fileName}`)
|
|
556
|
-
return
|
|
479
|
+
console.error(`No stream found for file: ${fileName}`)
|
|
480
|
+
return
|
|
557
481
|
}
|
|
558
482
|
|
|
559
|
-
const data = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes)
|
|
483
|
+
const data = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes)
|
|
560
484
|
|
|
561
|
-
this.currentFileBytesReceived += data.byteLength
|
|
562
|
-
this.totalBytesReceived += data.byteLength
|
|
485
|
+
this.currentFileBytesReceived += data.byteLength
|
|
486
|
+
this.totalBytesReceived += data.byteLength
|
|
563
487
|
|
|
564
488
|
try {
|
|
565
|
-
fileStream.enqueue(data)
|
|
489
|
+
fileStream.enqueue(data)
|
|
566
490
|
} catch (error) {
|
|
567
|
-
console.error(`Error enqueueing data for ${fileName}:`, error)
|
|
568
|
-
return
|
|
491
|
+
console.error(`Error enqueueing data for ${fileName}:`, error)
|
|
492
|
+
return
|
|
569
493
|
}
|
|
570
494
|
|
|
571
|
-
this.
|
|
495
|
+
this.connection?.send({
|
|
496
|
+
type: MessageType.ChunkAck,
|
|
497
|
+
fileName,
|
|
498
|
+
offset,
|
|
499
|
+
bytesReceived: data.byteLength,
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
this.emit('progress', this.getProgress())
|
|
572
503
|
|
|
573
504
|
if (final) {
|
|
574
505
|
try {
|
|
575
|
-
fileStream.close()
|
|
506
|
+
fileStream.close()
|
|
576
507
|
} catch (error) {
|
|
577
|
-
console.warn(`Error closing stream for ${fileName}:`, error)
|
|
508
|
+
console.warn(`Error closing stream for ${fileName}:`, error)
|
|
578
509
|
}
|
|
579
510
|
|
|
580
|
-
this.storeCompletedFile(fileName)
|
|
511
|
+
this.storeCompletedFile(fileName)
|
|
581
512
|
|
|
582
|
-
this.currentFileIndex
|
|
583
|
-
this.currentFileBytesReceived = 0
|
|
513
|
+
this.currentFileIndex++
|
|
514
|
+
this.currentFileBytesReceived = 0
|
|
584
515
|
|
|
585
516
|
if (this.currentFileIndex < this.filesInfo.length) {
|
|
586
|
-
this.requestNextFile()
|
|
517
|
+
this.requestNextFile()
|
|
587
518
|
} else {
|
|
588
|
-
this.status = ConnectionStatus.Done
|
|
589
|
-
this.
|
|
519
|
+
this.status = ConnectionStatus.Done
|
|
520
|
+
this.connection?.send({ type: MessageType.Done })
|
|
521
|
+
this.emit('complete', this.completedFiles)
|
|
590
522
|
}
|
|
591
523
|
}
|
|
592
524
|
}
|
|
593
525
|
|
|
594
|
-
/**
|
|
595
|
-
* Initialize streams for all files
|
|
596
|
-
*/
|
|
597
526
|
private initializeFileStreams(): void {
|
|
598
|
-
this.cleanupFileStreams()
|
|
527
|
+
this.cleanupFileStreams()
|
|
599
528
|
|
|
600
529
|
for (const fileInfo of this.filesInfo) {
|
|
601
|
-
let enqueue: ((chunk: Uint8Array) => void) | null = null
|
|
602
|
-
let close: (() => void) | null = null
|
|
530
|
+
let enqueue: ((chunk: Uint8Array) => void) | null = null
|
|
531
|
+
let close: (() => void) | null = null
|
|
603
532
|
|
|
604
533
|
const stream = new ReadableStream<Uint8Array>({
|
|
605
534
|
start(controller) {
|
|
606
|
-
enqueue = (chunk: Uint8Array) => controller.enqueue(chunk)
|
|
535
|
+
enqueue = (chunk: Uint8Array) => controller.enqueue(chunk)
|
|
607
536
|
close = () => {
|
|
608
537
|
try {
|
|
609
|
-
controller.close()
|
|
538
|
+
controller.close()
|
|
610
539
|
} catch (error) {
|
|
611
|
-
console.warn('Controller already closed:', error)
|
|
540
|
+
console.warn('Controller already closed:', error)
|
|
612
541
|
}
|
|
613
|
-
}
|
|
542
|
+
}
|
|
614
543
|
},
|
|
615
|
-
})
|
|
544
|
+
})
|
|
616
545
|
|
|
617
546
|
if (!enqueue || !close) {
|
|
618
|
-
throw new Error('Failed to initialize stream controllers')
|
|
547
|
+
throw new Error('Failed to initialize stream controllers')
|
|
619
548
|
}
|
|
620
549
|
|
|
621
550
|
this.fileStreams.set(fileInfo.fileName, {
|
|
622
551
|
stream,
|
|
623
552
|
enqueue,
|
|
624
553
|
close,
|
|
625
|
-
})
|
|
554
|
+
})
|
|
626
555
|
}
|
|
627
556
|
}
|
|
628
557
|
|
|
629
|
-
/**
|
|
630
|
-
* Store a completed file
|
|
631
|
-
*/
|
|
632
558
|
private async storeCompletedFile(fileName: string): Promise<void> {
|
|
633
|
-
const fileStream = this.fileStreams.get(fileName)
|
|
634
|
-
const fileInfo = this.filesInfo.find(info => info.fileName === fileName)
|
|
559
|
+
const fileStream = this.fileStreams.get(fileName)
|
|
560
|
+
const fileInfo = this.filesInfo.find((info) => info.fileName === fileName)
|
|
635
561
|
|
|
636
562
|
if (!fileStream || !fileInfo) {
|
|
637
|
-
console.error(`No stream or file info found for file: ${fileName}`)
|
|
638
|
-
return
|
|
563
|
+
console.error(`No stream or file info found for file: ${fileName}`)
|
|
564
|
+
return
|
|
639
565
|
}
|
|
640
566
|
|
|
641
567
|
try {
|
|
642
|
-
const fileData = await DownloadHelper.streamToUint8Array(fileStream.stream)
|
|
568
|
+
const fileData = await DownloadHelper.streamToUint8Array(fileStream.stream)
|
|
643
569
|
|
|
644
570
|
const completedFile: CompletedFile = {
|
|
645
571
|
...fileInfo,
|
|
646
572
|
data: fileData,
|
|
647
|
-
}
|
|
573
|
+
}
|
|
648
574
|
|
|
649
|
-
this.completedFiles.push(completedFile)
|
|
650
|
-
this.emit('fileComplete', completedFile)
|
|
651
|
-
} catch (error) {
|
|
652
|
-
console.error(`Error storing file ${fileName}:`, error)
|
|
653
|
-
this.emit('error', `Failed to store file: ${error.message}`)
|
|
575
|
+
this.completedFiles.push(completedFile)
|
|
576
|
+
this.emit('fileComplete', completedFile)
|
|
577
|
+
} catch (error: any) {
|
|
578
|
+
console.error(`Error storing file ${fileName}:`, error)
|
|
579
|
+
this.emit('error', `Failed to store file: ${error.message}`)
|
|
654
580
|
}
|
|
655
581
|
}
|
|
656
582
|
|
|
657
|
-
/**
|
|
658
|
-
* Download a completed file
|
|
659
|
-
*/
|
|
660
583
|
public async downloadFile(fileName: string): Promise<void> {
|
|
661
|
-
const completedFile = this.completedFiles.find(
|
|
584
|
+
const completedFile = this.completedFiles.find(
|
|
585
|
+
(file) => file.fileName === fileName,
|
|
586
|
+
)
|
|
662
587
|
|
|
663
588
|
if (!completedFile) {
|
|
664
|
-
throw new Error(`File not found: ${fileName}`)
|
|
589
|
+
throw new Error(`File not found: ${fileName}`)
|
|
665
590
|
}
|
|
666
591
|
|
|
667
592
|
try {
|
|
668
|
-
await DownloadHelper.downloadFile(fileName, completedFile.data)
|
|
669
|
-
} catch (error) {
|
|
670
|
-
console.error(`Error downloading file ${fileName}:`, error)
|
|
671
|
-
throw new Error(`Failed to download file: ${error.message}`)
|
|
593
|
+
await DownloadHelper.downloadFile(fileName, completedFile.data)
|
|
594
|
+
} catch (error: any) {
|
|
595
|
+
console.error(`Error downloading file ${fileName}:`, error)
|
|
596
|
+
throw new Error(`Failed to download file: ${error.message}`)
|
|
672
597
|
}
|
|
673
598
|
}
|
|
674
599
|
|
|
675
|
-
/**
|
|
676
|
-
* Get completed files
|
|
677
|
-
*/
|
|
678
600
|
public getCompletedFiles(): CompletedFile[] {
|
|
679
|
-
return [...this.completedFiles]
|
|
601
|
+
return [...this.completedFiles]
|
|
680
602
|
}
|
|
681
603
|
|
|
682
|
-
/**
|
|
683
|
-
* Download all completed files
|
|
684
|
-
*/
|
|
685
604
|
public async downloadAllFiles(): Promise<void> {
|
|
686
605
|
for (const file of this.completedFiles) {
|
|
687
606
|
try {
|
|
688
|
-
await this.downloadFile(file.fileName)
|
|
607
|
+
await this.downloadFile(file.fileName)
|
|
689
608
|
} catch (error) {
|
|
690
|
-
console.error(`Error downloading file ${file.fileName}:`, error)
|
|
691
|
-
// Continue with other files even if one fails
|
|
609
|
+
console.error(`Error downloading file ${file.fileName}:`, error)
|
|
692
610
|
}
|
|
693
611
|
}
|
|
694
612
|
}
|
|
695
613
|
|
|
696
|
-
/**
|
|
697
|
-
* Request the next file
|
|
698
|
-
*/
|
|
699
614
|
private requestNextFile(): void {
|
|
700
615
|
if (!this.connection || this.currentFileIndex >= this.filesInfo.length) {
|
|
701
|
-
return
|
|
616
|
+
return
|
|
702
617
|
}
|
|
703
618
|
|
|
704
|
-
const nextFile = this.filesInfo[this.currentFileIndex]
|
|
619
|
+
const nextFile = this.filesInfo[this.currentFileIndex]
|
|
705
620
|
|
|
706
621
|
this.connection.send({
|
|
707
622
|
type: MessageType.Start,
|
|
708
623
|
fileName: nextFile.fileName,
|
|
709
624
|
offset: 0,
|
|
710
|
-
})
|
|
625
|
+
})
|
|
711
626
|
}
|
|
712
627
|
|
|
713
|
-
// Browser detection helper methods
|
|
714
628
|
private getBrowserName(): string {
|
|
715
|
-
if (typeof navigator === 'undefined') return 'unknown'
|
|
716
|
-
const ua = navigator.userAgent
|
|
717
|
-
if (ua.includes('Firefox')) return 'Firefox'
|
|
718
|
-
if (ua.includes('Chrome')) return 'Chrome'
|
|
719
|
-
if (ua.includes('Safari')) return 'Safari'
|
|
720
|
-
if (ua.includes('Edge')) return 'Edge'
|
|
721
|
-
if (ua.includes('MSIE') || ua.includes('Trident/')) return 'IE'
|
|
722
|
-
return 'unknown'
|
|
629
|
+
if (typeof navigator === 'undefined') return 'unknown'
|
|
630
|
+
const ua = navigator.userAgent
|
|
631
|
+
if (ua.includes('Firefox')) return 'Firefox'
|
|
632
|
+
if (ua.includes('Chrome')) return 'Chrome'
|
|
633
|
+
if (ua.includes('Safari')) return 'Safari'
|
|
634
|
+
if (ua.includes('Edge')) return 'Edge'
|
|
635
|
+
if (ua.includes('MSIE') || ua.includes('Trident/')) return 'IE'
|
|
636
|
+
return 'unknown'
|
|
723
637
|
}
|
|
724
638
|
|
|
725
639
|
private getBrowserVersion(): string {
|
|
726
|
-
if (typeof navigator === 'undefined') return 'unknown'
|
|
727
|
-
const ua = navigator.userAgent
|
|
640
|
+
if (typeof navigator === 'undefined') return 'unknown'
|
|
641
|
+
const ua = navigator.userAgent
|
|
728
642
|
|
|
729
|
-
let match
|
|
643
|
+
let match
|
|
730
644
|
if ((match = ua.match(/(Firefox|Chrome|Safari|Edge|MSIE)\/(\d+\.\d+)/))) {
|
|
731
|
-
return match[2]
|
|
645
|
+
return match[2]
|
|
732
646
|
}
|
|
733
647
|
if ((match = ua.match(/rv:(\d+\.\d+)/))) {
|
|
734
|
-
return match[1]
|
|
648
|
+
return match[1]
|
|
735
649
|
}
|
|
736
650
|
|
|
737
|
-
return 'unknown'
|
|
651
|
+
return 'unknown'
|
|
738
652
|
}
|
|
739
653
|
|
|
740
654
|
private getOSName(): string {
|
|
741
|
-
if (typeof navigator === 'undefined') return 'unknown'
|
|
742
|
-
const ua = navigator.userAgent
|
|
655
|
+
if (typeof navigator === 'undefined') return 'unknown'
|
|
656
|
+
const ua = navigator.userAgent
|
|
743
657
|
|
|
744
|
-
if (ua.includes('Windows')) return 'Windows'
|
|
745
|
-
if (ua.includes('Mac OS X')) return 'macOS'
|
|
746
|
-
if (ua.includes('Linux')) return 'Linux'
|
|
747
|
-
if (ua.includes('Android')) return 'Android'
|
|
748
|
-
if (ua.includes('iOS')) return 'iOS'
|
|
658
|
+
if (ua.includes('Windows')) return 'Windows'
|
|
659
|
+
if (ua.includes('Mac OS X')) return 'macOS'
|
|
660
|
+
if (ua.includes('Linux')) return 'Linux'
|
|
661
|
+
if (ua.includes('Android')) return 'Android'
|
|
662
|
+
if (ua.includes('iOS')) return 'iOS'
|
|
749
663
|
|
|
750
|
-
return 'unknown'
|
|
664
|
+
return 'unknown'
|
|
751
665
|
}
|
|
752
666
|
|
|
753
667
|
private getOSVersion(): string {
|
|
754
|
-
if (typeof navigator === 'undefined') return 'unknown'
|
|
755
|
-
const ua = navigator.userAgent
|
|
668
|
+
if (typeof navigator === 'undefined') return 'unknown'
|
|
669
|
+
const ua = navigator.userAgent
|
|
756
670
|
|
|
757
|
-
let match
|
|
671
|
+
let match
|
|
758
672
|
if ((match = ua.match(/Windows NT (\d+\.\d+)/))) {
|
|
759
|
-
return match[1]
|
|
673
|
+
return match[1]
|
|
760
674
|
}
|
|
761
675
|
if ((match = ua.match(/Mac OS X (\d+[._]\d+)/))) {
|
|
762
|
-
return match[1].replace('_', '.')
|
|
676
|
+
return match[1].replace('_', '.')
|
|
763
677
|
}
|
|
764
678
|
if ((match = ua.match(/Android (\d+\.\d+)/))) {
|
|
765
|
-
return match[1]
|
|
679
|
+
return match[1]
|
|
766
680
|
}
|
|
767
681
|
if ((match = ua.match(/iPhone OS (\d+_\d+)/))) {
|
|
768
|
-
return match[1].replace('_', '.')
|
|
682
|
+
return match[1].replace('_', '.')
|
|
769
683
|
}
|
|
770
684
|
|
|
771
|
-
return 'unknown'
|
|
685
|
+
return 'unknown'
|
|
772
686
|
}
|
|
773
687
|
|
|
774
688
|
private getMobileVendor(): string {
|
|
775
|
-
if (typeof navigator === 'undefined') return ''
|
|
689
|
+
if (typeof navigator === 'undefined') return ''
|
|
776
690
|
|
|
777
|
-
const ua = navigator.userAgent
|
|
778
|
-
if (ua.includes('iPhone') || ua.includes('iPad')) return 'Apple'
|
|
779
|
-
if (ua.includes('Samsung')) return 'Samsung'
|
|
780
|
-
if (ua.includes('Pixel')) return 'Google'
|
|
781
|
-
if (ua.includes('Huawei')) return 'Huawei'
|
|
691
|
+
const ua = navigator.userAgent
|
|
692
|
+
if (ua.includes('iPhone') || ua.includes('iPad')) return 'Apple'
|
|
693
|
+
if (ua.includes('Samsung')) return 'Samsung'
|
|
694
|
+
if (ua.includes('Pixel')) return 'Google'
|
|
695
|
+
if (ua.includes('Huawei')) return 'Huawei'
|
|
782
696
|
|
|
783
|
-
return ''
|
|
697
|
+
return ''
|
|
784
698
|
}
|
|
785
699
|
|
|
786
700
|
private getMobileModel(): string {
|
|
787
|
-
if (typeof navigator === 'undefined') return ''
|
|
701
|
+
if (typeof navigator === 'undefined') return ''
|
|
788
702
|
|
|
789
|
-
const ua = navigator.userAgent
|
|
790
|
-
let match
|
|
703
|
+
const ua = navigator.userAgent
|
|
704
|
+
let match
|
|
791
705
|
|
|
792
706
|
if ((match = ua.match(/iPhone(\d+),(\d+)/))) {
|
|
793
|
-
return `iPhone ${match[1]}
|
|
707
|
+
return `iPhone ${match[1]}`
|
|
794
708
|
}
|
|
795
709
|
if ((match = ua.match(/iPad(\d+),(\d+)/))) {
|
|
796
|
-
return `iPad ${match[1]}
|
|
710
|
+
return `iPad ${match[1]}`
|
|
797
711
|
}
|
|
798
712
|
if ((match = ua.match(/SM-\w+/))) {
|
|
799
|
-
return match[0]
|
|
713
|
+
return match[0]
|
|
800
714
|
}
|
|
801
715
|
if ((match = ua.match(/Pixel \d+/))) {
|
|
802
|
-
return match[0]
|
|
716
|
+
return match[0]
|
|
803
717
|
}
|
|
804
718
|
|
|
805
|
-
return ''
|
|
719
|
+
return ''
|
|
806
720
|
}
|
|
807
721
|
}
|