mirror of
https://github.com/45Drives/cockpit-navigator.git
synced 2025-07-30 17:15:16 +02:00
add editing file permissions
This commit is contained in:
parent
06c6a04d59
commit
29bad44c52
@ -80,7 +80,7 @@
|
||||
<span v-if="level > 0">{{ entry.path.split('/').slice(-1 * (level + 1)).join('/') }}:</span>
|
||||
<span v-else>{{ entry.name }}:</span>
|
||||
{{ entry.mode.toString(8) }}, {{ entry.owner }}:{{ entry.group }}, {{ entry.sizeHuman }},
|
||||
modified: {{ entry.mtime }}
|
||||
modified: {{ entry.mtimeStr }}
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
@ -121,10 +121,8 @@ export default {
|
||||
const doubleClickCallback = () => {
|
||||
if (props.entry.resolvedType === 'd') {
|
||||
emit('browserAction', 'cd', props.entry);
|
||||
} else if (props.entry.resolvedType === 'f') {
|
||||
emit('browserAction', 'openFilePrompt', props.entry);
|
||||
} else {
|
||||
notifications.value.constructNotification('Cannot open or download file', `${props.entry.nameHTML} is a ${props.entry.resolvedTypeHuman}`, 'denied');
|
||||
emit('browserAction', 'openFilePrompt', props.entry);
|
||||
}
|
||||
}
|
||||
|
||||
|
98
navigator/src/components/FileModeMatrix.vue
Normal file
98
navigator/src/components/FileModeMatrix.vue
Normal file
@ -0,0 +1,98 @@
|
||||
<!--
|
||||
Copyright (C) 2022 Josh Boudreau <jboudreau@45drives.com>
|
||||
|
||||
This file is part of Cockpit File Sharing.
|
||||
|
||||
Cockpit File Sharing is free software: you can redistribute it and/or modify it under the terms
|
||||
of the GNU General Public License as published by the Free Software Foundation, either version 3
|
||||
of the License, or (at your option) any later version.
|
||||
|
||||
Cockpit File Sharing is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along with Cockpit File Sharing.
|
||||
If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="inline-grid grid-cols-[2fr_1fr_1fr_1fr] gap-2 justify-items-center">
|
||||
<label class="justify-self-start block text-sm font-medium"></label>
|
||||
<label class="text-label">Read</label>
|
||||
<label class="text-label">Write</label>
|
||||
<label class="text-label">Execute</label>
|
||||
|
||||
<label class="justify-self-start text-label">Owner</label>
|
||||
<input type="checkbox" class="input-checkbox" v-model="modeMatrix.owner.read" />
|
||||
<input type="checkbox" class="input-checkbox" v-model="modeMatrix.owner.write" />
|
||||
<input type="checkbox" class="input-checkbox" v-model="modeMatrix.owner.execute" />
|
||||
|
||||
<label class="justify-self-start text-label">Group</label>
|
||||
<input type="checkbox" class="input-checkbox" v-model="modeMatrix.group.read" />
|
||||
<input type="checkbox" class="input-checkbox" v-model="modeMatrix.group.write" />
|
||||
<input type="checkbox" class="input-checkbox" v-model="modeMatrix.group.execute" />
|
||||
|
||||
<label class="justify-self-start text-label">Other</label>
|
||||
<input type="checkbox" class="input-checkbox" v-model="modeMatrix.other.read" />
|
||||
<input type="checkbox" class="input-checkbox" v-model="modeMatrix.other.write" />
|
||||
<input type="checkbox" class="input-checkbox" v-model="modeMatrix.other.execute" />
|
||||
|
||||
<label class="justify-self-start text-label">Mode</label>
|
||||
<span class="col-span-3 font-mono font-medium whitespace-nowrap">{{ modeStr }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { reactive, computed } from 'vue';
|
||||
export default {
|
||||
props: {
|
||||
modelValue: Number,
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const modeStr = computed(
|
||||
() =>
|
||||
(props.modelValue & 0b100000000 ? 'r' : '-')
|
||||
+ (props.modelValue & 0b010000000 ? 'w' : '-')
|
||||
+ (props.modelValue & 0b001000000 ? 'x' : '-')
|
||||
+ (props.modelValue & 0b000100000 ? 'r' : '-')
|
||||
+ (props.modelValue & 0b000010000 ? 'w' : '-')
|
||||
+ (props.modelValue & 0b000001000 ? 'x' : '-')
|
||||
+ (props.modelValue & 0b000000100 ? 'r' : '-')
|
||||
+ (props.modelValue & 0b000000010 ? 'w' : '-')
|
||||
+ (props.modelValue & 0b000000001 ? 'x' : '-')
|
||||
+ ` (${props.modelValue.toString(8).padStart(3, '0')})`
|
||||
);
|
||||
|
||||
const computedBit = (mask, getter) => computed({
|
||||
get: () => getter() & mask ? true : false,
|
||||
set: (value) => emit('update:modelValue', value ? (props.modelValue | mask) : (props.modelValue & (~mask))),
|
||||
})
|
||||
|
||||
const modeMatrix = reactive({
|
||||
other: {
|
||||
execute: computedBit(0b000000001, () => props.modelValue),
|
||||
write: computedBit(0b000000010, () => props.modelValue),
|
||||
read: computedBit(0b000000100, () => props.modelValue),
|
||||
},
|
||||
group: {
|
||||
execute: computedBit(0b000001000, () => props.modelValue),
|
||||
write: computedBit(0b000010000, () => props.modelValue),
|
||||
read: computedBit(0b000100000, () => props.modelValue),
|
||||
},
|
||||
owner: {
|
||||
execute: computedBit(0b001000000, () => props.modelValue),
|
||||
write: computedBit(0b010000000, () => props.modelValue),
|
||||
read: computedBit(0b100000000, () => props.modelValue),
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
modeMatrix,
|
||||
modeStr,
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
'update:modelValue',
|
||||
],
|
||||
}
|
||||
</script>
|
128
navigator/src/components/FilePermissions.vue
Normal file
128
navigator/src/components/FilePermissions.vue
Normal file
@ -0,0 +1,128 @@
|
||||
<!--
|
||||
Copyright (C) 2022 Josh Boudreau <jboudreau@45drives.com>
|
||||
|
||||
This file is part of Cockpit File Sharing.
|
||||
|
||||
Cockpit File Sharing is free software: you can redistribute it and/or modify it under the terms
|
||||
of the GNU General Public License as published by the Free Software Foundation, either version 3
|
||||
of the License, or (at your option) any later version.
|
||||
|
||||
Cockpit File Sharing is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along with Cockpit File Sharing.
|
||||
If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<ModalPopup :showModal="show" :headerText="entry?.nameHTML" @apply="apply"
|
||||
@cancel="$emit('hide')">
|
||||
<div class="flex flex-col space-y-content items-start">
|
||||
<FileModeMatrix v-model="mode" />
|
||||
<div>
|
||||
<label class="block text-sm font-medium">Owner</label>
|
||||
<select class="input-textlike" v-model="owner">
|
||||
<option v-for="user in users" :value="user.user">{{ user.pretty }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium">Group</label>
|
||||
<select class="input-textlike" v-model="group">
|
||||
<option v-for="group in groups" :value="group.group">{{ group.pretty }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</ModalPopup>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { watch, ref, onMounted } from 'vue';
|
||||
import ModalPopup from "./ModalPopup.vue";
|
||||
import FileModeMatrix from "./FileModeMatrix.vue";
|
||||
import { useSpawn, errorString, canonicalPath } from "@45drives/cockpit-helpers";
|
||||
import { getUsers } from '../functions/getUsers';
|
||||
import { getGroups } from '../functions/getGroups';
|
||||
export default {
|
||||
props: {
|
||||
show: Boolean,
|
||||
entry: Object,
|
||||
onError: {
|
||||
type: Function,
|
||||
required: false,
|
||||
default: console.error,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const mode = ref(0);
|
||||
const owner = ref("");
|
||||
const group = ref("");
|
||||
const users = ref([]);
|
||||
const groups = ref([]);
|
||||
|
||||
const getPermissions = async () => {
|
||||
try {
|
||||
let modeStr;
|
||||
[modeStr, owner.value, group.value] = (
|
||||
await useSpawn(['stat', '--format=%a:%U:%G', props.entry.path], { superuser: 'try', host: props.entry.host }).promise()
|
||||
).stdout.trim().split(':');
|
||||
mode.value = parseInt(modeStr, 8);
|
||||
} catch (state) {
|
||||
const error = new Error(errorString(state));
|
||||
error.name = "Permissions Query Error";
|
||||
props.onError(error);
|
||||
emit('hide');
|
||||
}
|
||||
}
|
||||
|
||||
const apply = async () => {
|
||||
if (canonicalPath(props.entry.path) === '/') {
|
||||
const error = new Error("Cannot Edit Permissions for '/'. If you think you need to do this, you don't.");
|
||||
error.name = "Permissions Apply Error";
|
||||
props.onError(error);
|
||||
emit('hide');
|
||||
return;
|
||||
}
|
||||
const procs = [];
|
||||
procs.push(useSpawn(['chown', owner.value, props.entry.path], { superuser: 'try', host: props.entry.host }).promise());
|
||||
procs.push(useSpawn(['chgrp', group.value, props.entry.path], { superuser: 'try', host: props.entry.host }).promise());
|
||||
procs.push(useSpawn(['chmod', mode.value.toString(8), props.entry.path], { superuser: 'try', host: props.entry.host }).promise());
|
||||
for (const proc of procs) {
|
||||
try {
|
||||
await proc;
|
||||
} catch (state) {
|
||||
const error = new Error(errorString(state));
|
||||
error.name = "Permissions Apply Error";
|
||||
props.onError(error);
|
||||
}
|
||||
}
|
||||
emit('hide');
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
users.value = await getUsers();
|
||||
groups.value = await getGroups();
|
||||
});
|
||||
|
||||
watch(() => props.show, (show, lastShow) => {
|
||||
if (show && props.entry) getPermissions();
|
||||
}, { immediate: true });
|
||||
|
||||
return {
|
||||
mode,
|
||||
owner,
|
||||
group,
|
||||
users,
|
||||
groups,
|
||||
apply,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
ModalPopup,
|
||||
FileModeMatrix,
|
||||
},
|
||||
emits: [
|
||||
'hide',
|
||||
]
|
||||
}
|
||||
</script>
|
24
navigator/src/functions/getGroups.js
Normal file
24
navigator/src/functions/getGroups.js
Normal file
@ -0,0 +1,24 @@
|
||||
import { useSpawn } from "@45drives/cockpit-helpers";
|
||||
|
||||
export async function getGroups() {
|
||||
let groups = [];
|
||||
const groupDB = (await useSpawn(['getent', 'group'], { superuser: 'try' }).promise()).stdout;
|
||||
groupDB.split('\n').forEach((record) => {
|
||||
const fields = record.split(':');
|
||||
const group = fields[0];
|
||||
const gid = fields[2];
|
||||
if (gid >= 1000 || gid === '0')
|
||||
groups.push({ group: group, domain: false, pretty: group });
|
||||
})
|
||||
try {
|
||||
await useSpawn(['realm', 'list'], { superuser: 'try' }).promise(); // throws if not domain
|
||||
const domainGroupsDB = (await useSpawn(['wbinfo', '-g'], { superuser: 'try' }).promise()).stdout
|
||||
domainGroupsDB.split('\n').forEach((record) => {
|
||||
if (/^\s*$/.test(record))
|
||||
return;
|
||||
groups.push({ group: record.replace(/^[^\\]+\\/, ""), domain: true, pretty: record.replace(/^[^\\]+\\/, "") + " (domain)" });
|
||||
})
|
||||
} catch {}
|
||||
groups.sort((a, b) => a.pretty.localeCompare(b.pretty));
|
||||
return groups
|
||||
}
|
24
navigator/src/functions/getUsers.js
Normal file
24
navigator/src/functions/getUsers.js
Normal file
@ -0,0 +1,24 @@
|
||||
import { useSpawn } from "@45drives/cockpit-helpers";
|
||||
|
||||
export async function getUsers() {
|
||||
let users = [];
|
||||
const passwdDB = (await useSpawn(['getent', 'passwd'], { superuser: 'try' }).promise()).stdout;
|
||||
passwdDB.split('\n').forEach((record) => {
|
||||
const fields = record.split(':');
|
||||
const user = fields[0];
|
||||
const uid = fields[2];
|
||||
if (uid >= 1000 || uid === '0') // include root
|
||||
users.push({ user: user, domain: false, pretty: user });
|
||||
})
|
||||
try {
|
||||
await useSpawn(['realm', 'list'], { superuser: 'try' }).promise(); // throws if not domain
|
||||
const domainUsersDB = (await useSpawn(['wbinfo', '-u'], { superuser: 'try' }).promise()).stdout;
|
||||
domainUsersDB.split('\n').forEach((record) => {
|
||||
if (/^\s*$/.test(record))
|
||||
return;
|
||||
users.push({ user: record.replace(/^[^\\]+\\/, ""), domain: true, pretty: record.replace(/^[^\\]+\\/, "") + " (domain)" });
|
||||
})
|
||||
} catch {}
|
||||
users.sort((a, b) => a.pretty.localeCompare(b.pretty));
|
||||
return users;
|
||||
}
|
@ -68,25 +68,31 @@
|
||||
<div class="grow overflow-hidden">
|
||||
<DirectoryView :host="pathHistory.current()?.host" :path="pathHistory.current()?.path"
|
||||
:searchFilterRegExp="searchFilterRegExp" @cd="path => cd({ path })" @edit="openEditor"
|
||||
@browserAction="handleAction"
|
||||
ref="directoryViewRef" />
|
||||
@browserAction="handleAction" ref="directoryViewRef" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ModalPopup :showModal="openFilePromptModal.show" :headerText="openFilePromptModal.entry?.name ?? 'NULL'" @close="() => openFilePromptModal.close()">
|
||||
What would you like to do with this file?
|
||||
<ModalPopup :showModal="openFilePromptModal.show" :headerText="openFilePromptModal.entry?.name ?? 'NULL'"
|
||||
@close="() => openFilePromptModal.close()" autoWidth>
|
||||
What would you like to do with this {{ openFilePromptModal.entry?.resolvedTypeHuman }}?
|
||||
<template #footer>
|
||||
<button type="button" class="btn btn-secondary" @click="() => openFilePromptModal.close()">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" @click="() => openFilePromptModal.action('edit') ">
|
||||
<button type="button" class="btn btn-primary" @click="() => openFilePromptModal.action('editPermissions')">
|
||||
Edit permissions
|
||||
</button>
|
||||
<button v-if="openFilePromptModal.entry?.resolvedType === 'f'" type="button" class="btn btn-primary"
|
||||
@click="() => openFilePromptModal.action('edit')">
|
||||
Open for editing
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" @click="() => openFilePromptModal.action('download') ">
|
||||
<button v-if="openFilePromptModal.entry?.resolvedType === 'f'" type="button" class="btn btn-primary"
|
||||
@click="() => openFilePromptModal.action('download')">
|
||||
Download
|
||||
</button>
|
||||
</template>
|
||||
</ModalPopup>
|
||||
<FilePermissions :show="filePermissions.show" @hide="filePermissions.close" :entry="filePermissions.entry" />
|
||||
<Teleport to="#footer-buttons">
|
||||
<IconToggle v-model="darkMode" v-slot="{ value }">
|
||||
<MoonIcon v-if="value" class="size-icon icon-default" />
|
||||
@ -113,6 +119,7 @@ import { ArrowLeftIcon, ArrowRightIcon, ArrowUpIcon, RefreshIcon, ChevronDownIco
|
||||
import IconToggle from '../components/IconToggle.vue';
|
||||
import ModalPopup from '../components/ModalPopup.vue';
|
||||
import { fileDownload } from '@45drives/cockpit-helpers';
|
||||
import FilePermissions from '../components/FilePermissions.vue';
|
||||
|
||||
const encodePartial = (string) =>
|
||||
encodeURIComponent(string)
|
||||
@ -179,6 +186,20 @@ export default {
|
||||
openFilePromptModal.close();
|
||||
}
|
||||
});
|
||||
const filePermissions = reactive({
|
||||
show: false,
|
||||
entry: null,
|
||||
resetTimeoutHandle: null,
|
||||
open: (entry) => {
|
||||
clearTimeout(filePermissions.resetTimeoutHandle);
|
||||
filePermissions.entry = entry;
|
||||
filePermissions.show = true;
|
||||
},
|
||||
close: () => {
|
||||
filePermissions.show = false;
|
||||
filePermissions.resetTimeoutHandle = setTimeout(() => filePermissions.resetTimeoutHandle = filePermissions.entry = null, 500);
|
||||
},
|
||||
});
|
||||
|
||||
const cd = ({ path, host }) => {
|
||||
const newHost = host ?? (pathHistory.current().host);
|
||||
@ -195,7 +216,7 @@ export default {
|
||||
}
|
||||
|
||||
const up = () => {
|
||||
cd({path: pathHistory.current().path + '/..'});
|
||||
cd({ path: pathHistory.current().path + '/..' });
|
||||
}
|
||||
|
||||
const openEditor = ({ path, host }) => {
|
||||
@ -213,6 +234,10 @@ export default {
|
||||
openFilePromptModal.open(entry);
|
||||
}
|
||||
|
||||
const openFilePermissions = (entry) => {
|
||||
filePermissions.open(entry);
|
||||
}
|
||||
|
||||
const getSelected = () => directoryViewRef.value?.getSelected?.() ?? [];
|
||||
|
||||
const handleAction = (action, ...args) => {
|
||||
@ -223,6 +248,9 @@ export default {
|
||||
case 'edit':
|
||||
openEditor(...args);
|
||||
break;
|
||||
case 'editPermissions':
|
||||
openFilePermissions(...args);
|
||||
break;
|
||||
case 'openFilePrompt':
|
||||
openFilePrompt(...args);
|
||||
break;
|
||||
@ -267,6 +295,7 @@ export default {
|
||||
backHistoryDropdown,
|
||||
forwardHistoryDropdown,
|
||||
openFilePromptModal,
|
||||
filePermissions,
|
||||
cd,
|
||||
back,
|
||||
forward,
|
||||
@ -274,6 +303,7 @@ export default {
|
||||
openEditor,
|
||||
download,
|
||||
openFilePrompt,
|
||||
openFilePermissions,
|
||||
getSelected,
|
||||
handleAction,
|
||||
}
|
||||
@ -295,6 +325,7 @@ export default {
|
||||
ViewListIcon,
|
||||
ViewGridIcon,
|
||||
ModalPopup,
|
||||
FilePermissions,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
Loading…
x
Reference in New Issue
Block a user