Oct 31

小记:纯js环境(如node/小程序)并没有文件系统可用时,怎么上传base64文件

Lrdcq , 2020/10/31 20:36 , 程序 , 閱讀(905) , Via 本站原創
遇到这个问题的场景是,在react-native容器里,我们可以通过react-native-canvas绘制图片,并且拿到图片的base64信息。但是怎么把这个base64上传到服务端呢。谷歌搜索一番,一般的做法是通过类似于react-native-fs储存到本地,然后通过给地址的方式从formdata上传文件。由于我们的react-native容器有一套几乎和微信小程序一样的请求api,我们也搜索了小程序一般上传base64文件的方式——通过filesystemmanager储存到本地,然后通过uploadfile桥上传。

不过目前我们的容器里没有任何可以base64写文件的方式(当然,临时引入发布太慢了),因此尝试想办法直接用普通的fetch/request桥api发送文件。
这个解法,在没有dom api,没有文件api的上下文中通用(只要提供了通用的网络请求api)。

解法

总的来说,通过request一个普通的post亲戚,但是构造出formdata的postbody即可:
const base64str = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAtUlEQVR4Ae2KhW2CARCFL52gQ3SPztJ1ukAjdXfFZQHcY2gEieBuj//hLnGgX3J3T05G/HN9cRZSBhtMWMYx6lTlj5d7rDv8l3Eq5bLZoPvD59vDyuEf/2UcAIe5bCas/n3H1/vjwmHPP/7LNACOUslE7vfrFd8fzzPDnD3/ZBEAjmPRcPP36wXTw5y9rALAScDvwd/323Domcu6tFqtU5fDCvXvB3jpZRMAHChzpUyTl152kw6KhRT+1/FGwgAAAABJRU5ErkJggg==";

wx.request({
  url: 'http://host/upload/picture', 
  method: 'POST',
  header: {
    'token':'aaaaa',
    'content-type':'multipart/form-data; boundary=marker'
  },

  data: '\r\n--marker' +
    '\r\ncontent-disposition: form-data; name="picture"; filename="test.jpg"' +
    '\r\ncontent-type: image/png' +
    '\r\ncontent-transfer-encoding: base64' + //base64不行
    '\r\n' +
    '\r\n' + base64str +
    '\r\n--marker--',
  success: (res) => {
    console.log(res)
  }
});

具体描述的话:

1. 关键点是content-transfer-encoding: base64,如果不写这一行的话,默认是纯二进制传输。但是啊。。js字符串是没有二进制字符串的(严格来说,根据https://developer.mozilla.org/en-US/docs/Web/API/DOMString/Binary,只有dom api中的btoa会生产出二进制字符串,所以web里面确实可以直接串二进制信息,但是小程序或者rn就不行了)。

当然,用base64的话传输流程会大1/3左右,只能忍了。

2. 如果要添加其他post参数,当然也要严格遵循multipart/form-data的格式,手动拼接body信息。要注意的是确定前后段落的content-transfer-encoding,毕竟文件部分指定的base64会对后问有影响。参考文档:https://datatracker.ietf.org/doc/html/rfc7578

尝试quoted-printable格式

实际完成如上代码后,发送到服务端居然报错了
java.io.IOException: Incomplete parts
  at org.eclipse.jetty.util.MultiPartInputStreamParser.parse(MultiPartInputStreamParser.java:871)
  at org.eclipse.jetty.util.MultiPartInputStreamParser.getParts(MultiPartInputStreamParser.java:492)
  at org.eclipse.jetty.server.MultiParts$MultiPartsUtilParser.getParts(MultiParts.java:110)
  at org.eclipse.jetty.server.Request.getParts(Request.java:2349)
  at org.eclipse.jetty.server.Request.extractContentParameters(Request.java:488)
  at org.eclipse.jetty.server.Request.getParameters(Request.java:387)
  at org.eclipse.jetty.server.Request.getParameter(Request.java:1041)

排查一通后,发现稍旧的版本的jetty处理formdata的base64传输方式时,如果包含非字符(二进制数据),有处理缺陷。
因此确认问题后,尝试将base64转换为别的格式传输。

content-transfer-encoding实际攻击支持5种传输格式:(https://www.w3.org/Protocols/rfc1341/5_Content-Transfer-Encoding.html)

1. binary:二进制数据,就是因为js不支持二进制字符串所以我们需要encode的。
2. 7bit / 8bit:说明传输数据是<128 or <256的字符数据,类似于charencode的描述,但是不处理数据,所以也不能用。
3. base64:当然,如上。
4. quoted-printable:(可打印字符引用编码)将非可展示字符([0-9a-zA-Z])转为为两位hex的形式的encode方式。看起来如下:
"=E5=9C=A8=E6=89=80=E6=9C=89=E9=82=AE=E4=BB=B6=E5=A4=84=E7=90=86=E7=9A=84=E5=90=84=E5=BC=8F=E5=90=84=E6=A0=B7=E7=9A=84=E7=BC=96=E7=A0=81=E4=B8=AD=EF=BC=8C=E5=BE=88=E5=A4=9A=E7=BC=96=E7=A0=81=E7=9A=84=E7=9B=AE=E7=9A=84=E9=83=BD=E6=98=AF=E9=80=9A=E8=BF=87=E7=BC=96=E7=A0=81=E6=89=8B=E6=AE=B5=E4=BD=BF=E5=BE=97=E4=B8=83=E4=BD=8D=E5=AD=97=E7=AC=A6=E7=9A=84=E9=82=AE=E4=BB=B6=E5=8D=8F=E8=AE=AE=E4=BD=93=E7=B3=BB=E5=8F=AF=E4=BB=A5=E4=BC=A0=E9=80=81=E5=85=AB=E4=BD=8D=E7=9A=84=E4=BA=8C=E8=BF=9B=E5=88=B6=E6=96=87=E4=BB=B6=E3=80=81=E5=8F=8C=E5=AD=97=E8=8A=82=E8=AF=AD=E8=A8=80=E6=96=87=E5=AD=97=E7=AD=89=E7=AD=89=E3=80=82=20Quoted-Printable=E4=B9=9F=E6=98=AF=E8=BF=99=E6=A0=B7=E4=B8=80=E4=BA=9B=E7=BC=96=E7=A0=81=E4=B8=AD=E7=9A=84=E4=B8=80=E4=B8=AA=EF=BC=8C=20=E5=AE=83=E7=9A=84=E7=9B=AE=E7=9A=84=E5=90=8C=E6=A0=B7=E6=98=AF=E5=B8=AE=E5=8A=A9=E9=9D=9EASCII=20=E7=BC=96=E7=A0=81=E7=9A=84=E4=BF=A1=E4=BB=B6=E4=BC=A0=E8=BE=93=E9=80=9A=E8=BF=87=20SMTP=E3=80=82Quoted-Printable=20=E7=BC=96=E7=A0=81=E6=98=AF=E5=AD=97=E7=AC=A6=E5=AF=B9=E5=BA=94=E7=9A=84=E7=BC=96=E7=A0=81=EF=BC=8C=E6=AF=8F=E4=B8=AA=E6=9C=AA=E7=BC=96=E7=A0=81=E7=9A=84=E4=BA=8C=E8=BF=9B=E5=88=B6=E5=AD=97=E7=AC=A6=E8=A2=AB=E7=BC=96=E7=A0=81=E6=88=90=E4=B8=89=E4=B8=AA=E5=AD=97=E7=AC=A6=EF=BC=8C=E5=8D=B3=E4=B8=80=E4=B8=AA=E7=AD=89=E5=8F=B7=E5=92=8C=E4=B8=80=E4=B8=AA=E5=8D=81=E5=85=AD=E8=BF=9B=E5=88=B6=E7=9A=84=E6=95=B0=E5=AD=97=EF=BC=8C=E5=A6=82=E2=80=9C=3DA8=E2=80=9D=E3=80=82";

因此除去base64,这就是我们最后的选择了,虽然传输效率极低(就算相对于base64),但是没有别的选择了。

同时基于开源代码,编写了base64直接转换为quoted-printable的code:
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
const toquotedprintable = (bs, bc) => {
  const code = (255 & bs >> (-2 * bc & 6));
  if ((code >=48 && code <=57) || (code >=65 && code <=90) || (code >=97 && code <=122)) {
    return String.fromCharCode(code);
  } else {
    return '=' + ('00' + code.toString(16)).slice(-2);
  }
};
const Base64 = {
  base64toquotedprintable: (input:string = '') => {
    let str = input.replace(/=+$/, '');
    let output = '';

    if (str.length % 4 == 1) {
      throw new Error("'base64toquotedprintable' failed: The string to be decoded is not correctly encoded.");
    }

    for (let bc = 0, bs = 0, buffer, i = 0;
      buffer = str.charAt(i++);

      ~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer,
        bc++ % 4) ? output += toquotedprintable(bs, bc) : 0
    ) {
      buffer = chars.indexOf(buffer);
    }
    return output;
  }
};

关键词:base64 , request
logo