feat: enhance file transfer and improve user experience

- Standardized quote style across all files to be consistent
- Added functionality to generate TURN credentials dynamically
- Improved UI feedback for upload and download processes
- Introduced a copy URL button for easier sharing
- Implemented progress feedback during file download
- Disabled buttons during ongoing operations to prevent duplicate actions
- Ensured default mnemonic handling for smoother user interaction

These changes enhance the reliability and usability of file transfers.
This commit is contained in:
Kumi 2024-06-15 14:41:39 +02:00
parent 93704705c8
commit 847073333d
Signed by: kumi
GPG key ID: ECBCC9082395383F
4 changed files with 145 additions and 47 deletions

46
app.js
View file

@ -1,39 +1,39 @@
import express from 'express'; import express from "express";
import path from 'path'; import path from "path";
import { fileURLToPath } from 'url'; import { fileURLToPath } from "url";
import bip39 from 'bip39'; import bip39 from "bip39";
import crypto from 'crypto'; import crypto from "crypto";
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
const app = express(); const app = express();
const trackerUrl = process.env.TRACKER_URL || 'ws://localhost:8106'; const trackerUrl = process.env.TRACKER_URL || "ws://localhost:8106";
const stunServerUrl = process.env.STUN_SERVER_URL; const stunServerUrl = process.env.STUN_SERVER_URL;
const turnServerUrl = process.env.TURN_SERVER_URL; const turnServerUrl = process.env.TURN_SERVER_URL;
const turnSecret = process.env.TURN_SECRET; const turnSecret = process.env.TURN_SECRET;
const turnTTL = 86400; const turnTTL = 86400;
app.set('view engine', 'ejs'); app.set("view engine", "ejs");
app.use(express.static(path.join(__dirname, 'public'))); app.use(express.static(path.join(__dirname, "public")));
app.get('/', (req, res) => { app.get("/", (req, res) => {
res.render('index', { trackerUrl }); res.render("index", { trackerUrl });
}); });
app.get('/generate-mnemonic/:infoHash', (req, res) => { app.get("/generate-mnemonic/:infoHash", (req, res) => {
const infoHash = req.params.infoHash; const infoHash = req.params.infoHash;
const mnemonic = bip39.entropyToMnemonic(infoHash); const mnemonic = bip39.entropyToMnemonic(infoHash);
res.json({ mnemonic }); res.json({ mnemonic });
}); });
app.get('/get-infohash/:mnemonic', (req, res) => { app.get("/get-infohash/:mnemonic", (req, res) => {
const mnemonic = req.params.mnemonic; const mnemonic = req.params.mnemonic;
const infoHash = bip39.mnemonicToEntropy(mnemonic); const infoHash = bip39.mnemonicToEntropy(mnemonic);
res.json({ infoHash }); res.json({ infoHash });
}); });
app.get('/turn-credentials', (req, res) => { app.get("/turn-credentials", (req, res) => {
const iceServers = []; const iceServers = [];
if (stunServerUrl) { if (stunServerUrl) {
@ -43,20 +43,34 @@ app.get('/turn-credentials', (req, res) => {
if (turnServerUrl && turnSecret) { if (turnServerUrl && turnSecret) {
const unixTimeStamp = Math.floor(Date.now() / 1000) + turnTTL; const unixTimeStamp = Math.floor(Date.now() / 1000) + turnTTL;
const username = `${unixTimeStamp}:transfercoffee`; const username = `${unixTimeStamp}:transfercoffee`;
const hmac = crypto.createHmac('sha1', turnSecret); const hmac = crypto.createHmac("sha1", turnSecret);
hmac.update(username); hmac.update(username);
const credential = hmac.digest('base64'); const credential = hmac.digest("base64");
iceServers.push({ iceServers.push({
urls: turnServerUrl, urls: turnServerUrl,
username: username, username: username,
credential: credential credential: credential,
}); });
} }
res.json({ iceServers }); res.json({ iceServers });
}); });
app.get("/:mnemonic?", (req, res) => {
var mnemonic = req.params.mnemonic || "";
if (mnemonic) {
mnemonic = mnemonic.replaceAll(".", " ").trim();
if (!bip39.validateMnemonic(mnemonic)) {
return res.status(400).send("Invalid mnemonic");
}
}
res.render("index", { trackerUrl, mnemonic });
});
const PORT = process.env.PORT || 8105; const PORT = process.env.PORT || 8105;
app.listen(PORT, () => { app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`); console.log(`Server is running on port ${PORT}`);

View file

@ -37,6 +37,10 @@ body {
button:hover { button:hover {
background-color: #218838; background-color: #218838;
} }
button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.progress { .progress {
width: 100%; width: 100%;
background-color: #f3f3f3; background-color: #f3f3f3;

View file

@ -10,12 +10,20 @@ async function uploadFile() {
const fileInput = document.getElementById("fileInput"); const fileInput = document.getElementById("fileInput");
const file = fileInput.files[0]; const file = fileInput.files[0];
const uploadStats = document.getElementById("uploadStats"); const uploadStats = document.getElementById("uploadStats");
const uploadSection = document.getElementById("uploadSection");
const downloadSection = document.getElementById("downloadSection");
const copyButton = document.getElementById("copyButton");
const uploadButton = document.getElementById("uploadButton");
if (!file) { if (!file) {
alert("Please select a file to upload."); alert("Please select a file to upload.");
return; return;
} }
downloadSection.style.display = "none";
uploadSection.style.display = "block";
uploadButton.disabled = true;
const rtcConfig = { const rtcConfig = {
iceServers: await getRTCIceServers(), iceServers: await getRTCIceServers(),
}; };
@ -26,14 +34,17 @@ async function uploadFile() {
}; };
client.seed(file, opts, async (torrent) => { client.seed(file, opts, async (torrent) => {
downloadSection.style.display = 'none';
uploadSection.style.display = 'block';
fetch(`/generate-mnemonic/${torrent.infoHash}`) fetch(`/generate-mnemonic/${torrent.infoHash}`)
.then((response) => response.json()) .then((response) => response.json())
.then((data) => { .then((data) => {
const uploadResult = document.getElementById("uploadResult"); const uploadResult = document.getElementById("uploadResult");
uploadResult.innerHTML = `File uploaded. Share this mnemonic: <strong>${data.mnemonic}</strong>`; const downloadUrl = `${
window.location.origin
}/${data.mnemonic.replaceAll(" ", ".")}`;
uploadResult.innerHTML = `Seeding file. 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.`;
copyButton.style.display = "inline-block";
copyButton.setAttribute("data-url", downloadUrl);
}); });
let totalPeers = 0; let totalPeers = 0;
@ -60,6 +71,19 @@ async function uploadFile() {
}); });
} }
function copyToClipboard() {
const copyButton = document.getElementById("copyButton");
const url = copyButton.getAttribute("data-url");
navigator.clipboard
.writeText(url)
.then(() => {
alert("URL copied to clipboard");
})
.catch((err) => {
console.error("Failed to copy: ", err);
});
}
async function sha256(str) { async function sha256(str) {
const buffer = new TextEncoder().encode(str); const buffer = new TextEncoder().encode(str);
const hash = await crypto.subtle.digest("SHA-256", buffer); const hash = await crypto.subtle.digest("SHA-256", buffer);
@ -71,12 +95,19 @@ async function sha256(str) {
async function downloadFile() { async function downloadFile() {
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");
if (!mnemonicInput) { if (!mnemonicInput) {
alert("Please enter a mnemonic."); alert("Please enter a mnemonic.");
return; return;
} }
downloadSection.style.display = "block";
uploadSection.style.display = "none";
downloadButton.disabled = true;
downloadResult.innerHTML = "Preparing incoming file transfer, please wait...";
const rtcConfig = { const rtcConfig = {
iceServers: await getRTCIceServers(), iceServers: await getRTCIceServers(),
}; };
@ -92,9 +123,6 @@ async function downloadFile() {
}; };
client.add(torrentId, opts, (torrent) => { client.add(torrentId, opts, (torrent) => {
downloadSection.style.display = 'block';
uploadSection.style.display = 'none';
torrent.files[0].getBlob((err, blob) => { torrent.files[0].getBlob((err, blob) => {
if (err) { if (err) {
const downloadResult = document.getElementById("downloadResult"); const downloadResult = document.getElementById("downloadResult");
@ -120,5 +148,51 @@ async function downloadFile() {
downloadProgressBar.textContent = `${progress}%`; downloadProgressBar.textContent = `${progress}%`;
}); });
}); });
setInterval(() => {
const downloadResult = document.getElementById("downloadResult");
if (client.get(torrentId)) {
const torrent = client.get(torrentId);
const progress = Math.round(
(torrent.downloaded / torrent.length) * 100
);
downloadResult.innerHTML = `Downloading:
<strong>${torrent.files[0].name}</strong>
<br>
<br><strong>Status:</strong> ${
torrent.done ? "Completed, seeding" : "Downloading"
}
<br><strong>Peers:</strong> ${torrent.numPeers}
<br><strong>Downloaded:</strong> ${(
torrent.downloaded /
(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} seconds
<br><strong>Uploaded:</strong> ${(
torrent.uploaded /
(1024 * 1024)
).toFixed(2)} MB
<br><strong>Ratio:</strong> ${torrent.ratio.toFixed(2)}`;
return;
}
downloadResult.innerHTML = "File not found. Please check the mnemonic.";
}, 1000);
}); });
} }
document.addEventListener("DOMContentLoaded", () => {
if (mnemonic) {
const mnemonicInput = document.getElementById("mnemonicInput");
const downloadButton = document.getElementById("downloadButton");
mnemonicInput.value = mnemonic;
downloadButton.click();
}
});

View file

@ -14,19 +14,25 @@
<button onclick="uploadFile()">Share</button> <button onclick="uploadFile()">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 id="copyButton" style="display: none;" onclick="copyToClipboard()">Copy URL</button>
</div> </div>
<div class="section" id="downloadSection"> <div class="section" id="downloadSection">
<h2>Receive File</h2> <h2>Receive File</h2>
<input type="text" id="mnemonicInput" placeholder="Enter mnemonic" /> <input type="text" id="mnemonicInput" placeholder="Enter mnemonic" />
<button onclick="downloadFile()">Receive</button> <button id="downloadButton" onclick="downloadFile()">Receive</button>
<div class="progress" id="downloadProgress"> <div class="progress" id="downloadProgress">
<div class="progress-bar" id="downloadProgressBar">0%</div> <div class="progress-bar" id="downloadProgressBar">0%</div>
</div> </div>
<div class="result" id="downloadResult"></div> <div class="result" id="downloadResult"></div>
<div class="result" id="downloadStats"></div>
</div> </div>
</div> </div>
<script> <script>
const trackerUrl = "<%= trackerUrl %>"; const trackerUrl = "<%= trackerUrl %>";
<% if (typeof mnemonic !== 'undefined') { %>
const mnemonic = "<%= mnemonic %>";
<% } %>
</script> </script>
<script src="/js/index.js"></script> <script src="/js/index.js"></script>
</body> </body>