@@ -3,11 +3,44 @@ import fs from 'fs'
33import FormData from 'form-data'
44import path from 'path' ;
55
6- const API_HOSTNAME = 'https://api.appcircle.io'
6+ let apiHostname = 'https://api.appcircle.io'
77export const appcircleApi = axios . create ( {
8- baseURL : API_HOSTNAME . endsWith ( '/' ) ? API_HOSTNAME : `${ API_HOSTNAME } /`
8+ baseURL : `${ apiHostname } /`
99} )
1010
11+ export function setApiEndpoint ( endpoint : string ) : void {
12+ if ( ! endpoint ) return
13+ apiHostname = endpoint . replace ( / \/ + $ / , '' )
14+ appcircleApi . defaults . baseURL = `${ apiHostname } /`
15+ }
16+
17+ async function uploadWithRetry (
18+ doUpload : ( ) => Promise < any > ,
19+ maxRetries = 5
20+ ) : Promise < any > {
21+ let attempt = 0
22+ let delay = 1000
23+ while ( true ) {
24+ try {
25+ return await doUpload ( )
26+ } catch ( error : any ) {
27+ const status = error ?. response ?. status
28+ const retryable =
29+ status === 503 ||
30+ error ?. code === 'ECONNRESET' ||
31+ ( typeof error ?. message === 'string' &&
32+ error . message . includes ( 'socket hang up' ) )
33+ if ( ! retryable || attempt >= maxRetries ) {
34+ throw error
35+ }
36+ attempt ++
37+ const jitter = Math . floor ( Math . random ( ) * 300 )
38+ await new Promise ( resolve => setTimeout ( resolve , delay + jitter ) )
39+ delay *= 2
40+ }
41+ }
42+ }
43+
1144export class UploadServiceHeaders {
1245 static token = ''
1346
@@ -37,6 +70,10 @@ export async function uploadArtifact(options: {
3770 const uploadInfoResponse = await appcircleApi . get < {
3871 fileId : string ;
3972 uploadUrl : string ;
73+ configuration ?: {
74+ httpMethod : string ;
75+ signParameters : Record < string , string > ;
76+ } ;
4077 } > (
4178 `distribution/v1/profiles/${ options . distProfileId } /app-versions` ,
4279 {
@@ -53,18 +90,31 @@ export async function uploadArtifact(options: {
5390 }
5491 console . log ( "File upload information retrieved successfully with status code:" , uploadInfoResponse . status )
5592
56- const { fileId, uploadUrl } = uploadInfoResponse . data ;
57-
58- const fileContent = fs . readFileSync ( filePath ) ;
93+ const { fileId, uploadUrl, configuration } = uploadInfoResponse . data ;
94+ const httpMethod = configuration ?. httpMethod ?. toUpperCase ( ) ?? 'PUT'
95+ const signParameters = configuration ?. signParameters ?? { }
5996
6097 console . log ( "Uploading file to Appcircle..." )
61- const uploadResponse = await axios . put ( uploadUrl , fileContent , {
62- headers : {
63- 'Content-Type' : 'application/octet-stream'
64- } ,
65- maxContentLength : Infinity ,
66- maxBodyLength : Infinity
67- } ) ;
98+ const uploadResponse = await uploadWithRetry ( ( ) => {
99+ if ( httpMethod === 'POST' ) {
100+ // Presigned POST (e.g. MinIO on self-hosted): sign params first, file LAST.
101+ const form = new FormData ( )
102+ for ( const [ key , value ] of Object . entries ( signParameters ) ) {
103+ form . append ( key , value )
104+ }
105+ form . append ( 'file' , fs . createReadStream ( filePath ) , fileName )
106+ return axios . post ( uploadUrl , form , {
107+ maxContentLength : Infinity ,
108+ maxBodyLength : Infinity ,
109+ headers : { ...form . getHeaders ( ) }
110+ } )
111+ }
112+ return axios . put ( uploadUrl , fs . readFileSync ( filePath ) , {
113+ headers : { 'Content-Type' : 'application/octet-stream' } ,
114+ maxContentLength : Infinity ,
115+ maxBodyLength : Infinity
116+ } )
117+ } )
68118 if ( uploadResponse . status < 200 || uploadResponse . status >= 300 ) {
69119 throw new Error ( "Failed to upload file with status code: " + uploadResponse . status )
70120 }
@@ -158,7 +208,7 @@ export async function checkTaskStatus(
158208 taskId : string ,
159209 currentAttempt = 0
160210) {
161- const response = await fetch ( `${ API_HOSTNAME } /task/v1/tasks/${ taskId } ` , {
211+ const response = await fetch ( `${ apiHostname } /task/v1/tasks/${ taskId } ` , {
162212 method : 'GET' ,
163213 headers : {
164214 'Content-Type' : 'application/json' ,
0 commit comments