toxic-johann / toxic-johann.github.io

my blog
6 stars 0 forks source link

【2016-03-18】由webuploader android bug引发的问题 #22

Open toxic-johann opened 7 years ago

toxic-johann commented 7 years ago

这些天用webuploader实现一个手机上传图片的功能。做到一半发现,在电脑上是好的,在iOS上也是好的,但是在android上jpg图片会出错。

因为开始的时候报的是后端压缩错误,而我这边的压缩环境也布置的不好。一番捣鼓后,发觉还是不行。

仔细看报错,说我的文件是空文件,但是文件名是存在的。直到我用open命令试图打开这个“一直存在的图片文件”,才发现这个图片文件是空的。

那么bug就转到了前端上面去了。(啊,仔细去看报错啊,不要想当然。)

Google了一下发现是webuploader的固有bug。这个原因在webuploader的源代码上面也有标注。

if ( binary ) {
    // 强制设置成 content-type 为文件流。
    xhr.overrideMimeType &&
            xhr.overrideMimeType('application/octet-stream');

    // android直接发送blob会导致服务端接收到的是空文件。
    // bug详情。
    // https://code.google.com/p/android/issues/detail?id=39882
    // 所以先用fileReader读取出来再通过arraybuffer的方式发送。
    if ( Base.os.android ) {
        fr = new FileReader();

        fr.onload = function() {
            xhr.send( this.result );
            fr = fr.onload = null;
        };

        fr.readAsArrayBuffer( binary );
    } else {
        xhr.send( binary );
    }
} else {
    xhr.send( formData );
}

bug网址点击这里

根据网上解决方法,这种情况下,只需要将sendAsBinary这个条件设定为true,就可以避免这个bug了。

sendAsBinary {Object} [可选] [默认值:false] 是否已二进制的流的方式发送文件,这样整个上传内容php://input都为文件内容, 其他参数在$_GET数组中。

但是这样子又诱发出另一个问题了。因为这种方式下,我们收到的文件是在post域里的,post会进行一定处理。那么究竟是处理成什么样呢。我们不清楚。

如果我们只是简单地用self.post()方法取出来,会发现是一个已经包装过的对象了。那样子我们就不能确认这个代码究竟还是不是正确。而且如何取出来也是一个问题。

由于我们用thinkjs1.x版本,于是我们唯有探进去看。在core/http.js代码里面我们发现了这么一个处理方式。

/**
 * 普通的表单上传
 * @return {[type]} [description]
 */
_commonPost: function(){
  var buffers = [];
  var length = 0;
  var self = this;
  var deferred = getDefer();
  this.req.on('data', function(chunk){
    buffers.push(chunk);
    length += chunk.length;
  });
  this.req.on('end', function(){
    self.http.payload = Buffer.concat(buffers).toString();
    tag('form_parse', self.http).then(function(){
      //默认使用querystring.parse解析
      if (isEmpty(self.http.post) && self.http.payload) {
        self.http.post = self.filterQuery(querystring.parse(self.http.payload));
      }
      var post = self.http.post;
      var length = Object.keys(post).length;
      //最大表单数超过限制
      if (length > C('post_max_fields')) {
        self.res.statusCode = 413;
        self.http.end();
        return;
      }
      for(var name in post){
        //单个表单值长度超过限制
        if (post[name] && post[name].length > C('post_max_fields_size')) {
          self.res.statusCode = 413;
          self.http.end();
          return;
        }
      }
      deferred.resolve(self.http);
    })
  });
  return deferred.promise;
},

我们可以看到self.http.payload是刚拼接出来的chunk,而这个chunk这个时候存在来了,且以后也没有经过querystring之类的进行处理。我们假设这个可以相信的,那我们就进行处理吧。

结果这个时候我们发现无论处理成binary、base64还是utf-8格式,文件都已经损坏了。而且在代码比对上我们也发现的确有所不同。

这个时候实在无奈了,于是就修改了一下thinkjs源码进行尝试。

我把处理后的代码串的长度打出来进行比对。

console.log(length);
console.log(Buffer.concat(buffers).length)
console.log(Buffer.concat(buffers).toString().length)

结果发现第三个值教前两个值有所减小。于是上网查了下,发现由于编码问题,这些转码会容易造成数据丢失。

所以要么就是避免走过这里,要么就是更改这段代码。

最后还是找了老六看看有没有什么办法可以弥补。

最后发现thinkjs可以通过在头部打入一个标记标明上传的是文件,躲过这个处理。

post_ajax_filename_header: 'x-filename', //通过ajax上传文件时文件名对应的header,如果有这个header表示是文件上传

于是这个时候就折回去webuploader查看一下有没有方法可以修改header。

最后成功地发现了uploadBeforeSend事件

于是我们通过这个事件给请求打上header,从而走到正确的文件处理上。

if(type =="uploadBeforeSend"){
    var headers = arguments[3];
    headers["x-filename"] = arguments[2].name;
}

就此修复这个bug…………

花了一天…………醉了………………