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:
parent
ee1d360fb5
commit
4892240222
4 changed files with 54 additions and 71 deletions
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue