aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files/src/actions/convertUtils.ts
blob: 0ace3747d9cb663d6906c05c8617898553ecea3b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
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()
	}
}