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 5f74a4a216
commit 08a7b9d676
No known key found for this signature in database
GPG key ID: 7E849AE05218500F
6 changed files with 66 additions and 24 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" />

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

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