0

    一个简单易用的文件上传方案

    2023.04.24 | admin | 143次围观

    现在 OSS 服务算是一个基础服务了,很多云服务厂商都有提供这样的服务,价格也不贵,松哥自己的 用的就是类似的服务。

    不过对于中小公司来说,除了购买 OSS 服务之外,也可以自己搭建专业的文件服务器,自己搭建专门的文件服务器的话,曾经比较专业的做法是 FastDFS,松哥之前也专门为之录过视频发在 B 站上,感兴趣的小伙伴可以自行查看。不过 FastDFS 搭建比较麻烦,非常容易出错,所以对各位小伙伴来说多多少少有一点门槛。

    松哥在之前的文章录制的一些项目视频中,如果涉及到文件上传,基本上都是保存在项目本地,这种方式比较省事,但是安全性不高。

    所以,今天给大伙介绍一个较好的玩意 MinIO,看看这个工具带给我们什么惊喜。

    1. MinIO 简介

    MinIO 是一个基于 Apache License v2.0 开源协议的对象存储服务,它兼容亚马逊 S3 云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等,而一个对象文件可以是任意大小,从几 KB 到最大 5T 不等。

    MinIO 是一个非常轻量的服务,可以很简单的和其他应用的结合,类似 NodeJS, Redis 或者 MySQL。

    简单来说,可以使用 MinIO 来搭建一个对象存储服务,而且 MinIO 的 Java 客户端和亚马逊的 S3 云存储服务客户端接口兼容,换句话说,你会往 MinIO 上存数据,就会往 S3 上存数据。

    MinIO 的特点:

    兼容 Amazon S3:可以使用 MinIO SDK,MinIO Client,AWS SDK 和 AWS CLI 访问 MinIO 服务器。

    较强的数据保护能力:MinIO 使用 Minio Erasure Code 来防止硬件故障。

    高度可用:MinIO 服务器可以容忍分布式设置中高达(N/2)-1 节点故障。

    支持 Lambda 计算。

    具有加密和防篡改功能:MinIO 为加密数据提供了机密性,完整性和真实性保证,而且性能开销微乎其微。使用 AES-256-GCM,ChaCha20-Poly1305 和 AES-CBC 支持服务器端和客户端加密。

    可对接后端存储:除了 MinIO 自己的文件系统,还支持 DAS、 JBODs、NAS、Google 云存储和 Azure Blob 存储。

    2. MinIO 安装

    不废话了,赶紧装一个体验一把吧。

    为了省事,咱们就直接用 docker 来安装吧,如果你对 docker 还不熟悉,公众号后台回复 docker 获取松哥的 docker 教程。

    我们执行如下命令,安装 MinIO:

    docker run -p 9000:9000 -p 9001:9001 -d minio/minio server /data --console-address ":9000" --address ":9001"

    这个启动命令中配置了两个端口:console-address 是后台管理的网页端口;address 则是 API 通信端口。以上面的启动脚本为例,项目启动成功后,网页上的访问端口是 9000,如果我们通过 Java 代码上传文件,通信端口则是 9001。

    项目启动成功后,浏览器地址栏输入 :9000/login 即可访问到 MinIO 的后端页面:

    默认的登录用户名和密码均为 minioadmin。

    登录成功之后,我们首先创建一个 bucket,将来我们上传的文件都处于 bucket 之中,如下:

    创建成功之后,我们还需要设置一下桶的读取权限,确保文件将来上传成功之后可以读取到,点击左上角的设置按钮进行设置,如下:

    设置完成后,接下来我们就可以往这个桶中上传资源了,如下图:

    上传完成后,就可以看到刚刚上传的文件了:

    上传成功后,点击文件,然后点击右边的 Share 按钮会弹出来文件的访问链接,由于我们已经设置了文件可读,因此可以不用管这里的链接有效期了,直接通过路径的前面部分就可以访问到刚刚上传的图片了,如下:

    现在文件就可上传可访问了。是不是比 FastDFS 容易多了!

    不过前面这种安装方式其实有点小问题,因为我们没有为 docker 容器设置数据卷,所以如果你把 docker 容器不小心删除了,那么数据也就没了!

    所以我们要设置数据卷。

    修正后的 docker 脚本如下:

    docker run -p 9000:9000 -p 9001:9001 -d --name minio -v /Users/sang/minio/data:/data -v /Users/sang/minio/config:/root/.minio -e "MINIO_ROOT_USER=javaboy" -e "MINIO_ROOT_PASSWORD=123@45678" minio/minio server /data --console-address ":9000" --address ":9001"

    主要是加了数据卷映射功能,将 MinIO 的数据和配置文件映射到宿主机上,这样将来即使容器删除了,数据也都还在。

    注意上面也自定义了登录用户名和密码。

    按照上面的命令,重新创建容器之后,我们也创建一个桶并上传文件,上传成功之后,我们就可以在本地对应的文件夹看到我们上传的文件,如下:

    3. 整合 Spring Boot

    接下来我们再来看看在 Spring Boot 中如何玩 MinIO。

    首先我们创建一个 Spring Boot 项目,引入 Web 依赖,如下:

    项目创建成功之后,我们再来手动添加一下 MinIO 的依赖,如下:

    <dependency>
        <groupId>io.miniogroupId>
        <artifactId>minioartifactId>
        <version>8.2.1version>
    dependency>

    这里我尝试用了最新的版本,但是似乎有一些 BUG,我也没有深究,就换了 8.2.1 这个版本,这个版本是 OK 的。

    接下来我们来配置一下 application.yaml,配置一下文件上传所需要的基本信息:

    minio:
      endpoint: http://localhost:9001
      accessKey: javaboy
      secretKey: 123@45678
      nginxHost: http://local.javaboy.org:9001

    这里四个属性:

    endpoint:这是 MinIO 的 API 通信地址。

    accessKey 和 secretKey 是通信的用户名和密码,这跟网页上登录时候的用户名密码一致。

    nginxHost:这个配置用来生成上传文件的访问路径。对于这个路径,有的小伙伴可能会有疑问,nginxHost 不就是 endpoint 吗?为什么还要单独配置?因为对于文件服务器而言,我们上传文件是通过 MinIO,但是访问的时候不一定通过 MinIO,我们可能会自己搭建一个 Nginx 服务器,通过 Nginx 服务器来访问上传后的资源,大家知道 Nginx 非常擅长于做这个事情,效率非常高。所以这里的 nginxHost 其实是指 Nginx 的访问路径。

    接下来我们提供一个 MinioProperties 来接收这里的四个属性,如下:

    @ConfigurationProperties(prefix = "minio")
    public class MinioProperties {
        /**
         * 连接地址
         */

        private String endpoint;
        /**
         * 用户名
         */

        private String accessKey;
        /**
         * 密码
         */

        private String secretKey;
        /**
         * 域名
         */

        private String nginxHost;

        public String getEndpoint() {
            return endpoint;
        }

        public void setEndpoint(String endpoint) {
            this.endpoint = endpoint;
        }

        public String getAccessKey() {
            return accessKey;
        }

        public void setAccessKey(String accessKey) {
            this.accessKey = accessKey;
        }

        public String getSecretKey() {
            return secretKey;
        }

        public void setSecretKey(String secretKey) {
            this.secretKey = secretKey;
        }

        public String getNginxHost() {
            return nginxHost;
        }

        public void setNginxHost(String nginxHost) {
            this.nginxHost = nginxHost;
        }
    }

    将 application.yaml 中相关的配置注入到这个配置类中来。

    接下来我们需要提供一个 MinIOClient,通过这个客户端工具可以操作 MinIO,如下:

    @Configuration
    @EnableConfigurationProperties(MinioProperties.class)
    public class MinioConfig 
    {

        @Autowired
        private MinioProperties minioProperties;

        /**
         * 获取MinioClient
         */

        @Bean
        public MinioClient minioClient() {
            return MinioClient.builder()
                    .endpoint(minioProperties.getEndpoint())
                    .credentials(minioProperties.getAccessKey(), minioProperties.getSecretKey())
                    .build();
        }

    }

    这个也没啥好说的,传入通信地址以及用户名密码,就可以构建出一个 MinioClient 出来。

    当文件上传成功之后,我们可以通过 MinIO 去访问,也可以通过 Nginx 访问,所以接下来我们就需要提供一个类,来封装这两个地址:

    public class UploadResponse {
        private String minIoUrl;

        private String nginxUrl;

        public UploadResponse() {
        }

        public UploadResponse(String minIoUrl, String nginxUrl) {
            this.minIoUrl = minIoUrl;
            this.nginxUrl = nginxUrl;
        }

        public String getMinIoUrl() {
            return minIoUrl;
        }

        public void setMinIoUrl(String minIoUrl) {
            this.minIoUrl = minIoUrl;
        }

        public String getNginxUrl() {
            return nginxUrl;
        }

        public void setNginxUrl(String nginxUrl) {
            this.nginxUrl = nginxUrl;
        }
    }

    再来提供一个 MinIO 文件上传工具类:

    @Component
    public class MinioUtil {

        @Autowired
        private MinioProperties minioProperties;

        @Autowired
        private MinioClient client;

        /**
         * 创建bucket
         */

        public void createBucket(String bucketName) throws Exception {
            if (!client.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) {
                client.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
            }
        }

        /**
         * 上传文件
         */

        public UploadResponse uploadFile(MultipartFile file, String bucketName) throws Exception {
            //判断文件是否为空
            if (null == file || 0 == file.getSize()) {
                return null;
            }
            //判断存储桶是否存在  不存在则创建
            createBucket(bucketName);
            //文件名
            String originalFilename = file.getOriginalFilename();
            //新的文件名 = 存储桶文件名_时间戳.后缀名
            assert originalFilename != null;
            SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
            String fileName = bucketName + "_" +
                    System.currentTimeMillis() + "_" + format.format(new Date()) + "_" + new Random().nextInt(1000) +
                    originalFilename.substring(originalFilename.lastIndexOf("."));
            //开始上传
            client.putObject(
                    PutObjectArgs.builder().bucket(bucketName).object(fileName).stream(
                            file.getInputStream(), file.getSize(), -1)
                            .contentType(file.getContentType())
                            .build());
            String url = minioProperties.getEndpoint() + "/" + bucketName + "/" + fileName;
            String urlHost = minioProperties.getNginxHost() + "/" + bucketName + "/" + fileName;
            return new UploadResponse(url, urlHost);
        }

        /**
         * 获取全部bucket
         *
         * @return
         */

        public List getAllBuckets() throws Exception {
            return client.listBuckets();
        }

        /**
         * 根据bucketName获取信息
         *
         * @param bucketName bucket名称
         */

        public Optional getBucket(String bucketName) throws IOException, InvalidKeyException, NoSuchAlgorithmException, InsufficientDataException, InvalidResponseException, InternalException, ErrorResponseException, ServerException, XmlParserException, ServerException {
            return client.listBuckets().stream().filter(b -> b.name().equals(bucketName)).findFirst();
        }

        /**
         * 根据bucketName删除信息
         *
         * @param bucketName bucket名称
         */

        public void removeBucket(String bucketName) throws Exception {
            client.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
        }

        /**
         * 获取⽂件外链
         *
         * @param bucketName bucket名称
         * @param objectName ⽂件名称
         * @param expires    过期时间 <=7
         * @return url
         */

        public String getObjectURL(String bucketName, String objectName, Integer expires) throws Exception {
            return client.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder().bucket(bucketName).object(objectName).expiry(expires).build());
        }

        /**
         * 获取⽂件
         *
         * @param bucketName bucket名称
         * @param objectName ⽂件名称
         * @return ⼆进制流
         */

        public InputStream getObject(String bucketName, String objectName) throws Exception {
            return client.getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).build());
        }

        /**
         * 上传⽂件
         *
         * @param bucketName bucket名称
         * @param objectName ⽂件名称
         * @param stream     ⽂件流
         * @throws Exception https://docs.minio.io/cn/java-client-api-reference.html#putObject
         */

        public void putObject(String bucketName, String objectName, InputStream stream) throws
                Exception 
    {
            client.putObject(PutObjectArgs.builder().bucket(bucketName).object(objectName).stream(stream, stream.available(), -1).contentType(objectName.substring(objectName.lastIndexOf("."))).build());
        }

        /**
         * 上传⽂件
         *
         * @param bucketName  bucket名称
         * @param objectName  ⽂件名称
         * @param stream      ⽂件流
         * @param size        ⼤⼩
         * @param contextType 类型
         * @throws Exception https://docs.minio.io/cn/java-client-api-reference.html#putObject
         */

        public void putObject(String bucketName, String objectName, InputStream stream, long
                size, String contextType)
     throws Exception 
    {
            client.putObject(PutObjectArgs.builder().bucket(bucketName).object(objectName).stream(stream, size, -1).contentType(contextType).build());
        }

        /**
         * 获取⽂件信息
         *
         * @param bucketName bucket名称
         * @param objectName ⽂件名称
         * @throws Exception https://docs.minio.io/cn/java-client-api-reference.html#statObject
         */

        public StatObjectResponse getObjectInfo(String bucketName, String objectName) throws Exception {
            return client.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build());
        }

        /**
         * 删除⽂件
         *
         * @param bucketName bucket名称
         * @param objectName ⽂件名称
         * @throws Exception https://docs.minio.io/cn/java-client-apireference.html#removeObject
         */

        public void removeObject(String bucketName, String objectName) throws Exception {
            client.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(objectName).build());
        }
    }

    都是一些常规的 API 调用本地文件上传到服务器,我就不逐行解释了,接下来我们来一个文件上传接口:

    @RestController
    public class FileUploadController {
        @Autowired
        MinioUtil minioUtil;

        @PostMapping("/upload")
        public String fileUpload(MultipartFile file) throws Exception {
            UploadResponse bucket01 = minioUtil.uploadFile(file, "bucket01");
            System.out.println("bucket01.getMinIoUrl() = " + bucket01.getMinIoUrl());
            System.out.println("bucket01.getNginxUrl() = " + bucket01.getNginxUrl());
            return bucket01.getMinIoUrl();
        }
    }

    好啦,大功告成。

    接下来启动 Spring Boot 项目,然后调用这个接口上传文件,上传成功后,控制台会打印如下信息:

    这就表示文件上传成功了。

    4. 配置 nginx

    前面提到了 MinIO 可以结合 Nginx 来使用,那我们这里就来配一配 Nginx 看看。

    为了省事,Nginx 我也选择安装到 docker 容器中,但是前面安装 MinIO 时,我们已经做了数据卷映射,即上传到 MinIO 的文件实际上是保存在宿主机的,所以现在也得给 Nginx 配置数据卷,将来让 Nginx 也去 /Users/sang/minio/data 路径下查找文件。

    Nginx 安装指令如下:

    docker run --name nginx01 -p 8888:80 -v /Users/sang/minio/data:/usr/share/nginx/html:ro -d nginx

    这里两个关键点:

    设置 Nginx 端口为 8888。

    将 MinIO 映射到宿主机的数据卷,再次挂载到 Nginx 上去。

    大家知道,默认情况下,当我们访问 Nginx 的时候,Nginx 给我们展示出来的数据其实就是 /usr/share/nginx/html 目录下的,现在该目录其实就相当于我宿主机的 /Users/sang/minio/data 目录,所以我现在都不用修改 Nginx 的配置了本地文件上传到服务器,装好之后直接使用 Nginx 即可。

    好啦,接下来我们修改一下 application.yaml,如下:

    minio:
      endpoint: http://localhost:9001
      accessKey: javaboy
      secretKey: 123@45678
      nginxHost: http://local.javaboy.org:8888

    改完之后,再次上传文件,此时打印出来的文件访问路径如下:

    现在我们通过这个 Nginx 路径也能访问到刚刚上传的文件了。

    5. 小结

    好啦,今天就和小伙伴们分享一下 MinIO 的用法,并结合 Nginx 搭建了一个简单的文件服务器,感兴趣的小伙伴可以试试哦。

    公众号江南一点雨后台回复 minio_demo,获取本文源码下载链接。

    版权声明

    本文仅代表作者观点。
    本文系作者授权发表,未经许可,不得转载。

    标签: docker
    发表评论