2

如果需要要处理大量文件,又没有文件系统,那么把文件存储在minio中是一个很不错的选择,下面记录上传、对文件进行加密(MD5,sha1)、下载的整体过程,避免存储相同内容文件。
例如:
两个文件test1.pdf 和 test2.pdf,两个名称不相同,但是内容相同,那么minio中只要存储一条就可以。只要内容相同,即使名称相同,也只要存储一条就可以了。

最终效果:

上传:

image.png

下载:

image.png

开始前

如果你有镜像,则可以略过,
Minio镜像:镜像

如果你还想安装客户端也可以参考: 客户端安装

我们先看看 attachment 和 myfile 关系 是多对一,
attachment:

  • name:文件名称
  • ext: 后缀
  • file-id 对应的myfile

myfile:

  • md5: 编码信息
  • mime: 文件类型
  • name: 文件编码后的名称
  • path: 路径
  • quote_number: 对应的attachment数量
  • sha1:编码信息

image.png

上传

前台核心代码:
支持拖动和点击进行上传,采用nz-zorro第三方库的组件。

// html
<nz-upload
  nzType="drag"
  [nzMultiple]="false"
  nzAccept="application/pdf"
  [nzCustomRequest] = uploadToMinio>
  <p class="ant-upload-drag-icon">
    <span nz-icon nzType="inbox"></span>
  </p>
  <p class="ant-upload-text">上传附件</p>
  <p class="ant-upload-hint">
    支持拖动和点击
  </p>
</nz-upload>

uploadToMinio处理文件上传的回调函数,上传文件、处理响应,

// Component
uploadToMinio = (item: NzUploadXHRArgs): Subscription => {
    return this.attachmentService.uploadToMinio(item.postFile as File).subscribe({
      next: (response) => {
        // 上传成功之后
        if (response.type === HttpEventType.Response) {
          item.onSuccess(response.status, item.file, null);
          this.commonService.success(() => {
            this.onClose();
          }, '上传成功');
        }
      },
    });
  };

//Service
uploadToMinio(file: File): Observable<HttpEvent<Attachment>> {
    const formData: FormData = new FormData();
    formData.append('file', file);
    return this.httpClient.post<Attachment>(`upload/uploadToMinio`, formData, {reportProgress: true, observe: 'events'});
  }

将文件对象 file 添加到 FormData 中,使用键 'file' 来标识这个文件字段。在后端接收时,可以根据这个键名获取文件内容。

后台:

请先看流程图在审阅代码,代码没有模块化分离,希望读者更容易理解
0e3b53c9b616d2a43b4ce4482595ff74.png
image.png

@RestController
@RequestMapping("/upload")
public class UploadController {
    static final Logger logger = LoggerFactory.getLogger(UploadController.class);

    private String bucketName = "miniobrower";

    private String attachmentPath = "attachment" ;
    
    private String endpoint = "http://127.0.0.1:9000";
    
    private String accessKey = "UjXVlLV37Twzg0Hf****";

    private String secretKey = "sc0o244O3HrhjcOpqd5koPe00ePQlkeNvo5*****";

    MinioClient minioClient;

    @PostMapping("uploadToMinio")
    @JsonView(AttachmentController.UploadJsonView.class)
    public Attachment uploadToMinio(@RequestParam("file") MultipartFile multipartFile) throws Exception {

        Boolean found = this.bucketExists(bucketName);
        if (!found) {
            logger.debug("创建桶:" + bucketName);
            try {
                this.getMinioClient().makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
            } catch (Exception e) {
                logger.error("创建桶出错:" + e);
                throw new RuntimeException(e);
            }
        }

        // 存储路径
        Path saveFilePath = Paths.get(attachmentPath +"/"
                + new SimpleDateFormat("yyyyMMdd").format(new Date())
                + "/" + multipartFile.getOriginalFilename());

        logger.debug("判断上传的文件是否为空");
        if (multipartFile.isEmpty()) {
            throw new RuntimeException("上传的附件不能为空" + multipartFile);
        }

        logger.debug("新建附件对象");
        Attachment attachment = new Attachment();

        logger.debug("获取文件名");
        String fileName = multipartFile.getOriginalFilename();

        logger.debug("从文件名中截取拓展名");
        // 从"."最后一次出现的位置的下一位开始截取,获取扩展名
        assert fileName != null;
        String ext = fileName.substring(fileName.lastIndexOf(".") + 1);

        logger.debug("对文件进行sha1,md5加密");
        String sha1ToMultipartFile = Utils.encrypt(multipartFile, "SHA-1");
        String md5ToMultipartFile = Utils.encrypt(multipartFile, "MD5");

        logger.debug("设置文件信息");
          attachment.setName(fileName);
        attachment.setExt(ext);

        // 根据加密信息 去数据库中查询是否有当前文件信息
        MyFile oldFile = this.myFileRepository.findTopBySha1AndMd5(sha1ToMultipartFile, md5ToMultipartFile);

        // 判断是否保存file
        if (oldFile == null) {
            logger.debug("设置保存文件名");
              String saveName = Utils.md5(md5ToMultipartFile + System.currentTimeMillis()) + "." + ext;

            /**
             *  对file 进行赋值 都是Set方法,就不展示了
             * @param file 要保存的文件信息,包含信息如下:
             * @param getContentType 文件类型
             * @param saveFilePath 保存的路径
             * @param sha1ToMultipartFile sha1的加密信息
             * @param md5ToMultipartFile md5的加密信息
             * @param saveName 文件的名称
              */
            MyFile file = new MyFile();
            setMyFile(file, multipartFile.getContentType(), saveFilePath, sha1ToMultipartFile, md5ToMultipartFile, saveName);

            logger.debug(multipartFile.getOriginalFilename() + "文件转换字节");
            byte[] byteData = multipartFile.getBytes();
            logger.debug(multipartFile.getOriginalFilename() + "文件存入到minio中");
            this.uploadByte(bucketName, saveFilePath.toString(), byteData, "application/pdf");

            // 保存文件信息
            oldFile = this.myFileRepository.save(file);
        } else {
            // 如果走到这里来了,说明有相同文件,就没必要存入到minio中,+1 用来记录
            oldFile.setQuoteNumber(oldFile.getQuoteNumber() + 1);
            // 保存文件信息
            oldFile = this.myFileRepository.save(oldFile);
        }

        attachment.setFile(oldFile);
        // 保存附件信息
        this.attachmentRepository.save(attachment);
        return attachment;
    }


    /**
     * 建立连接
     */
    public MinioClient getMinioClient() {
            this.minioClient = MinioClient.builder()
                            .endpoint(this.endpoint)
                            .credentials(this.accessKey, this.secretKey)
                            .build();
        return this.minioClient;
    }

    /**
     *  判断桶名是否存在
     * @param bucketName 桶名
     */
    public Boolean bucketExists(String bucketName) {
        try {
            logger.debug("当前桶是否存在" + bucketName);
            return this.getMinioClient().bucketExists(
                    BucketExistsArgs.builder().bucket(bucketName).build()
            );
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 上传到minio中
     * @param bucket 桶名
     * @param prefix 路径
     * @param data 文件的字节
     * @param contentType 文件的类型
     */
    public void uploadByte(String bucket, String prefix, byte[] data, String contentType) {
        try {
            this.getMinioClient()
                    .putObject(PutObjectArgs.builder()
                            .bucket(bucket)
                            .object(prefix)
                            .stream(new ByteArrayInputStream(data), data.length, -1)
                            .contentType(contentType)
                            .build());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

}

下载

前台

下载比较简单,
我们只需要知道文件的唯一标识id,发起请求响应后台,
类型是responseType: 'blob' 表示服务器返回的数据类型为 blob

 download(attachment: Attachment): void {
    this.httpClient.get(`upload/download/${attachment.id}`, {responseType: 'blob'})
      .subscribe(blob => {
        saveAs(blob, attachment.name);
      });
  }

比较注意的是:saveAs(blob, attachment.name),保存到本地,其中blob表示需要保存的文件。
saveAs是FileSaver.js库中的函数,如果你没有当前环境你可以使用当前命令进行安装。

npm install file-saver --save

后台:

后台响应后会根据id 去数据库中查询attachment,通过attachment对应的myfile实体,myfile实体中存有路径(path)字段。
这时我们知道了: 桶名称、文件路径,就可以调用Minion的api,GetObjectArgs进行下载了,返回该文件的流,最后逐步读取流中的内容,outputStream.write(buffer, 0, bytesRead) 返回给前台

@GetMapping("download/{id}")
    public void download(@PathVariable Long id, HttpServletResponse response) throws IOException {
        OutputStream outputStream = response.getOutputStream();
        // 根据id 去数据库中查询是否有 该attachment
        Attachment attachment = this.attachmentRepository.findById(id).orElseThrow(EntityNotFoundException::new);
        
        logger.debug("下载文件:" + attachment.getName());
        try {
            InputStream inputStream = this.getMinioClient().getObject(
                    GetObjectArgs.builder()
                            .bucket(bucketName) // 指定bucket名称
                            .object(attachment.getFile().getPath()) // 指定文件名称
                            .build());
            // 输出流
            byte[] buffer = new byte[1024];
            int bytesRead;
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
            }

            // 关闭流
            inputStream.close();
            outputStream.close();
        } catch (Exception e) {
            this.logger.error("在执行getObjectContent时发生异常:" + e.getMessage());
            throw new RuntimeException(e);
        }
    }

文献

如果你还需要更多功能,可以参考:
minio官网api: https://min.io/docs/minio/linux/developers/go/API.html

希望这篇文章对你有一定帮助。


zZ_jie
401 声望9 粉丝

虚心接受问题,砥砺前行。