diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a8f07a00..cdeeb40a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ ## CHANGE LOG +### v6.0.1 +2014-04-03 issue [#40](https://github.com/qiniu/android-sdk/pull/40) + +- [#35] fix bugs and close idle connection +- [#36] 增加连接超时处理 + + ### v6.0.0 增加 SDK 实现规范 diff --git a/README.md b/README.md index d2377c9ea..cb40b8bef 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,8 @@ Qiniu Resource Storage SDK for Android ## 许可证 -Copyright (c) 2013 qiniu.com +Copyright (c) 2012-2014 qiniu.com 基于 MIT 协议发布: * [www.opensource.org/licenses/MIT](http://www.opensource.org/licenses/MIT) -* diff --git a/docs/README.md b/docs/README.md index 74d515a7b..1e7130168 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,168 +1,200 @@ --- -title: Android SDK 使用指南 +title: Android SDK使用文档 --- -- Android SDK 下载地址: -- Android SDK 源码地址: (请注意非 master 分支的代码在规格上可能承受变更) +# Android SDK使用文档 -此 Android SDK 基于 [七牛云存储官方API](http://docs.qiniu.com/api/index.html) 构建。在开发者的 Android App 工程项目中使用此 SDK 能够非常方便地将 Android 系统里边的文件快速直传到七牛云存储。 +## 目录 -出于安全考虑,使用此 SDK 无需设置密钥(AccessKey / SecretKey)。所有涉及到授权的操作,比如生成上传授权凭证(uploadToken)或下载授权凭证(downloadToken)均在业务服务器端进行。 +- [概述](#overview) +- [使用场景](#use-scenario) +- [接入SDK](#integration) +- [安全性](#security) +- [上传文件](#upload) + - [表单上传](#form-upload) + - [分片上传](#chunked-upload) + - [断点续上传](#resumable-upload) +- [下载文件](#download) +- [线程安全性](#thread-safety) -业务服务器负责生成和颁发授权,此 SDK 只负责实施具体的上传业务。 + +## 概述 -## 目录 +Android SDK只包含了最终用户使用场景中的必要功能。相比服务端SDK而言,客户端SDK不会包含对云存储服务的管理和配置功能。 -- [上传流程](#upload-flow) -- [下载流程](#download-flow) -- [接入SDK](#load) -- [使用SDK上传文件](#upload) -- [SDK 内置 demo 说明](#demo) -- [并发特性](#concurrency) -- [贡献代码](#contributing) -- [许可证](#license) +该SDK支持不低于2.2的Android版本(api8)。 - + +## 使用场景 -## 上传流程 +在使用Android SDK开发基于七牛云存储的应用之前,请理解正确的开发模型。客户端属于不可控的场景,恶意用户在拿到客户端后可能会对其进行反向工程,因此客户端程序中不可包含任何可能导致安全漏洞的业务逻辑和关键信息。 -1. 业务服务器使用七牛云存储服务端编程语言(如 PHP/Python/Ruby/Java)SDK 生成 uploadToken (上传授权凭证) +我们推荐的安全模型如下所示: -2. 客户端 Android 使用该 uploadToken 调用此 Android 封装的上传方法直传文件到七牛云存储 +![安全模型](http://developer.qiniu.com/docs/v6/api/overview/img/token.png) -3. 文件直传成功,七牛云存储向 uploadToken 生成之前所指定的业务服务器地址发起回调 +开发者需要合理划分客户端程序和业务服务器的职责范围。分发给最终用户的客户端程序中不应有需要使用管理凭证及SecretKey的场景。这些可能导致安全风险的使用场景均应被设计为在业务服务器上进行。 -4. 业务服务器接收来自七牛云存储回调的 POST 请求,处理相关 POST 参数,最后响应输出一段 JSON +更多的相关内容请查看[编程模型](http://developer.qiniu.com/docs/v6/api/overview/programming-model.html)和[安全机制](http://developer.qiniu.com/docs/v6/api/overview/security.html)。 -5. 七牛云存储接收业务服务器响应输出的这段 JSON,原封不动地通过 HTTP 返回给 Android 客户端程序 + +## 接入SDK +该SDK没有包含工程文件,这时需要自己新建一个工程,然后将src里面的代码复制到代码目录里面。 -注意事项: + +## 安全性 -- 此 Android SDK 当前只提供上传方法,即负责上述流程中的第2个步骤。 -- 业务服务器响应回调请求后输出 JSON,HTTP Headers 必须输出 `Content-Type` 为 `application/json`。 -- 文件上传成功后,业务服务器输出的 JSON 数据,可从所调用SDK上传代码的返回值中获取到。 +该SDK未包含凭证生成相关的功能。开发者对安全性的控制应遵循[安全机制](http://developer.qiniu.com/docs/v6/api/overview/security.html)中建议的做法,即客户端应向业务服务器请求上传和下载凭证,而不是直接在客户端使用AccessKey/SecretKey生成对应的凭证。在客户端使用SecretKey会导致严重的安全隐患。 +开发者可以在生成上传凭证前通过配置上传策略以控制上传的后续动作,比如在上传完成后通过回调机制通知业务服务器。该工作在业务服务器端进行,因此非本SDK的功能范畴。 - +完整的内容请参考[上传策略规格](http://developer.qiniu.com/docs/v6/api/reference/security/put-policy.html),[上传凭证规格](http://developer.qiniu.com/docs/v6/api/reference/security/upload-token.html),[下载凭证规格](http://developer.qiniu.com/docs/v6/api/reference/security/download-token.html)。关于上传后可以进行哪些后续动作,请查看[上传后续动作](http://developer.qiniu.com/docs/v6/api/overview/up/response/)。 -## 下载流程 + +## 上传文件 -此 Android SDK 没有提供下载文件的方法。所有上传到七牛云存储的文件,都能以如下方式进行访问: +开发者可以选择SDK提供的两种上传方式:表单上传和分片上传。表单上传使用一个HTTP POST请求完成文件的上传,因此比较适合较小的文件和较好的网络环境。相比而言,分片上传更能适应不稳定的网络环境,也比较适合上传比较大的文件(数百MB或更大)。 -公开资源: +若需深入了解上传方式之间的区别,请查看[上传类型](http://developer.qiniu.com/docs/v6/api/overview/up/upload-models.html#upload-types),[表单上传接口说明](http://developer.qiniu.com/docs/v6/api/overview/up/form-upload.html),[分片上传接口说明(断点续上传)](http://developer.qiniu.com/docs/v6/api/overview/up/chunked-upload.html)。 - http:/// + +### 表单上传 -私有资源: +开发者可以通过调用`IO.put()`方法来以表单形式上传一个文件。使用该方式时应确认相应的资源大小合适于使用单一HTTP请求即可上传。过大的文件在使用该方式上传时比较容易出现超时失败的问题。该方式比较适合用于上传经压缩的小图片和短音频等,不适合用于上传较大的视频(比如尺寸超过100MB的)。 - http:///?token= +该方法的详细说明如下: -其中\是bucket所对应的域名。七牛云存储为每一个bucket提供一个默认域名。默认域名可以到[七牛云存储开发者平台](https://portal.qiniu.com/)中,空间设置的域名设置一节查询。 +```java +public void put(String key, + InputStreamAt isa, + com.qiniu.io.PutExtra extra, + JSONObjectRet ret); +``` -出于安全考虑,此 SDK 不提供 `downloadToken` 的生成。除 Android / iOS SDK 以外,七牛云存储其他编程语言的 SDK 都有提供签发私有资源下载授权凭证(downloadToken)的实现。 +参数说明: -**注意: key必须采用utf8编码,如使用非utf8编码访问七牛云存储将反馈错误** +参数 | 类型 | 说明 +:---: | :----: | :--- +`key` | `String` | 将保存为的资源唯一标识。请参见[关键概念:键值对](http://developer.qiniu.com/docs/v6/api/overview/concepts.html#key-value)。 +`isa` | `InputStreamAt` | 待上传的本地文件。 +`extra` | [`PutExtra`](https://github.com/qiniu/android-sdk/blob/develop/src/com/qiniu/resumableio/PutExtra.java) | 上传参数。可以设置MIME类型等。 +`ret` | [`JSONObjectRet`](https://github.com/qiniu/android-sdk/blob/develop/src/com/qiniu/auth/JSONObjectRet.java) | 开发者需实现该接口以获取上传进度和上传结果。
若上传成功,该接口中的`onSuccess()`方法将被调用。否则`onFailure()`方法将被调用。 `onProgress()`会在文件上传量发生更改的时候被调用,而且处于MainThread环境之中,可以直接操作ProgressBar之类的进度提示控件。 - +开发者可以在调用方法前构造一个`PutExtra`对象,设置对应的上传参数以控制上传行为。可以设置的参数如下: -## 接入SDK +参数 | 类型 | 说明 +:---: | :----: | :--- +`mimeType` | `String` | 指定上传文件的MIME类型。如果未指定,服务端将做自动检测。一般情况下无需设置。 +`crc32` | `long` | 本文件的CRC校验码。服务端在上传完成后可以进行一次校验确认文件的完整性。 +`params` | `HashMap` | 可设置魔法变量和自定义变量。变量可帮助开发者快速的在客户端、业务服务器、云存储服务之间传递资源元信息。详见[变量](http://developer.qiniu.com/docs/v6/api/overview/up/response/vars.html)。 -本SDK的开发环境是 [Intellij IDEA](http://www.jetbrains.com/idea/),如果开发者使用的编辑器同为 IDEA, 直接打开项目即可,对于使用 [Eclipse](http://www.eclipse.org/) 编辑器的开发者,可以尝试导入项目。 +以下是一个关于`PutExtra`使用的示例: -导入后,填写相关必要参数即可运行SDK自带的 demo 程序,配置方法见 [SDK 内置 demo 说明](#demo) 。 +```java +extra.mimeType = "application/json"; // 强制设置MIME类型 +extra.params = new HashMap(); +extra.params.put("x:a", "bb"); // 设置一个自定义变量 +``` - +表单上传的示例代码请参见SDK示例中[`MyActivity.doUpload()`](https://github.com/qiniu/android-sdk/blob/develop/src/com/qiniu/demo/MyActivity.java)方法的实现。 -## 使用SDK上传文件 - -在 Android 中选择文件一般是通过 uri 作为路径, 一般调用以下代码 - -```{java} -// 在七牛绑定的对应bucket的域名. 默认是bucket.qiniudn.com -public static String bucketName = "bucketName"; -public static String domain = bucketName + ".qiniudn.com"; -// upToken 这里需要自行获取. SDK 将不实现获取过程. 当token过期后才再获取一遍 -public String UP_TOKEN = "token"; - -boolean uploading = false; -/** - * 普通上传文件 - * @param uri - */ -private void doUpload(Uri uri) { - if (uploading) { - hint.setText("上传中,请稍后"); - return; - } - uploading = true; - String key = IO.UNDEFINED_KEY; // 自动生成key - PutExtra extra = new PutExtra(); - extra.checkCrc = PutExtra.AUTO_CRC32; - extra.params.put("x:arg", "value"); - hint.setText("上传中"); - IO.putFile(this, UP_TOKEN, key, uri, extra, new JSONObjectRet() { - @Override - public void onSuccess(JSONObject resp) { - uploading = false; - String hash; - String value; - try { - hash = resp.getString("hash"); - value = resp.getString("x:arg"); - } catch (Exception ex) { - hint.setText(ex.getMessage()); - return; - } - String redirect = "http://" + domain + "/" + hash; - hint.setText("上传成功! " + hash); - Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(redirect)); - startActivity(intent); - } + +### 分片上传 - @Override - public void onFailure(Exception ex) { - uploading = false; - hint.setText("错误: " + ex.getMessage()); - } - }); -} -``` +顾名思义,分片上传会将一个文件划分为多个指定大小的数据块,分别上传。分片上传的关键价值在于可更好的适应不稳定的网络环境,以及成功上传超大的文件。分片上传功能也是实现断点续上传功能的基础。 +开发者可以通过调用`ResumableIO.put()`方法以分片形式上传一个文件。该方法签名和`IO.put()`一致。 - +分片上传的示例代码请参见SDK示例中[`MyResumableActivity.doResumableUpload()`](https://github.com/qiniu/android-sdk/blob/develop/src/com/qiniu/demo/MyResumableActivity.java)方法的实现。 -## SDK 内置 demo 说明 + +### 断点续上传 -注意:demo 程序无法直接运行,需要配置 `UpToken`, `BucketName`, `Domain`信息, 将其填写到 MyActivity 之中。`key`值可以在操作界面修改。当文件上传成功时,会试图跳转到浏览器访问已经上传的资源。如果失败,会toast提示。 +开发者可以基于分片上传机制实现断点续上传功能。 +```java +class ResumableIO { + public static void put(String key, + InputStreamAt isa, + com.qiniu.resumableio.PutExtra extra, + JSONObjectRet ret); +} +``` - +具体用法和`IO.put`的类似。 + +#### 续上传 +续上传的进度信息都储存在com.qiniu.resumableio.PutExtra. 所以当上传失败的时候,可以将PutExtra持久化下来,等到下一次上传的时候,再使用这个PutExtra,具体代码实现如下。 + +上传进度持久化: + +```java +final int PERSIST_PACE = 5; // 每5%进度持久化一次 +final PutExtra extra = new PutExtra(); +final String key = "key"; +final String filepath = "xx/xx/xx"; +// 准备上传 +db.execute("INSERT INTO `table_resumable_table` (`key`, `filepath`) VALUES ('" + key + "', '" + filepath + "')"); +ResumableIO.put(key, InputStreamAt.fromFile(new File(filepath)), extra, new JSONObjectRet() { + int process; + int lastPersistProcess = 0; + private void persist() { + // 持久化 + db.execute("UPDATE `table_resumable_table` SET extra='" + extra.toJSON() + "', process=" + process + " WHERE `key`='" + key + "' and `filepath`='" + filepath + "'"); + } + public void onSuccess(JSONObject obj) { + // 上传成功,删除记录 + db.execute("DELETE FROM `table_resumable_table` WHERE `key`='" + key + "' and `filepath`='" + filepath + "'"); + } + public void onProcess(int current, int total) { + process = current*100/total; + // 每特定进度持久化一次 + if (process - lastPersistProcess > PERSIST_PACE) { + persist(); + lastPersistProcess = process; + } + } + public void onFailure(Exception ex) { + // 忽略处理exception, + persist(); + } +}) +``` -## 并发特性 +恢复上传进度: -此 Android SDK 不是线程安全的,请勿在没有保护的情况下跨线程使用。 +```java +JSONObject ret = db.GetOne("SELECT * FROM `table_resumable_table` LIMIT 0, 1"); +PutExtra extra = new PutExtra(ret.optString("extraJson", "")); +String key = ret.optString("key", ""); +String filepath = ret.optString("filepath", ""); +// 实际情况中,很可能出现本地文件在续传时已被删除或者修改的情况,开发者应在恢复上传前先做相应的校验。 - +ResumableIO.put(key, InputStreamAt.fromFile(new File(filepath)), extra, new JSONObjectRet() {...}); +``` -## 贡献代码 + +### 上传中的并发性 -1. Fork -2. 创建您的特性分支 (`git checkout -b my-new-feature`) -3. 提交您的改动 (`git commit -am 'Added some feature'`) -4. 将您的修改记录提交到远程 `git` 仓库 (`git push origin my-new-feature`) -5. 然后到 github 网站的该 `git` 远程仓库的 `my-new-feature` 分支下发起 Pull Request +分片上传机制也提供了对一个文件并发上传的能力。 +目前本SDK的实现采用AsyncTask来进行异步操作,而Android系统底层默认是使用单线程来串行运行所有的AsyncTask。如果需要真正意义上的多线程上传,需要将AsyncTask放入线程池。详细操作请参考[这里](http://developer.android.com/reference/android/os/AsyncTask.html)。 - + +## 下载文件 -## 许可证 +该SDK并未提供下载文件相关的功能接口,因为文件下载是一个标准的HTTP GET过程。开发者只需理解资源URI的组成格式即可非常方便的构建资源URI,并在必要的时候加上下载凭证,即可使用HTTP GET请求获取相应资源。 -Copyright (c) 2013 www.qiniu.com +具体做法请参见[资源下载](http://developer.qiniu.com/docs/v6/api/overview/dn/download.html)和[资源下载的安全机制](http://developer.qiniu.com/docs/v6/api/overview/dn/security.html)。 -基于 MIT 协议发布: +从安全性和代码可维护性的角度考虑,我们建议下载URL的拼装过程也在业务服务器进行,让客户端从业务服务器请求。 -* [www.opensource.org/licenses/MIT](http://www.opensource.org/licenses/MIT) + +## 线程安全性 +Android 一般的情况下会使用一个主线程来控制UI,非主线程无法控制UI,在Android4.0+之后必须不能在主线程完成网络请求, +该SDK是根据以上的使用场景设计,所有网络的操作均使用AsyncTask异步运行,所有回调函数又都回到了主线程(`onSuccess()`, `onFailure()`, `onProgress()`),在回调函数内可以直接操作UI控件。 +如果您没有额外使用`new Thread()`等命令,该SDK将不会发生线程安全性问题。 diff --git a/src/com/qiniu/auth/Client.java b/src/com/qiniu/auth/Client.java index 03917eada..95f2a94da 100644 --- a/src/com/qiniu/auth/Client.java +++ b/src/com/qiniu/auth/Client.java @@ -22,6 +22,7 @@ import org.apache.http.util.EntityUtils; import java.io.IOException; +import java.util.concurrent.TimeUnit; public class Client { @@ -31,6 +32,11 @@ public Client(HttpClient client) { mClient = client; } + public void close() { + mClient.getConnectionManager().closeExpiredConnections(); + mClient.getConnectionManager().shutdown(); + } + public static ClientExecutor get(String url, CallRet ret) { Client client = Client.defaultClient(); return client.get(client.makeClientExecutor(), url, ret); @@ -77,6 +83,7 @@ protected HttpResponse roundtrip(HttpRequestBase httpRequest) throws IOException public class ClientExecutor extends AsyncTask implements ICancel { HttpRequestBase mHttpRequest; CallRet mRet; + boolean failed; public void setup(HttpRequestBase httpRequest, CallRet ret) { mHttpRequest = httpRequest; mRet = ret; @@ -90,22 +97,16 @@ protected Object doInBackground(Object... objects) { try { HttpResponse resp = roundtrip(mHttpRequest); int statusCode = resp.getStatusLine().getStatusCode(); - if (statusCode == 401) { // android 2.3 will not response - return new Exception("unauthorized!"); - } - byte[] data = EntityUtils.toByteArray(resp.getEntity()); + String xl = resp.getFirstHeader("X-Log").getValue(); - if (statusCode / 100 != 2) { - if (data.length == 0) { - String xlog = resp.getFirstHeader("X-Log").getValue(); - if (xlog.length() > 0) { - return new Exception(xlog); - } - return new Exception(resp.getStatusLine().getReasonPhrase()); - } - return new Exception(new String(data)); - } - return data; + if (statusCode == 401) return new Exception("unauthorized!"); // android 2.3 will not response + if (xl.contains("invalid BlockCtx")) return new Exception(xl); + + byte[] data = EntityUtils.toByteArray(resp.getEntity()); + if (statusCode / 100 == 2) return data; + if (data.length > 0) return new Exception(new String(data)); + if (xl.length() > 0) return new Exception(xl); + return new Exception(resp.getStatusLine().getStatusCode() + ":" + resp.getStatusLine().getReasonPhrase()); } catch (IOException e) { e.printStackTrace(); return e; @@ -114,17 +115,29 @@ protected Object doInBackground(Object... objects) { @Override protected void onProgressUpdate(Object... values) { + if (failed) return; + if (values.length == 1 && values[0] instanceof Exception) { + mRet.onFailure((Exception) values[0]); + failed = true; + return; + } mRet.onProcess((Long) values[0], (Long) values[1]); } @Override protected void onPostExecute(Object o) { + if (failed) return; if (o instanceof Exception) { mRet.onFailure((Exception) o); return; } mRet.onSuccess((byte[]) o); } + + public void onFailure(Exception ex) { + publishProgress(ex); + cancel(true); + } }; public static Client defaultClient() { diff --git a/src/com/qiniu/io/IO.java b/src/com/qiniu/io/IO.java index 134fe694c..506279dc4 100644 --- a/src/com/qiniu/io/IO.java +++ b/src/com/qiniu/io/IO.java @@ -12,6 +12,7 @@ import org.json.JSONObject; import java.io.File; +import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.util.Map; @@ -21,15 +22,21 @@ public class IO { public static String UNDEFINED_KEY = null; private static Client mClient; private static String mUptoken; + private static long mClientUseTime; public IO(Client client, String uptoken) { mClient = client; mUptoken = uptoken; } private static Client defaultClient() { + if (mClient != null && System.currentTimeMillis() - mClientUseTime > 3 * 60 * 1000) { // 1 minute + mClient.close(); + mClient = null; + } if (mClient == null) { mClient = Client.defaultClient(); } + mClientUseTime = System.currentTimeMillis(); return mClient; } @@ -44,9 +51,18 @@ private static Client defaultClient() { public void put(String key, InputStreamAt isa, PutExtra extra, JSONObjectRet ret) { MultipartEntity m = new MultipartEntity(); if (key != null) m.addField("key", key); - if (extra.checkCrc == PutExtra.AUTO_CRC32) extra.crc32 = isa.crc32(); + if (extra.checkCrc == PutExtra.AUTO_CRC32) { + try { + extra.crc32 = isa.crc32(); + } catch (IOException e) { + ret.onFailure(e); + return; + } + } if (extra.checkCrc != PutExtra.UNUSE_CRC32) m.addField("crc32", extra.crc32 + ""); - for (Map.Entry i: extra.params.entrySet()) m.addField(i.getKey(), i.getValue()); + for (Map.Entry i: extra.params.entrySet()) { + m.addField(i.getKey(), i.getValue()); + } m.addField("token", mUptoken); m.addFile("file", extra.mimeType, key == null ? "?" : key, isa); @@ -58,6 +74,11 @@ public void put(String key, InputStreamAt isa, PutExtra extra, JSONObjectRet ret public void onProcess(long current, long total) { executor.upload(current, total); } + + @Override + public void onFailure(Exception ex) { + executor.onFailure(ex); + } }); client.call(executor, Conf.UP_HOST, m, ret); } diff --git a/src/com/qiniu/resumableio/ResumableClient.java b/src/com/qiniu/resumableio/ResumableClient.java index 20cad3c77..688f9d0d2 100644 --- a/src/com/qiniu/resumableio/ResumableClient.java +++ b/src/com/qiniu/resumableio/ResumableClient.java @@ -47,14 +47,24 @@ public void onInit(int flag) { public void putInit() { int chunkSize = Math.min(writeNeed, CHUNK_SIZE); - crc32 = input.getCrc32(offset, chunkSize); + try { + crc32 = input.getCrc32(offset, chunkSize); + } catch (IOException e) { + onFailure(e); + return; + } canceler[0] = mkblk(input, offset, writeNeed, chunkSize, this); } public void putNext() { wrote = putRet.offset; int remainLength = Math.min((int) (input.length() - offset - putRet.offset), CHUNK_SIZE); - crc32 = input.getCrc32(offset+putRet.offset, remainLength); + try { + crc32 = input.getCrc32(offset+putRet.offset, remainLength); + } catch (IOException e) { + onFailure(e); + return; + } canceler[0] = bput(putRet.host, input, putRet.ctx, offset, putRet.offset, remainLength, this); } diff --git a/src/com/qiniu/resumableio/ResumableIO.java b/src/com/qiniu/resumableio/ResumableIO.java index 67ca5eec8..e9564e840 100644 --- a/src/com/qiniu/resumableio/ResumableIO.java +++ b/src/com/qiniu/resumableio/ResumableIO.java @@ -36,7 +36,7 @@ private synchronized void removeTask(Integer id) { idCancels.remove(id); } - public int putAndClose(final String key, final InputStreamAt input, final PutExtra extra, final JSONObjectRet ret) { + private int putAndClose(final String key, final InputStreamAt input, final PutExtra extra, final JSONObjectRet ret) { return put(key, input, extra, new JSONObjectRet() { @Override public void onSuccess(JSONObject obj) { @@ -121,6 +121,10 @@ public void onProcess(long current, long total) { @Override public void onFailure(Exception ex) { + if (failure[0]) { + ex.printStackTrace(); + return; + } if (--retryTime <= 0 || (ex.getMessage() != null && ex.getMessage().contains("Unauthorized"))) { removeTask(taskId); failure[0] = true; diff --git a/src/com/qiniu/utils/IOnProcess.java b/src/com/qiniu/utils/IOnProcess.java index 9d45a08f2..03d2206df 100644 --- a/src/com/qiniu/utils/IOnProcess.java +++ b/src/com/qiniu/utils/IOnProcess.java @@ -2,4 +2,5 @@ public interface IOnProcess { public void onProcess(long current, long total); + public void onFailure(Exception ex); } diff --git a/src/com/qiniu/utils/InputStreamAt.java b/src/com/qiniu/utils/InputStreamAt.java index ddac6314c..c4a1fc09a 100644 --- a/src/com/qiniu/utils/InputStreamAt.java +++ b/src/com/qiniu/utils/InputStreamAt.java @@ -7,7 +7,6 @@ import org.apache.http.entity.AbstractHttpEntity; import java.io.*; -import java.util.Arrays; import java.util.zip.CRC32; public class InputStreamAt implements Closeable { @@ -58,14 +57,14 @@ public InputStreamAt(byte[] data) { mData = data; } - public long getCrc32(long offset, int length) { + public long getCrc32(long offset, int length) throws IOException { CRC32 crc32 = new CRC32(); byte[] data = read(offset, length); crc32.update(data); return crc32.getValue(); } - public long crc32() { + public long crc32() throws IOException { if (mCrc32 >= 0) return mCrc32; CRC32 crc32 = new CRC32(); long index = 0; @@ -120,26 +119,23 @@ protected static File storeToFile(Context context, InputStream is) { } } - public byte[] read(long offset, int length) { - if (mClosed) return null; - try { - if (mFileStream != null) { - return fileStreamRead(offset, length); - } - if (mData != null) { - byte[] ret = new byte[length]; - System.arraycopy(mData, (int) offset, ret, 0, length); - return ret; - } - } catch (IOException e) { - e.printStackTrace(); + public byte[] read(long offset, int length) throws IOException { + if (mClosed) throw new IOException("inputStreamAt closed"); + if (mFileStream != null) { + return fileStreamRead(offset, length); } - - return null; + if (mData != null) { + byte[] ret = new byte[length]; + System.arraycopy(mData, (int) offset, ret, 0, length); + return ret; + } + throw new IOException("inputStreamAt not init"); } protected byte[] fileStreamRead(long offset, int length) throws IOException { if (mFileStream == null) return null; + long fileLength = mFileStream.length(); + if (length + offset > fileLength) length = (int) (fileLength - offset); byte[] data = new byte[length]; int read; @@ -147,18 +143,26 @@ protected byte[] fileStreamRead(long offset, int length) throws IOException { synchronized (data) { mFileStream.seek(offset); do { - read = mFileStream.read(data, totalRead, length); + read = mFileStream.read(data, totalRead, length - totalRead); if (read <= 0) break; totalRead += read; } while (length > totalRead); } if (totalRead != data.length) { - data = Arrays.copyOfRange(data, 0, totalRead); + data = copyOfRange(data, 0, totalRead); } return data; } + public static byte[] copyOfRange(byte[] original, int from, int to) { + int newLength = to - from; + if (newLength < 0) throw new IllegalArgumentException(from + " > " + to); + byte[] copy = new byte[newLength]; + System.arraycopy(original, from, copy, 0, Math.min(original.length - from, newLength)); + return copy; + } + @Override public synchronized void close(){ if (mClosed) return; diff --git a/src/com/qiniu/utils/MultipartEntity.java b/src/com/qiniu/utils/MultipartEntity.java index 93beb1eb6..dc88707d6 100644 --- a/src/com/qiniu/utils/MultipartEntity.java +++ b/src/com/qiniu/utils/MultipartEntity.java @@ -8,6 +8,7 @@ import java.io.OutputStream; import java.util.ArrayList; import java.util.Random; +import java.util.concurrent.*; public class MultipartEntity extends AbstractHttpEntity { private String mBoundary; @@ -33,7 +34,7 @@ public void addFile(String field, String contentType, String fileName, InputStre @Override public boolean isRepeatable() { - return false; + return true; } @Override @@ -55,6 +56,7 @@ public InputStream getContent() throws IOException, IllegalStateException { @Override public void writeTo(OutputStream outputStream) throws IOException { + writed = 0; outputStream.write(mData.toString().getBytes()); outputStream.flush(); writed += mData.toString().getBytes().length; @@ -80,6 +82,7 @@ public boolean isStreaming() { public void setProcessNotify(IOnProcess ret) { mNotify = ret; } + ExecutorService executor = Executors.newFixedThreadPool(1); class FileInfo { @@ -112,12 +115,18 @@ public void writeTo(OutputStream outputStream) throws IOException { int blockSize = (int) (getContentLength() / 100); if (blockSize > 256 * 1024) blockSize = 256 * 1024; - if (blockSize < 16 * 1024) blockSize = 16 * 1024; + if (blockSize < 32 * 1024) blockSize = 32 * 1024; long index = 0; long length = mIsa.length(); while (index < length) { int readLength = (int) StrictMath.min((long) blockSize, mIsa.length() - index); - outputStream.write(mIsa.read(index, readLength)); + int timeout = readLength * 2; + try { + write(timeout, outputStream, mIsa.read(index, readLength)); + } catch (Exception e) { + mNotify.onFailure(e); + return; + } index += blockSize; outputStream.flush(); writed += readLength; @@ -130,6 +139,18 @@ public void writeTo(OutputStream outputStream) throws IOException { } } + private void write(int timeout, final OutputStream outputStream, final byte[] data) throws InterruptedException, ExecutionException, TimeoutException { + Callable readTask = new Callable() { + @Override + public Object call() throws Exception { + outputStream.write(data); + return null; + } + }; + Future future = executor.submit(readTask); + future.get(timeout, TimeUnit.MILLISECONDS); + } + private static String getRandomString(int length) { String base = "abcdefghijklmnopqrstuvwxyz0123456789"; Random random = new Random();