fix(files_external): properly handle API errors

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
Ferdinand Thiessen 2026-03-17 23:10:30 +01:00
parent 44175e3cdb
commit 1688dff259
No known key found for this signature in database
GPG key ID: 7E849AE05218500F
6 changed files with 151 additions and 111 deletions

View file

@ -33,7 +33,7 @@ const open = defineModel<boolean>('open', { default: true })
const {
storage = { backendOptions: {}, mountOptions: {}, type: isAdmin ? 'system' : 'personal' },
} = defineProps<{
storage?: Partial<IStorage> & { backendOptions: IStorage['backendOptions'] }
storage?: Partial<IStorage>
}>()
defineEmits<{
@ -88,7 +88,7 @@ watch(authMechanisms, () => {
:label="t('files_external', 'Folder name')"
required />
<MountOptions v-model="internalStorage.mountOptions" />
<MountOptions v-model="internalStorage.mountOptions!" />
<ApplicableEntities
v-if="isAdmin"
@ -112,13 +112,13 @@ watch(authMechanisms, () => {
required />
<BackendConfiguration
v-if="backend"
v-if="backend && internalStorage.backendOptions"
v-model="internalStorage.backendOptions"
:class="$style.externalStorageDialog__configuration"
:configuration="backend.configuration" />
<AuthMechanismConfiguration
v-if="authMechanism"
v-if="authMechanism && internalStorage.backendOptions"
v-model="internalStorage.backendOptions"
:class="$style.externalStorageDialog__configuration"
:authMechanism="authMechanism" />

View file

@ -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<Partial<IMountOptions>>({ 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)
}
})

View file

@ -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<Record<number, IStorage>>(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'
}

View file

@ -59,7 +59,8 @@ async function addStorage(storage?: Partial<IStorage>) {
}
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<IStorage>) {
</NcButton>
<AddExternalStorageDialog
v-model="newStorage"
v-model:open="showDialog"
:storage="newStorage"
@close="addStorage" />
<UserMountSettings v-if="settings.isAdmin" />

184
package-lock.json generated
View file

@ -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",
@ -1509,28 +1509,28 @@
}
},
"node_modules/@floating-ui/core": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz",
"integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==",
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
"integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.10"
"@floating-ui/utils": "^0.2.11"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz",
"integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==",
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
"integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.7.4",
"@floating-ui/utils": "^0.2.10"
"@floating-ui/core": "^1.7.5",
"@floating-ui/utils": "^0.2.11"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
"version": "0.2.11",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
"integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
"license": "MIT"
},
"node_modules/@grpc/grpc-js": {
@ -2522,9 +2522,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",
@ -2532,8 +2532,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"
@ -2636,13 +2636,13 @@
}
},
"node_modules/@nextcloud/vue": {
"version": "9.5.0",
"resolved": "https://registry.npmjs.org/@nextcloud/vue/-/vue-9.5.0.tgz",
"integrity": "sha512-CQxBfHhF+Q+2r7RXd+l/eSjttJU8A2JFUyq5VpvjfpIql355kejc8bbNnM1pKgGRGSBuW9qw5Ohx0puzHge10w==",
"version": "9.6.0",
"resolved": "https://registry.npmjs.org/@nextcloud/vue/-/vue-9.6.0.tgz",
"integrity": "sha512-RZuMnrNwzajx3AbJcGHC49NUORSPHMRbzhHCBlR0Z5N3BvUmy5d7MPVTqsmJXiO5emSCTanJ+rwDeo6HTAX3ng==",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@ckpack/vue-color": "^1.6.0",
"@floating-ui/dom": "^1.7.5",
"@floating-ui/dom": "^1.7.6",
"@nextcloud/auth": "^2.5.3",
"@nextcloud/axios": "^2.5.2",
"@nextcloud/browser-storage": "^0.5.0",
@ -2652,14 +2652,14 @@
"@nextcloud/l10n": "^3.4.1",
"@nextcloud/logger": "^3.0.3",
"@nextcloud/router": "^3.1.0",
"@nextcloud/sharing": "^0.3.0",
"@nextcloud/sharing": "^0.4.0",
"@vuepic/vue-datepicker": "^11.0.3",
"@vueuse/components": "^14.2.0",
"@vueuse/core": "^14.0.0",
"@vueuse/components": "^14.2.1",
"@vueuse/core": "^14.2.1",
"blurhash": "^2.0.5",
"clone": "^2.1.2",
"debounce": "^3.0.0",
"dompurify": "^3.3.1",
"dompurify": "^3.3.3",
"emoji-mart-vue-fast": "^15.0.5",
"escape-html": "^1.0.3",
"floating-vue": "^5.2.2",
@ -2672,6 +2672,7 @@
"remark-breaks": "^4.0.0",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.2",
"remark-stringify": "^11.0.0",
"remark-unlink-protocols": "^1.0.0",
"splitpanes": "^4.0.4",
"striptags": "^3.2.0",
@ -2682,50 +2683,49 @@
"unist-builder": "^4.0.0",
"unist-util-visit": "^5.1.0",
"vue": "^3.5.18",
"vue-router": "^5.0.2",
"vue-router": "^5.0.3",
"vue-select": "^4.0.0-beta.6"
},
"engines": {
"node": "^20.11.0 || ^22 || ^24"
}
},
"node_modules/@nextcloud/vue/node_modules/@nextcloud/files": {
"version": "3.12.2",
"resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-3.12.2.tgz",
"integrity": "sha512-vBo8tf3Xh6efiF8CrEo3pKj9AtvAF6RdDGO1XKL65IxV8+UUd9Uxl2lUExHlzoDRRczCqfGfaWfRRaFhYqce5Q==",
"license": "AGPL-3.0-or-later",
"optional": true,
"node_modules/@nextcloud/vue/node_modules/@vueuse/core": {
"version": "14.2.1",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.2.1.tgz",
"integrity": "sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ==",
"license": "MIT",
"dependencies": {
"@nextcloud/auth": "^2.5.3",
"@nextcloud/capabilities": "^1.2.1",
"@nextcloud/l10n": "^3.4.1",
"@nextcloud/logger": "^3.0.3",
"@nextcloud/paths": "^3.0.0",
"@nextcloud/router": "^3.1.0",
"@nextcloud/sharing": "^0.3.0",
"cancelable-promise": "^4.3.1",
"is-svg": "^6.1.0",
"typescript-event-target": "^1.1.1",
"webdav": "^5.8.0"
"@types/web-bluetooth": "^0.0.21",
"@vueuse/metadata": "14.2.1",
"@vueuse/shared": "14.2.1"
},
"engines": {
"node": "^20.0.0 || ^22.0.0 || ^24.0.0"
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"vue": "^3.5.0"
}
},
"node_modules/@nextcloud/vue/node_modules/@nextcloud/sharing": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@nextcloud/sharing/-/sharing-0.3.0.tgz",
"integrity": "sha512-kV7qeUZvd1fTKeFyH+W5Qq5rNOqG9rLATZM3U9MBxWXHJs3OxMqYQb8UQ3NYONzsX3zDGJmdQECIGHm1ei2sCA==",
"license": "GPL-3.0-or-later",
"dependencies": {
"@nextcloud/initial-state": "^3.0.0",
"is-svg": "^6.1.0"
"node_modules/@nextcloud/vue/node_modules/@vueuse/metadata": {
"version": "14.2.1",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.2.1.tgz",
"integrity": "sha512-1ButlVtj5Sb/HDtIy1HFr1VqCP4G6Ypqt5MAo0lCgjokrk2mvQKsK2uuy0vqu/Ks+sHfuHo0B9Y9jn9xKdjZsw==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@nextcloud/vue/node_modules/@vueuse/shared": {
"version": "14.2.1",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.2.1.tgz",
"integrity": "sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"engines": {
"node": "^20.0.0 || ^22.0.0 || ^24.0.0"
},
"optionalDependencies": {
"@nextcloud/files": "^3.12.0"
"peerDependencies": {
"vue": "^3.5.0"
}
},
"node_modules/@nextcloud/vue/node_modules/focus-trap": {
@ -9021,17 +9021,6 @@
"@floating-ui/core": "^1.1.0"
}
},
"node_modules/focus-trap": {
"version": "7.6.6",
"resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.6.tgz",
"integrity": "sha512-v/Z8bvMCajtx4mEXmOo7QEsIzlIOqRXTIwgUfsFOF9gEsespdbD0AkPIka1bSXZ8Y8oZ+2IVDQZePkTfEHZl7Q==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"tabbable": "^6.3.0"
}
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
@ -14074,6 +14063,21 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/remark-stringify": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz",
"integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==",
"license": "MIT",
"dependencies": {
"@types/mdast": "^4.0.0",
"mdast-util-to-markdown": "^2.0.0",
"unified": "^11.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/remark-unlink-protocols": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/remark-unlink-protocols/-/remark-unlink-protocols-1.0.0.tgz",
@ -17758,14 +17762,14 @@
}
},
"node_modules/vue-router": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.2.tgz",
"integrity": "sha512-YFhwaE5c5JcJpNB1arpkl4/GnO32wiUWRB+OEj1T0DlDxEZoOfbltl2xEwktNU/9o1sGcGburIXSpbLpPFe/6w==",
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.3.tgz",
"integrity": "sha512-nG1c7aAFac7NYj8Hluo68WyWfc41xkEjaR0ViLHCa3oDvTQ/nIuLJlXJX1NUPw/DXzx/8+OKMng045HHQKQKWw==",
"license": "MIT",
"dependencies": {
"@babel/generator": "^7.28.6",
"@vue-macros/common": "^3.1.1",
"@vue/devtools-api": "^8.0.0",
"@vue/devtools-api": "^8.0.6",
"ast-walker-scope": "^0.8.3",
"chokidar": "^5.0.0",
"json5": "^2.2.3",
@ -17803,37 +17807,31 @@
}
},
"node_modules/vue-router/node_modules/@vue/devtools-api": {
"version": "8.0.6",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.0.6.tgz",
"integrity": "sha512-+lGBI+WTvJmnU2FZqHhEB8J1DXcvNlDeEalz77iYgOdY1jTj1ipSBaKj3sRhYcy+kqA8v/BSuvOz1XJucfQmUA==",
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.1.0.tgz",
"integrity": "sha512-O44X57jjkLKbLEc4OgL/6fEPOOanRJU8kYpCE8qfKlV96RQZcdzrcLI5mxMuVRUeXhHKIHGhCpHacyCk0HyO4w==",
"license": "MIT",
"dependencies": {
"@vue/devtools-kit": "^8.0.6"
"@vue/devtools-kit": "^8.1.0"
}
},
"node_modules/vue-router/node_modules/@vue/devtools-kit": {
"version": "8.0.6",
"resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.0.6.tgz",
"integrity": "sha512-9zXZPTJW72OteDXeSa5RVML3zWDCRcO5t77aJqSs228mdopYj5AiTpihozbsfFJ0IodfNs7pSgOGO3qfCuxDtw==",
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.1.0.tgz",
"integrity": "sha512-/NZlS4WtGIB54DA/z10gzk+n/V7zaqSzYZOVlg2CfdnpIKdB61bd7JDIMxf/zrtX41zod8E2/bbEBoW/d7x70Q==",
"license": "MIT",
"dependencies": {
"@vue/devtools-shared": "^8.0.6",
"@vue/devtools-shared": "^8.1.0",
"birpc": "^2.6.1",
"hookable": "^5.5.3",
"mitt": "^3.0.1",
"perfect-debounce": "^2.0.0",
"speakingurl": "^14.0.1",
"superjson": "^2.2.2"
"perfect-debounce": "^2.0.0"
}
},
"node_modules/vue-router/node_modules/@vue/devtools-shared": {
"version": "8.0.6",
"resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.0.6.tgz",
"integrity": "sha512-Pp1JylTqlgMJvxW6MGyfTF8vGvlBSCAvMFaDCYa82Mgw7TT5eE5kkHgDvmOGHWeJE4zIDfCpCxHapsK2LtIAJg==",
"license": "MIT",
"dependencies": {
"rfdc": "^1.4.1"
}
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.1.0.tgz",
"integrity": "sha512-h8uCb4Qs8UT8VdTT5yjY6tOJ//qH7EpxToixR0xqejR55t5OdISIg7AJ7eBkhBs8iu1qG5gY3QQNN1DF1EelAA==",
"license": "MIT"
},
"node_modules/vue-router/node_modules/chokidar": {
"version": "5.0.0",

View file

@ -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",