Compare commits
31 Commits
使用pug渲染的he
...
master
Author | SHA1 | Date | |
---|---|---|---|
1a23273708 | |||
b1b2b29689 | |||
35b84697cd | |||
dc449886bf | |||
e88d21e354 | |||
659aa9d111 | |||
ab862d08fd | |||
0ae0e1285b | |||
c1a5437532 | |||
dce08a43a4 | |||
15321afae2 | |||
87261e3d5c | |||
acebcd95f4 | |||
503fae7cc7 | |||
4006decb81 | |||
9854bfeed6 | |||
e757feed04 | |||
a1bff06a26 | |||
9f12482b30 | |||
a81b7c0a37 | |||
c34c1a2d38 | |||
d26791a77f | |||
67cd03f053 | |||
c818880fb5 | |||
a503d3c9f8 | |||
cc12a317d1 | |||
4857119778 | |||
24158f15a9 | |||
c81f6df88d | |||
d9c7a93aca | |||
6e3251ce25 |
4
.env.example
Normal file
4
.env.example
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
HOST="0.0.0.0"
|
||||||
|
PORT="3000"
|
||||||
|
MONGODB_URL="mongodb://user:pass@localhost/filesharing?authSource=admin"
|
||||||
|
UPLOAD_SIZE_LIMIT=10485760
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,3 +1,5 @@
|
|||||||
|
upload/
|
||||||
|
|
||||||
# ---> Node
|
# ---> Node
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
|
12
models/Filesharing.js
Normal file
12
models/Filesharing.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
const mongoose = require('mongoose')
|
||||||
|
const Filesharing = new mongoose.Schema({
|
||||||
|
md5: { type: String, require: true },
|
||||||
|
size: { type: Number, require: true },
|
||||||
|
encoding: { type: String, require: true },
|
||||||
|
mimetype: { type: String, require: true },
|
||||||
|
filename: { type: String, require: true },
|
||||||
|
password: { type: String, require: true },
|
||||||
|
downloads: { type: Number, default: 0 },
|
||||||
|
createdAt: { type: Date, default: Date.now },
|
||||||
|
})
|
||||||
|
module.exports = mongoose.model('Filesharing', Filesharing)
|
870
package-lock.json
generated
870
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "fileshare",
|
"name": "filesharing",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "文件分享服务器",
|
"description": "文件分享",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "nodemon",
|
"start": "nodemon",
|
||||||
@ -9,13 +9,21 @@
|
|||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "ssh://git@nas:2222/zhaoxin/fileshare.git"
|
"url": "ssh://git@nas:2222/zhaoxin/filesharing.git"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "Zhao Xin <7176466@qq.com>",
|
"author": "Zhao Xin <7176466@qq.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"bcryptjs": "^2.4.3",
|
||||||
|
"cron": "^2.1.0",
|
||||||
|
"dotenv": "^16.0.1",
|
||||||
"express": "^4.18.1",
|
"express": "^4.18.1",
|
||||||
|
"filesize": "^9.0.11",
|
||||||
|
"md5-file": "^5.0.0",
|
||||||
|
"moment": "^2.29.4",
|
||||||
|
"mongoose": "^6.5.3",
|
||||||
|
"multer": "^1.4.5-lts.1",
|
||||||
"pug": "^3.0.2"
|
"pug": "^3.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
48
public/style.css
Normal file
48
public/style.css
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
* {
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
user-select: none;
|
||||||
|
max-width: 480px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
p,
|
||||||
|
h1,
|
||||||
|
li,
|
||||||
|
form {
|
||||||
|
margin: 1rem auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
form button {
|
||||||
|
grid-column: span 2;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small {
|
||||||
|
color: gray;
|
||||||
|
font-size: xx-small;
|
||||||
|
}
|
||||||
|
|
||||||
|
audio,
|
||||||
|
video {
|
||||||
|
width: 480px;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 480px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
::marker {
|
||||||
|
content: '💾 ';
|
||||||
|
}
|
125
server.js
125
server.js
@ -1,11 +1,124 @@
|
|||||||
|
require('dotenv').config()
|
||||||
|
const HOST = process.env.HOST || 'localhost'
|
||||||
|
const PORT = process.env.PORT || '3000'
|
||||||
|
const MONGODB_URL = process.env.MONGODB_URL || 'mongodb+srv://test:It6E1HC5p5K83bS8@test.ekqr7.azure.mongodb.net/test?retryWrites=true&w=majority'
|
||||||
|
const UPLOAD_SIZE_LIMIT = process.env.UPLOAD_SIZE_LIMIT || 102400
|
||||||
|
const UPLOAD_TEMP_PATH = process.env.UPLOAD_TEMP_PATH || '/tmp'
|
||||||
|
const UPLOAD_SAVE_PATH = process.env.UPLOAD_SAVE_PATH || 'upload'
|
||||||
|
|
||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
const md5file = require('md5-file')
|
||||||
|
const filesize = require('filesize')
|
||||||
|
const { hash, compare } = require('bcryptjs')
|
||||||
|
|
||||||
|
const mongoose = require('mongoose')
|
||||||
|
mongoose.connect(MONGODB_URL, { useNewUrlParser: true }).catch(error => console.error(error))
|
||||||
|
mongoose.connection.on('connected', () => console.info('mongodb is connected'))
|
||||||
|
mongoose.connection.on('error', (error) => console.error(error))
|
||||||
|
const Filesharing = require('./models/Filesharing')
|
||||||
|
|
||||||
|
const CronJob = require('cron').CronJob
|
||||||
|
const job = new CronJob(
|
||||||
|
'0 * * * * *',
|
||||||
|
async () => {
|
||||||
|
const files = await Filesharing.find({ createdAt: { $lt: new Date(Date.now() - 24 * 3600 * 1000) } })
|
||||||
|
for (let file of files) {
|
||||||
|
const id = file.id
|
||||||
|
await Filesharing.findByIdAndDelete(id)
|
||||||
|
console.log(`删除共享 ${id}`)
|
||||||
|
const ref_number = await Filesharing.countDocuments({ md5: file.md5 })
|
||||||
|
if (ref_number == 0) {
|
||||||
|
const file_path = path.join(__dirname, UPLOAD_SAVE_PATH, file.md5)
|
||||||
|
fs.unlinkSync(file_path)
|
||||||
|
console.log(`删除文件 ${file_path}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|
||||||
const express = require('express')
|
const express = require('express')
|
||||||
const app = express()
|
const app = express()
|
||||||
|
app.use(express.static('public'))
|
||||||
app.set('view engine', 'pug')
|
app.set('view engine', 'pug')
|
||||||
app.get('/', (req, res) => {
|
app.use(express.urlencoded({ extended: false }))
|
||||||
res.render('index', { title: 'hello, world!' })
|
app.use(express.json())
|
||||||
|
app.locals.moment = require('moment')
|
||||||
|
app.locals.moment.locale('zh-cn')
|
||||||
|
app.locals.filesize = filesize.partial({ base: 2, standard: 'jedec' })
|
||||||
|
app.get('/', async (req, res) => {
|
||||||
|
const files = await Filesharing.find().sort({ createdAt: -1 })
|
||||||
|
res.render('index', { title: '文件分享', files })
|
||||||
})
|
})
|
||||||
const host = 'localhost'
|
app.get('/upload', async (req, res) => {
|
||||||
const port = 3000
|
res.render('upload', { title: '文件上传' })
|
||||||
const server = app.listen(port, host, () => {
|
})
|
||||||
console.log(`server is running at http://${host}:${port}`)
|
|
||||||
|
const multer = require('multer')
|
||||||
|
const upload = multer({ dest: UPLOAD_TEMP_PATH, limits: { fileSize: UPLOAD_SIZE_LIMIT } })
|
||||||
|
app.post('/upload', upload.single('file'), async (req, res) => {
|
||||||
|
const file_temp_path = path.join(UPLOAD_TEMP_PATH, req.file.filename)
|
||||||
|
const md5 = await md5file(file_temp_path)
|
||||||
|
const file_save_path = path.join(__dirname, UPLOAD_SAVE_PATH, md5)
|
||||||
|
if (!fs.existsSync(file_save_path)) {
|
||||||
|
fs.cpSync(file_temp_path, file_save_path) // 复制临时文件到UPLOAD_PATH
|
||||||
|
}
|
||||||
|
fs.unlinkSync(file_temp_path) // 删除临时文件
|
||||||
|
// 写入数据库
|
||||||
|
const file = await Filesharing.create({
|
||||||
|
md5,
|
||||||
|
size: req.file.size,
|
||||||
|
encoding: req.file.encoding,
|
||||||
|
mimetype: req.file.mimetype,
|
||||||
|
// 解决了multer将中文文件名错误编码的问题
|
||||||
|
filename: Buffer.from(req.file.originalname, 'latin1').toString('utf8'),
|
||||||
|
password: req.body.password ? await hash(req.body.password, 16) : '',
|
||||||
|
})
|
||||||
|
console.log(`上传文件 ${file.filename}`)
|
||||||
|
res.status(201).render('upload', { title: '文件上传', file })
|
||||||
|
})
|
||||||
|
app.get('/download/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
if (!req.params.id.match(/^[0-9a-fA-F]{24}$/)) throw '格式错误的ObjectId'
|
||||||
|
const file = await Filesharing.findById(req.params.id)
|
||||||
|
if (!file) throw '试图下载不存在的文件'
|
||||||
|
res.render('download', { title: '文件下载', file })
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
res.sendStatus(404)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
app.post('/download/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
if (!req.params.id.match(/^[0-9a-fA-F]{24}$/)) throw '格式错误的ObjectId'
|
||||||
|
const file = await Filesharing.findById(req.params.id)
|
||||||
|
if (!file) throw '试图下载不存在的文件'
|
||||||
|
if (file.password == '' || (await compare(req.body.password, file.password)) == true) {
|
||||||
|
await Filesharing.findByIdAndUpdate(req.params.id, { $inc: { downloads: 1 } })
|
||||||
|
console.log(`下载文件 ${file.filename}`)
|
||||||
|
res.status(200).download(path.join(UPLOAD_SAVE_PATH, file.md5), file.filename)
|
||||||
|
} else {
|
||||||
|
res.sendStatus(401)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
res.sendStatus(404)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
app.get('/file/:md5', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const file = await Filesharing.findOne({ md5: req.params.md5 })
|
||||||
|
if (!file) throw '试图下载不存在的文件'
|
||||||
|
if (file.password != '') throw '访问的文件需要密码'
|
||||||
|
res.sendFile(path.join(__dirname, UPLOAD_SAVE_PATH, req.params.md5))
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
res.sendStatus(404)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.listen(PORT, HOST, () => {
|
||||||
|
console.info(`server is running at http://${HOST}:${PORT}`)
|
||||||
})
|
})
|
||||||
|
32
views/download.pug
Normal file
32
views/download.pug
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
extends layout
|
||||||
|
block main
|
||||||
|
p
|
||||||
|
a(href='/') 返回首页
|
||||||
|
form(method='POST')
|
||||||
|
label 文件
|
||||||
|
label= file.filename
|
||||||
|
label 类型
|
||||||
|
label= file.mimetype
|
||||||
|
label 大小
|
||||||
|
label= filesize(file.size)
|
||||||
|
label 上传
|
||||||
|
label= moment(file.createdAt).fromNow()
|
||||||
|
label 热度
|
||||||
|
label= file.downloads
|
||||||
|
| 次下载
|
||||||
|
if file.password
|
||||||
|
label(for='password_input') 密码
|
||||||
|
input(id='password_input' name='password' type='password' autocomplete='off' required)
|
||||||
|
button(id='download_button' type='submit') 下载
|
||||||
|
if !file.password
|
||||||
|
- const mimetype = file.mimetype.split('/')[0]
|
||||||
|
- const src = '/file/' + file.md5
|
||||||
|
if mimetype == 'image'
|
||||||
|
p 预览
|
||||||
|
img(src=src)
|
||||||
|
if mimetype == 'audio'
|
||||||
|
p 预览
|
||||||
|
audio(controls src=src)
|
||||||
|
if mimetype == 'video'
|
||||||
|
p 预览
|
||||||
|
video(controls src=src)
|
@ -1,6 +1,14 @@
|
|||||||
doctype html
|
extends layout
|
||||||
html(lang='zh')
|
block main
|
||||||
head
|
p
|
||||||
title #{title}
|
a(href='upload') 我要上传
|
||||||
body
|
ul
|
||||||
h1 #{title}
|
if files.length==0
|
||||||
|
li 暂无文件
|
||||||
|
else
|
||||||
|
for file in files
|
||||||
|
li
|
||||||
|
a(href=`download/${file.id}`)= file.filename
|
||||||
|
if file.password
|
||||||
|
span ㊙️
|
||||||
|
span.small (#{moment(file.createdAt).fromNow()}, #{file.downloads} 次下载)
|
||||||
|
13
views/layout.pug
Normal file
13
views/layout.pug
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
doctype html
|
||||||
|
html(lang='zh')
|
||||||
|
head
|
||||||
|
meta(charset='UTF-8')
|
||||||
|
meta(http-equiv='X-UA-Compatible' content='IE=edge')
|
||||||
|
meta(name='viewport' content='width=device-width, initial-scale=1.0')
|
||||||
|
link(rel='icon' href='/favicon.ico' type='image/x-icon')
|
||||||
|
link(rel='stylesheet' href='/style.css')
|
||||||
|
title #{title}
|
||||||
|
body
|
||||||
|
.container
|
||||||
|
h1 #{title}
|
||||||
|
block main
|
19
views/upload.pug
Normal file
19
views/upload.pug
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
extends layout
|
||||||
|
block main
|
||||||
|
p
|
||||||
|
a(href='/') 返回首页
|
||||||
|
if file
|
||||||
|
section
|
||||||
|
p
|
||||||
|
| 文件
|
||||||
|
var #{file.filename}
|
||||||
|
| 分享成功!
|
||||||
|
p 链接
|
||||||
|
- const url = 'download/' + file.id
|
||||||
|
a(href=url)= url
|
||||||
|
form(method='POST' action='upload' enctype='multipart/form-data')
|
||||||
|
label(for='file_input') 文件
|
||||||
|
input(id='file_input' name='file' type='file' required)
|
||||||
|
label(for='password_input') 密码
|
||||||
|
input(id='password_input' name='password' type='password' autocomplete='off')
|
||||||
|
button(id='share_button' type='submit') 分享
|
Loading…
Reference in New Issue
Block a user