From 08a7b9d676fcbc33a99eeb1bec37ca481c3f098c Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Tue, 17 Mar 2026 23:10:30 +0100 Subject: [PATCH] fix(files_external): properly handle API errors Signed-off-by: Ferdinand Thiessen --- .../AddExternalStorageDialog.vue | 8 +-- .../AddExternalStorageDialog/MountOptions.vue | 9 ++-- apps/files_external/src/store/storages.ts | 54 +++++++++++++++++-- .../src/views/ExternalStoragesSection.vue | 5 +- package-lock.json | 12 ++--- package.json | 2 +- 6 files changed, 66 insertions(+), 24 deletions(-) diff --git a/apps/files_external/src/components/AddExternalStorageDialog/AddExternalStorageDialog.vue b/apps/files_external/src/components/AddExternalStorageDialog/AddExternalStorageDialog.vue index c1d68549bcd..7e5613bb9a3 100644 --- a/apps/files_external/src/components/AddExternalStorageDialog/AddExternalStorageDialog.vue +++ b/apps/files_external/src/components/AddExternalStorageDialog/AddExternalStorageDialog.vue @@ -33,7 +33,7 @@ const open = defineModel('open', { default: true }) const { storage = { backendOptions: {}, mountOptions: {}, type: isAdmin ? 'system' : 'personal' }, } = defineProps<{ - storage?: Partial & { backendOptions: IStorage['backendOptions'] } + storage?: Partial }>() defineEmits<{ @@ -88,7 +88,7 @@ watch(authMechanisms, () => { :label="t('files_external', 'Folder name')" required /> - + { required /> diff --git a/apps/files_external/src/components/AddExternalStorageDialog/MountOptions.vue b/apps/files_external/src/components/AddExternalStorageDialog/MountOptions.vue index c7f538e6e06..eb05f2ee720 100644 --- a/apps/files_external/src/components/AddExternalStorageDialog/MountOptions.vue +++ b/apps/files_external/src/components/AddExternalStorageDialog/MountOptions.vue @@ -14,17 +14,14 @@ import NcButton from '@nextcloud/vue/components/NcButton' import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch' import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper' import NcSelect from '@nextcloud/vue/components/NcSelect' +import { parseMountOptions } from '../../store/storages.ts' import { MountOptionsCheckFilesystem } from '../../types.ts' const mountOptions = defineModel>({ required: true }) watchEffect(() => { if (Object.keys(mountOptions.value).length === 0) { - mountOptions.value.encrypt = true - mountOptions.value.previews = true - mountOptions.value.enable_sharing = false - mountOptions.value.filesystem_check_changes = MountOptionsCheckFilesystem.OncePerRequest - mountOptions.value.encoding_compatibility = false - mountOptions.value.readonly = false + // parse and initialize with defaults if needed + mountOptions.value = parseMountOptions(mountOptions.value) } }) diff --git a/apps/files_external/src/store/storages.ts b/apps/files_external/src/store/storages.ts index d73c7d7857b..f2d7437c3ea 100644 --- a/apps/files_external/src/store/storages.ts +++ b/apps/files_external/src/store/storages.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import type { IStorage } from '../types.d.ts' +import type { IStorage } from '../types.ts' import axios from '@nextcloud/axios' import { loadState } from '@nextcloud/initial-state' @@ -11,6 +11,7 @@ import { addPasswordConfirmationInterceptors, PwdConfirmationMode } from '@nextc import { generateUrl } from '@nextcloud/router' import { defineStore } from 'pinia' import { ref, toRaw } from 'vue' +import { MountOptionsCheckFilesystem } from '../types.ts' const { isAdmin } = loadState<{ isAdmin: boolean }>('files_external', 'settings') @@ -30,7 +31,7 @@ export const useStorages = defineStore('files_external--storages', () => { toRaw(storage), { confirmPassword: PwdConfirmationMode.Strict }, ) - globalStorages.value.push(data) + globalStorages.value.push(parseStorage(data)) } /** @@ -45,7 +46,7 @@ export const useStorages = defineStore('files_external--storages', () => { toRaw(storage), { confirmPassword: PwdConfirmationMode.Strict }, ) - userStorages.value.push(data) + userStorages.value.push(parseStorage(data)) } /** @@ -77,7 +78,7 @@ export const useStorages = defineStore('files_external--storages', () => { { confirmPassword: PwdConfirmationMode.Strict }, ) - overrideStorage(data) + overrideStorage(parseStorage(data)) } /** @@ -87,7 +88,7 @@ export const useStorages = defineStore('files_external--storages', () => { */ async function reloadStorage(storage: IStorage) { const { data } = await axios.get(getUrl(storage)) - overrideStorage(data) + overrideStorage(parseStorage(data)) } // initialize the store @@ -111,6 +112,7 @@ export const useStorages = defineStore('files_external--storages', () => { const url = `apps/files_external/${type}` const { data } = await axios.get>(generateUrl(url)) return Object.values(data) + .map(parseStorage) } /** @@ -150,3 +152,45 @@ export const useStorages = defineStore('files_external--storages', () => { } } }) + +/** + * @param storage - The storage from API + */ +function parseStorage(storage: IStorage) { + return { + ...storage, + mountOptions: parseMountOptions(storage.mountOptions), + } +} + +/** + * Parse the mount options and convert string boolean values to + * actual booleans and numeric strings to numbers + * + * @param options - The mount options to parse + */ +export function parseMountOptions(options: IStorage['mountOptions']) { + const mountOptions = { ...options } + mountOptions.encrypt = convertBooleanOptions(mountOptions.encrypt, true) + mountOptions.previews = convertBooleanOptions(mountOptions.previews, true) + mountOptions.enable_sharing = convertBooleanOptions(mountOptions.enable_sharing, false) + mountOptions.filesystem_check_changes = typeof mountOptions.filesystem_check_changes === 'string' + ? Number.parseInt(mountOptions.filesystem_check_changes) + : (mountOptions.filesystem_check_changes ?? MountOptionsCheckFilesystem.OncePerRequest) + mountOptions.encoding_compatibility = convertBooleanOptions(mountOptions.encoding_compatibility, false) + mountOptions.readonly = convertBooleanOptions(mountOptions.readonly, false) + return mountOptions +} + +/** + * Convert backend encoding of boolean options + * + * @param option - The option value from API + * @param fallback - The fallback (default) value + */ +function convertBooleanOptions(option: unknown, fallback = false) { + if (option === undefined) { + return fallback + } + return option === true || option === 'true' || option === '1' +} diff --git a/apps/files_external/src/views/ExternalStoragesSection.vue b/apps/files_external/src/views/ExternalStoragesSection.vue index 484aa5cf110..617db37389e 100644 --- a/apps/files_external/src/views/ExternalStoragesSection.vue +++ b/apps/files_external/src/views/ExternalStoragesSection.vue @@ -59,7 +59,8 @@ async function addStorage(storage?: Partial) { } newStorage.value = undefined } catch (error) { - logger.error('Failed to add external storage', { error }) + logger.error('Failed to add external storage', { error, storage }) + newStorage.value = { ...storage } showDialog.value = true } } @@ -134,8 +135,8 @@ async function addStorage(storage?: Partial) { diff --git a/package-lock.json b/package-lock.json index 7f7538125ac..0a2f1f59174 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "@nextcloud/initial-state": "^3.0.0", "@nextcloud/l10n": "^3.4.1", "@nextcloud/logger": "^3.0.3", - "@nextcloud/password-confirmation": "^6.0.3", + "@nextcloud/password-confirmation": "^6.1.0", "@nextcloud/paths": "^3.1.0", "@nextcloud/router": "^3.1.0", "@nextcloud/sharing": "^0.4.0", @@ -2476,9 +2476,9 @@ } }, "node_modules/@nextcloud/password-confirmation": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@nextcloud/password-confirmation/-/password-confirmation-6.0.3.tgz", - "integrity": "sha512-tgbzwfhlXXd9Eq8ZnYrTeH8bEkdyIgybN45Tkip01b3xABUlr0tMGGj8+ZNp2pozytcK+k1l6fyvRPc09g0rIw==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@nextcloud/password-confirmation/-/password-confirmation-6.1.0.tgz", + "integrity": "sha512-N2ChXDbPcUcQCoAjj1yc65zfc61yiKpdM3qm/y3Y/y//NG7XWNNINwBg85lqJ2SISgmVStpsQ1d0blpFCoQXkQ==", "license": "MIT", "dependencies": { "@nextcloud/auth": "^2.5.3", @@ -2486,8 +2486,8 @@ "@nextcloud/l10n": "^3.4.1", "@nextcloud/logger": "^3.0.3", "@nextcloud/router": "^3.1.0", - "@nextcloud/vue": "^9.5.0", - "vue": "^3.5.29" + "@nextcloud/vue": "^9.6.0", + "vue": "^3.5.30" }, "engines": { "node": "^20.0.0 || ^22.0.0 || ^24.0.0" diff --git a/package.json b/package.json index 1af864b2f3e..f4ec8f47f13 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "@nextcloud/initial-state": "^3.0.0", "@nextcloud/l10n": "^3.4.1", "@nextcloud/logger": "^3.0.3", - "@nextcloud/password-confirmation": "^6.0.3", + "@nextcloud/password-confirmation": "^6.1.0", "@nextcloud/paths": "^3.1.0", "@nextcloud/router": "^3.1.0", "@nextcloud/sharing": "^0.4.0",