lan-file-transfer 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,14 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>LAN File Transfer</title>
8
+ <script type="module" crossorigin src="/assets/index-Ch9zqWdd.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-BTvut2MD.css">
10
+ </head>
11
+ <body>
12
+ <div id="root"></div>
13
+ </body>
14
+ </html>
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "client",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "@radix-ui/react-dialog": "^1.1.2",
13
+ "axios": "^1.7.7",
14
+ "lucide-react": "^0.451.0",
15
+ "react": "^18.3.1",
16
+ "react-dom": "^18.3.1",
17
+ "react-router-dom": "^6.27.0",
18
+ "sonner": "^1.7.1"
19
+ },
20
+ "devDependencies": {
21
+ "@types/react": "^18.3.12",
22
+ "@types/react-dom": "^18.3.1",
23
+ "@vitejs/plugin-react": "^4.3.4",
24
+ "autoprefixer": "^10.4.20",
25
+ "class-variance-authority": "^0.7.1",
26
+ "clsx": "^2.1.1",
27
+ "postcss": "^8.4.47",
28
+ "tailwind-merge": "^2.5.4",
29
+ "tailwindcss": "^3.4.14",
30
+ "tailwindcss-animate": "^1.0.7",
31
+ "typescript": "^5.6.3",
32
+ "vite": "^5.4.10"
33
+ }
34
+ }
@@ -0,0 +1,20 @@
1
+ module.exports = {
2
+ apps: [{
3
+ name: 'lan-transfer',
4
+ script: 'server/index.js',
5
+ cwd: __dirname,
6
+ env: {
7
+ NODE_ENV: 'production',
8
+ PORT: 3456,
9
+ },
10
+ watch: false,
11
+ autorestart: true,
12
+ max_restarts: 10,
13
+ min_uptime: '10s',
14
+ max_memory_restart: '500M',
15
+ error_file: './logs/error.log',
16
+ out_file: './logs/output.log',
17
+ merge_logs: true,
18
+ log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
19
+ }],
20
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "lan-file-transfer",
3
+ "version": "1.0.0",
4
+ "description": "LAN file transfer tool with pickup code download",
5
+ "bin": {
6
+ "lan-file-transfer": "./bin/lan-file-transfer.js"
7
+ },
8
+ "scripts": {
9
+ "build": "pnpm --filter client build",
10
+ "start": "node bin/lan-file-transfer.js",
11
+ "prepublishOnly": "pnpm --filter client build"
12
+ },
13
+ "dependencies": {
14
+ "better-sqlite3": "^11.6.0",
15
+ "cors": "^2.8.5",
16
+ "express": "^4.21.0",
17
+ "multer": "^1.4.5-lts.1",
18
+ "uuid": "^10.0.0"
19
+ },
20
+ "files": [
21
+ "bin/",
22
+ "server/index.js",
23
+ "server/db.js",
24
+ "server/upload.js",
25
+ "server/routes/",
26
+ "client/dist/",
27
+ "ecosystem.config.js",
28
+ "package.json"
29
+ ],
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/anomalyco/lan-file-transfer.git"
33
+ },
34
+ "keywords": [
35
+ "lan",
36
+ "file-transfer",
37
+ "pickup-code",
38
+ "sharing"
39
+ ],
40
+ "license": "MIT"
41
+ }
package/server/db.js ADDED
@@ -0,0 +1,106 @@
1
+ const Database = require('better-sqlite3')
2
+ const path = require('path')
3
+ const fs = require('fs')
4
+
5
+ let db
6
+
7
+ function initDb() {
8
+ const dataDir = path.join(__dirname, '..', 'data')
9
+ if (!fs.existsSync(dataDir)) {
10
+ fs.mkdirSync(dataDir, { recursive: true })
11
+ }
12
+
13
+ const dbPath = path.join(dataDir, 'files.db')
14
+ db = new Database(dbPath)
15
+
16
+ db.exec(`
17
+ CREATE TABLE IF NOT EXISTS files (
18
+ id TEXT PRIMARY KEY,
19
+ code TEXT UNIQUE NOT NULL,
20
+ name TEXT NOT NULL,
21
+ stored_name TEXT NOT NULL,
22
+ size INTEGER NOT NULL,
23
+ mime_type TEXT DEFAULT '',
24
+ uploader_session TEXT DEFAULT '',
25
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
26
+ download_count INTEGER DEFAULT 0
27
+ )
28
+ `)
29
+
30
+ db.exec(`
31
+ CREATE TABLE IF NOT EXISTS downloads (
32
+ id TEXT PRIMARY KEY,
33
+ file_id TEXT NOT NULL,
34
+ file_name TEXT NOT NULL,
35
+ file_code TEXT NOT NULL,
36
+ session_id TEXT NOT NULL,
37
+ downloaded_at TEXT NOT NULL DEFAULT (datetime('now'))
38
+ )
39
+ `)
40
+
41
+ try { db.exec("ALTER TABLE files ADD COLUMN uploader_session TEXT DEFAULT ''") } catch (e) {}
42
+
43
+ return db
44
+ }
45
+
46
+ function addFile({ id, code, name, stored_name, size, mime_type, uploader_session }) {
47
+ const stmt = db.prepare(`
48
+ INSERT INTO files (id, code, name, stored_name, size, mime_type, uploader_session)
49
+ VALUES (?, ?, ?, ?, ?, ?, ?)
50
+ `)
51
+ return stmt.run(id, code, name, stored_name, size, mime_type || '', uploader_session || '')
52
+ }
53
+
54
+ function getFileById(id) {
55
+ return db.prepare('SELECT * FROM files WHERE id = ?').get(id)
56
+ }
57
+
58
+ function getFileByCode(code) {
59
+ return db.prepare('SELECT * FROM files WHERE code = ?').get(code)
60
+ }
61
+
62
+ function getAllFiles() {
63
+ return db.prepare('SELECT * FROM files ORDER BY created_at DESC').all()
64
+ }
65
+
66
+ function getFilesBySession(sessionId) {
67
+ return db.prepare('SELECT * FROM files WHERE uploader_session = ? ORDER BY created_at DESC').all(sessionId)
68
+ }
69
+
70
+ function getDownloadsBySession(sessionId) {
71
+ return db.prepare('SELECT * FROM downloads WHERE session_id = ? ORDER BY downloaded_at DESC').all(sessionId)
72
+ }
73
+
74
+ function deleteFile(id) {
75
+ return db.prepare('DELETE FROM files WHERE id = ?').run(id)
76
+ }
77
+
78
+ function incrementDownloadCount(id) {
79
+ return db.prepare('UPDATE files SET download_count = download_count + 1 WHERE id = ?').run(id)
80
+ }
81
+
82
+ function addDownload({ id, file_id, file_name, file_code, session_id }) {
83
+ const stmt = db.prepare(`
84
+ INSERT INTO downloads (id, file_id, file_name, file_code, session_id)
85
+ VALUES (?, ?, ?, ?, ?)
86
+ `)
87
+ return stmt.run(id, file_id, file_name, file_code, session_id)
88
+ }
89
+
90
+ function getStorageStats() {
91
+ return db.prepare('SELECT COALESCE(SUM(size), 0) as totalSize, COUNT(*) as fileCount FROM files').get()
92
+ }
93
+
94
+ module.exports = {
95
+ initDb,
96
+ addFile,
97
+ getFileById,
98
+ getFileByCode,
99
+ getAllFiles,
100
+ getFilesBySession,
101
+ getDownloadsBySession,
102
+ deleteFile,
103
+ incrementDownloadCount,
104
+ addDownload,
105
+ getStorageStats
106
+ }
@@ -0,0 +1,53 @@
1
+ try { require('dotenv').config() } catch (e) {}
2
+ const express = require('express')
3
+ const cors = require('cors')
4
+ const path = require('path')
5
+ const fs = require('fs')
6
+ const { initDb } = require('./db')
7
+ const filesRouter = require('./routes/files')
8
+
9
+ initDb()
10
+
11
+ const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || '123456'
12
+
13
+ const app = express()
14
+
15
+ app.set('adminPassword', ADMIN_PASSWORD)
16
+
17
+ app.use(cors())
18
+ app.use(express.json())
19
+ app.use(express.urlencoded({ extended: true }))
20
+
21
+ app.use('/api', filesRouter)
22
+
23
+ const clientDist = path.join(__dirname, '..', 'client', 'dist')
24
+ if (fs.existsSync(clientDist)) {
25
+ app.use(express.static(clientDist))
26
+ app.get('*', (req, res) => {
27
+ if (!req.path.startsWith('/api')) {
28
+ res.sendFile(path.join(clientDist, 'index.html'))
29
+ }
30
+ })
31
+ }
32
+
33
+ const port = process.env.PORT || 3456
34
+ app.listen(port, '0.0.0.0', () => {
35
+ const pidFile = path.join(__dirname, '..', '.pid')
36
+ fs.writeFileSync(pidFile, String(process.pid), 'utf-8')
37
+
38
+ console.log(`\n LAN File Transfer`)
39
+ console.log(` Public: http://localhost:${port}`)
40
+ console.log(` Admin: http://localhost:${port}/admin`)
41
+ console.log(` Password: ${ADMIN_PASSWORD}\n`)
42
+
43
+ const os = require('os')
44
+ const nets = os.networkInterfaces()
45
+ for (const name of Object.keys(nets)) {
46
+ for (const net of nets[name]) {
47
+ if (net.family === 'IPv4' && !net.internal) {
48
+ console.log(` LAN: http://${net.address}:${port}`)
49
+ }
50
+ }
51
+ }
52
+ console.log('')
53
+ })
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "server",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "node index.js"
7
+ },
8
+ "dependencies": {
9
+ "better-sqlite3": "^11.6.0",
10
+ "cors": "^2.8.5",
11
+ "express": "^4.21.0",
12
+ "multer": "^1.4.5-lts.1",
13
+ "uuid": "^10.0.0"
14
+ }
15
+ }
@@ -0,0 +1,209 @@
1
+ const express = require('express')
2
+ const router = express.Router()
3
+ const path = require('path')
4
+ const fs = require('fs')
5
+ const crypto = require('crypto')
6
+ const db = require('../db')
7
+ const upload = require('../upload')
8
+
9
+ const uploadsDir = path.join(__dirname, '..', '..', 'uploads')
10
+
11
+ function generateCode() {
12
+ return String(Math.floor(1000 + Math.random() * 9000))
13
+ }
14
+
15
+ function getSession(req) {
16
+ return req.headers['x-session-id'] || ''
17
+ }
18
+
19
+ function requireAdmin(req, res, next) {
20
+ const pwd = req.headers['x-admin-password']
21
+ const adminPwd = req.app.get('adminPassword')
22
+ if (!pwd || pwd !== adminPwd) {
23
+ return res.status(401).json({ error: 'Unauthorized' })
24
+ }
25
+ next()
26
+ }
27
+
28
+ router.get('/files', requireAdmin, (req, res) => {
29
+ try {
30
+ const files = db.getAllFiles()
31
+ res.json({ files })
32
+ } catch (err) {
33
+ res.status(500).json({ error: 'Failed to fetch files' })
34
+ }
35
+ })
36
+
37
+ router.get('/my-files', (req, res) => {
38
+ try {
39
+ const session = getSession(req)
40
+ if (!session) return res.json({ files: [] })
41
+ const files = db.getFilesBySession(session)
42
+ res.json({ files })
43
+ } catch (err) {
44
+ res.status(500).json({ error: 'Failed to fetch files' })
45
+ }
46
+ })
47
+
48
+ router.get('/my-downloads', (req, res) => {
49
+ try {
50
+ const session = getSession(req)
51
+ if (!session) return res.json({ downloads: [] })
52
+ const downloads = db.getDownloadsBySession(session)
53
+ res.json({ downloads })
54
+ } catch (err) {
55
+ res.status(500).json({ error: 'Failed to fetch downloads' })
56
+ }
57
+ })
58
+
59
+ router.post('/upload', (req, res) => {
60
+ upload.single('file')(req, res, (err) => {
61
+ if (err) {
62
+ if (err.code === 'LIMIT_FILE_SIZE') {
63
+ return res.status(413).json({ error: 'File too large' })
64
+ }
65
+ return res.status(400).json({ error: err.message })
66
+ }
67
+
68
+ if (!req.file) {
69
+ return res.status(400).json({ error: 'No file provided' })
70
+ }
71
+
72
+ const { originalname, filename, size, mimetype } = req.file
73
+
74
+ let code
75
+ let attempts = 0
76
+ do {
77
+ code = generateCode()
78
+ attempts++
79
+ } while (db.getFileByCode(code) && attempts < 100)
80
+
81
+ const id = crypto.randomUUID()
82
+
83
+ db.addFile({
84
+ id,
85
+ code,
86
+ name: originalname,
87
+ stored_name: filename,
88
+ size,
89
+ mime_type: mimetype,
90
+ uploader_session: getSession(req)
91
+ })
92
+
93
+ const savedFile = db.getFileById(id)
94
+
95
+ res.json({
96
+ success: true,
97
+ code,
98
+ file: {
99
+ id: savedFile.id,
100
+ code: savedFile.code,
101
+ name: savedFile.name,
102
+ size: savedFile.size,
103
+ mime_type: savedFile.mime_type,
104
+ created_at: savedFile.created_at
105
+ }
106
+ })
107
+ })
108
+ })
109
+
110
+ router.get('/download/:id', (req, res) => {
111
+ try {
112
+ const file = db.getFileById(req.params.id)
113
+ if (!file) {
114
+ return res.status(404).json({ error: 'File not found' })
115
+ }
116
+
117
+ const filePath = path.join(uploadsDir, file.stored_name)
118
+ if (!fs.existsSync(filePath)) {
119
+ return res.status(404).json({ error: 'File not found on disk' })
120
+ }
121
+
122
+ db.incrementDownloadCount(file.id)
123
+
124
+ res.setHeader('Content-Disposition', `attachment; filename="${file.name}"`)
125
+ res.setHeader('Content-Type', file.mime_type || 'application/octet-stream')
126
+
127
+ const readStream = fs.createReadStream(filePath)
128
+ readStream.pipe(res)
129
+ } catch (err) {
130
+ res.status(500).json({ error: 'Failed to download file' })
131
+ }
132
+ })
133
+
134
+ router.post('/download-by-code', (req, res) => {
135
+ try {
136
+ const { code } = req.body
137
+ if (!code) {
138
+ return res.status(400).json({ error: 'Code is required' })
139
+ }
140
+
141
+ const file = db.getFileByCode(code)
142
+ if (!file) {
143
+ return res.status(404).json({ error: 'File not found' })
144
+ }
145
+
146
+ const filePath = path.join(uploadsDir, file.stored_name)
147
+ if (!fs.existsSync(filePath)) {
148
+ return res.status(404).json({ error: 'File not found on disk' })
149
+ }
150
+
151
+ db.incrementDownloadCount(file.id)
152
+
153
+ const session = getSession(req)
154
+ if (session) {
155
+ db.addDownload({
156
+ id: crypto.randomUUID(),
157
+ file_id: file.id,
158
+ file_name: file.name,
159
+ file_code: file.code,
160
+ session_id: session
161
+ })
162
+ }
163
+
164
+ res.setHeader('Content-Disposition', `attachment; filename="${file.name}"`)
165
+ res.setHeader('Content-Type', file.mime_type || 'application/octet-stream')
166
+
167
+ const readStream = fs.createReadStream(filePath)
168
+ readStream.pipe(res)
169
+ } catch (err) {
170
+ res.status(500).json({ error: 'Failed to download file' })
171
+ }
172
+ })
173
+
174
+ router.delete('/files/:id', requireAdmin, (req, res) => {
175
+ try {
176
+ const file = db.getFileById(req.params.id)
177
+ if (!file) {
178
+ return res.status(404).json({ error: 'File not found' })
179
+ }
180
+
181
+ const filePath = path.join(uploadsDir, file.stored_name)
182
+ if (fs.existsSync(filePath)) {
183
+ fs.unlinkSync(filePath)
184
+ }
185
+
186
+ db.deleteFile(file.id)
187
+
188
+ res.json({ success: true })
189
+ } catch (err) {
190
+ res.status(500).json({ error: 'Failed to delete file' })
191
+ }
192
+ })
193
+
194
+ router.get('/stats', requireAdmin, (req, res) => {
195
+ try {
196
+ const stats = db.getStorageStats()
197
+ res.json(stats)
198
+ } catch (err) {
199
+ res.status(500).json({ error: 'Failed to get stats' })
200
+ }
201
+ })
202
+
203
+ router.get('/admin/check', (req, res) => {
204
+ const pwd = req.headers['x-admin-password']
205
+ const adminPwd = req.app.get('adminPassword')
206
+ res.json({ valid: pwd === adminPwd })
207
+ })
208
+
209
+ module.exports = router
@@ -0,0 +1,34 @@
1
+ const multer = require('multer')
2
+ const path = require('path')
3
+ const crypto = require('crypto')
4
+
5
+ const uploadsDir = path.join(__dirname, '..', 'uploads')
6
+
7
+ function sanitizeFilename(originalName) {
8
+ const baseName = path.basename(originalName)
9
+ const ext = path.extname(baseName)
10
+ const nameWithoutExt = path.basename(baseName, ext)
11
+ const sanitizedName = nameWithoutExt.replace(/[^a-zA-Z0-9]/g, '_')
12
+ const sanitizedExt = ext.replace(/[^a-zA-Z0-9.]/g, '_')
13
+ return `${sanitizedName}${sanitizedExt}`
14
+ }
15
+
16
+ const storage = multer.diskStorage({
17
+ destination: function (req, file, cb) {
18
+ cb(null, uploadsDir)
19
+ },
20
+ filename: function (req, file, cb) {
21
+ const sanitized = sanitizeFilename(file.originalname)
22
+ const uniqueName = `${crypto.randomUUID()}-${sanitized}`
23
+ cb(null, uniqueName)
24
+ }
25
+ })
26
+
27
+ const upload = multer({
28
+ storage,
29
+ limits: {
30
+ fileSize: 1024 * 1024 * 1024
31
+ }
32
+ })
33
+
34
+ module.exports = upload