From cc40e96e99fa7d510915ae6c2930bca02012fa0f Mon Sep 17 00:00:00 2001 From: skjnldsv Date: Fri, 17 Jan 2025 20:33:15 +0100 Subject: [PATCH] fixup! fixup! fixup! feat(files): add conversion action Signed-off-by: skjnldsv --- apps/files/src/actions/convertAction.ts | 39 ++++++++---- apps/files/src/actions/convertUtils.ts | 82 +++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 12 deletions(-) create mode 100644 apps/files/src/actions/convertUtils.ts diff --git a/apps/files/src/actions/convertAction.ts b/apps/files/src/actions/convertAction.ts index 13e2647569bb8..5680a06e49dd0 100644 --- a/apps/files/src/actions/convertAction.ts +++ b/apps/files/src/actions/convertAction.ts @@ -5,42 +5,57 @@ import type { Node, View } from '@nextcloud/files' import { FileAction, registerFileAction } from '@nextcloud/files' -import { generateUrl } from '@nextcloud/router' +import { generateOcsUrl, generateUrl } from '@nextcloud/router' import { getCapabilities } from '@nextcloud/capabilities' import { t } from '@nextcloud/l10n' import AutoRenewSvg from '@mdi/svg/svg/autorenew.svg?raw' import logger from '../logger' +import axios from '@nextcloud/axios' +import { convertFile, convertFiles } from './convertUtils' +import { showError, showMessage } from '@nextcloud/dialogs' type ConversionsProvider = { from: string, - to: string[], + to: string, + displayName: string, } export const ACTION_CONVERT = 'convert' export const registerConvertActions = () => { // Generate sub actions - const convertProviders = getCapabilities()?.core?.conversions as ConversionsProvider[] ?? [] - const actions = convertProviders.map(provider => { - return provider.to.map(to => new FileAction({ - id: `convert-${provider.from}-${to}`, - displayName: () => t('files', 'Save as {to}', { to }), + const convertProviders = getCapabilities()?.files.file_conversions as ConversionsProvider[] ?? [] + const actions = convertProviders.map(({ to, from, displayName }) => { + return new FileAction({ + id: `convert-${from}-${to}`, + displayName: () => t('files', 'Save as {displayName}', { displayName }), iconSvgInline: () => generateIconSvg(to), enabled: (nodes: Node[]) => { // Check if some of the nodes are not of the right type - return !nodes.some(node => provider.from !== node.mime) + return !nodes.some(node => from !== node.mime) }, async exec(node: Node) { - logger.debug(`Convert to ${provider.from}`, { node }) + // If we're here, we know that the node have a fileid + convertFile(node.fileid as number, to) + + // Silently terminate, we'll handle the UI in the background return null }, - parent: ACTION_CONVERT, - })) - }).flat() + async execBatch(nodes: Node[]) { + const fileIds = nodes.map(node => node.fileid).filter(Boolean) as number[] + convertFiles(fileIds, to) + + // Silently terminate, we'll handle the UI in the background + return Array(nodes.length).fill(null) + }, + + // parent: ACTION_CONVERT, + }) + }) // Register main action registerFileAction(new FileAction({ diff --git a/apps/files/src/actions/convertUtils.ts b/apps/files/src/actions/convertUtils.ts new file mode 100644 index 0000000000000..6fd2a63df8884 --- /dev/null +++ b/apps/files/src/actions/convertUtils.ts @@ -0,0 +1,82 @@ +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import type { AxiosResponse } from 'axios' + +import { generateOcsUrl } from '@nextcloud/router' +import { showError, showMessage } from '@nextcloud/dialogs' +import { t } from '@nextcloud/l10n' +import axios from '@nextcloud/axios' +import PQueue from 'p-queue' + +import logger from '../logger' + +const queue = new PQueue({ concurrency: 5 }) + +const requestConversion = function(fileId: number, targetMimeType: string): Promise { + 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 = showMessage(t('files', 'Converting files…')) + + // Handle results + try { + const results = await Promise.allSettled(conversions) + const failed = results.filter(result => result.status === 'rejected') + if (failed.length) { + const messages = failed.map(result => result.reason?.response?.data?.ocs?.meta?.message) as string[] + + // If all failed files have the same error message, show it + if (new Set(messages).size === 1) { + showError(t('files', 'Failed to convert files: {message}', { message: messages[0] })) + return + } + + if (failed.length === fileIds.length) { + showError(t('files', 'Failed to convert files')) + return + } + showError(t('files', 'Some files could not be converted')) + return + } + + // All files converted + showMessage(t('files', 'Files successfully converted')) + } 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 loader toast + toast.hideToast() + } +} + +export const convertFile = async function(fileId: number, targetMimeType: string) { + const toast = showMessage(t('files', 'Converting files…')) + + try { + await queue.add(() => requestConversion(fileId, targetMimeType)) + showMessage(t('files', 'File successfully converted')) + } catch (error) { + // If the server returned an error message, show it + if (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 loader toast + toast.hideToast() + } +}