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:
parent
93704705c8
commit
847073333d
4 changed files with 145 additions and 47 deletions
90
app.js
90
app.js
|
@ -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}`);
|
||||
});
|
|
@ -37,6 +37,10 @@ body {
|
|||
button:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
button:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.progress {
|
||||
width: 100%;
|
||||
background-color: #f3f3f3;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue