Spaces:
Paused
Paused
| 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); | |
| }); | |