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 { 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 { 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 { 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 const savedKey = body['key'] ?? key const domain = Uploader.normalizeDomain(config.domain) return `${domain}/${savedKey}` } finally { httpRequest.destroy() } } }