【OSS对象存储】Springboot集成阿里云OSS + 私有化部署Minio

news2024/9/20 10:28:08

【OSS对象存储】Springboot集成阿里云OSS + 私有化部署Minio

  • 一、摘要
  • 二、POM依赖
  • 三、配置文件
  • 四、表结构设计
  • 五、代码实现
    • 5.1 代码包结构
    • 5.2 API封装
    • 5.3 增删改查
  • 六、扩展
    • 6.1 Minio配置https访问

一、摘要

  1. 掌握阿里云OSS、私有化部署Minio两种对象存储的使用方式
  2. 运用工厂+策略模式,封装OSS对象存储API,可实现动态切换
  3. Docker部署Minio

二、POM依赖

<!-- 阿里云对象存储 -->
<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
    <version>3.15.1</version>
</dependency>

<!-- Minio私有化对象存储 -->
<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>8.5.6</version>
</dependency>

三、配置文件

ai-wxapp:
  oss:
  #  strategy: ALI_OSS
  #  endpoint: oss-cn-beijing.aliyuncs.com
  #  accessKeyId: xxxxxx
  #  accessKeySecret: xxxxxx
  #  bucketName: xxxx
  strategy: MinIO
  endpoint: http://localhost:9000
  accessKeyId: xxxxxx
  accessKeySecret: xxxxxx
  bucketName: xxxx

OssConfig.java:

@Data
@Component
@RefreshScope
public class OssConfig {
    @Value("${ai-wxapp.oss.strategy}")
    private String ossStrategy;
    @Value("${ai-wxapp.oss.bucketName}")
    private String bucketName;
    @Value("${ai-wxapp.oss.endpoint}")
    private String endpoint;
    @Value("${ai-wxapp.oss.accessKeyId}")
    private String accessKeyId;
    @Value("${ai-wxapp.oss.accessKeySecret}")
    private String accessKeySecret;
}

四、表结构设计

表结构数据

CREATE TABLE `oss_file` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `file_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '文件id',
  `original_file_name` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '原始文件名',
  `file_ext` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '文件扩展名',
  `file_path` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '文件路径',
  `create_by` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '创建人',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_by` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '修改人',
  `update_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `oss_file_unique` (`file_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='对象存储信息表';

五、代码实现

5.1 代码包结构

在这里插入图片描述

5.2 API封装

ObjectStorageEnum.java:
```java
/**
 * @ClassName ObjectStorageEnum
 * @Description 对象存储策略枚举
 * @Author shn
 * @Date 2023/11/2 18:17
 * @Version 1.0
 **/
@Getter
public enum ObjectStorageEnum {
    /**
     * 阿里云OSS对象存储
     */
    ALI_OSS,
    /**
     * MinIO私有化对象存储
     */
    MinIO
}

IOssFileRepository.java

public interface IOssFileRepository {

    OssFileEntity getByFileId(String fileId);

    List<OssFileEntity> getByFileIds(List<String> fileIds);

    void saveEntity(OssFileEntity build);
}

ObjectStorageFactory.java

/**
 * @ClassName ObjectStorageFactory
 * @Description 对象存储策略工厂
 * @Author shn
 * @Date 2023/11/2 18:23
 * @Version 1.0
 **/
@Component
public class ObjectStorageFactory implements ApplicationContextAware, InitializingBean {

    private ApplicationContext applicationContext;

    private static final Map<ObjectStorageEnum, ObjectStorage> SERVICES = new HashMap<>();

    /**
     * 根据枚举获取策略实现类实例
     */
    public ObjectStorage getInstance(ObjectStorageEnum storageEnum) {
        return SERVICES.get(storageEnum);
    }

    @Override
    public void afterPropertiesSet() {
        Map<String, ObjectStorage> beans = applicationContext.getBeansOfType(ObjectStorage.class);
        for (ObjectStorage bean : beans.values()) {
            SERVICES.put(bean.getStrategyEnum(), bean);
        }
    }

    @Override
    public void setApplicationContext(@NotNull ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

ObjectStorage.java:

/**
 * @ClassName ObjectStorage
 * @Description 对象存储接口,异常全部对外抛出
 * @Author shn
 * @Date 2023/11/2 17:51
 * @Version 1.0
 **/
public interface ObjectStorage {

    /**
     * 获取策略枚举
     */
    ObjectStorageEnum getStrategyEnum();

    /**
     * 判断对象是否存在
     *
     * @param bucketName 桶名称
     * @param objectName 对象存储全路径,不包含桶名称
     */
    boolean objectExists(String bucketName, String objectName) throws Exception;

    /**
     * 上传对象
     *
     * @param bucketName 桶名称
     * @param objectName 对象存储全路径,不包含桶名称
     * @param in         输入流
     */
    void uploadObject(String bucketName, String objectName, InputStream in) throws Exception;

    /**
     * 下载对象
     *
     * @param bucketName 桶名称
     * @param objectName 对象存储全路径,不包含桶名称
     */
    byte[] downloadObject(String bucketName, String objectName) throws Exception;

    /**
     * 删除对象
     *
     * @param bucketName 桶名称
     * @param objectName 对象存储全路径,不包含桶名称
     */
    void deleteObject(String bucketName, String objectName) throws Exception;

    /**
     * 删除对象
     *
     * @param bucketName  桶名称
     * @param objectNames 对象存储全路径,不包含桶名称
     */
    void deleteObjects(String bucketName, List<String> objectNames);

    /**
     * 判断桶是否存在
     *
     * @param bucketName 桶名称
     */
    boolean bucketExists(String bucketName) throws Exception;

    /**
     * 创建桶
     *
     * @param bucketName 桶名称
     */
    void createBucket(String bucketName) throws Exception;

    /**
     * 删除桶
     *
     * @param bucketName 桶名称
     */
    void deleteBucket(String bucketName) throws Exception;

    /**
     * 列出桶中的对象
     *
     * @param bucketName 桶名称
     */
    List<String> listObjects(String bucketName) throws Exception;

    /**
     * 获取临时授权访问url
     *
     * @param bucketName    桶名称
     * @param objectName    对象存储全路径,不包含桶名称
     * @param expireSeconds 过期时间(单位:秒)
     * @return java.lang.String
     * @author shn 2023/11/8 13:27
     */
    String getTempAccessUrl(String bucketName, String objectName, int expireSeconds);
}

AliOssService.java

/**
 * @ClassName AliOssService
 * @Description 阿里云OSS对象存储
 * @Author shn
 * @Date 2023/11/2 18:10
 * @Version 1.0
 **/
@Slf4j
@Service("aliOssService")
public class AliOssService implements ObjectStorage {

    @Resource
    private OssConfig ossConfig;

    /**
     * 获取ossClient <br>
     * 1)每次请求都创建一个新的ossClient <br>
     * 2)虽然ossClient是线程安全的,但是考虑到OssConfig支持动态刷新,所以这里没有设计为单例。<br>
     * 3)因此ossClient需要手动关闭
     * 参考:<a href="https://help.aliyun.com/zh/oss/developer-reference/initialization-3">...</a> <br>
     */
    private OSS getOssClient() {
        return new OSSClientBuilder().build(ossConfig.getEndpoint(),
                ossConfig.getAccessKeyId(), ossConfig.getAccessKeySecret());
    }

    /**
     * 关闭ossClient
     */
    private void close(OSS ossClient) {
        if (ossClient != null) {
            ossClient.shutdown();
        }
    }

    @Override
    public ObjectStorageEnum getStrategyEnum() {
        return ObjectStorageEnum.ALI_OSS;
    }

    @Override
    public boolean objectExists(String bucketName, String objectName) {
        OSS ossClient = getOssClient();
        try {
            boolean bucketExist = ossClient.doesBucketExist(bucketName);
            if (!bucketExist) {
                return false;
            }
            boolean exists = ossClient.doesObjectExist(bucketName, objectName);
            if (!exists) {
                return false;
            }
        } finally {
            close(ossClient);
        }
        return true;
    }

    @Override
    public void uploadObject(String bucketName, String objectName, InputStream in) throws Exception {
        if (in.available() <= 0) {
            throw new IOException("输入流为空");
        }
        OSS ossClient = getOssClient();
        try {
            boolean bucketExist = ossClient.doesBucketExist(bucketName);
            if (!bucketExist) {
                throw new ServiceException("bucket不存在");
            }
            ossClient.putObject(bucketName, objectName, in);
            log.info("文件上传OSS成功,文件路径: {}", objectName);
        } finally {
            close(ossClient);
            in.close();
        }
    }

    @Override
    public byte[] downloadObject(String bucketName, String objectName) {
        OSS ossClient = getOssClient();
        try {
            boolean exists = ossClient.doesObjectExist(bucketName, objectName);
            if (!exists) {
                throw new ServiceException("文件不存在");
            }
            // ossObject包含文件所在的存储空间名称、文件名称、文件元信息以及一个输入流。
            OSSObject ossObject = ossClient.getObject(bucketName, objectName);
            InputStream in = ossObject.getObjectContent();
            // 该API默认会关闭流
            return IoUtil.readBytes(in);
        } finally {
            close(ossClient);
        }
    }

    @Override
    public void deleteObject(String bucketName, String objectName) {
        OSS ossClient = getOssClient();
        try {
            // 删除文件或目录。如果要删除目录,目录必须为空。
            ossClient.deleteObject(bucketName, objectName);
        } finally {
            close(ossClient);
        }
    }

    @Override
    public void deleteObjects(String bucketName, List<String> objectNames) {
        OSS ossClient = getOssClient();
        try {
            DeleteObjectsRequest request = new DeleteObjectsRequest(bucketName);
            request.setKeys(objectNames);
            // 静默删除,删除操作不返回删除结果信息,效率高,需结合业务场景选择是否开启
            request.setQuiet(true);
            ossClient.deleteObjects(request);
        } finally {
            close(ossClient);
        }
    }

    @Override
    public boolean bucketExists(String bucketName) {
        OSS ossClient = getOssClient();
        try {
            // 判断存储空间是否存在
            boolean exists = ossClient.doesBucketExist(bucketName);
            if (!exists) {
                return false;
            }
        } finally {
            close(ossClient);
        }
        return true;
    }

    @Override
    public void createBucket(String bucketName) {
        OSS ossClient = getOssClient();
        try {
            // 创建CreateBucketRequest对象。
            CreateBucketRequest createBucketRequest = new CreateBucketRequest(bucketName);
            // 如果创建存储空间的同时需要指定存储类型、存储空间的读写权限、数据容灾类型, 请参考如下代码。
            // 此处以设置存储空间的存储类型为标准存储为例介绍。
            //createBucketRequest.setStorageClass(StorageClass.Standard);
            // 数据容灾类型默认为本地冗余存储,即DataRedundancyType.LRS。如果需要设置数据容灾类型为同城冗余存储,请设置为DataRedundancyType.ZRS。
            //createBucketRequest.setDataRedundancyType(DataRedundancyType.ZRS);
            // 设置存储空间读写权限为公共读,默认为私有。
            //createBucketRequest.setCannedACL(CannedAccessControlList.PublicRead);
            // 在支持资源组的地域创建Bucket时,您可以为Bucket配置资源组。
            //createBucketRequest.setResourceGroupId(rsId);

            // 创建存储空间
            ossClient.createBucket(createBucketRequest);
        } finally {
            close(ossClient);
        }
    }

    @Override
    public void deleteBucket(String bucketName) {
        OSS ossClient = getOssClient();
        try {
            // 删除存储空间
            ossClient.deleteBucket(bucketName);
        } finally {
            close(ossClient);
        }
    }

    @Override
    public List<String> listObjects(String bucketName) {
        return null;
    }

    @Override
    public String getTempAccessUrl(String bucketName, String objectName, int expireSeconds) {
        OSS ossClient = getOssClient();
        try {
            boolean exists = ossClient.doesObjectExist(bucketName, objectName);
            if (!exists) {
                throw new ServiceException("文件不存在");
            }
            String url = ossClient.generatePresignedUrl(
                    bucketName, objectName, DateUtil.offsetSecond(new Date(), expireSeconds)).toString();
            return StringUtil.replaceHttp2Https(url);
        } finally {
            close(ossClient);
        }
    }
}

MinIoService.java:

@Slf4j
@Service("minIoService")
public class MinIoService implements ObjectStorage {

    @Resource
    private OssConfig ossConfig;

    /**
     * 获取 MinioClient,每次请求都创建一个新的 MinioClient
     */
    private MinioClient getMinioClient() {
        return MinioClient.builder()
                .endpoint(ossConfig.getEndpoint())
                .credentials(ossConfig.getAccessKeyId(), ossConfig.getAccessKeySecret())
                .build();
    }

    @Override
    public ObjectStorageEnum getStrategyEnum() {
        return ObjectStorageEnum.MinIO;
    }

    @SneakyThrows(Exception.class)
    @Override
    public boolean objectExists(String bucketName, String objectName) {
        return false;
    }

    @SneakyThrows(Exception.class)
    @Override
    public void uploadObject(String bucketName, String objectName, InputStream in) {
        if (in.available() <= 0) {
            throw new IOException("输入流为空");
        }
        try {
            getMinioClient().putObject(
                    PutObjectArgs.builder().bucket(bucketName).object(objectName)
                            .stream(in, in.available(), -1).build());
            log.info("文件上传Minio成功,文件路径: {}", objectName);
        } finally {
            in.close();
        }
    }

    @SneakyThrows(Exception.class)
    @Override
    public byte[] downloadObject(String bucketName, String objectName) {
        InputStream in = getMinioClient().getObject(
                GetObjectArgs.builder().bucket(bucketName).object(objectName).build());
        // 该API默认会关闭流
        return IoUtil.readBytes(in);
    }

    @SneakyThrows(Exception.class)
    @Override
    public void deleteObject(String bucketName, String objectName) {
        getMinioClient().removeObject(
                RemoveObjectArgs.builder().bucket(bucketName).object(objectName).build());
    }

    @SneakyThrows(Exception.class)
    @Override
    public void deleteObjects(String bucketName, List<String> objectNames) {
        if (CollectionUtil.isEmpty(objectNames)) {
            return;
        }
        getMinioClient().removeObjects(
                RemoveObjectsArgs.builder()
                        .bucket(bucketName)
                        .objects(objectNames.stream().map(DeleteObject::new).collect(Collectors.toList()))
                        .build());
    }

    @SneakyThrows(Exception.class)
    @Override
    public boolean bucketExists(String bucketName) {
        return getMinioClient().bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
    }

    @SneakyThrows(Exception.class)
    @Override
    public void createBucket(String bucketName) {
        MinioClient minioClient = getMinioClient();
        boolean exist = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
        if (!exist) {
            minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
        }
    }

    @SneakyThrows(Exception.class)
    @Override
    public void deleteBucket(String bucketName) {
        getMinioClient().removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
    }

    @SneakyThrows(Exception.class)
    @Override
    public List<String> listObjects(String bucketName) {
        return null;
    }

    @SneakyThrows(Exception.class)
    @Override
    public String getTempAccessUrl(String bucketName, String objectName, int expireSeconds) {
        String url = getMinioClient().getPresignedObjectUrl(
                GetPresignedObjectUrlArgs.builder()
                        .method(Method.GET)
                        .bucket(bucketName)
                        .object(objectName)
                        .expiry(expireSeconds).build());
        log.info("Minio origin url: {}", url);
        return url;
    }
}

5.3 增删改查

OssFileEntity.java:

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class OssFileEntity {

    private String fileId;

    private String originalFileName;

    private String fileExt;

    private String filePath;
}
IOssFileService.java
```java
public interface IOssFileService {

    OssFileEntity getByFileId(String fileId);

    byte[] downloadFile(String fileId) throws Exception;

    void deleteFiles(List<String> fileIds);

    String uploadFile(byte[] fileBytes, String originalFileName);

    String getTempAccessUrl(String fileId, int expireSeconds);

}

OssFileService.java

@Slf4j
@Service
public class OssFileService implements IOssFileService {

    @Resource
    private OssConfig ossConfig;
    @Resource
    private ObjectStorageFactory objectStorageFactory;
    @Resource
    private RedisUtils redisUtils;
    @Resource
    private IOssFileRepository ossFileRepository;

    private ObjectStorage getObjectStorage() {
        return objectStorageFactory.getInstance(ObjectStorageEnum.valueOf(ossConfig.getOssStrategy()));
    }

    public OssFileEntity getByFileId(String fileId) {
        OssFileEntity entity = ossFileRepository.getByFileId(fileId);
        if (entity == null) {
            throw new ServiceException(CommonError.OSS_FILE_NOT_EXIST);
        }
        return entity;
    }

    public byte[] downloadFile(String fileId) throws Exception {
        OssFileEntity entity = getByFileId(fileId);
        return getObjectStorage().downloadObject(ossConfig.getBucketName(), entity.getFilePath());
    }

    public void deleteFiles(List<String> fileIds) {
        List<OssFileEntity> list = ossFileRepository.getByFileIds(fileIds);
        if (CollectionUtil.isEmpty(list)) {
            return;
        }
        getObjectStorage().deleteObjects(ossConfig.getBucketName(),
                list.stream().map(OssFileEntity::getFilePath).collect(Collectors.toList()));
    }

    /**
     * 文件上传oss
     *
     * @param fileBytes        文件
     * @param originalFileName 文件原始名称
     * @return java.lang.String 文件id
     * @author shn 2023/12/13 11:42
     */
    public String uploadFile(byte[] fileBytes, String originalFileName) {
        if (ArrayUtil.isEmpty(fileBytes)
                || StringUtils.isBlank(originalFileName)) {
            throw new ServiceException(CommonError.OSS_FILE_UPLOAD_ERROR);
        }
        // 上传oss
        String fileId = this.generateFileId();
        String fileExtension = FileUtil.getFileExtension(originalFileName);
        String filePath = buildFilePath(fileId, fileExtension);
        try {
            InputStream inputStream = new ByteArrayInputStream(fileBytes);
            getObjectStorage().uploadObject(ossConfig.getBucketName(), filePath, inputStream);
        } catch (Exception e) {
            log.error("文档上传失败", e);
            throw new ServiceException(CommonError.OSS_FILE_UPLOAD_ERROR);
        }

        // 保存数据库
        ossFileRepository.saveEntity(OssFileEntity.builder()
                .fileId(fileId)
                .originalFileName(originalFileName)
                .fileExt(fileExtension)
                .filePath(filePath)
                .build());

        return fileId;
    }

    /**
     * 获取临时url
     *
     * @param fileId        文件id
     * @param expireSeconds 过期时间(秒)
     * @return java.lang.String
     * @author shn 2024/05/29 15:36
     */
    public String getTempAccessUrl(String fileId, int expireSeconds) {
        OssFileEntity entity = getByFileId(fileId);

        // 先查缓存
        String key = String.format(RedisKeys.OSS_FILE_URL_KEY, fileId);
        if (redisUtils.hasKey(key) && redisUtils.getExpire(key) > 0L) {
            return String.valueOf(redisUtils.get(key));
        }

        // 缓存不存在,则从OSS获取临时URL。设置OSS过期时间比 Redis晚2秒,保证 URL一直可用。
        String url = getObjectStorage().getTempAccessUrl(
                ossConfig.getBucketName(), entity.getFilePath(), expireSeconds + 2);
        // 保存临时URL到Redis
        redisUtils.set(key, url, expireSeconds, TimeUnit.SECONDS);

        return url;
    }

    /**
     * 构建文件存储路径
     */
    @NotNull
    private static String buildFilePath(String fileId, String fileExtension) {
        return DateUtil.today() + "/" + fileId + fileExtension;
    }

    /**
     * 生成文件id
     */
    private String generateFileId() {
        String fileId = "file_" + IdUtil.simpleUUID();
        log.info("generate new file id: {}", fileId);
        return fileId;
    }
}

六、扩展

6.1 Minio配置https访问

目标:实现https域名访问minio控制台、资源
步骤:
1)前往阿里云下载Apache证书文件
在这里插入图片描述
2)修改公钥和私钥文件名为private.key、public.crt 并上传至服务端
在这里插入图片描述
3)Nginx配置

upstream minio_s3 {
   #least_conn;
   server 127.0.0.1:9000;
}

upstream minio_console {
   #least_conn;
   server 127.0.0.1:9001;
}

server {
    listen 80;
    server_name huiling.xxxx.com;
    
    listen 443 ssl;
    server_name huiling.xxxx.com;
    # SSL-START SSL相关配置,请勿删除或修改下一行带注释的404规则
    # error_page 404/404.html;
    ssl_certificate     /etc/nginx/ssl/huiling/huiling.xxxx.com.pem;  # pem文件的路径
    ssl_certificate_key  /etc/nginx/ssl/huiling/huiling.xxxx.com.key; # key文件的路径
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;
    # error_page 497  https://$host$request_uri;

    proxy_buffering off;
    proxy_request_buffering off;
    
    location / {
      client_max_body_size 10m;
      proxy_set_header Host $http_host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme;

      proxy_connect_timeout 300;
      # Default is HTTP/1, keepalive is only enabled in HTTP/1.1
      proxy_http_version 1.1;
      proxy_set_header Connection "";
      chunked_transfer_encoding off;

      proxy_pass https://minio_s3; # This uses the upstream directive definition to load balance
   }

   location /minio/ui/ {
      rewrite ^/minio/ui/(.*) /$1 break;
      proxy_set_header Host $http_host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme;
      proxy_set_header X-NginX-Proxy true;

      # This is necessary to pass the correct IP to be hashed
      real_ip_header X-Real-IP;

      proxy_connect_timeout 300;

      # To support websockets in MinIO versions released after January 2023
      proxy_http_version 1.1;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "upgrade";
      # Some environments may encounter CORS errors (Kubernetes + Nginx Ingress)
      # Uncomment the following line to set the Origin request to an empty string
      # proxy_set_header Origin '';

      chunked_transfer_encoding off;

      proxy_pass https://minio_console; # This uses the upstream directive definition to load balance
   }
}

4)Docker启动配置

# 定义compose语义版本
version: '3.8'
# 定义服务
services:
  minio:
    image: minio/minio:latest
    container_name: minio
    restart: unless-stopped
    command: server /data --console-address ":9001" -address ":9000"
    environment:
      TZ: Asia/Shanghai
      LANG: en_US.UTF-8
      MINIO_SERVER_URL: https://huiling.xxx.com
      MINIO_BROWSER_REDIRECT_URL: https://huiling.xxx.com/minio/ui
      MINIO_ROOT_USER: minioadmin
      MINIO_ROOT_PASSWORD: minioadmin9339
    volumes:
      - "/usr/local/docker/minio/data:/data"
      - "/usr/local/docker/minio/config:/root/.minio"
    ports:
      - "9000:9000"
      - "9001:9001"

5)验证
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1947628.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

STM32-寄存器ADC配置指南

目录 输入方式&#xff1a; 模拟看门狗功能&#xff1a; ADC中断 配置一个Demo 设置时钟 自校准 通道选择 采样时间选择 转换模式选择 断续模式 启动转换 软件触发 外部触发 转换结束 关于DMA 模拟看门狗 ​编辑ADC数据位置​编辑 在STM32F中&#xff0c;ADC可…

FM与AM的特点

1.是什么&#xff1f; FM&#xff08;调频&#xff09;&#xff1a;通过改变载波频率来传递信息AM&#xff08;调幅&#xff09;&#xff1a;通过改变载波的振幅来传递信息 2.分别有什么特点&#xff1f; 抗干扰能力&#xff1a; FM&#xff1a;由于FM信号的传输不依赖于载波的…

c++初阶知识——string类详解

目录 前言&#xff1a; 1.标准库中的string类 1.1 auto和范围for auto 范围for 1.2 string类常用接口说明 1.string类对象的常见构造 1.3 string类对象的访问及遍历操作 1.4. string类对象的修改操作 1.5 string类非成员函数 2.string类的模拟实现 2.1 经典的string…

【CI/CD】docker + Nginx自动化构建部署

CI/CD是什么 CI/CD 是持续集成&#xff08;Continuous Integration&#xff09;和持续部署&#xff08;Continuous Deployment&#xff09;或持续交付&#xff08;Continuous Delivery&#xff09;的缩写&#xff0c;它们是现代软件开发中用于自动化软件交付过程的实践。 1、…

自动驾驶系列—智能巡航辅助功能中的路口通行功能介绍

自动驾驶系列—智能巡航辅助功能中的车道中央保持功能介绍 自动驾驶系列—智能巡航辅助功能中的车道变换功能介绍 自动驾驶系列—智能巡航辅助功能中的横向避让功能介绍 自动驾驶系列—智能巡航辅助功能中的路口通行功能介绍 文章目录 2. 功能定义3. 功能原理4. 传感器架构5. 实…

【Redis进阶】集群

1. 集群分片算法 1.1 集群概述 首先对于"集群"这个概念是存在不同理解的&#xff1a; 广义的"集群"&#xff1a;表示由多台主机构成的分布式系统&#xff0c;称为"集群"狭义的"集群"&#xff1a;指的是redis提供的一种集群模式&…

牛客JS题(二)直角三角形

注释很详细&#xff0c;直接上代码 涉及知识点&#xff1a; repeat格式化字符串 题干&#xff1a; 我的答案 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"></head><body><div classtriangle><…

C++树形结构(1 基础)

目录 一.基础&#xff1a; 1.概念&#xff1a; 2.定义&#xff1a; Ⅰ.树的相关基础术语&#xff1a; Ⅱ.树的层次&#xff1a; 3.树的性质&#xff1a; 二.存储思路&#xff1a; 1.结构体存储&#xff1a; 2.数组存储&#xff1a; 三.树的遍历模板&#xff1a; 四.信…

【BUG】已解决:NameError: name ‘python‘ is not defined

NameError: name ‘python‘ is not defined 目录 NameError: name ‘python‘ is not defined 【常见模块错误】 【解决方案】 欢迎来到英杰社区https://bbs.csdn.net/topics/617804998 欢迎来到我的主页&#xff0c;我是博主英杰&#xff0c;211科班出身&#xff0c;就职于…

【TortoiseGit】合并单个commit(提交)到指定分支上

0、前言 当我们用Git的时候经常用到多个分支&#xff0c;会经常有如下情况&#xff1a;一个dev分支下面会有多个test分支&#xff0c;而每个test分支由不同的开发者。而我们会有这样的需求&#xff1a; 当某个test分支完成了相应功能验证&#xff0c;就要把成功验证的功能代码…

【Git】上传代码命令至codeup云效管理平台

通过git命令上传本地代码库至阿里的codeup云效管理平台的代码管理模块&#xff0c;使用方便&#xff0c;且比github上传网络环境要求低&#xff0c;超大文件&#xff08;>100M&#xff09;的文件也可以批量上传&#xff0c;且上传速度喜人。 目录 &#x1f337;&#x1f33…

[Vulnhub] Acid-Reloaded SQLI+图片数据隐写提取+Pkexec权限提升+Overlayfs权限提升

信息收集 IP AddressOpening Ports192.168.101.158TCP:22,33447 $ nmap -p- 192.168.101.158 --min-rate 1000 -sC -sV Not shown: 65534 closed tcp ports (conn-refused) PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 6.7p1 Ubuntu 5ubuntu1.3 (Ubuntu Lin…

【NOI-题解】1009 - 数组逆序1162 - 数组元素的删除1211 - 数组元素的插入1161. 元素插入有序数组1159. 数组元素的移动

文章目录 一、前言二、问题问题&#xff1a;1009 - 数组逆序问题&#xff1a;1162 - 数组元素的删除问题&#xff1a;1211 - 数组元素的插入问题&#xff1a;1161. 元素插入有序数组问题&#xff1a;1159. 数组元素的移动 三、感谢 一、前言 本章节主要对数组问题中数组元素移…

昇思25天学习打卡营第23天 | 基于MindSpore的红酒分类实验

学习心得&#xff1a;基于MindSpore的红酒分类实验 在机器学习的学习路径中&#xff0c;理解和实践经典算法是非常重要的一步。最近我进行了一个有趣的实验&#xff0c;使用MindSpore框架实现了K近邻&#xff08;KNN&#xff09;算法进行红酒分类。这个实验不仅加深了我对KNN算…

Jenkins+Gitlab持续集成综合实战

一、持续集成应用背景&#xff1a; DevOps&#xff1a;&#xff08;英文Development&#xff08;开发&#xff09;和Operations&#xff08;技术运营&#xff09;的组合&#xff09;是一组过程、方法与系统的统称&#xff0c;用于促进开发&#xff08;应用程序/软件工程&#…

设计模式|观察者模式

观察者模式是一种行为设计模式&#xff0c;它定义了一种一对多的依赖关系&#xff0c;让多个观察者对象同时监听某一个主题对象。当主题对象发生变化时&#xff0c;它的所有观察者都会收到通知并更新。观察者模式常用于实现事件处理系统、发布-订阅模式等。在项目中&#xff0c…

C语言 | Leetcode C语言题解之第279题完全平方数

题目&#xff1a; 题解&#xff1a; // 判断是否为完全平方数 bool isPerfectSquare(int x) {int y sqrt(x);return y * y x; }// 判断是否能表示为 4^k*(8m7) bool checkAnswer4(int x) {while (x % 4 0) {x / 4;}return x % 8 7; }int numSquares(int n) {if (isPerfect…

CSA笔记5-局域网yum源配置互联网yum源配置源代码编译安装

局域网yum源配置&#xff1a;建设一个本地网络仓库给本地局域网用户提供下载安装 互联网yum源配置&#xff1a;在线获取最新安装文件的通道 1.局域网web方式yum源配置&#xff1a; 服务器提供yum安装服务&#xff1a; yum install -y httpd 安装并启动httpd 在本地源的基…

Spring AOP(2)原理(代理模式和源码解析)

目录 一、代理模式 二、静态代理 三、动态代理 1、JDK动态代理 &#xff08;1&#xff09;JDK动态代理实现步骤 &#xff08;2&#xff09;定义JDK动态代理类 &#xff08;3&#xff09;代码简单讲解 2、CGLIB动态代理 &#xff08;1&#xff09;CGLIB 动态代理类实现…

RockyLinux 9 PXE Server bios+uefi 自动化部署 RockLinux 8 9

pxe server 前言 PXE&#xff08;Preboot eXecution Environment&#xff0c;预启动执行环境&#xff09;是一种网络启动协议&#xff0c;允许计算机通过网络启动而不是使用本地硬盘。PXE服务器是实现这一功能的服务器&#xff0c;它提供了启动镜像和引导加载程序&#xff0c;…