用原生node寫一個檔案上傳的中介軟體

柯藍僧人發表於2019-04-10

直奔主題,就不墨跡了,檔案資料是post資料,html表單裡面post支援三種enctype,

用原生node寫一個檔案上傳的中介軟體
檔案上傳的那種是multipart/form-data。做來做個試驗。

const http = require('http');
const fs = require('fs');
let server = http.createServer((req,res) => {
  let str = '';
  req.on('data',(data) => {
    str+=data;
  });
  req.on('end',() => {
    console.log(str);
    res.end();
  })
})

server.listen(8080, () => {
  console.log(`伺服器已啟動,監聽在8080埠`);
});
複製程式碼

html

<form action="http://localhost:8080" enctype="multipart/form-data" method="post">
    <input type="text" name="user" id="">
    <input type="password" name="pass" id="">
    <input type="file" name="f1" id="">
    <input type="submit" value="提交">
  </form>
複製程式碼

用原生node寫一個檔案上傳的中介軟體

得到的結果是這樣的

用原生node寫一個檔案上傳的中介軟體
這裡的html檔案裡的內容沒有都截出來。所以看起來有點奇怪,我們來分析一下這段資料,顯然------WebKitFormBoundaryDqs4grRjEIPy5ZmY這個東東是分隔符。把這個東西稱為分隔符沒毛病吧,然後資料就變成了

分隔符
Content-Disposition: form-data; name="user"

chen
分隔符
Content-Disposition: form-data; name="pass"

123321`1
分隔符
Content-Disposition: form-data; name="f1"; filename="test.html"
Content-Type: text/html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
複製程式碼

然後把換行換成\r\n,資料就變成了這樣

<分隔符>\r\n
Content-Disposition: form-data; name="user"\r\n
\r\n
chen\r\n
<分隔符>\r\n
Content-Disposition: form-data; name="pass"\r\n
\r\n
123321`1\r\n
<分隔符>\r\n
Content-Disposition: form-data; name="f1"; filename="test.html"\r\n
Content-Type: text/html\r\n
\r\n
<檔案內容>\r\n
<分隔符>--
複製程式碼

然後把Content-Disposition: form-data;用資料型別描述換一下,為了方便分析資料,把它對應的值換成資料值。

<分隔符>\r\n資料描述\r\n\r\n資料值\r\n
<分隔符>\r\n資料描述\r\n\r\n資料值\r\n
<分隔符>\r\n資料描述1\r\n資料描述2\r\n\r\n<檔案內容>\r\n
<分隔符>--
複製程式碼

這樣下來,就方便解析資料了,把他們先用分隔符切一下,就變成了,

  空,
  \r\n資料描述\r\n\r\n資料值\r\n,
  \r\n資料描述\r\n\r\n資料值\r\n,
  \r\n資料描述1\r\n資料描述2\r\n\r\n<檔案內容>\r\n,
  --\r\n
]

這裡的空,可能有些疑惑,不過你反應5秒鐘應該就不疑惑了。空和最後一個資料沒有價值,
於是我們把他們去掉,於是資料就變成了這樣
[
  \r\n資料描述\r\n\r\n資料值\r\n,
  \r\n資料描述\r\n\r\n資料值\r\n,
  \r\n資料描述1\r\n資料描述2\r\n\r\n<檔案內容>\r\n,
]
複製程式碼

繼續,然後觀察一下這個資料,每行頭和尾都有一個\r\n,把這個也去掉,資料就變成了這樣

  資料描述\r\n\r\n資料值,
  資料描述\r\n\r\n資料值,
  資料描述1\r\n資料描述2\r\n\r\n<檔案內容>,
]

複製程式碼

然後再用\r\n\r\n對資料在切一次,就變成了這樣

普通資料:[資料描述, 資料值]
  或
  檔案資料:[資料描述1\r\n資料描述2, <檔案內容>]
複製程式碼

到這就可以動手寫程式碼了,

const http=require('http');
const fs=require('fs');
const uuid=require('uuid/v4');
Buffer.prototype.split=Buffer.prototype.split||function (b){
  let arr=[];

  let cur=0;
  let n=0;
  while((n=this.indexOf(b, cur))!=-1){
    arr.push(this.slice(cur, n));
    cur=n+b.length;
  }

  arr.push(this.slice(cur));

  return arr;
};
let server=http.createServer((req, res)=>{
  let arr=[];

  req.on('data', data=>{
    arr.push(data);
  });
  req.on('end', ()=>{
    let data=Buffer.concat(arr);
    //data
    //解析二進位制檔案上傳資料
    let post={};
    let files={};
    if(req.headers['content-type']){
      let str=req.headers['content-type'].split('; ')[1];
      if(str){
        let boundary='--'+str.split('=')[1];

        //1.用"分隔符切分整個資料"
        let arr=data.split(boundary);

        //2.丟棄頭尾兩個資料
        arr.shift();
        arr.pop();

        //3.丟棄掉每個資料頭尾的"\r\n"
        arr=arr.map(buffer=>buffer.slice(2,buffer.length-2));

        //4.每個資料在第一個"\r\n\r\n"處切成兩半
        arr.forEach(buffer=>{
          let n=buffer.indexOf('\r\n\r\n');

          let disposition=buffer.slice(0, n);
          let content=buffer.slice(n+4);

          disposition=disposition.toString();

          if(disposition.indexOf('\r\n')==-1){
            //普通資料
            //Content-Disposition: form-data; name="user"
            content=content.toString();

            let name=disposition.split('; ')[1].split('=')[1];
            name=name.substring(1, name.length-1);

            post[name]=content;
          }else{
            //檔案資料
            /*Content-Disposition: form-data; name="f1"; filename="a.txt"\r\n
            Content-Type: text/plain*/
            let [line1, line2]=disposition.split('\r\n');
            let [,name,filename]=line1.split('; ');
            let type=line2.split(': ')[1];

            name=name.split('=')[1];
            name=name.substring(1,name.length-1);

            filename=filename.split('=')[1];
            filename=filename.substring(1,filename.length-1);

            let path=`upload/${uuid().replace(/\-/g, '')}`;

            fs.writeFile(path, content, err=>{
              if(err){
                console.log('檔案寫入失敗', err);
              }else{
                files[name]={filename, path, type};
                console.log(files);
              }
            });
          }
        });

        //5.完成
        console.log(post);
      }
    }
    res.end();
  });
});
server.listen(8080,() => {
  console.log('伺服器啟動在8080埠);
})


複製程式碼

到這就可以測試了。

靈感來源blue課堂。

相關文章