设计初衷:
上机实验课的时候不想带电脑去机房, 但是在机房又可能需要使用到自己电脑里的文件, 再加上机房电脑没有安装网盘等应用, 每次上机都要下载登录比较麻烦, 所以用Node.js搭建了这个文件服务器。
技术栈:
JavaScript + Node.js + formidable
该文件服务器有以下好处:
1.方便在机房、网吧等临时使用的电脑上快速下载需要的文件, 只要打开浏览器就可以下载事先上传好的文件
2.方便手机和电脑在不安装其他应用的情况下传输文件, 因为手机和电脑都内置了浏览器
3.方便和别人共享文件, 不用担心限速
4.能在大部分情况下取代U盘, 带着U盘怕掉, 用的时候还要占用一个USB接口
该文件服务器的主要功能:
1. 上传文件:
多文件上传
文件上传大小限制
控制是否覆盖同名文件
上传等待提示
2. 显示文件:
显示文件总数
显示文件名、文件大小、修改时间
可以按照文件名、文件大小、修改时间对文件排序显示
3. 下载文件
4. 删除文件
5. 身份验证
6. 权限控制:
身份验证失败或没有验证只能得到public目录下的文件, 即使有下载地址也无法下载文件
7. 视频、音乐在线播放
8. 图片在线查看、文本在线编辑
9. 登录信息(包含IP地址)保存到日志文件
重要提醒, 本项目已更新
本次更新新增功能:
文件夹上传/文件夹下载/查看文件夹内容
上传文件时以百分比显示文件的上传进度
移动文件/新建文件/新建文件夹
更友好的弹窗提示(layer.js)
保存和查看登录记录
可以开放一个公共文件夹用来分享文件给任意人查看下载
显示根目录到当前文件夹的路径信息, 点击路径中的文件夹可以跳转到该文件夹
(更新后下载地址不变)
效果图
身份验证页面:
首页:
按文件大小从大到小排序显示:
点击“v.mp4”观看视频:
点击“游鸿明 - 21个人.mp3”播放音乐:
点击“文本文件.txt”在线编辑文件:
源代码下载地址:
运行方法
1.安装Node.js
2.安装项目依赖(formidable)
进入到项目的根目录,输入:
npm install
3.运行项目
进入到项目的根目录,输入:
node app.js
项目目录结构:
文件服务器
|-- config
|-- config.js
|-- control
|-- control.js
|-- log
|-- login_info.log
|-- node_modules
|-- public
|-- css
|-- index.css
|-- js
|-- jq.js
|-- editText.html
|-- playMusic.html
|-- playVideo.html
|-- verify.html
|-- uploads
|-- app.js
|-- index.html
|-- package.json
代码文件
首先介绍config.js文件,项目的配置文件
const path = require('path');
// 登录系统的账号密码
const systemUser = "zp"
const systemPassword = "xxx";
// 运行端口
const port = 3000
// 保存上传文件的目录
const uploadDir = path.join(process.cwd(), 'uploads/')
// 保存登录信息的日志文件
const login_info_path = path.join(process.cwd(), "log", 'login_info.log')
module.exports = {
port,
systemUser,
systemPassword,
uploadDir,
login_info_path
}
control.js:负责具体功能的实现
该文件定义并导出了6个处理函数,函数体代码比较多,在这里先省略
const fs = require('fs');
const formidable = require('formidable')
const {
systemUser,
systemPassword,
uploadDir,
login_info_path
} = require('../config/config.js')
// 定义6个处理函数,功能分别如下:
// 验证账号密码, 验证成功则设置cookie
function identityVerify(req, res) {}
// cookie验证
function cookieVerify(cookies) {}
// 读取uploadDir目录下的文件信息并返回
function getAllFileInfo(req, res) {}
// 上传文件
function uploadFile(req, res) {}
// 删除文件
function deleteFile(req, res) {}
// 修改文本文件
function modifyTextFile(req, res) {}
// 导出这6个函数
module.exports = {
identityVerify,
cookieVerify,
getAllFileInfo,
uploadFile,
deleteFile,
modifyTextFile
}
app.js: 项目的入口文件
创建服务并监听端口,收到请求后就调用control.js下对应的函数处理请求
const http = require('http');
const path = require('path');
const fs = require('fs');
const {
port,
uploadDir
} = require('./config/config.js')
const {
identityVerify,
cookieVerify,
getAllFileInfo,
uploadFile,
deleteFile,
modifyTextFile
} = require('./control/control');
// 如果uploadDir目录不存在就创建目录
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir)
}
// 发送页面
function sendPage(res, path, statusCode = 200) {
res.writeHead(statusCode, { 'Content-Type': 'text/html;charset=UTF-8' });
fs.createReadStream(path).pipe(res)
}
// 文件不存在返回404
function handle404(res, fileDir) {
if (!fs.existsSync(fileDir)) {
res.writeHead(404, { 'content-type': 'text/html;charset=UTF-8' });
res.end("404, no such file or directory");
console.log("no such file or directory: ", fileDir);
return true; // 处理成功
}
return false
}
var server = http.createServer(function(req, res) {
let url = decodeURI(req.url);
console.log("url: ", url);
let method = req.method.toLowerCase()
let parameterPosition = url.indexOf('?')
if (parameterPosition > -1) {
url = url.slice(0, parameterPosition) // 去掉url中的参数部分
console.log("去掉参数后的url: ", url);
}
// 访问public接口时发送public目录下的文件, 不需要任何验证
if (/^\/public\//.test(url)) {
let fileDir = '.' + url;
if (!handle404(res, fileDir)) {
fs.createReadStream(fileDir).pipe(res)
}
return;
}
// 身份验证的接口
if (url === '/identityVerify' && method === 'post') {
identityVerify(req, res)
return;
}
// cookie验证, 如果验证不成功, 就只发送verify.html
if (!cookieVerify(req.headers.cookie)) {
sendPage(res, './public/verify.html', 400);
return;
}
if (url === '/' && method === 'get') {
sendPage(res, './index.html');
} else if (url === '/getAllFileInfo' && method === 'get') {
// 读取uploadDir目录下的文件信息并返回
getAllFileInfo(req, res)
} else if (url === '/uploadFile' && method === 'post') {
// 上传文件
uploadFile(req, res)
} else if (/^\/deleteFile?/.test(url) && method === 'get') {
// 删除文件
deleteFile(req, res)
} else if (/^\/modifyTextFile?/.test(url) && method === 'post') {
// 修改文本文件
modifyTextFile(req, res)
} else {
// 下载文件, 默认发送uploads目录下的文件
let fileDir = path.join(uploadDir, url);
if (!handle404(res, fileDir)) {
console.log("下载文件: ", fileDir);
fs.createReadStream(fileDir).pipe(res)
}
}
})
server.listen(port);
console.log('running port:', port)
// 异常处理
process.on("uncaughtException", function(err) {
if (err.code == 'ENOENT') {
console.log("no such file or directory: ", err.path);
} else {
console.log(err);
}
})
process.on("SIGINT", function() {
process.exit()
})
process.on("exit", function() {
console.log("exit");
})
package.json文件
{
"name": "zp_file_server",
"version": "1.0.0",
"devDependencies": {
"formidable": "^1.2.1"
}
}
login_info.log文件是自动生成的登录日志文件
登录信息会自动保存到这个文件,格式如下:
后端的Node.js文件介绍完了,接下来介绍前端的文件
index.html文件
html部分:
<form id="uploadFileForm" name="uploadFileForm">
<h3>
上传文件
<label for="allowCoverage" style="font-size: 16px;margin-right: 15px;">
<input type="checkbox" name="allowCoverage" value="true" id="allowCoverage">允许覆盖同名文件
</label>
<button type="button" class="btn btn-radius30" onclick="clearCookie()">退出</button>
</h3>
<div>
</div>
<input type="file" id="fileSelect" name="uploadFile" multiple>
<button type="button" id="uploadFileBtn" style="color: blue;">上传文件</button>
<div id="uploading" hidden style="margin-top: 10px;color: red;">文件上传中, 请稍等...</div>
</form>
<h3 style="margin-left: 15px;">文件列表(<span id="fileCount">0</span>个文件):</h3>
<table>
<thead>
<tr>
<th onclick="sort('src')">文件名</th>
<th onclick="sort('size')">文件大小</th>
<th onclick="sort('mtimeMs')">修改时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id='allFile'>
</tbody>
</table>
js部分:
先发送ajax获取文件信息,得到文件信息的数据后传递给renderFileList函数
let fileCount = document.getElementById('fileCount');
let AllFileData = [];
$.ajax({
url: '/getAllFileInfo',
type: 'get',
success(d) {
d = JSON.parse(d);
console.log("文件信息: ", d);
AllFileData = d;
// 显示文件总数
fileCount.textContent = AllFileData.length;
renderFileList(d)
},
error(err) {
alert(err);
console.log("err: ", err);
}
})
renderFileList函数的具体实现:
// 将数据拼接成HTML然后添加到页面上
function renderFileList(dataArray) {
var fileListHTML = '';
for (let item of dataArray) {
// href='${item.src}'
fileListHTML += `
<tr>
<td>
<a
title='${item.src}'
onclick="processingResource('${item.src}')">
${item.src}
</a>
</td>
<td>${fileSizeFormat(item.size)}</td>
<td>${modifyTimeFormat(item.mtimeMs)}</td>
<td>
<button data-fileName='${item.src}' class="btn-download">下载</button>
<button data-fileName='${item.src}' class="btn-delete">删除</button>
</td>
</tr>`
}
document.getElementById('allFile').innerHTML = fileListHTML;
}
renderFileList函数在拼接HTML的过程中还会调用fileSizeFormat函数和modifyTimeFormat函数,这两个函数分别对文件大小和文件修改时间进行格式化处理:
// 文件大小的格式, 为了程序可读性就直接写1024而不是1024的乘除结果(例如1024*1024可以写成1048576,运行会更快一点)
function fileSizeFormat(fileSize) {
fileSize = Number(fileSize);
if (fileSize < 1024) {
return fileSize.toFixed(2) + 'B'
} else if (fileSize < 1024 * 1024) {
return (fileSize / 1024).toFixed(2) + 'KB'
} else if (fileSize < 1024 * 1024 * 1024) {
return (fileSize / 1024 / 1024).toFixed(2) + 'MB'
} else {
return (fileSize / 1024 / 1024 / 1024).toFixed(2) + 'GB'
}
}
// 修改时间显示的格式
function modifyTimeFormat(mtimeMs) {
return new Date(mtimeMs).toLocaleString()
}
此时就可以得到文件列表信息了:
当点击文件名时会调用processingResource函数,processingResource函数会根据文件后缀判断文件类型, mp3、mp4类型的文件会打开播放页面, 常见的文本文件会打开编辑页面:
function processingResource(src) {
console.log(src);
let hasSuffix = src.lastIndexOf('.');
let suffix; // 文件后缀
if (hasSuffix > -1) {
suffix = src.slice(hasSuffix + 1).toLocaleLowerCase();
console.log(suffix);
}
switch (suffix) {
case "mp3":
window.open("./public/playMusic.html?" + src)
break;
case "mp4":
window.open("./public/playVideo.html?" + src)
break;
case "txt":
case "css":
case "js":
case "java":
window.open("./public/editText.html?" + src)
break;
default:
window.open(src)
break;
}
}
点击退出时清除cookie:
function clearCookie() {
let cookie = document.cookie;
let cookieArray = [];
if (cookie) {
cookieArray = cookie.split(';')
}
console.log(cookie);
console.log(cookieArray);
cookieArray.map(function(item) {
let itemCookie = item.trim().split('=');
const cookie_key = itemCookie[0];
const cookie_value = itemCookie[1];
document.cookie = cookie_key + '=0;expires=' + new Date(0).toUTCString()
})
location.reload();
}
文件名、文件大小、修改时间的排序处理:
// 标记当前是从小到大排序还是从大到小排序, 文件名、大小、修改时间都使用sort_flag标记, 懒得单独记录它们的状态了
let sort_flag = false;
// 排序
function sort(type) {
let data = JSON.parse(JSON.stringify(AllFileData)) // 深拷贝数组
sort_flag = !sort_flag;
// 选择排序
for (let i = 0; i < data.length - 1; i++) {
for (let j = i + 1; j < data.length; j++) {
if (sort_flag) {
if (data[j][type] > data[i][type]) {
swap(data, i, j)
}
} else {
if (data[j][type] < data[i][type]) {
swap(data, i, j)
}
}
}
// 更新文件列表
renderFileList(data)
}
}
// 两数交换
function swap(data, i, j) {
let temp;
temp = data[i].src;
data[i].src = data[j].src;
data[j].src = temp;
temp = data[i].size;
data[i].size = data[j].size;
data[j].size = temp;
temp = data[i].mtimeMs;
data[i].mtimeMs = data[j].mtimeMs;
data[j].mtimeMs = temp;
}
文件删除和下载操作
// 给所有按钮的父元素添加点击事件
allFile.onclick = function(e) {
if (e.target.tagName !== 'BUTTON') return;
var fileName = e.target.dataset.filename;
var className = e.target.className;
// 删除
if (className.includes('btn-delete')) {
if (!confirm("确认删除?")) return;
$.ajax({
url: '/deleteFile?' + fileName,
type: 'get',
success(d) {
console.log("d: ", d);
if (d) {
alert(d);
} else {
e.target.parentElement.parentElement.remove();
fileCount.textContent = ~~fileCount.textContent - 1; // 页面显示的文件总数减一
}
},
error(err) {
console.log("err: ", err);
}
})
} else if (className.includes('btn-download')) { // 下载
let a_tag = document.createElement('a')
a_tag.href = fileName;
a_tag.download = fileName;
a_tag.click()
a_tag = null
}
}
文件上传
// 前端限制上传的文件最大为1GB, 程序员可以在浏览器控制台修改maxFileSize的值用来上传更大的文件, 后端限制最大10GB
let maxFileSize = 1 * 1024 * 1024 * 1024;
uploadFileBtn.onclick = function() {
if (!fileSelect.files.length) {
alert('请先选择文件')
return;
}
let allUploadFileSize = 0;
Array.from(fileSelect.files).forEach(file => {
allUploadFileSize += file.size;
})
if (allUploadFileSize > maxFileSize) {
alert('文件总大小大于1GB, 无法上传')
return;
}
if (allUploadFileSize > 10 * 1024 * 1024 * 1024) { // 后端限制文件最大为10GB
alert('文件总大小大于10GB, 前端改代码也无法上传...')
return;
}
// 防止快速连续多次点击导致重复上传
uploadFileBtn.disabled = true;
uploading.hidden = false;
var fd = new FormData(document.forms['uploadFileForm']);
$.ajax({
url: '/uploadFile',
type: 'post',
data: fd,
contentType: false, // 取消自动的设置请求头
processData: false, //取消自动格式化数据
enctype: 'multipart/form-data',
success(d) {
if (d) {
alert(d)
} else {
location.reload();
}
console.log("d: ", d);
},
error(err) {
console.log("err: ", err);
alert(err.responseText)
},
complete() {
uploadFileBtn.disabled = false;
uploading.hidden = true;
}
})
}
index.css文件
table td {
padding: 0 15px;
border: 1px solid #03a9f4;
}
table thead th {
user-select: none;
background: #eee;
cursor: pointer;
}
#allFile a {
text-decoration: none;
color: #03A9F4;
display: inline-block;
max-width: 300px;
line-height: 35px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
cursor: pointer;
}
#allFile a:hover {
text-decoration: underline;
color: #ff5722;
}
#allFile button {
display: inline-block;
width: 80px;
height: 30px;
line-height: 30px;
padding: 0 18px;
background-color: #009688;
color: #fff;
white-space: nowrap;
text-align: center;
font-size: 14px;
border: none;
cursor: pointer;
outline: none;
border-radius: 4px;
text-decoration: none;
}
#allFile button:hover {
opacity: .8;
filter: alpha(opacity=80);
color: #fff
}
#allFile button:active {
opacity: 1;
filter: alpha(opacity=100)
}
.btn-delete {
background-color: #cc614b!important;
}
.btn-radius30 {
border-radius: 30px!important;
}
.btn {
display: inline-block;
width: 80px;
height: 30px;
line-height: 30px;
padding: 0 18px;
background-color: #03A9F4;
color: #fff;
white-space: nowrap;
text-align: center;
font-size: 14px;
border: none;
cursor: pointer;
outline: none;
border-radius: 4px;
text-decoration: none;
box-shadow: 2px 2px 2px #FF9800;
}
#uploadFileForm {
width: 400px;
height: 110px;
border: 1px dashed red;
padding: 20px;
margin: 10px auto;
}
视频在线播放页面playVideo.html
<style>
.font-art {
text-shadow: 0 -1px 5px rgba(0, 0, 0, .4);
color: #60497C;
/* font-size: 2em; */
text-align: center;
font-weight: bold;
word-spacing: 20px;
margin-top: 10px;
cursor: pointer;
}
</style>
<h3 class="font-art" onclick="location.href='/'">视频无法播放请检查身份验证信息是否过期</h3>
<div style='width:90%;margin:auto;'>
<h3 id="fileName"></h3>
<video controls width='100%' autoplay id="playVideo"></video>
</div>
<script>
let src = location.search.slice(1);
playVideo.src = '/' + src;
fileName.textContent = decodeURI(src);
</script>
音乐在线播放页面和视频在线播放页面很像,playMusic.html:
<h3 class="font-art" onclick="location.href='/'">音乐无法播放请检查身份验证信息是否过期</h3>
<div style='width:90%;margin:auto;'>
<h3 id="fileName"></h3>
<audio controls width='100%' autoplay id="playMusic"></video>
</div>
<script>
let src = location.search.slice(1);
playMusic.src = '/' + src;
fileName.textContent = decodeURI(src);
</script>
文本在线编辑页面editText.html
先发起请求获取到某文本文件的内容,然后添加到textarea文本域里,用户修改textarea文本域里的内容,然后点击保存按钮,将新的文本信息发给后端,后端将新的文本信息写入到文本文件里,就实现了编辑功能。
editText.html主要代码:
<div style='width:90%;margin:auto;text-align: center;'>
<h3 id="showFileName"></h3>
<textarea name="" id="textareaEle" cols="30" rows="10"></textarea>
<div>
<button id="modifyBtn" class="btn">保存</button>
</div>
</div>
<script>
let src = location.search.slice(1);
var fileName = decodeURI(src);
showFileName.textContent = fileName;
$.ajax({
url: '/' + fileName,
type: 'get',
success(d) {
console.log("d: ", d);
textareaEle.value = d
},
error(err) {
console.log("err: ", err);
}
})
modifyBtn.onclick = function() {
$.ajax({
url: '/modifyTextFile?' + fileName,
type: 'post',
data: textareaEle.value,
success(d) {
console.log("d: ", d);
d = JSON.parse(d)
if (d.code === 0) {
} else {
}
alert(d.msg);
},
error(err) {
console.log("err: ", err);
}
})
}
</script>
文本编辑页面的保存功能对应的后端代码:
// 根据文件名和数据修改(覆盖)文本文件
function modifyTextFile(req, res) {
let url = decodeURI(req.url);
let fileName = url.slice(url.indexOf('?') + 1);
console.log("修改(覆盖)文本文件: ", fileName)
let WriteStream = fs.createWriteStream(uploadDir + fileName)
WriteStream.on('error', function(err) {
res.end(JSON.stringify({ code: 1, msg: JSON.stringify(err) }))
})
WriteStream.on('finish', function() {
res.end(JSON.stringify({ code: 0, msg: "保存成功" }))
})
req.on('data', function(data) {
WriteStream.write(data)
})
req.on('end', function() {
WriteStream.end()
WriteStream.close()
})
}
control.js文件的所有代码:
const fs = require('fs');
const formidable = require('formidable')
const {
systemUser,
systemPassword,
uploadDir,
login_info_path
} = require('../config/config.js')
const log = console.log;
let login_info_writeStream = fs.createWriteStream(login_info_path, { flags: 'a' })
//通过req的hearers来获取客户端ip
function getIp(req) {
let ip = req.headers['x-real-ip'] || req.headers['x-forwarded-for'] || req.connection.remoteAddres || req.socket.remoteAddress || '';
return ip;
}
// 验证账号密码, 验证成功则设置cookie, 验证结果写入到login_info.log日志文件里
function identityVerify(req, res) {
let clientIp = getIp(req);
console.log('客户端ip: ', clientIp);
let verify_str = ''
req.on('data', function(verify_data) {
verify_str += verify_data;
})
req.on('end', function() {
let verify_obj = {};
try {
verify_obj = JSON.parse(verify_str)
} catch (e) {
console.log(e);
}
log("verify_obj", verify_obj)
res.writeHead(200, {
'Content-Type': 'text/plain;charset=UTF-8'
});
// 保存登录信息日志
login_info_writeStream.write("Time: " + new Date().toLocaleString() + '\n')
login_info_writeStream.write("IP地址: " + clientIp + '\n')
if (verify_obj.user === systemUser && verify_obj.password === systemPassword) {
// 验证成功
log("验证成功")
login_info_writeStream.write("User: " + verify_obj.user + '\n验证成功\n\n')
// 设置cookie, 过期时间2小时
res.writeHead(200, {
'Set-Cookie': verify_obj.user + "=" + verify_obj.password + ";path=/;expires=" + new Date(Date.now() + 1000 * 60 * 60 * 2).toGMTString(),
});
res.end(JSON.stringify({ code: 0, msg: "验证成功" }));
} else {
// 验证失败
login_info_writeStream.write("User: " + verify_obj.user + "\t\t\t\tPassword: " + verify_obj.password + '\n验证失败\n\n')
res.end(JSON.stringify({ code: 1, msg: "验证失败" }));
}
})
}
// 把cookie拆分成数组
function cookiesSplitArray(cookies) {
// let cookies = req.headers.cookie;
let cookieArray = [];
if (cookies) {
cookieArray = cookies.split(';')
}
return cookieArray;
}
// 把单个cookie的键值拆开
function cookieSplitKeyValue(cookie) {
if (!cookie) return {};
let KeyValue = cookie.trim().split('=');
const cookie_key = KeyValue[0];
const cookie_value = KeyValue[1];
return { cookie_key, cookie_value }
}
// cookie验证
// 如果cookie中有一对键值等于系统登录的账号密码, 就认为验证成功(验证失败最多只能获得public目录下的文件)
function cookieVerify(cookies) {
const cookieArray = cookiesSplitArray(cookies)
// 新增的cookie一般在最后, 因此数组从后往前遍历
for (let index = cookieArray.length; index >= 0; index--) {
const item = cookieArray[index];
const { cookie_key, cookie_value } = cookieSplitKeyValue(item);
if (cookie_key === systemUser && cookie_value === systemPassword) {
return true;
}
}
return false;
}
// 读取uploadDir目录下的文件信息并返回
function getAllFileInfo(req, res) {
fs.readdir(uploadDir, (err, data) => {
// console.log(data);
let resultArray = [];
for (let d of data) {
let statSyncRes = fs.statSync(uploadDir + d);
// console.log("statSyncRes", statSyncRes)
resultArray.push({
src: d,
size: statSyncRes.size,
//mtimeMs: statSyncRes.mtimeMs, // 我发现有些电脑上的文件没有mtimeMs属性, 所以将mtime转成时间戳发过去
mtimeMs: new Date(statSyncRes.mtime).getTime()
})
}
// console.log(resultArray);
res.end(JSON.stringify(resultArray))
})
}
// 上传文件
function uploadFile(req, res) {
console.log("上传文件");
var form = new formidable.IncomingForm();
form.uploadDir = uploadDir; // 保存上传文件的目录
form.multiples = true; // 设置为多文件上传
form.keepExtensions = true; // 保持原有扩展名
form.maxFileSize = 10 * 1024 * 1024 * 1024; // 文件最大为10GB
// 文件大小超过限制会触发error事件
form.on("error", function(e) {
console.log("文件大小超过限制, error: ", e);
res.writeHead(400, { 'content-type': 'text/html;charset=UTF-8' });
res.end("文件大小超过10GB, 无法上传, 你难道不相信?")
})
form.parse(req, function(err, fields, files) {
if (err) {
console.log("err: ", err);
res.writeHead(500, { 'content-type': 'text/html;charset=UTF-8' });
res.end('上传文件失败: ' + JSON.stringify(err));
return;
}
// console.log(files);
// console.log(files.uploadFile);
if (!files.uploadFile) {
res.end('上传文件的name需要为uploadFile');
return
};
// 单文件上传时files.uploadFile为对象类型, 多文件上传时为数组类型,
// 单文件上传时也将files.uploadFile变成数组类型当做多文件上传处理;
if (Object.prototype.toString.call(files.uploadFile) === '[object Object]') {
files.uploadFile = [files.uploadFile];
// var fileName = files.uploadFile.name; // 单文件上传时直接.name就可以得到文件名
}
let err_msg = ''
for (let file of files.uploadFile) {
var fileName = file.name;
console.log("上传文件名: ", fileName);
var suffix = fileName.slice(fileName.lastIndexOf('.'));
var oldPath = file.path;
var newPath = uploadDir + fileName;
// log(oldPath)
// log(newPath)
// 如果不允许覆盖同名文件
if (fields.allowCoverage !== 'true') {
// 并且文件已经存在,那么在文件后面加上时间戳再加文件后缀
if (fs.existsSync(newPath)) {
newPath = newPath + '-' + Date.now() + suffix;
}
}
// 文件会被formidable自动保存, 而且文件名随机, 因此保存后需要改名
fs.rename(oldPath, newPath, function(err) {
if (err) {
console.log("err: ", err);
err_msg += JSON.stringify(err) + '\n';
}
})
}
//res.writeHead(200, { 'content-type': 'text/plain;charset=UTF-8' });
// res.writeHead(301, { 'Location': '/' });
res.end(err_msg);
});
}
// 根据文件名删除文件
function deleteFile(req, res) {
let url = decodeURI(req.url);
let fileName = url.slice(url.indexOf('?') + 1);
console.log("删除文件: ", fileName)
fs.unlink(uploadDir + fileName, (err) => {
if (err) {
console.log(err);
res.end('delete fail: ' + JSON.stringify(err));
return;
}
res.end();
});
}
// 根据文件名和数据修改(覆盖)文本文件
function modifyTextFile(req, res) {
let url = decodeURI(req.url);
let fileName = url.slice(url.indexOf('?') + 1);
console.log("修改(覆盖)文本文件: ", fileName)
let WriteStream = fs.createWriteStream(uploadDir + fileName)
WriteStream.on('error', function(err) {
res.end(JSON.stringify({ code: 1, msg: JSON.stringify(err) }))
})
WriteStream.on('finish', function() {
res.end(JSON.stringify({ code: 0, msg: "保存成功" }))
})
req.on('data', function(data) {
WriteStream.write(data)
})
req.on('end', function() {
WriteStream.end()
WriteStream.close()
})
}
module.exports = {
identityVerify,
cookieVerify,
getAllFileInfo,
uploadFile,
deleteFile,
modifyTextFile
}
index.html文件的所有代码:
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="shortcut icon" href="./public/favicon.ico" type="image/x-icon">
<title>zp文件服务器</title>
<script src="./public/js/jq.js"></script>
<link rel="stylesheet" href="./public/css/index.css">
</head>
<body>
<form id="uploadFileForm" name="uploadFileForm">
<h3>
上传文件
<label for="allowCoverage" style="font-size: 16px;margin-right: 15px;">
<input type="checkbox" name="allowCoverage" value="true" id="allowCoverage">允许覆盖同名文件
</label>
<button type="button" class="btn btn-radius30" onclick="clearCookie()">退出</button>
</h3>
<div>
</div>
<input type="file" id="fileSelect" name="uploadFile" multiple>
<button type="button" id="uploadFileBtn" style="color: blue;">上传文件</button>
<div id="uploading" hidden style="margin-top: 10px;color: red;">文件上传中, 请稍等...</div>
</form>
<h3 style="margin-left: 15px;">文件列表(<span id="fileCount">0</span>个文件):</h3>
<table>
<thead>
<tr>
<th onclick="sort('src')">文件名</th>
<th onclick="sort('size')">文件大小</th>
<th onclick="sort('mtimeMs')">修改时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id='allFile'>
</tbody>
</table>
<script>
// 前端限制上传的文件最大为1GB, 程序员可以在浏览器控制台修改maxFileSize的值用来上传更大的文件, 后端限制最大10GB
let maxFileSize = 1 * 1024 * 1024 * 1024;
let fileCount = document.getElementById('fileCount');
let AllFileData = [];
window.onload = function() {
// 获取文件信息
$.ajax({
url: '/getAllFileInfo',
type: 'get',
success(d) {
d = JSON.parse(d);
console.log("文件信息: ", d);
AllFileData = d;
// 显示文件总数
fileCount.textContent = AllFileData.length;
renderFileList(d)
},
error(err) {
alert(err);
console.log("err: ", err);
}
})
}
// 上传文件
uploadFileBtn.onclick = function() {
if (!fileSelect.files.length) {
alert('请先选择文件')
return;
}
let allUploadFileSize = 0;
Array.from(fileSelect.files).forEach(file => {
allUploadFileSize += file.size;
console.log(file.size)
})
if (allUploadFileSize > maxFileSize) {
alert('文件总大小大于1GB, 无法上传')
return;
}
if (allUploadFileSize > 10 * 1024 * 1024 * 1024) { // 后端限制文件最大为10GB
alert('文件总大小大于10GB, 前端改代码也无法上传...')
return;
}
// 防止快速连续多次点击导致重复上传
uploadFileBtn.disabled = true;
uploading.hidden = false;
var fd = new FormData(document.forms['uploadFileForm']);
$.ajax({
url: '/uploadFile',
type: 'post',
data: fd,
contentType: false, // 取消自动的设置请求头
processData: false, //取消自动格式化数据
enctype: 'multipart/form-data',
success(d) {
if (d) {
alert(d)
} else {
location.reload();
}
console.log("d: ", d);
},
error(err) {
console.log("err: ", err);
alert(err.responseText)
},
complete() {
uploadFileBtn.disabled = false;
uploading.hidden = true;
}
})
}
// 文件删除和下载
allFile.onclick = function(e) { // 给所有按钮的父元素添加点击事件
if (e.target.tagName !== 'BUTTON') return;
var fileName = e.target.dataset.filename;
var className = e.target.className;
// 删除
if (className.includes('btn-delete')) {
if (!confirm("确认删除?")) return;
$.ajax({
url: '/deleteFile?' + fileName,
type: 'get',
success(d) {
console.log("d: ", d);
if (d) {
alert(d);
} else {
e.target.parentElement.parentElement.remove();
fileCount.textContent = ~~fileCount.textContent - 1; // 页面显示的文件总数减一
}
},
error(err) {
console.log("err: ", err);
}
})
} else if (className.includes('btn-download')) { // 下载
let a_tag = document.createElement('a')
a_tag.href = fileName;
a_tag.download = fileName;
a_tag.click()
a_tag = null
}
}
function clearCookie() {
let cookie = document.cookie;
let cookieArray = [];
if (cookie) {
cookieArray = cookie.split(';')
}
console.log(cookie);
console.log(cookieArray);
cookieArray.map(function(item) {
let itemCookie = item.trim().split('=');
const cookie_key = itemCookie[0];
const cookie_value = itemCookie[1];
document.cookie = cookie_key + '=0;expires=' + new Date(0).toUTCString()
})
location.reload();
}
// 标记当前是从小到大排序还是从大到小排序, 文件名、大小、修改时间都使用sort_flag标记, 懒得单独记录它们的状态了
let sort_flag = false;
// 排序
function sort(type) {
let data = JSON.parse(JSON.stringify(AllFileData)) // 深拷贝数组
sort_flag = !sort_flag;
// 选择排序
for (let i = 0; i < data.length - 1; i++) {
for (let j = i + 1; j < data.length; j++) {
if (sort_flag) {
if (data[j][type] > data[i][type]) {
swap(data, i, j)
}
} else {
if (data[j][type] < data[i][type]) {
swap(data, i, j)
}
}
}
// 更新文件列表
renderFileList(data)
}
}
// 两数交换
function swap(data, i, j) {
let temp;
temp = data[i].src;
data[i].src = data[j].src;
data[j].src = temp;
temp = data[i].size;
data[i].size = data[j].size;
data[j].size = temp;
temp = data[i].mtimeMs;
data[i].mtimeMs = data[j].mtimeMs;
data[j].mtimeMs = temp;
}
// 将数据拼接成HTML然后添加到页面上
function renderFileList(dataArray) {
var fileListHTML = '';
for (let item of dataArray) {
// href='${item.src}'
fileListHTML += `
<tr>
<td>
<a
title='${item.src}'
onclick="processingResource('${item.src}')">
${item.src}
</a>
</td>
<td>${fileSizeFormat(item.size)}</td>
<td>${modifyTimeFormat(item.mtimeMs)}</td>
<td>
<button data-fileName='${item.src}' class="btn-download">下载</button>
<button data-fileName='${item.src}' class="btn-delete">删除</button>
</td>
</tr>`
}
document.getElementById('allFile').innerHTML = fileListHTML;
}
// 根据文件后缀判断文件类型, mp3、mp4类型的文件会打开播放页面, 常见的文本文件会打开编辑页面
function processingResource(src) {
console.log(src);
let hasSuffix = src.lastIndexOf('.');
let suffix; // 文件后缀
if (hasSuffix > -1) {
suffix = src.slice(hasSuffix + 1).toLocaleLowerCase();
console.log(suffix);
}
switch (suffix) {
case "mp3":
window.open("./public/playMusic.html?" + src)
break;
case "mp4":
window.open("./public/playVideo.html?" + src)
break;
case "txt":
case "css":
case "js":
case "java":
window.open("./public/editText.html?" + src)
break;
default:
window.open(src)
break;
}
}
// 文件大小的格式, 为了程序可读性就直接写1024而不是1024的乘除结果(例如1024*1024可以写成1048576,运行会更快一点)
function fileSizeFormat(fileSize) {
fileSize = Number(fileSize);
if (fileSize < 1024) {
return fileSize.toFixed(2) + 'B'
} else if (fileSize < 1024 * 1024) {
return (fileSize / 1024).toFixed(2) + 'KB'
} else if (fileSize < 1024 * 1024 * 1024) {
return (fileSize / 1024 / 1024).toFixed(2) + 'MB'
} else {
return (fileSize / 1024 / 1024 / 1024).toFixed(2) + 'GB'
}
}
// 修改时间显示的格式
function modifyTimeFormat(mtimeMs) {
return new Date(mtimeMs).toLocaleString()
}
</script>
</body>
</html>
项目部署至云服务器
1.购买云服务器,阿里云学生机只要9.5元/月:promotion.aliyun.com/ntms/act/ca…
2.在云服务器上安装Node.js
3.安装Forever
Forever可以守护Node.js应用,客户端断开的情况下,应用也能正常工作。
[sudo] npm install forever -g
4.安装项目依赖(formidable)
进入到项目的根目录,输入:
npm install
5.运行项目
进入到项目的根目录,输入:
forever start app.js
6.使用项目功能
接着打开浏览器输入云服务器的ip地址(或域名)+本项目的运行端口号(我的端口号是3008)就可以使用在线版的文件服务器了: