diff --git a/src/main/java/com/qiniu/storage/BucketManager.java b/src/main/java/com/qiniu/storage/BucketManager.java index 91eeba1fc..5d573b540 100644 --- a/src/main/java/com/qiniu/storage/BucketManager.java +++ b/src/main/java/com/qiniu/storage/BucketManager.java @@ -1251,15 +1251,53 @@ public BatchOperations() { * @return BatchOperations */ public BatchOperations addChgmOp(String bucket, String key, String newMimeType) { - String resource = encodedEntry(bucket, key); - String encodedMime = UrlSafeBase64.encodeToString(newMimeType); - ops.add(String.format("/chgm/%s/mime/%s", resource, encodedMime)); + return addChgmOp(bucket, key, newMimeType, null, null); + } + + /** + * 添加 chgm 指令 + * 相关链接 + * newMimeType 和 metaData 必须有其一 + * + * @param bucket 空间名 + * @param key 文件的 key + * @param newMimeType 修改后的 MimeType [可选] + * @param metas 需要修改的 metas,只包含需要更改的 metas,可增加 [可选] + * 服务接口中 key 必须包含 x-qn-meta- 前缀,SDK 会对 metas 中的 key 进行检测 + * - key 如果包含了 x-qn-meta- 前缀,则直接使用 key + * - key 如果不包含了 x-qn-meta- 前缀,则内部会为 key 拼接 x-qn-meta- 前缀 + * @param condition 自定义条件信息;只有条件匹配才会执行修改操作 [可选] + * @return BatchOperations + */ + public BatchOperations addChgmOp(String bucket, String key, String newMimeType, Map metas, Condition condition) { + StringBuilder builder = new StringBuilder() + .append("/chgm/").append(encodedEntry(bucket, key)); + if (newMimeType != null && !newMimeType.isEmpty()) { + builder.append("/mime/").append(UrlSafeBase64.encodeToString(newMimeType)); + } + + if (metas != null) { + for (String k : metas.keySet()) { + if (k.startsWith("x-qn-meta-")) { + builder.append("/").append(k); + } else { + builder.append("/x-qn-meta-").append(k); + } + builder.append("/").append(UrlSafeBase64.encodeToString(metas.get(k))); + } + } + + if (condition != null && condition.encodedString() != null) { + builder.append("/cond/").append(condition.encodedString()); + } + ops.add(builder.toString()); setExecBucket(bucket); return this; } /** * 添加 copy 指令 + * 如果目标文件名已被占用,则返回错误码 614,且不做任何覆盖操作; * * @param fromBucket 源空间名 * @param fromFileKey 源文件的 key @@ -1275,6 +1313,26 @@ public BatchOperations addCopyOp(String fromBucket, String fromFileKey, String t return this; } + /** + * 添加 copy 指令 + * + * @param fromBucket 源空间名 + * @param fromFileKey 源文件的 key + * @param toBucket 目标空间名 + * @param toFileKey 目标文件的 key + * @param force 当目标文件已存在时,是否木盖目标文件 + * false: 如果目标文件名已被占用,则返回错误码 614,且不做任何覆盖操作; + * true: 如果目标文件名已被占用,会强制覆盖目标文件 + * @return BatchOperations + */ + public BatchOperations addCopyOp(String fromBucket, String fromFileKey, String toBucket, String toFileKey, boolean force) { + String from = encodedEntry(fromBucket, fromFileKey); + String to = encodedEntry(toBucket, toFileKey); + ops.add(String.format("copy/%s/%s/force/%s", from, to, force)); + setExecBucket(fromBucket); + return this; + } + /** * 添加重命名指令 * @@ -1287,6 +1345,21 @@ public BatchOperations addRenameOp(String fromBucket, String fromFileKey, String return addMoveOp(fromBucket, fromFileKey, fromBucket, toFileKey); } + /** + * 添加重命名指令 + * + * @param fromBucket 源空间名 + * @param fromFileKey 源文件的 key + * @param toFileKey 目标文件的 key + * @param force 当目标文件已存在时,是否木盖目标文件 + * false: 如果目标文件名已被占用,则返回错误码 614,且不做任何覆盖操作; + * true: 如果目标文件名已被占用,会强制覆盖目标文件 + * @return BatchOperations + */ + public BatchOperations addRenameOp(String fromBucket, String fromFileKey, String toFileKey, boolean force) { + return addMoveOp(fromBucket, fromFileKey, fromBucket, toFileKey, force); + } + /** * 添加move指令 * @@ -1304,6 +1377,26 @@ public BatchOperations addMoveOp(String fromBucket, String fromKey, String toBuc return this; } + /** + * 添加move指令 + * + * @param fromBucket 源空间名 + * @param fromKey 源文件的 keys + * @param toBucket 目标空间名 + * @param toKey 目标文件的 keys + * @param force 当目标文件已存在时,是否木盖目标文件 + * false: 如果目标文件名已被占用,则返回错误码 614,且不做任何覆盖操作; + * true: 如果目标文件名已被占用,会强制覆盖目标文件 + * @return BatchOperations + */ + public BatchOperations addMoveOp(String fromBucket, String fromKey, String toBucket, String toKey, boolean force) { + String from = encodedEntry(fromBucket, fromKey); + String to = encodedEntry(toBucket, toKey); + ops.add(String.format("move/%s/%s/force/%s", from, to, force)); + setExecBucket(fromBucket); + return this; + } + /** * 添加delete指令 * @@ -1429,6 +1522,81 @@ public int size() { } } + public static final class Condition { + private final String hash; + private final String mime; + private final Long fSize; + private final Long putTime; + + private Condition(String hash, String mime, Long fSize, Long putTime) { + this.hash = hash; + this.mime = mime; + this.fSize = fSize; + this.putTime = putTime; + } + + String encodedString() { + StringBuilder builder = new StringBuilder(); + if (hash != null && !hash.isEmpty()) { + builder.append("hash=" + hash + "&"); + } + if (mime != null && !mime.isEmpty()) { + builder.append("mime=" + mime + "&"); + } + if (fSize != null) { + builder.append("fsize=" + fSize + "&"); + } + if (putTime != null) { + builder.append("putTime=" + putTime + "&"); + } + + String encoded = builder.toString(); + if (encoded.isEmpty()) { + return null; + } + + if (encoded.endsWith("&")) { + encoded = encoded.substring(0, encoded.length() - 1); + } + + return UrlSafeBase64.encodeToString(encoded); + } + + public static final class Builder { + private String hash; + private String mime; + private Long fileSize; + private Long putTime; + + public Builder() { + } + + public Builder setHash(String hash) { + this.hash = hash; + return this; + } + + public Builder setMime(String mime) { + this.mime = mime; + return this; + } + + public Builder setFileSize(Long fileSize) { + this.fileSize = fileSize; + return this; + } + + public Builder setPutTime(Long putTime) { + this.putTime = putTime; + return this; + } + + public Condition build() { + return new Condition(hash, mime, fileSize, putTime); + } + } + } + /** * 创建文件列表迭代器 */ diff --git a/src/main/java/com/qiniu/storage/Region.java b/src/main/java/com/qiniu/storage/Region.java index 8765c868c..4d93959cc 100644 --- a/src/main/java/com/qiniu/storage/Region.java +++ b/src/main/java/com/qiniu/storage/Region.java @@ -371,7 +371,7 @@ String getApiHost(RegionReqInfo regionReqInfo) throws QiniuException { } String getUcHost(RegionReqInfo regionReqInfo) throws QiniuException { - if (ucHosts == null || ucHosts.size() == 0) { + if (ucHosts == null || ucHosts.isEmpty()) { return ""; } return ucHosts.get(0); @@ -470,6 +470,13 @@ public Builder apiHost(String apiHost) { return this; } + public Builder ucHost(String... ucHosts) { + if (ucHosts.length > 0) { + this.region.ucHosts = Arrays.asList(ucHosts); + } + return this; + } + /** * 自动选择,其它参数设置无效 * diff --git a/src/test/java/com/qiniu/storage/BatchTest.java b/src/test/java/com/qiniu/storage/BatchTest.java new file mode 100644 index 000000000..906c2ab5b --- /dev/null +++ b/src/test/java/com/qiniu/storage/BatchTest.java @@ -0,0 +1,48 @@ +package com.qiniu.storage; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +public class BatchTest { + + @Test + @Tag("UnitTest") + public void testBatchCondition() { + + BucketManager.Condition condition = new BucketManager.Condition.Builder() + .build(); + String encodedString = condition.encodedString(); + Assertions.assertNull(encodedString); + + condition = new BucketManager.Condition.Builder() + .setHash("hash") + .build(); + encodedString = condition.encodedString(); + Assertions.assertEquals(encodedString, "aGFzaD1oYXNo"); + + condition = new BucketManager.Condition.Builder() + .setHash("hash") + .setPutTime(1993232L) + .build(); + encodedString = condition.encodedString(); + Assertions.assertEquals(encodedString, "aGFzaD1oYXNoJnB1dFRpbWU9MTk5MzIzMg=="); + + condition = new BucketManager.Condition.Builder() + .setHash("hash") + .setMime("application/txt") + .setPutTime(1993232L) + .build(); + encodedString = condition.encodedString(); + Assertions.assertEquals(encodedString, "aGFzaD1oYXNoJm1pbWU9YXBwbGljYXRpb24vdHh0JnB1dFRpbWU9MTk5MzIzMg=="); + + condition = new BucketManager.Condition.Builder() + .setHash("hash") + .setMime("application/txt") + .setFileSize(100L) + .setPutTime(1993232L) + .build(); + encodedString = condition.encodedString(); + Assertions.assertEquals(encodedString, "aGFzaD1oYXNoJm1pbWU9YXBwbGljYXRpb24vdHh0JmZzaXplPTEwMCZwdXRUaW1lPTE5OTMyMzI="); + } +} diff --git a/src/test/java/test/com/qiniu/TestConfig.java b/src/test/java/test/com/qiniu/TestConfig.java index 788268613..26aaf5937 100644 --- a/src/test/java/test/com/qiniu/TestConfig.java +++ b/src/test/java/test/com/qiniu/TestConfig.java @@ -22,6 +22,9 @@ public final class TestConfig { // test: ak, sk, auth public static final String testAccessKey = System.getenv("QINIU_ACCESS_KEY"); public static final String testSecretKey = System.getenv("QINIU_SECRET_KEY"); + public static final String testBucket = System.getenv("BUCKET"); + public static final String testBucketDomain = System.getenv("BUCKET_DOMAIN"); + // 内部测试环境 AK/SK public static final String innerAccessKey = System.getenv("testAK"); public static final String innerSecretKey = System.getenv("testSK"); @@ -46,7 +49,10 @@ public final class TestConfig { public static final String testPipeline = "sdktest"; // z1 - public static final String testBucket_z1 = "sdk-z1"; + public static final String testBucket_z1 = "javasdk-z1"; + public static final String testKey_z1 = "do_not_delete/1.png"; + public static final String testDomain_z1 = "javasdk-z1.qiniupkg.com"; + public static final String testDomain_z1_timeStamp = "javasdk-timestamp.peterpy.cn"; // na0 public static final String testBucket_na0 = "java-sdk-na0"; @@ -111,6 +117,17 @@ public static TestFile[] getTestFileArray(String fileSaveKey, String fileMimeTyp z0.regionId = "z0"; z0.region = Region.createWithRegionId("z0"); + TestFile z1 = new TestFile(); + z1.key = fileSaveKey; + z1.mimeType = fileMimeType; + z1.bucketName = testBucket_z1; + z1.testDomain = testDomain_z1; + z1.testUrl = "http://" + z1.testDomain + "/" + fileSaveKey; + z1.testDomainTimeStamp = testDomain_z0_timeStamp; + z1.testUrlTimeStamp = "http://" + testDomain_z0_timeStamp + "/" + fileSaveKey; + z1.regionId = "z1"; + z1.region = Region.createWithRegionId("z1"); + TestFile z0_auto = new TestFile(); z0_auto.key = fileSaveKey; z0_auto.mimeType = fileMimeType; @@ -122,7 +139,18 @@ public static TestFile[] getTestFileArray(String fileSaveKey, String fileMimeTyp z0_auto.regionId = "z0"; z0_auto.region = Region.region0(); - return new TestFile[]{z0}; + TestFile file = new TestFile(); + file.key = fileSaveKey; + file.mimeType = fileMimeType; + file.bucketName = (testBucket != null && !testBucket.isEmpty()) ? testBucket : testBucket_z0; + file.testDomain = (testBucketDomain != null && !testBucketDomain.isEmpty()) ? testBucketDomain : testDomain_z0; + file.testUrl = "http://" + file.testDomain + "/" + fileSaveKey; + file.testDomainTimeStamp = testDomain_z0_timeStamp; + file.testUrlTimeStamp = "http://" + testDomain_z0_timeStamp + "/" + fileSaveKey; + file.regionId = "z0"; + file.region = testBucket_z1.equals(testBucket) ? Region.region1() : Region.region0(); + + return new TestFile[]{file}; } public static TestFile[] getAllRegionTestFileArray() { @@ -221,7 +249,7 @@ public static TestFile[] getRetryTestFileArray() { regionGroup.addRegion(region00); regionGroup.addRegion(region01); na0.region = regionGroup; - return new TestFile[] { na0 }; + return new TestFile[]{na0}; } private static Region toRegion(Zone zone) { diff --git a/src/test/java/test/com/qiniu/storage/BucketTest2.java b/src/test/java/test/com/qiniu/storage/BucketTest2.java index a1498b16a..8721b2837 100644 --- a/src/test/java/test/com/qiniu/storage/BucketTest2.java +++ b/src/test/java/test/com/qiniu/storage/BucketTest2.java @@ -1140,6 +1140,31 @@ public void testBatchMove() { String moveToKey = key + "to"; BucketManager.BatchOperations ops = new BucketManager.BatchOperations().addMoveOp(bucket, moveFromKey, bucket, moveToKey); + try { + Response r = bucketManager.batch(ops); + BatchStatus[] bs = r.jsonToObject(BatchStatus[].class); + assertTrue(bs[0].code == 614 || bs[0].code == 200, "200 or 614"); + } catch (QiniuException e) { + fail(e.response.toString()); + } + + try { + bucketManager.copy(bucket, key, bucket, moveFromKey, true); + } catch (QiniuException ignored) { + } + + ops = new BucketManager.BatchOperations().addMoveOp(bucket, moveFromKey, + bucket, moveToKey, false); + try { + Response r = bucketManager.batch(ops); + BatchStatus[] bs = r.jsonToObject(BatchStatus[].class); + assertEquals(bs[0].code, 614); + } catch (QiniuException e) { + fail(e.response.toString()); + } + + ops = new BucketManager.BatchOperations().addMoveOp(bucket, moveFromKey, + bucket, moveToKey, true); try { Response r = bucketManager.batch(ops); BatchStatus[] bs = r.jsonToObject(BatchStatus[].class); @@ -1147,6 +1172,7 @@ public void testBatchMove() { } catch (QiniuException e) { fail(e.response.toString()); } + try { bucketManager.delete(bucket, moveToKey); } catch (QiniuException e) { @@ -1276,37 +1302,100 @@ public void testBatchCopyChgmDelete() { for (Map.Entry entry : bucketKeyMap.entrySet()) { String bucket = entry.getKey(); - String key = entry.getValue(); + String oKey = entry.getValue(); - // make 100 copies - String[] keyArray = new String[100]; - for (int i = 0; i < keyArray.length; i++) { - keyArray[i] = String.format("%s-copy-%d", key, i); - } + // 待操作的 key + String key = String.format("%s-copy-%d", oKey, new Date().getTime()); BucketManager.BatchOperations ops = new BucketManager.BatchOperations(); - for (int i = 0; i < keyArray.length; i++) { - ops.addCopyOp(bucket, key, bucket, keyArray[i]); - } + ops.addCopyOp(bucket, oKey, bucket, key); try { + // batch copy Response response = bucketManager.batch(ops); - assertTrue(batchStatusCode.contains(response.statusCode), "200 or 298"); + BatchStatus[] bs = response.jsonToObject(BatchStatus[].class); + assertTrue(bs[0].code == 200, "200"); + + + // batch copy: 非强制已存在不能成功 + ops = new BucketManager.BatchOperations(); + ops.addCopyOp(bucket, oKey, bucket, key, false); + response = bucketManager.batch(ops); + bs = response.jsonToObject(BatchStatus[].class); + assertTrue(bs[0].code == 614, "already exist, can't copy"); + + + // batch copy: 已存在,强制需成功 + ops = new BucketManager.BatchOperations(); + ops.addCopyOp(bucket, oKey, bucket, key, true); + response = bucketManager.batch(ops); + bs = response.jsonToObject(BatchStatus[].class); + assertTrue(bs[0].code == 200, "already exist, force should copy"); // clear ops ops.clearOps(); - // batch chane mimetype - for (int i = 0; i < keyArray.length; i++) { - ops.addChgmOp(bucket, keyArray[i], "image/png"); - } + // batch change mimetype + ops = new BucketManager.BatchOperations(); + ops.addChgmOp(bucket, key, "image/png"); response = bucketManager.batch(ops); - assertTrue(batchStatusCode.contains(response.statusCode), "200 or 298"); + bs = response.jsonToObject(BatchStatus[].class); + assertTrue(bs[0].code == 200, "200"); + + // 根据错误条件修改 + ops = new BucketManager.BatchOperations(); + ops.addChgmOp(bucket, key, "image/jpg", null, new BucketManager.Condition.Builder() + .setHash("ijdew[qoewqewwe") + .build()); + response = bucketManager.batch(ops); + bs = response.jsonToObject(BatchStatus[].class); + assertTrue(bs[0].code == 613, "298"); - // clear ops - for (int i = 0; i < keyArray.length; i++) { - ops.addDeleteOp(bucket, keyArray[i]); - } + + // 根据正确条件修改 + ops = new BucketManager.BatchOperations(); + ops.addChgmOp(bucket, key, "image/jpg", null, new BucketManager.Condition.Builder() + .setMime("image/png") + .build()); + response = bucketManager.batch(ops); + bs = response.jsonToObject(BatchStatus[].class); + assertTrue(bs[0].code == 200, "200"); + + // 增加 meta data + HashMap metas = new HashMap<>(); + metas.put("haha", "value"); + + // 根据错误条件增加 + ops = new BucketManager.BatchOperations(); + ops.addChgmOp(bucket, key, "", metas, new BucketManager.Condition.Builder() + .setHash("ijdew[qoewqewwe") + .build()); + response = bucketManager.batch(ops); + bs = response.jsonToObject(BatchStatus[].class); + assertTrue(bs[0].code == 613, "298"); + + + // 根据正确条件增加 + FileInfo fileInfo = bucketManager.stat(bucket, key); + ops = new BucketManager.BatchOperations(); + ops.addChgmOp(bucket, key, "", metas, new BucketManager.Condition.Builder() + .setMime(fileInfo.mimeType) + .setHash(fileInfo.hash) + .setFileSize(fileInfo.fsize) + .setPutTime(fileInfo.putTime) + .build()); + response = bucketManager.batch(ops); + bs = response.jsonToObject(BatchStatus[].class); + assertTrue(bs[0].code == 200, "298"); + + + fileInfo = bucketManager.stat(bucket, key); + Map meta = fileInfo.meta; + assertTrue(meta != null && meta.get("haha").equals("value"), "meta value"); + + // 删除文件列表 + ops = new BucketManager.BatchOperations(); + ops.addDeleteOp(bucket, key); response = bucketManager.batch(ops); assertTrue(batchStatusCode.contains(response.statusCode), "200 or 298");