首页 > 其他分享 >弄明白文件上传

弄明白文件上传

时间:2023-11-28 09:44:46浏览次数:35  
标签:function 文件 data upload 明白 file var 上传

先从一个例子开始,看一下掘金上传头像接口。

请求头:

掘金上传头像请求头

注意看图片中的content-type,后面会解释:

content-type: multipart/form-data; boundary=----WebKitFormBoundarycA7SgHXGF2nIiW3S

再看一下请求携带的参数(接口中还带了一大串查询参数,这不是重点,重点是form-data参数):
掘金上传头像form-data参数 1
掘金上传头像form-data参数 2

从上图中提炼出form-data参数的格式:

------WebKitFormBoundarycA7SgHXGF2nIiW3S
Content-Disposition: form-data; name="avatar"; filename="blob"
Content-Type: image/png

...图片数据
------WebKitFormBoundarycA7SgHXGF2nIiW3S--

即:

--分隔符换行符
Content-Disposition; name=参数名; filename=文件名 换行符
Content-Type换行符

参数名对应的参数值
--分隔符--换行符

这里的分隔符跟前面请求头中content-type后跟的boundary的值对应。

为什么会采用这种格式,因为这就是multipart/form-data标准,下面是标准中给出的例子:

6. Examples

   Suppose the server supplies the following HTML:

     <FORM ACTION="http://server.dom/cgi/handle"
           ENCTYPE="multipart/form-data"
           METHOD=POST>
     What is your name? <INPUT TYPE=TEXT NAME=submitter>
     What files are you sending? <INPUT TYPE=FILE NAME=pics>
     </FORM>

   and the user types "Joe Blow" in the name field, and selects a text
   file "file1.txt" for the answer to 'What files are you sending?'

   The client might send back the following data:

        Content-type: multipart/form-data, boundary=AaB03x

        --AaB03x
        content-disposition: form-data; name="field1"

        Joe Blow
        --AaB03x
        content-disposition: form-data; name="pics"; filename="file1.txt"
        Content-Type: text/plain

         ... contents of file1.txt ...
        --AaB03x--

   If the user also indicated an image file "file2.gif" for the answer
   to 'What files are you sending?', the client might client might send
   back the following data:

        Content-type: multipart/form-data, boundary=AaB03x

        --AaB03x
        content-disposition: form-data; name="field1"

        Joe Blow
        --AaB03x
        content-disposition: form-data; name="pics"
        Content-type: multipart/mixed, boundary=BbC04y

        --BbC04y
        Content-disposition: attachment; filename="file1.txt"
        Content-Type: text/plain

        ... contents of file1.txt ...
        --BbC04y
        Content-disposition: attachment; filename="file2.gif"
        Content-type: image/gif
        Content-Transfer-Encoding: binary

          ...contents of file2.gif...
        --BbC04y--
        --AaB03x--

multipart/form-data诞生的初衷就是为了满足文件上传需求,这在标准开篇也有说明,所以上传文件时,请求头中content-type首选是multipart/form-data

那如果不使用multipart/form-data能实现上传文件吗?答案是能。

简单点的你可以把文件转换为base64,以字符串的形式传给服务端,服务端拿到数据后再解码转换为文件。

复杂点的,你可以自己订一套标准,并实现它,用于文件上传。

文件上传实例项目概览

下面是例子的项目目录,用node做服务端。

代码目录:

|-- node_upload
    |-- index.js
    |-- back
    |-- front
    |-- upload

front文件夹下放前端页面,back文件夹下放接口代码,upload文件夹存放上传的文件。

index.js中,为了行文方便,提前把后面才用到的接口引入了,后面会实现这些接口:

var http = require("http"),
    url = require("url"),
    path = require("path"),
    fs = require("fs")
    port = process.argv[2] || 80;
// 接口
var upload = require('./back/upload');
var uploadBlob = require('./back/uploadBlob');

var mimeTypes = {
    "htm": "text/html",
    "html": "text/html",
    "jpeg": "image/jpeg",
    "jpg": "image/jpeg",
    "png": "image/png",
    "gif": "image/gif",
    "js": "text/javascript",
    "css": "text/css"
};

http.createServer(function(request, response) {

    // 接口逻辑
    if (request.url == '/upload' && request.method.toLowerCase() == 'post') {
        upload(request, response);
        return;
    }
    if (request.url == '/uploadBlob' && request.method.toLowerCase() == 'post') {
        uploadBlob(request, response);
        return;
    }

    /***以下代码是为了返回页面、静态资源 */
    var uri = url.parse(request.url).pathname
        // process.cwd() 当前工作目录
        , filename = path.join(process.cwd(), uri)
        , root = uri.split("/")[1];

    fs.exists(filename, function(exists) {
        if(!exists) {
            response.writeHead(404, {"Content-Type": "text/plain"});
            response.write("404 Not Found\n");
            response.end();
            console.error('404: ' + filename);
            return;
        }

        if (fs.statSync(filename).isDirectory()) filename += '/index.html';

        fs.readFile(filename, "binary", function(err, file) {
            if(err) {        
                response.writeHead(500, {"Content-Type": "text/plain"});
                response.write(err + "\n");
                response.end();
                console.error('500: ' + filename);
                return;
            }

            var mimeType = mimeTypes[path.extname(filename).split(".")[1]];
            response.writeHead(200, {"Content-Type": mimeType});
            response.write(file, "binary");
            response.end();
            console.log('200: ' + filename + ' as ' + mimeType);
        });
    });
}).listen(parseInt(port, 10));

console.log("Static file server running at\n  => http://localhost:" + port + "/\nCTRL + C to shutdown");

运行node index.js启动服务器,默认是监听80端口,如果80端口被占用,可以指定端口号启动,比如3000端口:node index.js 3000

不使用multipart/form-data上传文件

如果不使用multipart/form-data上传文件,那肯定不能使用表单的形式提交数据了,好在XMLHttpRequest.send()支持传入BlobArrayBuffer 等类型的数据,所以在例子中我们对可以把获取到的文件直接传给后端。

front文件夹下创建upload-ajax-blob.html文件,并添加如下代码:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8"/>
    <title>上传文件</title>
</head>
<body>
<input type="file" id="picker" />

<img id="image" />

<pre id="output"></pre>
<script>
    window.onload = function() {
        var output = document.getElementById("output");
        document.getElementById("picker").onchange = function () {
            var file = this.files[0];
            console.log(file);
            output.innerText = '';
            if(file.size > 1024*1024) {
                output.innerText = 'file size great than 1M';
                return;
            }
            var xhr = new XMLHttpRequest();
            xhr.responseType = 'json';
            xhr.onload = function(d) {
                output.innerText = JSON.stringify(xhr.response);
                document.getElementById("image").setAttribute('src',xhr.response.data);
            };
            xhr.onerror = function(err) {
                output.innerText = 'fail:' + err.toString();
            };
            xhr.open('POST', '/uploadBlob');
            xhr.setRequestHeader("Contnet-type", 'application/octet-stream');
            xhr.setRequestHeader("file-info", file.type + ';name=' + encodeURIComponent(file.name));
            xhr.send(file);
            // printFile(file,function(res) {
            //     xhr.send(res);
            // });
        };
    };
    // 测试传输ArrayBuffer
    function printFile(file,callback) {
        var reader = new FileReader();
        reader.onload = function (evt) {
            console.log(evt.target.result);
            callback(reader.result);
        };
        reader.readAsArrayBuffer(file);
    }

</script>

</body>
</html>

上面代码中接口地址是/uploadBlob,稍后会实现这个接口。

先看看Contnet-typeContnet-type的值设置为了application/octet-stream,表示请求主体是二进制数据。

另外代码中又设置了一个自定义请求头file-info,其值包含了上传文件的MIME类型和文件的名字。

接下来实现/uploadBlob接口。在back文件夹下创建uploadBlob.js文件,并添加以下代码:

const fs = require("fs");

function setResponseDate(res, data) {
    res.write(JSON.stringify(data));
}

function uploadBlob(req, res) {
    req.setEncoding("binary"); // 设置字符编码为二进制
    let fileInfo = req.headers["file-info"].split(';');
    let body = "";
    let fileName = decodeURIComponent(fileInfo[1].split('=')[1]);
    // 接收数据
    req.on("data", function(chunk) {
        body += chunk;
    });
    req.on("end", function() {
        const bufferData = Buffer.from(body, "binary");
        // 保存接收到的文件
        fs.writeFile(process.cwd() + '/upload/' + fileName, bufferData, function(err) {
            // res.end('sucess'),等价于res.write('sucess')+res.end()。
            var resData = {
                code: 200,
                msg: '',
                data: '/upload/' + fileName
            };
            resData.msg = 'success';
            if(err) {
                resData.msg = err.message;
                resData.code = 0;
                resData.data = null;
            }
            setResponseDate(res,resData);
            res.end();
        });
    });
}

module.exports = uploadBlob;

根据前端请求接口时设置的请求头,提取出上传文件的名字,然后保存文件到upload文件夹下。

下面是运行例子的结果:
非form表单上传 页面
文件上传时的请求头:
非form表单上传 请求报文
上传文件时的请求报文主体:
非form表单上传 请求报文主体
请求的响应:
非form表单上传 请求报文主体

例子中包含了上传ArrayBuffer数据的逻辑,可以试一下。

使用multipart/form-data上传文件

使用multipart/form-data上传文件时,可以直接使用form元素,也可以使用FormData

首先实现/upload接口。
back文件夹下创建upload.js文件,然后添加以下代码:

const fs = require("fs");

function setResponseDate(res, data) {
    res.write(JSON.stringify(data));
}
function parseFormData(dataBody,boundary) {
    var formList = dataBody.split('--' + boundary);
    var res = {};
    for (let i = 0; i < formList.length; i++) {
        var keyReg = /Content-Disposition: form-data;\s(name\=\"(\w+)\")/g
        var keyExecRes = keyReg.exec(formList[i]);

        if(keyExecRes) {
            res[keyExecRes[2]] = {
                content: formList[i]
            };
            var contentTypeReg = /Content-Type: ([a-zA-Z0-9_\-]+\/[a-zA-Z0-9_\-]+)\r\n\r\n/g;
            var fileNameReg = /Content-Disposition: form-data;\sname\=\"\w+\";\sfilename=\"([\w\.\-]+)\"/g;

            var fileNameExecRes = fileNameReg.exec(formList[i]);
            var contentTypeExecRes = contentTypeReg.exec(formList[i]);

            if(fileNameExecRes) { // 文件
                res[keyExecRes[2]]['filename'] = fileNameExecRes[1];
            }
            if(contentTypeExecRes) { // 文件媒体类型
                res[keyExecRes[2]]['contentType'] = contentTypeExecRes[1];
                res[keyExecRes[2]]['binaryStart'] = contentTypeReg.lastIndex;
            }
        }
    }

    return res;
}
function upload(req, res) {
    req.setEncoding("binary"); // 设置字符编码为二进制
    let body = "";
    // 边界字符
    // req.headers --- 请求头
    let boundary = req.headers["content-type"]
      .split("; ")[1]
      .replace("boundary=", "");

    // 接收数据
    req.on("data", function(chunk) {
        body += chunk;
    });
    req.on("end", function() {
        let contentType = "";
        let fileName = "";
        let fileExtension = "";
        let binary; // 文件数据
   
        var formData = parseFormData(body,boundary);
        for(let key in formData) {
            if(formData.hasOwnProperty(key)) {
                // file字段的值是文件
                if(key === 'file') {
                    fileName = formData[key]['filename'];
                    contentType = formData[key]['contentType'];
                    binary = formData[key]['content'].substring(formData[key]['binaryStart'], formData[key]['content'].length - 2);
                }
                // fileExtension 字段值是文件扩展名 (可选)
                if(key === 'fileExtension') {
                    let reg = /\r\n\r\n(\w+)\r\n/g
                    fileExtension = reg.exec(formData[key]['content'])[1];
                }
            }
        }
        if(fileExtension) {
            fileName = fileName + "." + fileExtension;
        }

        const bufferData = Buffer.from(binary, "binary");
        // 保存接收到的文件
        fs.writeFile(process.cwd() + '/upload/' + fileName, bufferData, function(err) {
            // res.end('sucess'),等价于res.write('sucess')+res.end()。
            var resData = {
                code: 200,
                msg: '',
                data: '/upload/' + fileName
            };
            resData.msg = 'success';
            if(err) {
                resData.msg = err.message;
                resData.code = 0;
                resData.data = null;
            }
            setResponseDate(res,resData);
            res.end();
        });
    });
}

module.exports = upload;

upload.js中根据multipart/form-data的格式对请求主体进行解析,获取到文件数据,然后把文件保存到upload文件夹。

使用form元素上传

front文件夹下创建upload-form.html里添加以下代码:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8"/>
    <title>上传文件</title>
</head>
<body>

<form action="/upload" enctype="multipart/form-data" method="post">
    <input type="file" name="file" /><br>
    <input type="submit" value="Upload" />
</form>
</body>
</html>

访问upload-form.html页面:
form表单上传 页面
文件上传时的请求头:
form表单上传 请求报文
上传文件时的请求报文主体(在chrome浏览器中看不到,在firefox中可以看到):
form表单上传 请求报文主体
下面是响应数据,图片上传成功了:
![form表单上传 请求报文主体]](/i/l/?n=23&i=blog/1005281/202311/1005281-20231128093542880-1505519899.png)

使用form表单上传就是这么简单,前端不需要写任何逻辑,前提是该设置的类型要设置对,并且后端要按对应类型的格式解析数据。

使用FormData上传

使用FormData上传时需要使用AJAX。不管是用jQueryaxiosXMLHttpRequestfetch,还是其他的请求库,重点不是各种请求库的api怎么使用,而是如何处理文件数据。

下面的例子中使用的是XMLHttpRequest

FormData的api文档可以看这里

这里主要用到append()方法。
FormData 接口的 append() 方法会添加一个新值到 FormData 对象内的一个已存在的键中,如果键不存在则会添加该键。

formData.append(name, value);
// 或者
formData.append(name, value, filename);

参数

  1. name value 中包含的数据对应的表单名称。
  2. value 表单的值。可以是USVString 或 Blob (包括子类型,如 File)。
  3. filename 可选 传给服务器的文件名称 (一个 USVString), 当一个 Blob 或 File 被作为第二个参数的时候, Blob 对象的默认文件名是 "blob"。 File 对象的默认文件名是该文件的名称。

注意 表单的字段名可以是name[]形式,为了与 PHP 数组命名习惯一致,方便PHP后端获取到数据后遍历。

主要看第二个参数,第二个参数支持BlobFileBlob子类),所以我们可以直接把input元素获取的文件添加到FormData,也可以通过Blob或者File创建一个文件添加到FormData

下面看例子。

例子1 直接把input元素获取的文件添加到FormData再上传。

front文件夹下创建upload-ajax-file.html里添加以下代码:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8"/>
    <title>上传文件</title>
</head>
<body>
<input type="file" id="picker" />

<img id="image" />

<pre id="output"></pre>
<script>
    window.onload = function() {
        var output = document.getElementById("output");
        document.getElementById("picker").onchange = function () {
            var file = this.files[0];
            output.innerText = '';
            if(file.size > 1024*1024) {
                output.innerText = 'file size great than 1M';
                return;
            }
            var xhr = new XMLHttpRequest();
            xhr.responseType = 'json';
            xhr.onload = function() {
                output.innerText = JSON.stringify(xhr.response);
                document.getElementById("image").setAttribute('src',xhr.response.data);
            };
            xhr.onerror = function(err) {
                output.innerText = 'fail:' + err.toString();
            };
            xhr.open('POST', '/upload');
            var fd = new window.FormData();
            // 跟后端约定 file字段的值是文件
            fd.append('file', file);
            xhr.send(fd);
        };

    };
</script>

</body>
</html>

访问upload-ajax-file.html页面:
AJAX file上传 请求报文主体
请求报文头部:
AJAX file上传 请求报文主体
请求报文主体:
AJAX file上传 请求报文主体
响应:
AJAX file上传 请求报文主体

例子2 使用Blob创建文件上传

front文件夹中创建upload-ajax-file-blob.html文件,并添加以下代码:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8"/>
    <title>上传文件</title>
</head>
<body>
<button id="blob-test">测试上传</button>

<pre id="output"></pre>
<script>
    window.onload = function() {
        var output = document.getElementById("output");
        document.getElementById("blob-test").addEventListener('click', function () {
            var file = new Blob(['<a id="a"><b id="b">name="rrrttt"</b></a>'],{
                type: 'text/html'
            });
            var fileExtension = 'html';
            output.innerText = '';
            if(file.size > 1024*1024) {
                output.innerText = 'file size great than 1M';
                return;
            }
            var xhr = new XMLHttpRequest();
            xhr.responseType = 'json';
            xhr.onload = function(d) {
                output.innerText = JSON.stringify(xhr.response);
            };
            xhr.onerror = function(err) {
                output.innerText = 'fail:' + err.toString();
            };
            xhr.open('POST', '/upload');
            var fd = new window.FormData();
            fd.append('file', file);
            fd.append('fileExtension', fileExtension);
            xhr.send(fd);
        });

    };
</script>

</body>
</html>

点击页面上的测试上传按钮,会使用Blob创建一个文件上传,并使用fileExtension告知服务端文件的扩展名。

访问页面:
AJAX file blob上传 请求报文主体
上传时的请求头:
AJAX file blob上传 请求报文主体
请求参数:
AJAX file blob上传 请求报文主体
响应:
AJAX file blob上传 请求报文主体

自己构造符合标准的请求报文上传文件

前面的例子都是通过浏览器上传文件,multipart/form-data格式的请求报文都是浏览器帮我们构建的,接下来看一个例子,不使用浏览器,通过node.js自己构造请求报文上传文件。

创建一个server_upload.js文件,添加以下代码:

const path = require("path");
const fs = require("fs");
const http = require("http");
// 定义一个分隔符,要确保唯一性
const boundaryKey = "-------------------------461591080941622511336662";
const request = http.request({
  method: "post",
  host: "localhost",
  port: "3000",
  path: "/upload",
  headers: {
    "Content-Type": "multipart/form-data; boundary=" + boundaryKey, // 在请求头上加上分隔符
    Connection: "keep-alive",
  },
});
// 写入内容头部
request.write(
  `--${boundaryKey}\r\nContent-Disposition: form-data; name="file"; filename="9.png"\r\nContent-Type: image/png\r\n\r\n`
);

// 写入内容
const fileStream = fs.createReadStream(path.join(__dirname, "../img/9.png"));
fileStream.pipe(request, { end: false });
fileStream.on("end", function() {
  // 写入尾部
  // request.end(data) 如果指定了 data,则相当于调用 request.write(data, encoding) 后跟 request.end(callback)。
  request.end("\r\n--" + boundaryKey + "--" + "\r\n");
});
request.on("response", function(res) {
    let body = '';
    res.on('data', function(chunk) {

        body += chunk;
    });
    res.on('end', function() {
        console.log(res.headers);
        console.log(body.toString());
    });
});

代码中用于上传的文件是写死的,需要在server_upload.js所在的目录下创建一个img文件夹,然后在该img文件夹下添加一张png格式的图片,并命名为9.png

运行node server_upload.js命令,可以在命令行工具中看到以下输出:

{
  date: 'Mon, 27 Nov 2023 09:03:43 GMT',
  connection: 'keep-alive',
  'keep-alive': 'timeout=5',
  'transfer-encoding': 'chunked'
}
{"code":200,"msg":"success","data":"/upload/9.png"}

参考资料

  1. 一文了解文件上传全过程(1.8w 字深度解析,进阶必备)
  2. https://www.w3.org/TR/html401/interact/forms.html#h-17.13.4
  3. Form-based File Upload in HTML
  4. https://github.com/ktont/javascript-file-upload
  5. 文件上传

标签:function,文件,data,upload,明白,file,var,上传
From: https://www.cnblogs.com/fogwind/p/17861160.html

相关文章

  • springboot实现文件上传下载
    1.用IDEA创建名叫springboot-file的SpringBoot项目,并将Packagename改为com.example.springboot,导入SpringWeb和thymeleaf依赖。(如果创建过程中遇到了问题,可以看我写的文章《IDEA中创建SpringBoot项目,并实现HelloWorld》中前三个步骤。)<dependency><groupId>org.springframew......
  • apache的文件工具类FileUtils
    org.apache.commons.io.FileUtils是apache提供用来操作文件的工具类,可以简化文件操作。<!--FileUtils--><dependency><groupId>commons-io</groupId><artifactId>commons-io</artifactId><version>2.4</version></dependen......
  • apache的文件名工具类FilenameUtils
    org.apache.commons.io.FilenameUtils。FileUtils和FilenameUtils分别是Apache对文件名和文件的封装,两者可以配合使用。<dependency><groupId>commons-io</groupId><artifactId>commons-io</artifactId><version>2.4</version></depe......
  • 百战商城项目---第11章 文件服务器 FastDFS 搭建
    1简介FastDFS是一个开源的轻量级分布式文件系统,它对文件进行管理,功能包括:文件存储、文件同步、文件访问(文件上传、文件下载)等,解决了大容量存储和负载均衡的问题。特别适合以文件为载体的在线服务,如相册网站、视频网站等等。FastDFS为互联网量身定制,充分考虑了冗余备份......
  • Linux基本命令之文件权限(一)
    一、文件权限文件的权限针对三类对象进行定义owner属主,缩写ugroup属组,缩写gother其他,缩写o每个文件针对每类访问者定义了三种主要权限r:Read读w:Write写x:eXecute执行另X:针对目录加执行权限,文件不加执行权限(因文件具备执行权限有安全隐患)注意:root账户不受文件权限的......
  • Linux文件权限
     权限的意义在于允许某一个用户或某个用户组以规定的方式去访问某个文件。基本权限UGO  首先介绍U、G、O这三个字母所代表的含义。   U:owne,属主   G:group,属组   O:other,其他用户  Linux系统通过U、G、O将用户分为三类,并对这三类用户分别设置三种......
  • 文件管理
    1.Linux文件系统结构: Linux文件系统采用层次化的树状结构,以根目录(/)为起点,包含了各级子目录和文件。 常见的目录包括/bin(存放二进制可执行文件)、/etc(存放系统配置文件)、/home(存放用户主目录)、/tmp(存放临时文件)、/usr(存放用户安装的程序和系统程序)、/root(超级......
  • 文件权限
    Linux文件权限与基本权限管理命令在Linux操作系统中,文件权限是系统安全性的重要组成部分。了解如何管理文件权限以及相关的命令对于系统管理员和用户来说至关重要。本文将深入探讨Linux文件权限的基本概念,并介绍一些常用的权限管理命令。文件权限基础1.文件权限表示方式Linu......
  • 文件管理
    探索Linux文件管理与Vim编辑器Linux操作系统以其强大的文件管理系统和灵活的命令行工具而闻名。在本文中,我们将深入了解Linux中的文件管理,并介绍一些常用的命令,以及强大的Vim编辑器的基本用法。Linux文件系统简介Linux文件系统是一个层次化的树状结构,类似于其他操作系......
  • 文件目录与权限
    基本概念​用户目录:位于/home/user,称之为用户工作目录或家目录,表示方式:#在home有一个user这里就是之前创建的msb123用户[root@localhost~]#cd/home[root@localhosthome]#lsmsb123#使用~回到root目录,使用/是回到根目录下[root@localhostmsb123]#cd~[root@localhos......