qgOssPut.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. /**
  2. * OSS 上传:直连阿里云(ali-oss SDK 自行签名);乐跑学校 API 仍经 RunForge 代理。
  3. */
  4. const OSS = require('ali-oss')
  5. const { forwardRequest } = require('../ProxyForwardClient')
  6. function ossLogLabel(traceId, taskId) {
  7. let s = ''
  8. if (traceId) s += `[${traceId}] `
  9. s += '[ossPut]'
  10. if (taskId) s += ` [${taskId}]`
  11. return s
  12. }
  13. function buildOssClient(sts, timeoutMs) {
  14. return new OSS({
  15. bucket: sts.bucket,
  16. region: sts.region || 'oss-cn-hangzhou',
  17. accessKeyId: sts.AccessKeyId,
  18. accessKeySecret: sts.AccessKeySecret,
  19. stsToken: sts.SecurityToken,
  20. secure: true,
  21. timeout: timeoutMs
  22. })
  23. }
  24. /** ali-oss urllib 期望 { status, statusCode, headers, data, res } */
  25. function toUrllibResponse(resp) {
  26. const status = resp?.status ?? resp?.statusCode
  27. if (status == null) {
  28. const err = new Error('代理转发未返回有效 HTTP status')
  29. err.code = 'PROXY_FORWARD_BAD_RESPONSE'
  30. throw err
  31. }
  32. let data = resp.data
  33. if (data == null || data === '') {
  34. data = Buffer.alloc(0)
  35. } else if (!Buffer.isBuffer(data)) {
  36. data = Buffer.from(typeof data === 'string' ? data : JSON.stringify(data))
  37. }
  38. const headers = resp.headers || {}
  39. return {
  40. status,
  41. statusCode: status,
  42. headers,
  43. data,
  44. res: { statusCode: status, headers }
  45. }
  46. }
  47. function patchOssClientForForward(client, { outboundMode, timeout, logger, logPrefix, scene, ossPath }) {
  48. const urllibMod = client.urllib
  49. const originalRequest = urllibMod.request.bind(urllibMod)
  50. async function runForward(reqUrl, reqOpt) {
  51. const method = (reqOpt.method || 'GET').toUpperCase()
  52. const headers = reqOpt.headers || {}
  53. const body = reqOpt.content ?? reqOpt.body
  54. if (outboundMode === 'direct') {
  55. return originalRequest(reqUrl, reqOpt)
  56. }
  57. try {
  58. const resp = await forwardRequest({
  59. method,
  60. url: reqUrl,
  61. data: body,
  62. headers,
  63. timeout: reqOpt.timeout || timeout,
  64. outboundMode,
  65. logger,
  66. logPrefix,
  67. scene: scene || 'oss_put',
  68. responseType: 'arraybuffer',
  69. validateStatus: () => true
  70. })
  71. return toUrllibResponse(resp)
  72. } catch (e) {
  73. if (outboundMode === 'proxy') throw e
  74. logger?.warn?.(`${logPrefix} OSS 经代理失败,改 SDK 直连: ${e.message || e}`)
  75. return originalRequest(reqUrl, reqOpt)
  76. }
  77. }
  78. // 兼容 urllib 的 callback / Promise 双模式,避免 ali-oss 收到 undefined status
  79. urllibMod.request = function patchedRequest(url, args, callback) {
  80. if (arguments.length === 2 && typeof args === 'function') {
  81. callback = args
  82. args = null
  83. }
  84. args = args || {}
  85. if (typeof callback === 'function') {
  86. runForward(url, args).then(
  87. (result) => {
  88. if (!result || result.status == null) {
  89. callback(new Error('OSS HTTP 响应缺少 status'))
  90. return
  91. }
  92. callback(null, result.data, result.res)
  93. },
  94. (err) => callback(err)
  95. )
  96. return
  97. }
  98. return runForward(url, args)
  99. }
  100. logger?.info?.(`${logPrefix} PUT ${ossPath} mode=${outboundMode}`)
  101. }
  102. /**
  103. * @param {object} sts OSS STS 凭证
  104. * @param {string} ossPath 对象 key
  105. * @param {Buffer|string} content
  106. * @param {{ logger?: object, traceId?: string, taskId?: string, outboundMode?: 'auto'|'direct'|'proxy', timeout?: number }} options
  107. */
  108. async function putOssWithQgOutbound(sts, ossPath, content, options = {}) {
  109. const {
  110. logger = null,
  111. traceId = null,
  112. taskId = null,
  113. outboundMode = 'direct',
  114. timeout = 60000
  115. } = options
  116. const logPrefix = () => ossLogLabel(traceId, taskId)
  117. if (outboundMode === 'direct') {
  118. logger?.info?.(`${logPrefix()} PUT 直连 ${ossPath}`)
  119. const client = buildOssClient(sts, timeout)
  120. return client.put(ossPath, content)
  121. }
  122. const client = buildOssClient(sts, timeout)
  123. patchOssClientForForward(client, {
  124. outboundMode,
  125. timeout,
  126. logger,
  127. logPrefix: logPrefix(),
  128. scene: 'oss_put',
  129. ossPath
  130. })
  131. return client.put(ossPath, content)
  132. }
  133. module.exports = {
  134. putOssWithQgOutbound
  135. }