diff --git a/library/src/androidTest/java/com/qiniu/android/SyncFormUploadTest.java b/library/src/androidTest/java/com/qiniu/android/SyncFormUploadTest.java new file mode 100644 index 000000000..6157d70de --- /dev/null +++ b/library/src/androidTest/java/com/qiniu/android/SyncFormUploadTest.java @@ -0,0 +1,273 @@ +package com.qiniu.android; + +import android.test.InstrumentationTestCase; +import android.test.suitebuilder.annotation.MediumTest; +import android.test.suitebuilder.annotation.SmallTest; + +import com.qiniu.android.common.FixedZone; +import com.qiniu.android.common.ServiceAddress; +import com.qiniu.android.common.Zone; +import com.qiniu.android.http.ResponseInfo; +import com.qiniu.android.storage.Configuration; +import com.qiniu.android.storage.UploadManager; +import com.qiniu.android.storage.UploadOptions; +import com.qiniu.android.utils.Etag; + +import junit.framework.Assert; + +import org.json.JSONObject; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +public class SyncFormUploadTest extends InstrumentationTestCase { + private UploadManager uploadManager; + private volatile String key; + private volatile ResponseInfo info; + private volatile JSONObject resp; + + public void setUp() throws Exception { + uploadManager = new UploadManager(); + } + + @SmallTest + public void testHello() throws Throwable { + final String expectKey = "你好;\"\r\n\r\n\r\n"; + Map params = new HashMap(); + params.put("x:foo", "fooval"); + final UploadOptions opt = new UploadOptions(params, null, true, null, null); + byte[] b = "hello".getBytes(); + info = uploadManager.syncPut(b, expectKey, TestConfig.token, opt); + resp = info.response; + + Assert.assertTrue(info.toString(), info.isOK()); + Assert.assertNotNull(info.reqId); + Assert.assertNotNull(resp); + + String hash = resp.optString("hash"); + Assert.assertEquals(hash, Etag.data(b)); + Assert.assertEquals(expectKey, key = resp.optString("key")); + } + + @SmallTest + public void test0Data() throws Throwable { + final String expectKey = "你好;\"\r\n\r\n\r\n"; + Map params = new HashMap(); + params.put("x:foo", "fooval"); + final UploadOptions opt = new UploadOptions(params, null, true, null, null); + + info = uploadManager.syncPut("".getBytes(), expectKey, TestConfig.token, opt); + resp = info.response; + +// key = resp.optString("key"); + Assert.assertEquals(info.toString(), ResponseInfo.ZeroSizeFile, info.statusCode); +// Assert.assertEquals(info.toString(), expectKey, key); + Assert.assertFalse(info.toString(), info.isOK()); + Assert.assertEquals(info.toString(), "", info.reqId); + Assert.assertNull(resp); + } + + @SmallTest + public void testNoKey() throws Throwable { + final String expectKey = null; + Map params = new HashMap(); + params.put("x:foo", "fooval"); + final UploadOptions opt = new UploadOptions(params, null, true, null, null); + info = uploadManager.syncPut("hello".getBytes(), expectKey, TestConfig.token, opt); + + resp = info.response; + key = resp.optString("key"); + Assert.assertTrue(info.toString(), info.isOK()); + + Assert.assertNotNull(info.reqId); + Assert.assertNotNull(resp); + Assert.assertEquals("Fqr0xh3cxeii2r7eDztILNmuqUNN", resp.optString("key", "")); + } + + @SmallTest + public void testInvalidToken() throws Throwable { + final String expectKey = "你好"; + info = uploadManager.syncPut("hello".getBytes(), expectKey, "invalid", null); + + resp = info.response; +// key = resp.optString("key"); +// Assert.assertEquals(info.toString(), expectKey, key); + Assert.assertEquals(info.toString(), ResponseInfo.InvalidToken, info.statusCode); + Assert.assertNotNull(info.reqId); + Assert.assertNull(resp); + } + + @SmallTest + public void testNoData() throws Throwable { + final String expectKey = "你好"; + + info = uploadManager.syncPut((byte[]) null, expectKey, "invalid", null); + + resp = info.response; + Assert.assertEquals(info.toString(), ResponseInfo.InvalidArgument, + info.statusCode); + Assert.assertNull(resp); + } + + @SmallTest + public void testNoToken() throws Throwable { + final String expectKey = "你好"; + info = uploadManager.syncPut(new byte[1], expectKey, null, null); + + resp = info.response; + Assert.assertEquals(info.toString(), ResponseInfo.InvalidArgument, info.statusCode); + Assert.assertNull(resp); + } + + @SmallTest + public void testEmptyToken() throws Throwable { + final String expectKey = "你好"; + info = uploadManager.syncPut(new byte[1], expectKey, "", null); + + resp = info.response; + Assert.assertEquals(info.toString(), ResponseInfo.InvalidArgument, + info.statusCode); + Assert.assertNull(resp); + } + + @MediumTest + public void testFile() throws Throwable { + final String expectKey = "世/界"; + final File f = TempFile.createFile(1); + Map params = new HashMap(); + params.put("x:foo", "fooval"); + final UploadOptions opt = new UploadOptions(params, null, true, null, null); + info = uploadManager.syncPut(f, expectKey, TestConfig.token, opt); + + resp = info.response; + key = resp.optString("key"); + Assert.assertEquals(info.toString(), expectKey, key); + Assert.assertTrue(info.toString(), info.isOK()); + //上传策略含空格 \"fname\":\" $(fname) \" + Assert.assertEquals(f.getName(), resp.optString("fname", "res doesn't include the FNAME").trim()); + Assert.assertNotNull(info.reqId); + Assert.assertNotNull(resp); + String hash = resp.getString("hash"); + Assert.assertEquals(hash, Etag.file(f)); + TempFile.remove(f); + } + + @MediumTest + public void test0File() throws Throwable { + final String expectKey = "世/界"; + final File f = TempFile.createFile(0); + Map params = new HashMap(); + params.put("x:foo", "fooval"); + final UploadOptions opt = new UploadOptions(params, null, true, null, null); + info = uploadManager.syncPut(f, expectKey, TestConfig.token, opt); + + resp = info.response; + Assert.assertEquals(f.toString(), 0, f.length()); + Assert.assertEquals(info.toString(), ResponseInfo.ZeroSizeFile, info.statusCode); + Assert.assertNull(resp); + Assert.assertFalse(info.toString(), info.isOK()); + Assert.assertEquals(info.toString(), "", info.reqId); + TempFile.remove(f); + } + + @SmallTest + public void testNoComplete() { + info = uploadManager.syncPut(new byte[0], null, null, null); + Assert.assertEquals(info.toString(), ResponseInfo.ZeroSizeFile, info.statusCode); + + info = uploadManager.syncPut("", null, null, null); + Assert.assertEquals(info.toString(), ResponseInfo.ZeroSizeFile, info.statusCode); + } + + @SmallTest + public void testIpBack() throws Throwable { + ServiceAddress s = new ServiceAddress("http://upwelcome.qiniu.com", Zone.zone0.upHost("").backupIps); + Zone z = new FixedZone(s, Zone.zone0.upHostBackup("")); + Configuration c = new Configuration.Builder() + .zone(z) + .build(); + + UploadManager uploadManager2 = new UploadManager(c); + final String expectKey = "你好;\"\r\n\r\n\r\n"; + Map params = new HashMap(); + params.put("x:foo", "fooval"); + final UploadOptions opt = new UploadOptions(params, null, true, null, null); + + info = uploadManager2.syncPut("hello".getBytes(), expectKey, TestConfig.token, opt); + + Assert.assertTrue(info.toString(), info.isOK()); + Assert.assertNotNull(info.reqId); + resp = info.response; + Assert.assertNotNull(resp); + key = resp.optString("key"); + Assert.assertEquals(info.toString(), expectKey, key); + } + + @SmallTest + public void testPortBackup() throws Throwable { + ServiceAddress s = new ServiceAddress("http://upload.qiniu.com:9999", null); + Zone z = new FixedZone(s, Zone.zone0.upHostBackup("")); + Configuration c = new Configuration.Builder() + .zone(z) + .build(); + UploadManager uploadManager2 = new UploadManager(c); + final String expectKey = "你好;\"\r\n\r\n\r\n"; + Map params = new HashMap(); + params.put("x:foo", "fooval"); + final UploadOptions opt = new UploadOptions(params, null, true, null, null); + + info = uploadManager2.syncPut("hello".getBytes(), expectKey, TestConfig.token, opt); + + Assert.assertTrue(info.toString(), info.isOK()); + Assert.assertNotNull(info.reqId); + resp = info.response; + Assert.assertNotNull(resp); + key = resp.optString("key"); + Assert.assertEquals(info.toString(), expectKey, key); + } + + @SmallTest + public void testDnsHijacking() throws Throwable { + ServiceAddress s = new ServiceAddress("http://uphijacktest.qiniu.com", Zone.zone0.upHost("").backupIps); + Zone z = new FixedZone(s, Zone.zone0.upHostBackup("")); + Configuration c = new Configuration.Builder() + .zone(z) + .build(); + UploadManager uploadManager2 = new UploadManager(c); + final String expectKey = "你好;\"\r\n\r\n\r\n"; + Map params = new HashMap(); + params.put("x:foo", "fooval"); + final UploadOptions opt = new UploadOptions(params, null, true, null, null); + + info = uploadManager2.syncPut("hello".getBytes(), expectKey, TestConfig.token, opt); + + resp = info.response; + Assert.assertTrue(info.toString(), info.isOK()); + Assert.assertNotNull(info.reqId); + Assert.assertNotNull(resp); + } + + @SmallTest + public void testHttps() throws Throwable { + + final String expectKey = "你好;\"\r\n\r\n\r\n"; + Map params = new HashMap(); + params.put("x:foo", "fooval"); + final UploadOptions opt = new UploadOptions(params, null, true, null, null); + ServiceAddress s = new ServiceAddress("https://up.qbox.me", null); + Zone z = new FixedZone(s, Zone.zone0.upHostBackup("")); + Configuration c = new Configuration.Builder() + .zone(z) + .build(); + UploadManager uploadManager2 = new UploadManager(c); + info = uploadManager2.syncPut("hello".getBytes(), expectKey, TestConfig.token, opt); + + resp = info.response; + key = resp.optString("key"); + Assert.assertEquals(info.toString(), expectKey, key); + Assert.assertTrue(info.toString(), info.isOK()); + Assert.assertNotNull(info.reqId); + Assert.assertNotNull(resp); + } +} diff --git a/library/src/androidTest/java/com/qiniu/android/TestFileRecorder.java b/library/src/androidTest/java/com/qiniu/android/TestFileRecorder.java index 83d088555..d9a7d0e4f 100644 --- a/library/src/androidTest/java/com/qiniu/android/TestFileRecorder.java +++ b/library/src/androidTest/java/com/qiniu/android/TestFileRecorder.java @@ -12,7 +12,6 @@ import com.qiniu.android.storage.UploadOptions; import com.qiniu.android.storage.persistent.FileRecorder; import com.qiniu.android.utils.Etag; -import com.qiniu.android.utils.UrlSafeBase64; import junit.framework.Assert; diff --git a/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml index 6c99cf791..d90d401e4 100644 --- a/library/src/main/AndroidManifest.xml +++ b/library/src/main/AndroidManifest.xml @@ -1,10 +1,9 @@ - - - - + + + diff --git a/library/src/main/java/com/qiniu/android/http/Client.java b/library/src/main/java/com/qiniu/android/http/Client.java index 8f4571131..dca64abdb 100644 --- a/library/src/main/java/com/qiniu/android/http/Client.java +++ b/library/src/main/java/com/qiniu/android/http/Client.java @@ -4,6 +4,7 @@ import com.qiniu.android.dns.DnsManager; import com.qiniu.android.dns.Domain; import com.qiniu.android.storage.UpCancellationSignal; +import com.qiniu.android.storage.UpToken; import com.qiniu.android.utils.AsyncRun; import com.qiniu.android.utils.StringMap; import com.qiniu.android.utils.StringUtils; @@ -347,6 +348,74 @@ public void accept(String key, Object value) { return buildResponseInfo(res, tag.ip, tag.duration); } + public ResponseInfo syncMultipartPost(String url, + PostArgs args, + final UpToken upToken) { + RequestBody file; + if (args.file != null) { + file = RequestBody.create(MediaType.parse(args.mimeType), args.file); + } else { + file = RequestBody.create(MediaType.parse(args.mimeType), args.data); + } + return syncMultipartPost(url, args.params, upToken, args.fileName, file); + } + + private ResponseInfo syncMultipartPost(String url, + StringMap fields, + final UpToken upToken, + String fileName, + RequestBody file) { + final MultipartBody.Builder mb = new MultipartBody.Builder(); + mb.addFormDataPart("file", fileName, file); + + fields.forEach(new StringMap.Consumer() { + @Override + public void accept(String key, Object value) { + mb.addFormDataPart(key, value.toString()); + } + }); + mb.setType(MediaType.parse("multipart/form-data")); + RequestBody body = mb.build(); + Request.Builder requestBuilder = new Request.Builder().url(url).post(body); + return syncSend(requestBuilder, null, upToken); + } + + public ResponseInfo syncSend(final Request.Builder requestBuilder, StringMap headers, final UpToken upToken) { + if (headers != null) { + headers.forEach(new StringMap.Consumer() { + @Override + public void accept(String key, Object value) { + requestBuilder.header(key, value.toString()); + } + }); + } + + requestBuilder.header("User-Agent", UserAgent.instance().getUa(upToken.accessKey)); + final ResponseTag tag = new ResponseTag(); + Request req = null; + try { + req = requestBuilder.tag(tag).build(); + okhttp3.Response response = httpClient.newCall(req).execute(); + return buildResponseInfo(response, tag.ip, tag.duration); + } catch (Exception e) { + e.printStackTrace(); + int statusCode = NetworkError; + String msg = e.getMessage(); + if (e instanceof UnknownHostException) { + statusCode = ResponseInfo.UnknownHost; + } else if (msg != null && msg.indexOf("Broken pipe") == 0) { + statusCode = ResponseInfo.NetworkConnectionLost; + } else if (e instanceof SocketTimeoutException) { + statusCode = ResponseInfo.TimedOut; + } else if (e instanceof java.net.ConnectException) { + statusCode = ResponseInfo.CannotConnectToHost; + } + + HttpUrl u = req.url(); + return new ResponseInfo(null, statusCode, "", "", "", u.host(), u.encodedPath(), "", u.port(), 0, 0, e.getMessage()); + } + } + private static class ResponseTag { public String ip = ""; public long duration = 0; diff --git a/library/src/main/java/com/qiniu/android/http/ResponseInfo.java b/library/src/main/java/com/qiniu/android/http/ResponseInfo.java index 3fe55f59e..53c8f852c 100644 --- a/library/src/main/java/com/qiniu/android/http/ResponseInfo.java +++ b/library/src/main/java/com/qiniu/android/http/ResponseInfo.java @@ -18,6 +18,8 @@ public final class ResponseInfo { public static final int Cancelled = -2; public static final int NetworkError = -1; + public static final int UnknownError = 0; + // <-- error code copy from ios public static final int TimedOut = -1001; public static final int UnknownHost = -1003; @@ -84,7 +86,7 @@ public final class ResponseInfo { public final long sent; /** - * hide, 内部使用 + * 响应体,json 格式 */ public final JSONObject response; diff --git a/library/src/main/java/com/qiniu/android/storage/FormUploader.java b/library/src/main/java/com/qiniu/android/storage/FormUploader.java index 4f9b12d19..a5a534aad 100644 --- a/library/src/main/java/com/qiniu/android/storage/FormUploader.java +++ b/library/src/main/java/com/qiniu/android/storage/FormUploader.java @@ -149,4 +149,109 @@ public void complete(ResponseInfo info, JSONObject response) { client.asyncMultipartPost(config.zone.upHost(token.token).address.toString(), args, progress, completion, options.cancellationSignal); } + + /** + * 上传数据,并以指定的key保存文件 + * + * @param client HTTP连接管理器 + * @param data 上传的数据 + * @param key 上传的数据保存的文件名 + * @param token 上传凭证 + * @param options 上传时的可选参数 + * + * @return 响应信息 ResponseInfo#response 响应体,序列化后 json 格式 + */ + public static ResponseInfo syncUpload(Client client, Configuration config, byte[] data, String key, UpToken token, UploadOptions options) { + try { + return syncUpload0(client, config, data, null, key, token, options); + } catch (Exception e) { + return new ResponseInfo(null, ResponseInfo.UnknownError, "", "", "", "", "", "", 0, 0, 0, e.getMessage()); // TODO + } + } + + /** + * 上传文件,并以指定的key保存文件 + * + * @param client HTTP连接管理器 + * @param file 上传的文件 + * @param key 上传的数据保存的文件名 + * @param token 上传凭证 + * @param options 上传时的可选参数 + * + * @return 响应信息 ResponseInfo#response 响应体,序列化后 json 格式 + */ + public static ResponseInfo syncUpload(Client client, Configuration config, File file, String key, UpToken token, UploadOptions options) { + try { + return syncUpload0(client, config, null, file, key, token, options); + } catch (Exception e) { + return new ResponseInfo(null, ResponseInfo.UnknownError, "", "", "", "", "", "", 0, 0, 0, e.getMessage()); // TODO + } + } + + private static ResponseInfo syncUpload0(Client client, Configuration config, byte[] data, File file, + String key, UpToken token, UploadOptions optionsIn) { + StringMap params = new StringMap(); + final PostArgs args = new PostArgs(); + if (key != null) { + params.put("key", key); + args.fileName = key; + } else { + args.fileName = "?"; + } + + // data is null , or file is null + if (file != null) { + args.fileName = file.getName(); + } + + params.put("token", token.token); + + final UploadOptions options = optionsIn != null ? optionsIn : UploadOptions.defaultOptions(); + params.putFileds(options.params); + + if (options.checkCrc) { + long crc = 0; + if (file != null) { + try { + crc = Crc32.file(file); + } catch (IOException e) { + e.printStackTrace(); + } + } else { + crc = Crc32.bytes(data); + } + params.put("crc32", "" + crc); + } + + args.data = data; + args.file = file; + args.mimeType = options.mimeType; + args.params = params; + args.userAgentPart = token.accessKey; + + ResponseInfo info = client.syncMultipartPost(config.zone.upHost(token.token).address.toString(), args, token); + + if (info.isOK()) { + return info; + } + + if (info.needRetry() || (info.isNotQiniu() && !token.hasReturnUrl())) { + if (info.isNetworkBroken() && !AndroidNetwork.isNetWorkReady()) { + options.netReadyHandler.waitReady(); + if (!AndroidNetwork.isNetWorkReady()) { + return info; + } + } + + URI u = config.zone.upHost(token.token).address; + if (config.zone.upHostBackup(token.token) != null + && (info.needSwitchServer() || info.isNotQiniu())) { + u = config.zone.upHostBackup(token.token).address; + } + + return client.syncMultipartPost(u.toString(), args, token); + } + + return info; + } } diff --git a/library/src/main/java/com/qiniu/android/storage/UploadManager.java b/library/src/main/java/com/qiniu/android/storage/UploadManager.java index 4c00a552e..d896c8bd1 100644 --- a/library/src/main/java/com/qiniu/android/storage/UploadManager.java +++ b/library/src/main/java/com/qiniu/android/storage/UploadManager.java @@ -11,8 +11,9 @@ /** * 七牛文件上传管理器 - * 一般默认可以使用这个类的方法来上传数据和文件。这个类自动检测文件的大小, - * 只要超过了{@link Configuration#putThreshold} + * 一般默认可以使用这个类的方法来上传数据和文件。会自动检测文件的大小, + * 只要超过了{@link Configuration#putThreshold} 异步方法会使用分片方片上传。 + * 同步上传方法使用表单方式上传,建议只对小文件使用同步方式 */ public final class UploadManager { private final Configuration config; @@ -195,4 +196,78 @@ public void onFailure(int reason) { }); } + + /** + * 同步上传文件。使用 form 表单方式上传,建议只在数据较小情况下使用此方式,如 file.size() < 1024 * 1024。 + * + * @param data 上传的数据 + * @param key 上传数据保存的文件名 + * @param token 上传凭证 + * @param options 上传数据的可选参数 + * + * @return 响应信息 ResponseInfo#response 响应体,序列化后 json 格式 + */ + public ResponseInfo syncPut(byte[] data, String key, String token, UploadOptions options) { + final UpToken decodedToken = UpToken.parse(token); + ResponseInfo info = areInvalidArg(key, data, null, token, decodedToken); + if (info != null) { + return info; + } + return FormUploader.syncUpload(client, config, data, key, decodedToken, options); + } + + /** + * 同步上传文件。使用 form 表单方式上传,建议只在文件较小情况下使用此方式,如 file.size() < 1024 * 1024。 + * + * @param file 上传的文件对象 + * @param key 上传数据保存的文件名 + * @param token 上传凭证 + * @param options 上传数据的可选参数 + * + * @return 响应信息 ResponseInfo#response 响应体,序列化后 json 格式 + */ + public ResponseInfo syncPut(File file, String key, String token, UploadOptions options) { + final UpToken decodedToken = UpToken.parse(token); + ResponseInfo info = areInvalidArg(key, null, file, token, decodedToken); + if (info != null) { + return info; + } + return FormUploader.syncUpload(client, config, file, key, decodedToken, options); + } + + /** + * 同步上传文件。使用 form 表单方式上传,建议只在文件较小情况下使用此方式,如 file.size() < 1024 * 1024。 + * + * @param file 上传的文件绝对路径 + * @param key 上传数据保存的文件名 + * @param token 上传凭证 + * @param options 上传数据的可选参数 + * + * @return 响应信息 ResponseInfo#response 响应体,序列化后 json 格式 + */ + public ResponseInfo syncPut(String file, String key, String token, UploadOptions options) { + return syncPut(new File(file), key, token, options); + } + + private static ResponseInfo areInvalidArg(final String key, byte[] data, File f, String token, + UpToken decodedToken) { + String message = null; + if (f == null && data == null) { + message = "no input data"; + } else if (token == null || token.equals("")) { + message = "no token"; + } + ResponseInfo info = null; + if (decodedToken == null) { + info = ResponseInfo.invalidToken("invalid token"); + } + if (message != null) { + info = ResponseInfo.invalidArgument(message); + } + if ((f != null && f.length() == 0) || (data != null && data.length == 0)) { + info = ResponseInfo.zeroSize(); + } + return info; + } + }