|
| 1 | +/** |
| 2 | + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors |
| 3 | + * SPDX-License-Identifier: AGPL-3.0-or-later |
| 4 | + */ |
| 5 | +import type { AxiosResponse } from '@nextcloud/axios' |
| 6 | +import type { Folder, View } from '@nextcloud/files' |
| 7 | + |
| 8 | +import { emit } from '@nextcloud/event-bus' |
| 9 | +import { generateOcsUrl } from '@nextcloud/router' |
| 10 | +import { showError, showLoading, showSuccess } from '@nextcloud/dialogs' |
| 11 | +import { t } from '@nextcloud/l10n' |
| 12 | +import axios from '@nextcloud/axios' |
| 13 | +import PQueue from 'p-queue' |
| 14 | + |
| 15 | +import logger from '../logger' |
| 16 | +import { useFilesStore } from '../store/files' |
| 17 | +import { getPinia } from '../store' |
| 18 | +import { usePathsStore } from '../store/paths' |
| 19 | + |
| 20 | +const queue = new PQueue({ concurrency: 5 }) |
| 21 | + |
| 22 | +const requestConversion = function(fileId: number, targetMimeType: string): Promise<AxiosResponse> { |
| 23 | + return axios.post(generateOcsUrl('/apps/files/api/v1/convert'), { |
| 24 | + fileId, |
| 25 | + targetMimeType, |
| 26 | + }) |
| 27 | +} |
| 28 | + |
| 29 | +export const convertFiles = async function(fileIds: number[], targetMimeType: string, parentFolder: Folder | null) { |
| 30 | + const conversions = fileIds.map(fileId => queue.add(() => requestConversion(fileId, targetMimeType))) |
| 31 | + |
| 32 | + // Start conversion |
| 33 | + const toast = showLoading(t('files', 'Converting files…')) |
| 34 | + |
| 35 | + // Handle results |
| 36 | + try { |
| 37 | + const results = await Promise.allSettled(conversions) |
| 38 | + const failed = results.filter(result => result.status === 'rejected') |
| 39 | + if (failed.length > 0) { |
| 40 | + const messages = failed.map(result => result.reason?.response?.data?.ocs?.meta?.message) as string[] |
| 41 | + logger.error('Failed to convert files', { fileIds, targetMimeType, messages }) |
| 42 | + |
| 43 | + // If all failed files have the same error message, show it |
| 44 | + if (new Set(messages).size === 1) { |
| 45 | + showError(t('files', 'Failed to convert files: {message}', { message: messages[0] })) |
| 46 | + return |
| 47 | + } |
| 48 | + |
| 49 | + if (failed.length === fileIds.length) { |
| 50 | + showError(t('files', 'All files failed to be converted')) |
| 51 | + return |
| 52 | + } |
| 53 | + |
| 54 | + // A single file failed |
| 55 | + if (failed.length === 1) { |
| 56 | + // If we have a message for the failed file, show it |
| 57 | + if (messages[0]) { |
| 58 | + showError(t('files', 'One file could not be converted: {message}', { message: messages[0] })) |
| 59 | + return |
| 60 | + } |
| 61 | + |
| 62 | + // Otherwise, show a generic error |
| 63 | + showError(t('files', 'One file could not be converted')) |
| 64 | + return |
| 65 | + } |
| 66 | + |
| 67 | + // We already check above when all files failed |
| 68 | + // if we're here, we have a mix of failed and successful files |
| 69 | + showError(t('files', '{count} files could not be converted', { count: failed.length })) |
| 70 | + showSuccess(t('files', '{count} files successfully converted', { count: fileIds.length - failed.length })) |
| 71 | + return |
| 72 | + } |
| 73 | + |
| 74 | + // All files converted |
| 75 | + showSuccess(t('files', 'Files successfully converted')) |
| 76 | + |
| 77 | + // Trigger a reload of the file list |
| 78 | + if (parentFolder) { |
| 79 | + emit('files:node:updated', parentFolder) |
| 80 | + } |
| 81 | + |
| 82 | + // Switch to the new files |
| 83 | + const firstSuccess = results[0] as PromiseFulfilledResult<AxiosResponse> |
| 84 | + const newFileId = firstSuccess.value.data.ocs.data.fileId |
| 85 | + window.OCP.Files.Router.goToRoute(null, { ...window.OCP.Files.Router.params, fileid: newFileId }, window.OCP.Files.Router.query) |
| 86 | + } catch (error) { |
| 87 | + // Should not happen as we use allSettled and handle errors above |
| 88 | + showError(t('files', 'Failed to convert files')) |
| 89 | + logger.error('Failed to convert files', { fileIds, targetMimeType, error }) |
| 90 | + } finally { |
| 91 | + // Hide loading toast |
| 92 | + toast.hideToast() |
| 93 | + } |
| 94 | +} |
| 95 | + |
| 96 | +export const convertFile = async function(fileId: number, targetMimeType: string, parentFolder: Folder | null) { |
| 97 | + const toast = showLoading(t('files', 'Converting file…')) |
| 98 | + |
| 99 | + try { |
| 100 | + const result = await queue.add(() => requestConversion(fileId, targetMimeType)) as AxiosResponse |
| 101 | + showSuccess(t('files', 'File successfully converted')) |
| 102 | + |
| 103 | + // Trigger a reload of the file list |
| 104 | + if (parentFolder) { |
| 105 | + emit('files:node:updated', parentFolder) |
| 106 | + } |
| 107 | + |
| 108 | + // Switch to the new file |
| 109 | + const newFileId = result.data.ocs.data.fileId |
| 110 | + window.OCP.Files.Router.goToRoute(null, { ...window.OCP.Files.Router.params, fileid: newFileId }, window.OCP.Files.Router.query) |
| 111 | + } catch (error) { |
| 112 | + // If the server returned an error message, show it |
| 113 | + if (error.response?.data?.ocs?.meta?.message) { |
| 114 | + showError(t('files', 'Failed to convert file: {message}', { message: error.response.data.ocs.meta.message })) |
| 115 | + return |
| 116 | + } |
| 117 | + |
| 118 | + logger.error('Failed to convert file', { fileId, targetMimeType, error }) |
| 119 | + showError(t('files', 'Failed to convert file')) |
| 120 | + } finally { |
| 121 | + // Hide loading toast |
| 122 | + toast.hideToast() |
| 123 | + } |
| 124 | +} |
| 125 | + |
| 126 | +/** |
| 127 | + * Get the parent folder of a path |
| 128 | + * |
| 129 | + * TODO: replace by the parent node straight away when we |
| 130 | + * update the Files actions api accordingly. |
| 131 | + * |
| 132 | + * @param view The current view |
| 133 | + * @param path The path to the file |
| 134 | + * @returns The parent folder |
| 135 | + */ |
| 136 | +export const getParentFolder = function(view: View, path: string): Folder | null { |
| 137 | + const filesStore = useFilesStore(getPinia()) |
| 138 | + const pathsStore = usePathsStore(getPinia()) |
| 139 | + |
| 140 | + const parentSource = pathsStore.getPath(view.id, path) |
| 141 | + if (!parentSource) { |
| 142 | + return null |
| 143 | + } |
| 144 | + |
| 145 | + const parentFolder = filesStore.getNode(parentSource) as Folder | undefined |
| 146 | + return parentFolder ?? null |
| 147 | +} |
0 commit comments