filepizza-client 0.1.0 → 2.0.0-alpha.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Fares Abawi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -18,3 +18,67 @@ The `filepizza-client` API provides bindings to [FilePizza server](https://githu
18
18
  ```bash
19
19
  npm install filepizza-client
20
20
  ```
21
+
22
+ ## Usage
23
+
24
+ Set up a FilePizza client instance and use it to upload files:
25
+
26
+ ```javascript
27
+ import { FilePizzaUploader } from 'filepizza-client';
28
+
29
+ const uploader = new FilePizzaUploader({
30
+ filePizzaServerUrl: 'https://your-filepizza-server.com',
31
+ // You can optionally specify an additional shared slug where multiple uploaders can connect and share files
32
+ sharedSlug: 'filepizza-demo'
33
+ });
34
+
35
+ await uploader.initialize();
36
+
37
+ uploader.on('progress', (progressInfo) => {
38
+ console.log(`Upload progress: ${progressInfo.overallProgress * 100}%`);
39
+ });
40
+
41
+ uploader.setFiles(fileList); // From an input element
42
+ const links = uploader.getShareableLinks();
43
+ console.log(`Shareable links: ${links.join(', ')}`);
44
+ ```
45
+
46
+ Set up a FilePizza client instance and use it to download files:
47
+
48
+ ```javascript
49
+ import { FilePizzaDownloader } from 'filepizza-client';
50
+
51
+ const downloader = new FilePizzaDownloader({
52
+ filePizzaServerUrl: 'https://your-filepizza-server.com'
53
+ });
54
+
55
+ await downloader.initialize();
56
+
57
+ downloader.on('progress', (progressInfo) => {
58
+ console.log(`Download progress: ${progressInfo.overallProgress * 100}%`);
59
+ });
60
+
61
+ await downloader.connect(filePizzaUrl);
62
+ await downloader.startDownload();
63
+ ```
64
+
65
+ ## Examples
66
+
67
+ ### Regular Example
68
+
69
+ Make sure to have the [FilePizza server](https://github.com/TeXlyre/filepizza-server) running locally or specify the server URL in the example code.
70
+
71
+ By default, the example uses `https://filepizza.emaily.re` as the demo server URL. This server allows origins from `http://localhost:8081` so you can immediately test the API without requiring to run the server locally.
72
+
73
+ *WARNING: The demo server (signaling and TURN as fallback) is not intended for production use and may be subject to rate limits or downtime. For production use, consider setting up your own [FilePizza server](https://github.com/TeXlyre/filepizza-server?tab=readme-ov-file#deployment-with-cloudflare-tunnel).*
74
+
75
+ To run the vite-bundled example locally, clone the repository and run:
76
+
77
+ ```bash
78
+ npm install
79
+ npm run build:example
80
+ npm run example
81
+ ```
82
+
83
+ Then open `http://localhost:8081` in your browser.
84
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "filepizza-client",
3
- "version": "0.1.0",
3
+ "version": "2.0.0-alpha.0",
4
4
  "description": "Client API for FilePizza, a free peer-to-peer file transfer service.",
5
5
  "author": "Fares Abawi <fares@abawi.me> (https://abawi.me)",
6
6
  "license": "MIT",
@@ -11,9 +11,6 @@
11
11
  "homepage": "https://texlyre.github.io/filepizza-client/",
12
12
  "main": "dist/index.js",
13
13
  "types": "dist/index.d.ts",
14
- "files": [
15
- "dist"
16
- ],
17
14
  "scripts": {
18
15
  "build": "tsc",
19
16
  "test": "jest",
@@ -28,7 +25,7 @@
28
25
  "web-streams-polyfill": "^3.2.1"
29
26
  },
30
27
  "peerDependencies": {
31
- "next": "^13.0.0",
28
+ "next": "^14.2.28",
32
29
  "react": "^18.0.0",
33
30
  "react-dom": "^18.0.0",
34
31
  "peerjs": "^1.5.1"
@@ -49,5 +46,20 @@
49
46
  "@vitejs/plugin-react": "^4.0.0",
50
47
  "react": "^18.2.0",
51
48
  "react-dom": "^18.2.0"
52
- }
49
+ },
50
+ "files": [
51
+ "dist/",
52
+ "src/",
53
+ "README.md",
54
+ "LICENSE"
55
+ ],
56
+ "keywords": [
57
+ "filepizza",
58
+ "filepizza-server",
59
+ "filepizza-2.0",
60
+ "webrtc",
61
+ "p2p",
62
+ "peer-to-peer",
63
+ "file-transfer"
64
+ ]
53
65
  }
@@ -0,0 +1,84 @@
1
+ // src/utils/download-helper.ts
2
+ export class DownloadHelper {
3
+ private static isNewChromiumBased() {
4
+ return 'showSaveFilePicker' in window;
5
+ }
6
+
7
+ static async downloadFile(fileName: string, data: Blob | Uint8Array): Promise<void> {
8
+ const blob = data instanceof Blob ? data : new Blob([data]);
9
+
10
+ if (this.isNewChromiumBased()) {
11
+ await this.downloadWithFileSystemAccessAPI(fileName, blob);
12
+ } else {
13
+ this.downloadWithBlobUrl(fileName, blob);
14
+ }
15
+ }
16
+
17
+ private static async downloadWithFileSystemAccessAPI(fileName: string, blob: Blob): Promise<void> {
18
+ try {
19
+ // @ts-ignore - TypeScript may not recognize showSaveFilePicker
20
+ const fileHandle = await window.showSaveFilePicker({
21
+ suggestedName: fileName,
22
+ types: [{
23
+ description: 'Files',
24
+ accept: { '*/*': ['.bin'] },
25
+ }],
26
+ });
27
+
28
+ const writable = await fileHandle.createWritable();
29
+ await writable.write(blob);
30
+ await writable.close();
31
+ } catch (error) {
32
+ console.error('Error downloading with File System Access API:', error);
33
+
34
+ // Fall back to blob URL method if the user cancels or there's an error
35
+ if (error.name !== 'AbortError') {
36
+ this.downloadWithBlobUrl(fileName, blob);
37
+ }
38
+ }
39
+ }
40
+
41
+ private static downloadWithBlobUrl(fileName: string, blob: Blob): void {
42
+ const url = URL.createObjectURL(blob);
43
+
44
+ const a = document.createElement('a');
45
+ a.href = url;
46
+ a.download = fileName;
47
+ a.style.display = 'none';
48
+
49
+ document.body.appendChild(a);
50
+ a.click();
51
+
52
+ setTimeout(() => {
53
+ document.body.removeChild(a);
54
+ URL.revokeObjectURL(url);
55
+ }, 100);
56
+ }
57
+
58
+ static async streamToUint8Array(stream: ReadableStream<Uint8Array>): Promise<Uint8Array> {
59
+ const reader = stream.getReader();
60
+ const chunks: Uint8Array[] = [];
61
+
62
+ try {
63
+ while (true) {
64
+ const { done, value } = await reader.read();
65
+ if (done) break;
66
+ chunks.push(value);
67
+ }
68
+ } finally {
69
+ reader.releaseLock();
70
+ }
71
+
72
+ // Combine chunks
73
+ const totalLength = chunks.reduce((total, chunk) => total + chunk.length, 0);
74
+ const combinedChunks = new Uint8Array(totalLength);
75
+
76
+ let offset = 0;
77
+ for (const chunk of chunks) {
78
+ combinedChunks.set(chunk, offset);
79
+ offset += chunk.length;
80
+ }
81
+
82
+ return combinedChunks;
83
+ }
84
+ }
@@ -0,0 +1,71 @@
1
+ // src/utils/event-emitter.ts
2
+ import { EventEmitter as EventEmitterInterface } from './types'
3
+
4
+ export class EventEmitter implements EventEmitterInterface {
5
+ private events: Record<string, Array<(...args: any[]) => void>> = {}
6
+
7
+ /**
8
+ * Register an event listener
9
+ */
10
+ on(event: string, listener: (...args: any[]) => void): this {
11
+ if (!this.events[event]) {
12
+ this.events[event] = []
13
+ }
14
+ this.events[event].push(listener)
15
+ return this
16
+ }
17
+
18
+ /**
19
+ * Remove an event listener
20
+ */
21
+ off(event: string, listener: (...args: any[]) => void): this {
22
+ if (this.events[event]) {
23
+ this.events[event] = this.events[event].filter(l => l !== listener)
24
+ }
25
+ return this
26
+ }
27
+
28
+ /**
29
+ * Emit an event
30
+ */
31
+ emit(event: string, ...args: any[]): boolean {
32
+ if (!this.events[event]) {
33
+ return false
34
+ }
35
+
36
+ this.events[event].forEach(listener => {
37
+ try {
38
+ listener(...args)
39
+ } catch (error) {
40
+ console.error(`Error in event listener for ${event}:`, error)
41
+ }
42
+ })
43
+
44
+ return true
45
+ }
46
+
47
+ /**
48
+ * Register a one-time event listener
49
+ */
50
+ once(event: string, listener: (...args: any[]) => void): this {
51
+ const onceListener = (...args: any[]) => {
52
+ this.off(event, onceListener)
53
+ listener(...args)
54
+ }
55
+
56
+ return this.on(event, onceListener)
57
+ }
58
+
59
+ /**
60
+ * Remove all listeners for an event
61
+ */
62
+ removeAllListeners(event?: string): this {
63
+ if (event) {
64
+ delete this.events[event]
65
+ } else {
66
+ this.events = {}
67
+ }
68
+
69
+ return this
70
+ }
71
+ }