diff --git a/unitylibs/core/workflow/workflow-acrobat/action-binder.js b/unitylibs/core/workflow/workflow-acrobat/action-binder.js index 2c50b7db..649a8d75 100644 --- a/unitylibs/core/workflow/workflow-acrobat/action-binder.js +++ b/unitylibs/core/workflow/workflow-acrobat/action-binder.js @@ -91,6 +91,7 @@ export default class ActionBinder { pre_upload_error_create_asset: -55, pre_upload_error_missing_verb_config: -56, pre_upload_error_transition_screen: -57, + pre_upload_error_direct_upload: -58, validation_error_validate_files: -100, validation_error_unsupported_type: -101, validation_error_empty_file: -102, @@ -234,6 +235,7 @@ export default class ActionBinder { createAsset: `${base}/asset`, finalizeAsset: `${base}/asset/finalize`, getMetadata: `${base}/asset/metadata`, + directUpload: `${base}/asset/upload`, }; unityConfig.connectorApiEndPoint = `${base}/asset/connector`; return unityConfig; diff --git a/unitylibs/core/workflow/workflow-acrobat/target-config.json b/unitylibs/core/workflow/workflow-acrobat/target-config.json index ab7f56ad..13d07591 100644 --- a/unitylibs/core/workflow/workflow-acrobat/target-config.json +++ b/unitylibs/core/workflow/workflow-acrobat/target-config.json @@ -14,6 +14,9 @@ }, "sendSplunkAnalytics": true, "verbsWithoutMfuToSfuFallback": ["compress-pdf"], + "awaitFinalizeVerbs": ["word-to-pdf"], + "directUploadVerbs": ["word-to-pdf"], + "directUploadMaxSize": 1048576, "nonpdfMfuFeedbackScreenTypeNonpdf": ["combine-pdf"], "nonpdfSfuProductScreen": ["word-to-pdf", "jpg-to-pdf", "ppt-to-pdf", "excel-to-pdf", "png-to-pdf", "createpdf", "chat-pdf", "chat-pdf-student", "summarize-pdf", "pdf-ai", "heic-to-pdf", "quiz-maker", "flashcard-maker", "mindmap-maker"], "mfuUploadAllowed": ["combine-pdf", "rotate-pages", "chat-pdf", "chat-pdf-student", "summarize-pdf", "pdf-ai", "quiz-maker", "flashcard-maker", "mindmap-maker"], diff --git a/unitylibs/core/workflow/workflow-acrobat/upload-handler.js b/unitylibs/core/workflow/workflow-acrobat/upload-handler.js index ba04bef4..3f2fbf2c 100644 --- a/unitylibs/core/workflow/workflow-acrobat/upload-handler.js +++ b/unitylibs/core/workflow/workflow-acrobat/upload-handler.js @@ -19,6 +19,77 @@ export default class UploadHandler { return feature === 'pdf-ai' ? 'chat-pdf-pdf-ai' : feature; } + isDirectUpload(file) { + const verb = this.actionBinder.workflowCfg.enabledFeatures[0]; + const directUploadVerbs = this.actionBinder.workflowCfg.targetCfg.directUploadVerbs || []; + const directUploadMaxSize = this.actionBinder.workflowCfg.targetCfg.directUploadMaxSize || 0; + return directUploadVerbs.includes(verb) && file.size <= directUploadMaxSize; + } + + async directUploadAsset(file, signal, workflowId = null) { + const formData = new FormData(); + formData.append('surfaceId', unityConfig.surfaceId); + formData.append('targetProduct', this.actionBinder.workflowCfg.productName); + formData.append('name', file.name); + formData.append('size', file.size); + formData.append('format', 'application/pdf'); + formData.append('file', file); + if (workflowId) formData.append('workflowId', workflowId); + const opts = await getApiCallOptions( + 'POST', + unityConfig.apiKey, + this.actionBinder.getAdditionalHeaders() || {}, + { body: formData, signal }, + ); + delete opts.headers['Content-Type']; + const { response, attempt } = await this.networkUtils.fetchFromServiceWithRetry( + this.actionBinder.acrobatApiConfig.acrobatEndpoint.directUpload, + opts, + this.actionBinder.workflowCfg.targetCfg.fetchApiConfig.default, + ); + return { ...response, attempt }; + } + + async directUploadSingleFile(file, fileData, isPdf = true) { + const abortSignal = this.actionBinder.getAbortSignal(); + this.actionBinder.dispatchAnalyticsEvent('uploading', fileData); + this.actionBinder.setIsUploading(true); + let assetData; + try { + assetData = await this.directUploadAsset(file, abortSignal); + } catch (error) { + this.initSplashScreen(); + await this.transitionScreen.showSplashScreen(); + this.handleUploadError(error, 'pre_upload_error_direct_upload'); + return false; + } + fileData.assetId = assetData.id; + this.actionBinder.setAssetId(assetData.id); + const effectiveFileType = await this.getEffectiveFileType(file); + const cOpts = { + assetId: assetData.id, + targetProduct: this.actionBinder.workflowCfg.productName, + payload: { + languageRegion: this.actionBinder.workflowCfg.langRegion, + languageCode: this.actionBinder.workflowCfg.langCode, + verb: this.getVerbForFeature(), + assetMetadata: { [assetData.id]: { name: file.name, size: file.size, type: effectiveFileType } }, + ...(!isPdf ? { feedback: 'nonpdf' } : {}), + }, + }; + const redirectSuccess = await this.actionBinder.handleRedirect(cOpts, fileData); + if (!redirectSuccess) return false; + + this.actionBinder.operations.push(assetData.id); + this.actionBinder.uploadTimestamp = Date.now(); + this.actionBinder.dispatchAnalyticsEvent('uploaded', { + ...fileData, + assetId: assetData.id, + maxRetryCount: assetData.attempt || 0, + }); + return true; + } + async getEffectiveFileType(file) { const { getExtension } = await import('../../../utils/FileUtils.js'); const isHeicWithoutMimeType = this.actionBinder.workflowCfg.enabledFeatures[0] === 'heic-to-pdf' @@ -198,7 +269,22 @@ export default class UploadHandler { return { failedFiles, attemptMap }; } + isAwaitFinalizeVerb(verb) { + const awaitFinalizeVerbs = this.actionBinder.workflowCfg.targetCfg.awaitFinalizeVerbs || []; + return awaitFinalizeVerbs.includes(verb); + } + + getFinalizeRetryConfig(verb) { + const baseConfig = this.actionBinder.workflowCfg.targetCfg.fetchApiConfig.finalizeAsset; + if (this.isAwaitFinalizeVerb(verb)) { + return { ...baseConfig, retryOn202: false }; + } + return baseConfig; + } + async verifyContent(assetData, signal) { + const verb = this.actionBinder.workflowCfg.enabledFeatures[0]; + const finalizeRetryConfig = this.getFinalizeRetryConfig(verb); try { const finalAssetData = { surfaceId: unityConfig.surfaceId, @@ -214,7 +300,8 @@ export default class UploadHandler { const finalizeJson = await this.networkUtils.fetchFromServiceWithRetry( this.actionBinder.acrobatApiConfig.acrobatEndpoint.finalizeAsset, finalizeOpts, - this.actionBinder.workflowCfg.targetCfg.fetchApiConfig.finalizeAsset, + finalizeRetryConfig, + (responseJson) => responseJson, ); if (!finalizeJson || Object.keys(finalizeJson).length !== 0) { if (this.actionBinder.MULTI_FILE) { @@ -338,6 +425,11 @@ export default class UploadHandler { } async uploadSingleFile(file, fileData, isPdf = true) { + if (this.isDirectUpload(file)) { + const success = await this.directUploadSingleFile(file, fileData, isPdf); + if (success) return; + } + const { maxConcurrentChunks } = this.getConcurrentLimits(); const abortSignal = this.actionBinder.getAbortSignal(); let cOpts = {}; diff --git a/unitylibs/utils/NetworkUtils.js b/unitylibs/utils/NetworkUtils.js index 555628b9..20f7dff1 100644 --- a/unitylibs/utils/NetworkUtils.js +++ b/unitylibs/utils/NetworkUtils.js @@ -122,6 +122,14 @@ export default class NetworkUtils { } } + shouldRetryPollingRequest(status, retryConfig, customRetryCheckResult) { + if (customRetryCheckResult) return true; + if (status >= 500 && status < 600) return true; + if (status === 429) return true; + if (status === 202 && retryConfig.retryOn202 !== false) return true; + return false; + } + async fetchFromServiceWithServerPollingRetry(url, options, retryConfig, onSuccess, onError) { const maxRetryDelay = retryConfig.retryParams?.maxRetryDelay || 300000; let timeLapsed = 0; @@ -133,7 +141,7 @@ export default class NetworkUtils { const { status, headers } = response; const responseJson = await this.getResponseJson(response); const customRetryCheckResult = retryConfig.extraRetryCheck && await retryConfig.extraRetryCheck(status, responseJson); - if (customRetryCheckResult || status === 202 || (status >= 500 && status < 600) || status === 429) { + if (this.shouldRetryPollingRequest(status, retryConfig, customRetryCheckResult)) { const retryDelay = (parseInt(headers.get('retry-after'), 10) * 1000) || retryConfig.retryParams?.defaultRetryDelay || 5000; await new Promise((resolve) => { setTimeout(resolve, retryDelay); }); timeLapsed += retryDelay;