Files
superwork/vscode/src/cc/handoffArchive.ts
T

117 lines
3.4 KiB
TypeScript

import { execFile, spawn } from 'node:child_process'
import { promises as fsp, statSync } from 'node:fs'
import * as path from 'node:path'
/** Gitea 工单附件上限。压缩后的 tgz 超过这个值,服务器会拒收。 */
export const HANDOFF_ATTACHMENT_MAX_BYTES = 100 * 1024 * 1024
export function formatByteSize(bytes: number): string {
const mb = bytes / (1024 * 1024)
if (mb >= 10)
return `${Math.round(mb)}MB`
if (mb >= 1)
return `${mb.toFixed(1)}MB`
const kb = bytes / 1024
if (kb >= 10)
return `${Math.round(kb)}KB`
if (kb >= 1)
return `${kb.toFixed(1)}KB`
return `${bytes}B`
}
export class HandoffArchiveTooLargeError extends Error {
readonly packedBytes: number
readonly limitBytes: number
constructor(packedBytes: number, limitBytes: number) {
super(`压缩后超过 Gitea 附件上限 ${formatByteSize(limitBytes)}`)
this.name = 'HandoffArchiveTooLargeError'
this.packedBytes = packedBytes
this.limitBytes = limitBytes
}
}
function tarUnpack(args: string[]): Promise<void> {
return new Promise((resolve, reject) => {
execFile('tar', args, { timeout: 120_000 }, (err, _stdout, stderr) => {
if (err) {
reject(new Error(`tar ${args[0]} 失败: ${(stderr ?? '').toString().trim() || err.message}`))
return
}
resolve()
})
})
}
export async function packHandoff(
stagingDir: string,
outFile: string,
opts?: { maxBytes?: number, onProgress?: (packedBytes: number) => void },
): Promise<void> {
const maxBytes = opts?.maxBytes ?? HANDOFF_ATTACHMENT_MAX_BYTES
await fsp.mkdir(path.dirname(outFile), { recursive: true })
await new Promise<void>((resolve, reject) => {
const child = spawn('tar', ['-czf', outFile, '-C', stagingDir, '.'], { stdio: ['ignore', 'ignore', 'pipe'] })
const decode = new TextDecoder()
let stderr = ''
let settled = false
let poll: ReturnType<typeof setInterval> | undefined
child.stderr?.on('data', (chunk: Uint8Array) => {
stderr += decode.decode(chunk, { stream: true })
})
const finish = (err?: Error): void => {
if (settled)
return
settled = true
if (poll)
clearInterval(poll)
if (err)
reject(err)
else
resolve()
}
const checkSize = (): void => {
let size = 0
try {
size = statSync(outFile).size
}
catch {
return
}
opts?.onProgress?.(size)
// 产出一超过上限就杀掉。等 tar 自己打完再失败,大会话会卡很久。
if (size > maxBytes) {
child.kill('SIGKILL')
finish(new HandoffArchiveTooLargeError(size, maxBytes))
}
}
poll = setInterval(checkSize, 200)
child.on('error', err => finish(err))
child.on('close', (code) => {
if (settled)
return
let size = 0
try {
size = statSync(outFile).size
}
catch {
// 包还没写出来
}
opts?.onProgress?.(size)
if (size > maxBytes) {
finish(new HandoffArchiveTooLargeError(size, maxBytes))
return
}
if (code === 0)
finish()
else
finish(new Error(`tar -czf 失败: ${stderr.trim() || `exit ${code}`}`))
})
})
}
export async function unpackHandoff(archive: string, dstDir: string): Promise<void> {
await fsp.mkdir(dstDir, { recursive: true })
await tarUnpack(['-xzf', archive, '-C', dstDir])
}