Node.js搭建文件服務器,實現文件上傳下載編輯播放的功能

設計初衷:

上機實驗課的時候不想帶電腦去機房, 可是在機房又可能須要使用到本身電腦裏的文件, 再加上機房電腦沒有安裝網盤等應用, 每次上機都要下載登陸比較麻煩, 因此用Node.js搭建了這個文件服務器。javascript

技術棧:

jQuery + Node.js + formidablecss

該文件服務器有如下好處:

1.方便在機房、網吧等臨時使用的電腦上快速下載須要的文件, 只要打開瀏覽器就能夠下載事先上傳好的文件html

2.方便手機和電腦在不安裝其餘應用的狀況下傳輸文件, 由於手機和電腦都內置了瀏覽器前端

3.方便和別人共享文件, 不用擔憂限速java

4.能在大部分狀況下取代U盤, 帶着U盤怕掉, 用的時候還要佔用一個USB接口node

該文件服務器的主要功能:

1. 上傳文件:

多文件上傳
文件上傳大小限制
控制是否覆蓋同名文件
上傳等待提示git

2. 顯示文件:

顯示文件總數程序員

顯示文件名、文件大小、修改時間github

能夠按照文件名、文件大小、修改時間對文件排序顯示ajax

3. 下載文件

4. 刪除文件

5. 身份驗證

6. 權限控制:

身份驗證失敗或沒有驗證只能獲得public目錄下的文件, 即便有下載地址也沒法下載文件

7. 視頻、音樂在線播放

8. 圖片在線查看、文本在線編輯

9. 登陸信息(包含IP地址)保存到日誌文件

效果圖

身份驗證頁面:

首頁:

按文件大小從大到小排序顯示:

點擊「v.mp4」觀看視頻:

點擊「游鴻明 - 21我的.mp3」播放音樂:

點擊「文本文件.txt」在線編輯文件:

源代碼下載地址:

github.com/1061186575/…

運行方法

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函數會根據文件後綴判斷文件類型, 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;
        }
    })
}

複製代碼

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; } // 根據文件後綴判斷文件類型, 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)就可使用在線版的文件服務器了:

相關文章
相關標籤/搜索