feat: Add support for multiple file transfers

Enhances the web application to allow users to upload and
download multiple files simultaneously using WebTorrent.
Updates UI and messages to reflect support for multiple
files. Adjusts progress indicators for batch file transfers
and updates mnemonic generation logic for compatibility.

Bumps application version to 0.1.0 for feature release.
This commit is contained in:
Kumi 2024-12-18 17:13:43 +01:00
parent ee1d360fb5
commit 4892240222
Signed by: kumi
GPG key ID: ECBCC9082395383F
4 changed files with 54 additions and 71 deletions

View file

@ -11,6 +11,7 @@ Transfer.coffee is a simple Node.js web application that allows users to share f
## Features ## Features
- Peer-to-peer file sharing using WebTorrent - Peer-to-peer file sharing using WebTorrent
- Transfer multiple files at once
- Mnemonic seed generation for easy file sharing - Mnemonic seed generation for easy file sharing
- Optional STUN and TURN server configuration for NAT traversal - Optional STUN and TURN server configuration for NAT traversal
- Progress indicators for file upload and download - Progress indicators for file upload and download

View file

@ -1,6 +1,6 @@
{ {
"name": "transfercoffee", "name": "transfercoffee",
"version": "0.0.2", "version": "0.1.0",
"description": "A WebTorrent-based file transfer application", "description": "A WebTorrent-based file transfer application",
"main": "app.js", "main": "app.js",
"type": "module", "type": "module",

View file

@ -11,17 +11,17 @@ async function getRTCIceServers() {
} }
} }
async function uploadFile(trackerUrl) { async function uploadFiles(trackerUrl) {
const fileInput = document.getElementById("fileInput"); const fileInput = document.getElementById("fileInput");
const file = fileInput.files[0]; const files = Array.from(fileInput.files);
const uploadStats = document.getElementById("uploadStats"); const uploadStats = document.getElementById("uploadStats");
const uploadSection = document.getElementById("uploadSection"); const uploadSection = document.getElementById("uploadSection");
const downloadSection = document.getElementById("downloadSection"); const downloadSection = document.getElementById("downloadSection");
const copyButton = document.getElementById("copyButton"); const copyButton = document.getElementById("copyButton");
const uploadButton = document.getElementById("uploadButton"); const uploadButton = document.getElementById("uploadButton");
if (!file) { if (!files.length) {
alert("Please select a file to upload."); alert("Please select at least one file to upload.");
return; return;
} }
@ -29,6 +29,8 @@ async function uploadFile(trackerUrl) {
uploadSection.style.display = "block"; uploadSection.style.display = "block";
uploadButton.disabled = true; uploadButton.disabled = true;
uploadStats.innerHTML = '';
try { try {
const rtcConfig = { const rtcConfig = {
iceServers: await getRTCIceServers(), iceServers: await getRTCIceServers(),
@ -39,17 +41,16 @@ async function uploadFile(trackerUrl) {
rtcConfig: rtcConfig, rtcConfig: rtcConfig,
}; };
client.seed(file, opts, async (torrent) => { client.seed(files, opts, async (torrent) => {
try { try {
const response = await fetch(`/generate-mnemonic/${torrent.infoHash}`); const response = await fetch(`/generate-mnemonic/${torrent.infoHash}`);
if (!response.ok) throw new Error("Failed to generate mnemonic"); if (!response.ok) throw new Error("Failed to generate mnemonic");
const data = await response.json(); const data = await response.json();
const uploadResult = document.getElementById("uploadResult"); const uploadResult = document.getElementById("uploadResult");
const downloadUrl = `${window.location.origin const downloadUrl = `${window.location.origin}/${data.mnemonic.replaceAll(" ", ".")}`;
}/${data.mnemonic.replaceAll(" ", ".")}`;
history.pushState({}, "", `/${data.mnemonic.replaceAll(" ", ".")}`); history.pushState({}, "", `/${data.mnemonic.replaceAll(" ", ".")}`);
uploadResult.innerHTML = `Seeding file. Share this mnemonic: <strong>${data.mnemonic}</strong> uploadResult.innerHTML = `Seeding files. Share this mnemonic: <strong>${data.mnemonic}</strong>
<br>Note that the file will be available for download only as long as you keep this page open.`; <br>Note that the files will be available for download only as long as you keep this page open.`;
copyButton.style.display = "inline-block"; copyButton.style.display = "inline-block";
copyButton.setAttribute("data-url", downloadUrl); copyButton.setAttribute("data-url", downloadUrl);
} catch (error) { } catch (error) {
@ -80,7 +81,7 @@ async function uploadFile(trackerUrl) {
}, 1000); }, 1000);
}); });
} catch (error) { } catch (error) {
console.error("Error sharing file:", error); console.error("Error sharing files:", error);
} }
} }
@ -101,7 +102,7 @@ async function sha256(str) {
.join(""); .join("");
} }
async function downloadFile(trackerUrl) { async function downloadFiles(trackerUrl) {
const mnemonicInput = document.getElementById("mnemonicInput").value; const mnemonicInput = document.getElementById("mnemonicInput").value;
const downloadProgressBar = document.getElementById("downloadProgressBar"); const downloadProgressBar = document.getElementById("downloadProgressBar");
const downloadButton = document.getElementById("downloadButton"); const downloadButton = document.getElementById("downloadButton");
@ -134,7 +135,8 @@ async function downloadFile(trackerUrl) {
}; };
client.add(torrentId, opts, (torrent) => { client.add(torrentId, opts, (torrent) => {
torrent.files[0].getBlob((err, blob) => { torrent.files.forEach((file) => {
file.getBlob((err, blob) => {
if (err) { if (err) {
downloadResult.innerHTML = `Error: ${err.message}`; downloadResult.innerHTML = `Error: ${err.message}`;
return; return;
@ -143,59 +145,38 @@ async function downloadFile(trackerUrl) {
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement("a"); const a = document.createElement("a");
a.href = url; a.href = url;
a.download = torrent.files[0].name; a.download = file.name;
a.click(); a.click();
});
downloadResult.innerHTML = `File downloaded: <strong>${torrent.files[0].name}</strong>`;
}); });
torrent.on("download", () => { torrent.on("download", () => {
const progress = Math.round( const progress = Math.round((torrent.downloaded / torrent.length) * 100);
(torrent.downloaded / torrent.length) * 100
);
downloadProgressBar.style.width = `${progress}%`; downloadProgressBar.style.width = `${progress}%`;
downloadProgressBar.textContent = `${progress}%`; downloadProgressBar.textContent = `${progress}%`;
}); });
});
setInterval(() => { setInterval(() => {
if (client.get(torrentId)) { const progressText = torrent.files.map(file => `${file.name}`).join(', ');
const torrent = client.get(torrentId); const progress = Math.round((torrent.downloaded / torrent.length) * 100);
const progress = Math.round(
(torrent.downloaded / torrent.length) * 100
);
downloadResult.innerHTML = `Downloading: downloadResult.innerHTML = `Downloading:
<strong>${torrent.files[0].name}</strong> <strong>${progressText}</strong>
<br> <br>
<br><strong>Status:</strong> ${torrent.done ? "Completed, seeding" : "Downloading" <br><strong>Status:</strong> ${torrent.done ? "Completed, seeding" : "Downloading"}
}
<br><strong>Peers:</strong> ${torrent.numPeers} <br><strong>Peers:</strong> ${torrent.numPeers}
<br> <br>
<br><strong>Downloaded:</strong> ${( <br><strong>Downloaded:</strong> ${(torrent.downloaded / (1024 * 1024)).toFixed(2)} MB / ${(torrent.length / (1024 * 1024)).toFixed(2)} MB (${progress}%)
torrent.downloaded / <br><strong>Speed:</strong> ${(torrent.downloadSpeed / (1024 * 1024)).toFixed(2)} MB/s
(1024 * 1024)
).toFixed(2)} MB / ${(torrent.length / (1024 * 1024)).toFixed(
2
)} MB (${progress}%)
<br><strong>Speed:</strong> ${(
torrent.downloadSpeed /
(1024 * 1024)
).toFixed(2)} MB/s
<br><strong>ETA:</strong> ${(torrent.timeRemaining / 1000).toFixed(0)} seconds <br><strong>ETA:</strong> ${(torrent.timeRemaining / 1000).toFixed(0)} seconds
<br> <br>
<br><strong>Uploaded:</strong> ${( <br><strong>Uploaded:</strong> ${(torrent.uploaded / (1024 * 1024)).toFixed(2)} MB
torrent.uploaded /
(1024 * 1024)
).toFixed(2)} MB
<br><strong>Ratio:</strong> ${torrent.ratio.toFixed(2)}`; <br><strong>Ratio:</strong> ${torrent.ratio.toFixed(2)}`;
return;
}
downloadResult.innerHTML = "File not found. Please check the mnemonic.";
}, 1000); }, 1000);
});
} catch (error) { } catch (error) {
console.error("Error downloading file:", error); console.error("Error downloading files:", error);
alert("Failed to download file. Please check the mnemonic and try again."); alert("Failed to download files. Please check the mnemonic and try again.");
} finally { } finally {
downloadButton.disabled = false; downloadButton.disabled = false;
} }
@ -208,13 +189,13 @@ document.addEventListener("DOMContentLoaded", () => {
document document
.getElementById("uploadButton") .getElementById("uploadButton")
.addEventListener("click", () => uploadFile(trackerUrl)); .addEventListener("click", () => uploadFiles(trackerUrl));
document document
.getElementById("copyButton") .getElementById("copyButton")
.addEventListener("click", copyToClipboard); .addEventListener("click", copyToClipboard);
document document
.getElementById("downloadButton") .getElementById("downloadButton")
.addEventListener("click", () => downloadFile(trackerUrl)); .addEventListener("click", () => downloadFiles(trackerUrl));
if (mnemonic) { if (mnemonic) {
const mnemonicInput = document.getElementById("mnemonicInput"); const mnemonicInput = document.getElementById("mnemonicInput");

View file

@ -17,27 +17,23 @@
<div class="section" id="intro"> <div class="section" id="intro">
<p> <p>
Transfer.coffee is a simple way to share files between devices. Just Transfer.coffee is a simple way to share files between devices. Just
select a file and share the generated URL with the recipient. The select one or multiple files and share the generated URL with the
recipient can then download the file by entering the mnemonic. recipient. The recipient can then download the file by entering the
mnemonic.
</p> </p>
<p> <p>
The files are shared using WebTorrent, a peer-to-peer file sharing The files are shared using WebTorrent, a peer-to-peer file sharing
protocol. This means that the file is not stored on a central server protocol. This means that the files are not stored on a central server
and is instead shared directly between the sender and the recipient. and is instead shared directly between the sender and the recipient.
</p> </p>
</div> </div>
<div class="section" id="uploadSection"> <div class="section" id="uploadSection">
<h2>Share File</h2> <h2>Share File</h2>
<input type="file" id="fileInput" /> <input type="file" id="fileInput" multiple />
<button id="uploadButton">Share</button> <button id="uploadButton">Share</button>
<div class="result" id="uploadResult"></div> <div class="result" id="uploadResult"></div>
<div class="result" id="uploadStats"></div> <div class="result" id="uploadStats"></div>
<button <button id="copyButton" style="display: none">Copy URL</button>
id="copyButton"
style="display: none"
>
Copy URL
</button>
</div> </div>
<div class="section" id="downloadSection"> <div class="section" id="downloadSection">
<h2>Receive File</h2> <h2>Receive File</h2>
@ -70,7 +66,12 @@
</p> </p>
</div> </div>
</div> </div>
<div id="config" data-tracker-url="<%= trackerUrl %>" data-mnemonic="<%= mnemonic %>" style="display: none;"></div> <div
id="config"
data-tracker-url="<%= trackerUrl %>"
data-mnemonic="<%= mnemonic %>"
style="display: none"
></div>
<script src="/js/index.js"></script> <script src="/js/index.js"></script>
</body> </body>
</html> </html>