| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142 |
- import { http } from '@kit.NetworkKit'
- import util from '@ohos.util'
- import fs from '@ohos.file.fs'
- import { Base64Util, DateUtil, FileUtil, LogUtil } from '@pura/harmony-utils'
- export interface UploadConfig {
- uploadToken: string; // UpToken
- domain: string; // 例如 https://your.cdn.domain
- keyPrefix?: string; // 例如 logs/ttmusic
- uploadHost?: string; // 可覆盖上传Host,不传使用默认美图云 Host
- }
- export class Uploader {
- private static readonly DEFAULT_UPLOAD_HOST = 'https://up.meitudata.com'
- private static readonly BACKUP_UPLOAD_HOST = 'https://upload.meitudata.com'
- /**
- * 检查网络连接和DNS解析
- */
- private static async checkNetworkConnectivity(host: string): Promise<boolean> {
- try {
- const httpRequest = http.createHttp()
- const response = await httpRequest.request(`${host}/`, {
- method: http.RequestMethod.GET,
- connectTimeout: 8000,
- readTimeout: 10000,
- expectDataType: http.HttpDataType.STRING,
- header: {
- 'User-Agent': 'TTMusic-LogCollector/1.0'
- }
- })
- httpRequest.destroy()
- return response.responseCode >= 200 && response.responseCode < 500
- } catch (error) {
- LogUtil.warn('Uploader', `网络连接检查失败(${host}): ${(error as Error).message}`)
- return false
- }
- }
- static async uploadFile(filePath: string, config: UploadConfig): Promise<string> {
- const stat = FileUtil.lstatSync(filePath)
- const file = FileUtil.openSync(filePath, fs.OpenMode.READ_ONLY)
- try {
- const buffer = new ArrayBuffer(stat.size)
- FileUtil.readSync(file.fd, buffer, { offset: 0, length: stat.size })
- const u8 = new Uint8Array(buffer)
- const contentBase64 = Base64Util.encodeToStrSync(u8)
- const key = Uploader.buildKey(filePath, config)
- const keyBase64 = Uploader.toUrlSafeBase64(key)
- const mainHost = config.uploadHost ?? Uploader.DEFAULT_UPLOAD_HOST
- LogUtil.info('Uploader', `开始上传文件: ${filePath}, 大小: ${stat.size} bytes`)
- LogUtil.info('Uploader', `使用上传主机: ${mainHost}`)
- // 对于 401 错误,先尝试直接上传不做网络预检查
- try {
- LogUtil.info('Uploader', `直接尝试主主机上传: ${mainHost}`)
- return await Uploader.doUpload(mainHost, stat.size, key, keyBase64, contentBase64, config)
- } catch (err) {
- LogUtil.warn('Uploader', `主上传Host失败(${mainHost}): ${(err as Error).message},尝试预检查后使用备用`)
- // 预检查主机的网络连接性
- const backupHostReachable = await Uploader.checkNetworkConnectivity(Uploader.BACKUP_UPLOAD_HOST)
- if (!backupHostReachable) {
- throw new Error(`上传失败且备用主机不可达: ${(err as Error).message}`)
- }
- LogUtil.info('Uploader', `使用备用主机上传: ${Uploader.BACKUP_UPLOAD_HOST}`)
- return await Uploader.doUpload(Uploader.BACKUP_UPLOAD_HOST, stat.size, key, keyBase64, contentBase64, config)
- }
- } catch (err) {
- LogUtil.error('Uploader', `上传异常: ${(err as Error).message}`)
- throw new Error((err as Error).message)
- } finally {
- FileUtil.closeSync(file.fd)
- }
- }
- private static buildKey(filePath: string, config: UploadConfig): string {
- const prefix = config.keyPrefix ?? 'ttmusic-logs'
- const filename = FileUtil.getFileName(filePath)
- const timestamp = DateUtil.getTodayStr('yyyyMMdd_HHmmss')
- return `${prefix}/${timestamp}_${filename}`
- }
- private static toUrlSafeBase64(text: string): string {
- const encoder = new util.TextEncoder()
- const u8 = encoder.encode(text)
- const base64 = Base64Util.encodeToStrSync(u8)
- return base64.replace(/\+/g, '-').replace(/\//g, '_')
- }
- private static normalizeDomain(domain: string): string {
- if (domain.endsWith('/')) {
- return domain.slice(0, -1)
- }
- return domain
- }
- private static async doUpload(host: string, size: number, key: string, keyBase64: string, contentBase64: string, config: UploadConfig): Promise<string> {
- const url = `${host}/putb64/${size}/key/${encodeURIComponent(keyBase64)}`
- // 调试信息
- LogUtil.info('Uploader', `上传URL: ${url}`)
- LogUtil.info('Uploader', `上传Key: ${key}`)
- LogUtil.info('Uploader', `Token前缀: ${config.uploadToken.substring(0, 20)}...`)
- const httpRequest = http.createHttp()
- try {
- const requestOptions: http.HttpRequestOptions = {
- method: http.RequestMethod.POST,
- connectTimeout: 10000,
- readTimeout: 20000,
- expectDataType: http.HttpDataType.STRING,
- header: {
- 'Content-Type': 'application/octet-stream',
- 'Authorization': `UpToken ${config.uploadToken}`,
- 'User-Agent': 'TTMusic-LogCollector/1.0'
- },
- extraData: contentBase64
- }
- LogUtil.info('Uploader', `请求头: Authorization = UpToken ${config.uploadToken.substring(0, 20)}...`)
- const response = await httpRequest.request(url, requestOptions)
- LogUtil.info('Uploader', `响应状态: ${response.responseCode}`)
- if (response.result) {
- LogUtil.info('Uploader', `响应内容: ${response.result}`)
- }
- if (response.responseCode !== 200) {
- throw new Error(`上传失败: HTTP ${response.responseCode}`)
- }
- const body = JSON.parse(response.result as string) as Record<string, string>
- const savedKey = body['key'] ?? key
- const domain = Uploader.normalizeDomain(config.domain)
- return `${domain}/${savedKey}`
- } finally {
- httpRequest.destroy()
- }
- }
- }
|