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.
- package/README.md +97 -0
- package/bin/lan-file-transfer.js +36 -0
- package/client/dist/assets/index-BTvut2MD.css +1 -0
- package/client/dist/assets/index-Ch9zqWdd.js +190 -0
- package/client/dist/index.html +14 -0
- package/client/package.json +34 -0
- package/ecosystem.config.js +20 -0
- package/package.json +41 -0
- package/server/db.js +106 -0
- package/server/index.js +53 -0
- package/server/package.json +15 -0
- package/server/routes/files.js +209 -0
- package/server/upload.js +34 -0
|
@@ -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
|
+
}
|
package/server/index.js
ADDED
|
@@ -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
|
package/server/upload.js
ADDED
|
@@ -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
|