| 1 | import { basename } from 'node:path' |
| 2 | import { Injectable, Logger } from '@nestjs/common' |
| 3 | import { AppException, CommonResponse, ResponseCode } from '@yikart/common' |
| 4 | import { AssetType } from '@yikart/mongodb' |
| 5 | import axios, { AxiosRequestConfig } from 'axios' |
| 6 | import { config } from '../../../config' |
| 7 | |
| 8 | interface UploadSignResult { |
| 9 | id: string |
| 10 | path: string |
| 11 | url: string |
| 12 | uploadUrl: string |
| 13 | } |
| 14 | |
| 15 | @Injectable() |
| 16 | export class RelayClientService { |
| 17 | private readonly logger = new Logger(RelayClientService.name) |
| 18 | |
| 19 | get enabled() { |
| 20 | return !!config.relay |
| 21 | } |
| 22 | |
| 23 | async get<T>(path: string, params?: Record<string, any>): Promise<T> { |
| 24 | return this.request<T>({ method: 'GET', url: path, params }) |
| 25 | } |
| 26 | |
| 27 | async post<T>(path: string, data?: unknown): Promise<T> { |
| 28 | return this.request<T>({ method: 'POST', url: path, data }) |
| 29 | } |
| 30 | |
| 31 | async patch<T>(path: string, data?: unknown): Promise<T> { |
| 32 | return this.request<T>({ method: 'PATCH', url: path, data }) |
| 33 | } |
| 34 | |
| 35 | async delete<T>(path: string, data?: unknown): Promise<T> { |
| 36 | return this.request<T>({ method: 'DELETE', url: path, data }) |
| 37 | } |
| 38 | |
| 39 | async uploadFileFromLocalUrl(localUrl: string): Promise<string> { |
| 40 | const filename = basename(new URL(localUrl).pathname) |
| 41 | |
| 42 | const fileResponse = await axios.get(localUrl, { responseType: 'arraybuffer' }) |
| 43 | const contentType = fileResponse.headers['content-type'] || 'application/octet-stream' |
| 44 | const size = (fileResponse.data as ArrayBuffer).byteLength |
| 45 | |
| 46 | const signResult = await this.post<UploadSignResult>('/assets/uploadSign', { |
| 47 | filename, |
| 48 | type: AssetType.PublishMedia, |
| 49 | size, |
| 50 | }) |
| 51 | |
| 52 | if (!signResult?.uploadUrl) { |
| 53 | throw new Error(`uploadSign returned no uploadUrl: ${JSON.stringify(signResult)}`) |
| 54 | } |
| 55 | |
| 56 | await axios.put(signResult.uploadUrl, fileResponse.data, { |
| 57 | headers: { 'Content-Type': contentType }, |
| 58 | }) |
| 59 | |
| 60 | await this.post(`/assets/${signResult.id}/confirm`, {}) |
| 61 | |
| 62 | return signResult.url |
| 63 | } |
| 64 | |
| 65 | private async request<T>(options: AxiosRequestConfig): Promise<T> { |
| 66 | if (!config.relay) { |
| 67 | throw new AppException(ResponseCode.RelayServerUnavailable) |
| 68 | } |
| 69 | |
| 70 | try { |
| 71 | const response = await axios<CommonResponse<T>>({ |
| 72 | ...options, |
| 73 | url: `${config.relay.serverUrl}${options.url}`, |
| 74 | headers: { |
| 75 | ...options.headers, |
| 76 | 'x-api-key': config.relay.apiKey, |
| 77 | }, |
| 78 | }) |
| 79 | if (response.data.code !== 0) { |
| 80 | this.logger.error({ message: 'Relay API returned error', url: options.url, code: response.data.code, relayMessage: response.data.message }) |
| 81 | throw new Error(`Relay API error [${response.data.code}]: ${response.data.message}`) |
| 82 | } |
| 83 | return response.data.data as T |
| 84 | } |
| 85 | catch (error) { |
| 86 | this.logger.error(error, `Relay request failed: ${options.url}`) |
| 87 | throw new AppException(ResponseCode.RelayServerUnavailable) |
| 88 | } |
| 89 | } |
| 90 | } |
| 91 |