LANShare / server.js
SunbalAzizLCWU's picture
Reduce cleanup interval from 1 hour to 20 minutes
0dff490 unverified
Raw
History Blame Contribute Delete
6.78 kB
const fs = require('fs');
const path = require('path');
const os = require('os');
const express = require('express');
const http = require('http');
const multer = require('multer');
const QRCode = require('qrcode');
const { Server } = require('socket.io');
const app = express();
const server = http.createServer(app);
const io = new Server(server);
const PORT = process.env.PORT || 3000;
const uploadsDir = path.join(__dirname, 'uploads');
const publicDir = path.join(__dirname, 'public');
fs.mkdirSync(uploadsDir, { recursive: true });
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static(publicDir));
function getLanIp() {
const interfaces = os.networkInterfaces();
for (const interfaceEntries of Object.values(interfaces)) {
for (const entry of interfaceEntries || []) {
if (entry && entry.family === 'IPv4' && !entry.internal) {
return entry.address;
}
}
}
return '127.0.0.1';
}
const lanIp = getLanIp();
const lanUrl = `http://${lanIp}:${PORT}`;
const storage = multer.diskStorage({
destination: (req, _file, callback) => {
try {
const roomId = getRequestIP(req);
const roomUploadsDir = getRoomUploadsDir(roomId);
fs.mkdirSync(roomUploadsDir, { recursive: true });
callback(null, roomUploadsDir);
} catch (error) {
callback(error);
}
},
filename: (_req, file, callback) => {
callback(null, `${Date.now()}-${file.originalname}`);
},
});
const upload = multer({ storage });
const rooms = new Map();
function normalizeIp(ip) {
if (!ip) {
return 'unknown';
}
return ip.startsWith('::ffff:') ? ip.slice(7) : ip;
}
function getRequestIP(req) {
const forwarded = req.headers['x-forwarded-for'];
const rawIp = forwarded ? forwarded.split(',')[0].trim() : req.ip || req.socket?.remoteAddress;
return normalizeIp(rawIp);
}
function getSocketIP(socket) {
const forwarded = socket.handshake.headers['x-forwarded-for'];
const rawIp = forwarded ? forwarded.split(',')[0].trim() : socket.handshake.address;
return normalizeIp(rawIp);
}
function getRoomUploadsDir(roomId) {
return path.join(uploadsDir, roomId);
}
function getRoom(roomId) {
if (!rooms.has(roomId)) {
rooms.set(roomId, {
devices: new Map(),
});
}
return rooms.get(roomId);
}
function deleteRoomIfEmpty(roomId) {
const room = rooms.get(roomId);
if (!room || room.devices.size > 0) {
return;
}
rooms.delete(roomId);
}
function cleanupUploads() {
try {
const cutoffTime = Date.now() - (60 * 60 * 1000);
const roomDirectories = fs.existsSync(uploadsDir) ? fs.readdirSync(uploadsDir) : [];
for (const roomDirectory of roomDirectories) {
if (roomDirectory === '.gitkeep') {
continue;
}
const roomPath = path.join(uploadsDir, roomDirectory);
let stats;
try {
stats = fs.statSync(roomPath);
} catch (error) {
console.error(`Failed to inspect ${roomDirectory}:`, error);
continue;
}
if (!stats.isDirectory()) {
continue;
}
const files = fs.readdirSync(roomPath);
for (const fileName of files) {
if (fileName === '.gitkeep') {
continue;
}
const filePath = path.join(roomPath, fileName);
try {
const fileStats = fs.statSync(filePath);
if (fileStats.isFile() && fileStats.mtimeMs < cutoffTime) {
fs.unlinkSync(filePath);
}
} catch (error) {
console.error(`Failed to clean up ${fileName}:`, error);
}
}
}
} catch (error) {
console.error('Upload cleanup failed:', error);
}
}
function broadcastDevices(roomId) {
const room = rooms.get(roomId);
if (!room) {
return;
}
io.to(roomId).emit(
'devices-update',
Array.from(room.devices.entries()).map(([id, name]) => ({ id, name }))
);
}
app.post('/upload', (req, res) => {
try {
upload.single('file')(req, res, (error) => {
try {
if (error) {
throw error;
}
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
const roomId = getRequestIP(req);
const room = getRoom(roomId);
const uploadedAt = new Date().toISOString();
const fileInfo = {
name: req.file.originalname,
size: req.file.size,
type: req.file.mimetype,
uploadedBy: req.body.uploadedBy || req.body.deviceName || 'Unknown',
time: uploadedAt,
url: `/uploads/${encodeURIComponent(req.file.filename)}`,
};
io.to(roomId).emit('new-file', fileInfo);
return res.status(200).json(fileInfo);
} catch (err) {
return res.status(500).json({ error: err.message || 'Upload failed' });
}
});
} catch (error) {
return res.status(500).json({ error: error.message || 'Upload failed' });
}
});
app.get('/uploads/:filename', (req, res) => {
try {
const roomId = getRequestIP(req);
const filename = path.basename(req.params.filename);
const filePath = path.join(getRoomUploadsDir(roomId), filename);
if (!fs.existsSync(filePath)) {
return res.status(404).json({ error: 'Not found' });
}
return res.download(filePath, filename);
} catch (error) {
return res.status(500).json({ error: error.message || 'Download failed' });
}
});
app.get('/qr', async (_req, res) => {
try {
const pngBuffer = await QRCode.toBuffer(lanUrl, {
type: 'png',
errorCorrectionLevel: 'M',
});
res.setHeader('Content-Type', 'image/png');
return res.status(200).send(pngBuffer);
} catch (error) {
return res.status(500).json({ error: error.message || 'QR generation failed' });
}
});
io.on('connection', (socket) => {
const roomId = getSocketIP(socket);
socket.on('join', (deviceName) => {
const room = getRoom(roomId);
room.devices.set(socket.id, deviceName || 'Unknown');
socket.data.roomId = roomId;
socket.join(roomId);
broadcastDevices(roomId);
});
socket.on('notepad-update', (text) => {
socket.to(roomId).emit('notepad-update', text);
});
socket.on('disconnect', () => {
const activeRoomId = socket.data.roomId || roomId;
const room = rooms.get(activeRoomId);
if (room) {
room.devices.delete(socket.id);
broadcastDevices(activeRoomId);
deleteRoomIfEmpty(activeRoomId);
}
});
});
app.get('/health', (_req, res) => res.status(200).json({ status: 'ok' }));
app.use((_req, res) => {
res.status(404).json({ error: 'Not found' });
});
server.listen(PORT, () => {
console.log(`LocalShare running at ${lanUrl}`);
cleanupUploads();
setInterval(cleanupUploads, 20 * 60 * 1000);
});