diff options
Diffstat (limited to 'apps/files/src/actions/convertUtils.ts')
-rw-r--r-- | apps/files/src/actions/convertUtils.ts | 139 |
1 files changed, 139 insertions, 0 deletions
diff --git a/apps/files/src/actions/convertUtils.ts b/apps/files/src/actions/convertUtils.ts new file mode 100644 index 00000000000..0ace3747d9c --- /dev/null +++ b/apps/files/src/actions/convertUtils.ts @@ -0,0 +1,139 @@ +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import type { AxiosResponse, AxiosError } from '@nextcloud/axios' +import type { OCSResponse } from '@nextcloud/typings/ocs' + +import { emit } from '@nextcloud/event-bus' +import { generateOcsUrl } from '@nextcloud/router' +import { showError, showLoading, showSuccess } from '@nextcloud/dialogs' +import { n, t } from '@nextcloud/l10n' +import axios, { isAxiosError } from '@nextcloud/axios' +import PQueue from 'p-queue' + +import { fetchNode } from '../services/WebdavClient.ts' +import logger from '../logger' + +type ConversionResponse = { + path: string + fileId: number +} + +interface PromiseRejectedResult<T> { + status: 'rejected' + reason: T +} + +type PromiseSettledResult<T, E> = PromiseFulfilledResult<T> | PromiseRejectedResult<E>; +type ConversionSuccess = AxiosResponse<OCSResponse<ConversionResponse>> +type ConversionError = AxiosError<OCSResponse<ConversionResponse>> + +const queue = new PQueue({ concurrency: 5 }) +const requestConversion = function(fileId: number, targetMimeType: string): Promise<AxiosResponse> { + return axios.post(generateOcsUrl('/apps/files/api/v1/convert'), { + fileId, + targetMimeType, + }) +} + +export const convertFiles = async function(fileIds: number[], targetMimeType: string) { + const conversions = fileIds.map(fileId => queue.add(() => requestConversion(fileId, targetMimeType))) + + // Start conversion + const toast = showLoading(t('files', 'Converting files …')) + + // Handle results + try { + const results = await Promise.allSettled(conversions) as PromiseSettledResult<ConversionSuccess, ConversionError>[] + const failed = results.filter(result => result.status === 'rejected') as PromiseRejectedResult<ConversionError>[] + if (failed.length > 0) { + const messages = failed.map(result => result.reason?.response?.data?.ocs?.meta?.message) + logger.error('Failed to convert files', { fileIds, targetMimeType, messages }) + + // If all failed files have the same error message, show it + if (new Set(messages).size === 1 && typeof messages[0] === 'string') { + showError(t('files', 'Failed to convert files: {message}', { message: messages[0] })) + return + } + + if (failed.length === fileIds.length) { + showError(t('files', 'All files failed to be converted')) + return + } + + // A single file failed and if we have a message for the failed file, show it + if (failed.length === 1 && messages[0]) { + showError(t('files', 'One file could not be converted: {message}', { message: messages[0] })) + return + } + + // We already check above when all files failed + // if we're here, we have a mix of failed and successful files + showError(n('files', 'One file could not be converted', '%n files could not be converted', failed.length)) + showSuccess(n('files', 'One file successfully converted', '%n files successfully converted', fileIds.length - failed.length)) + return + } + + // All files converted + showSuccess(t('files', 'Files successfully converted')) + + // Extract files that are within the current directory + // in batch mode, you might have files from different directories + // ⚠️, let's get the actual current dir, as the one from the action + // might have changed as the user navigated away + const currentDir = window.OCP.Files.Router.query.dir as string + const newPaths = results + .filter(result => result.status === 'fulfilled') + .map(result => result.value.data.ocs.data.path) + .filter(path => path.startsWith(currentDir)) + + // Fetch the new files + logger.debug('Files to fetch', { newPaths }) + const newFiles = await Promise.all(newPaths.map(path => fetchNode(path))) + + // Inform the file list about the new files + newFiles.forEach(file => emit('files:node:created', file)) + + // Switch to the new files + const firstSuccess = results[0] as PromiseFulfilledResult<ConversionSuccess> + const newFileId = firstSuccess.value.data.ocs.data.fileId + window.OCP.Files.Router.goToRoute(null, { ...window.OCP.Files.Router.params, fileid: newFileId.toString() }, window.OCP.Files.Router.query) + } catch (error) { + // Should not happen as we use allSettled and handle errors above + showError(t('files', 'Failed to convert files')) + logger.error('Failed to convert files', { fileIds, targetMimeType, error }) + } finally { + // Hide loading toast + toast.hideToast() + } +} + +export const convertFile = async function(fileId: number, targetMimeType: string) { + const toast = showLoading(t('files', 'Converting file …')) + + try { + const result = await queue.add(() => requestConversion(fileId, targetMimeType)) as AxiosResponse<OCSResponse<ConversionResponse>> + showSuccess(t('files', 'File successfully converted')) + + // Inform the file list about the new file + const newFile = await fetchNode(result.data.ocs.data.path) + emit('files:node:created', newFile) + + // Switch to the new file + const newFileId = result.data.ocs.data.fileId + window.OCP.Files.Router.goToRoute(null, { ...window.OCP.Files.Router.params, fileid: newFileId.toString() }, window.OCP.Files.Router.query) + } catch (error) { + // If the server returned an error message, show it + if (isAxiosError(error) && error.response?.data?.ocs?.meta?.message) { + showError(t('files', 'Failed to convert file: {message}', { message: error.response.data.ocs.meta.message })) + return + } + + logger.error('Failed to convert file', { fileId, targetMimeType, error }) + showError(t('files', 'Failed to convert file')) + } finally { + // Hide loading toast + toast.hideToast() + } +} |