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");