AndroidでHTTPのアップロードを使う

イメージ的には、

// ファイルを追加
MultipartEntity me = new MultipartEntity();
for (File file : files)
  me.AddFileEntry(new FileInputStream(file), file.getName());

// POSTリクエストの作成
HttpPost post = new HttpPost("http://server_to_upload_onto.example.com");
post.setEntity(me);

// 投げる
DefaultHttpClient client = new DefaultHttpClient();
HttpResponse res = client.execute(post);
int status = res.getStatusLine().getStatusCode();
...

ってやりたいんですが、ファイルのエントリを足す毎にメモリを消費されると、ファイルの個数が多くなったときにヤバいことになるので、なるべくメモリを使いたくない。でも、標準ではそんなHttpEntityはないし、外部ライブラリにも頼りたくない。

っていう感じで、さくっと書いてみました。

class MultipartEntity implements HttpEntity {
  
  String _boundary = UUID.randomUUID().toString();
  String _contentType = String.format("multipart/form-data; boundary=%s", _boundary);
  
  class FileEntry {
    public InputStream stream;
    public String fileName;
  }
  ArrayList<FileEntry> _entries = new ArrayList<FileEntry>();

  MultipartEntity() {
  }
  
  void AddFileEntry(InputStream stream, String fileName) {
    FileEntry fe = new FileEntry();
    fe.stream = stream;
    fe.fileName = fileName;
    _entries.add(fe);
  }
  
  void AddFileEntry(File file) throws FileNotFoundException {
    AddFileEntry(new FileInputStream(file), file.getName());
  }

  @Override
  public void consumeContent() throws IOException {
  }

  @Override
  public InputStream getContent() throws IOException,
      IllegalStateException {
    return new MultipartInputStream();
  }

  @Override
  public Header getContentEncoding() {
    return null;
  }

  @Override
  public long getContentLength() {
    return -1;
  }

  @Override
  public Header getContentType() {
    return new BasicHeader("Content-Type", _contentType);
  }

  @Override
  public boolean isChunked() {
    return false;
  }

  @Override
  public boolean isRepeatable() {
    return false;
  }

  @Override
  public boolean isStreaming() {
    return false;
  }

  @Override
  public void writeTo(OutputStream outstream) throws IOException {
    byte[] buf = new byte[1024 * 1024];
    InputStream is = getContent();
    for (; ;) {
      int ret = is.read(buf);
      if (ret < 0)
        break;
      outstream.write(buf);
    }
  }
  
  class MultipartInputStream extends InputStream {
    ArrayList<InputStream> _streams = new ArrayList<InputStream>();
    int _pos = 0;
    int _markPos = 0;
    
    MultipartInputStream() {
      int count = _entries.size();
      String hdr = "";
      for (int i = 0; i < count; i++) {
        hdr += String.format(
            "--%s\r\n" +
            "Content-Disposition: form-data; name=\"file-%d\"; filename=\"%s\"\r\n" +
            "Content-Type: application/octet-stream\r\n" +
            "Content-Transfer-Encoding: binary\r\n\r\n",
            _boundary, i, _entries.get(i).fileName);
        _streams.add(new ByteArrayInputStream(hdr.getBytes()));
        
        _streams.add(_entries.get(i).stream);
        
        hdr = String.format("\r\n--%s%s\r\n", _boundary, i + 1 == count ? "--" : "");
      }
      _streams.add(new ByteArrayInputStream(hdr.getBytes()));        
    }
    
    @Override
    public int read() throws IOException {
      for (; _pos < _streams.size(); _pos++) {
        int ret = _streams.get(_pos).read();
        if (ret >= 0)
          return ret;
      }
      return -1;
    }

    @Override
    public int available() throws IOException {
      for (; _pos < _streams.size(); _pos++) {
        int ret = _streams.get(_pos).available();
        if (ret > 0)
          return ret;
      }
      return 0;
    }

    @Override
    public void close() throws IOException {
      for (int i = 0; i < _streams.size(); i++) {
        _streams.get(i).close();
      }
    }

    @Override
    public void mark(int readlimit) {
      _markPos = _pos;
      _streams.get(_pos).mark(readlimit);
    }

    @Override
    public boolean markSupported() {
      for (int i = 0; i < _streams.size(); i++) {
        if (!_streams.get(_pos).markSupported())
          return false;
      }
      return true;
    }

    @Override
    public int read(byte[] buffer, int offset, int length)
        throws IOException {
      int all = 0;
      int ret = -1;
      for (; _pos < _streams.size(); _pos++) {
        for (;;) {
          ret = _streams.get(_pos).read(buffer, offset, length);
          Log.i("MultipartInputStream", String.format("Stream #%d: %d bytes (of %d bytes) to offset %d", _pos, ret, length, offset));
          if (ret <= 0)
            break;
          all += ret;
          offset += ret;
          length -= ret;
          if (length == 0) {
            Log.i("MultipartInputStream", String.format("Read: %d bytes", all));
            return all;
          }
        }
      }
      Log.i("MultipartInputStream", String.format("Read: %d bytes (ret=%d)", all, ret));
      return ret < 0 && all == 0 ? -1 : all;
    }

    @Override
    public int read(byte[] b) throws IOException {
      return read(b, 0, b.length);
    }

    @Override
    public synchronized void reset() throws IOException {
      _pos = _markPos;
      _streams.get(_pos).reset();
    }

    @Override
    public long skip(long byteCount) throws IOException {
      long allSkipped = 0;
      for (; _pos < _streams.size(); _pos++) {
        long skipped = _streams.get(_pos).skip(byteCount);
        allSkipped += skipped;
        byteCount -= skipped;
        if (byteCount == 0)
          break;
      }
      return allSkipped;
    }
  }
}