上機實驗課的時候不想帶電腦去機房, 可是在機房又可能須要使用到本身電腦裏的文件, 再加上機房電腦沒有安裝網盤等應用, 每次上機都要下載登陸比較麻煩, 因此用Node.js搭建了這個文件服務器。javascript
jQuery + Node.js + formidablecss
1.方便在機房、網吧等臨時使用的電腦上快速下載須要的文件, 只要打開瀏覽器就能夠下載事先上傳好的文件html
2.方便手機和電腦在不安裝其餘應用的狀況下傳輸文件, 由於手機和電腦都內置了瀏覽器前端
3.方便和別人共享文件, 不用擔憂限速java
4.能在大部分狀況下取代U盤, 帶着U盤怕掉, 用的時候還要佔用一個USB接口node
多文件上傳
文件上傳大小限制
控制是否覆蓋同名文件
上傳等待提示git
顯示文件總數程序員
顯示文件名、文件大小、修改時間github
能夠按照文件名、文件大小、修改時間對文件排序顯示ajax
身份驗證失敗或沒有驗證只能獲得public目錄下的文件, 即便有下載地址也沒法下載文件
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
複製代碼
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
}
複製代碼
該文件定義並導出了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
}
複製代碼
建立服務並監聽端口,收到請求後就調用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");
})
複製代碼
{
"name": "zp_file_server",
"version": "1.0.0",
"devDependencies": {
"formidable": "^1.2.1"
}
}
複製代碼
登陸信息會自動保存到這個文件,格式以下:
後端的Node.js文件介紹完了,接下來介紹前端的文件
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函數會根據文件後綴判斷文件類型, mp三、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;
}
})
}
複製代碼
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;
}
複製代碼
<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>
複製代碼
<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>
複製代碼
先發起請求獲取到某文本文件的內容,而後添加到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()
})
}
複製代碼
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
}
複製代碼
<!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; } // 根據文件後綴判斷文件類型, mp三、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)就可使用在線版的文件服務器了: