xiaweiss / vite-plugin-ali-oss

Upload the production files bundled in the project to Ali OSS, except for html
MIT License
32 stars 10 forks source link

add Parallel upload and GZIP compression #17

Open yanzhangshuai opened 1 week ago

yanzhangshuai commented 1 week ago

I made 3 improvements to the code

  1. Parallel uploads.
  2. Retry
  3. GZIP compression
import color from 'picocolors'
import fg from 'fast-glob'
import path from 'path'
import OSS from 'ali-oss'
import { URL } from 'node:url'
import zlib from 'zlib'
import fs from 'fs-extra'

import { normalizePath, type Plugin, type ResolvedBuildOptions } from "vite";

const DEF_GZIP = true

const RETRY_COUNT = 3

export interface vitePluginAliOssOptions {
  retry?:           number
  gzip?:            boolean | number
  enabled?:         boolean
  region:           string
  accessKeyId:      string
  accessKeySecret:  string
  bucket:           string
  overwrite?:       boolean
  ignore?:          string[]
  headers?:         Record<string, string>
  test?:            boolean
}
export default function vitePluginAliOss(options: vitePluginAliOssOptions): Plugin {
  let baseConfig = '/'
  let buildConfig: ResolvedBuildOptions

  return {
    name: 'vite-plugin-ali-oss',
    enforce: 'post',
    apply: 'build',
    configResolved(config) {
      baseConfig = config.base
      buildConfig = config.build
    },

    closeBundle: {
      sequential: true,
      order: 'post',
      async handler() {
        if (!options.enabled) {
          return
        }

        if (!/^http/i.test(baseConfig)) {
          throw Error('[vite-plugin-ali-oss] base must be a url')
        }

        const outDirPath = normalizePath(path.resolve(normalizePath(buildConfig.outDir)))

        options.gzip  = options?.gzip === undefined ? DEF_GZIP : options.gzip
        options.retry = options?.retry === undefined || options?.retry < 0 ? RETRY_COUNT : options.retry

        const { pathname: ossBasePath, origin: ossOrigin } = new URL(baseConfig)
        const ssrClient = buildConfig.ssrManifest
        const ssrServer = buildConfig.ssr

        const client = new OSS({
          region:           options.region,
          accessKeyId:      options.accessKeyId,
          accessKeySecret:  options.accessKeySecret,
          bucket:           options.bucket
        })

        const files = await fg(
          outDirPath + '/**/*',
          {
            dot: true,
            ignore:
              // custom ignore
              options.ignore !== undefined ? options.ignore :
                // ssr client ignore
                ssrClient ? ['**/ssr-manifest.json', '**/*.html'] :
                  // ssr server ignore
                  ssrServer ? ['**'] :
                    // default ignore
                    ['**/*.html']
          }
        )

        console.log('')
        console.log('ali oss upload start' + (ssrClient ? ' (ssr client)' : ssrServer ? ' (ssr server)' : ''))
        console.log('')

        const upload = async (ossFilePath: string, filePath: string, headers: Record<string, string | number | boolean> = {}) => {

          headers = Object.assign({}, options.headers || {}, headers || {})

          const output = `${buildConfig.outDir + filePath} => ${color.green(ossOrigin + ossFilePath)}`

          const fn = (file: string | Buffer, retryCount = 0) => {

            return new Promise((resolve, reject) => {

              client.put(ossFilePath, file, { headers })
              .then(res => {
                console.log(`ali oss ${color.green('upload success')}: ${output}`)
                resolve(res)
              })
              .catch(err => {
                if (retryCount < options.retry!) {
                  console.log(`${color.yellow(`retry ${retryCount + 1}`)} upload ${output}`)
                  return fn(file, retryCount + 1)
                }

                reject(err) 
              })
            })

          }

          if (!options.gzip) {
            return fn(filePath)
          }

          const file = await getFile(filePath, options.gzip)
          headers['Content-Encoding'] = 'gzip'
          return fn(file)
        }

        const startTime = new Date().getTime()

        const req = files.map(async f => {
          const filePath = normalizePath(f).split(outDirPath)[1] // eg: '/assets/vendor.bfb92b77.js'
          const ossFilePath = ossBasePath.replace(/\/$/, '') + filePath // eg: '/base/assets/vendor.bfb92b77.js'
          const output = `${buildConfig.outDir + filePath} => ${color.green(ossOrigin + ossFilePath)}`

          try {
            if (options.test) {
              console.log(`test upload path: ${output}`)
              return Promise.resolve()
            }

            if (options.overwrite) {
              return upload(ossFilePath, f)
            }

            await client.head(ossFilePath);
            console.log(`ali oss ${color.gray('files exists')}: ${output}`)
            return Promise.resolve()

          } catch (error: any) {
            if (error.code === 'NoSuchKey') {
              return upload(ossFilePath, f, { 'x-oss-forbid-overwrite': true })

            } else {
              throw new Error(error)
            }
          }

        })

        try {

          await Promise.all(req)

          const duration = (new Date().getTime() - startTime) / 1000

          console.log('')
          console.log(`ali oss upload complete ^_^, cost ${duration.toFixed(2)}s`)
          console.log('')
        } catch (error) {
          console.log('')
          console.log(`ali oss ${color.red('upload fail')}` + color.red(`, error: ${error}`))
          console.log('')
        }
      }
    }
  }
}
xiaweiss commented 1 week ago

感谢贡献

  1. Parallel uploads 服务器的带宽是有限的,并发上传不一定缩短时间,我需要测试看看

  2. Retry 重试的 case,不太容易测试,我生产环境也没遇到过这类报错。粗看代码的话,重试后没有 resolve,这块的逻辑我需要花时间再看看

  3. GZIP compression

oss 只需要下载时开启 gzip 即可,上传并不需要吧? https://www.alibabacloud.com/help/zh/oss/user-guide/how-do-i-compress-objects-that-i-download-from-oss-in-the-gzip-format

能否提供更多 gzip 相关细节?感谢🙏

xiaweiss commented 1 week ago

根据以下资料,可能需要本地进行 gzip 压缩操作

https://github.com/ali-sdk/ali-oss/issues/555 https://github.com/ali-oss-gzip/ali-oss-gzip/blob/master/lib/uploader.js#L45

yanzhangshuai commented 6 days ago

感谢贡献

  1. Parallel uploads 服务器的带宽是有限的,并发上传不一定缩短时间,我需要测试看看
  2. Retry 重试的 case,不太容易测试,我生产环境也没遇到过这类报错。粗看代码的话,重试后没有 resolve,这块的逻辑我需要花时间再看看
  3. GZIP compression

oss 只需要下载时开启 gzip 即可,上传并不需要吧? https://www.alibabacloud.com/help/zh/oss/user-guide/how-do-i-compress-objects-that-i-download-from-oss-in-the-gzip-format

能否提供更多 gzip 相关细节?感谢🙏

  1. 并行上传我这里速度提升是成倍级别的,大概我这里只上传代码。

  2. 重试的确不太容易测试,我这里测试了两天,也仅碰到过几次,重试的代码是有resolve的,就是为了resolve,才用了new Promise, await就不行了。

  3. gzip,压缩是为了提高上传速度,

这三个功能来源于我之前使用的webpack插件 webpack-alioss-plugin

xiaweiss commented 6 days ago

好的,我抽空看看