share-files-tool
Version:
A simple file sharing application.
676 lines (591 loc) • 24.2 kB
JavaScript
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const os = require('os');
// const QRCode = require('qrcode');
const qr = require('qr-image');
const { exec } = require('child_process');
const net = require('net');
const app = express();
const cors = require('cors');
const child_process = require("child_process");
const http = require('http');
const socketIo = require('socket.io');
const server = http.createServer(app);
const io = socketIo(server);
app.use(express.json());
// 启用 CORS,对所有请求允许
app.use(cors());
let PORT = 3000;
let _url;
const isPortAvailable = (port) => {
return new Promise((resolve) => {
const server = net.createServer();
server.unref();
server.on('error', () => resolve(false));
server.listen(port, () => {
server.close(() => resolve(true));
});
});
};
newwindow = false;
// 1024 - 65535
const getRandomPort = async () => {
// console.log("process.argv:",process.argv);
if (process.argv.includes('once')) {
newwindow = true;
return 3000;
}
let port;
let isAvailable = false;
while (!isAvailable) {
port = Math.floor(Math.random() * (65535 - 1024 + 1)) + 1024;
isAvailable = await isPortAvailable(port);
}
return port;
};
const uploadsDir = path.join(__dirname, 'uploads');
const clipboardPath = path.join(__dirname, 'clipboard.txt');
try {
if (!fs.existsSync(uploadsDir)) {
fs.mkdirSync(uploadsDir);
}
} catch (error) {
console.log(error);
redlog(`If catch a permission error, try to run with sudo: sudo chmod -R 777 ${__dirname}`);
}
// 监听 uploads 目录
fs.watch(uploadsDir, (eventType, filename) => {
// if (filename) {
// console.log(`Event type: ${eventType}, filename: ${filename}`);
// // 当 uploads 目录内有文件变化时,推送更新到所有连接的客户端
// io.emit('uploadsUpdated', { eventType, filename });
// }
io.emit('uploadsUpdated', true);
});
function readClipboardContent() {
return fs.readFileSync(clipboardPath, 'utf8');
}
fs.watchFile(clipboardPath, (curr, prev) => {
const content = readClipboardContent();
io.emit('clipboardUpdated', content);
});
io.on('connection', (socket) => {
socket.emit('clipboardUpdated', readClipboardContent());
});
setTimeout(() => {
io.emit('refreshPage', true);
}, 1500);
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, uploadsDir);
},
filename: (req, file, cb) => {
const originalName = file.originalname;
const ext = path.extname(originalName);
const nameWithoutExt = path.basename(originalName, ext);
let newFileName = originalName;
if (fs.existsSync(path.join(uploadsDir, newFileName))) {
const creationDate = new Date();
const formattedDate = `${creationDate.getMonth() + 1}${creationDate.getDate()}${creationDate.getFullYear()}${creationDate.getHours().toString().padStart(2, '0')}${creationDate.getMinutes().toString().padStart(2, '0')}${creationDate.getSeconds().toString().padStart(2, '0')}`;
newFileName = `${nameWithoutExt}_${formattedDate}${ext}`;
}
cb(null, newFileName);
}
});
const upload = multer({ storage: storage });
app.use(express.static('public'));
// app.use('/uploads', express.static('src/uploads'));
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
function getSuccessHtml() {
const responseHtml = `
<html>
<head>
<meta charset="UTF-8">
<title>successfully</title>
<style>
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background-color: #f4f4f4; text-align: center;}
h1 { color: #333; font-size: 2.5rem; }
button {
margin-top: 3rem;
padding: 1.4rem;
width: 23rem;
background-color: #5cb85c; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 2.5rem; }
button:hover { background-color: #4cae4c; }
p { text-align: center; font-size: 2.5rem; margin: 4rem 0;}
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
</style>
<script>
window.addEventListener('load', () => {
setTimeout(() => {
window.location.href='/list-downloads'
}, 0);
});
</script>
</head>
<body>
<h1>The file uploads successfully!</h1>
<div class="container">
<button style="margin-top: 20px;" onclick="window.location.href='/list-downloads'">View lists</button>
<button style="margin-top: 20px;" onclick="window.location.href='/'">Back to Home</button>
</div>
</body>
</html>
`;
return responseHtml;
}
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, '../public', 'index.html'));
});
app.post('/upload', upload.array('files'), (req, res) => {
res.send(getSuccessHtml());
});
app.get('/upload-success', (req, res) => {
res.send(getSuccessHtml());
});
app.get('/download/:filename', (req, res) => {
const filePath = path.join(uploadsDir, req.params.filename);
fs.access(filePath, fs.constants.R_OK, (err) => {
if (err) {
console.error('File access error:', err);
return res.status(404).send('File not found or cannot be accessed.');
}
res.download(filePath, (err) => {
if (err) {
console.error('Error downloading file:', err);
res.status(500).send('Could not download file.');
}
});
});
});
app.get('/delete/:filename', (req, res) => {
const filePath = path.join(uploadsDir, req.params.filename);
fs.unlink(filePath, (err) => {
if (err) {
return res.status(500).send(`Can't delete the file. ${err}`);
}
res.redirect('/list-downloads'); // 删除后重定向回文件列表
});
});
// 抽取预览页面的 HTML 模板
function getPreviewHtml(fileType, fileUrl, filename) {
const isImage = fileType === 'image';
const isVideo = fileType === 'video';
const isPdf = fileType === 'pdf';
const isTxt = fileType === 'txt';
let content;
if (isImage) {
content = `<img id="media" src="${fileUrl}" alt="${filename}">`;
} else if (isVideo) {
content = `
<video id="media" controls>
<source src="${fileUrl}" type="video/mp4">
Your browser does not support the video tag.
</video>
`;
} else if (isPdf) {
content = `
<div id="media">
<div id="pdf-container" style="width: 100%; height: 80vh; overflow: auto; text-align: center;">
<canvas id="pdf-canvas"></canvas>
</div>
</div>
<div style="margin-top: 10px;">
<button id="prev-page" onclick="prevPage()">Previous</button>
<span id="page-num">Page: 1</span> / <span id="page-count">Loading...</span>
<button id="next-page" onclick="nextPage()">Next</button>
</div>
<script>
let pdfDoc = null;
let pageNum = 1;
let pageRendering = false;
let pageNumPending = null;
const scale = 1.5;
const url = '${fileUrl}';
const canvas = document.getElementById('pdf-canvas');
const ctx = canvas.getContext('2d');
function renderPage(num) {
pageRendering = true;
pdfDoc.getPage(num).then(page => {
const viewport = page.getViewport({ scale });
canvas.height = viewport.height;
canvas.width = viewport.width;
const renderContext = {
canvasContext: ctx,
viewport: viewport
};
page.render(renderContext).promise.then(() => {
pageRendering = false;
if (pageNumPending !== null) {
renderPage(pageNumPending);
pageNumPending = null;
}
});
});
document.getElementById('page-num').textContent = 'Page: ' + num;
}
function queueRenderPage(num) {
if (pageRendering) {
pageNumPending = num;
} else {
renderPage(num);
}
}
function prevPage() {
if (pageNum <= 1) return;
pageNum--;
queueRenderPage(pageNum);
}
function nextPage() {
if (pageNum >= pdfDoc.numPages) return;
pageNum++;
queueRenderPage(pageNum);
}
pdfjsLib.getDocument(url).promise.then(pdf => {
pdfDoc = pdf;
document.getElementById('page-count').textContent = pdfDoc.numPages;
renderPage(pageNum);
}).catch(error => {
console.error('Failed to load PDF:', error);
document.getElementById('pdf-container').innerHTML = '<p>Failed to load PDF.</p>';
});
</script>
`;
} else if (isTxt) {
content = `
<iframe id="media" src="${fileUrl}" width="100%" height="100%" style="border: none;"></iframe>
`;
} else {
content = `<p style="font-size: 2.5rem;">The file type does not support preview.</p>`;
}
return `
<html>
<head>
<title>Preview</title>
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
background-color: #f4f4f4;
padding: 20px;
margin: 0;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
#media {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
button {
background-color: #5cb85c;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 2rem;
margin-top: 20px;
}
button:hover {
background-color: #4cae4c;
}
.btn-group-div button:last-child {
margin-left: 1rem;
}
</style>
<script src="/js/pdf.min.js"></script>
<script src="/js/pdf.worker.min.js"></script>
</head>
<body>
<div id="media-container">
${content}
</div>
<div class="btn-group-div">
<button onclick="window.history.back()">Back</button>
<button onclick="window.location.href='/download/${filename}'">Download</button>
</div>
<script>
function adjustMediaSize() {
const media = document.getElementById('media');
const container = document.getElementById('media-container');
const screenWidth = window.innerWidth;
const screenHeight = window.innerHeight;
if (screenWidth > screenHeight) {
media.style.height = '85vh';
media.style.width = 'auto';
} else {
media.style.width = '100vw';
media.style.height = 'auto';
}
}
// 页面加载时调整媒体尺寸
window.addEventListener('load', adjustMediaSize);
// 窗口大小变化时调整媒体尺寸
window.addEventListener('resize', adjustMediaSize);
</script>
</body>
</html>
`;
}
// 添加预览文件的路由
app.get('/preview/:filename', (req, res) => {
const filename = req.params.filename;
const filePath = path.join(__dirname, 'uploads', filename);
// const fileUrl = `/uploads/${filename}`;
const fileUrl = `${_url}/uploads/${filename}`;
if (!fs.existsSync(filePath)) {
return res.status(404).send('File not found');
}
// const fileUrl = `/uploads/${filename}`;
// 检查文件类型
if (filename.endsWith('.jpg') || filename.endsWith('.png') || filename.endsWith('.jpeg') || filename.endsWith('.gif')) {
res.send(getPreviewHtml('image', fileUrl, filename));
} else if (filename.endsWith('.mp4') || filename.endsWith('.webm') || filename.endsWith('.ogg')) {
res.send(getPreviewHtml('video', fileUrl, filename));
} else if (filename.endsWith('.pdf')) {
res.send(getPreviewHtml('pdf', fileUrl, filename));
} else if (filename.endsWith('.txt')) {
res.send(getPreviewHtml('txt', fileUrl, filename));
} else {
res.send(getPreviewHtml('none', fileUrl, filename));
// res.status(404).send('The file type does not support preview.');
}
});
app.get('/list-downloads', (req, res) => {
fs.readdir(uploadsDir, (err, files) => {
if (err) {
console.log("err:", err);
return res.status(500).send('Unable to read download folder');
}
var isEmpty = files.length === 0;
let fileListHtml = `
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<title>downloadable files</title>
<style>
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background-color: #f4f4f4; text-align: center;}
h1 { color: #333; margin: 2rem 0; font-size: 2.5rem;}
button { background-color: #5cb85c; color: white; border: none; padding: 2rem; border-radius: 5px; cursor: pointer; font-size: 2.5rem; }
button:hover { background-color: #4cae4c; }
ul { list-style-type: none; padding: 0; }
li { margin: 4rem 0; font-size: 3rem; }
a { text-decoration: none; color: #007bff; }
a:hover { text-decoration: underline; }
table {
font-size: 2rem;
}
td {
text-align: center;
}
tr {
font-size: 2.5rem;
}
.fa-sync-alt {
font-size: 50px ;
margin-right: 1.3rem;
}
.preview-btn {
padding: 1rem 1.3rem;
}
.btn-group {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.btn-group a {
background-color: #5cb85c;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 2.5rem;
padding: 1rem 1.3rem;
white-space: pre-wrap;
}
.btn-group a:last-child {
margin-top: 1rem;
}
.empty-msg {
font-size: 2.5rem;
text-align: center;
margin-top: 5rem;
}
</style>
<script src="/socket.io/socket.io.js"></script>
</head>
<body>
<div>
<h1>List of downloadable files <i class="fas fa-sync-alt" id="refreshIcon" style="cursor:pointer; float:right; font-size:24px;" title="Refresh"></i></h1>
</div>
<script>
window.addEventListener('load', function () {
document.getElementById('refreshIcon').addEventListener('click', function() { location.reload(); });
if (io) {
const socket = io();
socket.on('uploadsUpdated', (data) => {
location.reload();
});
socket.on('refreshPage', data => {
location.reload();
})
}
if (${isEmpty}) {
setTimeout(() => {
window.location.href='/'
}, 800);
}
});
function previewFile(filename) {
window.location.href = "/preview/"+filename;
}
</script>
<p class="empty-msg" style="display: ${isEmpty ? 'block' : 'none'};">Cann't Found Any File</p>
<table style="width: 100%; border-collapse: collapse;" style="display: ${isEmpty ? 'none' : 'table-header-group'};">
<thead style="display: ${isEmpty ? 'none' : 'table-header-group'};">
<tr>
<th style="border: 1px solid #ddd; padding: 16px; height: 80px;">File Name</th>
<th style="border: 1px solid #ddd; padding: 16px; height: 80px;">Create Date</th>
<th style="border: 1px solid #ddd; padding: 16px; height: 80px;">Size (MB)</th>
<th style="border: 1px solid #ddd; padding: 16px; height: 80px;">Action</th>
<th style="border: 1px solid #ddd; padding: 16px; height: 80px;">Preview</th>
</tr>
</thead>
<tbody>`;
files.forEach(file => {
const filePath = path.join(uploadsDir, file);
const stats = fs.statSync(filePath);
const creationDate = new Date(stats.birthtime);
const formattedDate = `${creationDate.getMonth() + 1}-${creationDate.getDate()}-${creationDate.getFullYear()} ${creationDate.getHours().toString().padStart(2, '0')}:${creationDate.getMinutes().toString().padStart(2, '0')}:${creationDate.getSeconds().toString().padStart(2, '0')}`;
const sizeInMB = (stats.size / (1024 * 1024)).toFixed(2);
fileListHtml += `
<tr>
<td style="border: 1px solid #ddd; padding: 30px 10px;word-wrap: break-word; word-break: break-all;">${file}</td>
<td style="border: 1px solid #ddd; padding: 30px 10px;word-wrap: break-word;">${formattedDate}</td>
<td style="border: 1px solid #ddd; padding: 30px 10px;word-wrap: break-word; word-break: break-all;">${sizeInMB}</td>
<td style="border: 1px solid #ddd; padding: 30px 10px;word-wrap: break-word;">
<div class="btn-group">
<a href="/download/${file}" download>Download</a>
<a href="/delete/${file}" onclick="return confirm('Surely want to delete?')"> Delete </a>
</div>
</td>
<td style="border: 1px solid #ddd; padding: 30px 10px;word-wrap: break-word;">
<button class="preview-btn" onclick="previewFile('${file}')">Preview</button>
</td>
</tr>`;
});
fileListHtml += `
</tbody>
</table>
<button style="margin-top: 3rem;" onclick="window.location.href='/'">Back to Home</button>
</body>
</html>`;
res.send(fileListHtml);
});
});
// 路由:共享粘贴板
app.post('/shareClipboard', async (req, res) => {
try {
const content = req.body && req.body.content;
fs.writeFileSync(clipboardPath, content || "", 'utf8');
res.json({ message: 'shared: ' + content, content });
} catch (error) {
res.status(500).json({ message: 'Failed to read clipboard contents:', error });
}
});
// 路由:获取粘贴板
app.get('/getClipboard', (req, res) => {
const content = fs.readFileSync(clipboardPath, 'utf8');
res.json({ content: content || "" });
});
app.post('/clearClipboard', (req, res) => {
fs.writeFileSync(clipboardPath, '', 'utf8'); // 清空文件内容
res.json({ message: 'cleared clipboard' });
});
const log = (msg) => {
console.log(`\x1b[32m ${msg} \x1b[0m`);
};
const redlog = (msg) => {
console.log(`\x1b[31m ${msg} \x1b[0m`);
}
const runCommand = (command) => {
return new Promise((resolve, reject) => {
child_process.exec(command, (error, stdout, stderr) => {
if (error) {
reject(stdout + stderr);
}
else {
resolve(stdout);
}
});
});
}
let currentIp;
const getIpAddress = async () => {
if (currentIp) return currentIp;
const networkInterfaces = os.networkInterfaces();
for (const interfaceName in networkInterfaces) {
for (const iface of networkInterfaces[interfaceName]) {
if (iface.family === 'IPv4' && !iface.internal && iface.address.startsWith('192.168')) {
currentIp = iface.address;
return currentIp;
}
}
}
if (os.platform() === 'darwin') {
try {
const ip = await runCommand('ipconfig getifaddr en0');
currentIp = ip.trim();
return currentIp;
} catch (error) {
}
}
currentIp = '127.0.0.1';
return currentIp;
};
app.get('/qrcode', async (req, res) => {
try {
const qrCodeImage = qr.imageSync(_url, { type: 'png' });
const base64QRImage = Buffer.from(qrCodeImage).toString('base64');
const dataUrl = `data:image/png;base64,${base64QRImage}`;
res.send(dataUrl);
} catch (err) {
res.status(500).send('An error occurred while generating the QR code');
}
});
(async () => {
PORT = await getRandomPort();
const ipAddress = await getIpAddress();
const url = `http://${ipAddress}:${PORT}`;
_url = url;
server.listen(PORT, async () => {
log(`\nrun at: ${url}`);
redlog(`\nIf catch a permission error, try to run with sudo: sudo chmod -R 777 ${__dirname} \n`);
if (os.platform() === 'win32') {
// Windows
exec(`start http://localhost:${PORT}`);
} else if (os.platform() === 'darwin') {
// macOS
if (newwindow) {
exec(`open -a "Google Chrome" --args --new-tab http://localhost:${PORT}`);
} else {
exec(`open http://localhost:${PORT}`);
}
} else {
redlog('The function of automatically opening the browser is not implemented, for non-Windows and macOS systems');
}
});
})();