Uploader.ets 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. import { http } from '@kit.NetworkKit'
  2. import util from '@ohos.util'
  3. import fs from '@ohos.file.fs'
  4. import { Base64Util, DateUtil, FileUtil, LogUtil } from '@pura/harmony-utils'
  5. export interface UploadConfig {
  6. uploadToken: string; // UpToken
  7. domain: string; // 例如 https://your.cdn.domain
  8. keyPrefix?: string; // 例如 logs/ttmusic
  9. uploadHost?: string; // 可覆盖上传Host,不传使用默认美图云 Host
  10. }
  11. export class Uploader {
  12. private static readonly DEFAULT_UPLOAD_HOST = 'https://up.meitudata.com'
  13. private static readonly BACKUP_UPLOAD_HOST = 'https://upload.meitudata.com'
  14. /**
  15. * 检查网络连接和DNS解析
  16. */
  17. private static async checkNetworkConnectivity(host: string): Promise<boolean> {
  18. try {
  19. const httpRequest = http.createHttp()
  20. const response = await httpRequest.request(`${host}/`, {
  21. method: http.RequestMethod.GET,
  22. connectTimeout: 8000,
  23. readTimeout: 10000,
  24. expectDataType: http.HttpDataType.STRING,
  25. header: {
  26. 'User-Agent': 'TTMusic-LogCollector/1.0'
  27. }
  28. })
  29. httpRequest.destroy()
  30. return response.responseCode >= 200 && response.responseCode < 500
  31. } catch (error) {
  32. LogUtil.warn('Uploader', `网络连接检查失败(${host}): ${(error as Error).message}`)
  33. return false
  34. }
  35. }
  36. static async uploadFile(filePath: string, config: UploadConfig): Promise<string> {
  37. const stat = FileUtil.lstatSync(filePath)
  38. const file = FileUtil.openSync(filePath, fs.OpenMode.READ_ONLY)
  39. try {
  40. const buffer = new ArrayBuffer(stat.size)
  41. FileUtil.readSync(file.fd, buffer, { offset: 0, length: stat.size })
  42. const u8 = new Uint8Array(buffer)
  43. const contentBase64 = Base64Util.encodeToStrSync(u8)
  44. const key = Uploader.buildKey(filePath, config)
  45. const keyBase64 = Uploader.toUrlSafeBase64(key)
  46. const mainHost = config.uploadHost ?? Uploader.DEFAULT_UPLOAD_HOST
  47. LogUtil.info('Uploader', `开始上传文件: ${filePath}, 大小: ${stat.size} bytes`)
  48. LogUtil.info('Uploader', `使用上传主机: ${mainHost}`)
  49. // 对于 401 错误,先尝试直接上传不做网络预检查
  50. try {
  51. LogUtil.info('Uploader', `直接尝试主主机上传: ${mainHost}`)
  52. return await Uploader.doUpload(mainHost, stat.size, key, keyBase64, contentBase64, config)
  53. } catch (err) {
  54. LogUtil.warn('Uploader', `主上传Host失败(${mainHost}): ${(err as Error).message},尝试预检查后使用备用`)
  55. // 预检查主机的网络连接性
  56. const backupHostReachable = await Uploader.checkNetworkConnectivity(Uploader.BACKUP_UPLOAD_HOST)
  57. if (!backupHostReachable) {
  58. throw new Error(`上传失败且备用主机不可达: ${(err as Error).message}`)
  59. }
  60. LogUtil.info('Uploader', `使用备用主机上传: ${Uploader.BACKUP_UPLOAD_HOST}`)
  61. return await Uploader.doUpload(Uploader.BACKUP_UPLOAD_HOST, stat.size, key, keyBase64, contentBase64, config)
  62. }
  63. } catch (err) {
  64. LogUtil.error('Uploader', `上传异常: ${(err as Error).message}`)
  65. throw new Error((err as Error).message)
  66. } finally {
  67. FileUtil.closeSync(file.fd)
  68. }
  69. }
  70. private static buildKey(filePath: string, config: UploadConfig): string {
  71. const prefix = config.keyPrefix ?? 'ttmusic-logs'
  72. const filename = FileUtil.getFileName(filePath)
  73. const timestamp = DateUtil.getTodayStr('yyyyMMdd_HHmmss')
  74. return `${prefix}/${timestamp}_${filename}`
  75. }
  76. private static toUrlSafeBase64(text: string): string {
  77. const encoder = new util.TextEncoder()
  78. const u8 = encoder.encode(text)
  79. const base64 = Base64Util.encodeToStrSync(u8)
  80. return base64.replace(/\+/g, '-').replace(/\//g, '_')
  81. }
  82. private static normalizeDomain(domain: string): string {
  83. if (domain.endsWith('/')) {
  84. return domain.slice(0, -1)
  85. }
  86. return domain
  87. }
  88. private static async doUpload(host: string, size: number, key: string, keyBase64: string, contentBase64: string, config: UploadConfig): Promise<string> {
  89. const url = `${host}/putb64/${size}/key/${encodeURIComponent(keyBase64)}`
  90. // 调试信息
  91. LogUtil.info('Uploader', `上传URL: ${url}`)
  92. LogUtil.info('Uploader', `上传Key: ${key}`)
  93. LogUtil.info('Uploader', `Token前缀: ${config.uploadToken.substring(0, 20)}...`)
  94. const httpRequest = http.createHttp()
  95. try {
  96. const requestOptions: http.HttpRequestOptions = {
  97. method: http.RequestMethod.POST,
  98. connectTimeout: 10000,
  99. readTimeout: 20000,
  100. expectDataType: http.HttpDataType.STRING,
  101. header: {
  102. 'Content-Type': 'application/octet-stream',
  103. 'Authorization': `UpToken ${config.uploadToken}`,
  104. 'User-Agent': 'TTMusic-LogCollector/1.0'
  105. },
  106. extraData: contentBase64
  107. }
  108. LogUtil.info('Uploader', `请求头: Authorization = UpToken ${config.uploadToken.substring(0, 20)}...`)
  109. const response = await httpRequest.request(url, requestOptions)
  110. LogUtil.info('Uploader', `响应状态: ${response.responseCode}`)
  111. if (response.result) {
  112. LogUtil.info('Uploader', `响应内容: ${response.result}`)
  113. }
  114. if (response.responseCode !== 200) {
  115. throw new Error(`上传失败: HTTP ${response.responseCode}`)
  116. }
  117. const body = JSON.parse(response.result as string) as Record<string, string>
  118. const savedKey = body['key'] ?? key
  119. const domain = Uploader.normalizeDomain(config.domain)
  120. return `${domain}/${savedKey}`
  121. } finally {
  122. httpRequest.destroy()
  123. }
  124. }
  125. }