7cbddf5d48
Co-authored-by: Cursor <cursoragent@cursor.com>
102 lines
3.0 KiB
TypeScript
102 lines
3.0 KiB
TypeScript
import { computed } from 'vue'
|
|
import { storeToRefs } from 'pinia'
|
|
import { http } from '@/api'
|
|
import { useSnackbar } from '@/composables/useSnackbar'
|
|
import { useFilesStore } from '@/stores/files'
|
|
import type { FilesCapabilityResult } from '@/types/api'
|
|
import {
|
|
buildFilesAccessHint,
|
|
isEntryLikelyUnreadable,
|
|
} from '@/utils/fileMode'
|
|
import { FILES_HINT_DISMISSED_KEY } from './constants'
|
|
import { isNotFoundError } from './useFilesBrowse'
|
|
|
|
export function useFilesPermissions(options: {
|
|
browse: (opts?: { force?: boolean }) => Promise<void | undefined>
|
|
}) {
|
|
const snackbar = useSnackbar()
|
|
const store = useFilesStore()
|
|
const {
|
|
selectedServer,
|
|
files,
|
|
browseError,
|
|
filesCapability,
|
|
lastReadDeniedPath,
|
|
accessHintDismissed,
|
|
setupSudoLoading,
|
|
setupSudoDone,
|
|
} = storeToRefs(store)
|
|
|
|
const sshUserForFiles = computed(() => filesCapability.value?.ssh_user ?? null)
|
|
|
|
const unreadableFileCount = computed(() => {
|
|
const user = sshUserForFiles.value
|
|
if (!user) return 0
|
|
return files.value.filter((f) => isEntryLikelyUnreadable(f, user)).length
|
|
})
|
|
|
|
const filesAccessHint = computed(() => {
|
|
if (accessHintDismissed.value || !selectedServer.value) return null
|
|
if (browseError.value && isNotFoundError(browseError.value)) return null
|
|
const cap = filesCapability.value
|
|
return buildFilesAccessHint({
|
|
sshUser: cap?.ssh_user,
|
|
canSudo: cap?.can_sudo_nopasswd ?? false,
|
|
whitelistOk: cap?.whitelist_ok ?? false,
|
|
isRootSsh: cap?.is_root ?? false,
|
|
filesElevation: cap?.files_elevation,
|
|
capabilityMessage: cap?.message,
|
|
lastReadDeniedPath: lastReadDeniedPath.value,
|
|
unreadableCount: unreadableFileCount.value,
|
|
})
|
|
})
|
|
|
|
function dismissFilesAccessHint() {
|
|
accessHintDismissed.value = true
|
|
sessionStorage.setItem(FILES_HINT_DISMISSED_KEY, '1')
|
|
}
|
|
|
|
async function refreshFilesCapability() {
|
|
if (!selectedServer.value) {
|
|
filesCapability.value = null
|
|
return
|
|
}
|
|
try {
|
|
filesCapability.value = await http.get<FilesCapabilityResult>(
|
|
`/servers/${selectedServer.value}/files-capability`,
|
|
)
|
|
} catch {
|
|
filesCapability.value = null
|
|
}
|
|
}
|
|
|
|
async function setupFilesSudo() {
|
|
if (!selectedServer.value) return
|
|
setupSudoLoading.value = true
|
|
try {
|
|
const res = await http.post<{ ok: boolean; message: string }>(
|
|
`/servers/${selectedServer.value}/setup-files-sudo`,
|
|
{},
|
|
)
|
|
setupSudoDone.value = true
|
|
snackbar(res.message || 'sudoers 配置成功,文件管理器现在可自动提权', 'success')
|
|
dismissFilesAccessHint()
|
|
await refreshFilesCapability()
|
|
await options.browse({ force: true })
|
|
} catch (e: unknown) {
|
|
const msg = e instanceof Error ? e.message : '配置失败'
|
|
snackbar(`一键配置失败:${msg}`, 'error')
|
|
} finally {
|
|
setupSudoLoading.value = false
|
|
}
|
|
}
|
|
|
|
return {
|
|
sshUserForFiles,
|
|
filesAccessHint,
|
|
dismissFilesAccessHint,
|
|
refreshFilesCapability,
|
|
setupFilesSudo,
|
|
}
|
|
}
|