@yuntools/ali-oss
Version:
阿里云 OSS 命令行工具 ossutil 封装,支持 ESM,CJS 导入,提供 TypeScript 类型定义
341 lines (298 loc) • 9.9 kB
text/typescript
import assert from 'node:assert/strict'
import { statSync } from 'node:fs'
import { rm } from 'node:fs/promises'
import { homedir } from 'node:os'
import { join } from 'node:path'
import { run } from 'rxrunscript'
import { cpKeys, downloadKeys } from './config.js'
import {
combineProcessRet,
genParams,
parseRespStdout,
processResp,
} from './helper.js'
import { DataDownload } from './method/download.js'
import {
CpOptions,
DataCp,
DataSign,
DataStat,
DownloadOptions,
LinkOptions,
MkdirOptions,
MvOptions,
ProbUpOptions,
RmOptions,
RmrfOptions,
SignOptions,
StatOptions,
SyncLocalOptions,
SyncRemoteOptions,
UploadOptions,
} from './method/index.js'
import { SyncCloudOptions } from './method/sync.js'
import { processInputFnMap } from './process-input.js'
import { regxStat } from './rule.js'
import {
BaseOptions,
CmdKey,
Config,
ConfigPath,
DataBase,
DataKey,
FnKey,
Msg,
ProcessRet,
} from './types.js'
/**
* 阿里云 OSS 服务接口,
* 基于命令行工具 ossutil 封装
*/
export class OssClient {
debug = false
configPath = ''
private readonly config: Config | undefined = void 0
constructor(
/**
* 配置参数或者配置文件路径
* @default ~/.ossutilconfig
*/
protected readonly configInput?: Config | ConfigPath,
public cmd = 'ossutil',
) {
if (typeof configInput === 'string') {
this.configPath = configInput
const pathExists = statSync(this.configPath).isFile()
assert(pathExists, `${Msg.cloudConfigFileNotExists}: ${this.configPath}`)
}
else if (typeof configInput === 'object') {
this.config = configInput
}
else {
this.configPath = join(homedir(), '.ossutilconfig')
const pathExists = statSync(this.configPath).isFile()
assert(pathExists, `${Msg.cloudConfigFileNotExists}: ${this.configPath}`)
}
}
/**
* 删除 OSS 配置文件
*/
async destroy(): Promise<void> {
const { configPath } = this
await rm(configPath)
}
/**
* 在远程之间拷贝文件。
* 若 force 为空或者 false,且目标文件存在时会卡在命令行提示输入阶段(无显示)最后导致超时异常
* @note 下载文件使用 `download()`
* @link https://help.aliyun.com/document_detail/120057.html
*/
async cp(options: CpOptions): Promise<ProcessRet<DataCp>> {
if (! options.force) {
const statRet = await this.stat(options as StatOptions)
if (! statRet.exitCode) {
const ret: ProcessRet<DataCp> = {
exitCode: 1,
exitSignal: '',
stdout: '',
stderr: `${Msg.cloudFileAlreadyExists}: "${options.target}"`,
data: void 0,
}
return ret
}
}
const ret = await this.runner<CpOptions, DataCp>(options, FnKey.cp, cpKeys)
return ret
}
/**
* 创建软链接
* @link https://help.aliyun.com/document_detail/120059.html
*/
async createSymlink(options: LinkOptions): Promise<ProcessRet> {
const keys = [DataKey.elapsed]
const ret = await this.runner<CpOptions, DataCp>(options, FnKey.link, keys)
return ret
}
/**
* 下载远程文件到本地
* 若 force 为空或者 false,且目标文件存在时会卡在命令行提示输入阶段(无显示)最后导致超时异常
* @link https://help.aliyun.com/document_detail/120057.html
*/
async download(options: DownloadOptions): Promise<ProcessRet<DataDownload>> {
const ret = await this.runner<DownloadOptions, DataDownload>(options, FnKey.download, downloadKeys)
return ret
}
/**
* 创建目录
* @link https://help.aliyun.com/document_detail/120062.html
*/
async mkdir(options: MkdirOptions): Promise<ProcessRet> {
const keys: DataKey[] = [DataKey.elapsed]
const ret = await this.runner<MkdirOptions>(options, FnKey.mkdir, keys)
return ret
}
/**
* 移动云端的 OSS 对象
* 流程为先 `cp()` 然后 `rm()`
*/
async mv(options: MvOptions): Promise<ProcessRet<DataStat | DataBase>> {
const opts: MvOptions = {
...options,
encodeSource: true,
}
const cp = await this.cp(opts)
if (cp.exitCode) {
return cp
}
const opts2: RmOptions = {
...options,
target: options.src,
}
const remove = await this.rm(opts2)
if (remove.exitCode) {
return remove
}
const statRet = await this.stat(opts)
return statRet
}
/**
* OSS 远程路径是否存在
*/
async pathExists(options: StatOptions): Promise<boolean> {
const statRet = await this.stat(options)
const exists = !! (statRet.exitCode === 0 && statRet.data)
return exists
}
/**
* 探测上传状态
* @link https://help.aliyun.com/document_detail/120061.html
*/
async probeUpload(options: ProbUpOptions): Promise<ProcessRet> {
const keys = [DataKey.elapsed]
const ret = await this.runner<ProbUpOptions>(options, FnKey.probeUpload, keys)
return ret
}
/**
* 删除云对象,不支持删除 bucket 本身
* 如果在 recusive 为 false 时删除目录,则目录参数值必须以 '/' 结尾,否则不会删除成功
* @link https://help.aliyun.com/document_detail/120053.html
*/
async rm(options: RmOptions): Promise<ProcessRet> {
const keys = [DataKey.elapsed, DataKey.averageSpeed]
const ret = await this.runner<RmOptions>(options, FnKey.rm, keys)
return ret
}
/**
* 递归删除,相当于 `rm -rf`
* @link https://help.aliyun.com/document_detail/120053.html
*/
async rmrf(options: RmrfOptions): Promise<ProcessRet> {
const keys = [DataKey.elapsed, DataKey.averageSpeed]
const ret = await this.runner<RmrfOptions>(options, FnKey.rmrf, keys)
return ret
}
/**
* sign(生成签名URL)
* @link https://help.aliyun.com/document_detail/120064.html
*/
async sign(options: SignOptions): Promise<ProcessRet<DataSign>> {
const keys = [DataKey.elapsed, DataKey.httpUrl, DataKey.httpShareUrl]
const ret = await this.runner<SignOptions, DataSign>(options, FnKey.sign, keys)
if (ret.data?.httpUrl) {
ret.data.link = options.disableEncodeSlash
? ret.data.httpUrl
: decodeURIComponent(ret.data.httpUrl)
}
return ret
}
/**
* 查看 Bucket 和 Object 信息
* @link https://help.aliyun.com/document_detail/120054.html
*/
async stat(options: StatOptions): Promise<ProcessRet<DataStat>> {
const keys: DataKey[] = [DataKey.elapsed].concat(Array.from(regxStat.keys()))
const ret = await this.runner<StatOptions, DataStat>(options, FnKey.stat, keys)
return ret
}
/**
* 在 OSS 之间同步文件
* - force 参数默认 true
* - 若 force 为 false,且目标文件存在时会卡在命令行提示输入阶段(无显示)最后导致超时异常
* @link https://help.aliyun.com/document_detail/256354.html
*/
async syncCloud(options: SyncCloudOptions): Promise<ProcessRet<DataCp>> {
const ret = await this.runner<SyncCloudOptions, DataCp>(options, FnKey.syncCloud, cpKeys)
return ret
}
/**
* 同步 OSS 文件到本地
* - force 参数默认 true
* - 若 force 为 false,且目标文件存在时会卡在命令行提示输入阶段(无显示)最后导致超时异常
* @link https://help.aliyun.com/document_detail/256352.html
*/
async syncLocal(options: SyncLocalOptions): Promise<ProcessRet<DataCp>> {
const ret = await this.runner<SyncLocalOptions, DataCp>(options, FnKey.syncLocal, cpKeys)
return ret
}
/**
* 同步本地文件到 OSS
* - force 参数默认 true
* - 若 force 为 false,且目标文件存在时会卡在命令行提示输入阶段(无显示)最后导致超时异常
* @link https://help.aliyun.com/document_detail/193394.html
*/
async syncRemote(options: SyncRemoteOptions): Promise<ProcessRet<DataCp>> {
const ret = await this.runner<SyncRemoteOptions, DataCp>(options, FnKey.syncRemote, cpKeys)
return ret
}
/**
* 上传本地文件到 OSS
* 若 force 为空或者 false,且目标文件存在时会卡在命令行提示输入阶段(无显示)最后导致超时异常
* @link https://help.aliyun.com/document_detail/120057.html
*/
async upload(options: UploadOptions): Promise<ProcessRet<DataCp>> {
if (! options.force) {
const statRet = await this.stat(options as StatOptions)
if (! statRet.exitCode) {
const ret: ProcessRet<DataCp> = {
exitCode: 1,
exitSignal: '',
stdout: '',
stderr: `${Msg.cloudFileAlreadyExists}: "${options.target}"`,
data: void 0,
}
return ret
}
}
const ret = await this.runner<UploadOptions, DataCp>(options, FnKey.upload, cpKeys)
return ret
}
private async runner<T extends BaseOptions, R extends DataBase = DataBase>(
options: T,
fnKey: FnKey,
retKeys: DataKey[],
): Promise<ProcessRet<R>> {
assert(fnKey, 'fnKey is required')
assert(retKeys, 'retKeys is required')
assert(retKeys.length, 'retKeys must be an array')
// @ts-ignore
const cmdKey = CmdKey[fnKey] as CmdKey
assert(cmdKey, 'cmdKey is required')
const ps = await this.genCliParams(fnKey, options)
const resp$ = run(`${this.cmd} ${cmdKey} ${ps.join(' ')} `)
const res = await processResp(resp$, this.debug)
const data = parseRespStdout<R>(res, retKeys, this.debug)
const ret = combineProcessRet<R>(res, data)
return ret
}
private async genCliParams<T extends BaseOptions>(
fnKey: FnKey,
options: T,
): Promise<string[]> {
const func = processInputFnMap.get(fnKey)
assert(typeof func === 'function', `${fnKey} is not a function`)
const map = await func(options, this.config)
assert(map)
const ret = genParams(this.configPath, map)
return ret
}
}