aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files/src/store/renaming.ts
blob: 2ac9e06ba1617475032117b2b4f6b3ffe828fff4 (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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
/**
 * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
 * SPDX-License-Identifier: AGPL-3.0-or-later
 */
import type { Node } from '@nextcloud/files'

import axios, { isAxiosError } from '@nextcloud/axios'
import { emit, subscribe } from '@nextcloud/event-bus'
import { FileType, NodeStatus } from '@nextcloud/files'
import { t } from '@nextcloud/l10n'
import { spawnDialog } from '@nextcloud/vue/functions/dialog'
import { basename, dirname, extname } from 'path'
import { defineStore } from 'pinia'
import logger from '../logger'
import Vue, { defineAsyncComponent, ref } from 'vue'
import { useUserConfigStore } from './userconfig'

export const useRenamingStore = defineStore('renaming', () => {
	/**
	 * The currently renamed node
	 */
	const renamingNode = ref<Node>()
	/**
	 * The new name of the currently renamed node
	 */
	const newNodeName = ref('')

	/**
	 * Internal flag to only allow calling `rename` once.
	 */
	const isRenaming = ref(false)

	/**
	 * Execute the renaming.
	 * This will rename the node set as `renamingNode` to the configured new name `newName`.
	 *
	 * @return true if success, false if skipped (e.g. new and old name are the same)
	 * @throws Error if renaming fails, details are set in the error message
	 */
	async function rename(): Promise<boolean> {
		if (renamingNode.value === undefined) {
			throw new Error('No node is currently being renamed')
		}

		// Only rename once so we use this as some kind of mutex
		if (isRenaming.value) {
			return false
		}
		isRenaming.value = true

		const node = renamingNode.value
		Vue.set(node, 'status', NodeStatus.LOADING)

		const userConfig = useUserConfigStore()

		let newName = newNodeName.value.trim()
		const oldName = node.basename
		const oldExtension = extname(oldName)
		const newExtension = extname(newName)
		// Check for extension change for files
		if (node.type === FileType.File
			&& oldExtension !== newExtension
			&& userConfig.userConfig.show_dialog_file_extension
			&& !(await showFileExtensionDialog(oldExtension, newExtension))
		) {
			// user selected to use the old extension
			newName = basename(newName, newExtension) + oldExtension
		}

		const oldEncodedSource = node.encodedSource
		try {
			if (oldName === newName) {
				return false
			}

			// rename the node
			node.rename(newName)
			logger.debug('Moving file to', { destination: node.encodedSource, oldEncodedSource })
			// create MOVE request
			await axios({
				method: 'MOVE',
				url: oldEncodedSource,
				headers: {
					Destination: node.encodedSource,
					Overwrite: 'F',
				},
			})

			// Success 🎉
			emit('files:node:updated', node)
			emit('files:node:renamed', node)
			emit('files:node:moved', {
				node,
				oldSource: `${dirname(node.source)}/${oldName}`,
			})

			// Reset the state not changed
			if (renamingNode.value === node) {
				$reset()
			}

			return true
		} catch (error) {
			logger.error('Error while renaming file', { error })
			// Rename back as it failed
			node.rename(oldName)
			if (isAxiosError(error)) {
				// TODO: 409 means current folder does not exist, redirect ?
				if (error?.response?.status === 404) {
					throw new Error(t('files', 'Could not rename "{oldName}", it does not exist any more', { oldName }))
				} else if (error?.response?.status === 412) {
					throw new Error(t(
						'files',
						'The name "{newName}" is already used in the folder "{dir}". Please choose a different name.',
						{
							newName,
							dir: basename(renamingNode.value!.dirname),
						},
					))
				}
			}
			// Unknown error
			throw new Error(t('files', 'Could not rename "{oldName}"', { oldName }))
		} finally {
			Vue.set(node, 'status', undefined)
			isRenaming.value = false
		}
	}

	/**
	 * Reset the store state
	 */
	function $reset(): void {
		newNodeName.value = ''
		renamingNode.value = undefined
	}

	// Make sure we only register the listeners once
	subscribe('files:node:rename', (node: Node) => {
		renamingNode.value = node
		newNodeName.value = node.basename
	})

	return {
		$reset,

		newNodeName,
		rename,
		renamingNode,
	}
})

/**
 * Show a dialog asking user for confirmation about changing the file extension.
 *
 * @param oldExtension the old file name extension
 * @param newExtension the new file name extension
 */
async function showFileExtensionDialog(oldExtension: string, newExtension: string): Promise<boolean> {
	const { promise, resolve } = Promise.withResolvers<boolean>()
	spawnDialog(
		defineAsyncComponent(() => import('../views/DialogConfirmFileExtension.vue')),
		{ oldExtension, newExtension },
		(useNewExtension: unknown) => resolve(Boolean(useNewExtension)),
	)
	return await promise
}