123云盘API封装

介绍

使用该工具类时需要到123云盘开放平台申请 CLIENT_IDCLIENT_SECRET

这是对123云盘的所有API链接进行封装,使用时需导入一下Maven坐标吗,主要用于配合解析返回数据

1
2
3
4
5
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.16.0</version>
</dependency>

使用时会生成一个配置 config/123pan.json 这里面存放着 Access_Token 和它的过期时间基本上所有的请求都需要携带 Access_Token

Access_Token 的获取会在文件加载时进行判断获取,也可以使用 OneTwoThreeCloudDisk.getAccessToken() 进行手动获取,每次请求API时会先判断 Access_Token 是否可用

请求如果请求失败会有三次重试机会,可以更改 RETRY_MAX 改变次数

API介绍

所有的API都有些注释(这个API干什么用的以及需要什么参数),到时候按照要求来,返回值请参考官方文档,所有接口统一返回 Map

常量介绍

CLIENT_ID:申请到的客户端ID

CLIENT_SECRET:申请到的客户端密钥

RETRY_MAX:请求出错后的重试次数

PRIVATE_KEY:URL鉴权处所设置的鉴权密钥

UID:本用户的ID

EXPIRED_TIME_SEC:防盗链过期的时间(秒)

JSON_FILE:这里设置JSON文件的地址里面存放着ACCESS_TOKEN和过期时间

API:请求API的根域名

自定义API

测算文件的大小和MD5

工具API:fileSizeAndMD5()

对文件进行分片

工具API:splitFile()

分片的时候会创建一个 part 的文件夹,里面存放着分片信息,路径格式为:part/文件名/文件名-num.part

上传分片文件

工具API:uploadShardsPUT()

一条龙上传文件

工具API:uploadFile()

只需要提供要上传文件的路径、文件名(带后缀)、目录ID即可自动完成所有操作,上传完成之后会删除所有分片

返回信息(Map):

  1. 如果 code 码为0则说明完成请求
  2. 如果 code 码为1则说明上传分片时上传失败,返回 errorNum(上传失败的分片数)、countNum(全部分片数量)、preuploadID(预上传ID)
  3. 如果 code 码为2则说明查询上传结果重试了60次还是没有合并成功

一条龙上传文件 - 返回直链

工具API:uploadFileAndGetDirectLink()

介绍:包含一条龙上传文件并且上传完成之后返回直链信息

要求:需要上传的目录必须是直链空间

URL鉴权 - 防盗链

工具API:URLAuthentication()

介绍:在云盘内开启URL鉴权之后所访问呢的链接就需要进行加密校验了,只需要把直链链接传入即可返回加密链接

注意:需要在常量中添加相对应的数据

一条龙上传文件 - 返回直链+防盗链

工具API:uploadFilesAndGetAuthenticationLink()

介绍:包含一条龙上传文件并且上传完成之后返回直链信息

要求:需要上传的目录必须是直链空间,需要在常量中添加相对应的数据

散装 API

获取用户信息

官方文档:获取用户信息 (yuque.com)

工具API:getUserInfo()

创建离线下载任务

官方文档:创建离线下载任务 (yuque.com)

工具API:createOfflineDownloadTask()

获取离线下载进度

官方文档:获取离线下载进度 (yuque.com)

工具API:getProgressYourOfflineDownload()

创建分享链接

官方文档:创建分享链接 (yuque.com)

工具API:createSharedLink()

获取access_token

官方文档:获取access_token (yuque.com)

工具API:OneTwoThreeCloudDisk.getAccessToken()

直链

所有直链相关的API以 straight 开头

获取直链链接

官方文档:获取直链链接 (yuque.com)

工具API:straight_GetADirectLink()

启用直链空间

官方文档:启用直链空间 (yuque.com)

工具API:straight_EnableDirectLinkSpace()

禁用直链空间

官方文档:禁用直链空间 (yuque.com)

工具API:straight_DisableDirectLinkSpace()

获取直链转码链接

官方文档:获取直链转码链接 (yuque.com)

工具API:straight_GetDirectLinkTranscode()

发起直链转码

官方文档:发起直链转码 (yuque.com)

工具API:straight_InitiateDirectChainTranscode()

查询直链转码进度

官方文档:查询直链转码进度 (yuque.com)

工具API:straight_QueryTranscodingProgress()

文件管理

所有文件管理的API以 file 开头

获取文件列表

官方文档:获取文件列表 (yuque.com)

工具API:file_GetListOfFiles()

文件重命名

官方文档:文件重命名 (yuque.com)

工具API:file_heavyNaming()

从回收站恢复文件

官方文档:从回收站恢复文件 (yuque.com)

工具API:file_RecoverFilesFromRecycleBin()

删除文件至回收站

官方文档:删除文件至回收站 (yuque.com)

工具API:file_DeleteFilesToRecycleBin()

彻底删除文件

官方文档:彻底删除文件 (yuque.com)

工具API:file_DeleteFilesCompletely()

移动文件

官方文档:移动文件 (yuque.com)

工具API:file_MoveFiles()

上传文件

上传流程:上传流程 (yuque.com)

上传文件 - 创建目录

官方文档:创建目录 (yuque.com)

工具API:file_CreateCatalog()

上传文件 - 创建文件

官方文档:创建文件 (yuque.com)

工具API:file_CreateFile()

上传文件 - 列举已上传分片

官方文档:列举已上传分片 (yuque.com)

工具API:file_ListUploadedParts()

上传文件 - 获取上传地址

官方文档:获取上传地址 (yuque.com)

工具API:file_ObtainUploadURL()

上传文件 - 上传完毕

官方文档:上传完毕 (yuque.com)

工具API:file_UploadCompleted()

上传文件 - 异步轮询获取上传结果

官方文档:异步轮询获取上传结果 (yuque.com)

工具API:file_AsyncPollToObtainUploadResults()

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.*;
import java.math.BigInteger;
import java.net.*;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.*;

/**
* @version 1.5
* @description: 123云盘开放API封装
* @author: 曦暮流年
* @see <a href="https://blog.ximuliunian.top/2024/05/09/云盘/123云盘API封装/">API文档详情</a>
*/
public class OneTwoThreeCloudPan {
// 客户端ID
private static final String CLIENT_ID = "";
// 客户端密钥
private static final String CLIENT_SECRET = "";
// 请求出错后的重试次数
private static final int RETRY_MAX = 3;
// 密钥 - URL鉴权
private static final String PRIVATE_KEY = "";
// 用户UID - URL鉴权
private static final long UID = ;
// 防盗链过期时间(秒) - URL鉴权
private static final long EXPIRED_TIME_SEC = 3 * 60;
// 123云盘 JSON文件
private static String JSON_FILE = "config/123pan.json";
// 请求API
private static final String API = "https://open-api.123pan.com";
// 请求令牌
private static String ACCESS_TOKEN;
// 令牌过期时间
private static String EXPIRED_AT;
// 创建客户端
private static final HttpClient client = HttpClient.newHttpClient();
// JSON解析
private static final ObjectMapper mapper = new ObjectMapper();

// 初始化配置文件
static {
File file = new File(JSON_FILE);
// 获取父目录
File parentDir = file.getParentFile();

// 如果父目录不存在,则创建
if (!parentDir.exists()) {
if (parentDir.mkdirs()) System.out.println("父目录创建成功");
else System.out.println("父目录创建失败");
}
// 如果文件不存在,则创建文件
if (!file.exists()) {
try {
if (file.createNewFile()) {
System.out.println("初始化123云盘配置成功");
System.out.println("开始获取AccessToken");
getAccessToken();
System.out.println("获取AccessToken成功");
} else throw new RuntimeException("初始化123云盘配置失败");
} catch (IOException e) {
throw new RuntimeException("初始化123云盘配置失败");
}
} else {
// 读取文件并给常量赋值
try {
ACCESS_TOKEN = mapper.readTree(file).get("data").get("accessToken").asText();
EXPIRED_AT = mapper.readTree(file).get("data").get("expiredAt").asText();
} catch (IOException e) {
// 获取失败大概率是因为文件为空,重新发送请求获取内容
getAccessToken();
}
}
}

/**
* 获取access_token
*
* @see <a href="https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/gn1nai4x0v0ry9ki">https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/gn1nai4x0v0ry9ki</a>
*/
public static Map<String, Object> getAccessToken() {
try {
// 请求体
String body = mapper.writeValueAsString(Map.of(
"clientID", CLIENT_ID,
"clientSecret", CLIENT_SECRET
));

// 创建请求
HttpRequest request = HttpRequest.newBuilder()
.header("platform", "open_platform")
.uri(new URI(API + "/api/v1/access_token"))
.POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8))
.build();

// 验证当前时间是否与过期时间相隔太大
ZonedDateTime givenTime = EXPIRED_AT == null ? ZonedDateTime.now() : ZonedDateTime.parse(EXPIRED_AT);
ZonedDateTime currentTime = ZonedDateTime.now();
Duration duration = Duration.between(currentTime, givenTime);

// 如果相隔时间小于3天或者令牌过期或者过期时间为空,则发送请求
if (duration.toDays() < 3 || !givenTime.isAfter(currentTime) || EXPIRED_AT.isBlank()) {
// 发送请求
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

// 获取返回响应并更新到JSON文件中
Map<String, Object> map = mapper.readValue(response.body(), Map.class);
mapper.writeValue(new File(JSON_FILE), map);

// 校验Code
Integer code = (Integer) map.get("code");
if (code == 0) System.out.println("请求成功");
else if (code == 401) throw new RuntimeException("access_token无效");
else if (code == 429) throw new RuntimeException("请求太频繁");
else throw new RuntimeException("异常 - 状态码:" + code + ";原因:" + map.get("message"));

// 更新常量
Map<String, Object> data = (Map<String, Object>) map.get("data");
ACCESS_TOKEN = (String) data.get("accessToken");
EXPIRED_AT = (String) data.get("expiredAt");

return map;
} else throw new RuntimeException("令牌过期距离过期时间过长");
} catch (URISyntaxException e) {
throw new RuntimeException("创建请求失败");
} catch (JsonProcessingException e) {
throw new RuntimeException("创建请求体失败");
} catch (IOException | InterruptedException e) {
throw new RuntimeException("发送请求失败");
}
}

/**
* 构建POST请求
*
* @param url 请求路径,带/
* @param body 请求体
* @return HttpRequest
*/
private HttpResponse<String> buildRequestPOST(String url, Map<String, Object> body) {
// 验证当前时间是否与过期时间相隔太大
ZonedDateTime givenTime = ZonedDateTime.parse(EXPIRED_AT);
ZonedDateTime currentTime = ZonedDateTime.now();
Duration duration = Duration.between(currentTime, givenTime);
// 如果令牌时间少于三天或者令牌过期则重新获取
if (duration.toDays() < 3 || !givenTime.isAfter(currentTime)) getAccessToken();

// 创建请求
HttpRequest.Builder builder = HttpRequest.newBuilder();
builder.header("Authorization", "Bearer " + ACCESS_TOKEN);
builder.header("Platform", "open_platform");
builder.header("Content-Type", "application/json");

int retry = 0;
while (true) {
try {
builder.POST(HttpRequest.BodyPublishers.ofString(mapper.writeValueAsString(body), StandardCharsets.UTF_8));
builder.uri(new URI(API + url));
return client.send(builder.build(), HttpResponse.BodyHandlers.ofString());
} catch (JsonProcessingException e) {
throw new RuntimeException("创建请求体失败");
} catch (URISyntaxException e) {
throw new RuntimeException("创建请求地址失败");
} catch (IOException | InterruptedException e) {
if (retry >= RETRY_MAX) throw new RuntimeException("发送POST请求失败");
System.out.println("发送POST请求失败,重试中...");
retry++;
}
}
}


/**
* 构建GET请求
*
* @param url 请求路径,开头带/
* @param queryString GET请求地址参数,无则填null
* @return HttpRequest
*/

private HttpResponse<String> buildRequestGET(String url, Map<String, Object> queryString) {
// 验证当前时间是否与过期时间相隔太大
ZonedDateTime givenTime = ZonedDateTime.parse(EXPIRED_AT);
ZonedDateTime currentTime = ZonedDateTime.now();
Duration duration = Duration.between(currentTime, givenTime);
// 如果令牌时间少于三天或者令牌过期则重新获取
if (duration.toDays() < 3 || !givenTime.isAfter(currentTime)) getAccessToken();

// 创建请求
HttpRequest.Builder builder = HttpRequest.newBuilder();
builder.header("Authorization", "Bearer " + ACCESS_TOKEN);
builder.header("Platform", "open_platform");
builder.GET();

// 拼接参数
StringBuilder sb = new StringBuilder();
if (queryString != null)
queryString.forEach((k, v) -> sb.append(k).append("=").append(v).append("&"));

int retry = 0;
// 请求重试
while (true) {
try {
URI uri = new URI(API + url + "?" + sb.toString());
builder.uri(uri);
return client.send(builder.build(), HttpResponse.BodyHandlers.ofString());
} catch (URISyntaxException e) {
throw new RuntimeException("创建请求地址失败");
} catch (IOException | InterruptedException e) {
if (retry >= RETRY_MAX) throw new RuntimeException("发送GET请求失败");
System.out.println("发送GET请求失败,重试中......");
retry++;
}
}
}

/**
* 校验 Code 是否正确
*
* @param code 状态码
*/
private void codeVerify(int code, String msg) {
switch (code) {
case 0 -> System.out.println("请求成功");

case 401 -> throw new RuntimeException("access_token无效");
case 429 -> throw new RuntimeException("请求太频繁");

default -> throw new RuntimeException("异常 - 状态码:" + code + ";原因:" + msg);
}
}

/**
* 响应内容处理
*
* @param response 响应内容
* @param msg 属于什么方法
* @return Map
*/
private Map<String, Object> responseProcess(HttpResponse<String> response, String msg) {
try {
Map<String, Object> map = mapper.readValue(response.body(), Map.class);
codeVerify((Integer) map.get("code"), (String) map.get("message"));
return (Map<String, Object>) map.get("data");
} catch (JsonProcessingException e) {
throw new RuntimeException(msg + " - JSON解析失败\n\t返回内容:" + response.body());
}
}

/**
* 获取用户信息
*
* @see <a href="https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/fa2w0rosunui2v4m">https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/fa2w0rosunui2v4m</a>
*/
public Map<String, Object> getUserInfo() {
System.out.println("请求用户信息......");
HttpResponse<String> send = buildRequestGET("/api/v1/user/info", null);
return responseProcess(send, "获取用户信息");
}

/**
* 创建离线下载任务<br/>
* 离线下载任务仅支持 http/https 任务创建
*
* @param url 下载资源地址(http/https) - 必填
* @param fileName 自定义文件名称(带后缀)无则填null - 非必填
* @param dirID 目录ID - 非必填
* @param callBackUrl 回调地址,无则填null - 非必填
* @see <a href="https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/wn77piehmp9t8ut4">https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/wn77piehmp9t8ut4</a>
*/
public Map<String, Object> createOfflineDownloadTask(String url, String fileName, String dirID, String callBackUrl) {
System.out.println("请求创建离线下载任务......");
Map<String, Object> body = new HashMap<>();
body.put("url", url);
body.put("fileName", fileName == null ? "" : fileName);
body.put("dirID", dirID == null ? "" : dirID);
body.put("callBackUrl", callBackUrl == null ? "" : callBackUrl);
HttpResponse<String> response = buildRequestPOST("/api/v1/offline/download", body);
return responseProcess(response, "创建离线下载任务");
}

/**
* 获取离线下载进度
*
* @param taskID 离线下载任务ID
* @see <a href="https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/dvxrtr9gfyk2lvlu">https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/dvxrtr9gfyk2lvlu</a>
*/
public Map<String, Object> getProgressYourOfflineDownload(int taskID) {
System.out.println("请求获取离线下载进度......");
Map<String, Object> map = new HashMap<>();
map.put("taskID", taskID);
HttpResponse<String> response = buildRequestGET("/api/v1/offline/download/process", map);
return responseProcess(response, "获取离线下载进度");
}

/**
* 分享链接有效期
* <br/>
* ONE_DAY - 1天<br/>
* SEVEN_DAYS - 7天<br/>
* THIRTY_DAYS - 30天<br/>
* PERMANENT - 永久
*/
public enum ShareExpire {
ONE_DAY(1),
SEVEN_DAYS(7),
THIRTY_DAYS(30),
PERMANENT(0);
private final int days;

ShareExpire(int days) {
this.days = days;
}

public int getDays() {
return days;
}
}

/**
* 创建分享链接
*
* @param shareName 分享名称 - 必填
* @param shareExpire 分享链接有效期天数 - 必填
* @param fileIDList 分享文件ID列表,以逗号分割,最大只支持拼接100个文件ID,示例:1,2,3 - 必填
* @param sharePwd 分享密码,无则填null - 选填
* @see <a href="https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/dwd2ss0qnpab5i5s">https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/dwd2ss0qnpab5i5s</a>
*/
public Map<String, Object> createSharedLink(String shareName, ShareExpire shareExpire, String fileIDList, String sharePwd) {
System.out.println("请求创建分享链接......");
Map<String, Object> body = new HashMap<>();
body.put("shareName", shareName);
body.put("shareExpire", shareExpire.getDays());
body.put("fileIDList", fileIDList);
body.put("sharePwd", sharePwd == null ? "" : sharePwd);
HttpResponse<String> response = buildRequestPOST("/api/v1/share/create", body);
return responseProcess(response, "创建分享链接");
}

/**
* 排序规则
* <br/>
* FILE_ID - 文件ID<br/>
* SIZE - 文件大小<br/>
* FILE_NAME - 文件名
*/
public enum OrderBy {
FILE_ID("file_id"),
SIZE("size"),
FILE_NAME("file_name");
private final String value;

OrderBy(String fileName) {
this.value = fileName;
}

public String getValue() {
return value;
}
}

/**
* 获取文件列表
*
* @param parentFileId 文件夹ID,根目录传0 - 必填
* @param page 页码数 - 必填
* @param limit 每页条数,最大不超过100 - 必填
* @param orderBy 排序字段 - 必填
* @param orderDirection 排序方向,1为升序,0为降序 - 必填
* @param trashed 是否查看回收站的文件 - 必填
* @param searchData 搜索关键字,无则填null - 选填
* @see <a href="https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/hosdqqax0knovnm2">https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/hosdqqax0knovnm2</a>
*/
public Map<String, Object> file_GetListOfFiles(int parentFileId, int page, int limit, OrderBy orderBy, int orderDirection, boolean trashed, String searchData) {
System.out.println("请求获取文件列表......");
Map<String, Object> body = new HashMap<>();
body.put("parentFileId", parentFileId);
body.put("page", page);
body.put("limit", Math.min(limit, 100));
body.put("orderBy", orderBy.getValue());
body.put("orderDirection", orderDirection == 1 ? "asc" : "desc");
body.put("trashed", trashed);
body.put("searchData", searchData == null ? "" : searchData);
HttpResponse<String> response = buildRequestGET("/api/v1/file/list", body);
return responseProcess(response, "获取文件列表");
}

/**
* 移动文件<br/>
* 批量移动文件,单级最多支持100个
*
* @param fileIDs 文件id数组 - 必填
* @param toParentFileID 要移动到的目标文件夹id,移动到根目录时填写0 - 必填
* @see <a href="https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/rsyfsn1gnpgo4m4f">https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/rsyfsn1gnpgo4m4f</a>
*/
public Map<String, Object> file_MoveFiles(List<String> fileIDs, String toParentFileID) {
System.out.println("请求移动文件......");
Map<String, Object> body = new HashMap<>();
body.put("fileIDs", fileIDs);
body.put("toParentFileID", toParentFileID);
HttpResponse<String> response = buildRequestPOST("/api/v1/file/move", body);
return responseProcess(response, "移动文件");
}

/**
* 删除文件至回收站<br/>
* 删除的文件,会放入回收站中
*
* @param fileIDs 文件id数组,一次性最大不能超过100 个文件 - 必填
* @see <a href="https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/en07662k2kki4bo6">https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/en07662k2kki4bo6</a>
*/
public Map<String, Object> file_DeleteFilesToRecycleBin(List<String> fileIDs) {
System.out.println("请求删除文件至回收站......");
Map<String, Object> body = new HashMap<>();
body.put("fileIDs", fileIDs);
HttpResponse<String> response = buildRequestPOST("/api/v1/file/trash", body);
return responseProcess(response, "删除文件至回收站");
}

/**
* 文件重命名<br/>
* 说明:批量重命名文件,最多支持同时30个文件重命名<br/>
* 每个成员的格式为:文件ID|新的文件名<br/>
* 示例:"114514|新的文件名"
*
* @param names 重命名列表
* @see <a href="https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/ec18ovepgciazfuc">https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/ec18ovepgciazfuc</a>
*/
public Map<String, Object> file_heavyNaming(List<String> names) {
System.out.println("请求文件重命名......");
HashMap<String, Object> map = new HashMap<>();
map.put("renameList", names);
HttpResponse<String> send = buildRequestPOST("/api/v1/file/rename", map);
return responseProcess(send, "文件重命名");
}

/**
* 从回收站恢复文件<br/>
* 将回收站的文件恢复至删除前的位置
*
* @param fileIDs 文件id数组,一次性最大不能超过100 个文件 - 必填
* @see <a href="https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/kx9f8b6wk6g55uwy">https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/kx9f8b6wk6g55uwy</a>
*/
public Map<String, Object> file_RecoverFilesFromRecycleBin(List<String> fileIDs) {
System.out.println("请求从回收站恢复文件......");
Map<String, Object> body = new HashMap<>();
body.put("fileIDs", fileIDs);
HttpResponse<String> response = buildRequestPOST("/api/v1/file/recover", body);
return responseProcess(response, "从回收站恢复文件");
}

/**
* 彻底删除文件<br/>
* 彻底删除文件前,文件必须要在回收站中,否则无法删除
*
* @param fileIDs 文件id数组,参数长度最大不超过100 - 必填
* @see <a href="https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/sg2gvfk5i3dwoxtg">https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/sg2gvfk5i3dwoxtg</a>
*/
public Map<String, Object> file_DeleteFilesCompletely(List<String> fileIDs) {
System.out.println("请求彻底删除文件......");
Map<String, Object> body = new HashMap<>();
body.put("fileIDs", fileIDs);
HttpResponse<String> response = buildRequestPOST("/api/v1/file/delete", body);
return responseProcess(response, "彻底删除文件");
}

/**
* 创建目录
*
* @param name 目录名(注:不能重名) - 必填
* @param parentID 父目录id,上传到根目录时填写0 - 必填
* @see <a href="https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/gvz09ibuuo97i5ue">https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/gvz09ibuuo97i5ue</a>
*/
public Map<String, Object> file_CreateCatalog(String name, int parentID) {
System.out.println("请求创建目录......");
Map<String, Object> body = new HashMap<>();
body.put("name", name);
body.put("parentID", parentID);
HttpResponse<String> response = buildRequestPOST("/upload/v1/file/mkdir", body);
return responseProcess(response, "创建目录");
}

/**
* 根据文件的路径算出文件的大小和MD5值
*
* @param filePath 文件路径
* @return Map("size":文件大小,"md5":文件md5)
*/
public Map<String, Object> fileSizeAndMD5(String filePath) {
System.out.println("测算 " + filePath + " 文件大小与MD5......");
Map<String, Object> map = new HashMap<>();
try {
File file = new File(filePath);
MessageDigest md5 = MessageDigest.getInstance("MD5");
FileInputStream fis = new FileInputStream(file);
byte[] buffer = new byte[1024];
int length;
while ((length = fis.read(buffer)) != -1) {
md5.update(buffer, 0, length);
}
fis.close();
byte[] digest = md5.digest();
BigInteger bigInt = new BigInteger(1, digest);
map.put("md5", bigInt.toString(16));
map.put("size", file.length());
return map;
} catch (NoSuchAlgorithmException | IOException e) {
throw new RuntimeException("测算文件大小与MD5异常 - " + e.getMessage());
}
}

/**
* 对文件进行分片<br/>
* 分片的文件保存路径为 part/{文件名}/{文件名}-{分片序号}.part
*
* @param filePath 被分片的文件
* @param partSize 分片大小
* @return Map("num":分片数,"path":保存路径,"fileName":文件名)
*/
public Map<String, Object> splitFile(String filePath, int partSize) {
System.out.println("分片中......");
File file = new File(filePath);
int numParts = (int) Math.ceil((double) file.length() / partSize);
Map<String, Object> partInfo = new HashMap<>();
try (FileInputStream fis = new FileInputStream(file)) {
// 如果文件夹不存在则创建
File dir = new File(String.format("part/%s", file.getName()));
if (!dir.exists()) dir.mkdirs();
partInfo.put("fileName", file.getName() + "-");
partInfo.put("path", String.format("part/%s", file.getName()));
// 分片
for (int i = 1; i <= numParts; i++) {
// 分片文件名
String partFileName = String.format("part/%s/%s-%d.part", file.getName(), file.getName(), i);
try (FileOutputStream fos = new FileOutputStream(partFileName)) {
byte[] buffer = new byte[partSize];
int bytesRead = fis.read(buffer);
fos.write(buffer, 0, bytesRead);
} catch (IOException e) {
e.printStackTrace();
}
}
} catch (IOException e) {
e.printStackTrace();
}

partInfo.put("num", numParts);
System.out.println("分片完成");
return partInfo;
}

/**
* 创建文件
*
* @param parentFileID 父目录id,上传到根目录时填写0 - 必填
* @param filename 文件名要小于128个字符且不能包含以下任何字符:"\/:*?|><(注:不能重名,带后缀) - 必填
* @param etag 文件md5 - 必填
* @param size 文件大小,单位为 byte 字节 - 必填
* @see <a href="https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/tutyp6gd8m20z0nz">https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/tutyp6gd8m20z0nz</a>
*/
public Map<String, Object> file_CreateFile(int parentFileID, String filename, String etag, Number size) {
System.out.println("请求创建文件......");
Map<String, Object> body = new HashMap<>();
body.put("parentFileID", parentFileID);
body.put("filename", filename);
body.put("etag", etag);
body.put("size", size);
HttpResponse<String> response = buildRequestPOST("/upload/v1/file/create", body);
return responseProcess(response, "创建文件");
}

/**
* 获取上传地址
*
* @param preuploadID 预上传ID - 必填
* @param sliceNo 分片序号,从1开始自增 - 必填
* @see <a href="https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/tefyp5usugp3lnsr">https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/tefyp5usugp3lnsr</a>
*/
public Map<String, Object> file_ObtainUploadURL(String preuploadID, int sliceNo) {
System.out.println("请求获取上传地址......");
Map<String, Object> body = new HashMap<>();
body.put("preuploadID", preuploadID);
body.put("sliceNo", sliceNo);
HttpResponse<String> response = buildRequestPOST("/upload/v1/file/get_upload_url", body);
return responseProcess(response, "获取上传地址");
}

/**
* 上传分片文件
*
* @param path 分片文件路径
* @param serverUrl 上传地址
* @return true上传成功 false上传失败
*/
public boolean uploadShardsPUT(String path, String serverUrl) {
System.out.println("分片文件 " + path + " 上传中......");
int retry = 0;
while (true) {
try {
// 初始化数据
File file = new File(path);
URL url = new URL(serverUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("PUT");
connection.setDoOutput(true);
connection.setRequestProperty("Content-Type", "application/octet-stream");
connection.setRequestProperty("Content-Length", String.valueOf(file.length()));

// 上传
OutputStream outputStream = connection.getOutputStream();
FileInputStream fileInputStream = new FileInputStream(file);
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = fileInputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}

// 上传完成
outputStream.flush();
outputStream.close();
fileInputStream.close();

// 获取响应
int responseCode = connection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
System.out.println("文件:" + path + " 上传成功");
return true;
} else {
if (retry >= RETRY_MAX) {
System.out.println("上传分片文件失败");
return false;
}
System.out.println("文件:" + path + " 上传失败,错误码:" + responseCode + "错误响应:" + connection.getResponseMessage());
System.out.println("重试上传文件:" + path);
retry++;
}
} catch (IOException e) {
if (retry >= RETRY_MAX) {
System.out.println("上传分片文件失败");
return false;
}
System.out.println("文件:" + path + " 上传失败,错误信息:" + e.getMessage());
System.out.println("重试上传文件:" + path);
retry++;
}
}
}

/**
* 列举已上传分片<br/>
* 该接口用于最后一片分片上传完成时,列出云端分片供用户自行比对。比对正确后调用上传完毕接口<br/>
* 当文件大小小于 sliceSize 分片大小时,无需调用该接口。该结果将返回空值。
*
* @param preuploadID 预备上传ID - 必填
* @see <a href="https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/vfciz4tmloogx6b6">https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/vfciz4tmloogx6b6</a>
*/
public Map<String, Object> file_ListUploadedParts(String preuploadID) {
System.out.println("请求列举已上传分片......");
Map<String, Object> body = new HashMap<>();
body.put("preuploadID", preuploadID);
HttpResponse<String> response = buildRequestPOST("/upload/v1/file/list_upload_parts", body);
return responseProcess(response, "列举已上传分片");
}

/**
* 上传完毕<br/>
* 文件上传完成后请求
*
* @param preuploadID 预上传ID - 必填
* @see <a href="https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/te21efi99a9edqd6">https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/te21efi99a9edqd6</a>
*/
public Map<String, Object> file_UploadCompleted(String preuploadID) {
System.out.println("请求上传完毕API......");
Map<String, Object> body = new HashMap<>();
body.put("preuploadID", preuploadID);
HttpResponse<String> response = buildRequestPOST("/upload/v1/file/upload_complete", body);
return responseProcess(response, "上传完毕");
}

/**
* 异步轮询获取上传结果
*
* @param preuploadID 预上传ID - 必填
* @see <a href="https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/qgg0sxkfeqygam7e">https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/qgg0sxkfeqygam7e</a>
*/
public Map<String, Object> file_AsyncPollToObtainUploadResults(String preuploadID) {
System.out.println("请求上传结果......");
Map<String, Object> body = new HashMap<>();
body.put("preuploadID", preuploadID);
HttpResponse<String> response = buildRequestPOST("/upload/v1/file/upload_async_result", body);
return responseProcess(response, "异步轮询获取上传结果");
}

/**
* 启用直链空间
*
* @param fileID 启用直链空间的文件夹的fileID - 必填
* @see <a href="https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/cl3gvdmho288d376">https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/cl3gvdmho288d376</a>
*/
public Map<String, Object> straight_EnableDirectLinkSpace(int fileID) {
System.out.println("请求启用直链空间......");
Map<String, Object> body = new HashMap<>();
body.put("fileID", fileID);
HttpResponse<String> response = buildRequestPOST("/api/v1/direct-link/enable", body);
return responseProcess(response, "启用直链空间");
}

/**
* 禁用直链空间
*
* @param fileID 禁用直链空间的文件夹的fileID - 必填
* @see <a href="https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/ccgz6fwf25nd9psl">https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/ccgz6fwf25nd9psl</a>
*/
public Map<String, Object> straight_DisableDirectLinkSpace(int fileID) {
System.out.println("请求禁用直链空间......");
Map<String, Object> body = new HashMap<>();
body.put("fileID", fileID);
HttpResponse<String> response = buildRequestPOST("/api/v1/direct-link/disable", body);
return responseProcess(response, "禁用直链空间");
}

/**
* 获取直链链接
*
* @param fileID 需要获取直链链接的文件的fileID - 必填
* @see <a href="https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/tdxfsmtemp4gu4o2">https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/tdxfsmtemp4gu4o2</a>
*/
public Map<String, Object> straight_GetADirectLink(int fileID) {
System.out.println("请求获取直链......");
Map<String, Object> body = new HashMap<>();
body.put("fileID", fileID);
HttpResponse<String> response = buildRequestGET("/api/v1/direct-link/url", body);
return responseProcess(response, "获取直链");
}

/**
* 获取直链转码链接
*
* @param fileID 启用直链空间的文件夹的fileID - 必填
* @see <a href="https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/xz2uv5t7z8bfmbrg">https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/xz2uv5t7z8bfmbrg</a>
*/
public Map<String, Object> straight_GetDirectLinkTranscode(int fileID) {
System.out.println("请求获取直链转码链接......");
Map<String, Object> body = new HashMap<>();
body.put("fileID", fileID);
HttpResponse<String> response = buildRequestGET("/api/v1/direct-link/get/m3u8", body);
return responseProcess(response, "获取直链转码链接");
}

/**
* 发起直链转码
*
* @param ids 需要转码的文件ID列表,请注意该文件必须要在直链空间下,且源文件是视频文件才能进行转码操作。<br/>
* 示例:[1,2,3,4]
* @see <a href="https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/wegmv21pgdfvolg4">https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/wegmv21pgdfvolg4</a>
*/
public Map<String, Object> straight_InitiateDirectChainTranscode(List<String> ids) {
System.out.println("请求发起直链转码......");
Map<String, Object> body = new HashMap<>();
body.put("ids", ids);
HttpResponse<String> response = buildRequestPOST("/api/v1/direct-link/doTranscode", body);
return responseProcess(response, "发起直链转码");
}

/**
* 查询直链转码进度
*
* @param ids 视频文件ID列表。<br/>
* 示例:[1,2,3,4]
* @see <a href="https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/mf5nk6zbn7zvlgyt">https://123yunpan.yuque.com/org-wiki-123yunpan-muaork/cr6ced/mf5nk6zbn7zvlgyt</a>
*/
public Map<String, Object> straight_QueryTranscodingProgress(List<String> ids) {
System.out.println("请求查询直链转码进度......");
Map<String, Object> body = new HashMap<>();
body.put("ids", ids);
HttpResponse<String> response = buildRequestPOST("/api/v1/direct-link/queryTranscode", body);
return responseProcess(response, "查询直链转码进度");
}

/**
* 根据路径文件自动完成:请求、分片、上传 操作
*
* @param path 上传文件
* @param filename 文件名
* @param catalogID 存放在那个目录中,根目录为0
*/
public Map<String, Object> uploadFile(String path, String filename, int catalogID) {
// 结果集
Map<String, Object> result = new HashMap<>();
// 获取MD5以及大小
Map<String, Object> info = fileSizeAndMD5(path);

// 请求服务器创建文件
Map<String, Object> data = file_CreateFile(catalogID, filename, (String) info.get("md5"), (Number) info.get("size"));
// 如果是秒传则直接返回
if ((boolean) data.get("reuse")) {
System.out.println("已秒传");
result.put("code", 0);
result.put("fileID", data.get("fileID"));
return result;
}

// 保存 preuploadID并分片
String preuploadID = (String) data.get("preuploadID");
Map<String, Object> splitFile = splitFile(path, (int) data.get("sliceSize"));
// 保存上传文件的MD5值
int num = (Integer) splitFile.get("num");
String[] uploadMD5 = new String[num];

// 开始上传
for (int i = 0; i < num; i++) {
// 获取上传链接
Map<String, Object> obtainUploadURLData = file_ObtainUploadURL(preuploadID, i + 1);

// 上传分片并保存MD5值
String path1 = splitFile.get("path") + "/" + splitFile.get("fileName") + (i + 1) + ".part";
uploadMD5[i] = (String) fileSizeAndMD5(path1).get("md5");
boolean uploadResults = uploadShardsPUT(path1, (String) obtainUploadURLData.get("presignedURL"));
if (!uploadResults) {
result.put("code", 1);
result.put("errorNum", (i + 1));
result.put("countNum", splitFile.get("num"));
result.put("preuploadID", preuploadID);
return result;
}
}

// 如果文件原本的大小 < sliceSize 那么不执行下面逻辑
if (!((long) info.get("size") < (int) data.get("sliceSize"))) {
List<Integer> abnormalSharding = new ArrayList<>();
try {
String map = mapper.writeValueAsString(file_ListUploadedParts(preuploadID));
// 循环校验md5值是否正确
for (int i = 0; i < uploadMD5.length; i++) {
String md5 = mapper.readTree(map).get("parts").get(i).get("etag").asText();
if (!md5.equals(uploadMD5[i])) abnormalSharding.add(i + 1);
System.out.println("分片序号" + (i + 1) + "与云端校验结果 - " + (md5.equals(uploadMD5[i]) ? "一致" : "不一致"));
}

// 如果有异常分片则返回报错
if (!abnormalSharding.isEmpty()) {
System.out.println("分片MD5异常");
result.put("code", 2);
result.put("abnormalSharding", abnormalSharding);
result.put("preuploadID", preuploadID);
System.out.println("有MD5与服务器相匹配错误的分片,逻辑会继续执行,请在执行完毕之后检查云盘是否存在文件");
}
} catch (JsonProcessingException e) {
throw new RuntimeException("Map转JSON失败");
}
}

// 上传完毕
Map<String, Object> uploadCompletedData = file_UploadCompleted(preuploadID);
if (!(boolean) uploadCompletedData.get("async")) {
result.put("code", 0);
result.put("fileID", uploadCompletedData.get("fileID"));
// 删除所有分片
deleteFolder((String) splitFile.get("path"));
return result;
} else {
// 轮询请求 60 次
int retry = 0;
while (retry <= 60) {
// 请求合并结果
Map<String, Object> asyncData = file_AsyncPollToObtainUploadResults(preuploadID);
if (!(boolean) asyncData.get("completed")) {
// 没有获取数据,阻塞1.5秒后据徐请求
try {
Thread.sleep(1500);
retry++;
} catch (InterruptedException e) {
throw new RuntimeException("阻塞时间1.5秒");
}
} else {
// 删除所有分片
deleteFolder((String) splitFile.get("path"));

// 获取到返回数据
result = new HashMap<>();
result.put("code", 0);
result.put("fileID", asyncData.get("fileID"));
return result;
}
}
}
// 删除所有分片
deleteFolder((String) splitFile.get("path"));
result.put("code", 2);
result.put("msg", "需要异步查询上传结果");
result.put("preuploadID", preuploadID);
return result;
}

/**
* 删除文件夹中所有内容
*
* @param deletePath 删除路径
*/
private static void deleteFolder(String deletePath) {
File folder = new File(deletePath);
File[] files = folder.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
// 递归删除子文件夹
deleteFolder(file.getPath());
} else {
// 删除文件
if (!file.delete()) {
System.out.println("无法删除文件: " + file);
}
}
}
}

// 删除空文件夹或子文件夹已经被删除后的文件夹
if (!folder.delete()) {
System.out.println("无法删除文件夹: " + folder);
}
}

/**
* 上传文件并获取到响应的直链<br/>
* 一定要把文件上传到直链空间内
*
* @param path 上传文件
* @param filename 文件名
* @param catalogID 目录
*/
public Map<String, Object> uploadFileAndGetDirectLink(String path, String filename, int catalogID) {
// 上传文件
Map<String, Object> result = new HashMap<>();
Map<String, Object> uploadFile = uploadFile(path, filename, catalogID);
if ((int) uploadFile.get("code") == 0) {
// 请求直链
int fileID = (int) uploadFile.get("fileID");
Map<String, Object> getADirectLink = straight_GetADirectLink(fileID);
result.put("fileID", fileID);
result.put("url", getADirectLink.get("url"));
return result;
} else {
System.out.println("上传文件失败");
return uploadFile;
}
}

/**
* URL鉴权 - 防盗链<br/>
* 对直链进行加密
*
* @param url 加密URL
* @see <a href="https://www.123pan.com/faq">https://www.123pan.com/faq</a>
*/
public String URLAuthentication(String url) {
try {
// URL解码
url = URLDecoder.decode(url, StandardCharsets.UTF_8);
String path = new URL(url).getPath();
long timestamp = new Date().getTime() / 1000 + EXPIRED_TIME_SEC;
String randomUUID = UUID.randomUUID().toString().replaceAll("-", "");
String unsignedStr = String.format("%s-%d-%s-%d-%s", path, timestamp, randomUUID, UID, PRIVATE_KEY);
// MD5加密
MessageDigest md5 = MessageDigest.getInstance("MD5");
byte[] secretBytes = md5.digest(unsignedStr.getBytes());
StringBuilder md5str = new StringBuilder();
// 把数组每一字节换成16进制连成md5字符串
int digital;
for (byte aByte : secretBytes) {
digital = aByte;
if (digital < 0) digital += 256;
if (digital < 16) md5str.append("0");
md5str.append(Integer.toHexString(digital));
}
String md5sum = md5str.toString().toLowerCase();
return url + "?auth_key=" + String.format("%d-%s-%d-", timestamp, randomUUID, UID) + md5sum;
} catch (MalformedURLException e) {
throw new RuntimeException("无效的URL");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("无效的算法");
}
}

/**
* 上传文件并获取鉴权链接
*
* @param path 上传文件
* @param filename 文件名
* @param catalogID 目录
*/
public Map<String, Object> uploadFilesAndGetAuthenticationLink(String path, String filename, int catalogID) {
// 上传文件
Map<String, Object> result = new HashMap<>();
Map<String, Object> uploadFile = uploadFile(path, filename, catalogID);
if ((int) uploadFile.get("code") == 0) {
// 请求直链
int fileID = (int) uploadFile.get("fileID");
Map<String, Object> getADirectLink = straight_GetADirectLink(fileID);
// 获取防盗链
String authentication = URLAuthentication(getADirectLink.get("url").toString());
result.put("fileID", fileID);
result.put("url", getADirectLink.get("url"));
result.put("authentication", authentication);
return result;
} else {
System.out.println("上传文件失败");
return uploadFile;
}
}
}