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

92
app.js
View file

@ -1,63 +1,77 @@
import express from 'express';
import path from 'path';
import { fileURLToPath } from 'url';
import bip39 from 'bip39';
import crypto from 'crypto';
import express from "express";
import path from "path";
import { fileURLToPath } from "url";
import bip39 from "bip39";
import crypto from "crypto";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
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 turnServerUrl = process.env.TURN_SERVER_URL;
const turnSecret = process.env.TURN_SECRET;
const turnTTL = 86400;
app.set('view engine', 'ejs');
app.use(express.static(path.join(__dirname, 'public')));
app.set("view engine", "ejs");
app.use(express.static(path.join(__dirname, "public")));
app.get('/', (req, res) => {
res.render('index', { trackerUrl });
app.get("/", (req, res) => {
res.render("index", { trackerUrl });
});
app.get('/generate-mnemonic/:infoHash', (req, res) => {
const infoHash = req.params.infoHash;
const mnemonic = bip39.entropyToMnemonic(infoHash);
res.json({ mnemonic });
app.get("/generate-mnemonic/:infoHash", (req, res) => {
const infoHash = req.params.infoHash;
const mnemonic = bip39.entropyToMnemonic(infoHash);
res.json({ mnemonic });
});
app.get('/get-infohash/:mnemonic', (req, res) => {
const mnemonic = req.params.mnemonic;
const infoHash = bip39.mnemonicToEntropy(mnemonic);
res.json({ infoHash });
app.get("/get-infohash/:mnemonic", (req, res) => {
const mnemonic = req.params.mnemonic;
const infoHash = bip39.mnemonicToEntropy(mnemonic);
res.json({ infoHash });
});
app.get('/turn-credentials', (req, res) => {
const iceServers = [];
app.get("/turn-credentials", (req, res) => {
const iceServers = [];
if (stunServerUrl) {
iceServers.push({ urls: stunServerUrl });
if (stunServerUrl) {
iceServers.push({ urls: stunServerUrl });
}
if (turnServerUrl && turnSecret) {
const unixTimeStamp = Math.floor(Date.now() / 1000) + turnTTL;
const username = `${unixTimeStamp}:transfercoffee`;
const hmac = crypto.createHmac("sha1", turnSecret);
hmac.update(username);
const credential = hmac.digest("base64");
iceServers.push({
urls: turnServerUrl,
username: username,
credential: credential,
});
}
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");
}
}
if (turnServerUrl && turnSecret) {
const unixTimeStamp = Math.floor(Date.now() / 1000) + turnTTL;
const username = `${unixTimeStamp}:transfercoffee`;
const hmac = crypto.createHmac('sha1', turnSecret);
hmac.update(username);
const credential = hmac.digest('base64');
iceServers.push({
urls: turnServerUrl,
username: username,
credential: credential
});
}
res.json({ iceServers });
res.render("index", { trackerUrl, mnemonic });
});
const PORT = process.env.PORT || 8105;
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 {
background-color: #218838;
}
button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.progress {
width: 100%;
background-color: #f3f3f3;

View file

@ -10,12 +10,20 @@ async function uploadFile() {
const fileInput = document.getElementById("fileInput");
const file = fileInput.files[0];
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) {
alert("Please select a file to upload.");
return;
}
downloadSection.style.display = "none";
uploadSection.style.display = "block";
uploadButton.disabled = true;
const rtcConfig = {
iceServers: await getRTCIceServers(),
};
@ -26,14 +34,17 @@ async function uploadFile() {
};
client.seed(file, opts, async (torrent) => {
downloadSection.style.display = 'none';
uploadSection.style.display = 'block';
fetch(`/generate-mnemonic/${torrent.infoHash}`)
.then((response) => response.json())
.then((data) => {
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;
@ -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) {
const buffer = new TextEncoder().encode(str);
const hash = await crypto.subtle.digest("SHA-256", buffer);
@ -71,12 +95,19 @@ async function sha256(str) {
async function downloadFile() {
const mnemonicInput = document.getElementById("mnemonicInput").value;
const downloadProgressBar = document.getElementById("downloadProgressBar");
const downloadButton = document.getElementById("downloadButton");
if (!mnemonicInput) {
alert("Please enter a mnemonic.");
return;
}
downloadSection.style.display = "block";
uploadSection.style.display = "none";
downloadButton.disabled = true;
downloadResult.innerHTML = "Preparing incoming file transfer, please wait...";
const rtcConfig = {
iceServers: await getRTCIceServers(),
};
@ -92,9 +123,6 @@ async function downloadFile() {
};
client.add(torrentId, opts, (torrent) => {
downloadSection.style.display = 'block';
uploadSection.style.display = 'none';
torrent.files[0].getBlob((err, blob) => {
if (err) {
const downloadResult = document.getElementById("downloadResult");
@ -120,5 +148,51 @@ async function downloadFile() {
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>
<div class="result" id="uploadResult"></div>
<div class="result" id="uploadStats"></div>
<button id="copyButton" style="display: none;" onclick="copyToClipboard()">Copy URL</button>
</div>
<div class="section" id="downloadSection">
<h2>Receive File</h2>
<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-bar" id="downloadProgressBar">0%</div>
</div>
<div class="result" id="downloadResult"></div>
<div class="result" id="downloadStats"></div>
</div>
</div>
<script>
const trackerUrl = "<%= trackerUrl %>";
<% if (typeof mnemonic !== 'undefined') { %>
const mnemonic = "<%= mnemonic %>";
<% } %>
</script>
<script src="/js/index.js"></script>
</body>