UtilsAliOSS.mjs

import path from "path";
import { glob } from "glob";
import OSS from "ali-oss";

/**
 * Utility functions for ali-oss
 * @module OSS-Ali
 */

/**
 * @typedef {import('ali-oss')} AliOSSClient
 */

/**
 * @typedef {Object} ParamsAliOSSConstructor
 * @property {string} accessKeyId
 * @property {string} accessKeySecret
 * @property {string} bucket
 * @property {string} region
 */

/**
 * Get Ali OSS client
 * @param {ParamsAliOSSConstructor} payload
 * @returns {AliOSSClient}
 */
export function getClientFromAliOSS(payload) {
  const { accessKeyId, accessKeySecret, bucket, region } = payload;
  return new OSS({
    accessKeyId,
    accessKeySecret,
    bucket,
    region,
  });
}

/**
 * @typedef {Object} ParamsAliOSSGetObjectUrl
 * @property {AliOSSClient} client
 * @property {string} key
 * @property {string} [baseUrl] 一般为CDN加速域名,以http(s)开头
 */

/**
 * Get object url
 * @param {ParamsAliOSSGetObjectUrl} payload
 * @returns {string}
 */
export function getObjectUrlFromAliOSS(payload) {
  const { client, key, baseUrl } = payload;
  return client.getObjectUrl(key, baseUrl);
}

/**
 * @typedef {import('ali-oss').RequestOptions} AliRequestOptions
 * @typedef {import('ali-oss').ObjectMeta} AliObjectMeta
 */

/**
 * @typedef {Object} ParamsAliOSSListFiles
 * @property {AliOSSClient} client
 * @property {string} prefix
 * @property {number} [maxKeys = 100] max objects, default is 100, limit to 1000, set it to 0 or ignore it if you want to list all files
 * @property {AliRequestOptions} [options]
 */

/**
 * List files
 * @param {ParamsAliOSSListFiles} payload
 * @returns {Promise<AliObjectMeta[]>}
 */
export async function listFilesFromAliOSS(payload) {
  const { client, prefix, maxKeys = 100, options } = payload;
  /** @type {AliObjectMeta[]} */
  let resultObjects = [];
  /** @type {string | null} */
  let continuationToken = "";
  do {
    const data = await client.listV2(
      {
        prefix,
        "max-keys": String(!maxKeys ? 1000 : maxKeys),
      },
      {
        timeout: 30000,
        ...(options || {}),
      },
    );
    if (data.objects) {
      resultObjects = resultObjects.concat(data.objects);
    }
    // @ts-ignore
    continuationToken = data.nextContinuationToken || "";
  } while (continuationToken && !maxKeys);

  return resultObjects;
}

/**
 * @typedef {Object} ParamsAliDeleteRemotePathList
 * @property {AliOSSClient} client
 * @property {string[]} remotePathList
 */

/**
 * @typedef {Object} ReturnAliDeleteRemotePathList
 * @property {string[]} successItems
 * @property {string[]} failItems
 */

/**
 * @typedef {Object} ReturnAliDeleteResultDeleted
 * @property {string} key
 */

/**
 * @typedef {Object} ReturnAliDeleteResult
 * @property {ReturnAliDeleteResultDeleted[]} deleted
 */

/**
 * Delete files
 * @param {ParamsAliDeleteRemotePathList} payload
 * @returns {Promise<ReturnAliDeleteRemotePathList>}
 */
export async function deleteRemotePathListFromAliOSS(payload) {
  const { client, remotePathList } = payload;
  /** @type {string[]} */
  const successItems = [];
  /** @type {string[]} */
  const failItems = [];
  if (remotePathList.length === 0) {
    return {
      successItems: [],
      failItems: [],
    };
  }

  // 有目录需要清空的话,清空对应目录下的文件
  for (const prefix of remotePathList) {
    const fileList = await listFilesFromAliOSS({
      client,
      prefix,
      maxKeys: 0,
    });
    const keysToDelete = fileList.map((item) => item.name);
    /** @type {ReturnAliDeleteResult} */
    // @ts-ignore
    const result = await client.deleteMulti(remotePathList);
    const rawDeleted = result.deleted || [];
    /** @type {string[]} */
    const deletedKeys = rawDeleted.map((item) => item.key);
    keysToDelete.forEach((key) => {
      if (deletedKeys.includes(key)) {
        successItems.push(key);
      } else {
        failItems.push(key);
      }
    });
  }

  return {
    successItems,
    failItems,
  };
}

/**
 * @typedef {Object} ParamsUploadLocalFile
 * @property {AliOSSClient} client
 * @property {string} localPath
 * @property {string} remotePath
 * @property {string} [baseUrl]
 * @property {import('ali-oss').PutObjectOptions} [config]
 */

/**
 * @typedef {Object} ReturnUploadLocalFile
 * @property {string} name
 * @property {string} url
 * @property {string} cdnUrl
 */

/**
 * Upload local file to aliyun oss
 * @param {ParamsUploadLocalFile} payload
 * @returns {Promise<ReturnUploadLocalFile>}
 */
export async function uploadLocalFileToAliOSS(payload) {
  const { client, localPath, remotePath, baseUrl, config = {} } = payload;
  // 自定义请求头
  const headers = {
    // 指定Object的存储类型。
    "x-oss-storage-class": "Standard",
    // 指定Object的访问权限。
    "x-oss-object-acl": "public-read",
    // 通过文件URL访问文件时,指定以附件形式下载文件,下载后的文件名称定义为example.txt。
    // 'Content-Disposition': `attachment; filename="${filePathAndName.split('/').reverse()[0]}"`,
    // 不以附件形式下载,直接访问
    "Content-Disposition": "inline",
    // 设置Object的标签,可同时设置多个标签。
    "x-oss-tagging": "Tag1=1&Tag2=2",
    // 指定PutObject操作时是否覆盖同名目标Object。此处设置为true,表示禁止覆盖同名Object。
    "x-oss-forbid-overwrite": "false",
    ...(config?.headers || {}),
  };
  const result = await client.put(
    remotePath,
    localPath,
    // 自定义headers
    {
      ...config,
      headers,
    },
  );

  const name = result.name;
  const cdnUrl = getObjectUrlFromAliOSS({
    client,
    key: name,
    baseUrl,
  });
  return {
    name,
    url: result.url,
    cdnUrl,
  };
}

/**
 * Normalize path
 * @param {string} filePath
 * @returns {string}
 *
 * @ignore
 */
const normalizePath = (filePath) => {
  return filePath.replace(/\\/g, "/");
};

/**
 * @typedef {Object} ParamsUploadDirToAliOSS
 * @property {AliOSSClient} client
 * @property {string} localPath
 * @property {string[]} ignorePathList
 * @property {boolean} [recursive = false]
 */

/**
 * Upload directory to aliyun oss
 * @param {ParamsUploadDirToAliOSS} payload
 * @returns {Promise<ReturnUploadLocalFile[]>}
 */
export async function uploadDirToAliOSS(payload) {
  const { client, localPath, ignorePathList, recursive = false } = payload;
  const globPath = recursive
    ? path.resolve(localPath, "**/*")
    : path.resolve(localPath, "*");
  /** @type {import('glob').GlobOptionsWithFileTypesUnset} */
  const globConfig = {
    windowsPathsNoEscape: true,
    // only want the files, not the dirs
    nodir: true,
    ignore: Array.from(new Set(["node_modules", ...(ignorePathList || [])])),
  };
  const allFiles = await glob.glob(globPath, globConfig);
  const rootPath = `${normalizePath(path.resolve(localPath))}/`;
  const allPaths = allFiles.map((filePath) => {
    return {
      localPath: normalizePath(filePath),
      remotePath: normalizePath(filePath).replace(rootPath, ""),
    };
  });
  return await Promise.all(
    allPaths.map(({ localPath, remotePath }) => {
      return uploadLocalFileToAliOSS({
        client,
        localPath,
        remotePath,
      });
    }),
  );
}