nodejs文件服务器

nodejs环境配置

nodejs安装下载参照菜鸟教程nodejs

可以将node.exe添加到环境变量中,直接使用node命令。

js代码

'use strict';
var http = require('http');
var fs = require('fs');
var path = require('path');
var querystring = require('querystring');
var host = process.env.host || '0.0.0.0';
var port = process.env.PORT || 1337;
http.globalAgent.maxSockets = 50;


// content-encoding-gzip 压缩 .pipe(zlib.createGzip())
var zlib = require('zlib');

var downloadcount = 1;
var uploadcount = 1;


var server = http.createServer(function (req, res) {

    // 获取request的方式和后面的url
    var { method, url } = req;

    // 网页的请求,返回index.html
    if (method === 'GET' && url === '/') {

        // 内容类型是网页,把网页的流写到response中
        res.writeHead(200, { 'Content-Type': 'text/html;charset=UTF-8' });
        getHtml().pipe(res);
    }

    // 获取文件的请求,返回文件列表
    else if (method === 'GET' && url === '/getfile') {

        // 把文件列表用回车分隔返回,在控制台查看
        var result = fileList.join('\r\n')
        res.writeHead(200, { 'Content-Type': 'text/plain;charset=UTF-8' });
        res.end(result);
    }

    // 下载文件的请求GET
    else if (method === 'GET' && url != '/favicon.ico') {

        if (url.startsWith('/FileServer/uploads/')) {

            var fileName = url.slice('/FileServer/uploads/'.length).trim();

            // 判断一下文件名是不是空
            if (fileName != '') {

                // 拼接文件夹开始下载文件
                const filePath = path.join(__dirname, '/FileBase/', fileName);
                downloadFile(filePath, res);

                console.log(fileName + ' download' + "(" + downloadcount++ + ")");
            }
            else {

                // 文件名是空的,返回文件名不能为空的响应
                res.writeHead(404, { 'Content-Type': 'text/html' });
                res.end('file name is null');
            }
        }

    }

    // 上传文件的请求
    else if (method === 'POST' && url === '/FileServer/fileupload') {

        // 获取post的数据,然后返回
        getPostData(req).then((params) => {

            //console.log(params);
            console.log(JSON.stringify(params) + "(" + uploadcount++ + ")");

            res.writeHead(200, { 'Content-Type': 'application/json;charset=UTF-8' });
            res.end(JSON.stringify(params));

        })
    }

    // 下载文件的请求
    else if (method === 'POST' && url === '/download') {

        // 获取post的数据
        getPostData(req).then((params) => {

            // 判断是不是符合标准
            if (params && querystring.parse(params).type_id == 1) {

                // 获取要下载的文件名
                var fileName = querystring.parse(params).filename;

                // 判断一下文件名是不是空
                if (fileName.trim() != '') {

                    // 拼接文件夹开始下载文件
                    const filePath = path.join(__dirname, '/FileBase/', fileName);
                    downloadFile(filePath, res);
                }
                else {

                    // 文件名是空的,返回文件名不能为空的响应
                    res.writeHead(412, { 'Content-Type': 'text/html' });
                    res.end('file name is null');
                }
            }
        })
    }
});

// 开启服务端口
server.listen(port, host);

// 获取网页的可读流
const getHtml = () => {
    return fs.createReadStream(path.join(__dirname, 'index.html'));
}

// 通用代码,buffer流的split方法需要重定义
// spl表示分隔符
Buffer.prototype.split = Buffer.prototype.split || function (spl) {
    // 定义数组接收分隔出来的内容
    let arr = [];
    // 表示当前遍历的位置
    let cur = 0;
    // 用来接收spl分隔符索引到的位置
    let n = 0;
    // 如果索引值存在,即不为-1, 同时将索引到的位置赋值给n
    while ((n = this.indexOf(spl, cur)) != -1) {
        // 切割从当前位置到索引位置,再将该段字符串存到数组中
        arr.push(this.slice(cur, n));
        // 当前位置向后进行遍历,寻找下一个分隔符
        cur = n + spl.length
    }
    // 退出循环再来一手,要不输出少一个
    arr.push(this.slice(cur))
    return arr
}

// 文件处理 --现成代码
const fileHandler = (str, buffers, req) => {

    let boundary = '--' + str.split('=')[1];

    // 第一步 使用boundary切割,形式类似于 ------WebKitFormBoundary8QtZlZVe7Tqym8tG

    let result = buffers.split(boundary);

    // 第二步 丢弃首部的空元素和尾部的--元素

    result.shift();
    result.pop();

    // 第三步 现在只剩下两种形式
    /***********************************************************************************
    *                                                                                  *
    * \r\n数据描述\r\n\r\n数据值\r\n 和 \r\n数据描述1\r\n数据描述2\r\n\r\n文件内容\r\n *
    *                                                                                  *
    ***********************************************************************************/
    result = result.map(buffer => buffer.slice(2, buffer.length - 2))

    // 第四步 现在只剩下两种数据形式
    /********************************************************************
    *                                                                   *
    * 数据描述\r\n\r\n数据值 和 数据描述1\r\n数据描述2\r\n\r\n文件内容  *
    *                                                                   *
    * 继续使用\r\n\r\n进行切割                                          *
    ********************************************************************/

    result = result.map((buffer) => {
        return buffer.split('\r\n\r\n');
    })

    // 第五步 组合数据

    let obj = {};

    result.forEach((array) => {

        // 普通表单数据格式
        /****************************************************
        *   Content-Disposition: form-data; name="type_id"  *
        *   1                                               *
        ****************************************************/
        if (array[0].indexOf('\r\n') == -1) {

            // 普通表单数据

            const arr = array[0].toString().split(';');

            let key = arr[arr.length - 1].split('=')[1].toString();

            key = key.replace(/['"]/ig, '');

            obj[key] = array[1].toString();

        }
        else {

            // 文件数据
            /***************************************************************************************
            * Content-Disposition: form-data; name="photo"; filename="854138674122619089.jpg"      *
            * Content-Type: image/jpeg                                                             *
            *                                                                                      *
            * xxxxxxxxxxxxxx一堆二进制数据                                                         *
            ***************************************************************************************/

            // 承载二进制的文件数据
            const fileData = array[1];

            const arr = array[0].split('\r\n')[0].split(';');
            let filename = arr[arr.length - 1].split('=')[1].toString();
            filename = filename.replace(/['"]/ig, '');
            const filePath = `/FileBase/${filename}`;

            // TODO 对于文本文件存在\r\n的会进行切片,变成多个buffer,并且\r\n会不见,处理一手
            var ans = 2;
            if (array.length > ans) {
                array.forEach(item => {

                    if (ans == 2) {
                        ans--;
                    }
                    else if (ans == 1) {
                        fs.writeFileSync(path.join(__dirname, filePath), item + '\r\n\r\n', (err) => {
                            console.error(err);
                        })
                        ans--;
                    }
                    else {
                        // append 追加文件内容,需要同步写入
                        fs.writeFileSync(path.join(__dirname, filePath), item + '\r\n\r\n', {
                            flag: 'a', encoding: 'utf-8'
                        }, (err) => {
                            console.error(err);
                        })
                    }
                })
            }
            else {
                // writeFile 直接打开文件默认是 w 模式,所以如果文件存在,该方法写入的内容会覆盖旧的文件内容。
                fs.writeFile(path.join(__dirname, filePath), fileData, (err) => {
                    console.error(err);
                })
            }

            obj.fileUrl = filePath;

            obj.fileName = filename;

            // 上传文件之后更新文件列表
            if (!fileList.includes(filename)) {
                fileList.push(filename);
            }
        }

    })

    return obj;

}

// TODO 测试
/********************************
*   300并发                     *
*   网络环境不好测试并发        *
********************************/

// TODO 功能
/*
 * try catch
 * 并发
 * 文件夹
 */

// 获取post数据
const getPostData = (req) => {

    // 开启异步处理
    return new Promise((resolve) => {

        let chunk = [];

        // 当有数据流时加到数组中
        req.on('data', (buffer) => {
            chunk.push(buffer);
        })

        // 当数据完成时开始处理
        req.on('end', () => {

            const buffers = Buffer.concat(chunk);

            // 获取boundary分隔符,上传文件的时候在req的Content-Type中会带这个参数
            let boundary = req.headers['content-type'].split('; ')[1];

            // 触发文件下载
            if (boundary && boundary.includes('boundary')) {
                // 将文件上传解析完毕的参数赋值给boundary
                boundary = fileHandler(boundary, buffers, req);
            }
            else {
                boundary = null;
            }

            // 文件上传
            if (boundary) {

                resolve(boundary);

            }
            // 普通post请求,原样返回
            else {

                let data = buffers.toString();

                resolve(data);

            }
        })
    })
}

// 下载文件
const downloadFile = (pathUrl, res) => {

    // 判断一下文件是否存在
    // TODO 这里是异步调用,之后改改看
    fs.exists(pathUrl, function (exists) {

        // 存在文件,返回文件流
        if (exists) {
            try {

                // 创建可读流,获取文件信息并返回
                const readStream = fs.createReadStream(pathUrl);

                const stats = fs.statSync(pathUrl);

                const filename = path.basename(pathUrl);

                // 这里中文名的文件会字符异常,需要转ASCII编码
                // TODO 目前这样转码可以,之后改改看
                var filenameURI = encodeURI(filename);

                // 内容类型是二进制文件, 描述是附件需要下载
                res.writeHead(200, {
                    'Content-Type': 'application/octet-stream',
                    'Content-Disposition': 'attachment; filename=' + filenameURI,
                    'Content-Length': stats.size
                });

                readStream.pipe(res);
            }
            catch (ex) {
                console.error(ex);
            }
        }
        // 不存在文件,返回文件没找到的响应
        else {
            console.log('文件不存在');

            res.writeHead(404, { 'Content-Type': 'text/html' });
            res.end('file is not found');

        }
    });
}

// 异步下载文件测试
const downloadFileAsync = (pathUrl, res) => {


    return new Promise((resolve) => {
        // 判断一下文件是否存在
        // TODO 这里是异步调用,之后改改看
        fs.exists(pathUrl, function (exists) {

            // 存在文件,返回文件流
            if (exists) {
                try {

                    // 创建可读流,获取文件信息并返回
                    const readStream = fs.createReadStream(pathUrl);

                    const stats = fs.statSync(pathUrl);

                    const filename = path.basename(pathUrl);

                    // 这里中文名的文件会字符异常,需要转ASCII编码
                    // TODO 目前这样转码可以,之后改改看
                    var filenameURI = encodeURI(filename);

                    // 内容类型是二进制文件, 描述是附件需要下载
                    res.writeHead(200, {
                        'Content-Type': 'application/octet-stream',
                        'Content-Disposition': 'attachment; filename=' + filenameURI,
                        'Content-Length': stats.size
                    });

                    readStream.pipe(res);

                    resolve('ok');
                }
                catch (ex) {
                    console.error(ex);
                }
            }
            // 不存在文件,返回文件没找到的响应
            else {
                console.log('文件不存在');

                res.writeHead(404, { 'Content-Type': 'text/html' });
                res.end('file is not found');

                resolve('file is not found');
            }
        });
    });
}



// 文件列表
var fileList = [];

// 获取所有文件 
const getAllFile = () => {
    // 读文件夹并把所有文件写到文件列表中
    fs.readdir(path.join(__dirname, 'FileBase'), function (err, files) {
        if (err) {
            return console.error(err);
        }
        files.forEach(function (file) {
            fileList.push(file);
        })
    })
}

// 初始化文件列表
getAllFile();

html代码

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title>超过三百兆的文件最好压缩一手</title>
</head>
<body>

    <p>
        <input type="file" id="uploadfile" />
    </p>

    <p>
        <button id="upload">上传文件</button>
    </p>

    <p>
        <button id="getfile">获取文件</button>
        <input type="text" id="filename" />
    </p>

    <p>
        <button id="download">下载文件</button>
    </p>

    <p>
        <button id="getdownload">GET下载文件</button>
    </p>
</body>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script>
    // 注册按钮的点击事件
    $(document).ready(() => {

        // 下载文件按钮点击事件
        $("#download").on("click", () => {

            // POST传的参数
            var options = {
                url: '/download',
                data: { type_id: 1, filename: $("#filename")[0].value }
            };

            // 合并post方法和上面的参数
            var config = $.extend(true, { method: (options.method || 'post') }, options);

            // 网页创建一个提交的表单(如果post直接带上面参数就行)
            var $iframe = $('<iframe id="down-file-iframe" />');
            var $form = $('<form target="down-file-iframe" method="' + config.method + '" />');
            $form.attr('action', config.url);
            for (var key in config.data) {
                $form.append('<input type="hidden" name="' + key + '" value="' + config.data[key] + '" />');
            }
            $iframe.append($form);

            // 把表单加进来,提交表单,移除表单
            $(document.body).append($iframe);
            $form[0].submit();
            $iframe.remove();


            console.log("download");

        })

        // 上传文件按钮点击事件
        $("#upload").on("click", () => {

            // 提交的数据(文件就是选择的文件)
            var formData = new FormData();
            formData.append("file", $("#uploadfile")[0].files[0]);
            formData.append("type_id", 1);

            // 直接发送post请求
            $.ajax({
                url: '/FileServer/fileupload',
                type: 'post',
                data: formData,
                contentType: false,
                processData: false,
                success: function (res) {
                    console.log(res);
                }
            })


            console.log("upload");

        })

        // 获取文件按钮点击事件
        $("#getfile").on("click", () => {

            // 直接发送get请求
            $.ajax({
                url: '/getfile',
                type: 'get',
                contentType: false,
                processData: false,
                success: function (res) {
                    console.log(res);
                }
            })


            console.log("getfile");

        })


        // GET下载文件按钮点击事件
        $("#getdownload").on("click", () => {

            // 直接发送get请求
            $.ajax({
                url: '/FileServer/uploads/' + $("#filename")[0].value,
                type: 'get',
                contentType: false,
                processData: false,
                success: function (res) {
                    console.log(res);
                }
            })


            console.log("Getdownload");

        })
    })
</script>
</html>

源码下载

注意在同级目录新建一个FileBase文件夹

源码

引用

文件服务器git地址

猜你喜欢

转载自blog.csdn.net/qq_41863100/article/details/121742257