封装AWS S3上传和断点续传的

1,046 阅读4分钟

简介

本文档旨在详细介绍如何使用 AWS S3 的多段式上传功能管理大文件上传。我们将深入探讨一个 JavaScript 类 S3,它封装了 AWS SDK 的相关功能,提供了便捷的文件上传、下载和断点续传等操作。

环境配置

在开始使用 AWS S3 进行文件操作前,首先需要配置 AWS SDK 和相关的凭证信息。以下是配置代码片段:

import { S3Client } from "@aws-sdk/client-s3";

const config = {
    credentials: {
        accessKeyId: "xxx",
        secretAccessKey: "xxx",
    },
    region: "xxx",
}

const s3 = new S3Client(config);

S3 类实现

我们将通过一个名为 S3 的 JavaScript 类来实现对 AWS S3 的各种操作,包括文件上传、获取文件路径、断点续传、终止任务以及通过 URL 下载文件。

类的定义和构造函数

class S3 {
    constructor(config, bucket = "xxx") {
        this.bucket = bucket
        this.s3 = new S3Client(config);
        this.parts = []
        this.createResponse = {}
        this.tasks = []
    }
}

在构造函数中,我们初始化了 S3Client 实例,并设置了一些基本的属性,包括 bucket(桶名称)、parts(存储文件分片信息的数组)、createResponse(存储创建多段上传的响应信息)以及 tasks(任务列表)。

文件上传功能

文件上传功能主要通过 UploadFile 方法实现。该方法使用了 AWS SDK 提供的 PutObjectCommand 命令,将文件上传到指定的 S3 存储桶中。

// 上传文件
UploadFile(file) {
    return this.s3.send(new PutObjectCommand({ Key: file.name, Body: file, Bucket: this.bucket, ContentType: file.type }))
}

获取文件路径

GetFileUrl 方法用于获取文件的 URL 地址,以便前端可以通过 URL 直接访问文件内容。该方法首先通过 GetObjectCommand 获取文件流,然后将其转换为 Blob 对象,最后生成一个 URL 地址。

// 获取文件路径
async GetFileUrl(file) {
    const stream = await this.s3.send(new GetObjectCommand({ Key: file.name, Bucket: this.bucket }));
    const streamBlob = await binconv.readableStreamToBlob(stream.Body);
    const blob = new Blob([streamBlob], { type: stream.ContentType });
    return Promise.resolve(URL.createObjectURL(blob))
}

断点续传功能

断点续传功能是该类的核心功能之一,主要通过 uploadPointerFile 方法实现。该方法将大文件分割成多个小片段,并逐一上传到 S3。

// 断点续传
async uploadPointerFile(file, callback, chunkSize = 5 * 1024 * 1024) {
    callback(0, '解析中...')
    const {md5} = await md5ByFile(file)
    // 创建文件数据
    const createMultipartUploadCommand = new CreateMultipartUploadCommand({ Key: md5, Bucket: this.bucket, ContentType: file.type });
    // 创建文件记录
    this.createResponse = await this.s3.send(createMultipartUploadCommand);
    // 切片 
    let start = 0;
    let index = 0
    const number = Math.ceil(file.size / chunkSize)
    while (start < file.size) {
        if (this.status === status.STOP) {
            callback(-1, '已取消...')
            break
        }
        const chunk = file.slice(start, start + chunkSize);
        const reader = new FileReader();
        reader.readAsArrayBuffer(chunk);
        const res = await new Promise((resolve, reject) => {
            reader.onload = async () => {
                // 上传切片
                const uploadPartCommand = new UploadPartCommand({
                    Bucket: this.bucket,
                    Key: md5,
                    UploadId: this.createResponse.UploadId,
                    PartNumber: index + 1,
                    Body: chunk
                });
                const partResponse = await this.s3.send(uploadPartCommand);
                resolve(partResponse)
            };
        });
        this.parts.push({ PartNumber: index + 1, ETag: res.ETag });
        start = start + chunkSize;
        index += 1
        callback(0, null, Math.round(index * 100 / number))
        // 合并分片
        if (number == index) {
            const sortedParts = this.parts.sort((a, b) => a.PartNumber - b.PartNumber);
            const params = {
                Bucket: this.bucket,
                Key: md5,
                UploadId: this.createResponse.UploadId,
                MultipartUpload: { Parts: sortedParts }
            };
            const completeMultipartUploadCommand = new CompleteMultipartUploadCommand(params);
            const data = await this.s3.send(completeMultipartUploadCommand).catch((e) => console.log(e));
            callback(1, {...data,md5,file}, 100)
        }
    }
}

终止任务功能

当需要终止一个正在进行的上传任务时,可以使用 abortUpload 方法。该方法会发送一个 AbortMultipartUploadCommand 命令给 AWS S3,从而取消当前的多段上传任务。

// 终止任务
async abortUpload(file) {
    const CMD = new AbortMultipartUploadCommand({ Bucket: this.bucket, Key: file.name, UploadId: this.createResponse.UploadId })
    const data = await this.s3.send(CMD).catch((e) => console.log(e));
    if (data) {
        this.status = "STOP"
    }
    return data
}

URL 下载文件

通过 URL 下载文件的功能是使用一个隐藏的 a 标签来实现的。该标签设置了 download 属性,并将 href 指向文件的 URL。当标签被点击时,浏览器会自动下载文件。

// url下载文件
downloadFile(name, url) {
    const elink = document.createElement('a');
    elink.download = name;
    elink.style.display = 'none';
    elink.href = url;
    document.body.appendChild(elink);
    elink.click();
    document.body.removeChild(elink);
}

完整代码

以下是完整的 S3 类代码:

import { CompleteMultipartUploadCommand, CreateMultipartUploadCommand, GetObjectCommand, PutObjectCommand, S3Client, UploadPartCommand, AbortMultipartUploadCommand } from "@aws-sdk/client-s3";
import * as binconv from 'binconv';
import { md5ByFile } from "./md5";

const config = {
    credentials: {
        accessKeyId: "xxx",
        secretAccessKey: "xxx",
    },
    region: "xxx",
}

const status = { STOP: "STOP", OK: "OK", ING: "ING" }
class S3 {
    constructor(config, bucket = "xxx") {
        this.bucket = bucket
        this.s3 = new S3Client(config);
        this.parts = []
        this.createResponse = {}
        this.tasks = []
    }
    // 上传文件
    UploadFile(file) {
        return this.s3.send(new PutObjectCommand({ Key: file.name, Body: file, Bucket: this.bucket, ContentType: file.type }))
    }
    // 获取文件路径
    async GetFileUrl(file) {
        const stream = await this.s3.send(new GetObjectCommand({ Key: file.name, Bucket: this.bucket }));
        const streamBlob = await binconv.readableStreamToBlob(stream.Body);
        const blob = new Blob([streamBlob], { type: stream.ContentType });
        return Promise.resolve(URL.createObjectURL(blob))
    }
    // 断点续传
    async uploadPointerFile(file, callback, chunkSize = 5 * 1024 * 1024,) {
        callback(0, '解析中...')
        const {md5} = await md5ByFile(file)
        // 创建文件数据
        const createMultipartUploadCommand = new CreateMultipartUploadCommand({ Key: md5, Bucket: this.bucket, ContentType: file.type });
        // 创建文件记录
        this.createResponse = await this.s3.send(createMultipartUploadCommand);
        // 切片 
        let start = 0;
        let index = 0
        const number = Math.ceil(file.size / chunkSize)
        while (start < file.size) {
            if (this.status === status.STOP) {
                callback(-1, '已取消...')
                break
            }
            const chunk = file.slice(start, start + chunkSize);
            const reader = new FileReader();
            reader.readAsArrayBuffer(chunk);
            const res = await new Promise((resolve, reject) => {
                reader.onload = async () => {
                    // 上传切片
                    const uploadPartCommand = new UploadPartCommand({
                        Bucket: this.bucket,
                        Key: md5,
                        UploadId: this.createResponse.UploadId,
                        PartNumber: index + 1,
                        Body: chunk
                    });
                    const partResponse = await this.s3.send(uploadPartCommand);
                    resolve(partResponse)
                };
            });
            this.parts.push({ PartNumber: index + 1, ETag: res.ETag });
            start = start + chunkSize;
            index += 1
            callback(0, null, Math.round(index * 100 / number))
            // 合并分片
            if (number == index) {
                const sortedParts = this.parts.sort((a, b) => a.PartNumber - b.PartNumber);
                const params = {
                    Bucket: this.bucket,
                    Key: md5,
                    UploadId: this.createResponse.UploadId,
                    MultipartUpload: { Parts: sortedParts }
                };
                const completeMultipartUploadCommand = new CompleteMultipartUploadCommand(params);
                const data = await this.s3.send(completeMultipartUploadCommand).catch((e) => console.log(e));
                callback(1, {...data,md5,file}, 100)
            }
        }
    }

    // 终止任务
    async abortUpload(file) {
        const CMD = new AbortMultipartUploadCommand({ Bucket: this.bucket, Key: file.name, UploadId: this.createResponse.UploadId })
        const data = await this.s3.send(CMD).catch((e) => console.log(e));
        if (data) {
            this.status = "STOP"
        }
        return data
    }

    // url下载文件
    downloadFile(name, url) {
        const elink = document.createElement('a');
        elink.download = name;
        elink.style.display = 'none';
        elink.href = url;
        document.body.appendChild(elink);
        elink.click();
        document.body.removeChild(elink);
    }

    
}
export default new S3(config)

总结

通过上述实现,我们能够方便地使用 AWS S3 进行大文件的上传和管理。无论是普通文件上传、断点续传,还是获取文件 URL 和终止上传任务,这个封装类都提供了简洁的接口。同时,通过断点续传功能,可以有效提高上传大文件的效率和可靠性。

希望本文档能为您提供有效的指导,帮助您更好地理解和使用 AWS S3 进行文件管理。如果有任何问题或建议,欢迎与我们联系。