diff --git a/.gitignore b/.gitignore
index 1a03bac5..12fba5cd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,10 +24,25 @@ hs_err_pid*
# maven ignore
target/
-.mvn/
bin/
.sts4-cache/
*.versionsBackup
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+pom.xml.next
+release.properties
+dependency-reduced-pom.xml
+buildNumber.properties
+.mvn/timing.properties
+# https://github.com/takari/maven-wrapper#usage-without-binary-jar
+.mvn/wrapper/maven-wrapper.jar
+
+# Eclipse m2e generated files
+# Eclipse Core
+.project
+# JDT-specific (Eclipse Java Development Tools)
+.classpath
# eclipse ignore
.settings/
diff --git a/.mvn/jvm.config b/.mvn/jvm.config
new file mode 100644
index 00000000..59485b46
--- /dev/null
+++ b/.mvn/jvm.config
@@ -0,0 +1 @@
+-XX:+UseG1GC -Xmx2g -Xms2g
\ No newline at end of file
diff --git a/.mvn/maven.config b/.mvn/maven.config
new file mode 100644
index 00000000..a8ef5051
--- /dev/null
+++ b/.mvn/maven.config
@@ -0,0 +1,2 @@
+-s=.mvn/settings.xml
+-Dmaven.test.skip=true
\ No newline at end of file
diff --git a/.mvn/settings.xml b/.mvn/settings.xml
new file mode 100644
index 00000000..4e2a6f0a
--- /dev/null
+++ b/.mvn/settings.xml
@@ -0,0 +1,13 @@
+
+
+
+
+ aliyun
+ Aliyun Maven Mirror
+ https://maven.aliyun.com/repository/central
+ central
+
+
+
\ No newline at end of file
diff --git a/.mvn/wrapper/MavenWrapperDownloader.java b/.mvn/wrapper/MavenWrapperDownloader.java
new file mode 100644
index 00000000..b901097f
--- /dev/null
+++ b/.mvn/wrapper/MavenWrapperDownloader.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2007-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import java.net.*;
+import java.io.*;
+import java.nio.channels.*;
+import java.util.Properties;
+
+public class MavenWrapperDownloader {
+
+ private static final String WRAPPER_VERSION = "0.5.6";
+ /**
+ * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
+ */
+ private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
+ + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
+
+ /**
+ * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
+ * use instead of the default one.
+ */
+ private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
+ ".mvn/wrapper/maven-wrapper.properties";
+
+ /**
+ * Path where the maven-wrapper.jar will be saved to.
+ */
+ private static final String MAVEN_WRAPPER_JAR_PATH =
+ ".mvn/wrapper/maven-wrapper.jar";
+
+ /**
+ * Name of the property which should be used to override the default download url for the wrapper.
+ */
+ private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
+
+ public static void main(String args[]) {
+ System.out.println("- Downloader started");
+ File baseDirectory = new File(args[0]);
+ System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
+
+ // If the maven-wrapper.properties exists, read it and check if it contains a custom
+ // wrapperUrl parameter.
+ File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
+ String url = DEFAULT_DOWNLOAD_URL;
+ if(mavenWrapperPropertyFile.exists()) {
+ FileInputStream mavenWrapperPropertyFileInputStream = null;
+ try {
+ mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
+ Properties mavenWrapperProperties = new Properties();
+ mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
+ url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
+ } catch (IOException e) {
+ System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
+ } finally {
+ try {
+ if(mavenWrapperPropertyFileInputStream != null) {
+ mavenWrapperPropertyFileInputStream.close();
+ }
+ } catch (IOException e) {
+ // Ignore ...
+ }
+ }
+ }
+ System.out.println("- Downloading from: " + url);
+
+ File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
+ if(!outputFile.getParentFile().exists()) {
+ if(!outputFile.getParentFile().mkdirs()) {
+ System.out.println(
+ "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
+ }
+ }
+ System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
+ try {
+ downloadFileFromURL(url, outputFile);
+ System.out.println("Done");
+ System.exit(0);
+ } catch (Throwable e) {
+ System.out.println("- Error downloading");
+ e.printStackTrace();
+ System.exit(1);
+ }
+ }
+
+ private static void downloadFileFromURL(String urlString, File destination) throws Exception {
+ if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
+ String username = System.getenv("MVNW_USERNAME");
+ char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
+ Authenticator.setDefault(new Authenticator() {
+ @Override
+ protected PasswordAuthentication getPasswordAuthentication() {
+ return new PasswordAuthentication(username, password);
+ }
+ });
+ }
+ URL website = new URL(urlString);
+ ReadableByteChannel rbc;
+ rbc = Channels.newChannel(website.openStream());
+ FileOutputStream fos = new FileOutputStream(destination);
+ fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
+ fos.close();
+ rbc.close();
+ }
+
+}
diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 00000000..ffdc10e5
--- /dev/null
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,2 @@
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.1/apache-maven-3.8.1-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar
diff --git a/.sdkmanrc b/.sdkmanrc
new file mode 100644
index 00000000..48aafd15
--- /dev/null
+++ b/.sdkmanrc
@@ -0,0 +1,4 @@
+# Enable auto-env through the sdkman_auto_env config
+# Add key=value pairs of SDKs to use below
+maven=3.8.1
+java=8.0.302-open
\ No newline at end of file
diff --git a/README.md b/README.md
index 8e42b26b..e529002d 100644
--- a/README.md
+++ b/README.md
@@ -27,7 +27,7 @@
-
+
@@ -36,7 +36,7 @@
### 📚简介
一行代码将文件存储到本地、FTP、SFTP、WebDAV、阿里云 OSS、华为云 OBS、七牛云 Kodo、腾讯云 COS、百度云 BOS、又拍云 USS、MinIO、
-Amazon S3、GoogleCloud Storage、金山云 KS3、美团云 MSS、京东云 OSS、天翼云 OOS、移动 云EOS、沃云 OSS、
+Amazon S3、GoogleCloud Storage、FastDFS、 Azure Blob Storage、Cloudflare R2、金山云 KS3、美团云 MSS、京东云 OSS、天翼云 OOS、移动 云EOS、沃云 OSS、
网易数帆 NOS、Ucloud US3、青云 QingStor、平安云 OBS、首云 OSS、IBM COS、其它兼容 S3 协议的存储平台。查看 [所有支持的存储平台](https://x-file-storage.xuyanwu.cn/#/存储平台)
💡 通过 WebDAV 连接到 Alist 后,可以使用百度网盘、天翼云盘、阿里云盘、迅雷网盘等常见存储服务,查看 [Alist 支持的存储平台](https://alist-doc.nn.ci/docs/webdav)
@@ -51,6 +51,8 @@ Gitee:https://gitee.com/dromara/x-file-storage
这里是简要的更新记录,查看 [详细的更新记录](https://x-file-storage.xuyanwu.cn/#/更新记录)
+`2.1.0` 修复大量问题,新增存储平台 FastDFS 和 Azure Blob Storage,新增复制、移动(重命名)文件,手动分片上传(断点续传)和计算哈希等功能,详情查看 [更新记录](https://x-file-storage.xuyanwu.cn/#/更新记录?id=_210)
+
`2.0.0` 捐赠至 [dromara](https://dromara.org/zh) 开源社区,更改项目名、包名,优化项目结构、支持 Metadata 元数据等,从旧版升级需要注意,详情查看 [更新记录](https://x-file-storage.xuyanwu.cn/#/更新记录?id=_200)
`1.0.3` 修复了 FileStorageClientFactory 未自动加载等问题,查看 [更新记录](https://x-file-storage.xuyanwu.cn/#/更新记录?id=_103)
@@ -65,11 +67,11 @@ Gitee:https://gitee.com/dromara/x-file-storage
### 📅更新计划
-- 接入存储平台:HDFS、FastDFS、杉岩 OBS、Samba、NFS
-- 大文件手动分片上传(1.0.0 已支持大文件自动分片上传)
-- 复制或移动文件
+- 接入存储平台:HDFS、火山云 TOS、Samba、NFS
+- 用户端直传
+- 追加缩略图
+- 列出文件
- 文件内容预加载
-- 上传无需强制获取 Size
- 新增 Access 模块,尝试通过 HTTP、FTP、WebDAV 等协议对外提供接口,方便其它程序使用
-------
@@ -87,7 +89,7 @@ Gitee:https://gitee.com/dromara/x-file-storage
org.dromara.x-file-storage
x-file-storage-spring
- 2.0.0
+ 2.1.0
@@ -99,6 +101,8 @@ Gitee:https://gitee.com/dromara/x-file-storage
`application.yml` 配置文件中添加以下基础配置
+关于配置文件及 FileInfo 中各种路径(path)的区别,可以参考 [常见问题](https://x-file-storage.xuyanwu.cn/#/常见问题?id=配置文件及-fileinfo-中各种路径(path)的区别?)
+
```yaml
dromara:
x-file-storage: #文件存储配置
@@ -263,6 +267,11 @@ X File Storage 的源码分为两个分支,功能如下:
5. 登录 Gitee 或 Github 在你首页可以看到一个 pull request 按钮,点击它,填写一些说明信息,然后提交即可
6. 等待维护者合并
+#### 🧬 开发约定 & 配置
+1. JDK 11+、Maven 3.8.1+
+2. 代码风格,目前代码风格通过 spotless-maven-plugin + palantir-java-format 统一控制,Maven 构建时统一格式化代码
+3. 在开发阶段,IDE 要识别到 spotless-maven-plugin + palantir-java-format 需要安装插件:[palantir-java-format](https://plugins.jetbrains.com/plugin/13180-palantir-java-format)
+
#### 📐PR遵照的原则
欢迎任何人为 X File Storage 添砖加瓦,贡献代码,为了易用性和可维护性,需要提交的 pr(pull request)符合一些规范,规范如下:
diff --git a/docs/Metadata.md b/docs/Metadata.md
index f51fef15..90c0260b 100644
--- a/docs/Metadata.md
+++ b/docs/Metadata.md
@@ -2,7 +2,7 @@
## 使用
-可以在上传时传入 Metadata 和 UserMetadata ,目前仅 华为云 OBS、阿里云 OSS、腾讯云 COS、百度云 BOS、七牛云 Kodo、又拍云 USS、MinIO、Amazon S3、GoogleCloud Storage 平台支持
+可以在上传时传入 Metadata 和 UserMetadata ,目前仅 华为云 OBS、阿里云 OSS、腾讯云 COS、百度云 BOS、七牛云 Kodo、又拍云 USS、MinIO、Amazon S3、GoogleCloud Storage、FastDFS、Azure Blob Storage 平台支持
```java
//判断是否支持 Metadata
@@ -34,15 +34,30 @@ FileInfo fileInfo = fileStorageService.of(file)
```yaml
dromara:
x-file-storage:
- upload-not-support-metadata-throw-exception: false
+ upload-not-support-metadata-throw-exception: false # 上传时
+ copy-not-support-metadata-throw-exception: false # 复制时
+ move-not-support-metadata-throw-exception: false # 移动时
```
**第二种(仅当前)**
```java
+//上传时
FileInfo fileInfo = fileStorageService.of(file)
.setNotSupportMetadataThrowException(false) //在不支持 Metadata 的存储平台不抛出异常
.putUserMetadata("role","666")
.upload();
+
+//复制时
+FileInfo fileInfo = fileStorageService.copy(fileInfo)
+ .setNotSupportMetadataThrowException(false) //在不支持 Metadata 的存储平台不抛出异常
+ .setPlatform("local-plus-1")
+ .copy();
+
+//移动时
+FileInfo fileInfo = fileStorageService.move(fileInfo)
+ .setNotSupportMetadataThrowException(false) //在不支持 Metadata 的存储平台不抛出异常
+ .setPlatform("local-plus-1")
+ .move();
```
diff --git a/docs/README.md b/docs/README.md
index 65b233fe..a06d28ba 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -22,21 +22,22 @@
-
+
-
-
-
+
+
+[tg.md](https://x-file-storage.xuyanwu.cn/assets/tg/tg.md ':include')
+
-------
# 📚简介
一行代码将文件存储到本地、FTP、SFTP、WebDAV、阿里云 OSS、华为云 OBS、七牛云 Kodo、腾讯云 COS、百度云 BOS、又拍云 USS、MinIO、
-Amazon S3、GoogleCloud Storage、金山云 KS3、美团云 MSS、京东云 OSS、天翼云 OOS、移动 云EOS、沃云 OSS、
+Amazon S3、GoogleCloud Storage、FastDFS、 Azure Blob Storage、Cloudflare R2、金山云 KS3、美团云 MSS、京东云 OSS、天翼云 OOS、移动 云EOS、沃云 OSS、
网易数帆 NOS、Ucloud US3、青云 QingStor、平安云 OBS、首云 OSS、IBM COS、其它兼容 S3 协议的存储平台。查看 [所有支持的存储平台](存储平台)
💡 通过 WebDAV 连接到 Alist 后,可以使用百度网盘、天翼云盘、阿里云盘、迅雷网盘等常见存储服务,查看 [Alist 支持的存储平台](https://alist-doc.nn.ci/docs/webdav)
@@ -51,6 +52,8 @@ Gitee:https://gitee.com/dromara/x-file-storage
这里是简要的更新记录,查看 [详细的更新记录](更新记录)
+`2.1.0` 修复大量问题,新增存储平台 FastDFS 和 Azure Blob Storage,新增复制、移动(重命名)文件,手动分片上传(断点续传)和计算哈希等功能,详情查看 [更新记录](更新记录?id=_210)
+
`2.0.0` 捐赠至 [dromara](https://dromara.org/zh) 开源社区,更改项目名、包名,优化项目结构、支持 Metadata 元数据等,从旧版升级需要注意,详情查看 [更新记录](更新记录?id=_200)
`1.0.3` 修复了 FileStorageClientFactory 未自动加载等问题,查看 [更新记录](更新记录?id=_103)
@@ -65,11 +68,11 @@ Gitee:https://gitee.com/dromara/x-file-storage
# 📅更新计划
-- 接入存储平台:HDFS、FastDFS、杉岩 OBS、Samba、NFS
-- 大文件手动分片上传(1.0.0 已支持大文件自动分片上传)
-- 复制或移动文件
+- 接入存储平台:HDFS、火山云 TOS、Samba、NFS
+- 用户端直传
+- 追加缩略图
+- 列出文件
- 文件内容预加载
-- 上传无需强制获取 Size
- 新增 Access 模块,尝试通过 HTTP、FTP、WebDAV 等协议对外提供接口,方便其它程序使用
-------
@@ -288,6 +291,15 @@ X File Storage 感谢各位小伙伴的信任与支持,如果您已经在项
+
+
+
+
+
+
+
+
+
diff --git a/docs/_navbar.md b/docs/_navbar.md
index 1367826f..42a7ab40 100644
--- a/docs/_navbar.md
+++ b/docs/_navbar.md
@@ -1,5 +1,6 @@
-* 🌟文档版本 2.0.0
+* 🌟文档版本 2.1.0
+ * [2.1.0](https://x-file-storage.xuyanwu.cn/2.1.0/)
* [2.0.0](https://x-file-storage.xuyanwu.cn/2.0.0/)
* [1.0.3](https://x-file-storage.xuyanwu.cn/1.0.3/)
* [1.0.2](https://x-file-storage.xuyanwu.cn/1.0.2/)
diff --git a/docs/_sidebar.md b/docs/_sidebar.md
index 2677c2c2..5ddd343f 100644
--- a/docs/_sidebar.md
+++ b/docs/_sidebar.md
@@ -7,6 +7,7 @@
* [🥦存储平台](存储平台 "存储平台")
* [🌽文件适配器](文件适配器 "文件适配器")
* [🔍️识别文件的 MIME 类型](识别文件的MIME类型 "识别文件的 MIME 类型")
+* [🍵️计算哈希](hash "hash")
* [🧪切面](切面 "切面")
* [🌱脱离 SpringBoot 单独使用](脱离SpringBoot单独使用 "脱离 SpringBoot 单独使用")
* [🙋♂️常见问题](常见问题 "常见问题")
diff --git a/docs/acl.md b/docs/acl.md
index e125c0fc..61297b93 100644
--- a/docs/acl.md
+++ b/docs/acl.md
@@ -1,6 +1,8 @@
# ACL 访问控制列表
-也叫预定义访问策略,目前仅 华为云 OBS、阿里云 OSS、腾讯云 COS、百度云 BOS、Amazon S3、GoogleCloud Storage 平台支持
+也叫预定义访问策略,目前仅 华为云 OBS、阿里云 OSS、腾讯云 COS、百度云 BOS、Amazon S3、GoogleCloud Storage、Azure Blob Storage 平台支持
+
+Azure Blob Storage 即使将文件的 ACL 设置为 公共读`PUBLIC_READ` ,上传成功后的 `url` 也无法通过浏览器直接公开访问 ,详情阅读 [兼容性说明-AzureBlobStorage](存储平台?id=OCI_AzureBlobStorage) 章节
## 设置 ACL
@@ -109,6 +111,27 @@ fileStorageService.of(file).setFileAcl(Arrays.asList(acl2,acl3)).upload();
```
+#### **Azure Blob Storage**
+
+ACL 参考文档:https://learn.microsoft.com/zh-cn/azure/storage/blobs/data-lake-storage-access-control
+SDK 参考文档:https://learn.microsoft.com/zh-cn/azure/storage/blobs/data-lake-storage-acl-java#set-acls
+
+```java
+//第一种:使用官方 SDK 中的 PathPermissions 对象
+PathPermissions permissions = new PathPermissions()
+ .setGroup(new RolePermissions().setReadPermission(true))
+ .setOwner(new RolePermissions().setReadPermission(true).setWritePermission(true));
+fileStorageService.of(file).setFileAcl(permissions).upload();
+
+//第二种,使用官方 SDK 中的 PathAccessControlEntry 对象
+PathAccessControlEntry acl = PathAccessControlEntry.parse("user::rw-");
+fileStorageService.of(file).setFileAcl(acl).upload();
+
+//第二种可以一次设置多个
+List acl = PathAccessControlEntry.parseList("user::rw-,group::r--,other::---");
+fileStorageService.of(file).setFileAcl(acl).upload();
+```
+
> [!WARNING|label:重要提示:]
@@ -142,13 +165,28 @@ AccessControlList acl = client.getObjectAcl(fileStorage.getBucketName(),fileStor
```yaml
dromara:
x-file-storage:
- upload-not-support-alc-throw-exception: false
+ upload-not-support-alc-throw-exception: false # 上传时
+ copy-not-support-alc-throw-exception: false # 复制时
+ move-not-support-alc-throw-exception: false # 移动时
```
**第二种(仅当前)**
```java
+//上传时
FileInfo fileInfo = fileStorageService.of(file)
.setNotSupportAclThrowException(false) //在不支持 ACL 的存储平台不抛出异常
.setAcl(Constant.ACL.PRIVATE)
.upload();
+
+//复制时
+FileInfo fileInfo = fileStorageService.copy(fileInfo)
+ .setNotSupportAclThrowException(false) //在不支持 ACL 的存储平台不抛出异常
+ .setPlatform("local-plus-1")
+ .copy();
+
+//移动时
+FileInfo fileInfo = fileStorageService.move(fileInfo)
+ .setNotSupportAclThrowException(false) //在不支持 ACL 的存储平台不抛出异常
+ .setPlatform("local-plus-1")
+ .move();
```
diff --git a/docs/hash.md b/docs/hash.md
new file mode 100644
index 00000000..d0916e7f
--- /dev/null
+++ b/docs/hash.md
@@ -0,0 +1,100 @@
+# 计算哈希
+
+可以在上传、下载的同时计算文件的哈希值,例如 `MD5` `SHA256` 等,更多哈希以实际 API 为准
+
+### 直接上传文件时
+
+```java
+FileInfo fileInfo = fileStorageService.of(file)
+ .setHashCalculatorMd5() //计算 MD5
+ .setHashCalculatorSha256() //计算 SHA256
+ .setHashCalculator(Constant.Hash.MessageDigest.MD2) //指定哈希名称,这里定义了一些常用的哈希名称
+ .setHashCalculator("SHA-512") //指定哈希名称,内部是通过 MessageDigest 来计算哈希值的,只要是 MessageDigest 支持的名称就都可以
+ .setHashCalculator(MessageDigest.getInstance("SHA-384")) //指定 MessageDigest
+ .upload();
+
+//上传成功后即可这样获取到对应的哈希值
+HashInfo hashInfo = fileInfo.getHashInfo();
+String md5 = hashInfo.getMd5();
+String sha256 = hashInfo.getSha256();
+```
+
+### 手动分片上传-上传分片时
+
+注意,在上传到本地、又拍云 USS 等存储平台时,会自动调用 setHashCalculatorMd5() 方法计算 MD5 作为分片的 etag 值,其它存储平台不会
+
+```java
+FilePartInfo filePartInfo = fileStorageService.uploadPart(fileInfo, partNumber, bytes, (long) bytes.length)
+ .setHashCalculatorMd5() //计算 MD5
+ .setHashCalculatorSha256() //计算 SHA256
+ .setHashCalculator(Constant.Hash.MessageDigest.MD2) //指定哈希名称,这里定义了一些常用的哈希名称
+ .setHashCalculator("SHA-512") //指定哈希名称,内部是通过 MessageDigest 来计算哈希值的,只要是 MessageDigest 支持的名称就都可以
+ .setHashCalculator(MessageDigest.getInstance("SHA-384")) //指定 MessageDigest
+ .upload();
+
+//上传成功后即可这样获取到对应的哈希值
+HashInfo hashInfo = filePartInfo.getHashInfo();
+String md5 = hashInfo.getMd5();
+String sha256 = hashInfo.getSha256();
+```
+
+### 下载时
+
+```java
+Downloader downloader = fileStorageService.downloadTh(fileInfo)
+ .setHashCalculatorMd5() //计算 MD5
+ .setHashCalculatorSha256() //计算 SHA256
+ .setHashCalculator(Constant.Hash.MessageDigest.MD2) //指定哈希名称,这里定义了一些常用的哈希名称
+ .setHashCalculator("SHA-512") //指定哈希名称,内部是通过 MessageDigest 来计算哈希值的,只要是 MessageDigest 支持的名称就都可以
+ .setHashCalculator(MessageDigest.getInstance("SHA-384")); //指定 MessageDigest
+
+//下载为 byte[]
+byte[] thBytes = downloader.bytes();
+
+//下载成功后即可这样获取到对应的哈希值
+HashInfo hashInfo = downloader.getHashCalculatorManager().getHashInfo();
+String md5 = hashInfo.getMd5();
+String sha256 = hashInfo.getSha256();
+```
+
+### 自定义计算哈希
+
+只需要实现 `HashCalculator` 接口即可,这里用 `MD5` 举例
+
+```java
+FileInfo fileInfo = fileStorageService.of(file)
+ .setHashCalculator(new HashCalculator() {
+ private final MessageDigest messageDigest = MessageDigest.getInstance("MD5");
+
+ /**
+ * 获取哈希名称,例如 MD5、SHA1、SHA256等
+ */
+ @Override
+ public String getName() {
+ return messageDigest.getAlgorithm();
+ }
+
+ /**
+ * 获取哈希值,一般情况下获取后将不能继续增量计算哈希
+ */
+ @Override
+ public String getValue() {
+ return HexUtil.encodeHexStr(messageDigest.digest());
+ }
+
+ /**
+ * 增量计算哈希
+ * @param bytes 字节数组
+ */
+ @Override
+ public void update(byte[] bytes) {
+ messageDigest.update(bytes);
+ }
+ })
+ .upload();
+
+//上传成功后即可这样获取到对应的哈希值
+HashInfo hashInfo = fileInfo.getHashInfo();
+String md5 = hashInfo.get("MD5");
+```
+
diff --git a/docs/index.html b/docs/index.html
index a33875dd..07fafca5 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -7,8 +7,8 @@
-
+