diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b6763f..da52507 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ -## Cockpit File Sharing 3.2.3-1 +## Cockpit File Sharing 3.2.4-1 -* Fixed issue saving NFS exports file when /etc/exports.d DNE \ No newline at end of file +* Add settings menu for configuring smb.conf and exports file locations +* Fix parsing "valid users" field for Samba shares \ No newline at end of file diff --git a/file-sharing/src/components/Config.vue b/file-sharing/src/components/Config.vue new file mode 100644 index 0000000..6c0c3b6 --- /dev/null +++ b/file-sharing/src/components/Config.vue @@ -0,0 +1,180 @@ + + + + + + + + + Samba + + + Configuration Path + + + + NFS + + + Configuration Path + + + + + + + + diff --git a/file-sharing/src/components/FilePermissions.vue b/file-sharing/src/components/FilePermissions.vue index 7375486..a585ee7 100644 --- a/file-sharing/src/components/FilePermissions.vue +++ b/file-sharing/src/components/FilePermissions.vue @@ -55,7 +55,7 @@ If not, see . diff --git a/file-sharing/src/components/NfsManager.vue b/file-sharing/src/components/NfsManager.vue index 992c473..09addc8 100644 --- a/file-sharing/src/components/NfsManager.vue +++ b/file-sharing/src/components/NfsManager.vue @@ -16,27 +16,47 @@ If not, see . --> - + Shares - + - + - + - + Path - + Edit/Delete @@ -45,9 +65,16 @@ If not, see . - + @@ -56,18 +83,37 @@ If not, see . Import/Export Config - + - - Import - Export + + Import + Export - + @@ -80,22 +126,25 @@ import NfsShare from "./NfsShare.vue"; import NfsShareEditor from "./NfsShareEditor.vue"; import { PlusIcon, XCircleIcon, ExclamationCircleIcon } from "@heroicons/vue/solid"; import LoadingSpinner from "./LoadingSpinner.vue"; -import { ref, reactive, inject, onBeforeUnmount } from "vue"; +import { ref, reactive, inject, onBeforeUnmount, watch } from "vue"; import { NfsExportSyntax } from "@45drives/cockpit-syntaxes"; import { useSpawn, errorString, errorStringHTML, fileDownload } from "@45drives/cockpit-helpers"; import Table from "./Table.vue"; import { notificationsInjectionKey, usersInjectionKey, groupsInjectionKey } from "../keys"; import ModalPopup from "./ModalPopup.vue"; +import { useConfig } from "./Config.vue"; export default { setup(props) { + const config = useConfig(); const shares = ref([]); const processing = ref(0); const showAddShare = ref(false); - const exportsFile = reactive(cockpit.file("/etc/exports.d/cockpit-file-sharing.exports", { superuser: 'try', syntax: NfsExportSyntax })); const users = inject(usersInjectionKey); const groups = inject(groupsInjectionKey); const notifications = inject(notificationsInjectionKey); + let exportsFile = null; + let exportsFileWatchHandle = null; const confirmationModal = reactive({ showModal: false, @@ -118,11 +167,10 @@ export default { cancelCallback: () => { }, }); - const loadShares = async () => { + const loadShares = (sharesOnDisk) => { processing.value++; try { - shares.value = await exportsFile.read() ?? []; - shares.value.sort((a, b) => a.path.localeCompare(b.path)); + shares.value = sharesOnDisk.sort((a, b) => a.path.localeCompare(b.path)) ?? []; } catch (error) { notifications.value.constructNotification("Failed to load share configuration", errorStringHTML(error), 'error', 0); } finally { @@ -130,18 +178,8 @@ export default { } }; - const init = async () => { - const procs = []; - procs.push(loadShares()); - for (let proc of procs) - await proc; - }; - - init(); - const writeExportsFile = async () => { try { - await useSpawn(['mkdir', '-p', '/etc/exports.d'], { superuser: 'try' }).promise(); await exportsFile.replace(shares.value); } catch (error) { error.message = `Failed to write exports file: ${errorString(error.message)}`; @@ -160,13 +198,14 @@ export default { const updateShare = async (share, newShare) => { const oldShare = {}; + const rollback = [...shares.value]; processing.value++; try { if (share) { Object.assign(oldShare, share); Object.assign(share, newShare); } else { - shares.value.push({ ...newShare }); + shares.value = [...shares.value, { ...newShare }]; } await writeExportsFile(); await validateExportsFile(); @@ -176,7 +215,7 @@ export default { if (share) { Object.assign(share, oldShare); } else { - shares.value.pop(); + shares.value = rollback; } try { await writeExportsFile(); @@ -184,7 +223,6 @@ export default { notifications.value.constructNotification("Failed to revert exports file", errorStringHTML(error), 'error'); } } finally { - shares.value.sort((a, b) => a.path.localeCompare(b.path)); processing.value--; } } @@ -202,7 +240,6 @@ export default { notifications.value.constructNotification(`Successfully deleted share`, "", 'success'); } catch (error) { notifications.value.constructNotification("Failed to delete share", errorStringHTML(error), 'error'); - loadShares(); } } @@ -226,7 +263,6 @@ export default { notifications.value.constructNotification("Failed to import config", errorStringHTML(error), 'error'); shares.value = oldShares; } - shares.value.sort((a, b) => a.path.localeCompare(b.path)); processing.value--; } reader.onerror = (event) => { @@ -243,6 +279,12 @@ export default { fileDownload(exportsFile.path, filename); } + watch(() => config.nfs.confPath, () => { + exportsFileWatchHandle?.remove(); + exportsFile = cockpit.file(config.nfs.confPath, { superuser: 'try', syntax: NfsExportSyntax }); + exportsFileWatchHandle = exportsFile.watch(loadShares); + }, { immediate: true }); + return { shares, processing, diff --git a/file-sharing/src/components/SambaManager.vue b/file-sharing/src/components/SambaManager.vue index ff4bdeb..11ba054 100644 --- a/file-sharing/src/components/SambaManager.vue +++ b/file-sharing/src/components/SambaManager.vue @@ -65,11 +65,11 @@ If not, see . @click="importSmbConf" class="btn btn-secondary" > - Import configuration from /etc/samba/smb.conf + Import configuration from {{ config.samba.confPath }} File Sharing uses Samba's net registry to configure shares. Click this button to import - configuration from /etc/samba/smb.conf into the net + configuration from {{ config.samba.confPath }} into the net registry for management. @@ -107,9 +107,11 @@ import LoadingSpinner from "./LoadingSpinner.vue"; import { notificationsInjectionKey, usersInjectionKey, groupsInjectionKey } from "../keys"; import ModalPopup from "./ModalPopup.vue"; import InfoTip from "./InfoTip.vue"; +import { useConfig } from "./Config.vue"; export default { setup(props, ctx) { + const config = useConfig(); const shares = ref([]); const globalConfig = reactive({ advancedSettings: [] }); const users = inject(usersInjectionKey); @@ -204,13 +206,13 @@ export default { const checkConf = async () => { processing.value++; try { - const smbConfFile = cockpit.file("/etc/samba/smb.conf", { superuser: 'try' }); + const smbConfFile = cockpit.file(config.samba.confPath, { superuser: 'try' }); const smbConf = await smbConfFile.read(); const globalSectionText = smbConf.match(/^\s*\[ ?global ?\].*$(?:\n^(?!\s*\[).*$)*/mi)?.[0]; if (!globalSectionText || !/^[\t ]*include[\t ]*=[\t ]*registry/m.test(globalSectionText)) { notifications.value.constructNotification( "Samba is Misconfigured", - "`include = registry` is missing from the global section of /etc/samba/smb.conf, which is required for File Sharing to manage shares.", + `'include = registry' is missing from the global section of ${config.samba.confPath}, which is required for File Sharing to manage shares.`, 'error' ).addAction("Fix now", async () => { processing.value++; @@ -228,12 +230,14 @@ export default { } }); } - // fix end of line comment from previous version of file sharing - smbConfFile.modify(content => - content.replace('include = registry # inserted by cockpit-file-sharing', '# inclusion of net registry, inserted by cockpit-file-sharing:\n\tinclude = registry') - ); + if (/include = registry # inserted by cockpit-file-sharing/.test(smbConf)) { + // fix end of line comment from previous version of file sharing + smbConfFile.modify(content => + content.replace('include = registry # inserted by cockpit-file-sharing', '# inclusion of net registry, inserted by cockpit-file-sharing:\n\tinclude = registry') + ); + } } catch (error) { - notifications.value.constructNotification("Failed to validate /etc/samba/smb.conf: ", errorStringHTML(error), 'error'); + notifications.value.constructNotification(`Failed to validate ${config.samba.confPath}: `, errorStringHTML(error), 'error'); } finally { processing.value--; } @@ -277,7 +281,6 @@ export default { try { const procs = []; procs.push(parseNetConf().then(() => shares.value.sort((a, b) => a.name.localeCompare(b.name)))); - procs.push(checkConf()); procs.push(getCtdbHosts()); procs.push(getCephLayoutPools()); await Promise.all(procs); @@ -314,7 +317,7 @@ export default { } const importSmbConf = async () => { - const testContent = (await useSpawn(['net', 'conf', 'import', '-T', '/etc/samba/smb.conf'], { superuser: 'try' }).promise()).stdout; + const testContent = (await useSpawn(['net', 'conf', 'import', '-T', config.samba.confPath], { superuser: 'try' }).promise()).stdout; if (!await confirmationModal.ask( "This will permanently overwrite current configuration", "New configuration content:\n" + @@ -329,12 +332,12 @@ export default { return; try { processing.value++; - const smbConfFile = cockpit.file("/etc/samba/smb.conf", { superuser: 'try' }); + const smbConfFile = cockpit.file(config.samba.confPath, { superuser: 'try' }); const smbConf = await smbConfFile.read(); smbConfFile.close(); await importConfig(smbConf); } catch (error) { - notifications.value.constructNotification("Failed to read /etc/samba/smb.conf", errorStringHTML(error), 'error'); + notifications.value.constructNotification(`Failed to read ${config.samba.confPath}`, errorStringHTML(error), 'error'); return; } finally { processing.value--; @@ -342,9 +345,9 @@ export default { await new Promise(resolve => setTimeout(resolve, 300)); // temporary hack until modals are fixed if (await confirmationModal.ask( 'Replace smb.conf to avoid conflicts?', - 'Your original /etc/samba/smb.conf ' + + `Your original ${config.samba.confPath} ` + 'will be backed up ' + - 'to /etc/samba/smb.conf.bak and replaced with\n' + + `to ${config.samba.confPath}.bak and replaced with\n` + '\n' + '[global]\n' + ' include = registry\n' + @@ -353,10 +356,10 @@ export default { false )) { try { - const backupDescription = (await useSpawn(['cp', '-v', '--backup=numbered', '/etc/samba/smb.conf', '/etc/samba/smb.conf.bak'], { superuser: 'try' }).promise()).stdout; + const backupDescription = (await useSpawn(['cp', '-v', '--backup=numbered', config.samba.confPath, `${config.samba.confPath}.bak`], { superuser: 'try' }).promise()).stdout; notifications.value.constructNotification("Backed up original smb.conf", backupDescription.trim(), 'info'); try { - await cockpit.file('/etc/samba/smb.conf', { superuser: 'try' }).replace( + await cockpit.file(config.samba.confPath, { superuser: 'try' }).replace( '# this config was generated by cockpit-file-sharing after importing smb.conf\n' + `# original smb.conf location:\n` + backupDescription.replace(/^/m, '# ') + @@ -364,11 +367,11 @@ export default { ' include = registry\n'); await useSpawn(['smbcontrol', 'all', 'reload-config'], { superuser: 'try' }).promise(); } catch (error) { - notifications.value.constructNotification("Failed to replace contents of /etc/samba/smb.conf", errorStringHTML(error), 'error'); + notifications.value.constructNotification(`Failed to replace contents of ${config.samba.confPath}`, errorStringHTML(error), 'error'); return; } } catch (error) { - notifications.value.constructNotification("Failed to back up /etc/samba/smb.conf", "Original config is unmodified.\n" + errorStringHTML(error), 'error'); + notifications.value.constructNotification(`Failed to back up ${config.samba.confPath}`, "Original config is unmodified.\n" + errorStringHTML(error), 'error'); return; } } @@ -410,6 +413,8 @@ export default { processOutputDownload(['net', 'conf', 'list'], filename, { superuser: 'try' }); } + watch(() => config.samba.confPath, () => checkConf(), { immediate: true }); + const watchHandles = []; watchHandles.push(cockpit.file('/etc/ctdb/nodes', { superuser: 'try' }).watch(() => getCtdbHosts(), { read: false })); @@ -417,6 +422,7 @@ export default { onBeforeUnmount(() => watchHandles.map(handle => handle?.remove?.())); return { + config, shares, globalConfig, users, diff --git a/file-sharing/src/components/SambaShareEditor.vue b/file-sharing/src/components/SambaShareEditor.vue index 437dc1b..6e3d0fb 100644 --- a/file-sharing/src/components/SambaShareEditor.vue +++ b/file-sharing/src/components/SambaShareEditor.vue @@ -305,7 +305,7 @@ If not, see .