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