nodejs使用aws-sdk上传下载(移动云EOS)

1,166 阅读10分钟

前言

最近对接了一个新的对象储存库,和 minio 类似,是移动云的 EOS,实际上使用的是 亚马逊的 aws 云服务框架

我们实际除了使用平台配置和查看文件、权限等参数,就是在对接 aws 的框架,至于为什么,因为移动 EOS 的文档太老了,他使用的又是纯粹的 AWS 的服务,因此直接对接 AWS 库即可,不然会被他的文档误导(无法使用,还好自己灵活对接,找到的库的来源,不然就得问他们技术了)

对接下来一天就完全完成功能了,当然要看了这篇文章,基础功能可能几十分钟功能就能完成了

下面是使用的文档地址:

aws-sdk安装使用--基础

aws-sdk/client-s3/S3Client参数介绍

aws-sdk/s3-request-presigner上传下载签名相关介绍--(put上传签名与get下载查看签名)

@aws-sdk/s3-presigned-post上传相关介绍--(post-formdate上传签名)

@aws-sdk/client-s3的npm

@aws-sdk/s3-request-presigner的npm

@aws-sdk/s3-presigned-post的npm

对象储存库 aws-sdk

对接这个对象库,实际上一般只使用两个库 @aws-sdk/client-s3@aws-sdk/s3-request-presigner

如果只是内部使用,前端对接的,那么只需要一个库 @aws-sdk/client-s3 即可

@aws-sdk/s3-request-presigner是一个后端用来签名给前端用的库,避免前端对接 aws 相关文档了

如果还要对接小程序等不能正常使用 put 上传的,那么还需要用到 @aws-sdk/s3-presigned-post来 进行 post 签名,然后使用 post + formdate 上传

ps: 推荐三个库一起安装,然后就会形成最常见,相对比较通用的功能

安装基础库

需要加入两个库,一个基础功能库,一个签名库,如果只是一个端使用(@aws-sdk/client-s3),不需要用到后面的签名库(@aws-sdk/s3-request-presigner)

yarn add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner @aws-sdk/s3-presigned-post

或者 
npm i @aws-sdk/client-s3 @aws-sdk/s3-request-presigner @aws-sdk/s3-presigned-post

初始化 S3Client

S3Client 使用我们的基础库 @aws-sdk/client-s3 ,初始化需要用到它,我们可以直接通过传递参数创建一个对象

this.client = new S3Client({
    //必传,添加我们的地区即可,EOS 等平台上面有该参数,用自己的
    region: 'tianjin1',
    //endpoint 记得自己加上 https://,记得不要用桶域名,直接使用基础域名即可
    endpoint: env.config.AWS_POINT,
    //验证用户身份和校验的两个key,可以通过平台申请
    credentials: {
        accessKeyId: env.config.AWS_ACCESSKEY,
        secretAccessKey: env.config.AWS_SECRETKEY,
    },
})

基础上传下载文件(单端专用,实际单端使用也可使用后续签名方式)

这个功能讲的是单端对接用的,服务器也可能用到,一般用不到,这一直接以下载为例了,上传 和 minio 基本一个样子

aws-sdk/client-s3/S3Client参数介绍

需要使用基础的 send方法发送指令,指令直接根据文档创建即可,一般都有 Bucket、Key两个参数,一个是桶名字,一个是文件名

前端直接下载

这里就先以下载为例 需要使用到 GetObjectCommand格式对象,返回的内容在 Body 中,是 Stream流,可以读取

//这里不建议使用,本人部署到正式服务器后,该方法就直接超时了
//获取text文本(转化utf8)
this.client.send(
    new GetObjectCommand({
        Bucket: this.bucketName,
        Key: filename,
    }),
)
.then(async function (obj) {
    //这里直接转化成text文本了
    await obj?.Body.transformToString()
    //await obj?.Body.transformToString('utf-8') //根据需要转化为utf-8
})
.catch(function (err) {
    reject(err)
})

Body 中的 stream流的参数转化,系统已经给我们了,可以直接转化成自己想要的格式

export interface SdkStreamMixin {
    transformToByteArray: () => Promise<Uint8Array>;
    transformToString: (encoding?: string) => Promise<string>;
    transformToWebStream: () => ReadableStream;
}

前端直接上传

上传和下载类似,使用的是 PutObjectCommand, 只不过需要往其 Body 中塞取内容,其有下面几种类型,直接看着转化就行了

NodeJsRuntimeStreamingBlobPayloadInputTypes = string | Uint8Array | Buffer | Readable;

上传代码大致如下所示

this.client.send(
    new PutObjectCommand({
        Bucket: this.bucketName,
        Key: filename,
        Body: ...
    }),
)

服务端签名(服务端重点-多端通用--强推)

前面的都是单端对接后端直接用的,一般是web端直接操作了,直接服务端签名的则是比较通用,无论是单端还是多端都可以,非常方便快捷

签名的目的就是为了授权给其他端,让其他端可以直接通过签名的url或者参数,直接下载或者上传文件,这样其他端可以很方便的实现下载或者上传逻辑

服务端下载签名 + put上传签名

签名使用的就不是基础库 @aws-sdk/client-s3 了,而是 @aws-sdk/s3-request-presigner,通过该库的 getSignedUrl 方法实现下载上传签名即可

//下载签名,可以设置签名有效期,不传也会有默认的有效期
signedUrl(filename: string) {
    //需要注意的是如果filename不存在,则会报错,外部或者这里需要处理一下
    const command = new GetObjectCommand({
        Bucket: this.bucketName,
        Key: filename,
    })
    return getSignedUrl(this.client, command, {
        expiresIn: 7 * 24 * 3600, //7天
    })
}

//上传签名,可以设置签名有效期,不传也会有默认的有效期
async signUpload(filename: string) {
    //需要注意的是如果filename不存在,则会报错,外部或者这里需要处理一下
    const command = new PutObjectCommand({
        Bucket: this.bucketName,
        Key: filename,
    })
    return getSignedUrl(this.client, command, {
        expiresIn: 24 * 3600, //限制一天上传完毕
    })
}

签名后会返回一个 url,专门用来下载、上传,对于上传, 直接 put 将内容放到 body 即可

下面就是签名好的url,看尾巴就知道是哪个了

//签名号的上传url
https://.../2.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-......&x-id=GetObject

//签名号的下载url
https://.../4.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-......&x-id=PutObject

服务端 post + formdata 上传签名(附注意事项)

这个就需要额外添加 @aws-sdk/s3-presigned-post 仓库了(如果所有端都可以 put 上传,这个不要也行)

 //需要注意的是如果filename不存在,则会报错,外部或者这里需要处理一下
createPresignedPost(this.client, {
    Bucket: this.bucketName,
    Key: filename,
    Expires: 24 * 3600, //1天后失效
})

返回格式

{
  "url": "https://test123.eos-tianjin-1.cmecloud.cn/",
  "fields": {
    "bucket": "test123",
    "X-Amz-Algorithm": "AWS4-HMAC-SHA256",
    "X-Amz-Credential": "I0E1JM2GRH63Z5U6P03I/20231220/tianjin1/s3/aws4_request",
    "X-Amz-Date": "20231220T124138Z",
    "key": "1703076098628.jpg",
    "Policy": "eyJleHBpcmF0aW9uIjoiMjAyMy0xMi0yMFQxMzo0MTozOFoiLCJjb25kaXRpb25zIjpbeyJidWNrZXQiOiJ6aHVvbGFuZy0yMzEyMTMisdfaskfjksadhfkasdhfkasjhdfkyMC90aWFuamluMS9zMy9hd3M0X3JlcXVlc3QifSx7IlgtQW16LURhdGUiOiIyMDIzMTIyMFQxMjQxMzhaIn0seyJrZXkiOiIxNzAzMDc2MDk4NjI4LmpwZyJ9XX0=",
    "X-Amz-Signature": "d46196ca16ab8a9asdfkljaskdfhaksdhfkjashdfkjashdkjf9d1852f1d5f7228"
  }
}

上传时,除了使用基础 url,我们需要将返回的 fields 参数都放到 body 中,格式为 form-data,需要最后一个参数放 file,然后就可以上传成功了

:form-data 最后一个参数如果不是 file,则会上传失败报错,不要问为什么,要问就问设计者吧😂

在服务端下载(补充-使用到时推荐--签名+fetch)

即使在服务端直接下载,要部署到多端也不能直接使用上面的直接下载,也是需要签名 + fetch 的方式获取,否则可能会出现测试时能用,正式服务器不能用的情况(个人踩坑了)

const url = await this.awsService.signedUrl(
    filename,
)
const res = await fetch(url)
const myfileText = await res.text()

ps:在服务器上传,和客户端也是一个样子,因此不多介绍了

内网下载(服务器部署到内网)

正常我们一般使用外网域名签名,当服务器部署到内网且服务器需要下载内容时,或者给签名访问的用户群体是内网用户时,此时需要用到内网域名签名

这时,我们需要额外添加一个内网签名的方法(http),这样对于需要内网访问的操作或接口,直接使用内网域名签名即可,外网用外网的签名即可

this.privateClient = new S3Client({
    region: 'tianjin1',
    endpoint: envConfig.awsPointPrivate,
    credentials: {
        accessKeyId: envConfig.awsAccessKey,
        secretAccessKey: envConfig.awsSecretKey,
    },
})

//私有域名签名
signedUrlByPrivate(filename: string) {
    const command = new GetObjectCommand({
        Bucket: this.bucketName,
        Key: filename,
    })
    return getSignedUrl(this.privateClient, command, {
        expiresIn: 7 * 24 * 3600, //7天
    })
}
const url = await this.awsService.signedUrlByPrivate(
    filename,
)
const res = await fetch(url)
const myfileText = await res.text()

服务端 put 分段传输 (签名形式)

文档参考地址

分段传输一共分为三个步骤,创建请求-分段传输-合并文件(取消传输)

//下面是分段上传的三个步骤,创建、上传、合并(取消)
//创建请求任务,获得指定参数,filename为生成的文件名,一般服务器生成唯一值
async createPartUpload(filename: string) {
    const res = await this.client.send(
        new CreateMultipartUploadCommand({
            Bucket: this.bucketName,
            Key: filename,
        }),
    )
    return {
        // bucketName: res.Bucket, //只连接一个的就没必要了
        key: res.Key,
        upload_id: res.UploadId,
        first_part_number: 1, //默认从第一个开始,告知一下
    }
}

//每个分段传输前调用,需要传递更新参数,5M-5G之间,否则合并都会不成功,数量最多不超哥1000个,也就是最大5T
partUpload(filename: string, uploadId: string, partNumber: number) {
    return getSignedUrl(
        this.client,
        new UploadPartCommand({
            Bucket: this.bucketName,
            Key: filename,
            PartNumber: partNumber,
            UploadId: uploadId,
        }),
    )
}

//查看分段
listPartUpload(filename: string, uploadId: string) {
    return this.client.send(
        new ListPartsCommand({
            Bucket: this.bucketName,
            Key: filename,
            UploadId: uploadId,
        }),
    )
}

//合并分段
async completePartUpload(filename: string, uploadId: string) {
    let listparts = null
    try {
        //获取分段信息,除了用于获取分段参数,也可以用来判断是否上传分段了
        listparts = await this.client.send(
            new ListPartsCommand({
                Bucket: this.bucketName,
                Key: filename,
                UploadId: uploadId,
            }),
        )
    } catch (err) {
        console.log(err)
    }
    if (!listparts?.Parts) {
        return Promise.reject('没有发现上传的分段信息')
    }
    return this.client.send(
        new CompleteMultipartUploadCommand({
            Bucket: this.bucketName,
            Key: filename,
            MultipartUpload: {
                Parts: listparts.Parts,
            },
            UploadId: uploadId,
        }),
    )
}

//取消请求任务
aboutPartUpload(filename: string, uploadId: string) {
    return this.client.send(
        new AbortMultipartUploadCommand({
            Bucket: this.bucketName,
            Key: filename,
            UploadId: uploadId,
        }),
    )
}

注意事项(可能存在的formdata上传、跨域问题)

aws 对一些参数不是那么友好,或者是比较严格

对于 formdate 上传图片,formdate 其他参数在前,file文件最后

对于前端上传请求时,如果用的 umi-request,一般默认会设置 redentials: 'include' 请将该单个 request 的请求设置参数 credentials: 'omit',这样就可避免一些跨域或者是其他系列问题了(如果不设置会出现类似的跨域问题,实际上可能根本不存在跨越问题)

内外网访问问题

移动云的坑,外网(https)访问,即使是https,其也无法访问内网内容,因此需要自己主动区分,如果用到内网(http),需要主动调整到内网域名签名访问,只能说碰到了,自己灵活对待吧

最后

我们使用时建议三个库一起用,基础 + 下载签名(上传加上也可以) + post 签名,这样就比较通用了

看他和 minio 一对比,发现太像了,很多技术都是殊途同归的,因此接触新的东西无疑对我们来说是一个提升,老将不死只是逐渐凋零

ps:这个移动云测试时,有台电脑测试的时候挺容易失败的(我这里没失败过),报错ssl问题,在EOS控制台也会上传失败,也是这个问题,个人感觉不是很稳哈,处理失败逻辑时,可以在上传时添加一个循环,以保证用户端实际使用体验