【工具类】Java实现支持S3、阿里云OSS、腾讯云COS、MinIO、FTP、SFTP、本地存储等文件的常用操作

代码有点长,可能会有点卡

功能说明

  • S3、阿里云OSS、腾讯云COS、MinIO使用的是S3兼容协议,所以大部分代码可以通用
  • 支持的文件常用操作有:
    • 创建桶、判断桶是否存在、删除桶
    • 创建文件夹
    • 上传文件、下载文件、判断文件是否存在、重命名、复制文件、获取文件外链(部分支持)、删除文件、批量删除文件
    • 获取文件详情、条件查询文件列表(文件名模糊查询、多个文件后缀查询、查询某个目录下面的直接文件和目录)

文件说明

具体代码

POM文件导入依赖

<!--S3兼容协议API-->
<dependency>
	<groupId>software.amazon.awssdk</groupId>
	<artifactId>aws-sdk-java</artifactId>
	<version>2.17.201</version>
</dependency>

<!-- FTP -->
<dependency>
	<groupId>commons-net</groupId>
	<artifactId>commons-net</artifactId>
	<version>3.9.0</version>
</dependency>

<!-- SFTP -->
<dependency>
	<groupId>com.jcraft</groupId>
	<artifactId>jsch</artifactId>
	<version>0.1.54</version>
</dependency>

<!-- yaml工具 -->
<dependency>
    <groupId>org.yaml</groupId>
    <artifactId>snakeyaml</artifactId>
    <version>2.2</version>
</dependency>

实体类

import cn.hutool.core.text.CharSequenceUtil;
import com.smartadmin.common.model.StorageConfig;
import lombok.Getter;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * 存储配置属性类
 *
 * @Author: hanYong
 * @CreateTime: 2021-07-18
 */
@Getter
public class StorageProperties {

    private List<StorageConfig> client = new ArrayList<>();

    public void setClient(List<StorageConfig> client) {
        this.client = client;
    }


    /**
     * 根据存储平台获取存储平台配置
     *
     * @param platform 存储平台
     * @return 存储平台配置
     */
    public StorageConfig getStorageConfig(String platform) {
        for (StorageConfig storageConfig : client) {
            if (CharSequenceUtil.equals(storageConfig.getPlatform(), platform)) {
                return storageConfig;
            }
        }

        return null;
    }

    /**
     * 判断是否开启了某个存储平台
     *
     * @param platform 存储平台
     * @return 结果
     */
    public Boolean isEnableStorage(String platform){
        StorageConfig storageConfig = getStorageConfig(platform);
        return Objects.nonNull(storageConfig) && storageConfig.getEnable();
    }

}
import com.smartadmin.common.api.AliyunOssFileOperatorApi;
import com.smartadmin.common.api.AmazonS3FileOperatorApi;
import com.smartadmin.common.api.FileOperatorApi;
import com.smartadmin.common.api.FtpFileOperatorApi;
import com.smartadmin.common.api.LocalFileOperatorApi;
import com.smartadmin.common.api.MinIoFileOperatorApi;
import com.smartadmin.common.api.SftpFileOperatorApi;
import com.smartadmin.common.api.TencentCosFileOperatorApi;
import lombok.Getter;

/**
 * 存储平台
 *
 * @Author: hanYong
 * @CreateTime: 2021-07-18
 */
@Getter
public enum StoragePlatformEnum {

    /**
     * minio
     */
    FTP("ftp", FtpFileOperatorApi.class),

    /**
     * minio
     */
    SFTP("sftp", SftpFileOperatorApi.class),

    /**
     * minio
     */
    MINIO("minio", MinIoFileOperatorApi.class),

    /**
     * 阿里云oss
     */
    ALIYUN_OSS("aliyun-oss", AliyunOssFileOperatorApi.class),

    /**
     * 腾讯云cos
     */
    TENCENT_COS("tencent-cos", TencentCosFileOperatorApi.class),


    /**
     * 腾讯云cos
     */
    AMAZON_S3("amazon-s3", AmazonS3FileOperatorApi.class),

    /**
     * 本地存储
     */
    LOCAL("local", LocalFileOperatorApi.class);


    private final String value;

    private final Class<? extends FileOperatorApi> aClass;

    StoragePlatformEnum(String value, Class<? extends FileOperatorApi> aClass) {
        this.value = value;
        this.aClass = aClass;
    }

}

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;

import java.util.Date;
import java.util.List;

@Data
public class FileInfo {

    /**
     * 文件唯一标识
     */
    private String key;

    /**
     * 存储平台标识
     */
    private String platform;

    /**
     * 存储空间
     */
    private String bucket;

    /**
     * 文件名
     */
    private String name;

    /**
     * 是否为目录
     */
    private Boolean isDirectory;

    /**
     * 原文件名
     */
    private String oldFilename;

    /**
     * 文件外链过期时间
     */
    private Long timeoutMillis;

    /**
     * 文件路径
     */
    private String path;

    /**
     * 文件类别
     */
    private String fileCategory;

    /**
     * 文件类型
     */
    private String type;

    /**
     * 文件大小(单位:字节)
     */
    private Long size;

    /**
     * 友好的文件大小信息,例如 KB、MB、GB 等
     */
    private String friendlySize;

    /**
     * 修改日期
     */
    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    private Date lastModified;

    /**
     * 备注
     */
    private String remark;

    /**
     * 文件唯一标识集合
     */
    private List<String> keys;


}

import lombok.Data;

import java.util.ArrayList;
import java.util.List;

@Data
public class ListFileInfo {

    /**
     * 文件唯一标识
     */
    private String key;

    /**
     * 文件名
     */
    private String name;

    /**
     * 文件路径
     */
    private String path;

    /**
     * 文件类型
     */
    private List<String> types = new ArrayList<>();


    /**
     * 文件唯一标识集合
     */
    private List<String> keys = new ArrayList<>();

    /**
     * 排序字段
     */
    private String orderBy = "lastModified asc";
}
import com.smartadmin.common.utils.base.ApplicationUtil;
import lombok.Data;

/**
 * 存储配置实体类
 *
 * @Author: hanYong
 * @CreateTime: 2021-07-18
 */
@Data
public class StorageConfig {

    /**
     * 存储平台标识
     */
    private String platform = "local";

    /**
     * 开启存储
     */
    private Boolean enable = true;

    /**
     * 请求域名
     */
    private String endpoint;

    /**
     * 访问密钥
     */
    private String accessKey;

    /**
     * 访问密码
     */
    private String accessSecret;

    /**
     * 存储空间
     */
    private String bucket = ApplicationUtil.getProjectName();

    /**
     * 基础路径(只有本地才用到)
     */
    private String basePath;

}
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.smartadmin.common.enums.StoragePlatformEnum;
import com.smartadmin.common.model.FileInfo;
import com.smartadmin.common.model.ListFileInfo;
import com.smartadmin.common.model.StorageConfig;
import com.smartadmin.common.properties.StorageProperties;

import java.io.InputStream;
import java.util.List;
import java.util.Map;
import java.util.Optional;

/**
 * 文件操作API
 * <p>
 * 如果存在未包含的操作,可以调用getClient()自行获取client进行操作
 *
 * @author: hanYong
 * @createTime: 2023-08-29
 */
public interface FileOperatorApi {


    /**
     * 关闭客户端
     */
    void closeClient();

    /**
     * 获取客户端
     */
    Object getClient();

    /**
     * 判断存储空间是否存在
     *
     * @return @return Boolean {@code true} 存在 {@code false} 不存在
     */
    Boolean existBucket();

    /**
     * 判断存储空间是否存在
     *
     * @param bucket 存储空间
     * @return @return Boolean {@code true} 存在 {@code false} 不存在
     */
    Boolean existBucket(String bucket);

    /**
     * 创建存储空间
     *
     * @return Boolean {@code true} 创建成功 {@code false} 创建失败
     */
    Boolean createBucket();

    /**
     * 创建存储空间
     *
     * @param bucket 存储空间
     * @return Boolean {@code true} 创建成功 {@code false} 创建失败
     */
    Boolean createBucket(String bucket);

    /**
     * 创建存储空间
     *
     * @param bucket 存储空间
     * @param expirationDays 存储空间的生命周期,单位天
     * @return Boolean {@code true} 创建成功 {@code false} 创建失败
     */
    Boolean createBucket(String bucket, Integer expirationDays);

    /**
     * 删除存储空间
     *
     * @return Boolean {@code true} 删除成功 {@code false} 删除失败
     */
    Boolean deleteBucket();

    /**
     * 删除存储空间
     *
     * @param bucket 存储空间
     * @return Boolean {@code true} 删除成功 {@code false} 删除失败
     */
    Boolean deleteBucket(String bucket);

    /**
     * 判断文件是否存在
     *
     * @param key 唯一标示id,例如a.txt, doc/a.txt
     * @return Boolean {@code true} 存在 {@code false} 不存在
     */
    Boolean existFile(String key);

    /**
     * 判断文件是否存在
     *
     * @param bucket 存储空间
     * @param key 唯一标示id,例如a.txt, doc/a.txt
     * @return Boolean {@code true} 存在 {@code false} 不存在
     */
    Boolean existFile(String bucket, String key);

    /**
     * 创建文件夹
     *
     * @param path 文件夹路径
     * @return Boolean {@code true} 创建成功 {@code false} 创建失败
     */
    Boolean createFolder(String path);

    /**
     * 创建文件夹
     *
     * @param bucket 存储空间
     * @param path 文件夹路径
     * @return Boolean {@code true} 创建成功 {@code false} 创建失败
     */
    Boolean createFolder(String bucket, String path);

    /**
     * 上传文件
     *
     * @param bytes 文件字节数组
     */
    String uploadFile(byte[] bytes);

    /**
     * 上传文件
     *
     * @param inputStream 文件流
     */
    String uploadFile(InputStream inputStream);

    /**
     * 上传文件
     *
     * @param key   唯一标示id,例如a.txt, doc/a.txt
     * @param bytes 文件字节数组
     */
    String uploadFile(String key, byte[] bytes);

    /**
     * 上传文件
     *
     * @param key         唯一标示id,例如a.txt, doc/a.txt
     * @param inputStream 文件流
     */
    String uploadFile(String key, InputStream inputStream);

    /**
     * 上传文件
     *
     * @param bucket 存储空间
     * @param key    唯一标示id,例如a.txt, doc/a.txt
     * @param bytes  文件字节数组
     */
    String uploadFile(String bucket, String key, byte[] bytes);

    /**
     * 上传文件
     *
     * @param bucket      存储空间
     * @param key         唯一标示id,例如a.txt, doc/a.txt
     * @param inputStream 文件流
     */
    String uploadFile(String bucket, String key, InputStream inputStream);

    /**
     * 上传文件
     *
     * @param bucket         存储空间
     * @param key            唯一标示id,例如a.txt, doc/a.txt
     * @param bytes          文件字节数组
     * @param objectMetadata 元数据信息
     */
    String uploadFile(String bucket, String key, byte[] bytes, Map<String, String> objectMetadata);

    /**
     * 上传文件
     *
     * @param bucket         存储空间
     * @param key            唯一标示id,例如a.txt, doc/a.txt
     * @param inputStream    文件流
     * @param objectMetadata 元数据信息
     */
    String uploadFile(String bucket, String key, InputStream inputStream, Map<String, String> objectMetadata);

    /**
     * 下载文件
     *
     * @param key 唯一标示id,例如a.txt, doc/a.txt
     * @return byte[] 字节数组为文件的字节数组
     */
    byte[] downloadFile(String key);

    /**
     * 下载文件
     *
     * @param bucket 存储空间
     * @param key    唯一标示id,例如a.txt, doc/a.txt
     * @return byte[] 字节数组为文件的字节数组
     */
    byte[] downloadFile(String bucket, String key);

    /**
     * 复制文件
     *
     * @param originFileKey 源文件名称
     * @param newFileKey    新文件名称
     */
    Boolean copyFile(String originFileKey, String newFileKey);

    /**
     * 复制文件
     *
     * @param originBucketName 源存储空间
     * @param originFileKey    源文件名称
     * @param newBucketName    新存储空间
     * @param newFileKey       新文件名称
     */
    Boolean copyFile(String originBucketName, String originFileKey, String newBucketName, String newFileKey);

    /**
     * 重命名文件
     *
     * @param originFileKey 源文件名称
     * @param newFileKey    新文件名称
     */
    Boolean renameFile(String originFileKey, String newFileKey);

    /**
     * 重命名文件
     *
     * @param bucket        存储空间
     * @param originFileKey 源文件名称
     * @param newFileKey    新文件名称
     */
    Boolean renameFile(String bucket, String originFileKey, String newFileKey);

    /**
     * 获取文件的下载地址,生成外网地址
     *
     * @param key           文件唯一标识
     * @param timeoutMillis url失效时间,单位毫秒(最高7天)
     * @return 外部系统可以直接访问的url
     */
    String getFileLink(String key, Long timeoutMillis);

    /**
     * 获取文件的下载地址,生成外网地址
     *
     * @param bucket        存储空间
     * @param key           文件唯一标识
     * @param timeoutMillis url失效时间,单位毫秒(最高7天)
     * @return 外部系统可以直接访问的url
     */
    String getFileLink(String bucket, String key, Long timeoutMillis);

    /**
     * 删除文件
     *
     * @param key 文件唯一标识
     */
    Boolean deleteFile(String key);

    /**
     * 删除文件
     *
     * @param bucket 存储空间
     * @param key    文件唯一标识
     */
    Boolean deleteFile(String bucket, String key);

    /**
     * 批量删除文件
     *
     * @param keys 文件唯一标识集合
     */
    Boolean deleteFiles(List<String> keys);

    /**
     * 批量删除文件
     *
     * @param bucket 存储空间
     * @param keys 文件唯一标识集合
     */
    Boolean deleteFiles(String bucket, List<String> keys);

    /**
     * 获取文件信息
     *
     * @param key    文件唯一标识
     * @return 文件信息
     */
    FileInfo getFileInfo(String key);

    /**
     * 获取文件信息
     *
     * @param bucket 存储空间
     * @param key    文件唯一标识
     * @return 文件信息
     */
    FileInfo getFileInfo(String bucket, String key);

    /**
     * 获取文件列表
     *
     * @param listFileInfo 查询条件
     * @return 文件列表
     */
    List<FileInfo> listFile(ListFileInfo listFileInfo);

    /**
     * 获取文件列表
     *
     * @param bucket 存储空间
     * @param listFileInfo 查询条件
     * @return 文件列表
     */
    List<FileInfo> listFile(String bucket, ListFileInfo listFileInfo);

    /**
     * 获取文件元数据信息
     *
     * @param key    文件唯一标识
     * @return 元数据信息
     */
    Map<String, String> getObjectMetadata(String key);

    /**
     * 获取文件元数据信息
     *
     * @param bucket 存储空间
     * @param key    文件唯一标识
     * @return 元数据信息
     */
    Map<String, String> getObjectMetadata(String bucket, String key);

    /**
     * 获取当前api的文件存储类型
     */
    StoragePlatformEnum getStoragePlatformEnum();

    /**
     * 获取存储空间,如果没传存储空间,就返回默认的
     *
     * @param bucket 存储空间
     * @return 存储空间
     */
    default String getBucketName(String bucket) {
        if (StrUtil.isNotBlank(bucket)) {
            return bucket;
        }

        StorageProperties storageProperties = SpringUtil.getBean(StorageProperties.class);
        bucket = Optional.ofNullable(storageProperties)
                .map(storageProperties1 -> storageProperties1.getStorageConfig(getStoragePlatformEnum().getValue()))
                .map(StorageConfig::getBucket)
                .orElse(null);

        return bucket;
    }

}

S3兼容协议文件API

import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.file.FileNameUtil;
import cn.hutool.core.io.unit.DataSizeUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.text.StrPool;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import com.smartadmin.common.enums.StoragePlatformEnum;
import com.smartadmin.common.model.FileInfo;
import com.smartadmin.common.model.ListFileInfo;
import com.smartadmin.common.model.StorageConfig;
import lombok.extern.slf4j.Slf4j;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.core.ResponseBytes;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.core.sync.ResponseTransformer;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.S3Configuration;
import software.amazon.awssdk.services.s3.model.BucketLifecycleConfiguration;
import software.amazon.awssdk.services.s3.model.CommonPrefix;
import software.amazon.awssdk.services.s3.model.CopyObjectRequest;
import software.amazon.awssdk.services.s3.model.CopyObjectResponse;
import software.amazon.awssdk.services.s3.model.CreateBucketRequest;
import software.amazon.awssdk.services.s3.model.CreateBucketResponse;
import software.amazon.awssdk.services.s3.model.Delete;
import software.amazon.awssdk.services.s3.model.DeleteBucketRequest;
import software.amazon.awssdk.services.s3.model.DeleteBucketResponse;
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
import software.amazon.awssdk.services.s3.model.DeleteObjectsRequest;
import software.amazon.awssdk.services.s3.model.DeleteObjectsResponse;
import software.amazon.awssdk.services.s3.model.ExpirationStatus;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
import software.amazon.awssdk.services.s3.model.HeadBucketRequest;
import software.amazon.awssdk.services.s3.model.HeadBucketResponse;
import software.amazon.awssdk.services.s3.model.HeadObjectRequest;
import software.amazon.awssdk.services.s3.model.HeadObjectResponse;
import software.amazon.awssdk.services.s3.model.LifecycleExpiration;
import software.amazon.awssdk.services.s3.model.LifecycleRule;
import software.amazon.awssdk.services.s3.model.LifecycleRuleFilter;
import software.amazon.awssdk.services.s3.model.ListObjectsV2Request;
import software.amazon.awssdk.services.s3.model.ListObjectsV2Response;
import software.amazon.awssdk.services.s3.model.ObjectIdentifier;
import software.amazon.awssdk.services.s3.model.PutBucketLifecycleConfigurationRequest;
import software.amazon.awssdk.services.s3.model.PutBucketLifecycleConfigurationResponse;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectResponse;
import software.amazon.awssdk.services.s3.model.S3Error;
import software.amazon.awssdk.services.s3.model.S3Exception;
import software.amazon.awssdk.services.s3.model.S3Object;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest;

import java.io.InputStream;
import java.net.URI;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

@Slf4j
public class AmazonS3FileOperatorApi implements FileOperatorApi {

    /**
     * 存储配置
     */
    protected final StorageConfig storageConfig;

    /**
     * S3兼容协议文件操作客户端
     */
    protected S3Client s3Client;

    public AmazonS3FileOperatorApi(StorageConfig storageConfig) {
        this.storageConfig = storageConfig;
        this.initClient();
    }

    /**
     * 初始化操作的客户端
     */
    public void initClient() {
        // S3兼容协议文件操作客户端实例。
        s3Client = S3Client.builder()
                .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(storageConfig.getAccessKey(), storageConfig.getAccessSecret())))
                .region(Region.AWS_GLOBAL)
                .endpointOverride(URI.create(storageConfig.getEndpoint()))
                .serviceConfiguration(S3Configuration.builder()
                        .pathStyleAccessEnabled(true)
                        .chunkedEncodingEnabled(false)
                        .build())
                .build();
    }

    /**
     * 销毁操作的客户端
     */
    @Override
    public void closeClient() {
        if (Objects.nonNull(s3Client)) {
            s3Client.close();
        }
    }

    /**
     * 获取操作的客户端
     * <p>
     * 例如,获取阿里云的客户端com.aliyun.oss.OSS
     */
    @Override
    public Object getClient() {
        return s3Client;
    }

    /**
     * 判断存储空间是否存在
     * <p>
     * 例如:传入参数exampleBucket-1250000000,返回true代表存在此桶BucketAuthEnum.java
     *
     * @return boolean true-存在,false-不存在
     */
    @Override
    public Boolean existBucket() {
        return existBucket(storageConfig.getBucket());
    }

    /**
     * 判断存储空间是否存在
     * <p>
     * 例如:传入参数exampleBucket-1250000000,返回true代表存在此桶BucketAuthEnum.java
     *
     * @param bucket 存储空间
     * @return boolean true-存在,false-不存在
     */
    @Override
    public Boolean existBucket(String bucket) {
        if (CharSequenceUtil.isBlank(bucket)) {
            return false;
        }

        try {
            HeadBucketResponse response = s3Client.headBucket(HeadBucketRequest.builder()
                    .bucket(bucket)
                    .build());
            return response.sdkHttpResponse().isSuccessful();
        } catch (Exception e) {
            log.error("存储桶不存在或访问受限!error:{}", e.getMessage());
            return false;
        }
    }

    /**
     * 创建存储空间
     *
     * @return Boolean {@code true} 创建成功 {@code false} 创建失败
     */
    @Override
    public Boolean createBucket() {
        return createBucket(storageConfig.getBucket(), null);
    }

    /**
     * 创建存储空间
     *
     * @param bucket 存储空间
     * @return Boolean {@code true} 创建成功 {@code false} 创建失败
     */
    @Override
    public Boolean createBucket(String bucket) {
        return createBucket(bucket, null);
    }

    /**
     * 创建存储空间
     *
     * @param bucket         存储空间
     * @param expirationDays 存储空间的生命周期,单位天
     * @return Boolean {@code true} 创建成功 {@code false} 创建失败
     */
    @Override
    public Boolean createBucket(String bucket, Integer expirationDays) {
        if (CharSequenceUtil.isBlank(bucket)) {
            return false;
        }

        try {
            if(ObjectUtil.isNotEmpty(expirationDays) && expirationDays > 0){
                // 创建生命周期规则
                List<LifecycleRule> rules = new ArrayList<>();
                LifecycleRule rule = LifecycleRule.builder()
                        .id("ExampleRule")
                        .expiration(LifecycleExpiration.builder().days(expirationDays).build())
                        .filter(LifecycleRuleFilter.builder().prefix("prefix/").build())
                        .status(ExpirationStatus.ENABLED)
                        .build();
                rules.add(rule);

                // 创建生命周期配置
                BucketLifecycleConfiguration configuration = BucketLifecycleConfiguration.builder()
                        .rules(rules)
                        .build();

                // 应用生命周期配置到存储桶
                PutBucketLifecycleConfigurationRequest request = PutBucketLifecycleConfigurationRequest.builder()
                        .bucket(bucket)
                        .lifecycleConfiguration(configuration)
                        .build();

                PutBucketLifecycleConfigurationResponse response = s3Client.putBucketLifecycleConfiguration(request);

                return response.sdkHttpResponse().isSuccessful();
            } else {
                CreateBucketRequest bucketRequest = CreateBucketRequest.builder()
                        .bucket(bucket)
                        .build();

                CreateBucketResponse response = s3Client.createBucket(bucketRequest);
                return response.sdkHttpResponse().isSuccessful();
            }
        } catch (Exception e) {
            log.error("创建存储空间失败!error:{}", e.getMessage());
            return false;
        }
    }

    /**
     * 删除存储空间
     *
     * @return Boolean {@code true} 删除成功 {@code false} 删除失败
     */
    @Override
    public Boolean deleteBucket() {
        return deleteBucket(storageConfig.getBucket());
    }

    /**
     * 删除存储空间
     *
     * @param bucket 存储空间
     * @return Boolean {@code true} 删除成功 {@code false} 删除失败
     */
    @Override
    public Boolean deleteBucket(String bucket) {
        if (CharSequenceUtil.isBlank(bucket)) {
            return false;
        }

        try {
            // 创建 DeleteBucketRequest 请求
            DeleteBucketRequest request = DeleteBucketRequest.builder()
                    .bucket(bucket)
                    .build();

            // 删除存储桶
            DeleteBucketResponse deleteBucketResponse = s3Client.deleteBucket(request);

            return deleteBucketResponse.sdkHttpResponse().isSuccessful();
        } catch (Exception e){
            log.error("删除存储空间失败!error:{}", e.getMessage());
            return false;
        }
    }

    /**
     * 判断文件是否存在
     *
     * @param key 唯一标示id,例如a.txt, doc/a.txt
     * @return Boolean {@code true} 存在 {@code false} 不存在
     */
    @Override
    public Boolean existFile(String key) {
        return existFile(storageConfig.getBucket(), key);
    }

    /**
     * 判断文件是否存在
     *
     * @param bucket 存储空间
     * @param key    唯一标示id,例如a.txt, doc/a.txt
     * @return Boolean {@code true} 存在 {@code false} 不存在
     */
    @Override
    public Boolean existFile(String bucket, String key) {
        if (CharSequenceUtil.isBlank(bucket) || CharSequenceUtil.isBlank(key)) {
            return false;
        }

        try {
            // 创建 HeadObjectRequest 请求
            HeadObjectRequest request = HeadObjectRequest.builder()
                    .bucket(bucket)
                    .key(key)
                    .build();

            // 发送 HeadObject 请求,检查文件是否存在
            HeadObjectResponse response = s3Client.headObject(request);

            return response.sdkHttpResponse().isSuccessful();
        } catch (Exception e){
            log.error("判断文件是否存在失败!error:{}", e.getMessage());
            return false;
        }
    }

    /**
     * 创建文件夹
     *
     * @param path 文件夹路径
     * @return Boolean {@code true} 创建成功 {@code false} 创建失败
     */
    @Override
    public Boolean createFolder(String path) {
        return createFolder(storageConfig.getBucket(), path);
    }

    /**
     * 创建文件夹
     *
     * @param bucket 存储空间
     * @param path   文件夹路径
     * @return Boolean {@code true} 创建成功 {@code false} 创建失败
     */
    @Override
    public Boolean createFolder(String bucket, String path) {
        if (CharSequenceUtil.isBlank(bucket) || CharSequenceUtil.isBlank(path)) {
            return false;
        }

        try {
            // 创建 PutObjectRequest 请求
            PutObjectRequest putObjectRequest = PutObjectRequest.builder()
                    .bucket(bucket)
                    .key(path)
                    .build();

            PutObjectResponse putObjectResponse = s3Client.putObject(putObjectRequest, RequestBody.empty());

            return putObjectResponse.sdkHttpResponse().isSuccessful();
        } catch (Exception e){
            log.error("判断创建文件夹是否失败!error:{}", e.getMessage());
            return false;
        }
    }

    /**
     * 上传文件
     *
     * @param bytes 文件字节数组
     */
    @Override
    public String uploadFile(byte[] bytes) {
        return uploadFile(storageConfig.getBucket(), IdUtil.simpleUUID(), bytes, null);
    }

    /**
     * 上传文件
     *
     * @param inputStream 文件流
     */
    @Override
    public String uploadFile(InputStream inputStream) {
        return uploadFile(storageConfig.getBucket(), IdUtil.simpleUUID(), inputStream, null);
    }

    /**
     * 上传文件
     *
     * @param key   唯一标示id,例如a.txt, doc/a.txt
     * @param bytes 文件字节数组
     */
    @Override
    public String uploadFile(String key, byte[] bytes) {
        return uploadFile(storageConfig.getBucket(), key, bytes, null);
    }

    /**
     * 上传文件
     *
     * @param key         唯一标示id,例如a.txt, doc/a.txt
     * @param inputStream 文件流
     */
    @Override
    public String uploadFile(String key, InputStream inputStream) {
        return uploadFile(storageConfig.getBucket(), key, inputStream, null);
    }

    /**
     * 上传文件
     *
     * @param bucket 存储空间
     * @param key    唯一标示id,例如a.txt, doc/a.txt
     * @param bytes  文件字节数组
     */
    @Override
    public String uploadFile(String bucket, String key, byte[] bytes) {
        return uploadFile(bucket, key, bytes, null);
    }

    /**
     * 上传文件
     *
     * @param bucket      存储空间
     * @param key         唯一标示id,例如a.txt, doc/a.txt
     * @param inputStream 文件流
     */
    @Override
    public String uploadFile(String bucket, String key, InputStream inputStream) {
        return uploadFile(bucket, key, inputStream, null);
    }

    /**
     * 上传文件
     *
     * @param bucket         存储空间
     * @param key            唯一标示id,例如a.txt, doc/a.txt
     * @param bytes          文件字节数组
     * @param objectMetadata 元数据信息
     */
    @Override
    public String uploadFile(String bucket, String key, byte[] bytes, Map<String, String> objectMetadata) {
        return uploadFile(bucket, key, IoUtil.toStream(bytes), objectMetadata);
    }

    /**
     * 上传文件
     *
     * @param bucket         存储空间
     * @param key            唯一标示id,例如a.txt, doc/a.txt
     * @param inputStream    文件流
     * @param objectMetadata 元数据信息
     */
    @Override
    public String uploadFile(String bucket, String key, InputStream inputStream, Map<String, String> objectMetadata) {
        if(CharSequenceUtil.isBlank(bucket) || CharSequenceUtil.isBlank(key)) {
            IoUtil.close(inputStream);
            return null;
        }

        try {
            PutObjectRequest objectRequest = PutObjectRequest.builder()
                    .bucket(bucket)
                    .metadata(objectMetadata)
                    .key(key)
                    .build();

            s3Client.putObject(objectRequest, RequestBody.fromInputStream(inputStream, inputStream.available()));
            return key;
        } catch (Exception e) {
            log.error("上传文件失败!error:{}", e.getMessage());
            return null;
        } finally {
            IoUtil.close(inputStream);
        }
    }

    /**
     * 下载文件
     *
     * @param key 唯一标示id,例如a.txt, doc/a.txt
     * @return byte[] 字节数组为文件的字节数组
     */
    @Override
    public byte[] downloadFile(String key) {
        return downloadFile(storageConfig.getBucket(), key);
    }

    /**
     * 下载文件
     *
     * @param bucket 存储空间
     * @param key    唯一标示id,例如a.txt, doc/a.txt
     * @return byte[] 字节数组为文件的字节数组
     */
    @Override
    public byte[] downloadFile(String bucket, String key) {
        if(CharSequenceUtil.isBlank(bucket) || CharSequenceUtil.isBlank(key)) {
            return new byte[0];
        }

        try {
            GetObjectRequest getObjectRequest = GetObjectRequest.builder()
                    .bucket(bucket)
                    .key(key)
                    .build();

            ResponseBytes<GetObjectResponse> s3ClientObject = s3Client.getObject(getObjectRequest,
                    ResponseTransformer.toBytes());

            return s3ClientObject.asByteArray();
        } catch (Exception e) {
            log.error("下载文件失败!error:{}", e.getMessage());
            return new byte[0];
        }
    }

    /**
     * 复制文件
     *
     * @param originFileKey 源文件名称
     * @param newFileKey    新文件名称
     */
    @Override
    public Boolean copyFile(String originFileKey, String newFileKey) {
        return copyFile(storageConfig.getBucket(), originFileKey, storageConfig.getBucket(), newFileKey);
    }

    /**
     * 复制文件
     *
     * @param originBucketName 源存储空间
     * @param originFileKey    源文件名称
     * @param newBucketName    新存储空间
     * @param newFileKey       新文件名称
     */
    @Override
    public Boolean copyFile(String originBucketName, String originFileKey, String newBucketName, String newFileKey) {
        if (!CharSequenceUtil.isAllNotBlank(originBucketName, originFileKey, newBucketName, newFileKey)) {
            return false;
        }

        try {
            CopyObjectRequest copyObjectRequest = CopyObjectRequest.builder()
                    .sourceBucket(originBucketName)
                    .sourceKey(originFileKey)
                    .destinationBucket(newBucketName)
                    .destinationKey(newFileKey)
                    .build();

            CopyObjectResponse copyObjectResponse = s3Client.copyObject(copyObjectRequest);
            return copyObjectResponse.sdkHttpResponse().isSuccessful();
        } catch (S3Exception e) {
            log.error("复制文件失败!error:{}", e.getMessage());
            return false;
        }
    }

    /**
     * 重命名文件
     *
     * @param originFileKey 源文件名称
     * @param newFileKey    新文件名称
     */
    @Override
    public Boolean renameFile(String originFileKey, String newFileKey) {
        return renameFile(storageConfig.getBucket(), originFileKey, newFileKey);
    }

    /**
     * 重命名文件
     *
     * @param bucket        存储空间
     * @param originFileKey 源文件名称
     * @param newFileKey    新文件名称
     */
    @Override
    public Boolean renameFile(String bucket, String originFileKey, String newFileKey) {
        if (!CharSequenceUtil.isAllNotBlank(bucket, originFileKey, newFileKey)) {
            return false;
        }

        copyFile(bucket, originFileKey, bucket, newFileKey);

        Boolean existFile = existFile(bucket, newFileKey);
        if(Boolean.TRUE.equals(existFile)){
            return deleteFile(bucket, newFileKey);
        }

        return false;
    }

    /**
     * 获取文件的下载地址,生成外网地址
     *
     * @param key           文件唯一标识
     * @param timeoutMillis url失效时间,单位毫秒
     * @return 外部系统可以直接访问的url
     */
    @Override
    public String getFileLink(String key, Long timeoutMillis) {
        return getFileLink(storageConfig.getBucket(), key, timeoutMillis);
    }

    /**
     * 获取文件的下载地址,生成外网地址
     *
     * @param bucket        存储空间
     * @param key           文件唯一标识
     * @param timeoutMillis url失效时间,单位毫秒
     * @return 外部系统可以直接访问的url
     */
    @Override
    public String getFileLink(String bucket, String key, Long timeoutMillis) {
        String preSignatureUrl = null;
        try {
            int defaultTimeoutMillis = 7 * 24 * 60 * 60 * 1000;
            timeoutMillis = (Long) ObjectUtil.defaultIfNull(timeoutMillis, defaultTimeoutMillis);
            if(timeoutMillis > defaultTimeoutMillis){
                timeoutMillis = (long) defaultTimeoutMillis;
            }

            PresignedGetObjectRequest presignedGetObjectRequest;
            try (S3Presigner s3PreSigner = S3Presigner.builder()
                    .region(Region.AWS_GLOBAL)
                    .credentialsProvider(ProfileCredentialsProvider.create())
                    .build()) {

                GetObjectRequest getObjectRequest =
                        GetObjectRequest.builder()
                                .bucket(bucket)
                                .key(key)
                                .build();

                //设置预签名URL可访问时间
                GetObjectPresignRequest getObjectPresignRequest = GetObjectPresignRequest.builder()
                        .signatureDuration(Duration.ofMillis(timeoutMillis))
                        .getObjectRequest(getObjectRequest)
                        .build();

                presignedGetObjectRequest = s3PreSigner.presignGetObject(getObjectPresignRequest);
            }

            preSignatureUrl = String.valueOf(presignedGetObjectRequest.url());

        } catch (Exception e) {
            log.error("获取文件的下载地址失败!error:{}", e.getMessage());
            return null;
        }
        return preSignatureUrl;
    }

    /**
     * 删除文件
     *
     * @param key 文件唯一标识
     */
    @Override
    public Boolean deleteFile(String key) {
        return deleteFile(storageConfig.getBucket(), key);
    }

    /**
     * 删除文件
     *
     * @param bucket 存储空间
     * @param key    文件唯一标识
     */
    @Override
    public Boolean deleteFile(String bucket, String key) {
        try {
            DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder()
                    .bucket(bucket)
                    .key(key)
                    .build();

            return s3Client.deleteObject(deleteObjectRequest).sdkHttpResponse().isSuccessful();
        } catch (Exception e) {
            log.error("删除文件失败!error:{}", e.getMessage());
            return false;
        }
    }

    /**
     * 批量删除文件
     *
     * @param keys 文件唯一标识集合
     */
    @Override
    public Boolean deleteFiles(List<String> keys) {
        return deleteFiles(storageConfig.getBucket(), keys);
    }

    /**
     * 批量删除文件
     *
     * @param bucket 存储空间
     * @param keys   文件唯一标识集合
     */
    @Override
    public Boolean deleteFiles(String bucket, List<String> keys) {
        if(CharSequenceUtil.isBlank(bucket) || ObjectUtil.isEmpty(keys)){
            return false;
        }

        try {
            List<ObjectIdentifier> identifierList = keys.stream().map(key ->
                    ObjectIdentifier.builder().key(key).build()).toList();

            DeleteObjectsRequest multiObjectDeleteRequest = DeleteObjectsRequest.builder()
                    .bucket(bucket)
                    .delete(Delete.builder().objects(identifierList).build())
                    .build();

            DeleteObjectsResponse deleteObjectsResponse = s3Client.deleteObjects(multiObjectDeleteRequest);

            if(deleteObjectsResponse.errors().size() < keys.size()){
                String s = deleteObjectsResponse.errors().stream().map(S3Error::key).collect(Collectors.joining(","));
                // 部分文件删除成功
                log.error("部分文件删除成功!未删除成功的文件有:【{}】", s);
            }

            return deleteObjectsResponse.sdkHttpResponse().isSuccessful();
        } catch (Exception e) {
            log.error("批量删除文件失败!error:{}", e.getMessage());
            return false;
        }
    }

    /**
     * 获取文件信息
     *
     * @param key 文件唯一标识
     * @return 文件信息
     */
    @Override
    public FileInfo getFileInfo(String key) {
        return getFileInfo(storageConfig.getBucket(), key);
    }

    /**
     * 获取文件信息
     *
     * @param bucket 存储空间
     * @param key    文件唯一标识
     * @return 文件信息
     */
    @Override
    public FileInfo getFileInfo(String bucket, String key) {
        if(CharSequenceUtil.isBlank(bucket) || CharSequenceUtil.isBlank(key)){
            return null;
        }

        Map<String, String> objectMetadata = getObjectMetadata(bucket, key);
        if(ObjectUtil.isEmpty(objectMetadata)){
            return null;
        }

        FileInfo fileInfo = new FileInfo();
        fileInfo.setKey(MapUtil.getStr(objectMetadata, "key"));
        fileInfo.setPath(MapUtil.getStr(objectMetadata, "path"));
        fileInfo.setName(MapUtil.getStr(objectMetadata, "name"));
        fileInfo.setBucket(bucket);
        fileInfo.setPlatform(getStoragePlatformEnum().getValue());
        fileInfo.setSize(MapUtil.getLong(objectMetadata, "Content-Length"));
        fileInfo.setFriendlySize(DataSizeUtil.format(fileInfo.getSize()));
        fileInfo.setType(MapUtil.getStr(objectMetadata, "Content-Type"));
        fileInfo.setLastModified(new Date(MapUtil.getLong(objectMetadata, "lastModified")));
        fileInfo.setTimeoutMillis(MapUtil.getLong(objectMetadata, "timeoutMillis"));
        return fileInfo;
    }

    /**
     * 获取文件列表
     *
     * @param listFileInfo 查询条件
     * @return 文件列表
     */
    @Override
    public List<FileInfo> listFile(ListFileInfo listFileInfo) {
        return listFile(storageConfig.getBucket(), listFileInfo);
    }

    /**
     * 获取文件列表
     *
     * @param bucket       存储空间
     * @param listFileInfo 查询条件
     * @return 文件列表
     */
    @Override
    public List<FileInfo> listFile(String bucket, ListFileInfo listFileInfo) {
        if (CharSequenceUtil.isBlank(bucket)) {
            return new ArrayList<>();
        }

        List<FileInfo> fileInfoList = new ArrayList<>();
        try {
            if(ObjectUtil.isNotEmpty(listFileInfo.getPath())){
                String path = listFileInfo.getPath();
                if(CharSequenceUtil.equals(StrPool.SLASH, listFileInfo.getPath())){
                    path = CharSequenceUtil.EMPTY;
                } else {
                    path = FileUtil.normalize(path) + StrPool.SLASH;
                }

                ListObjectsV2Request listRequest = ListObjectsV2Request.builder().bucket(bucket).maxKeys(5000)
                        .prefix(path).delimiter(StrPool.SLASH).build();

                ListObjectsV2Response listObjectsV2Response = s3Client.listObjectsV2(listRequest);

                Optional.ofNullable(listObjectsV2Response.commonPrefixes()).orElse(ListUtil.empty())
                        .forEach(file -> fileInfoList.add(toFileInfo(file, bucket)));

                Optional.ofNullable(listObjectsV2Response.contents()).orElse(ListUtil.empty())
                        .forEach(file -> fileInfoList.add(toFileInfo(file, bucket)));
            } else if(ObjectUtil.isNotEmpty(listFileInfo.getTypes())) {
                List<String> typeList = listFileInfo.getTypes();
                List<String> pathList = ListUtil.of(CharSequenceUtil.EMPTY);
                List<String> pathList2 = new ArrayList<>();
                ListObjectsV2Request listRequest;
                for (int i = 0; i < pathList.size(); i++) {
                    if(i == 0){
                        pathList2 = new ArrayList<>();
                    }

                    if(fileInfoList.size() >= 5000){
                        break;
                    }

                    listRequest = ListObjectsV2Request.builder().bucket(bucket).maxKeys(5000)
                            .prefix(pathList.get(i)).delimiter(StrPool.SLASH).build();

                    ListObjectsV2Response listObjectsV2Response = s3Client.listObjectsV2(listRequest);

                    // 文件名称筛选
                    Optional.ofNullable(listObjectsV2Response.contents()).orElse(ListUtil.empty())
                            .stream().filter(file -> typeList.contains(FileNameUtil.getSuffix(file.key())))
                            .forEach(file -> fileInfoList.add(toFileInfo(file, bucket)));


                    List<CommonPrefix> fileList = Optional.ofNullable(listObjectsV2Response.commonPrefixes()).orElse(ListUtil.empty());
                    for (CommonPrefix file : fileList) {
                        // 记录目录,方便一层一层的查询
                        pathList2.add(file.prefix());
                    }

                    if(i + 1 == pathList.size() && ObjectUtil.isNotEmpty(pathList2)){
                        pathList = pathList2;
                        i = -1;
                    }
                }
            } else if(ObjectUtil.isNotEmpty(listFileInfo.getKey()) || ObjectUtil.isNotEmpty(listFileInfo.getKeys())){
                List<String> keys = new ArrayList<>();
                if(ObjectUtil.isNotEmpty(listFileInfo.getKeys())){
                    keys.addAll(listFileInfo.getKeys());
                }
                if(ObjectUtil.isNotEmpty(listFileInfo.getKey())){
                    keys.add(listFileInfo.getKey());
                }
                // 获取某几个文件
                for (String key : keys) {
                    if(CharSequenceUtil.equals(StrPool.SLASH, key)){
                        continue;
                    }

                    String parentPath = FileUtil.normalize(Paths.get(key).getParent().toString()) + StrPool.SLASH;
                    ListObjectsV2Request listRequest = ListObjectsV2Request.builder().bucket(bucket).maxKeys(5000)
                            .prefix(parentPath).delimiter(StrPool.SLASH).build();

                    ListObjectsV2Response listObjectsV2Response = s3Client.listObjectsV2(listRequest);

                    Optional.ofNullable(listObjectsV2Response.commonPrefixes()).orElse(ListUtil.empty())
                            .stream().filter(file -> CharSequenceUtil.equals(FileNameUtil.getName(file.prefix()), FileNameUtil.getName(key)))
                            .forEach(file -> fileInfoList.add(toFileInfo(file, bucket)));

                    Optional.ofNullable(listObjectsV2Response.contents()).orElse(ListUtil.empty())
                            .stream().filter(file -> CharSequenceUtil.equals(file.key(), key))
                            .forEach(file -> fileInfoList.add(toFileInfo(file, bucket)));
                }
            } else if(ObjectUtil.isNotEmpty(listFileInfo.getName())){
                String name = listFileInfo.getName();

                List<String> pathList = ListUtil.of(CharSequenceUtil.EMPTY);
                List<String> pathList2 = new ArrayList<>();
                ListObjectsV2Request listRequest;
                for (int i = 0; i < pathList.size(); i++) {
                    if(i == 0){
                        pathList2 = new ArrayList<>();
                    }

                    listRequest = ListObjectsV2Request.builder().bucket(bucket).maxKeys(5000)
                            .prefix(pathList.get(i)).delimiter(StrPool.SLASH).build();

                    ListObjectsV2Response listObjectsV2Response = s3Client.listObjectsV2(listRequest);

                    // 文件名称筛选
                    Optional.ofNullable(listObjectsV2Response.contents()).orElse(ListUtil.empty())
                            .stream().filter(file -> FileNameUtil.getName(file.key()).contains(name))
                            .forEach(file -> fileInfoList.add(toFileInfo(file, bucket)));


                    List<CommonPrefix> fileList = Optional.ofNullable(listObjectsV2Response.commonPrefixes()).orElse(ListUtil.empty());
                    for (CommonPrefix file : fileList) {
                        // 记录目录,方便一层一层的查询
                        pathList2.add(file.prefix());
                        // 判断目录名称是否匹配
                        if (FileNameUtil.getName(file.prefix()).contains(name)) {
                            fileInfoList.add(toFileInfo(file, bucket));
                        }
                    }

                    if(i + 1 == pathList.size() && ObjectUtil.isNotEmpty(pathList2)){
                        pathList = pathList2;
                        i = -1;
                    }
                }
            } else {
                int k = 0;
                List<String> pathList = ListUtil.of(CharSequenceUtil.EMPTY);
                List<String> pathList2 = new ArrayList<>();
                ListObjectsV2Request listRequest;
                for (int i = 0; i < pathList.size(); i++) {
                    if(i == 0){
                        ++k;
                        if(k == 10){
                            break;
                        }
                        pathList2 = new ArrayList<>();
                    }

                    if(fileInfoList.size() == 5000){
                        break;
                    }

                    listRequest = ListObjectsV2Request.builder().bucket(bucket).maxKeys(5000)
                            .prefix(pathList.get(i)).delimiter(StrPool.SLASH).build();

                    ListObjectsV2Response listObjectsV2Response = s3Client.listObjectsV2(listRequest);

                    List<CommonPrefix> fileList = Optional.ofNullable(listObjectsV2Response.commonPrefixes()).orElse(ListUtil.empty());
                    for (CommonPrefix file : fileList) {
                        // 记录目录,方便一层一层的查询
                        pathList2.add(file.prefix());
                        fileInfoList.add(toFileInfo(file, bucket));
                    }

                    Optional.ofNullable(listObjectsV2Response.contents()).orElse(ListUtil.empty())
                            .forEach(file -> fileInfoList.add(toFileInfo(file, bucket)));

                    if(i + 1 == pathList.size() && ObjectUtil.isNotEmpty(pathList2)){
                        pathList = pathList2;
                        i = -1;
                    }
                }
            }

        } catch (S3Exception e) {
            log.error("复制文件失败!error:{}", e.getMessage());
        }

        return fileInfoList;
    }

    public FileInfo toFileInfo(CommonPrefix commonPrefix, String bucket){
        FileInfo fileInfo = new FileInfo();

        fileInfo.setKey(commonPrefix.prefix());
        fileInfo.setPath(FileUtil.normalize(Paths.get(fileInfo.getKey()).getParent().toString()));
        fileInfo.setName(FileNameUtil.getName(fileInfo.getKey()));
        fileInfo.setBucket(bucket);
        fileInfo.setIsDirectory(true);
        fileInfo.setPlatform(getStoragePlatformEnum().getValue());
        fileInfo.setSize(0L);
        fileInfo.setFriendlySize("");
        fileInfo.setType(null);
        fileInfo.setLastModified(null);
        return fileInfo;
    }

    public FileInfo toFileInfo(S3Object file, String bucket){
        FileInfo fileInfo = new FileInfo();

        fileInfo.setKey(file.key());
        fileInfo.setPath(FileUtil.normalize(Paths.get(fileInfo.getKey()).getParent().toString()));
        fileInfo.setName(FileNameUtil.getName(fileInfo.getKey()));
        fileInfo.setBucket(bucket);
        fileInfo.setIsDirectory(false);
        fileInfo.setPlatform(getStoragePlatformEnum().getValue());
        fileInfo.setSize(file.size());
        fileInfo.setFriendlySize(DataSizeUtil.format(fileInfo.getSize()));
        fileInfo.setType(null);
        fileInfo.setLastModified(new Date(file.lastModified().getEpochSecond() * 1000));
        return fileInfo;
    }

    /**
     * 获取文件元数据信息
     *
     * @param key 文件唯一标识
     * @return 元数据信息
     */
    @Override
    public Map<String, String> getObjectMetadata(String key) {
        return getObjectMetadata(storageConfig.getBucket(), key);
    }

    /**
     * 获取文件元数据信息
     *
     * @param bucket 存储空间
     * @param key    文件唯一标识
     * @return 元数据信息
     */
    @Override
    public Map<String, String> getObjectMetadata(String bucket, String key) {
        if(CharSequenceUtil.isBlank(bucket) || CharSequenceUtil.isBlank(key)){
            return new HashMap<>(0);
        }

        try {
            HeadObjectRequest headObjectRequest = HeadObjectRequest.builder()
                    .bucket(bucket)
                    .key(key)
                    .build();

            HeadObjectResponse headObjectResponse = s3Client.headObject(headObjectRequest);
            if(!headObjectResponse.sdkHttpResponse().isSuccessful()){
                return new HashMap<>(0);
            }
            Map<String, String> map = new LinkedHashMap<>(headObjectResponse.metadata());
            if(key.contains(StrPool.SLASH)){
                map.put("path", key.substring(0, key.lastIndexOf(StrPool.SLASH)));
            } else {
                map.put("path", StrPool.SLASH);
            }
            map.put("key", key);
            map.put("name", FileUtil.getName(key));
            map.put("Content-Type", headObjectResponse.contentType());
            map.put("Content-Length", headObjectResponse.contentLength() + "");
            map.put("lastModified", String.valueOf(headObjectResponse.lastModified().getEpochSecond() * 1000));
            return map;
        } catch (Exception e){
            log.error("获取文件元数据失败!error:{}", e.getMessage());
            return new HashMap<>(0);
        }
    }

    /**
     * 获取当前api的文件存储类型
     */
    @Override
    public StoragePlatformEnum getStoragePlatformEnum() {
        return StoragePlatformEnum.AMAZON_S3;
    }
}

阿里云OSS文件API

import java.net.URI;

public class AliyunOssFileOperatorApi extends AmazonS3FileOperatorApi{

    public AliyunOssFileOperatorApi(StorageConfig storageConfig) {
        super(storageConfig);
    }

    /**
     * 初始化操作的客户端
     */
    @Override
    public void initClient() {
        // S3兼容协议文件操作客户端实例。
        s3Client = S3Client.builder()
                .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(storageConfig.getAccessKey(), storageConfig.getAccessSecret())))
                .region(Region.AWS_GLOBAL)
                .endpointOverride(URI.create(storageConfig.getEndpoint()))
                .serviceConfiguration(S3Configuration.builder()
                        .pathStyleAccessEnabled(false)
                        .chunkedEncodingEnabled(false)
                        .build())
                .build();
    }

    /**
     * 获取当前api的文件存储类型
     */
    @Override
    public StoragePlatformEnum getStoragePlatformEnum() {
        return StoragePlatformEnum.ALIYUN_OSS;
    }
}

腾讯COS文件API

import com.smartadmin.common.enums.StoragePlatformEnum;
import com.smartadmin.common.model.StorageConfig;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;

import java.net.URI;

public class TencentCosFileOperatorApi extends AmazonS3FileOperatorApi{

    public TencentCosFileOperatorApi(StorageConfig storageConfig) {
        super(storageConfig);
    }

    /**
     * 初始化操作的客户端
     */
    @Override
    public void initClient() {
        // S3兼容协议文件操作客户端实例。
        s3Client = S3Client.builder()
                .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(storageConfig.getAccessKey(), storageConfig.getAccessSecret())))
                .region(Region.AWS_GLOBAL)
                .endpointOverride(URI.create(storageConfig.getEndpoint()))
                .build();
    }

    /**
     * 获取当前api的文件存储类型
     */
    @Override
    public StoragePlatformEnum getStoragePlatformEnum() {
        return StoragePlatformEnum.TENCENT_COS;
    }
}

MinIO文件API

import com.smartadmin.common.enums.StoragePlatformEnum;
import com.smartadmin.common.model.StorageConfig;

public class MinIoFileOperatorApi extends AmazonS3FileOperatorApi{

    public MinIoFileOperatorApi(StorageConfig storageConfig) {
        super(storageConfig);
    }

    /**
     * 获取当前api的文件存储类型
     */
    @Override
    public StoragePlatformEnum getStoragePlatformEnum() {
        return StoragePlatformEnum.MINIO;
    }
}

FTP文件API

import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.file.FileNameUtil;
import cn.hutool.core.io.unit.DataSizeUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.text.StrPool;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.extra.ftp.Ftp;
import cn.hutool.extra.ftp.FtpMode;
import com.smartadmin.common.enums.StoragePlatformEnum;
import com.smartadmin.common.model.FileInfo;
import com.smartadmin.common.model.ListFileInfo;
import com.smartadmin.common.model.StorageConfig;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.net.ftp.FTPFile;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.InputStream;
import java.net.URI;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

@Slf4j
public class FtpFileOperatorApi implements FileOperatorApi{

    /**
     * 路径前缀
     */
    private String pathPrefix = "";

    /**
     * 存储配置
     */
    protected final StorageConfig storageConfig;

    private Ftp ftp;

    public FtpFileOperatorApi(StorageConfig storageConfig) {
        this.storageConfig = storageConfig;
        this.initClient();
    }

    /**
     * 初始化客户端
     */
    private void initClient() {
        try {
            URI uri = URLUtil.toURI(storageConfig.getEndpoint());
            if(CharSequenceUtil.isNotBlank(storageConfig.getAccessKey())){
                ftp = new Ftp(uri.getHost(),
                        uri.getPort(),
                        storageConfig.getAccessKey(),
                        storageConfig.getAccessSecret());
            } else {
                ftp = new Ftp(uri.getHost(),
                        uri.getPort());
            }
            ftp.setMode(FtpMode.Passive);

            // 判断存储空间是否存在,不存在则创建
            if(CharSequenceUtil.isNotBlank(storageConfig.getBasePath())){
                pathPrefix = FileUtil.normalize(storageConfig.getBasePath());
                if (CharSequenceUtil.isNotBlank(pathPrefix) && !ftp.exist(pathPrefix)) {
                    ftp.mkDirs(pathPrefix);
                }
            }
        } finally {
            closeClient();
        }
    }

    /**
     * 关闭客户端
     */
    @Override
    public void closeClient() {
        try {
            ftp.close();
        } catch (Exception ignored){
        }
    }

    /**
     * 获取客户端
     */
    @Override
    public Object getClient() {
        ftp.init();
        return ftp;
    }

    private String getBucketPath(String bucket){
        return FileUtil.normalize(pathPrefix + File.separator + bucket);
    }

    /**
     * 判断存储空间是否存在
     *
     * @return @return Boolean {@code true} 存在 {@code false} 不存在
     */
    @Override
    public Boolean existBucket() {
        return existBucket(storageConfig.getBucket());
    }

    /**
     * 判断存储空间是否存在
     *
     * @param bucket 存储空间
     * @return @return Boolean {@code true} 存在 {@code false} 不存在
     */
    @Override
    public Boolean existBucket(String bucket) {
        if (CharSequenceUtil.isBlank(bucket)) {
            return false;
        }

        try {
            ftp = (Ftp) getClient();
            return ftp.exist(getBucketPath(bucket));
        } finally {
            closeClient();
        }
    }

    /**
     * 创建存储空间
     *
     * @return Boolean {@code true} 创建成功 {@code false} 创建失败
     */
    @Override
    public Boolean createBucket() {
        return createBucket(storageConfig.getBucket(), null);
    }

    /**
     * 创建存储空间
     *
     * @param bucket 存储空间
     * @return Boolean {@code true} 创建成功 {@code false} 创建失败
     */
    @Override
    public Boolean createBucket(String bucket) {
        return createBucket(bucket, null);
    }

    /**
     * 创建存储空间
     *
     * @param bucket         存储空间
     * @param expirationDays 存储空间的生命周期,单位天
     * @return Boolean {@code true} 创建成功 {@code false} 创建失败
     */
    @Override
    public Boolean createBucket(String bucket, Integer expirationDays) {
        if (CharSequenceUtil.isBlank(bucket)) {
            return false;
        }

        try {
            ftp = (Ftp) getClient();
            ftp.mkDirs(getBucketPath(bucket));
            return true;
        } finally {
            closeClient();
        }
    }

    /**
     * 删除存储空间
     *
     * @return Boolean {@code true} 删除成功 {@code false} 删除失败
     */
    @Override
    public Boolean deleteBucket() {
        return deleteBucket(storageConfig.getBucket());
    }

    /**
     * 删除存储空间
     *
     * @param bucket 存储空间
     * @return Boolean {@code true} 删除成功 {@code false} 删除失败
     */
    @Override
    public Boolean deleteBucket(String bucket) {
        if (CharSequenceUtil.isBlank(bucket)) {
            return false;
        }

        try {
            ftp = (Ftp) getClient();
            return ftp.delDir(getBucketPath(bucket));
        } finally {
            closeClient();
        }
    }

    /**
     * 判断文件是否存在
     *
     * @param key 唯一标示id,例如a.txt, doc/a.txt
     * @return Boolean {@code true} 存在 {@code false} 不存在
     */
    @Override
    public Boolean existFile(String key) {
        return existFile(storageConfig.getBucket(), key);
    }

    /**
     * 判断文件是否存在
     *
     * @param bucket 存储空间
     * @param key    唯一标示id,例如a.txt, doc/a.txt
     * @return Boolean {@code true} 存在 {@code false} 不存在
     */
    @Override
    public Boolean existFile(String bucket, String key) {
        if (CharSequenceUtil.isBlank(bucket) || CharSequenceUtil.isBlank(key)) {
            return false;
        }

        try {
            ftp = (Ftp) getClient();
            return ftp.exist(FileUtil.normalize(getBucketPath(bucket) + File.separator  + key));
        } finally {
            closeClient();
        }
    }

    /**
     * 创建文件夹
     *
     * @param path 文件夹路径
     * @return Boolean {@code true} 创建成功 {@code false} 创建失败
     */
    @Override
    public Boolean createFolder(String path) {
        return createFolder(storageConfig.getBucket(), path);
    }

    /**
     * 创建文件夹
     *
     * @param bucket 存储空间
     * @param path   文件夹路径
     * @return Boolean {@code true} 创建成功 {@code false} 创建失败
     */
    @Override
    public Boolean createFolder(String bucket, String path) {
        if (CharSequenceUtil.isBlank(bucket) || CharSequenceUtil.isBlank(path)) {
            return false;
        }

        try {
            ftp = (Ftp) getClient();
            String dirPath = FileUtil.normalize(getBucketPath(bucket) + File.separator  + path);
            ftp.mkDirs(dirPath);
            return ftp.exist(dirPath);
        } finally {
            closeClient();
        }
    }

    /**
     * 上传文件
     *
     * @param bytes 文件字节数组
     */
    @Override
    public String uploadFile(byte[] bytes) {
        return uploadFile(storageConfig.getBucket(), IdUtil.simpleUUID(), bytes, null);
    }

    /**
     * 上传文件
     *
     * @param inputStream 文件流
     */
    @Override
    public String uploadFile(InputStream inputStream) {
        return uploadFile(storageConfig.getBucket(), IdUtil.simpleUUID(), inputStream, null);
    }

    /**
     * 上传文件
     *
     * @param key   唯一标示id,例如a.txt, doc/a.txt
     * @param bytes 文件字节数组
     */
    @Override
    public String uploadFile(String key, byte[] bytes) {
        return uploadFile(storageConfig.getBucket(), key, bytes, null);
    }

    /**
     * 上传文件
     *
     * @param key         唯一标示id,例如a.txt, doc/a.txt
     * @param inputStream 文件流
     */
    @Override
    public String uploadFile(String key, InputStream inputStream) {
        return uploadFile(storageConfig.getBucket(), key, inputStream, null);
    }

    /**
     * 上传文件
     *
     * @param bucket 存储空间
     * @param key    唯一标示id,例如a.txt, doc/a.txt
     * @param bytes  文件字节数组
     */
    @Override
    public String uploadFile(String bucket, String key, byte[] bytes) {
        return uploadFile(bucket, key, bytes, null);
    }

    /**
     * 上传文件
     *
     * @param bucket      存储空间
     * @param key         唯一标示id,例如a.txt, doc/a.txt
     * @param inputStream 文件流
     */
    @Override
    public String uploadFile(String bucket, String key, InputStream inputStream) {
        return uploadFile(bucket, key, inputStream, null);
    }

    /**
     * 上传文件
     *
     * @param bucket         存储空间
     * @param key            唯一标示id,例如a.txt, doc/a.txt
     * @param bytes          文件字节数组
     * @param objectMetadata 元数据信息
     */
    @Override
    public String uploadFile(String bucket, String key, byte[] bytes, Map<String, String> objectMetadata) {
        return uploadFile(bucket, key, IoUtil.toStream(bytes), objectMetadata);
    }

    /**
     * 上传文件
     *
     * @param bucket         存储空间
     * @param key            唯一标示id,例如a.txt, doc/a.txt
     * @param inputStream    文件流
     * @param objectMetadata 元数据信息
     */
    @Override
    public String uploadFile(String bucket, String key, InputStream inputStream, Map<String, String> objectMetadata) {
        if(CharSequenceUtil.isBlank(bucket) || CharSequenceUtil.isBlank(key)) {
            IoUtil.close(inputStream);
            return null;
        }

        try {
            ftp = (Ftp) getClient();

            String path = FileUtil.normalize(getBucketPath(bucket) + File.separator + key);
            Path directoryPath = Paths.get(path).getParent();
            String directory = directoryPath != null ? FileUtil.normalize(directoryPath.toString()) : "";
            String filename = FileUtil.getName(path);

            if(CharSequenceUtil.isNotBlank(directory) && !ftp.exist(directory)){
                ftp.mkDirs(directory);
            }

            ftp.upload(directory, filename, inputStream);
            return key;
        } finally {
            IoUtil.close(inputStream);
            closeClient();
        }
    }

    /**
     * 下载文件
     *
     * @param key 唯一标示id,例如a.txt, doc/a.txt
     * @return byte[] 字节数组为文件的字节数组
     */
    @Override
    public byte[] downloadFile(String key) {
        return downloadFile(storageConfig.getBucket(), key);
    }

    /**
     * 下载文件
     *
     * @param bucket 存储空间
     * @param key    唯一标示id,例如a.txt, doc/a.txt
     * @return byte[] 字节数组为文件的字节数组
     */
    @Override
    public byte[] downloadFile(String bucket, String key) {
        if(CharSequenceUtil.isBlank(bucket) || CharSequenceUtil.isBlank(key)) {
            return new byte[0];
        }

        try {
            ftp = (Ftp) getClient();
            String path = FileUtil.normalize(getBucketPath(bucket) + File.separator + key);
            if(!ftp.exist(path)){
                return new byte[0];
            }

            Path directoryPath = Paths.get(path).getParent();
            String directory = directoryPath != null ? FileUtil.normalize(directoryPath.toString()) : "";
            String filename = FileUtil.getName(path);
            try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()){
                ftp.download(directory, filename, outputStream);
                return outputStream.toByteArray();
            } catch (Exception e) {
                log.error("下载文件失败!error:{}", e.getMessage());
                return new byte[0];
            }
        } finally {
            closeClient();
        }
    }

    /**
     * 复制文件
     *
     * @param originFileKey 源文件名称
     * @param newFileKey    新文件名称
     */
    @Override
    public Boolean copyFile(String originFileKey, String newFileKey) {
        return copyFile(storageConfig.getBucket(), originFileKey, storageConfig.getBucket(), newFileKey);
    }

    /**
     * 复制文件
     *
     * @param originBucketName 源存储空间
     * @param originFileKey    源文件名称
     * @param newBucketName    新存储空间
     * @param newFileKey       新文件名称
     */
    @Override
    public Boolean copyFile(String originBucketName, String originFileKey, String newBucketName, String newFileKey) {
        if (!CharSequenceUtil.isAllNotBlank(originBucketName, originFileKey, newBucketName, newFileKey)) {
            return false;
        }

        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()){
            ftp = (Ftp) getClient();
            String path = FileUtil.normalize(getBucketPath(originBucketName) + File.separator + originFileKey);
            if(!ftp.exist(path)){
                return false;
            }

            // 先获取文件数据
            Path directoryPath = Paths.get(path).getParent();
            String directory = directoryPath != null ? FileUtil.normalize(directoryPath.toString()) : "";
            String filename = FileUtil.getName(path);
            ftp.download(directory, filename, outputStream);
            byte[] bytes = outputStream.toByteArray();

            // 在上传到对应的位置
            path = FileUtil.normalize(getBucketPath(newBucketName) + File.separator + newFileKey);
            directoryPath = Paths.get(path).getParent();
            directory = directoryPath != null ? FileUtil.normalize(directoryPath.toString()) : "";
            filename = FileUtil.getName(path);

            if(CharSequenceUtil.isNotBlank(directory) && !ftp.exist(directory)){
                ftp.mkDirs(directory);
            }

            try (ByteArrayInputStream inputStream = IoUtil.toStream(bytes)){
                return ftp.upload(directory, filename, inputStream);
            }
        } catch (Exception e){
            return false;
        } finally {
            closeClient();
        }
    }

    /**
     * 重命名文件
     *
     * @param originFileKey 源文件名称
     * @param newFileKey    新文件名称
     */
    @Override
    public Boolean renameFile(String originFileKey, String newFileKey) {
        return renameFile(storageConfig.getBucket(), originFileKey, newFileKey);
    }

    /**
     * 重命名文件
     *
     * @param bucket        存储空间
     * @param originFileKey 源文件名称
     * @param newFileKey    新文件名称
     */
    @Override
    public Boolean renameFile(String bucket, String originFileKey, String newFileKey) {
        if (!CharSequenceUtil.isAllNotBlank(bucket, originFileKey, newFileKey)) {
            return false;
        }

        copyFile(bucket, originFileKey, bucket, newFileKey);

        Boolean existFile = existFile(bucket, newFileKey);
        if(Boolean.TRUE.equals(existFile)){
            return deleteFile(bucket, newFileKey);
        }

        return false;
    }

    /**
     * 获取文件的下载地址,生成外网地址
     *
     * @param key           文件唯一标识
     * @param timeoutMillis url失效时间,单位毫秒(最高7天)
     * @return 外部系统可以直接访问的url
     */
    @Override
    public String getFileLink(String key, Long timeoutMillis) {
        return getFileLink(storageConfig.getBucket(), key, timeoutMillis);
    }

    /**
     * 获取文件的下载地址,生成外网地址
     *
     * @param bucket        存储空间
     * @param key           文件唯一标识
     * @param timeoutMillis url失效时间,单位毫秒(最高7天)
     * @return 外部系统可以直接访问的url
     */
    @Override
    public String getFileLink(String bucket, String key, Long timeoutMillis) {
        // empty
        return null;
    }

    /**
     * 删除文件
     *
     * @param key 文件唯一标识
     */
    @Override
    public Boolean deleteFile(String key) {
        return deleteFile(storageConfig.getBucket(), key);
    }

    /**
     * 删除文件
     *
     * @param bucket 存储空间
     * @param key    文件唯一标识
     */
    @Override
    public Boolean deleteFile(String bucket, String key) {
        if(CharSequenceUtil.isBlank(bucket) || CharSequenceUtil.isBlank(key)) {
            return false;
        }

        try {
            ftp = (Ftp) getClient();
            String path = FileUtil.normalize(getBucketPath(bucket) + File.separator + key);
            if(!ftp.exist(path)){
                return false;
            }

            return ftp.delFile(FileUtil.normalize(path));
        } finally {
            closeClient();
        }
    }

    /**
     * 批量删除文件
     *
     * @param keys 文件唯一标识集合
     */
    @Override
    public Boolean deleteFiles(List<String> keys) {
        return deleteFiles(storageConfig.getBucket(), keys);
    }

    /**
     * 批量删除文件
     *
     * @param bucket 存储空间
     * @param keys   文件唯一标识集合
     */
    @Override
    public Boolean deleteFiles(String bucket, List<String> keys) {
        if(CharSequenceUtil.isBlank(bucket) || ObjectUtil.isEmpty(keys)){
            return false;
        }

        try {
            ftp = (Ftp) getClient();

            for (String key : keys) {
                String path = FileUtil.normalize(getBucketPath(bucket) + File.separator + key);
                if(!ftp.exist(path)){
                    continue;
                }

                ftp.delFile(FileUtil.normalize(path));
            }
        } finally {
            closeClient();
        }

        return true;
    }

    /**
     * 获取文件信息
     *
     * @param key 文件唯一标识
     * @return 文件信息
     */
    @Override
    public FileInfo getFileInfo(String key) {
        return getFileInfo(storageConfig.getBucket(), key);
    }

    /**
     * 获取文件信息
     *
     * @param bucket 存储空间
     * @param key    文件唯一标识
     * @return 文件信息
     */
    @Override
    public FileInfo getFileInfo(String bucket, String key) {
        if(CharSequenceUtil.isBlank(bucket) || CharSequenceUtil.isBlank(key)){
            return null;
        }

        Map<String, String> objectMetadata = getObjectMetadata(bucket, key);
        if(ObjectUtil.isEmpty(objectMetadata)){
            return null;
        }

        FileInfo fileInfo = new FileInfo();
        fileInfo.setKey(MapUtil.getStr(objectMetadata, "key"));
        fileInfo.setPath(MapUtil.getStr(objectMetadata, "path"));
        fileInfo.setName(MapUtil.getStr(objectMetadata, "name"));
        fileInfo.setBucket(bucket);
        fileInfo.setPlatform(getStoragePlatformEnum().getValue());
        fileInfo.setSize(MapUtil.getLong(objectMetadata, "Content-Length"));
        fileInfo.setFriendlySize(DataSizeUtil.format(fileInfo.getSize()));
        fileInfo.setType(MapUtil.getStr(objectMetadata, "Content-Type"));
        fileInfo.setLastModified(new Date(MapUtil.getLong(objectMetadata, "lastModified")));
        fileInfo.setTimeoutMillis(MapUtil.getLong(objectMetadata, "timeoutMillis"));
        return fileInfo;
    }

    /**
     * 获取文件列表
     *
     * @param listFileInfo 查询条件
     * @return 文件列表
     */
    @Override
    public List<FileInfo> listFile(ListFileInfo listFileInfo) {
        return listFile(storageConfig.getBucket(), listFileInfo);
    }

    /**
     * 获取文件列表
     *
     * @param bucket       存储空间
     * @param listFileInfo 查询条件
     * @return 文件列表
     */
    @Override
    public List<FileInfo> listFile(String bucket, ListFileInfo listFileInfo) {
        if(CharSequenceUtil.isBlank(bucket)){
            return new ArrayList<>();
        }

        List<FileInfo> fileInfoList = new ArrayList<>();
        try {
            ftp = (Ftp) getClient();

            if(ObjectUtil.isNotEmpty(listFileInfo.getPath())) {
                // 查询某个路径下的所有文件及目录(层级1)
                String path = FileUtil.normalize(getBucketPath(bucket) + File.separator + listFileInfo.getPath());
                List<FTPFile> fileList = ftp.lsFiles(path, Objects::nonNull);
                for (FTPFile ftpFile : fileList) {
                    fileInfoList.add(toFileInfo(listFileInfo.getPath(), ftpFile, bucket));
                }
            } else if (ObjectUtil.isNotEmpty(listFileInfo.getTypes())) {
                // 获取指定类型的文件
                               // 获取指定类型的文件
                String basePath = FileUtil.normalize(getBucketPath(bucket));
                List<String> pathList = ListUtil.of(basePath);
                List<String> pathList2 = new ArrayList<>();
                String path;
                for (int i = 0; i < pathList.size(); i++) {
                    if(i == 0){
                        pathList2 = new ArrayList<>();
                    }

                    if(fileInfoList.size() >= 5000){
                        break;
                    }

                    path = pathList.get(i);
                    List<FTPFile> ftpFileList = ftp.lsFiles(path, Objects::nonNull);
                    for (FTPFile ftpFile : ftpFileList) {
                        if (ftpFile.isDirectory()) {
                            pathList2.add(FileUtil.normalize(path + File.separator + ftpFile.getName()));
                            continue;
                        }
                        if(listFileInfo.getTypes().contains(FileUtil.getSuffix(ftpFile.getName()))){
                            fileInfoList.add(toFileInfo(path.replace(basePath, StrPool.SLASH), ftpFile, bucket));
                        }
                    }

                    if(i + 1 == pathList.size() && ObjectUtil.isNotEmpty(pathList2)){
                        pathList = pathList2;
                        i = -1;
                    }
                }

            } else if (ObjectUtil.isNotEmpty(listFileInfo.getKeys()) || ObjectUtil.isNotEmpty(listFileInfo.getKey())) {
                List<String> keys = new ArrayList<>();
                if(ObjectUtil.isNotEmpty(listFileInfo.getKeys())){
                    keys.addAll(listFileInfo.getKeys());
                }
                if(ObjectUtil.isNotEmpty(listFileInfo.getKey())){
                    keys.add(listFileInfo.getKey());
                }
                // 获取某几个文件
                for (String key : keys) {
                    if(CharSequenceUtil.equals(StrPool.SLASH, key)){
                        continue;
                    }

                    String parentPath = FileUtil.normalize(Paths.get(key).getParent().toString());
                    String path = FileUtil.normalize(getBucketPath(bucket) + File.separator + parentPath);
                    List<FTPFile> ftpFileList = ftp.lsFiles(path, Objects::nonNull);
                    for (FTPFile ftpFile : ftpFileList) {
                        if (CharSequenceUtil.equals(FileNameUtil.getName(ftpFile.getName()), FileNameUtil.getName(key))) {
                            fileInfoList.add(toFileInfo(parentPath, ftpFile, bucket));
                        }
                    }
                }
            } else if (ObjectUtil.isNotEmpty(listFileInfo.getName())) {
                // 按文件名称模糊搜索
                String basePath = FileUtil.normalize(getBucketPath(bucket));
                List<String> pathList = ListUtil.of(basePath);
                List<String> pathList2 = new ArrayList<>();
                String path;
                for (int i = 0; i < pathList.size(); i++) {
                    if(i == 0){
                        pathList2 = new ArrayList<>();
                    }

                    path = pathList.get(i);
                    List<FTPFile> ftpFileList = ftp.lsFiles(path, Objects::nonNull);
                    for (FTPFile ftpFile : ftpFileList) {
                        if(ftpFile.getName().contains(listFileInfo.getName())){
                            fileInfoList.add(toFileInfo(path.replace(basePath, StrPool.SLASH), ftpFile, bucket));
                        }

                        if (ftpFile.isDirectory()) {
                            pathList2.add(FileUtil.normalize(path + File.separator + ftpFile.getName()));
                        }
                    }

                    if(i + 1 == pathList.size() && ObjectUtil.isNotEmpty(pathList2)){
                        pathList = pathList2;
                        i = -1;
                    }
                }
            } else {
                int k = 0;
                String basePath = FileUtil.normalize(getBucketPath(bucket));
                List<String> pathList = ListUtil.of(basePath);
                List<String> pathList2 = new ArrayList<>();
                String path;
                for (int i = 0; i < pathList.size(); i++) {
                    if(i == 0){
                        ++k;
                        if(k == 10){
                            break;
                        }

                        pathList2 = new ArrayList<>();
                    }

                    if(fileInfoList.size() >= 5000){
                        break;
                    }

                    path = pathList.get(i);
                    List<FTPFile> ftpFileList = ftp.lsFiles(path, Objects::nonNull);
                    for (FTPFile ftpFile : ftpFileList) {
                        fileInfoList.add(toFileInfo(path.replace(basePath, StrUtil.EMPTY), ftpFile, bucket));

                        if (ftpFile.isDirectory()) {
                            pathList2.add(FileUtil.normalize(path + File.separator + ftpFile.getName()));
                        }
                    }

                    if(i + 1 == pathList.size() && ObjectUtil.isNotEmpty(pathList2)){
                        pathList = pathList2;
                        i = -1;
                    }
                }

            }

            return fileInfoList;
        } finally {
            closeClient();
        }
    }

    private FileInfo toFileInfo(String path, FTPFile file, String bucket){
        FileInfo fileInfo = new FileInfo();
        fileInfo.setKey(FileUtil.normalize(path + File.separator + file.getName()));
        if(CharSequenceUtil.isBlank(path) || CharSequenceUtil.equals(StrPool.SLASH, path)){
            fileInfo.setPath(StrPool.SLASH);
        } else {
            fileInfo.setPath(FileUtil.normalize(path));
        }
        fileInfo.setName(FileUtil.getName(file.getName()));
        fileInfo.setBucket(bucket);
        fileInfo.setIsDirectory(file.isDirectory());
        fileInfo.setPlatform(getStoragePlatformEnum().getValue());
        fileInfo.setSize(file.getSize());
        fileInfo.setFriendlySize(DataSizeUtil.format(fileInfo.getSize()));
        fileInfo.setType(FileUtil.getMimeType(fileInfo.getKey()));
        fileInfo.setLastModified(file.getTimestamp().getTime());
        return fileInfo;
    }

    /**
     * 获取文件元数据信息
     *
     * @param key 文件唯一标识
     * @return 元数据信息
     */
    @Override
    public Map<String, String> getObjectMetadata(String key) {
        return getObjectMetadata(storageConfig.getBucket(), key);
    }

    public static void main(String[] args) {
        System.out.println(Paths.get(null).getParent());
    }

    /**
     * 获取文件元数据信息
     *
     * @param bucket 存储空间
     * @param key    文件唯一标识
     * @return 元数据信息
     */
    @Override
    public Map<String, String> getObjectMetadata(String bucket, String key) {
        if(CharSequenceUtil.isBlank(bucket) || CharSequenceUtil.isBlank(key)){
            return Collections.emptyMap();
        }

        try {
            ftp = (Ftp) getClient();

            String path = FileUtil.normalize(getBucketPath(bucket) + File.separator + key);
            if (!ftp.exist(path)) {
                return Collections.emptyMap();
            }

            Path directoryPath = Paths.get(path).getParent();
            String directory = directoryPath != null ? FileUtil.normalize(directoryPath.toString()) : "";
            FTPFile ftpFile =ftp.lsFiles(directory, file -> file.getName().equals(FileUtil.getName(path)))
                    .stream().findFirst().orElse(null);
            Map<String, String> map = new HashMap<>();
            if(key.contains(StrPool.SLASH)){
                map.put("path", key.substring(0, key.lastIndexOf(StrPool.SLASH)));
            } else {
                map.put("path", StrPool.SLASH);
            }
            map.put("key", key);
            map.put("name", FileUtil.getName(key));
            map.put("Content-Type", null);
            map.put("Content-Length", ftpFile.getSize() + "");
            map.put("lastModified", ftpFile.getTimestamp().getTime().getTime() + "");

            return map;
        } catch (Exception e) {
            return Collections.emptyMap();
        } finally {
            closeClient();
        }
    }

    /**
     * 获取当前api的文件存储类型
     */
    @Override
    public StoragePlatformEnum getStoragePlatformEnum() {
        return StoragePlatformEnum.FTP;
    }
}

SFTP文件API

import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.file.FileNameUtil;
import cn.hutool.core.io.unit.DataSizeUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.text.StrPool;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.extra.ssh.JschUtil;
import cn.hutool.extra.ssh.Sftp;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.SftpATTRS;
import com.jcraft.jsch.SftpException;
import com.smartadmin.common.enums.StoragePlatformEnum;
import com.smartadmin.common.model.FileInfo;
import com.smartadmin.common.model.ListFileInfo;
import com.smartadmin.common.model.StorageConfig;
import lombok.extern.slf4j.Slf4j;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.InputStream;
import java.net.URI;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

@Slf4j
public class SftpFileOperatorApi implements FileOperatorApi{

    /**
     * 路径前缀
     */
    private String pathPrefix = "";

     /**
     * 存储配置
     */
    protected final StorageConfig storageConfig;

    private Sftp sftp;

    public SftpFileOperatorApi(StorageConfig storageConfig) {
        this.storageConfig = storageConfig;
        this.initClient();
    }

    /**
     * 初始化客户端
     */
    private void initClient() {
        try {
            // 创建sftp客户端
            URI uri = URLUtil.toURI(storageConfig.getEndpoint());
            sftp = JschUtil.createSftp(uri.getHost(),
                    uri.getPort(),
                    storageConfig.getAccessKey(),
                    storageConfig.getAccessSecret());

            // 判断存储空间是否存在,不存在则创建
            if(CharSequenceUtil.isNotBlank(storageConfig.getBasePath())){
                pathPrefix = FileUtil.normalize(storageConfig.getBasePath());
                if (CharSequenceUtil.isNotBlank(pathPrefix) && !sftp.exist(pathPrefix)) {
                    sftp.mkDirs(pathPrefix);
                }
            }
        } finally {
            // 关闭客户端
            closeClient();
        }
    }

    /**
     * 关闭客户端
     */
    @Override
    public void closeClient() {
        try {
            sftp.close();
        } catch (Exception ignored){
        }
    }

    /**
     * 获取客户端
     */
    @Override
    public Object getClient() {
        URI uri = URLUtil.toURI(storageConfig.getEndpoint());
        sftp.init(
                uri.getHost(),
                uri.getPort(),
                storageConfig.getAccessKey(),
                storageConfig.getAccessSecret(),
                CharsetUtil.CHARSET_UTF_8
        );
        return sftp;
    }

    private String getBucketPath(String bucket){
        return FileUtil.normalize(pathPrefix + File.separator + bucket);
    }

    /**
     * 判断存储空间是否存在
     *
     * @return @return Boolean {@code true} 存在 {@code false} 不存在
     */
    @Override
    public Boolean existBucket() {
        return existBucket(storageConfig.getBucket());
    }

    /**
     * 判断存储空间是否存在
     *
     * @param bucket 存储空间
     * @return @return Boolean {@code true} 存在 {@code false} 不存在
     */
    @Override
    public Boolean existBucket(String bucket) {
        if (CharSequenceUtil.isBlank(bucket)) {
            return false;
        }

        try {
            sftp = (Sftp) getClient();
            return sftp.exist(getBucketPath(bucket));
        } finally {
            closeClient();
        }
    }

    /**
     * 创建存储空间
     *
     * @return Boolean {@code true} 创建成功 {@code false} 创建失败
     */
    @Override
    public Boolean createBucket() {
        return createBucket(storageConfig.getBucket(), null);
    }

    /**
     * 创建存储空间
     *
     * @param bucket 存储空间
     * @return Boolean {@code true} 创建成功 {@code false} 创建失败
     */
    @Override
    public Boolean createBucket(String bucket) {
        return createBucket(bucket, null);
    }

    /**
     * 创建存储空间
     *
     * @param bucket         存储空间
     * @param expirationDays 存储空间的生命周期,单位天
     * @return Boolean {@code true} 创建成功 {@code false} 创建失败
     */
    @Override
    public Boolean createBucket(String bucket, Integer expirationDays) {
        if (CharSequenceUtil.isBlank(bucket)) {
            return false;
        }

        try {
            sftp = (Sftp) getClient();
            sftp.mkDirs(getBucketPath(bucket));
            return true;
        } finally {
            closeClient();
        }
    }

    /**
     * 删除存储空间
     *
     * @return Boolean {@code true} 删除成功 {@code false} 删除失败
     */
    @Override
    public Boolean deleteBucket() {
        return deleteBucket(storageConfig.getBucket());
    }

    /**
     * 删除存储空间
     *
     * @param bucket 存储空间
     * @return Boolean {@code true} 删除成功 {@code false} 删除失败
     */
    @Override
    public Boolean deleteBucket(String bucket) {
        if (CharSequenceUtil.isBlank(bucket)) {
            return false;
        }

        try {
            sftp = (Sftp) getClient();
            return sftp.delDir(getBucketPath(bucket));
        } finally {
            closeClient();
        }
    }

    /**
     * 判断文件是否存在
     *
     * @param key 唯一标示id,例如a.txt, doc/a.txt
     * @return Boolean {@code true} 存在 {@code false} 不存在
     */
    @Override
    public Boolean existFile(String key) {
        return existFile(storageConfig.getBucket(), key);
    }

    /**
     * 判断文件是否存在
     *
     * @param bucket 存储空间
     * @param key    唯一标示id,例如a.txt, doc/a.txt
     * @return Boolean {@code true} 存在 {@code false} 不存在
     */
    @Override
    public Boolean existFile(String bucket, String key) {
        if (CharSequenceUtil.isBlank(bucket) || CharSequenceUtil.isBlank(key)) {
            return false;
        }

        try {
            sftp = (Sftp) getClient();
            return sftp.exist(FileUtil.normalize(getBucketPath(bucket) + File.separator  + key));
        } finally {
            closeClient();
        }
    }

    /**
     * 创建文件夹
     *
     * @param path 文件夹路径
     * @return Boolean {@code true} 创建成功 {@code false} 创建失败
     */
    @Override
    public Boolean createFolder(String path) {
        return existFile(storageConfig.getBucket(), path);
    }

    /**
     * 创建文件夹
     *
     * @param bucket 存储空间
     * @param path   文件夹路径
     * @return Boolean {@code true} 创建成功 {@code false} 创建失败
     */
    @Override
    public Boolean createFolder(String bucket, String path) {
        if (CharSequenceUtil.isBlank(bucket) || CharSequenceUtil.isBlank(path)) {
            return false;
        }

        try {
            sftp = (Sftp) getClient();
            String dirPath = FileUtil.normalize(getBucketPath(bucket) + File.separator  + path);
            sftp.mkDirs(dirPath);
            return sftp.exist(dirPath);
        } finally {
            closeClient();
        }
    }

    /**
     * 上传文件
     *
     * @param bytes 文件字节数组
     */
    @Override
    public String uploadFile(byte[] bytes) {
        return uploadFile(storageConfig.getBucket(), IdUtil.simpleUUID(), bytes, null);
    }

    /**
     * 上传文件
     *
     * @param inputStream 文件流
     */
    @Override
    public String uploadFile(InputStream inputStream) {
        return uploadFile(storageConfig.getBucket(), IdUtil.simpleUUID(), inputStream, null);
    }

    /**
     * 上传文件
     *
     * @param key   唯一标示id,例如a.txt, doc/a.txt
     * @param bytes 文件字节数组
     */
    @Override
    public String uploadFile(String key, byte[] bytes) {
        return uploadFile(storageConfig.getBucket(), key, bytes, null);
    }

    /**
     * 上传文件
     *
     * @param key         唯一标示id,例如a.txt, doc/a.txt
     * @param inputStream 文件流
     */
    @Override
    public String uploadFile(String key, InputStream inputStream) {
        return uploadFile(storageConfig.getBucket(), key, inputStream, null);
    }

    /**
     * 上传文件
     *
     * @param bucket 存储空间
     * @param key    唯一标示id,例如a.txt, doc/a.txt
     * @param bytes  文件字节数组
     */
    @Override
    public String uploadFile(String bucket, String key, byte[] bytes) {
        return uploadFile(bucket, key, bytes, null);
    }

    /**
     * 上传文件
     *
     * @param bucket      存储空间
     * @param key         唯一标示id,例如a.txt, doc/a.txt
     * @param inputStream 文件流
     */
    @Override
    public String uploadFile(String bucket, String key, InputStream inputStream) {
        return uploadFile(bucket, key, inputStream, null);
    }

    /**
     * 上传文件
     *
     * @param bucket         存储空间
     * @param key            唯一标示id,例如a.txt, doc/a.txt
     * @param bytes          文件字节数组
     * @param objectMetadata 元数据信息
     */
    @Override
    public String uploadFile(String bucket, String key, byte[] bytes, Map<String, String> objectMetadata) {
        return uploadFile(bucket, key, IoUtil.toStream(bytes), objectMetadata);
    }

    /**
     * 上传文件
     *
     * @param bucket         存储空间
     * @param key            唯一标示id,例如a.txt, doc/a.txt
     * @param inputStream    文件流
     * @param objectMetadata 元数据信息
     */
    @Override
    public String uploadFile(String bucket, String key, InputStream inputStream, Map<String, String> objectMetadata) {
        if(CharSequenceUtil.isBlank(bucket) || CharSequenceUtil.isBlank(key)) {
            IoUtil.close(inputStream);
            return null;
        }

        try {
            sftp = (Sftp) getClient();

            String path = FileUtil.normalize(getBucketPath(bucket) + File.separator + key);
            Path directoryPath = Paths.get(path).getParent();
            String directory = directoryPath != null ? FileUtil.normalize(directoryPath.toString()) : "";
            String filename = FileUtil.getName(path);

            if(CharSequenceUtil.isNotBlank(directory) && !sftp.exist(directory)){
                sftp.mkDirs(directory);
            }

            sftp.upload(directory, filename, inputStream);
            return key;
        } finally {
            IoUtil.close(inputStream);
            closeClient();
        }
    }

    /**
     * 下载文件
     *
     * @param key 唯一标示id,例如a.txt, doc/a.txt
     * @return byte[] 字节数组为文件的字节数组
     */
    @Override
    public byte[] downloadFile(String key) {
        return downloadFile(storageConfig.getBucket(), key);
    }

    /**
     * 下载文件
     *
     * @param bucket 存储空间
     * @param key    唯一标示id,例如a.txt, doc/a.txt
     * @return byte[] 字节数组为文件的字节数组
     */
    @Override
    public byte[] downloadFile(String bucket, String key) {
        if(CharSequenceUtil.isBlank(bucket) || CharSequenceUtil.isBlank(key)) {
            return new byte[0];
        }

        try {
            sftp = (Sftp) getClient();
            String path = FileUtil.normalize(getBucketPath(bucket) + File.separator + key);
            if(!sftp.exist(path)){
                return new byte[0];
            }

            try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()){
                sftp.download(path, outputStream);
                return outputStream.toByteArray();
            } catch (Exception e) {
                log.error("下载文件失败!error:{}", e.getMessage());
                return new byte[0];
            }
        } finally {
            closeClient();
        }
    }

    /**
     * 复制文件
     *
     * @param originFileKey 源文件名称
     * @param newFileKey    新文件名称
     */
    @Override
    public Boolean copyFile(String originFileKey, String newFileKey) {
        return copyFile(storageConfig.getBucket(), originFileKey, storageConfig.getBucket(), newFileKey);
    }

    /**
     * 复制文件
     *
     * @param originBucketName 源存储空间
     * @param originFileKey    源文件名称
     * @param newBucketName    新存储空间
     * @param newFileKey       新文件名称
     */
    @Override
    public Boolean copyFile(String originBucketName, String originFileKey, String newBucketName, String newFileKey) {
        if (!CharSequenceUtil.isAllNotBlank(originBucketName, originFileKey, newBucketName, newFileKey)) {
            return false;
        }

        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()){
            sftp = (Sftp) getClient();
            String path = FileUtil.normalize(getBucketPath(originBucketName) + File.separator + originFileKey);
            if(!sftp.exist(path)){
                return false;
            }

            // 先获取文件数据
            sftp.download(path, outputStream);
            byte[] bytes = outputStream.toByteArray();

            // 在上传到对应的位置
            path = FileUtil.normalize(getBucketPath(newBucketName) + File.separator + newFileKey);
            Path directoryPath = Paths.get(path).getParent();
            String directory = directoryPath != null ? FileUtil.normalize(directoryPath.toString()) : "";
            String filename = FileUtil.getName(path);

            if(CharSequenceUtil.isNotBlank(directory) && !sftp.exist(directory)){
                sftp.mkDirs(directory);
            }

            try (ByteArrayInputStream inputStream = IoUtil.toStream(bytes)){
                return sftp.upload(directory, filename, inputStream);
            }
        } catch (Exception e){
            return false;
        } finally {
            closeClient();
        }
    }

    /**
     * 重命名文件
     *
     * @param originFileKey 源文件名称
     * @param newFileKey    新文件名称
     */
    @Override
    public Boolean renameFile(String originFileKey, String newFileKey) {
        return renameFile(storageConfig.getBucket(), originFileKey, newFileKey);
    }

    /**
     * 重命名文件
     *
     * @param bucket        存储空间
     * @param originFileKey 源文件名称
     * @param newFileKey    新文件名称
     */
    @Override
    public Boolean renameFile(String bucket, String originFileKey, String newFileKey) {
        if (!CharSequenceUtil.isAllNotBlank(bucket, originFileKey, newFileKey)) {
            return false;
        }

        copyFile(bucket, originFileKey, bucket, newFileKey);

        Boolean existFile = existFile(bucket, newFileKey);
        if(Boolean.TRUE.equals(existFile)){
            return deleteFile(bucket, newFileKey);
        }

        return false;
    }

    /**
     * 获取文件的下载地址,生成外网地址
     *
     * @param key           文件唯一标识
     * @param timeoutMillis url失效时间,单位毫秒(最高7天)
     * @return 外部系统可以直接访问的url
     */
    @Override
    public String getFileLink(String key, Long timeoutMillis) {
        return getFileLink(storageConfig.getBucket(), key, timeoutMillis);
    }

    /**
     * 获取文件的下载地址,生成外网地址
     *
     * @param bucket        存储空间
     * @param key           文件唯一标识
     * @param timeoutMillis url失效时间,单位毫秒(最高7天)
     * @return 外部系统可以直接访问的url
     */
    @Override
    public String getFileLink(String bucket, String key, Long timeoutMillis) {
        // empty
        return null;
    }

    /**
     * 删除文件
     *
     * @param key 文件唯一标识
     */
    @Override
    public Boolean deleteFile(String key) {
        return deleteFile(storageConfig.getBucket(), key);
    }

    /**
     * 删除文件
     *
     * @param bucket 存储空间
     * @param key    文件唯一标识
     */
    @Override
    public Boolean deleteFile(String bucket, String key) {
        if(CharSequenceUtil.isBlank(bucket) || CharSequenceUtil.isBlank(key)) {
            return false;
        }

        try {
            sftp = (Sftp) getClient();
            String path = FileUtil.normalize(getBucketPath(bucket) + File.separator + key);
            if(!sftp.exist(path)){
                return false;
            }

            return sftp.delFile(FileUtil.normalize(path));
        } finally {
            closeClient();
        }
    }

    /**
     * 批量删除文件
     *
     * @param keys 文件唯一标识集合
     */
    @Override
    public Boolean deleteFiles(List<String> keys) {
        return deleteFiles(storageConfig.getBucket(), keys);
    }

    /**
     * 批量删除文件
     *
     * @param bucket 存储空间
     * @param keys   文件唯一标识集合
     */
    @Override
    public Boolean deleteFiles(String bucket, List<String> keys) {
        if(CharSequenceUtil.isBlank(bucket) || ObjectUtil.isEmpty(keys)){
            return false;
        }

        try {
            sftp = (Sftp) getClient();

            for (String key : keys) {
                String path = FileUtil.normalize(getBucketPath(bucket) + File.separator + key);
                if(!sftp.exist(path)){
                    continue;
                }

                sftp.delFile(FileUtil.normalize(path));
            }
        } finally {
            closeClient();
        }

        return true;
    }

    /**
     * 获取文件信息
     *
     * @param key 文件唯一标识
     * @return 文件信息
     */
    @Override
    public FileInfo getFileInfo(String key) {
        return getFileInfo(storageConfig.getBucket(), key);
    }

    /**
     * 获取文件信息
     *
     * @param bucket 存储空间
     * @param key    文件唯一标识
     * @return 文件信息
     */
    @Override
    public FileInfo getFileInfo(String bucket, String key) {
        if(CharSequenceUtil.isBlank(bucket) || CharSequenceUtil.isBlank(key)){
            return null;
        }

        Map<String, String> objectMetadata = getObjectMetadata(bucket, key);
        if(ObjectUtil.isEmpty(objectMetadata)){
            return null;
        }

        FileInfo fileInfo = new FileInfo();
        fileInfo.setKey(MapUtil.getStr(objectMetadata, "key"));
        fileInfo.setPath(MapUtil.getStr(objectMetadata, "path"));
        fileInfo.setName(MapUtil.getStr(objectMetadata, "name"));
        fileInfo.setBucket(bucket);
        fileInfo.setPlatform(getStoragePlatformEnum().getValue());
        fileInfo.setSize(MapUtil.getLong(objectMetadata, "Content-Length"));
        fileInfo.setFriendlySize(DataSizeUtil.format(fileInfo.getSize()));
        fileInfo.setType(MapUtil.getStr(objectMetadata, "Content-Type"));
        fileInfo.setLastModified(new Date(MapUtil.getLong(objectMetadata, "lastModified")));
        fileInfo.setTimeoutMillis(MapUtil.getLong(objectMetadata, "timeoutMillis"));
        return fileInfo;
    }

    /**
     * 获取文件列表
     *
     * @param listFileInfo 查询条件
     * @return 文件列表
     */
    @Override
    public List<FileInfo> listFile(ListFileInfo listFileInfo) {
        return listFile(storageConfig.getBucket(), listFileInfo);
    }

    /**
     * 获取文件列表
     *
     * @param bucket       存储空间
     * @param listFileInfo 查询条件
     * @return 文件列表
     */
    @Override
    public List<FileInfo> listFile(String bucket, ListFileInfo listFileInfo) {
        if(CharSequenceUtil.isBlank(bucket)){
            return new ArrayList<>();
        }

        List<FileInfo> fileInfoList = new ArrayList<>();
        try {
            sftp = (Sftp) getClient();

            if(ObjectUtil.isNotEmpty(listFileInfo.getPath())) {
                // 查询某个路径下的所有文件及目录(层级1)
                String path = FileUtil.normalize(getBucketPath(bucket) + File.separator + listFileInfo.getPath());
                List<ChannelSftp.LsEntry> fileList = sftp.lsEntries(path, Objects::nonNull);
                for (ChannelSftp.LsEntry lsEntry : fileList) {
                    fileInfoList.add(toFileInfo(listFileInfo.getPath(), lsEntry, bucket));
                }
            } else if (ObjectUtil.isNotEmpty(listFileInfo.getTypes())) {
                // 获取指定类型的文件
                String basePath = FileUtil.normalize(getBucketPath(bucket));
                List<String> pathList = ListUtil.of(basePath);
                List<String> pathList2 = new ArrayList<>();
                String path;
                for (int i = 0; i < pathList.size(); i++) {
                    if(i == 0){
                        pathList2 = new ArrayList<>();
                    }

                    if(fileInfoList.size() >= 5000){
                        break;
                    }

                    path = pathList.get(i);
                    List<ChannelSftp.LsEntry> lsEntries = sftp.lsEntries(path, Objects::nonNull);
                    for (ChannelSftp.LsEntry lsEntry : lsEntries) {
                        if (lsEntry.getAttrs().isDir()) {
                            pathList2.add(FileUtil.normalize(path + File.separator + lsEntry.getFilename()));
                            continue;
                        }
                        if(listFileInfo.getTypes().contains(FileUtil.getSuffix(lsEntry.getFilename()))){
                            fileInfoList.add(toFileInfo(path.replace(basePath, StrPool.SLASH), lsEntry, bucket));
                        }
                    }

                    if(i + 1 == pathList.size() && ObjectUtil.isNotEmpty(pathList2)){
                        pathList = pathList2;
                        i = -1;
                    }
                }

            } else if (ObjectUtil.isNotEmpty(listFileInfo.getKeys()) || ObjectUtil.isNotEmpty(listFileInfo.getKey())) {
                List<String> keys = new ArrayList<>();
                if(ObjectUtil.isNotEmpty(listFileInfo.getKeys())){
                    keys.addAll(listFileInfo.getKeys());
                }
                if(ObjectUtil.isNotEmpty(listFileInfo.getKey())){
                    keys.add(listFileInfo.getKey());
                }
                // 获取某几个文件
                for (String key : keys) {
                    if(CharSequenceUtil.equals(StrPool.SLASH, key)){
                        continue;
                    }

                    String parentPath = FileUtil.normalize(Paths.get(key).getParent().toString());
                    String path = FileUtil.normalize(getBucketPath(bucket) + File.separator + parentPath);
                    List<ChannelSftp.LsEntry> ftpFiles = sftp.lsEntries(path, Objects::nonNull);
                    for (ChannelSftp.LsEntry ftpFile : ftpFiles) {
                        if (CharSequenceUtil.equals(FileNameUtil.getName(ftpFile.getFilename()), FileNameUtil.getName(key))) {
                            fileInfoList.add(toFileInfo(parentPath, ftpFile, bucket));
                        }
                    }
                }
            } else if (ObjectUtil.isNotEmpty(listFileInfo.getName())) {
                // 按文件名称模糊搜索
                String basePath = FileUtil.normalize(getBucketPath(bucket));
                List<String> pathList = ListUtil.of(basePath);
                List<String> pathList2 = new ArrayList<>();
                String path;
                for (int i = 0; i < pathList.size(); i++) {
                    if(i == 0){
                        pathList2 = new ArrayList<>();
                    }

                    path = pathList.get(i);
                    List<ChannelSftp.LsEntry> lsEntries = sftp.lsEntries(path, Objects::nonNull);
                    for (ChannelSftp.LsEntry lsEntry : lsEntries) {
                        if(lsEntry.getFilename().contains(listFileInfo.getName())){
                            fileInfoList.add(toFileInfo(path.replace(basePath, StrPool.SLASH), lsEntry, bucket));
                        }

                        if (lsEntry.getAttrs().isDir()) {
                            pathList2.add(FileUtil.normalize(path + File.separator + lsEntry.getFilename()));
                        }
                    }

                    if(i + 1 == pathList.size() && ObjectUtil.isNotEmpty(pathList2)){
                        pathList = pathList2;
                        i = -1;
                    }
                }
            } else {
                int k = 0;
                String basePath = FileUtil.normalize(getBucketPath(bucket));
                List<String> pathList = ListUtil.of(basePath);
                List<String> pathList2 = new ArrayList<>();
                String path;
                for (int i = 0; i < pathList.size(); i++) {
                    if(i == 0){
                        ++k;
                        if(k == 10){
                            break;
                        }

                        pathList2 = new ArrayList<>();
                    }

                    if(fileInfoList.size() >= 5000){
                        break;
                    }

                    path = pathList.get(i);
                    List<ChannelSftp.LsEntry> lsEntries = sftp.lsEntries(path, Objects::nonNull);
                    for (ChannelSftp.LsEntry lsEntry : lsEntries) {
                        fileInfoList.add(toFileInfo(path.replace(basePath, StrUtil.EMPTY), lsEntry, bucket));

                        if (lsEntry.getAttrs().isDir()) {
                            pathList2.add(FileUtil.normalize(path + File.separator + lsEntry.getFilename()));
                        }
                    }

                    if(i + 1 == pathList.size() && ObjectUtil.isNotEmpty(pathList2)){
                        pathList = pathList2;
                        i = -1;
                    }
                }

            }

            return fileInfoList;
        } finally {
            closeClient();
        }
    }

    private FileInfo toFileInfo(String path, ChannelSftp.LsEntry file, String bucket){
        FileInfo fileInfo = new FileInfo();
        fileInfo.setKey(FileUtil.normalize(path + File.separator + file.getFilename()));
        if(CharSequenceUtil.isBlank(path) || CharSequenceUtil.equals(StrPool.SLASH, path)){
            fileInfo.setPath(StrPool.SLASH);
        } else {
            fileInfo.setPath(FileUtil.normalize(path));
        }
        fileInfo.setName(FileUtil.getName(file.getFilename()));
        fileInfo.setBucket(bucket);
        fileInfo.setIsDirectory(file.getAttrs().isDir());
        fileInfo.setPlatform(getStoragePlatformEnum().getValue());
        fileInfo.setSize(file.getAttrs().getSize());
        fileInfo.setFriendlySize(DataSizeUtil.format(fileInfo.getSize()));
        fileInfo.setType(FileUtil.getMimeType(fileInfo.getKey()));
        fileInfo.setLastModified(new Date(file.getAttrs().getMTime() * 1000L));
        return fileInfo;
    }

    /**
     * 获取文件元数据信息
     *
     * @param key 文件唯一标识
     * @return 元数据信息
     */
    @Override
    public Map<String, String> getObjectMetadata(String key) {
        return getObjectMetadata(storageConfig.getBucket(), key);
    }

    /**
     * 获取文件元数据信息
     *
     * @param bucket 存储空间
     * @param key    文件唯一标识
     * @return 元数据信息
     */
    @Override
    public Map<String, String> getObjectMetadata(String bucket, String key) {
        if(CharSequenceUtil.isBlank(bucket) || CharSequenceUtil.isBlank(key)){
            return Collections.emptyMap();
        }


        try {
            sftp = (Sftp) getClient();

            String path = FileUtil.normalize(getBucketPath(bucket) + File.separator + key);
            if(!sftp.exist(path)){
                return Collections.emptyMap();
            }

            SftpATTRS sftpATTRS = sftp.getClient().lstat(path);

            Map<String, String> map = new HashMap<>();
            if(key.contains(StrPool.SLASH)){
                map.put("path", key.substring(0, key.lastIndexOf(StrPool.SLASH)));
            } else {
                map.put("path", StrPool.SLASH);
            }
            map.put("key", key);
            map.put("name", FileUtil.getName(key));
            map.put("Content-Type", null);
            map.put("Content-Length", sftpATTRS.getSize() + "");
            map.put("lastModified", sftpATTRS.getMTime() * 1000L + "");

            return map;
        } catch (SftpException e) {
            return Collections.emptyMap();
        } finally {
            closeClient();
        }
    }

    /**
     * 获取当前api的文件存储类型
     */
    @Override
    public StoragePlatformEnum getStoragePlatformEnum() {
        return StoragePlatformEnum.SFTP;
    }
}

本地存储文件API

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.file.FileNameUtil;
import cn.hutool.core.io.unit.DataSizeUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.text.StrPool;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.system.SystemUtil;
import com.smartadmin.common.enums.StoragePlatformEnum;
import com.smartadmin.common.model.FileInfo;
import com.smartadmin.common.model.ListFileInfo;
import com.smartadmin.common.model.StorageConfig;
import com.smartadmin.common.utils.FileAttributeUtil;

import java.io.File;
import java.io.InputStream;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

public class LocalFileOperatorApi implements FileOperatorApi{

    /**
     * 路径前缀
     */
    private String pathPrefix = "";

    /**
     * 存储配置
     */
    protected final StorageConfig storageConfig;

    public LocalFileOperatorApi(StorageConfig storageConfig) {
        this.storageConfig = storageConfig;
        initClient();
    }

    /**
     * 初始化客户端
     */
    private void initClient() {
        // 格式化路径为相对路径
        if(StrUtil.isNotBlank(storageConfig.getBasePath())){
            pathPrefix = FileUtil.normalize(storageConfig.getBasePath());
        } else {
            pathPrefix = SystemUtil.getOsInfo().isWindows() ? "D:/apps/storage" : "/apps/storage";
        }


        if (!FileUtil.exist(pathPrefix)) {
            FileUtil.mkdir(pathPrefix);
        }
    }

    /**
     * 关闭客户端
     */
    @Override
    public void closeClient() {
        // empty
    }

    /**
     * 获取客户端
     */
    @Override
    public Object getClient() {
        // empty
        return null;
    }

    private String getBucketPath(String bucket){
        return FileUtil.normalize(pathPrefix + File.separator + bucket);
    }

    /**
     * 判断存储空间是否存在
     *
     * @return @return Boolean {@code true} 存在 {@code false} 不存在
     */
    @Override
    public Boolean existBucket() {
        return existBucket(storageConfig.getBucket());
    }

    /**
     * 判断存储空间是否存在
     *
     * @param bucket 存储空间
     * @return @return Boolean {@code true} 存在 {@code false} 不存在
     */
    @Override
    public Boolean existBucket(String bucket) {
        if (CharSequenceUtil.isBlank(bucket)) {
            return false;
        }

        return FileUtil.exist(getBucketPath(bucket));
    }

    /**
     * 创建存储空间
     *
     * @return Boolean {@code true} 创建成功 {@code false} 创建失败
     */
    @Override
    public Boolean createBucket() {
        return createBucket(storageConfig.getBucket(), null);
    }

    /**
     * 创建存储空间
     *
     * @param bucket 存储空间
     * @return Boolean {@code true} 创建成功 {@code false} 创建失败
     */
    @Override
    public Boolean createBucket(String bucket) {
        return createBucket(bucket, null);
    }

    /**
     * 创建存储空间
     *
     * @param bucket         存储空间
     * @param expirationDays 存储空间的生命周期,单位天
     * @return Boolean {@code true} 创建成功 {@code false} 创建失败
     */
    @Override
    public Boolean createBucket(String bucket, Integer expirationDays) {
        if(CharSequenceUtil.isBlank(bucket)){
            return false;
        }

        return FileUtil.mkdir(getBucketPath(bucket)).exists();
    }

    /**
     * 删除存储空间
     *
     * @return Boolean {@code true} 删除成功 {@code false} 删除失败
     */
    @Override
    public Boolean deleteBucket() {
        return deleteBucket(storageConfig.getBucket());
    }

    /**
     * 删除存储空间
     *
     * @param bucket 存储空间
     * @return Boolean {@code true} 删除成功 {@code false} 删除失败
     */
    @Override
    public Boolean deleteBucket(String bucket) {
        if(CharSequenceUtil.isBlank(bucket)){
            return false;
        }

        return FileUtil.del(getBucketPath(bucket));
    }

    /**
     * 判断文件是否存在
     *
     * @param key 唯一标示id,例如a.txt, doc/a.txt
     * @return Boolean {@code true} 存在 {@code false} 不存在
     */
    @Override
    public Boolean existFile(String key) {
        return existFile(storageConfig.getBucket(), key);
    }

    /**
     * 判断文件是否存在
     *
     * @param bucket 存储空间
     * @param key    唯一标示id,例如a.txt, doc/a.txt
     * @return Boolean {@code true} 存在 {@code false} 不存在
     */
    @Override
    public Boolean existFile(String bucket, String key) {
        if(CharSequenceUtil.isBlank(bucket) || CharSequenceUtil.isBlank(key)){
            return false;
        }

        return FileUtil.exist(FileUtil.normalize(getBucketPath(bucket) + File.separator + key));
    }

    /**
     * 创建文件夹
     *
     * @param path 文件夹路径
     * @return Boolean {@code true} 创建成功 {@code false} 创建失败
     */
    @Override
    public Boolean createFolder(String path) {
        return createFolder(storageConfig.getBucket(), path);
    }

    /**
     * 创建文件夹
     *
     * @param bucket 存储空间
     * @param path   文件夹路径
     * @return Boolean {@code true} 创建成功 {@code false} 创建失败
     */
    @Override
    public Boolean createFolder(String bucket, String path) {
        if(CharSequenceUtil.isBlank(bucket) || CharSequenceUtil.isBlank(path)){
            return false;
        }

        // 拼接路径并格式化路径为相对路径
        String dirPath = FileUtil.normalize(getBucketPath(bucket) + File.separator + path);

        File file = FileUtil.mkdir(dirPath);
        return file.exists();
    }


    /**
     * 上传文件
     *
     * @param bytes 文件字节数组
     */
    @Override
    public String uploadFile(byte[] bytes) {
        return uploadFile(storageConfig.getBucket(), IdUtil.simpleUUID(), bytes, null);
    }

    /**
     * 上传文件
     *
     * @param inputStream 文件流
     */
    @Override
    public String uploadFile(InputStream inputStream) {
        return uploadFile(storageConfig.getBucket(), IdUtil.simpleUUID(), inputStream, null);
    }

    /**
     * 上传文件
     *
     * @param key   唯一标示id,例如a.txt, doc/a.txt
     * @param bytes 文件字节数组
     */
    @Override
    public String uploadFile(String key, byte[] bytes) {
        return uploadFile(storageConfig.getBucket(), key, bytes, null);
    }

    /**
     * 上传文件
     *
     * @param key         唯一标示id,例如a.txt, doc/a.txt
     * @param inputStream 文件流
     */
    @Override
    public String uploadFile(String key, InputStream inputStream) {
        return uploadFile(storageConfig.getBucket(), key, inputStream, null);
    }

    /**
     * 上传文件
     *
     * @param bucket 存储空间
     * @param key    唯一标示id,例如a.txt, doc/a.txt
     * @param bytes  文件字节数组
     */
    @Override
    public String uploadFile(String bucket, String key, byte[] bytes) {
        return uploadFile(bucket, key, bytes, null);
    }

    /**
     * 上传文件
     *
     * @param bucket      存储空间
     * @param key         唯一标示id,例如a.txt, doc/a.txt
     * @param inputStream 文件流
     */
    @Override
    public String uploadFile(String bucket, String key, InputStream inputStream) {
        return uploadFile(bucket, key, inputStream, null);
    }

    /**
     * 上传文件
     *
     * @param bucket         存储空间
     * @param key            唯一标示id,例如a.txt, doc/a.txt
     * @param bytes          文件字节数组
     * @param objectMetadata 元数据信息
     */
    @Override
    public String uploadFile(String bucket, String key, byte[] bytes, Map<String, String> objectMetadata) {
        return uploadFile(bucket, key, IoUtil.toStream(bytes), objectMetadata);
    }

    /**
     * 上传文件
     *
     * @param bucket         存储空间
     * @param key            唯一标示id,例如a.txt, doc/a.txt
     * @param inputStream    文件流
     * @param objectMetadata 元数据信息
     */
    @Override
    public String uploadFile(String bucket, String key, InputStream inputStream, Map<String, String> objectMetadata) {
        if(CharSequenceUtil.isBlank(bucket) || CharSequenceUtil.isBlank(key)){
            IoUtil.close(inputStream);
            return null;
        }

        try {
            // 拼接路径并格式化路径为相对路径
            String path = FileUtil.normalize(getBucketPath(bucket) + File.separator + key);

            FileUtil.writeFromStream(inputStream, path);

            // 设置元数据
            if(ObjectUtil.isNotEmpty(objectMetadata)){
                for (Map.Entry<String, String> entry : objectMetadata.entrySet()) {
                    FileAttributeUtil.setAttribute(path, entry.getKey(), entry.getValue());
                }
            }

            if(!FileUtil.exist(path)){
                return null;
            }

            return key;
        } finally {
            IoUtil.close(inputStream);
        }
    }

    /**
     * 下载文件
     *
     * @param key 唯一标示id,例如a.txt, doc/a.txt
     * @return byte[] 字节数组为文件的字节数组
     */
    @Override
    public byte[] downloadFile(String key) {
        return downloadFile(storageConfig.getBucket(), key);
    }

    /**
     * 下载文件
     *
     * @param bucket 存储空间
     * @param key    唯一标示id,例如a.txt, doc/a.txt
     * @return byte[] 字节数组为文件的字节数组
     */
    @Override
    public byte[] downloadFile(String bucket, String key) {
        if(CharSequenceUtil.isBlank(bucket) || CharSequenceUtil.isBlank(key)){
            return null;
        }

        // 拼接路径并格式化路径为相对路径
        String path = FileUtil.normalize(getBucketPath(bucket) + File.separator + key);

        // 判断文件是否存在
        if (!FileUtil.exist(path)) {
            return null;
        }

        return FileUtil.readBytes(path);
    }

    /**
     * 复制文件
     *
     * @param originFileKey 源文件名称
     * @param newFileKey    新文件名称
     */
    @Override
    public Boolean copyFile(String originFileKey, String newFileKey) {
        return copyFile(storageConfig.getBucket(), originFileKey, storageConfig.getBucket(), newFileKey);
    }

    /**
     * 复制文件
     *
     * @param originBucketName 源存储空间
     * @param originFileKey    源文件名称
     * @param newBucketName    新存储空间
     * @param newFileKey       新文件名称
     */
    @Override
    public Boolean copyFile(String originBucketName, String originFileKey, String newBucketName, String newFileKey) {
        if (!CharSequenceUtil.isAllNotBlank(originBucketName, originFileKey, newBucketName, newFileKey)) {
            return false;
        }

        String originFile = FileUtil.normalize(getBucketPath(originBucketName) + File.separator + originFileKey);

        // 判断文件存在不存在
        if (!FileUtil.exist(originFile)) {
            return false;
        }

        // 拷贝文件
        String destFile = FileUtil.normalize(getBucketPath(originBucketName) + File.separator + newFileKey);
        File file = FileUtil.copy(originFile, destFile, true);
        return file.exists();
    }

    /**
     * 重命名文件
     *
     * @param originFileKey 源文件名称
     * @param newFileKey   新文件名称
     */
    @Override
    public Boolean renameFile(String originFileKey, String newFileKey) {
        return renameFile(storageConfig.getBucket(), originFileKey, newFileKey);
    }

    /**
     * 重命名文件
     *
     * @param bucket        存储空间
     * @param originFileKey 源文件名称
     * @param newFileKey   新文件名称
     */
    @Override
    public Boolean renameFile(String bucket, String originFileKey, String newFileKey) {
        if (!CharSequenceUtil.isAllNotBlank(bucket, originFileKey, newFileKey)) {
            return false;
        }

        File targetFile = new File(FileUtil.normalize(getBucketPath(bucket) + File.separator + originFileKey));
        File destFile = new File(FileUtil.normalize(getBucketPath(bucket) + File.separator + newFileKey));
        FileUtil.move(targetFile, destFile, false);
        return destFile.exists();
    }

    /**
     * 获取文件的下载地址,生成外网地址
     *
     * @param key           文件唯一标识
     * @param timeoutMillis url失效时间,单位毫秒(最高7天)
     * @return 外部系统可以直接访问的url
     */
    @Override
    public String getFileLink(String key, Long timeoutMillis) {
        return getFileLink(storageConfig.getBucket(), key, timeoutMillis);
    }

    /**
     * 获取文件的下载地址,生成外网地址
     *
     * @param bucket        存储空间
     * @param key           文件唯一标识
     * @param timeoutMillis url失效时间,单位毫秒(最高7天)
     * @return 外部系统可以直接访问的url
     */
    @Override
    public String getFileLink(String bucket, String key, Long timeoutMillis) {
        // empty
        return null;
    }

    /**
     * 删除文件
     *
     * @param key 文件唯一标识
     */
    @Override
    public Boolean deleteFile(String key) {
        return deleteFile(storageConfig.getBucket(), key);
    }

    /**
     * 删除文件
     *
     * @param bucket 存储空间
     * @param key    文件唯一标识
     */
    @Override
    public Boolean deleteFile(String bucket, String key) {
        if(CharSequenceUtil.isBlank(bucket) || CharSequenceUtil.isBlank(key)){
            return false;
        }

        // 拼接路径并格式化路径为相对路径
        String path = FileUtil.normalize(getBucketPath(bucket) + File.separator + key);

        // 判断文件是否存在
        if (!FileUtil.exist(path)) {
            return false;
        }

        return FileUtil.del(path);
    }

    /**
     * 批量删除文件
     *
     * @param keys 文件唯一标识集合
     */
    @Override
    public Boolean deleteFiles(List<String> keys) {
        return deleteFiles(storageConfig.getBucket(), keys);
    }

    /**
     * 批量删除文件
     *
     * @param bucket 存储空间
     * @param keys   文件唯一标识集合
     */
    @Override
    public Boolean deleteFiles(String bucket, List<String> keys) {
        if(CharSequenceUtil.isBlank(bucket) || ObjectUtil.isEmpty(keys)){
            return false;
        }

        keys.stream().map(s -> getBucketPath(bucket) + File.separator + s).forEach(FileUtil::del);
        return true;
    }

    /**
     * 获取文件信息
     *
     * @param key 文件唯一标识
     * @return 文件信息
     */
    @Override
    public FileInfo getFileInfo(String key) {
        return getFileInfo(storageConfig.getBucket(), key);
    }

    /**
     * 获取文件信息
     *
     * @param bucket 存储空间
     * @param key    文件唯一标识
     * @return 文件信息
     */
    @Override
    public FileInfo getFileInfo(String bucket, String key) {
        if(CharSequenceUtil.isBlank(bucket) || CharSequenceUtil.isBlank(key)){
            return null;
        }

        Map<String, String> objectMetadata = getObjectMetadata(bucket, key);
        if(ObjectUtil.isEmpty(objectMetadata)){
            return null;
        }

        FileInfo fileInfo = new FileInfo();
        fileInfo.setKey(MapUtil.getStr(objectMetadata, "key"));
        fileInfo.setPath(MapUtil.getStr(objectMetadata, "path"));
        fileInfo.setName(MapUtil.getStr(objectMetadata, "name"));
        fileInfo.setBucket(bucket);
        fileInfo.setPlatform(getStoragePlatformEnum().getValue());
        fileInfo.setSize(MapUtil.getLong(objectMetadata, "Content-Length"));
        fileInfo.setFriendlySize(DataSizeUtil.format(fileInfo.getSize()));
        fileInfo.setType(MapUtil.getStr(objectMetadata, "Content-Type"));
        fileInfo.setLastModified(new Date(MapUtil.getLong(objectMetadata, "lastModified")));
        fileInfo.setTimeoutMillis(MapUtil.getLong(objectMetadata, "timeoutMillis"));
        return fileInfo;
    }

    /**
     * 获取文件列表
     *
     * @param listFileInfo 查询条件
     * @return 文件列表
     */
    @Override
    public List<FileInfo> listFile(ListFileInfo listFileInfo) {
        return listFile(storageConfig.getBucket(), listFileInfo);
    }

    /**
     * 获取文件列表
     *
     * @param bucket       存储空间
     * @param listFileInfo 查询条件
     * @return 文件列表
     */
    @Override
    public List<FileInfo> listFile(String bucket, ListFileInfo listFileInfo) {
        if(CharSequenceUtil.isBlank(bucket)){
            return new ArrayList<>();
        }

        List<File> fileList = new ArrayList<>();
        if(ObjectUtil.isNotEmpty(listFileInfo.getPath())) {
            // 查询某个路径下的所有文件及目录(层级1)
            String path = FileUtil.normalize(getBucketPath(bucket) + File.separator + listFileInfo.getPath());
            fileList = FileUtil.loopFiles(new File(path), 1, pathname -> !pathname.isHidden());
        } else if (ObjectUtil.isNotEmpty(listFileInfo.getTypes())) {
            // 获取指定类型的文件
            String path = FileUtil.normalize(getBucketPath(bucket) + File.separator);
            AtomicInteger k = new AtomicInteger();
            fileList = FileUtil.loopFiles(new File(path), -1, pathname -> {
                boolean isNotHidden = !pathname.isHidden()
                        && listFileInfo.getTypes().contains(FileUtil.getSuffix(pathname)) && k.get() <= 5000;

                if(isNotHidden){
                    k.incrementAndGet();
                }

                return isNotHidden;
            });
        } else if (ObjectUtil.isNotEmpty(listFileInfo.getKeys()) || ObjectUtil.isNotEmpty(listFileInfo.getKey())) {
            List<String> keys = new ArrayList<>();
            if(ObjectUtil.isNotEmpty(listFileInfo.getKeys())){
                keys.addAll(listFileInfo.getKeys());
            }
            if(ObjectUtil.isNotEmpty(listFileInfo.getKey())){
                keys.add(listFileInfo.getKey());
            }
            // 获取某几个文件
            for (String key : keys) {
                if(CharSequenceUtil.equals(StrPool.SLASH, key)){
                    continue;
                }

                String path = FileUtil.normalize(getBucketPath(bucket) + File.separator + key);
                File file = new File(path);

                if (!file.exists() || file.isHidden()) {
                    // 文件不存在或者是隐藏文件
                    continue;
                }

                fileList.add(file);
            }
        } else if (ObjectUtil.isNotEmpty(listFileInfo.getName())) {
            // 按文件名称模糊搜索
            String path = FileUtil.normalize(getBucketPath(bucket) + File.separator);
            fileList = FileUtil.loopFiles(new File(path), -1, pathname -> !pathname.isHidden()
                    && FileNameUtil.getName(pathname).contains(listFileInfo.getName()));
        } else {
            // 查询某个路径下的所有文件及目录
            String path = FileUtil.normalize(getBucketPath(bucket) + File.separator);
            AtomicInteger k = new AtomicInteger();
            fileList = FileUtil.loopFiles(new File(path), 10, pathname -> {
                boolean isNotHidden = !pathname.isHidden() && k.get() <= 5000;

                if(isNotHidden){
                    k.incrementAndGet();
                }

                return isNotHidden;
            });
        }

        if(ObjectUtil.isEmpty(fileList)) {
            return new ArrayList<>();
        }

        List<FileInfo> fileInfoList = new ArrayList<>();
        FileInfo fileInfo;
        for (File file : fileList) {
            fileInfo = new FileInfo();
            fileInfo.setKey(FileUtil.normalize(file.getAbsolutePath()).replace(getBucketPath(bucket), ""));
            fileInfo.setPath(FileUtil.normalize(Paths.get(fileInfo.getKey()).getParent().toString()));
            fileInfo.setName(FileUtil.getName(fileInfo.getKey()));
            fileInfo.setBucket(bucket);
            fileInfo.setIsDirectory(file.isDirectory());
            fileInfo.setPlatform(getStoragePlatformEnum().getValue());
            fileInfo.setSize(FileUtil.size(file));
            fileInfo.setFriendlySize(DataSizeUtil.format(fileInfo.getSize()));
            fileInfo.setType(FileUtil.getMimeType(fileInfo.getKey()));
            fileInfo.setLastModified(new Date(file.lastModified()));
            fileInfoList.add(fileInfo);
        }

        return fileInfoList;
    }

    public static void main(String[] args) {
        String name = FileUtil.getName("C:/Users/hanyo/Desktop/日期.txt");
        System.out.println(name);
        String prefix = FileUtil.getPrefix("C:/Users/hanyo/Desktop/日期.txt");
        System.out.println(prefix);
        String suffix = FileUtil.getSuffix("C:/Users/hanyo/Desktop/日期.txt");
        System.out.println(suffix);
        String parent = FileUtil.getParent("C:/Users/hanyo/Desktop/日期.txt", 1);
        System.out.println(parent);
        String parent2 = FileUtil.getParent("C:/Users/hanyo/Desktop/日期", 1);
        System.out.println(parent2);
        String parent3 = FileUtil.getParent("/Users", 1);
        System.out.println(parent3);

    }

    /**
     * 获取文件元数据信息
     *
     * @param key 文件唯一标识
     * @return 元数据信息
     */
    @Override
    public Map<String, String> getObjectMetadata(String key) {
        return getObjectMetadata(storageConfig.getBucket(), key);
    }

    /**
     * 获取文件元数据信息
     *
     * @param bucket 存储空间
     * @param key    文件唯一标识
     * @return 元数据信息
     */
    @Override
    public Map<String, String> getObjectMetadata(String bucket, String key) {
        // 拼接路径并格式化路径为相对路径
        String filePath = FileUtil.normalize(getBucketPath(bucket) + File.separator + key);
        if (!FileUtil.exist(filePath)) {
            return new HashMap<>(0);
        }

        File file = FileUtil.file(filePath);
        if(Boolean.FALSE.equals(existFile(bucket, key))){
            return new HashMap<>(0);
        }

        Map<String, String> map = FileAttributeUtil.getAttributes(filePath);
        if(key.contains(StrPool.SLASH)){
            map.put("path", key.substring(0, key.lastIndexOf(StrPool.SLASH)));
        } else {
            map.put("path", StrPool.SLASH);
        }
        map.put("key", key);
        map.put("name", FileUtil.getName(key));
        map.put("Content-Type", null);
        map.put("Content-Length", FileUtil.size(file) + "");
        map.put("lastModified", String.valueOf(file.lastModified()));

        return map;
    }

    /**
     * 获取当前api的文件存储类型
     */
    @Override
    public StoragePlatformEnum getStoragePlatformEnum() {
        return StoragePlatformEnum.LOCAL;
    }


}

代码测试

创建配置文件storage.yml

# 配置示例文件
sm:
  storage:
    client:
      ######################### minio配置  #########################
      - platform: minio
        endpoint: http://101.32.27.52:1900
        accessKey: AKIAIOSFODNN3EXAMPLE
        accessSecret: wJalrXUtnFE1I/K7MDENG/bPxRfiCYEXAMPLEKEY
        bucket: test666
        enable: false
      ######################### 阿里云oss配置 #########################
      - platform: aliyun-oss
        endpoint: http://s3.oss-cn-beijing.aliyuncs.com
        accessKey: LTAI5tDTr***tkNrQhHELptbG
        accessSecret: k2gK4vZkY***TXURQk3SKR83Fjze5h
        bucket: bucketdemo666
        enable: true
      ######################### 腾讯云cos配置 #########################
      - platform: tencent-cos
        endpoint: http://cos.ap-beijing.myqcloud.com
        accessKey: AKID3xeuxlLgnf***f7LK5cu1uupX00nM
        accessSecret: OXTP7cv1tXx****t52kFZheR1OXc
        bucket: test-1257373000
        enable: false
      ######################### ftp配置 #########################
      - platform: ftp
        endpoint: http://192.168.18.173:21
        accessKey: ftp
        accessSecret: ftp
        bucket: demo
        enable: true
      ######################### sftp配置 #########################
      - platform: sftp
        endpoint: http://101.35.26.52:22
        accessKey: root
        accessSecret: 12313#$%^&234YHsN
        basePath: /apps/storage/dev
        bucket: demo
        enable: true
      #########################  本地存储配置  #########################
      - platform: local
        bucket: demo
        basePath: D:/apps/storage/dev
        enable: true

创建读取配置文件的工具类

注意修改里面的路径

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.lang.Dict;
import cn.hutool.setting.yaml.YamlUtil;
import com.smartadmin.common.properties.StorageProperties;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;

public class StoragePropertiesUtil {

    public static StorageProperties getStorageProperties() {
        return getStorageProperties("配置示例/storage.yml");
    }


    public static StorageProperties getStorageProperties(String ymlPath) {
        Dict dict = YamlUtil.loadByPath(ymlPath);
        Map sm = dict.getBean("sm");
        Map storage = Optional.ofNullable(sm).map(map -> (Map) map.get("storage")).orElse(new LinkedHashMap<>());
        StorageProperties storageProperties = BeanUtil.mapToBean(storage, StorageProperties.class, true);
        return storageProperties;
    }

}

文件API测试

import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.json.JSONUtil;
import com.smartadmin.common.enums.StoragePlatformEnum;
import com.smartadmin.common.model.FileInfo;
import com.smartadmin.common.model.ListFileInfo;
import com.smartadmin.common.model.StorageConfig;
import com.smartadmin.common.properties.StorageProperties;
import com.smartadmin.common.utils.StoragePropertiesUtil;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 文件操作API工厂
 *
 * @Author: hanYong
 * @CreateTime: 2021-07-18
 */
public class OperatorApiFactory {

    /**
     * 根据存储平台获取文件操作API
     *
     * @param storagePlatformEnum 存储平台
     * @param storageProperties 存储配置
     * @return 文件操作API
     */
    public static FileOperatorApi getFileOperatorApi(StoragePlatformEnum storagePlatformEnum,
                                                     StorageProperties storageProperties) {
        if(ObjectUtil.isNotEmpty(storagePlatformEnum) && ObjectUtil.isNotEmpty(storageProperties)){
            StorageConfig storageConfig = storageProperties.getStorageConfig(storagePlatformEnum.getValue());
            if(ObjectUtil.isNotEmpty(storageConfig) && Boolean.TRUE.equals(storageConfig.getEnable())){
                return ReflectUtil.newInstance(storagePlatformEnum.getAClass(), storageConfig);
            }
        }

        return null;
    }


    public static void main(String[] args) {
        FileOperatorApi fileOperatorApi = getFileOperatorApi(
                StoragePlatformEnum.ALIYUN_OSS,
                StoragePropertiesUtil.getStorageProperties()
        );
//        fileOperatorApi.createBucket();
        ListFileInfo listFileInfo = new ListFileInfo();
//        listFileInfo.setKeys(List.of("/d.txt", "/fff", "/fff/ttt/新建文本文档.txt"));
        //listFileInfo.setKeys(List.of("demo/新建文件夹/", "demo/fff/新建文本文档.txt"));
        //listFileInfo.setName("新建");
//        listFileInfo.setPath("demo");
        listFileInfo.setTypes(ListUtil.of("txt"));
        List<FileInfo> fileInfos = fileOperatorApi.listFile(listFileInfo);
        System.out.println(JSONUtil.toJsonPrettyStr(fileInfos));

        //System.out.println("判断存储空间是否存在:" + fileOperatorApi.existBucket("demo"));
        //System.out.println("创建存储空间:" + fileOperatorApi.createBucket("demo"));
        //System.out.println("判断存储空间是否存在:" + fileOperatorApi.existBucket("demo"));
        //System.out.println("删除存储空间:" + fileOperatorApi.deleteBucket("demo"));
        //System.out.println("判断存储空间是否存在:" + fileOperatorApi.existBucket());
        //System.out.println("-----------------------------------------------");
        //System.out.println("判断文件是否存在:" + fileOperatorApi.existFile("a.txt"));
        //System.out.println("创建文件:" + fileOperatorApi.uploadFile("a.txt", StrUtil.bytes("123")));
        //System.out.println("判断文件是否存在:" + fileOperatorApi.existFile("a.txt"));
        //System.out.println("复制文件:" + fileOperatorApi.copyFile("a.txt", "b.txt"));
        //System.out.println("复制文件:" + fileOperatorApi.copyFile("a.txt", "fff/ttt/c.txt"));
        //System.out.println("复制文件:" + fileOperatorApi.copyFile("a.txt", "d.txt"));
        //System.out.println("删除文件:" + fileOperatorApi.deleteFile("a.txt"));
        //System.out.println("批量删除文件:" + fileOperatorApi.deleteFiles(ListUtil.of("a.txt", "b.txt")));
        //System.out.println("获取文件元数据:" + fileOperatorApi.getObjectMetadata("d.txt"));
        //System.out.println("获取文件数据:" + StrUtil.str(fileOperatorApi.downloadFile("d.txt"), CharsetUtil.CHARSET_UTF_8));
        //System.out.println("获取文件信息:" + fileOperatorApi.getFileInfo("fff/ttt/c.txt"));
        //System.out.println("获取文件链接:" + fileOperatorApi.getFileLink("fff/ttt/c.txt", 7L));
        //System.out.println("-----------------------------------------------");

    }

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值