@mwcp/ali-oss
Version:
Ali OSS Component for midway.js
523 lines (461 loc) • 13.6 kB
text/typescript
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
/* eslint-disable max-lines-per-function */
import { AttrNames, SpanStatusCode } from '@mwcp/otel'
import type { Attributes, TraceService } from '@mwcp/otel'
import { genError, genISO8601String } from '@waiting/shared-core'
import { FnKey, OssClient } from '@yuntools/ali-oss'
import type {
BaseOptions,
Config as OssConfig,
CpOptions as AliCpOptions,
DataBase,
DataCp,
DataDownload,
DataSign,
DataStat,
ProcessRet,
} from '@yuntools/ali-oss'
import { ConfigKey } from './types.js'
import type {
CpOptions,
DownloadOptions,
InstanceConfig,
LinkOptions,
MkdirOptions, MvOptions,
PathExistsOptions,
QuerySpanInfo,
RmOptions, RmrfOptions,
SignOptions, StatOptions, SyncLocalOptions, SyncRemoteOptions,
UploadOptions,
} from './types.js'
/** 阿里云 OSS oss-utils 命令行封装组件 */
export class AliOssComponent {
traceService: TraceService
private readonly client: OssClient
private readonly querySpanMap = new WeakMap<object, QuerySpanInfo>()
constructor(protected readonly config: InstanceConfig) {
const opts: OssConfig = {
accessKeyId: config.accessKeyId,
accessKeySecret: config.accessKeySecret,
endpoint: config.endpoint,
}
if (config.stsToken) {
opts.stsToken = config.stsToken
}
const client = new OssClient(opts, config.cmd)
client.debug = !! config.debug
this.client = client
}
/**
* 拷贝文件,
* - 拷贝本地文件/目录到远程建议使用 `upload()` 或者 `syncRemote()` 方法
* - 拷贝远程文件/目录到本地建议使用 `download()` 或者 `syncLocal()` 方法
*
* 若 force 为空或者 false,且目标文件存在时会卡在命令行提示输入阶段(无显示)最后导致超时异常
* @link https://help.aliyun.com/document_detail/120057.html
*/
async cp(
/** 本地文件、目录或者远程 OSS 对象 */
src: string,
/** OSS 对象,不包括 bucket */
target: string,
options?: CpOptions,
): Promise<ProcessRet<DataCp>> {
const opts: RunnerOptions<CpOptions> = {
fnKey: FnKey.cp,
options,
target,
src,
}
const ret = await this.runner<CpOptions, ProcessRet<DataCp>>(opts)
return ret
}
/**
* 上传本地文件到 OSS
* 若 force 为空或 false,且目标文件存在时会卡在命令行提示输入阶段(无显示)最后导致超时异常
* @link https://help.aliyun.com/document_detail/120057.html
*/
async upload(
/** 本地目录或文件 */
src: string,
/** OSS 对象,不包括 bucket */
target: string,
options?: UploadOptions,
): Promise<ProcessRet<DataCp>> {
const opts: RunnerOptions<UploadOptions> = {
fnKey: FnKey.upload,
options,
target,
src,
}
const ret = await this.runner<UploadOptions, ProcessRet<DataCp>>(opts)
return ret
}
/**
* 下载远程文件到本地
* 若 force 为空或 false,且目标文件存在时会卡在命令行提示输入阶段(无显示)最后导致超时异常
* @link https://help.aliyun.com/document_detail/120057.html
*/
async download(
/** OSS 对象,不包括 bucket */
src: string,
/** 本地目录或文件 */
target: string,
options?: DownloadOptions,
): Promise<ProcessRet<DataDownload>> {
const opts: RunnerOptions<DownloadOptions> = {
fnKey: FnKey.download,
options,
target,
src,
}
const ret = await this.runner<DownloadOptions, ProcessRet<DataDownload>>(opts)
return ret
}
/**
* 创建软链接
* @link https://help.aliyun.com/document_detail/120059.html
*/
async createSymlink(
/** OSS 对象,不包括 bucket */
src: string,
/** OSS 软连接对象,不包括 bucket */
target: string,
options?: LinkOptions,
): Promise<ProcessRet> {
const opts: RunnerOptions<LinkOptions> = {
fnKey: FnKey.link,
options,
target,
src,
}
const ret = await this.runner<LinkOptions>(opts)
return ret
}
/**
* 创建目录
* @link https://help.aliyun.com/document_detail/120062.html
*/
async mkdir(
/** OSS 对象,不包括 bucket */
target: string,
options?: MkdirOptions,
): Promise<ProcessRet> {
const opts: RunnerOptions<MkdirOptions> = {
fnKey: FnKey.mkdir,
options,
target,
src: void 0,
}
const ret = await this.runner(opts)
return ret
}
/**
* 移动云端的 OSS 对象
* 流程为先 `cp()` 然后 `rm()`
*/
async mv(
/** OSS 源对象,不包括 bucket */
src: string,
/** OSS 目的对象,不包括 bucket */
target: string,
options?: MvOptions,
): Promise<ProcessRet<DataStat | DataBase>> {
const opts: RunnerOptions<MvOptions> = {
fnKey: FnKey.mv,
options,
target,
src,
}
const ret = await this.runner<MvOptions, ProcessRet<DataStat | DataBase>>(opts)
return ret
}
/**
* OSS 远程路径是否存在
*/
async pathExists(
/** OSS 对象,不包括 bucket */
target: string,
options?: StatOptions,
): Promise<boolean> {
const opts: RunnerOptions<PathExistsOptions> = {
fnKey: FnKey.pathExists,
options,
target,
src: void 0,
}
const ret = await this.runner<PathExistsOptions, boolean>(opts)
return ret
}
/**
* 删除云对象,不支持删除 bucket 本身
* 如果在 recursive 为 false 时删除目录,则目录参数值必须以 '/' 结尾,否则不会删除成功
* @link https://help.aliyun.com/document_detail/120053.html
*/
async rm(
/** OSS 对象,不包括 bucket */
target: string,
options?: RmOptions,
): Promise<ProcessRet> {
const opts: RunnerOptions<RmOptions> = {
fnKey: FnKey.rm,
options,
target,
src: void 0,
}
const ret = await this.runner(opts)
return ret
}
/**
* 递归删除,相当于 `rm -rf`
* @link https://help.aliyun.com/document_detail/120053.html
*/
async rmrf(
/** OSS 对象,不包括 bucket */
target: string,
options?: RmrfOptions,
): Promise<ProcessRet> {
const opts: RunnerOptions<RmrfOptions> = {
fnKey: FnKey.rmrf,
options,
target,
src: void 0,
}
const ret = await this.runner(opts)
return ret
}
/**
* sign(生成签名URL)
* @link https://help.aliyun.com/document_detail/120064.html
*/
async sign(
/** OSS 对象,不包括 bucket */
src: string,
options?: SignOptions,
): Promise<ProcessRet<DataSign>> {
const opts: RunnerOptions<SignOptions> = {
fnKey: FnKey.sign,
options,
target: void 0,
src,
}
const ret = await this.runner<SignOptions, ProcessRet<DataSign>>(opts)
return ret
}
/**
* 查看 Bucket 和 Object 信息
* @link https://help.aliyun.com/document_detail/120054.html
*/
async stat(
/** OSS 对象,不包括 bucket */
target: string,
options?: StatOptions,
): Promise<ProcessRet> {
const opts: RunnerOptions<StatOptions> = {
fnKey: FnKey.stat,
options,
target,
src: void 0,
}
const ret = await this.runner<StatOptions, ProcessRet>(opts)
return ret
}
/**
* 同步 OSS 文件到本地
* - force 参数默认 true
* - 若 force 为 false,且目标文件存在时会卡在命令行提示输入阶段(无显示)最后导致超时异常
* @link https://help.aliyun.com/document_detail/256352.html
*/
async syncLocal(
/** OSS 对象,不包括 bucket */
src: string,
/** 本地目录 */
target: string,
options?: SyncLocalOptions,
): Promise<ProcessRet<DataCp>> {
const opts: RunnerOptions<SyncLocalOptions> = {
fnKey: FnKey.syncLocal,
options,
target,
src,
}
const ret = await this.runner<SyncLocalOptions, ProcessRet<DataCp>>(opts)
return ret
}
/**
* 同步本地文件到 OSS
* - force 参数默认 true
* - 若 force 为 false,且目标文件存在时会卡在命令行提示输入阶段(无显示)最后导致超时异常
* @link https://help.aliyun.com/document_detail/193394.html
*/
async syncRemote(
/** 本地目录 */
src: string,
/** OSS 对象,不包括 bucket */
target: string,
options?: SyncRemoteOptions,
): Promise<ProcessRet<DataCp>> {
const opts: RunnerOptions<SyncRemoteOptions> = {
fnKey: FnKey.syncRemote,
options,
target,
src,
}
const ret = await this.runner<SyncRemoteOptions, ProcessRet<DataCp>>(opts)
return ret
}
protected async runner<
T extends BaseOptions,
R extends ProcessRet<DataBase> | boolean = ProcessRet<DataBase>,
>(options: RunnerOptions<T>): Promise<R> {
const { fnKey } = options
const opts = this.genOptions<T>(options)
const id = { time: Symbol(Date.now()) }
try {
await this.tracer('start', id, opts)
// @ts-ignore
const ret = await this.client[fnKey](opts) as Promise<R>
await this.tracer('finish', id, opts)
return await ret
}
catch (ex) {
const err = ex instanceof Error
? ex
: typeof ex === 'string' ? new Error(ex) : new Error(JSON.stringify(ex))
await this.tracer('error', id, opts, err)
throw err
}
}
protected genOptions<T extends BaseOptions>(input: RunnerOptions<T>): _RunnerOption<T> {
const ret = {
...this.config,
src: input.src,
target: input.target,
...input.options,
}
return ret as unknown as _RunnerOption<T>
}
protected async tracer<T extends BaseOptions>(
type: 'start' | 'finish' | 'error',
id: { time: symbol },
options: _RunnerOption<T>,
err?: Error,
): Promise<void> {
if (! options.enableTrace) { return }
if (! this.traceService) { return }
const end = Date.now()
const tmp = options as unknown as AliCpOptions & InstanceConfig
const opts: Attributes = {
acl: tmp.acl ?? '',
src: tmp.src,
payer: tmp.payer ?? '',
recursive: tmp.recursive ?? false,
sampleThrottleMs: tmp.sampleThrottleMs ?? 10000,
target: tmp.target,
}
switch (type) {
case 'start': {
const { span } = this.traceService.startScopeSpan({
name: ConfigKey.componentName,
scope: id,
})
const spanInfo: QuerySpanInfo = {
span,
timestamp: Date.now(),
}
this.querySpanMap.set(id, spanInfo)
const attrs: Attributes = {
[AttrNames.QueryCostThrottleInMS]: opts['sampleThrottleMs'],
qid: id.time.toString(),
acl: opts['acl'],
payer: opts['payer'],
recursive: opts['recursive'],
src: opts['src'],
target: opts['target'],
}
this.traceService.setAttributes(span, attrs)
const event: Attributes = {
event: AttrNames.QueryStart,
time: genISO8601String(),
}
this.traceService.addEvent(span, event)
break
}
case 'finish': {
const spanInfo = this.querySpanMap.get(id)
if (! spanInfo) {
console.warn('Retrieve spanInfo undefined.', opts)
return
}
const { span } = spanInfo
const start = spanInfo.timestamp
const cost = end - start
const tags: Attributes = {
[AttrNames.QueryCost]: cost,
}
this.traceService.setAttributes(span, tags)
const event: Attributes = {
event: AttrNames.QueryFinish,
time: genISO8601String(),
[AttrNames.QueryCost]: cost,
}
if (typeof opts['sampleThrottleMs'] === 'number'
&& opts['sampleThrottleMs'] > 0 && cost > opts['sampleThrottleMs']) {
const tags2: Attributes = {
// [AttrNames.SAMPLING_PRIORITY]: 50,
[AttrNames.LogLevel]: 'warn',
}
this.traceService.setAttributes(span, tags2)
this.traceService.addEvent(span, event)
}
else {
this.traceService.addEvent(span, event)
}
this.traceService.endSpan({ span, scope: id })
break
}
case 'error': {
const spanInfo = this.querySpanMap.get(id)
if (! spanInfo) {
console.warn('Retrieve spanInfo undefined.', opts)
return
}
const { span } = spanInfo
const start = spanInfo.timestamp
const cost = end - start
const attr: Attributes = {
[AttrNames.QueryCost]: cost,
}
this.traceService.setAttributes(span, attr)
const input = {
event: AttrNames.QueryError,
time: genISO8601String(),
[AttrNames.LogLevel]: 'error',
[AttrNames.QueryCost]: cost,
// [AttrNames.Error]: err,
}
this.traceService.addEvent(span, input)
this.traceService.endSpan({
span,
scope: id,
spanStatusOptions: {
code: SpanStatusCode.ERROR,
error: genError({
error: err,
altMessage: 'Unknown error.',
}),
},
})
break
}
}
return
}
}
interface RunnerOptions<T extends BaseOptions> {
fnKey: FnKey
options: T | undefined
src: string | undefined
target: string | undefined
}
type _RunnerOption<T extends BaseOptions> = InstanceConfig & T