本地使用minio之前后端关键点

step1 下载minio并启动

minio仓库地址有各个不同的部署方案,我这里是windows,就使用windows的部署方案,启动脚本如下:

minio.exe server .\data --console-address ":52400"

将这行写入txt文本文件,后缀改为bat,双击启动,在终端地址有webui的地址和登录用户名密码

step2 创建存储桶

打开浏览器输入http://127.0.0.1:52400/ ,输入账号密码进入页面

进入创建页面后简单输入一个名字就好,至于其他的开关功能项可以根据字意和查看minio的文档

之后我们把minio存储桶的私有属性改为公有,这样会方便我们的使用,如果是不打算使用大厂的oss对象存储的线上项目的话根据情况设置

然后我们在创建access key,用以调用api上传

step3 我们设计一下minio文件对应的vo或者entity

我这里后端使用node nestjs框架,数据库使用mysql typeorm框架

ts 复制代码
import { CreateDateColumn, DeleteDateColumn, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';

export abstract class DataBaseEntity {
  @PrimaryGeneratedColumn()
    id?: number;

  @CreateDateColumn()
    createDate?: Date;

  @UpdateDateColumn()
    updateDate?: Date;

  @DeleteDateColumn()
    deletedDate?: Date;

  isSelected?: boolean;
}
ts 复制代码
import { Column, Entity, ManyToOne, OneToMany, JoinColumn } from 'typeorm';
import { DataBaseEntity } from './BaseEntity/DataBaseEntity';
import { DatabaseEnum } from '@/enums/DatabaseEnum';
import { FileType } from '@/enums/FileType';
import { SameImageGroup } from '@/entitys/SameImageGroup';
import { FileEntityWith } from '@/entitys/FileEntityWith';

@Entity({ name: 'file', database: DatabaseEnum.total })
export class FileEntity extends DataBaseEntity {
  @Column({ unique: true })
    hash?: string;

  @Column({ nullable: true })
    imageHashFor16bits?: string;

  @Column()
    fileType?: FileType;

  @Column({ default: 0 })
    width?: number;

  @Column({ default: 0 })
    height?: number;

  @Column({ nullable: true, type: 'decimal', scale: 3, precision: 10 })
    size?: number; // 单位MB

  @Column({ nullable: true, type: 'decimal', scale: 3, precision: 10 })
    duration?:number;

  @Column({ default: true })
    isTemp?: boolean;

  @OneToMany(() => FileEntityWith, fileEntityWiths => fileEntityWiths.fileEntity)
    fileEntityWiths?: FileEntityWith[];

  @ManyToOne(() => SameImageGroup, sameImageGroup => sameImageGroup.fileEntities)
  @JoinColumn({ name: 'sameImageGroupId', referencedColumnName: 'id' })
    sameImageGroup?: SameImageGroup;
}

基础的DataBaseEntity不用说,是每张表都需要的基本字段

hash是文件的唯一标识,防止文件重复,毕竟本地的话空间还是紧张的,hash计算方式参考《本地使用minio之获取文件hash》

imageHashFor16bits是我用来对图片做去重的,有些图片稍微压缩一下hash计算就会不一致,但是基本上是同一张图,计算方式参开《本地使用minio之计算图片相似性》

fileType是指文件的类型,比如图片、视频、音频等,这里看个人具体需求,毕竟jpg、png都可归属于图片,文件类型获取方式参考《本地使用minio之获取文件类型(MimeType)》

width、height、size、duration计算方式参考《本地使用minio之获取图片和视频信息》

isTemp是我最初用来筛选存储的文件是否有价值,后来文件多了之后基本不管这个标记了,所有记录文件最初这个标记都是true,我在前端页面做了个审查功能,审查完就是false

fileEntityWiths是我用来做外联的,比如这个文件有一个tag,这里可以不用在意

sameImageGroup,是代表哪些图片相似,在图片存储时计算imageHashFor16bits,相同的imageHashFor16bits划分为一组,这里可以忽略

step4 文件上传到minio

我个人项目里前端浏览器和后端node都需要具备单独上传文件到minio中,所以我这里会两个端都说明一下

后端上传比较简单,对minio的sdk简单封装一下就可以

ts 复制代码
@Injectable()
export class MinIOService {
  minioClient = new Minio.Client({
    endPoint: appConfig.minio.endPoint,
    port: appConfig.minio.port,
    useSSL: appConfig.minio.useSSL,
    accessKey: appConfig.minio.accessKey,
    secretKey: appConfig.minio.secretKey,
  });

  bucketName = 'nest-monorepo';

  async statObject(fileHash: string) {
    return this.minioClient.statObject(
      this.bucketName,
      fileHash,
    )
      .catch(() => {
        return null;
      });
  }
  
  async presignedPostPolicy(vo: MinIoPresignedPostPolicyVo) {
    const stat = await this.statObject(vo.key);

    if (!isEmpty(stat) && vo.key !== 'test') {
      return null;
    }

    const policy = this.minioClient.newPostPolicy();
    policy.setBucket(this.bucketName);
    policy.setKey(vo.key);

    if (vo.contentType) {
      policy.setContentType(vo.contentType);
    }

    if (!isEmpty(vo.metaData)) {
      policy.setUserMetaData(vo.metaData);
    }

    return this.minioClient.presignedPostPolicy(policy);
  }

  async putObject(fileHash: string, stream: stream.Readable | Buffer | string, size?: number, metaData?: ItemBucketMetadata) {
    const [, stat] = await TsUtils.tuple(this.statObject(fileHash));
    if (!isEmpty(stat)) {
      return null;
    }

    await this.minioClient.putObject(
      this.bucketName,
      fileHash,
      stream,
      size,
      metaData,
    );
  }

}

await this.putObject(fileHash, buffer, null, { 'content-type': mimetype.mime });

前端上传的话我这里使用后端node 签发的预签名,直接通过浏览器上传到minio

预签名node端

ts 复制代码
@Post('/getPresignedPostPolicy')
async getPresignedPostPolicy(@Body() vo: MinIoPresignedPostPolicyVo, @Req() req: Request) {
  if (isEmpty(vo.key)) {
    throw new HttpException('key is empty', HttpStatus.INTERNAL_SERVER_ERROR);
  }
  const postPolicyResult = await this.minIOService.presignedPostPolicy(vo);

  if (isEmpty(postPolicyResult)) {
    throw new HttpException('file is in minio', HttpStatus.INTERNAL_SERVER_ERROR);
  }

  const url = new URL(postPolicyResult.postURL);
  url.hostname = req.hostname;
  postPolicyResult.postURL = url.toString();
  return CommonAPIResultVo.build(StatusCode.OK, null, postPolicyResult);
}

浏览器端使用预签名上传

javascript 复制代码
async uploadFilePost(file: File) {
  const fileVo = await FileEntity.getFileVoByFile(file);

  const { data: hasFileForMinio } = await FilesAPI.hasFileInMinio(fileVo.hash);

  if (!hasFileForMinio) {
    const { data } = await axios.post<CommonAPIResultVo<PostPolicyResult>>('/api/fileEntity/getPresignedPostPolicy', {
      key: fileVo.hash,
      contentType: (await this.getFileTypeResult(file))?.mime || file.type,
      metaData: {
        fileName: file.name,
      },
    } as MinIoPresignedPostPolicyVo);
    const postPolicy = data.data;
    const formData = new FormData();

    Object.entries(postPolicy.formData).forEach(([k, v]) => {
      formData.append(k, v);
    });
    formData.append('file', file);
    const url = new URL(postPolicy.postURL);
    url.hostname = window.location.hostname;
    url.port = String(9000);
    const response = await axios.post(url.toString(), formData);
    if (response.status !== 204) {
      throw new Error('直传失败');
    }
  }

  const { data: fileEntity } = await FilesAPI.getFileEntityByHash(fileVo.hash);

  if (!isEmpty(fileEntity)) {
    return fileEntity;
  }

  const { data } = await FilesAPI.addFileEntity(fileVo);
  return data;
}

其中预签名直传关键点在于if (!hasFileForMinio)中,在if前面是通过前端获取一下文件的一些信息,比如hash,if后面则是查看一下文件是否被删除过或者是否已经记录过,只是一道保险

上传完之后,因为存储桶前面设置为了public,所以直接访问http://127.0.0.1:9000/存储桶名称/hash, 就可以看到上传的文件了,如果设置正确的contentType并且浏览器支持预览,那就可以直接在浏览器上看到,contentType不支持浏览器预览,那文件会预览不了,但是下载是可以的

相关推荐
ohMyGod_12333 分钟前
React16,17,18,19新特性更新对比
前端·javascript·react.js
前端小趴菜0535 分钟前
React-forwardRef-useImperativeHandle
前端·vue.js·react.js
@大迁世界35 分钟前
第1章 React组件开发基础
前端·javascript·react.js·前端框架·ecmascript
Hilaku39 分钟前
从一个实战项目,看懂 `new DataTransfer()` 的三大妙用
前端·javascript·jquery
爱分享的程序员42 分钟前
前端面试专栏-算法篇:20. 贪心算法与动态规划入门
前端·javascript·node.js
我想说一句43 分钟前
事件委托与合成事件:前端性能优化的"偷懒"艺术
前端·javascript
爱泡脚的鸡腿1 小时前
Web第二次笔记
前端·javascript
良辰未晚1 小时前
Canvas 绘制模糊?那是你没搞懂 DPR!
前端·canvas
Dream耀1 小时前
React合成事件揭秘:高效事件处理的幕后机制
前端·javascript
P7Dreamer1 小时前
Vue 3 + Element Plus 实现可定制的动态表格列配置组件
前端·vue.js