有请Minio统一管理文件(实操JS、JAVA)

7,904 阅读13分钟

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

前言

  • 在线音乐戳我呀!
  • 音乐博客源码上线啦!
  • 最近在整理自己的在线音乐(因为最近换服务器了),发现上传的图片文件很杂乱,如:音乐上传到minio中(文件服务器),IT知识模块的图片上传在Node指定的文件夹中,其他模块的文件又是Node的另外一个文件夹。
  • 就想着能不能全部都由一个来管理这些文件,优先想到之前实习的时候公司(dpf)搭建个文件服务器,也用了一下确实是挺方便的,也就是今天的主角 -- Minio。
  • 其实自己的在线音乐已经用了很久了,只不过没有说对全部文件都归其管理,再加上现在应用部署在Docker上,不知道为什么Node一直映射失败,但Minio却映射得出来,这也是一开始初心想文件都放在minio中。
  • 接下来会分享如何搭建、可能会遇到的问题,一五一十盘出。
  • Are you ready ?

假期快乐。

先来张图效果。

fbd57b4cd563eedf704d7ae9570938d.png

界面直观、可视化界面、方便管理、上传下载流程简单,还能设置权限、失效时间等等。

Minio

  • 是什么

  • 怎么下载安装使用

  • 代码层面如何开发

一、Minio是什么?

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

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

同时也支持多种语言客户端集成开发,如JavaScript 、Java、Python、Golang、.NET

简而言之就是:文件服务器,存放管理我们的文件。

二、Minio怎么下载安装使用?

我们拿window、docker环境进行手把手教学。目前这两种环境较多人使用吧。(😁其实我也是只用这两种环境,其他环境不敢有话语权)

2.1 Windows版

2.1.1 下载

下载好了之后,文件后缀是exe。

2.1.2 使用

进入minio.exe所在目录。

cmd走起。

minio.exe server G:\tsblog\minio

参数说明G:\tsblog\minio这是文件上传之后的存储目录(我们图片上传到哪)

运行成功。会看到下面的界面。

20190330122426574.png

这里minio会给出ACCESS-KEY 和 SECRET-KEY,供后台管理登录使用。

2.1.3 登录使用

打开浏览器,使用命令行界面给出的地址都可以登录。

默认端口9000:http://localhost:9000/

480a8c6edeff17ad3b8e425cd7f4040.png

输入ACCESS-KEY 和 SECRET-KEY即可登录minio的管理界面了。

2.1.4 创建桶和上传图片

在后台管理界面你可以上创建你的Bucket(桶),可以理解为一个文件夹用来存放图片。桶创建成功之后就可以上传图片了。如下图:

左边是桶(文件夹),右边是桶中的文件。

baef6d635f119a5e9dd37ad88e06e17.png

在文件列表的右边可以复制出图片的访问地址,在浏览器中就可以访问图片了。这个时候的图片地址是带过期时间和密钥的。

888924df317095b7e1d3317936950f5.png

2.2 Docker版

我们会将minio部署到线上,众所周知,大部分服务器都是Linux系统,像我自己还装着Docker管理我的应用程序。

2.2.1 下载MinIO 镜像包

docker pull minio/minio

20200818103209519.png

2.2.2 启动MinIO 镜像

使用docker run -p 9000:9000 minio/minio server /data 端口可以自定义修改,默认会生成ACCESS_KEY与SECRET_KEY,看下图提示生成的是minioadmin/minioadmin

20200818103354496.png

当然,我们正常不会这样子启动,肯定是要自定义账号密码。还要把minio文件给映射出来(这才是重要)

使用以下脚本启动MinIO镜像,生成永久MinIO的容器。

一般复制这串代码即可适用大部分情况。

docker run -p 9000:9000 --name minio -d
-e "MINIO_ACCESS_KEY=admin" 
-e "MINIO_SECRET_KEY=123456" 
-v /home/minio:/data 
-v /home/minio/config:/root/.minio 
minio/minio  server /data

参数说明

  • run:启动镜像

  • -p 9000:9000:设置端口,并映射给外面的端口

  • -d:后台启动

  • -e "MINIO_ACCESS_KEY=admin" -e "MINIO_SECRET_KEY=123456" :设置账号密码(MINIO_ACCESS_KEY:密码Key;MINIO_SECRET_KEY:密码值)

  • -v /home/minio:/data -v /home/minio/config:/root/.minio :虚拟配置目录,文件映射出来,我的minio文件夹资源放在/home/minio/下,若要改的话,要改下这里的路径

  • minio/minio  server:这是我们一开始拉取下来的minio镜像名

有小伙子不知道文件映射出来是什么意思,请允许我解析一波。
我们把minio部署在docker中运行。
比如我们在minio中上传一个文件,那我们在linux服务器上怎么查看这个文件在哪?
这个时候就要文件映射,通俗的说就是将minio中上传的文件给映射到linux上,这样子我们在linux上就可以实时同步看到我们上传的文件。

2.2.3 查看MinIO 镜像

查看minio有没有启动成功:docker ps

20200818105135478.png

登录客户端也可以登录成功!

三、Minio实战(代码层面如何开发)

我们知道,MinIO是一个非常轻量的服务,可以支持多种语言客户端集成开发,如JavaScript 、Java、Python、Golang、.NET

我们拿JavaScript 、Java进行手把手教学。目前这两种环境较多人使用吧。(😁其实我也是只用这两种环境,其他语言咱也不敢有话语权)

3.1 JavaScript引入minio并使用

在线音乐中的【IT知识】模块文件本是由node,以原生上传文件保存到服务器本地中,但后面我想把全部文件统一管理,在前几天换做上传到minio中。

本例子用koa2作为JavaScript的后台。

3.1.1 koa2安装minio

npm i minio --save-dev

3.1.2 新建minio.js

配置minio账号密码端口。

var Minio = require("minio");
let endPoint = "127.0.0.1";

var minioClient = new Minio.Client({
  endPoint,
  port: 9000,
  useSSL: false,
  accessKey: "admin",
  secretKey: "123456",
});

module.exports = minioClient;

于是就可以在js接口中开始使用minio了,当然在写之前是需要引入上面定义的minio.js文件。

下面简单列出几个常用的方法操作。

3.1.3 列出全部存储桶(文件夹)

minioClient.listBuckets().then((res) => {
  console.log(res);
})

3.1.4 列出一个存储桶下的所有文件

var stream = minioClient.listObjectsV2("music", "", true);
let arr = [];

stream.on("data", (obj) => {
    arr.push(obj);  // 每一个文件都会去执行该data方法,类似于遍历
});

stream.on("end", function () {
        console.log(arr);  
        // 全部遍历完成后,在这里会打印出该music文件夹下的所有文件
});

stream.on("error", function (err) {
    console.log(err);
});

3.1.5 判断是否有这个存储桶

minioClient.bucketExists("music", function (err, exists) {
    if (err) {
      console.log(err);
    
    }
    if (exists) {
      console.log('存在')
    } else {
      console.log('不存在')
    }
});

3.1.6 创建一个新的存储桶

minioClient.makeBucket("music", "us-east-1", function (err) {
    if (err) {
      console.log("Error creating bucket.", err);
    }

    console.log('创建成功')
});

3.1.7 下载对象并将其保存为本地文件系统中的文件

minioClient.fGetObject(
    "music", 
    "周杰伦 - Mojito202006152354441192.flac", 
    "H:/85/周杰伦 - Mojito202006152354441192.flac", 
    function (err) {
        if (err) {
          console.log(err);
        }

        console.log('下载成功');
    }
);

3.1.8 下载的临时url

minioClient.presignedUrl("GET", "music", "周.flac", 24 * 60 * 60, function (
    err,
    presignedUrl
  ) {
    if (err) {
      console.log(err);
    }

    console.log(presignedUrl);
  });

3.1.9 往存储桶上传一个文件

上传文件的时候要注意文件名字可能会重复,所以这里加了随机数作为文件名字;

当然,规则都是人定的,可以根据自己的场景制订不同写法;

可以看到minioClient.putObject()函数中打印的那些字段,一般都会存在数据库中。

const bucketsName = "music", file = File对象;
const path = file[0][1].path;
const fileName = file[0][1].name;
const type = file[0][1].type;
const nowDate = moment(new Date()).format("YYYYMMDDHHmmss");
const random = Math.round((Math.random() * 9 + 1) * 1000);
const fileNameRandom = nowDate + random;

const fileSuffix = fileName.substring(fileName.lastIndexOf(".")); // 文件后缀名字
const filePrefix = fileName.substring(0, fileName.lastIndexOf(".")); // 文件名字前面那段

let objectName = filePrefix.length > 20
  ? filePrefix.substring(0, 20) + fileNameRandom + fileSuffix
  : filePrefix + fileNameRandom + fileSuffix;



  var fileStream = fs.createReadStream(path);
  fs.stat(path, function (err, stats) {
    if (err) {
      console.log(err);
    }


    minioClient.putObject(
      bucketsName,
      objectName,
      fileStream,
      stats.size,
      { "Content-Type": type },
      function (err, etag) {
        if (err) {
          console.log(err);
        }

        console.log({
          fileName,
          bucketsName,
          fileContentType: type,
          size: stats.size,
          fileNameRandom,
          url: objectName,
        });
      }
    );
  });
});

3.1.10 获取文件信息

minioClient.statObject("test", '9.png', function (err, stat) {
    if (err) {
      console.log(err);
    }

    console.log(stat);
});

3.1.11 移除一个对象

minioClient.removeObject("test", '9.png', function (e) {
    if (e) {
      console.log("Unable to remove Objects ", e);
    }

    console.log('移除成功');
});

3.1.12 删除objectsList 中的所有对象

minioClient.removeObjects("test", ['9.png'], function (e) {
    if (e) {
      console.log("Unable to remove Objects ", e);
    }

    console.log('移除成功');
});

我在github上写了个minio工具类,懒人可以借鉴一下(可以的话给个⭐Star):github.com/git-Dignity…

minio官方文档中也有明确的操作例子:docs.min.io/docs/javasc…

3.2 Java引入minio并使用

在线音乐中的【音乐】模块文件一直都是由minio管理的。

其实java和JavaScript的写法基本一样。

大同小异。

3.2.1 java安装minio

pom.xml文件引入依赖

<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>3.0.10</version>
</dependency>

3.2.2 在application.yml

我喜欢把配置参数(账号密码)写在application.yml文件中,规范管理。

#文件服务器

minio:
  endpoint: http://39.108.185.253  # 外网
  accessKey: admin
  secretKey: 123456
  endpointIn: http://127.0.0.1:9000  # 内网

3.2.3 创建实体类Minio.java

package com.example.bkapi.common.entity;

import org.springframework.boot.context.properties.ConfigurationProperties;


@ConfigurationProperties(prefix = "minio")
public class Minio {

    private String endpoint;
    private String accessKey;
    private String secretKey;
    private String endpointIn;

    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 getEndpointIn() {
        return endpointIn;
    }

    public void setEndpointIn(String endpointIn) {
        this.endpointIn = endpointIn;
    }
}

于是就可以在接口中开始使用minio了,当然在写之前是需要引入上面定义的Minio实体类。

private static Minio minio;

下面简单列出几个常用的方法操作。

3.2.4 创建minioClient

这里的写法其实和上面的3.1.2 新建minio.js 是一样的,为了创建Minio实例。

下面的minio.getEndpoint()获取到application.yml定义的http://39.108.185.253

下面的minio.getAccessKey()获取到application.yml定义的admin

下面的minio.getSecretKey()获取到application.yml定义的123456

大家的参数可以写死在这里,看个人习惯。

public static MinioClient createMinioClient() throws InvalidPortException, InvalidEndpointException {
    MinioClient minioClient = new MinioClient(minio.getEndpoint(), minio.getAccessKey(), minio.getSecretKey());
    return minioClient;
}

3.2.5 创建内网minioClient

下面的minio.getEndpoint()获取到application.yml定义的http://127.0.0.1:9000

其他参数和上方createMinioClient方法一样。

public static MinioClient createInMinioClient() throws InvalidPortException, InvalidEndpointException {
    MinioClient minioClient = new MinioClient(minio.getEndpointIn(), minio.getAccessKey(), minio.getSecretKey());
    return minioClient;
}

3.2.6 创建具有给定区域的新存储桶

先判断有没有这个桶,没有在进行创建新桶。

public static void makeBucket(String bucketName){
    try {
        MinioClient minioClient = createInMinioClient();

        // Create bucket if it doesn't exist.
        boolean found = minioClient.bucketExists(bucketName);
        if (found) {
            System.out.println(bucketName + " already exists");
        } else {
            // Create bucket 'my-bucketname'.
            minioClient.makeBucket(bucketName);
            System.out.println(bucketName + " is created successfully");
        }
    } catch (MinioException e) {
        System.out.println("Error occurred: " + e);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

3.2.7 列出所有桶

public static List<Bucket> listBuckets(){
    List<Bucket> bucketList = null;
    try {
        MinioClient minioClient = createInMinioClient();

        // List buckets that have read access.
        bucketList = minioClient.listBuckets();
        for (Bucket bucket : bucketList) {
            System.out.println(bucket.creationDate() + ", " + bucket.name());
        }
    } catch (MinioException e) {
        System.out.println("Error occurred: " + e);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return bucketList;
}

3.2.8 检查是否存在存储桶

public static boolean bucketExists(String bucketName){

    boolean found = false;
    try {
        MinioClient minioClient = createInMinioClient();

        // Check whether 'my-bucketname' exists or not.
        found = minioClient.bucketExists(bucketName);
        if (found) {
            System.out.println(bucketName + " exists");
        } else {
            System.out.println(bucketName + " does not exist");
        }
    } catch (MinioException e) {
        System.out.println("Error occurred: " + e);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return found;
}

3.2.9 删除一个桶

先判断桶存不存在,存在则删除该桶。

注意: - removeBucket不会删除存储桶内的对象。需要使用removeObject API删除对象。

public static void removeBucket (String bucketName){
    try {
        MinioClient minioClient = createInMinioClient();
        // Check if my-bucket exists before removing it.
        boolean found = minioClient.bucketExists(bucketName);
        if (found) {
            // Remove bucket my-bucketname. This operation will succeed only if the bucket is empty.
            minioClient.removeBucket(bucketName);
            System.out.println(bucketName + " is removed successfully");
        } else {
            System.out.println(bucketName + " does not exist");
        }
    } catch(MinioException e) {
        System.out.println("Error occurred: " + e);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

3.2.10 列出给定存储桶中的对象信息

先判断桶存不存在,存在则列出给定存储桶中的对象信息。

public static Iterable<Result<Item>> listObjects(String bucketName){
    Iterable<Result<Item>> myObjects = null;
    try {
        MinioClient minioClient = createInMinioClient();

        // Check whether 'mybucket' exists or not.
        boolean found = minioClient.bucketExists(bucketName);
        if (found) {
            // List objects from 'my-bucketname'
            myObjects = minioClient.listObjects(bucketName);
            for (Result<Item> result : myObjects) {
                Item item = result.get();
                System.out.println(item.lastModified() + ", " + item.size() + ", " + item.objectName());
            }
        } else {
            System.out.println(bucketName + " does not exist");
        }
    } catch (MinioException e) {
        System.out.println("Error occurred: " + e);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return myObjects;
}

3.2.11 列出给定存储桶和前缀中的对象信息

先判断桶存不存在,存在则列出给定存储桶和前缀中的对象信息。

public static Iterable<Result<Item>> listObjects(String bucketName,String prefix){
    Iterable<Result<Item>> myObjects = null;
    try {
        MinioClient minioClient = createInMinioClient();
        // Check whether 'mybucket' exists or not.
        boolean found = minioClient.bucketExists(bucketName);
        if (found) {
            // List objects from 'my-bucketname'
            myObjects = minioClient.listObjects(bucketName,prefix);
            for (Result<Item> result : myObjects) {
                Item item = result.get();
                System.out.println(item.lastModified() + ", " + item.size() + ", " + item.objectName());
            }
        } else {
            System.out.println(bucketName + " does not exist");
        }
    } catch (MinioException e) {
        System.out.println("Error occurred: " + e);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return myObjects;
}

3.2.12 将对象信息列为Iterable 在给定的桶,前缀和递归标志

public static Iterable<Result<Item>> listObjects(String bucketName, String prefix, boolean recursive){
    Iterable<Result<Item>> myObjects = null;
    try {
        MinioClient minioClient = createInMinioClient();
        // Check whether 'mybucket' exists or not.
        boolean found = minioClient.bucketExists(bucketName);
        if (found) {
            // List objects from 'my-bucketname'
            myObjects = minioClient.listObjects(bucketName,prefix,recursive);
            for (Result<Item> result : myObjects) {
                Item item = result.get();
                System.out.println(item.lastModified() + ", " + item.size() + ", " + item.objectName());
            }
        } else {
            System.out.println(bucketName + " does not exist");
        }
    } catch (MinioException e) {
        System.out.println("Error occurred: " + e);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return myObjects;
}

3.2.13 将对象下载为流

public static InputStream getObject(String bucketName, String objectName){
        InputStream stream = null;
        try {
            MinioClient minioClient = createInMinioClient();
            // Check whether the object exists using statObject().
            // If the object is not found, statObject() throws an exception,
            // else it means that the object exists.
            // Execution is successful.
            minioClient.statObject(bucketName, objectName);

            // Get input stream to have content of 'my-objectname' from 'my-bucketname'
            stream = minioClient.getObject(bucketName, objectName);
        } catch (MinioException e) {
            System.out.println("Error occurred: " + e);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return stream;
    }

3.2.14 将对象下载并保存为本地文件系统中的文件

public static void getObject(String bucketName, String objectName, String fileName){
    try {
        MinioClient minioClient = createInMinioClient();
        // Check whether the object exists using statObject().
        // If the object is not found, statObject() throws an exception,
        // else it means that the object exists.
        // Execution is successful.
        minioClient.statObject(bucketName, objectName);

        // Gets the object's data and stores it in photo.jpg
        minioClient.getObject(bucketName, objectName, fileName);

    } catch (MinioException e) {
        System.out.println("Error occurred: " + e);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

3.2.15 通过InputStream上传对象

public static ObjectStat putObject(String bucketName, String objectName, InputStream stream, long size, String contentType){
    try {
        MinioClient minioClient = createInMinioClient();
        // 创建对象
        minioClient.putObject(bucketName, objectName, stream, size, contentType);

        System.out.println(objectName + " is uploaded successfully");
    } catch(MinioException e) {
        System.out.println("Error occurred: " + e);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return statObject(bucketName,objectName);
}

3.2.16 通过文件上传到对象中

public static int putObject(String bucketName, String objectName, String fileName){
    try {
        MinioClient minioClient = createInMinioClient();

        minioClient.putObject(bucketName,  objectName, fileName);
        System.out.println(objectName + " is uploaded successfully");
    } catch(MinioException e) {
        System.out.println("Error occurred: " + e);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return 1;
}

3.2.17 从objectName指定的对象中将数据拷贝到destObjectName指定的对象

public static void copyObject(String bucketName, String objectName, String destBucketName, String destObjectName, CopyConditions cpConds, Map<String, String> metadata){
    try {
        MinioClient minioClient = createInMinioClient();

        CopyConditions copyConditions = new CopyConditions();
        copyConditions.setMatchETagNone("TestETag");

        minioClient.copyObject(bucketName,  objectName, destBucketName, destObjectName, copyConditions);//copyConditions 或 cpConds
        System.out.println(objectName + " is uploaded successfully");
    } catch(MinioException e) {
        System.out.println("Error occurred: " + e);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

3.2.18 删除一个对象

public static void removeObject(String bucketName, String objectName){
    try {
        MinioClient minioClient = createInMinioClient();

        // 从bucketName中删除objectName。
        minioClient.removeObject(bucketName, objectName);
        System.out.println("successfully removed " + bucketName + "/" +objectName);
    } catch (MinioException e) {
        System.out.println("Error: " + e);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

3.2.19 删除多个对象

public static Iterable<Result<DeleteError>> removeObject(String bucketName, Iterable<String> objectNames){
    Iterable<Result<DeleteError>> results = null;
    try {
        MinioClient minioClient = createInMinioClient();
        // 删除my-bucketname里的多个对象
        results = minioClient.removeObject(bucketName, objectNames);

        for (Result<DeleteError> errorResult: results) {
            DeleteError error = errorResult.get();
            System.out.println("Failed to remove '" + error.objectName() + "'. Error:" + error.message());
        }
    } catch (MinioException e) {
        System.out.println("Error: " + e);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return results;
}

3.2.20 获取文件永久地址

public static String getObjectUrl(String bucketName, String objectName){
    String url = null;
    try {
        MinioClient minioClient = createInMinioClient();

        url = minioClient.getObjectUrl(bucketName, objectName);
        System.out.println(url);
    } catch(MinioException e) {
        System.out.println("Error occurred: " + e);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return url;
}

我在github上写了个minio工具类,懒人可以借鉴一下(可以的话给个⭐Star):github.com/git-Dignity…

minio官方文档中也有明确的操作例子:docs.min.io/docs/java-c…

最后

上个星期,大学学霸过来坐坐,闲聊之中,看到我在做在线音乐,说起了我打算项目中的文件都要由minio管理,于是我就问起他文件都是怎么管理的,也是上传后端指定的文件夹。

于是我就打开minio给他看,一开始看了也觉得只是可视化界面,我说还有文件过期、权限等功能,oh ~ 还有这些功能,那不错,发下给我。

bd244e3d6e8959d37aa3870abac80a3.png

如果对您有帮助,你的点赞是我前进的润滑剂。

相关文献

Minio Windows安装和使用

MinIO Docker部署初探

以往推荐

尤大大说我的代码全部不加分号

老湿说的万物皆对象,你也信?

Vue-Cli3搭建组件库

Vue实现动态路由(和面试官吹项目亮点)

项目中你不知道的Axios骚操作(手写核心原理、兼容性)

VuePress搭建项目组件文档

koa2+vue+nginx部署

vue-typescript-admin-template后台管理系统

原文链接

juejin.cn/post/701435…