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 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) {
|
||||||
iceServers.push({ urls: 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) {
|
res.render("index", { trackerUrl, mnemonic });
|
||||||
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 });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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}`);
|
||||||
});
|
});
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue