Split app UI layout packages
This commit is contained in:
@@ -0,0 +1,778 @@
|
||||
package appui
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"gioui.org/layout"
|
||||
"gioui.org/x/explorer"
|
||||
"git.julianfamily.org/keepassgo/internal/appstate"
|
||||
"git.julianfamily.org/keepassgo/internal/session"
|
||||
"git.julianfamily.org/keepassgo/internal/vault"
|
||||
"git.julianfamily.org/keepassgo/internal/webdav"
|
||||
)
|
||||
|
||||
func (u *ui) createVaultAction() error {
|
||||
key, err := u.currentMasterKey()
|
||||
defer u.clearMasterPassword()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := u.state.ConfigureSecurity(vault.SecuritySettings{
|
||||
Cipher: strings.TrimSpace(u.securityCipher.Text()),
|
||||
KDF: strings.TrimSpace(u.securityKDF.Text()),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := u.state.CreateVault(key); err != nil {
|
||||
return err
|
||||
}
|
||||
if u.lifecycleMode == "local" {
|
||||
u.selectedVaultRemoteProfileID = ""
|
||||
u.selectedVaultRemoteCredentialEntryID = ""
|
||||
u.selectedVaultRemoteSyncMode = appstate.SyncModeManual
|
||||
u.remoteBaseURL.SetText("")
|
||||
u.remotePath.SetText("")
|
||||
u.remoteUsername.SetText("")
|
||||
u.remotePassword.SetText("")
|
||||
if err := u.state.SaveAs(u.saveAsTargetPath()); err != nil {
|
||||
return err
|
||||
}
|
||||
u.vaultPath.SetText(u.saveAsTargetPath())
|
||||
u.noteRecentVault(u.saveAsTargetPath())
|
||||
}
|
||||
u.resetPasswordPeek()
|
||||
u.currentPath = append([]string(nil), u.state.CurrentPath...)
|
||||
u.loadSecuritySettingsFromSession()
|
||||
u.editingEntry = false
|
||||
u.filter()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ui) openVaultAction() error {
|
||||
key, err := u.currentMasterKey()
|
||||
defer u.clearMasterPassword()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path := strings.TrimSpace(u.vaultPath.Text())
|
||||
if path == "" {
|
||||
return errors.New(errVaultPathRequired)
|
||||
}
|
||||
if err := u.state.OpenVault(path, key); err != nil {
|
||||
return err
|
||||
}
|
||||
u.noteRecentVault(path)
|
||||
u.resetPasswordPeek()
|
||||
u.currentPath = append([]string(nil), u.state.CurrentPath...)
|
||||
u.restoreRecentVaultGroup(path)
|
||||
u.syncSavedRemoteBindingSelection()
|
||||
if err := u.synchronizeSelectedRemoteBindingOnOpen(); err != nil {
|
||||
u.showStatusMessage("Remote sync on open failed: " + err.Error())
|
||||
}
|
||||
u.loadSecuritySettingsFromSession()
|
||||
u.editingEntry = false
|
||||
u.filter()
|
||||
u.applyPendingLifecycleOpenIntent()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ui) startOpenVaultAction() {
|
||||
manager, ok := u.state.Session.(*session.Manager)
|
||||
if !ok {
|
||||
u.runAction("open vault", u.openVaultAction)
|
||||
return
|
||||
}
|
||||
key, err := u.currentMasterKey()
|
||||
u.clearMasterPassword()
|
||||
if err != nil {
|
||||
u.state.ErrorMessage = u.describeActionError("open vault", err)
|
||||
u.requestMasterPassFocus = true
|
||||
return
|
||||
}
|
||||
path := strings.TrimSpace(u.vaultPath.Text())
|
||||
if path == "" {
|
||||
u.state.ErrorMessage = u.describeActionError("open vault", errors.New(errVaultPathRequired))
|
||||
u.requestMasterPassFocus = true
|
||||
return
|
||||
}
|
||||
u.lastLifecycleAction = "open vault"
|
||||
u.runBackgroundAction("open vault", func() (func() error, error) {
|
||||
prepared, err := session.PrepareLocalOpen(path, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return func() error {
|
||||
manager.ApplyPreparedLocalOpen(prepared)
|
||||
u.noteRecentVault(path)
|
||||
u.resetPasswordPeek()
|
||||
u.currentPath = append([]string(nil), u.state.CurrentPath...)
|
||||
u.restoreRecentVaultGroup(path)
|
||||
u.syncSavedRemoteBindingSelection()
|
||||
if err := u.synchronizeSelectedRemoteBindingOnOpen(); err != nil {
|
||||
u.showStatusMessage("Remote sync on open failed: " + err.Error())
|
||||
}
|
||||
u.loadSecuritySettingsFromSession()
|
||||
u.editingEntry = false
|
||||
u.filter()
|
||||
u.applyPendingLifecycleOpenIntent()
|
||||
return nil
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ui) shouldShowLifecycleRemoteSyncAction() bool {
|
||||
return strings.TrimSpace(u.vaultPath.Text()) != ""
|
||||
}
|
||||
|
||||
func (u *ui) lifecycleRemoteSyncActionLabel() string {
|
||||
path := strings.TrimSpace(u.vaultPath.Text())
|
||||
if path == "" {
|
||||
return "Open Vault And Set Up Remote Sync"
|
||||
}
|
||||
if hasBoundRecentRemote(u.recentRemotes, path) {
|
||||
return "Open Vault And Open Remote Sync Settings"
|
||||
}
|
||||
return "Open Vault And Set Up Remote Sync"
|
||||
}
|
||||
|
||||
func (u *ui) beginLifecycleRemoteSyncOpen() {
|
||||
path := strings.TrimSpace(u.vaultPath.Text())
|
||||
switch {
|
||||
case path == "":
|
||||
u.pendingLifecycleOpenIntent = lifecycleOpenIntentNone
|
||||
case hasBoundRecentRemote(u.recentRemotes, path):
|
||||
u.pendingLifecycleOpenIntent = lifecycleOpenIntentRemoteSyncSettings
|
||||
default:
|
||||
u.pendingLifecycleOpenIntent = lifecycleOpenIntentRemoteSyncSetup
|
||||
}
|
||||
u.startOpenVaultAction()
|
||||
}
|
||||
|
||||
func (u *ui) applyPendingLifecycleOpenIntent() {
|
||||
intent := u.pendingLifecycleOpenIntent
|
||||
u.pendingLifecycleOpenIntent = lifecycleOpenIntentNone
|
||||
switch intent {
|
||||
case lifecycleOpenIntentRemoteSyncSetup, lifecycleOpenIntentRemoteSyncSettings:
|
||||
u.openRemoteSyncSetupDialog()
|
||||
}
|
||||
}
|
||||
|
||||
func (u *ui) saveAction() error {
|
||||
if err := u.state.Save(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := u.synchronizeSelectedRemoteBindingOnSave(); err != nil {
|
||||
return err
|
||||
}
|
||||
u.filter()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ui) saveAsAction() error {
|
||||
path := u.saveAsTargetPath()
|
||||
if err := u.state.SaveAs(path); err != nil {
|
||||
return err
|
||||
}
|
||||
u.vaultPath.SetText(path)
|
||||
u.noteRecentVault(path)
|
||||
u.filter()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ui) openRemoteAction() error {
|
||||
key, err := u.currentMasterKey()
|
||||
defer u.clearMasterPassword()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if binding, resolved, ok, err := u.bootstrapSelectedVaultRemoteBinding(key); err != nil {
|
||||
return err
|
||||
} else if ok {
|
||||
if err := u.state.OpenBoundRemoteVault(binding, key); err != nil {
|
||||
return err
|
||||
}
|
||||
u.remoteBaseURL.SetText(resolved.Profile.BaseURL)
|
||||
u.remotePath.SetText(resolved.Profile.Path)
|
||||
u.noteRecentRemote(resolved.Profile.BaseURL, resolved.Profile.Path)
|
||||
u.resetPasswordPeek()
|
||||
u.restoreRecentRemoteGroup(resolved.Profile.BaseURL, resolved.Profile.Path)
|
||||
u.loadSecuritySettingsFromSession()
|
||||
u.editingEntry = false
|
||||
u.filter()
|
||||
return nil
|
||||
}
|
||||
client := webdav.Client{
|
||||
BaseURL: strings.TrimSpace(u.remoteBaseURL.Text()),
|
||||
Username: strings.TrimSpace(u.remoteUsername.Text()),
|
||||
Password: u.remotePassword.Text(),
|
||||
}
|
||||
if err := u.state.OpenRemoteVault(client, strings.TrimSpace(u.remotePath.Text()), key); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := u.materializeCurrentRemoteCache(); err != nil {
|
||||
return err
|
||||
}
|
||||
u.noteRecentRemote(
|
||||
strings.TrimSpace(u.remoteBaseURL.Text()),
|
||||
strings.TrimSpace(u.remotePath.Text()),
|
||||
)
|
||||
u.resetPasswordPeek()
|
||||
u.restoreRecentRemoteGroup(strings.TrimSpace(u.remoteBaseURL.Text()), strings.TrimSpace(u.remotePath.Text()))
|
||||
u.loadSecuritySettingsFromSession()
|
||||
u.editingEntry = false
|
||||
u.filter()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ui) startOpenRemoteAction() {
|
||||
manager, ok := u.state.Session.(*session.Manager)
|
||||
if !ok {
|
||||
u.runAction("open remote vault", u.openRemoteAction)
|
||||
return
|
||||
}
|
||||
key, err := u.currentMasterKey()
|
||||
u.clearMasterPassword()
|
||||
if err != nil {
|
||||
u.state.ErrorMessage = u.describeActionError("open remote vault", err)
|
||||
u.requestMasterPassFocus = true
|
||||
return
|
||||
}
|
||||
client := webdav.Client{
|
||||
BaseURL: strings.TrimSpace(u.remoteBaseURL.Text()),
|
||||
Username: strings.TrimSpace(u.remoteUsername.Text()),
|
||||
Password: u.remotePassword.Text(),
|
||||
}
|
||||
remotePath := strings.TrimSpace(u.remotePath.Text())
|
||||
u.lastLifecycleAction = "open remote vault"
|
||||
u.runBackgroundAction("open remote vault", func() (func() error, error) {
|
||||
binding, bindingOK := u.selectedVaultRemoteBinding()
|
||||
if bindingOK && !u.hasOpenVault() && strings.TrimSpace(binding.LocalVaultPath) != "" {
|
||||
preparedLocal, err := session.PrepareLocalOpen(binding.LocalVaultPath, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resolved, err := binding.Resolve(preparedLocal.Model)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
preparedRemote, err := session.PrepareRemoteOpen(webdav.Client{
|
||||
BaseURL: resolved.Profile.BaseURL,
|
||||
Username: resolved.Credentials.Username,
|
||||
Password: resolved.Credentials.Password,
|
||||
}, resolved.Profile.Path, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return func() error {
|
||||
manager.ApplyPreparedLocalOpen(preparedLocal)
|
||||
u.vaultPath.SetText(binding.LocalVaultPath)
|
||||
u.noteRecentVault(binding.LocalVaultPath)
|
||||
u.restoreRecentVaultGroup(binding.LocalVaultPath)
|
||||
manager.ApplyPreparedRemoteOpen(preparedRemote)
|
||||
u.remoteBaseURL.SetText(resolved.Profile.BaseURL)
|
||||
u.remotePath.SetText(resolved.Profile.Path)
|
||||
u.noteRecentRemote(resolved.Profile.BaseURL, resolved.Profile.Path)
|
||||
u.resetPasswordPeek()
|
||||
u.restoreRecentRemoteGroup(resolved.Profile.BaseURL, resolved.Profile.Path)
|
||||
u.loadSecuritySettingsFromSession()
|
||||
u.editingEntry = false
|
||||
u.filter()
|
||||
return nil
|
||||
}, nil
|
||||
}
|
||||
if u.hasOpenVault() {
|
||||
if _, resolved, ok, err := u.resolvedSelectedVaultRemoteBinding(); err != nil {
|
||||
return nil, err
|
||||
} else if ok {
|
||||
client = webdav.Client{
|
||||
BaseURL: resolved.Profile.BaseURL,
|
||||
Username: resolved.Credentials.Username,
|
||||
Password: resolved.Credentials.Password,
|
||||
}
|
||||
remotePath = resolved.Profile.Path
|
||||
u.remoteBaseURL.SetText(resolved.Profile.BaseURL)
|
||||
u.remotePath.SetText(resolved.Profile.Path)
|
||||
}
|
||||
}
|
||||
prepared, err := session.PrepareRemoteOpen(client, remotePath, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return func() error {
|
||||
manager.ApplyPreparedRemoteOpen(prepared)
|
||||
if err := u.materializeCurrentRemoteCache(); err != nil {
|
||||
return err
|
||||
}
|
||||
u.noteRecentRemote(
|
||||
strings.TrimSpace(u.remoteBaseURL.Text()),
|
||||
remotePath,
|
||||
)
|
||||
u.resetPasswordPeek()
|
||||
u.restoreRecentRemoteGroup(strings.TrimSpace(u.remoteBaseURL.Text()), remotePath)
|
||||
u.loadSecuritySettingsFromSession()
|
||||
u.editingEntry = false
|
||||
u.filter()
|
||||
return nil
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ui) lockAction() error {
|
||||
u.clearMasterPassword()
|
||||
if err := u.state.Lock(); err != nil {
|
||||
return err
|
||||
}
|
||||
u.requestMasterPassFocus = true
|
||||
u.currentPath = append([]string(nil), u.state.CurrentPath...)
|
||||
u.resetPasswordPeek()
|
||||
u.editingEntry = false
|
||||
u.filter()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ui) unlockAction() error {
|
||||
key, err := u.currentMasterKey()
|
||||
defer u.clearMasterPassword()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := u.state.Unlock(key); err != nil {
|
||||
return err
|
||||
}
|
||||
u.resetPasswordPeek()
|
||||
u.currentPath = append([]string(nil), u.state.CurrentPath...)
|
||||
u.loadSecuritySettingsFromSession()
|
||||
u.editingEntry = false
|
||||
u.filter()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ui) startUnlockAction() {
|
||||
manager, ok := u.state.Session.(*session.Manager)
|
||||
if !ok {
|
||||
u.runAction("unlock vault", u.unlockAction)
|
||||
return
|
||||
}
|
||||
key, err := u.currentMasterKey()
|
||||
u.clearMasterPassword()
|
||||
if err != nil {
|
||||
u.state.ErrorMessage = u.describeActionError("unlock vault", err)
|
||||
u.requestMasterPassFocus = true
|
||||
return
|
||||
}
|
||||
encoded := append([]byte(nil), manager.EncodedBytes()...)
|
||||
u.runBackgroundAction("unlock vault", func() (func() error, error) {
|
||||
prepared, err := session.PrepareUnlock(encoded, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return func() error {
|
||||
manager.ApplyPreparedUnlock(prepared)
|
||||
u.resetPasswordPeek()
|
||||
u.currentPath = append([]string(nil), u.state.CurrentPath...)
|
||||
u.loadSecuritySettingsFromSession()
|
||||
u.editingEntry = false
|
||||
u.filter()
|
||||
return nil
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ui) changeMasterKeyAction() error {
|
||||
key, err := u.currentMasterKey()
|
||||
defer u.clearMasterPassword()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return u.state.ChangeMasterKey(key)
|
||||
}
|
||||
|
||||
func (u *ui) loadSecuritySettingsFromSession() {
|
||||
settings, err := u.state.SecuritySettings()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
u.securityCipher.SetText(settings.Cipher)
|
||||
u.securityKDF.SetText(settings.KDF)
|
||||
}
|
||||
|
||||
func (u *ui) clearMasterPassword() {
|
||||
u.masterPassword.SetText("")
|
||||
}
|
||||
|
||||
func (u *ui) synchronizeAction() error {
|
||||
if err := u.state.Synchronize(); err != nil {
|
||||
return err
|
||||
}
|
||||
u.syncMenuOpen = false
|
||||
u.filter()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ui) openAdvancedSyncDialog() {
|
||||
u.syncDialogOpen = true
|
||||
u.syncMenuOpen = false
|
||||
u.showSyncPassword = false
|
||||
u.syncDialogList.Position = layout.Position{}
|
||||
u.syncDialogPurpose = syncDialogPurposeAdvanced
|
||||
u.syncSourceMode = u.syncDefaultSourceMode
|
||||
u.syncDirection = u.syncDefaultDirection
|
||||
if strings.TrimSpace(u.syncLocalPath.Text()) == "" {
|
||||
u.syncLocalPath.SetText(strings.TrimSpace(u.vaultPath.Text()))
|
||||
}
|
||||
u.syncSavedRemoteBindingSelection()
|
||||
u.prefillAdvancedSyncRemoteFromSavedBinding()
|
||||
}
|
||||
|
||||
func (u *ui) openRemoteSyncSetupDialog() {
|
||||
u.syncDialogOpen = true
|
||||
u.syncMenuOpen = false
|
||||
u.showSyncPassword = false
|
||||
u.syncDialogList.Position = layout.Position{}
|
||||
u.syncDialogPurpose = syncDialogPurposeRemoteSetup
|
||||
u.syncSourceMode = syncSourceRemote
|
||||
u.syncDirection = syncDirectionPush
|
||||
u.syncSetupAutomatic.Value = true
|
||||
if strings.TrimSpace(u.syncLocalPath.Text()) == "" {
|
||||
u.syncLocalPath.SetText(strings.TrimSpace(u.vaultPath.Text()))
|
||||
}
|
||||
u.syncSavedRemoteBindingSelection()
|
||||
u.prefillAdvancedSyncRemoteFromSavedBinding()
|
||||
if _, ok := u.selectedVaultRemoteBinding(); ok && u.selectedVaultRemoteSyncMode == appstate.SyncModeManual {
|
||||
u.syncSetupAutomatic.Value = false
|
||||
}
|
||||
}
|
||||
|
||||
func (u *ui) clearSyncLocalImport() {
|
||||
u.syncLocalImportName = ""
|
||||
u.syncLocalImportContent = nil
|
||||
}
|
||||
|
||||
func (u *ui) selectedSyncLocalImport() (string, []byte, bool) {
|
||||
name := strings.TrimSpace(u.syncLocalImportName)
|
||||
if name == "" || name != strings.TrimSpace(u.syncLocalPath.Text()) || len(u.syncLocalImportContent) == 0 {
|
||||
return "", nil, false
|
||||
}
|
||||
return name, append([]byte(nil), u.syncLocalImportContent...), true
|
||||
}
|
||||
|
||||
func sanitizeSyncSourceMode(mode syncSourceMode) syncSourceMode {
|
||||
switch mode {
|
||||
case syncSourceRemote:
|
||||
return syncSourceRemote
|
||||
default:
|
||||
return syncSourceLocal
|
||||
}
|
||||
}
|
||||
|
||||
func sanitizeSyncDirection(direction syncDirection) syncDirection {
|
||||
switch direction {
|
||||
case syncDirectionPush:
|
||||
return syncDirectionPush
|
||||
default:
|
||||
return syncDirectionPull
|
||||
}
|
||||
}
|
||||
|
||||
func (u *ui) advancedSyncAction() error {
|
||||
switch u.syncDirection {
|
||||
case syncDirectionPush:
|
||||
return u.advancedSyncToAction()
|
||||
default:
|
||||
return u.advancedSyncFromAction()
|
||||
}
|
||||
}
|
||||
|
||||
func (u *ui) advancedSyncFromAction() error {
|
||||
switch u.syncSourceMode {
|
||||
case syncSourceRemote:
|
||||
client := webdav.Client{
|
||||
BaseURL: strings.TrimSpace(u.syncRemoteBaseURL.Text()),
|
||||
Username: strings.TrimSpace(u.syncRemoteUsername.Text()),
|
||||
Password: u.syncRemotePassword.Text(),
|
||||
}
|
||||
if err := u.state.SynchronizeFromRemote(client, strings.TrimSpace(u.syncRemotePath.Text())); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
if name, content, ok := u.selectedSyncLocalImport(); ok {
|
||||
if err := u.state.SynchronizeFromLocalBytes(name, content); err != nil {
|
||||
return err
|
||||
}
|
||||
break
|
||||
}
|
||||
path := strings.TrimSpace(u.syncLocalPath.Text())
|
||||
if path == "" {
|
||||
return errors.New(errVaultPathRequired)
|
||||
}
|
||||
if err := u.state.SynchronizeFromLocal(path); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
u.syncDialogOpen = false
|
||||
u.showSyncPassword = false
|
||||
u.filter()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ui) startChooseSyncLocalSourceAction() {
|
||||
if runtime.GOOS != "android" || u.fileExplorer == nil {
|
||||
u.runAction("choose sync path", func() error {
|
||||
u.clearSyncLocalImport()
|
||||
return u.chooseExistingFileAction(&u.syncLocalPath)
|
||||
})
|
||||
return
|
||||
}
|
||||
u.runBackgroundAction("choose sync file", func() (func() error, error) {
|
||||
file, err := u.fileExplorer.ChooseFile(".kdbx")
|
||||
if err != nil {
|
||||
if errors.Is(err, explorer.ErrUserDecline) {
|
||||
return func() error { return nil }, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
content, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
label := "Selected Android vault"
|
||||
return func() error {
|
||||
u.syncLocalImportName = label
|
||||
u.syncLocalImportContent = append([]byte(nil), content...)
|
||||
u.syncLocalPath.SetText(label)
|
||||
return nil
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func pickedDocumentName(file io.ReadCloser, fallback string) string {
|
||||
if named, ok := file.(interface{ Name() string }); ok {
|
||||
if base := filepath.Base(strings.TrimSpace(named.Name())); base != "" && base != "." && base != string(filepath.Separator) {
|
||||
return base
|
||||
}
|
||||
}
|
||||
fallback = filepath.Base(strings.TrimSpace(fallback))
|
||||
if fallback == "" || fallback == "." || fallback == string(filepath.Separator) {
|
||||
return "selected-vault.kdbx"
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func (u *ui) startChooseVaultPathAction() {
|
||||
if runtime.GOOS != "android" || u.fileExplorer == nil {
|
||||
u.runAction("choose vault path", func() error { return u.chooseExistingFileAction(&u.vaultPath) })
|
||||
return
|
||||
}
|
||||
u.runBackgroundAction("choose vault file", func() (func() error, error) {
|
||||
file, err := u.fileExplorer.ChooseFile(".kdbx")
|
||||
if err != nil {
|
||||
if errors.Is(err, explorer.ErrUserDecline) {
|
||||
return func() error { return nil }, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
content, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
name := pickedDocumentName(file, "selected-vault.kdbx")
|
||||
return func() error {
|
||||
return u.importSharedVaultBytesAction(name, content)
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ui) startImportSharedVaultAction() {
|
||||
if !supportsSharedVaultImport(runtime.GOOS) || u.fileExplorer == nil {
|
||||
return
|
||||
}
|
||||
u.runBackgroundAction("import shared vault", func() (func() error, error) {
|
||||
file, err := u.fileExplorer.ChooseFile(".kdbx")
|
||||
if err != nil {
|
||||
if errors.Is(err, explorer.ErrUserDecline) {
|
||||
return func() error { return nil }, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
content, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return func() error {
|
||||
return u.importSharedVaultBytesAction("shared-vault.kdbx", content)
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ui) advancedSyncToAction() error {
|
||||
switch u.syncSourceMode {
|
||||
case syncSourceRemote:
|
||||
baseURL := strings.TrimSpace(u.syncRemoteBaseURL.Text())
|
||||
remotePath := strings.TrimSpace(u.syncRemotePath.Text())
|
||||
client := webdav.Client{
|
||||
BaseURL: baseURL,
|
||||
Username: strings.TrimSpace(u.syncRemoteUsername.Text()),
|
||||
Password: u.syncRemotePassword.Text(),
|
||||
}
|
||||
if err := u.state.SynchronizeToRemote(client, remotePath); err != nil {
|
||||
return err
|
||||
}
|
||||
if u.syncDialogPurpose == syncDialogPurposeRemoteSetup {
|
||||
if err := u.persistSyncDialogRemoteBinding(baseURL, remotePath); err != nil {
|
||||
return err
|
||||
}
|
||||
u.showStatusMessage("Remote sync is set up for this vault.")
|
||||
}
|
||||
default:
|
||||
path := strings.TrimSpace(u.syncLocalPath.Text())
|
||||
if path == "" {
|
||||
return errors.New(errVaultPathRequired)
|
||||
}
|
||||
if err := u.state.SynchronizeToLocal(path); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
u.syncDialogOpen = false
|
||||
u.showSyncPassword = false
|
||||
u.filter()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ui) persistSyncDialogRemoteBinding(baseURL, remotePath string) error {
|
||||
baseURL = strings.TrimSpace(baseURL)
|
||||
remotePath = strings.TrimSpace(remotePath)
|
||||
if baseURL == "" || remotePath == "" {
|
||||
return fmt.Errorf("remote setup requires base URL and path")
|
||||
}
|
||||
input := appstate.RemoteBindingInput{
|
||||
LocalVaultPath: strings.TrimSpace(u.vaultPath.Text()),
|
||||
RemoteProfileID: "remote-profile-" + remoteBindingSuffix(baseURL, remotePath, strings.TrimSpace(u.syncRemoteUsername.Text())),
|
||||
RemoteProfileName: friendlyRecentRemoteLabel(recentRemoteRecord{BaseURL: baseURL, Path: remotePath}),
|
||||
BaseURL: baseURL,
|
||||
RemotePath: remotePath,
|
||||
CredentialEntryID: "remote-credential-" + remoteBindingSuffix(baseURL, remotePath, strings.TrimSpace(u.syncRemoteUsername.Text())),
|
||||
CredentialTitle: "WebDAV Sign-In" + func() string {
|
||||
if user := strings.TrimSpace(u.syncRemoteUsername.Text()); user != "" {
|
||||
return " · " + user
|
||||
}
|
||||
return ""
|
||||
}(),
|
||||
Username: strings.TrimSpace(u.syncRemoteUsername.Text()),
|
||||
Password: u.syncRemotePassword.Text(),
|
||||
CredentialPath: append([]string(nil), u.currentPath...),
|
||||
SyncMode: u.syncSetupMode(),
|
||||
}
|
||||
binding, err := u.state.ConfigureRemoteBinding(input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := u.state.Save(); err != nil {
|
||||
return err
|
||||
}
|
||||
u.selectedVaultRemoteProfileID = binding.RemoteProfileID
|
||||
u.selectedVaultRemoteCredentialEntryID = binding.CredentialEntryID
|
||||
u.selectedVaultRemoteSyncMode = binding.SyncMode
|
||||
u.remoteBaseURL.SetText(baseURL)
|
||||
u.remotePath.SetText(remotePath)
|
||||
u.remoteUsername.SetText(strings.TrimSpace(u.syncRemoteUsername.Text()))
|
||||
u.remotePassword.SetText(u.syncRemotePassword.Text())
|
||||
u.noteRecentRemote(baseURL, remotePath)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ui) saveAsTargetPath() string {
|
||||
path := strings.TrimSpace(u.saveAsPath.Text())
|
||||
if path != "" {
|
||||
return path
|
||||
}
|
||||
return u.defaultSaveAsPath
|
||||
}
|
||||
|
||||
func (u *ui) importedVaultDestination(name string) string {
|
||||
baseTarget := u.saveAsTargetPath()
|
||||
baseDir := filepath.Dir(baseTarget)
|
||||
baseName := filepath.Base(strings.TrimSpace(name))
|
||||
switch {
|
||||
case baseName == "" || baseName == "." || baseName == string(filepath.Separator):
|
||||
return baseTarget
|
||||
case strings.HasSuffix(strings.ToLower(baseName), ".kdbx"):
|
||||
return filepath.Join(baseDir, baseName)
|
||||
default:
|
||||
return baseTarget
|
||||
}
|
||||
}
|
||||
|
||||
func (u *ui) consumePendingSharedVaultImport() {
|
||||
path := strings.TrimSpace(u.pendingSharedVaultPath)
|
||||
if path == "" {
|
||||
return
|
||||
}
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
u.state.ErrorMessage = fmt.Sprintf("import shared vault: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
name := "shared-vault.kdbx"
|
||||
if namePath := strings.TrimSpace(u.pendingSharedVaultNamePath); namePath != "" {
|
||||
if rawName, err := os.ReadFile(namePath); err == nil {
|
||||
if trimmed := strings.TrimSpace(string(rawName)); trimmed != "" {
|
||||
name = trimmed
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := u.importSharedVaultBytesAction(name, content); err != nil {
|
||||
u.state.ErrorMessage = fmt.Sprintf("import shared vault: %v", err)
|
||||
return
|
||||
}
|
||||
_ = os.Remove(path)
|
||||
if namePath := strings.TrimSpace(u.pendingSharedVaultNamePath); namePath != "" {
|
||||
_ = os.Remove(namePath)
|
||||
}
|
||||
}
|
||||
|
||||
func (u *ui) importSharedVaultBytesAction(name string, content []byte) error {
|
||||
target := u.importedVaultDestination(name)
|
||||
if err := os.MkdirAll(filepath.Dir(target), 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(target, append([]byte(nil), content...), 0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
u.lifecycleMode = "local"
|
||||
u.vaultPath.SetText(target)
|
||||
u.noteRecentVault(target)
|
||||
u.state.ErrorMessage = ""
|
||||
u.state.StatusMessage = ""
|
||||
u.requestMasterPassFocus = true
|
||||
u.filter()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ui) currentShareableVaultPath() string {
|
||||
return strings.TrimSpace(u.vaultPath.Text())
|
||||
}
|
||||
|
||||
func (u *ui) shareCurrentVaultAction() error {
|
||||
if u.vaultSharer == nil {
|
||||
return fmt.Errorf("vault sharing is not available on this platform")
|
||||
}
|
||||
path := u.currentShareableVaultPath()
|
||||
if path == "" {
|
||||
return errors.New(errVaultPathRequired)
|
||||
}
|
||||
if err := u.state.Save(); err != nil {
|
||||
return err
|
||||
}
|
||||
return u.vaultSharer.ShareVault(path, friendlyRecentVaultLabel(path))
|
||||
}
|
||||
Reference in New Issue
Block a user