写点什么

前端使用 TypeScript 上传文件到 MinIO

作者:喵个咪
  • 2023-08-20
    湖南
  • 本文字数:6560 字

    阅读完需:约 22 分钟

TypeScript 前端上传文件到 MinIO

在以前,前端要上传文件到服务端,比较的麻烦,要么通过 HTTP 服务上传,要么通过 FTP 上传。这两者的可靠性都极低。


但是,后来,有了对象存储服务(Object Storage Service),对象存储也称为基于对象的存储,是一种计算机数据存储架构,旨在处理大量非结构化数据。与其他架构不同,它将数据指定为不同的单元,并捆绑元数据和唯一标识符,用于查找和访问每个数据单元。


OSS 具有与平台无关的 RESTful API 接口,您可以在任何应用、任何时间、任何地点存储和访问任意类型的数据。


网上比较著名的开源 OSS 有:MinIOCeph。其中 MinIO 的使用率是越来越高,可以说是很普及了。因此,我首选使用它来做文件上传和管理的系统。

什么是 MinIO?

官方解释:MinIO 是一个用 Golang 开发的基于 Apache License v2.0 开源协议的对象存储服务。


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


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


Minio 使用纠删码 erasure code 和校验和 checksum 来保护数据免受硬件故障和数据损坏。因此,即便您丢失一半数量(N/2)的硬盘,您仍然可以恢复数据。

本地 Docker 部署测试服务器

docker pull bitnami/minio:latest
# MINIO_ROOT_USER最少3个字符# MINIO_ROOT_PASSWORD最少8个字符# 第一次运行的时候,服务会自动关闭,手动再次启动就可以正常运行了.docker run -itd \ --name minio-server \ -p 9000:9000 \ -p 9001:9001 \ --env MINIO_ROOT_USER="root" \ --env MINIO_ROOT_PASSWORD="123456789" \ --env MINIO_DEFAULT_BUCKETS='images' \ --env MINIO_FORCE_NEW_KEYS="yes" \ --env BITNAMI_DEBUG=true \ bitnami/minio:latest
复制代码

TypeScript 实现文件上传

在 TypeScript 下,我们可用的文件上传方法有三种,可用于实现文件的上传:


  1. XMLHttpRequest

  2. Fetch API

  3. Axios


需要注意的是: 事实上,后两种 API 都是对XMLHttpRequest进行的封装。

1. XMLHttpRequest

function xhrUploadFile(file: File, url: string) {  const xhr = new XMLHttpRequest();  xhr.open('PUT', url, true);  xhr.send(file);
xhr.onload = () => { if (xhr.status === 200) { console.log(`${file.name} 上传成功`); } else { console.error(`${file.name} 上传失败`); } };}
复制代码

2. Fetch API

function fetchUploadFile(file: File, url: string) {  fetch(url, {    method: 'PUT',    body: file,  })    .then((response) => {      console.log(`${file.name} 上传成功`, response);    })    .catch((error) => {      console.error(`${file.name} 上传失败`, error);    });}
复制代码

3. Axios

function axiosUploadFile(file: File, url: string) {  const instance = axios.create();  instance    .put(url, file, {      headers: {        'Content-Type': file.type,      },    })    .then(function (response) {      console.log(`${file.name} 上传成功`, response);    })    .catch(function (error) {      console.error(`${file.name} 上传失败`, error);    });}
复制代码

MinIO 上传 API

它有 4 个 API 可供调用:


  1. putObject 从流上传

  2. fPutObject 从文件上传

  3. PresignedPutObject 提供一个临时的 HTTP PUT 操作预签名上传链接以供上传

  4. PresignedPostPolicy 提供一个临时的 HTTP POST 操作预签名上传链接以供上传


使用方法 1 和 2 的话,必须要在前端暴露用于连接 MinIO 的访问密钥。这样很不安全,并且官方的 Js 客户端也压根就没想过开放给浏览器。


而使用方法 3 和 4 的话,我们可以由服务端来生成一个临时的上传链接,提供给前端上传之用,无需暴露访问 MinIO 的密钥给前端,这样非常的安全,因此我采用的是第 3、4 种方式


在下面,我们主要讨论的也是这两种方法,前两种不实用,故而不做任何讨论。


第三种方式,官方有一篇文章: Upload Files Using Pre-signed URLs

实现 go 后端

首先对 MinIO 的 SDK 做一个简单的封装:


package minio
import ( "context" "log" "net/url" "time"
"github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials")
const ( defaultExpiryTime = time.Second * 24 * 60 * 60 // 1 day
endpoint string = "localhost:9000" accessKeyID string = "root" secretAccessKey string = "123456789" useSSL bool = false)
type Client struct { cli *minio.Client}
func NewMinioClient() *Client { cli, err := minio.New(endpoint, &minio.Options{ Creds: credentials.NewStaticV4(accessKeyID, secretAccessKey, ""), Secure: useSSL, }) if err != nil { log.Fatalln(err) }
return &Client{ cli: cli, }}
func (c *Client) PostPresignedUrl(ctx context.Context, bucketName, objectName string) (string, map[string]string, error) { expiry := defaultExpiryTime
policy := minio.NewPostPolicy() _ = policy.SetBucket(bucketName) _ = policy.SetKey(objectName) _ = policy.SetExpires(time.Now().UTC().Add(expiry))
presignedURL, formData, err := c.cli.PresignedPostPolicy(ctx, policy) if err != nil { log.Fatalln(err) return "", map[string]string{}, err }
return presignedURL.String(), formData, nil}
func (c *Client) PutPresignedUrl(ctx context.Context, bucketName, objectName string) (string, error) { expiry := defaultExpiryTime
presignedURL, err := c.cli.PresignedPutObject(ctx, bucketName, objectName, expiry) if err != nil { log.Fatalln(err) return "", err }
return presignedURL.String(), nil}
复制代码


然后我们需要提供两个接口用于提供给前端获取 MinIO 的预签名链接:


package http
import ( "context" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "main/minio" "net/http")
type Response struct { Code int `json:"code"` Msg string `json:"msg"` Data interface{} `json:"data"`}
func ResponseJSON(c *gin.Context, httpCode, errCode int, msg string, data interface{}) { c.JSON(httpCode, Response{ Code: errCode, Msg: msg, Data: data, }) return}
type Server struct { srv *gin.Engine minioClient *minio.Client}
func NewHttpServer() *Server { srv := &Server{ srv: gin.New(), minioClient: minio.NewMinioClient(), }
srv.init()
return srv}
func (s *Server) init() { s.srv.Use( gin.Logger(), gin.Recovery(), cors.Default(), ) s.registerRouter()}
func (s *Server) registerRouter() { s.srv.GET("/presignedPutUrl/:filename", s.handlePutPresignedUrl) s.srv.GET("/presignedPostUrl/:filename", s.handlePostPresignedUrl)}
func (s *Server) handlePutPresignedUrl(c *gin.Context) { fileName := c.Param("filename")
presignedURL, err := s.minioClient.PutPresignedUrl(context.Background(), "images", fileName) if err != nil { c.String(500, "get presigned url failed") return }
type ResponseData struct { Url string `json:"url"` } var resp ResponseData resp.Url = presignedURL ResponseJSON(c, http.StatusOK, 200, "", resp)}
func (s *Server) handlePostPresignedUrl(c *gin.Context) { fileName := c.Param("filename")
presignedURL, formData, err := s.minioClient.PostPresignedUrl(context.Background(), "images", fileName) if err != nil { c.String(500, "get presigned url failed") return }
type ResponseData struct { Url string `json:"url"` FormData map[string]string `json:"formData"` } var resp ResponseData resp.Url = presignedURL resp.FormData = formData ResponseJSON(c, http.StatusOK, 200, "", resp)}
func (s *Server) Run() { // Listen and serve on 0.0.0.0:8080 _ = s.srv.Run(":8080")}
复制代码


这样我们就有了一个提供 MinIO 预签名的 REST 服务了。

前端实现 PUT 方法上传文件

import axios from 'axios';
export class PutFile { static xhr(file: File, url: string) { const xhr = new XMLHttpRequest(); xhr.open('PUT', url, true); xhr.send(file);
xhr.onload = () => { if (xhr.status === 200 || xhr.status === 204) { console.log(`[${xhr.status}] ${file.name} 上传成功`); } else { console.error(`[${xhr.status}] ${file.name} 上传失败`); } }; }
static fetch(file: File, url: string) { fetch(url, { method: 'PUT', body: file, }) .then((response) => { console.log(`${file.name} 上传成功`, response); }) .catch((error) => { console.error(`${file.name} 上传失败`, error); }); }
static axios(file: File, url: string) { axios .put(url, file, { headers: { 'Content-Type': file.type, }, }) .then(function (response) { console.log(`${file.name} 上传成功`, response); }) .catch(function (error) { console.error(`${file.name} 上传失败`, error); }); }}
export function retrievePutUrl(file: File, cb: (file: File, url: string) => void) { const url = `http://localhost:8080/presignedPutUrl/${file.name}`; axios.get(url) .then(function (response) { cb(file, response.data.data.url); }) .catch(function (error) { console.error(error); });}
export function xhrPutFile(file?: File) { console.log('XhrPutFile', file); if (file) { retrievePutUrl(file, (file, url) => { PutFile.xhr(file, url); }); }}
export function fetchPutFile(file?: File) { console.log('FetchPutFile', file); if (file) { retrievePutUrl(file, (file, url) => { PutFile.fetch(file, url); }); }}
export function axiosPutFile(file?: File) { console.log('AxiosPutFile', file); if (file) { retrievePutUrl(file, (file, url) => { PutFile.axios(file, url); }); }}
复制代码

前端实现 POST 方法上传文件

import axios from 'axios';
export class PostFile { static xhr(file: File, url: string, data: object) { const formData = new FormData(); formData.append('file', file); Object.entries(data).forEach(([k, v]) => { formData.append(k, v); });
const xhr = new XMLHttpRequest(); xhr.open('POST', url, true); xhr.send(formData);
xhr.onload = () => { if (xhr.status === 200 || xhr.status === 204) { console.log(`[${xhr.status}] ${file.name} 上传成功`); } else { console.error(`[${xhr.status}] ${file.name} 上传失败`); } }; }
static fetch(file: File, url: string, data: object) { const formData = new FormData(); formData.append('file', file); Object.entries(data).forEach(([k, v]) => { formData.append(k, v); });
fetch(url, { method: 'POST', body: formData, }) .then((response) => { console.log(`${file.name} 上传成功`, response); }) .catch((error) => { console.error(`${file.name} 上传失败`, error); }); }
static axios(file: File, url: string, data: object) { const formData = new FormData(); formData.append('file', file); Object.entries(data).forEach(([k, v]) => { formData.append(k, v); });
axios.post( url, formData, { headers: { 'Content-Type': 'multipart/form-data', }, }) .then(function (response) { console.log(`${file.name} 上传成功`, response); }) .catch(function (error) { console.error(`${file.name} 上传失败`, error); }); }}
export function retrievePostUrl(file: File, cb: (file: File, url: string, data: object) => void) { const url = `http://localhost:8080/presignedPostUrl/${file.name}`; axios.get(url) .then(function (response) { cb(file, response.data.data.url, response.data.data.formData); }) .catch(function (error) { console.error(error); });}
export function xhrPostFile(file?: File) { console.log('xhrPostFile', file); if (file) { retrievePostUrl(file, (file: File, url: string, data: object) => { PostFile.xhr(file, url, data); }); }}
export function fetchPostFile(file?: File) { console.log('fetchPostFile', file); if (file) { retrievePostUrl(file, (file: File, url: string, data: object) => { PostFile.fetch(file, url, data); }); }}
export function axiosPostFile(file?: File) { console.log('axiosPostFile', file); if (file) { retrievePostUrl(file, (file: File, url: string, data: object) => { PostFile.axios(file, url, data); }); }}
复制代码

踩过的坑

1. presignedPutObject方式上传提交的方法必须得是PUT

我试过了用POST去上传文件,但是结果显然是:我失败了,必须得用PUT去上传,正如其方法名中带有Put

2. 直接发送File即可

看了不少文章都是这么干的: 构造一个FormData,然后把文件打进去,如果用putObjectfPutObject这两个方法上传,这是没问题的:


fileUpload(file) {    const url = 'http://example.com/file-upload';    const formData = new FormData();    formData.append('file', file)    const config = {        headers: {            'content-type': 'multipart/form-data'        }    }    return post(url, formData, config)}
复制代码


如果使用以上的方式上传,文件头会被插入一段数据,看起来像是这样子的:


------WebKitFormBoundaryaym16ehT29q60rUxContent-Disposition: form-data; name="file"; filename="webfonts.zip"Content-Type: application/zip
复制代码


它是遵照了 rfc1867 定义的协议,插入的协议数据。


但是如果是使用presignedPutObject的方式则是不行的,接收到的文件里面将会有上面的协议数据,不需要构造FormData,直接发送File就可以了。

3. 使用Axios上传的时候,需要自己把Content-Type填写成为file.type

直接使用XMLHttpRequestFetch API都会自动填写成为文件真实的Content-Type。而Axios则不会,需要自己填写进去,或许是我不会使用Axios,但是这是一个需要注意的地方,否则在 MinIO 里边的Content-Type会被填写成为Axios默认的Content-Type

示例代码

Github: https://github.com/tx7do/minio-typescript-example


Gitee: https://gitee.com/tx7do/minio-typescript-example


  • 后端采用 go+gin 实现;

  • 前端有 React 和 Vue 的实现,要实现进度条和多文件上传也是容易的。

发布于: 刚刚阅读数: 3
用户头像

喵个咪

关注

还未添加个人签名 2022-06-01 加入

还未添加个人简介

评论

发布
暂无评论
前端使用TypeScript上传文件到MinIO_typescript_喵个咪_InfoQ写作社区