Implement local-first remote sync flow
This commit is contained in:
+277
-30
@@ -22,7 +22,7 @@ func TestVisibleEntriesFollowsCurrentPathWithoutSearch(t *testing.T) {
|
||||
Entries: []vault.Entry{
|
||||
{ID: "bellagio", Title: "Bellagio", Path: []string{"Crew", "Internet"}},
|
||||
{ID: "vault-console", Title: "Vault Console", Path: []string{"Crew", "Internet"}},
|
||||
{ID: "surveillance-console", Title: "Surveillance Console", Path: []string{"Crew", "Home Assistant"}},
|
||||
{ID: "surveillance-console", Title: "Surveillance Console", Path: []string{"Crew", "Security Office"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -54,7 +54,7 @@ func TestVisibleEntriesAtParentGroupOnlyShowsDirectEntries(t *testing.T) {
|
||||
{ID: "joe-note", Title: "Crew Note", Path: []string{"Crew"}},
|
||||
{ID: "bellagio", Title: "Bellagio", Path: []string{"Crew", "Internet"}},
|
||||
{ID: "vault-console", Title: "Vault Console", Path: []string{"Crew", "Internet"}},
|
||||
{ID: "surveillance-console", Title: "Surveillance Console", Path: []string{"Crew", "Home Assistant"}},
|
||||
{ID: "surveillance-console", Title: "Surveillance Console", Path: []string{"Crew", "Security Office"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -164,6 +164,71 @@ func TestIssueRotateDisableRevokeAndDeleteAPIToken(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoteProfilesReturnsVaultProfiles(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
state := State{
|
||||
Session: stubSession{
|
||||
model: vault.Model{
|
||||
RemoteProfiles: []vault.RemoteProfile{
|
||||
{
|
||||
ID: "family-webdav",
|
||||
Name: "Family Vault",
|
||||
Backend: vault.RemoteBackendWebDAV,
|
||||
BaseURL: "https://dav.example.invalid/remote.php/dav",
|
||||
Path: "files/family/keepass.kdbx",
|
||||
},
|
||||
{
|
||||
ID: "archive-webdav",
|
||||
Name: "Archive Vault",
|
||||
Backend: vault.RemoteBackendWebDAV,
|
||||
BaseURL: "https://dav.example.invalid/remote.php/dav",
|
||||
Path: "files/family/archive.kdbx",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
got, err := state.RemoteProfiles()
|
||||
if err != nil {
|
||||
t.Fatalf("RemoteProfiles() error = %v", err)
|
||||
}
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("len(RemoteProfiles()) = %d, want 2", len(got))
|
||||
}
|
||||
if got[0].ID != "archive-webdav" || got[1].ID != "family-webdav" {
|
||||
t.Fatalf("RemoteProfiles() = %#v, want sorted by name/id", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoteCredentialEntriesReturnsSortedVaultEntries(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
state := State{
|
||||
Session: stubSession{
|
||||
model: vault.Model{
|
||||
Entries: []vault.Entry{
|
||||
{ID: "cred-2", Title: "Zulu Sign-In", Username: "zuser", Path: []string{"Crew", "Internet"}},
|
||||
{ID: "cred-1", Title: "Alpha Sign-In", Username: "auser", Path: []string{"Crew", "Internet"}},
|
||||
{ID: "cred-3", Title: "Mint Sign-In", Username: "frankcatton", Path: []string{"Crew", "Safe House"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
got, err := state.RemoteCredentialEntries()
|
||||
if err != nil {
|
||||
t.Fatalf("RemoteCredentialEntries() error = %v", err)
|
||||
}
|
||||
if len(got) != 3 {
|
||||
t.Fatalf("len(RemoteCredentialEntries()) = %d, want 3", len(got))
|
||||
}
|
||||
if got[0].ID != "cred-1" || got[1].ID != "cred-3" || got[2].ID != "cred-2" {
|
||||
t.Fatalf("RemoteCredentialEntries() = %#v, want entries sorted by title", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVisibleEntriesUsesGlobalSearchWhenQueryPresent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -173,7 +238,7 @@ func TestVisibleEntriesUsesGlobalSearchWhenQueryPresent(t *testing.T) {
|
||||
Entries: []vault.Entry{
|
||||
{ID: "bellagio", Title: "Bellagio", Path: []string{"Crew", "Internet"}},
|
||||
{ID: "vault-console", Title: "Vault Console", URL: "https://vault.crew.example.invalid", Path: []string{"Crew", "Internet"}},
|
||||
{ID: "surveillance-console", Title: "Surveillance Console", URL: "https://surveillance.crew.example.invalid", Path: []string{"Crew", "Home Assistant"}},
|
||||
{ID: "surveillance-console", Title: "Surveillance Console", URL: "https://surveillance.crew.example.invalid", Path: []string{"Crew", "Security Office"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -187,7 +252,7 @@ func TestVisibleEntriesUsesGlobalSearchWhenQueryPresent(t *testing.T) {
|
||||
}
|
||||
|
||||
if len(got) != 1 || got[0].Title != "Surveillance Console" {
|
||||
t.Fatalf("VisibleEntries() = %#v, want Home Assistant search match", got)
|
||||
t.Fatalf("VisibleEntries() = %#v, want Security Office search match", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,7 +265,7 @@ func TestVisibleEntriesReturnsDescendantsAfterClearingSearch(t *testing.T) {
|
||||
Entries: []vault.Entry{
|
||||
{ID: "bellagio", Title: "Bellagio", Path: []string{"Crew", "Internet"}},
|
||||
{ID: "vault-console", Title: "Vault Console", URL: "https://vault.crew.example.invalid", Path: []string{"Crew", "Internet"}},
|
||||
{ID: "surveillance-console", Title: "Surveillance Console", URL: "https://surveillance.crew.example.invalid", Path: []string{"Crew", "Home Assistant"}},
|
||||
{ID: "surveillance-console", Title: "Surveillance Console", URL: "https://surveillance.crew.example.invalid", Path: []string{"Crew", "Security Office"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -350,7 +415,7 @@ func TestVisibleEntriesUsesGlobalSearchWithinRecycleBin(t *testing.T) {
|
||||
model: vault.Model{
|
||||
RecycleBin: []vault.Entry{
|
||||
{ID: "deleted-1", Title: "Deleted Bellagio", Path: []string{"Root", "Internet"}},
|
||||
{ID: "deleted-2", Title: "Deleted HVAC", URL: "https://climate.example.com", Path: []string{"Root", "Home"}},
|
||||
{ID: "deleted-2", Title: "Deleted Vault Vent", URL: "https://climate.example.com", Path: []string{"Root", "Safe House"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -419,7 +484,7 @@ func TestChildGroupsUsesCurrentModelAndCurrentPath(t *testing.T) {
|
||||
model: vault.Model{
|
||||
Entries: []vault.Entry{
|
||||
{ID: "bellagio", Title: "Bellagio", Path: []string{"Crew", "Internet"}},
|
||||
{ID: "surveillance-console", Title: "Surveillance Console", Path: []string{"Crew", "Home Assistant"}},
|
||||
{ID: "surveillance-console", Title: "Surveillance Console", Path: []string{"Crew", "Security Office"}},
|
||||
{ID: "alma", Title: "Alma (WA Prep)", Path: []string{"Tricia", "School"}},
|
||||
},
|
||||
},
|
||||
@@ -432,8 +497,8 @@ func TestChildGroupsUsesCurrentModelAndCurrentPath(t *testing.T) {
|
||||
t.Fatalf("ChildGroups() error = %v", err)
|
||||
}
|
||||
|
||||
if !slices.Equal(got, []string{"Home Assistant", "Internet"}) {
|
||||
t.Fatalf("ChildGroups() = %v, want [Home Assistant Internet]", got)
|
||||
if !slices.Equal(got, []string{"Internet", "Security Office"}) {
|
||||
t.Fatalf("ChildGroups() = %v, want [Internet Security Office]", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -603,7 +668,7 @@ func TestUpsertEntryPersistsEntryAndSelectsIt(t *testing.T) {
|
||||
ID: "vault-console",
|
||||
Title: "Vault Console",
|
||||
Username: "dannyocean",
|
||||
Password: "token-1",
|
||||
Password: "bellagio-pass-1",
|
||||
URL: "https://vault.crew.example.invalid",
|
||||
Path: []string{"Root", "Internet"},
|
||||
}
|
||||
@@ -619,7 +684,7 @@ func TestUpsertEntryPersistsEntryAndSelectsIt(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("VisibleEntries() error = %v", err)
|
||||
}
|
||||
if len(got) != 1 || got[0].Password != "token-1" {
|
||||
if len(got) != 1 || got[0].Password != "bellagio-pass-1" {
|
||||
t.Fatalf("VisibleEntries() = %#v, want persisted vault-console entry", got)
|
||||
}
|
||||
|
||||
@@ -964,6 +1029,185 @@ func TestOpenRemoteVaultResetsSelectionPathAndDirtyState(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenBoundRemoteVaultResolvesClientFromVaultBinding(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sess := &lifecycleStubSession{
|
||||
model: vault.Model{
|
||||
Entries: []vault.Entry{
|
||||
{
|
||||
ID: "remote-creds-1",
|
||||
Title: "Bellagio WebDAV Sign-In",
|
||||
Username: "linuscaldwell",
|
||||
Password: "bellagio-pass-1",
|
||||
Path: []string{"Crew", "Internet"},
|
||||
},
|
||||
},
|
||||
RemoteProfiles: []vault.RemoteProfile{
|
||||
{
|
||||
ID: "family-webdav",
|
||||
Name: "Family Vault",
|
||||
Backend: vault.RemoteBackendWebDAV,
|
||||
BaseURL: "https://dav.example.invalid/remote.php/dav",
|
||||
Path: "files/family/keepass.kdbx",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
state := State{
|
||||
Session: sess,
|
||||
CurrentPath: []string{"Root", "Internet"},
|
||||
SelectedEntryID: "vault-console",
|
||||
Dirty: true,
|
||||
}
|
||||
|
||||
err := state.OpenBoundRemoteVault(RemoteBinding{
|
||||
LocalVaultPath: "/tmp/family.kdbx",
|
||||
RemoteProfileID: "family-webdav",
|
||||
CredentialEntryID: "remote-creds-1",
|
||||
SyncMode: SyncModeAutomaticOnOpenSave,
|
||||
}, vault.MasterKey{Password: "correct horse battery staple"})
|
||||
if err != nil {
|
||||
t.Fatalf("OpenBoundRemoteVault() error = %v", err)
|
||||
}
|
||||
|
||||
if got := sess.remoteClient.BaseURL; got != "https://dav.example.invalid/remote.php/dav" {
|
||||
t.Fatalf("remote client base URL = %q, want remote.php/dav URL", got)
|
||||
}
|
||||
if got := sess.remoteClient.Username; got != "linuscaldwell" {
|
||||
t.Fatalf("remote client username = %q, want linuscaldwell", got)
|
||||
}
|
||||
if got := sess.remoteClient.Password; got != "bellagio-pass-1" {
|
||||
t.Fatalf("remote client password = %q, want bellagio-pass-1", got)
|
||||
}
|
||||
if got := sess.remotePath; got != "files/family/keepass.kdbx" {
|
||||
t.Fatalf("remotePath = %q, want files/family/keepass.kdbx", got)
|
||||
}
|
||||
if len(state.CurrentPath) != 0 {
|
||||
t.Fatalf("CurrentPath = %v, want empty", state.CurrentPath)
|
||||
}
|
||||
if state.SelectedEntryID != "" {
|
||||
t.Fatalf("SelectedEntryID = %q, want empty", state.SelectedEntryID)
|
||||
}
|
||||
if state.Dirty {
|
||||
t.Fatal("Dirty = true, want false after bound remote open")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenBoundRemoteVaultReturnsResolutionErrors(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sess := &lifecycleStubSession{model: vault.Model{}}
|
||||
state := State{Session: sess}
|
||||
|
||||
err := state.OpenBoundRemoteVault(RemoteBinding{
|
||||
LocalVaultPath: "/tmp/family.kdbx",
|
||||
RemoteProfileID: "missing-profile",
|
||||
CredentialEntryID: "remote-creds-1",
|
||||
}, vault.MasterKey{Password: "correct horse battery staple"})
|
||||
if !errors.Is(err, vault.ErrRemoteProfileNotFound) {
|
||||
t.Fatalf("OpenBoundRemoteVault() error = %v, want ErrRemoteProfileNotFound", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigureRemoteBindingPersistsIntoCurrentVaultAndMarksDirty(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sess := &mutableStubSession{model: vault.Model{}}
|
||||
state := State{Session: sess}
|
||||
|
||||
binding, err := state.ConfigureRemoteBinding(RemoteBindingInput{
|
||||
LocalVaultPath: "/tmp/family.kdbx",
|
||||
RemoteProfileID: "family-webdav",
|
||||
RemoteProfileName: "Family Vault",
|
||||
BaseURL: "https://dav.example.invalid/remote.php/dav",
|
||||
RemotePath: "files/family/keepass.kdbx",
|
||||
CredentialEntryID: "remote-creds-1",
|
||||
CredentialTitle: "Bellagio WebDAV Sign-In",
|
||||
Username: "linuscaldwell",
|
||||
Password: "bellagio-pass-1",
|
||||
CredentialPath: []string{"Crew", "Internet"},
|
||||
SyncMode: SyncModeAutomaticOnOpenSave,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ConfigureRemoteBinding() error = %v", err)
|
||||
}
|
||||
|
||||
if !state.Dirty {
|
||||
t.Fatal("Dirty = false, want true after ConfigureRemoteBinding")
|
||||
}
|
||||
if got := binding.RemoteProfileID; got != "family-webdav" {
|
||||
t.Fatalf("binding.RemoteProfileID = %q, want family-webdav", got)
|
||||
}
|
||||
if got := len(sess.model.RemoteProfiles); got != 1 {
|
||||
t.Fatalf("len(RemoteProfiles) = %d, want 1", got)
|
||||
}
|
||||
credentials, err := sess.model.EntryByID("remote-creds-1")
|
||||
if err != nil {
|
||||
t.Fatalf("EntryByID(remote-creds-1) error = %v", err)
|
||||
}
|
||||
if credentials.Username != "linuscaldwell" || credentials.Password != "bellagio-pass-1" {
|
||||
t.Fatalf("stored credential entry = %#v, want linuscaldwell/bellagio-pass-1", credentials)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigureRemoteBindingRequiresMutableSession(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
state := State{Session: stubSession{model: vault.Model{}}}
|
||||
_, err := state.ConfigureRemoteBinding(RemoteBindingInput{
|
||||
LocalVaultPath: "/tmp/family.kdbx",
|
||||
RemoteProfileID: "family-webdav",
|
||||
BaseURL: "https://dav.example.invalid/remote.php/dav",
|
||||
RemotePath: "files/family/keepass.kdbx",
|
||||
CredentialEntryID: "remote-creds-1",
|
||||
Password: "bellagio-pass-1",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("ConfigureRemoteBinding() error = nil, want mutability error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveRemoteBindingRemovesVaultDataAndMarksDirty(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sess := &mutableStubSession{model: vault.Model{
|
||||
Entries: []vault.Entry{{
|
||||
ID: "remote-creds-1",
|
||||
Title: "Bellagio WebDAV Sign-In",
|
||||
Username: "linuscaldwell",
|
||||
Password: "bellagio-pass-1",
|
||||
}},
|
||||
RemoteProfiles: []vault.RemoteProfile{{
|
||||
ID: "family-webdav",
|
||||
Name: "Family Vault",
|
||||
Backend: vault.RemoteBackendWebDAV,
|
||||
BaseURL: "https://dav.example.invalid/remote.php/dav",
|
||||
Path: "files/family/keepass.kdbx",
|
||||
}},
|
||||
}}
|
||||
state := State{Session: sess}
|
||||
|
||||
err := state.RemoveRemoteBinding(RemoteBinding{
|
||||
LocalVaultPath: "/tmp/family.kdbx",
|
||||
RemoteProfileID: "family-webdav",
|
||||
CredentialEntryID: "remote-creds-1",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("RemoveRemoteBinding() error = %v", err)
|
||||
}
|
||||
|
||||
if !state.Dirty {
|
||||
t.Fatal("Dirty = false, want true after RemoveRemoteBinding")
|
||||
}
|
||||
if got := len(sess.model.RemoteProfiles); got != 0 {
|
||||
t.Fatalf("len(RemoteProfiles) = %d, want 0", got)
|
||||
}
|
||||
if _, err := sess.model.EntryByID("remote-creds-1"); !errors.Is(err, vault.ErrEntryNotFound) {
|
||||
t.Fatalf("EntryByID(remote-creds-1) error = %v, want ErrEntryNotFound", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLockClearsSelectionAndMakesVaultUnavailable(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -1149,10 +1393,10 @@ func TestNavigateToPathReplacesPathAndClearsSelection(t *testing.T) {
|
||||
SelectedEntryID: "vault-console",
|
||||
}
|
||||
|
||||
state.NavigateToPath([]string{"Root", "Home Assistant"})
|
||||
state.NavigateToPath([]string{"Root", "Security Office"})
|
||||
|
||||
if !slices.Equal(state.CurrentPath, []string{"Root", "Home Assistant"}) {
|
||||
t.Fatalf("CurrentPath = %v, want [Root Home Assistant]", state.CurrentPath)
|
||||
if !slices.Equal(state.CurrentPath, []string{"Root", "Security Office"}) {
|
||||
t.Fatalf("CurrentPath = %v, want [Root Security Office]", state.CurrentPath)
|
||||
}
|
||||
if got := state.SelectedEntryID; got != "" {
|
||||
t.Fatalf("SelectedEntryID = %q, want empty", got)
|
||||
@@ -1185,8 +1429,8 @@ func TestDeleteCurrentGroupMovesToParentAndMarksDirty(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("ChildGroups() error = %v", err)
|
||||
}
|
||||
if !slices.Equal(got, []string{"Home Assistant", "Internet"}) {
|
||||
t.Fatalf("ChildGroups() = %v, want [Home Assistant Internet]", got)
|
||||
if !slices.Equal(got, []string{"Internet", "Security Office"}) {
|
||||
t.Fatalf("ChildGroups() = %v, want [Internet Security Office]", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1208,8 +1452,8 @@ func TestCreateGroupPersistsGroupAndMarksDirty(t *testing.T) {
|
||||
t.Fatalf("ChildGroups() error = %v", err)
|
||||
}
|
||||
|
||||
if !slices.Equal(got, []string{"Finance", "Home Assistant", "Internet"}) {
|
||||
t.Fatalf("ChildGroups() = %v, want Finance, Home Assistant, Internet", got)
|
||||
if !slices.Equal(got, []string{"Finance", "Internet", "Security Office"}) {
|
||||
t.Fatalf("ChildGroups() = %v, want Finance, Internet, Security Office", got)
|
||||
}
|
||||
if !state.Dirty {
|
||||
t.Fatal("Dirty = false, want true after CreateGroup")
|
||||
@@ -1282,8 +1526,8 @@ func TestDeleteCurrentGroupRemovesItNavigatesToParentAndMarksDirty(t *testing.T)
|
||||
t.Fatalf("ChildGroups() error = %v", err)
|
||||
}
|
||||
|
||||
if !slices.Equal(got, []string{"Home Assistant", "Internet"}) {
|
||||
t.Fatalf("ChildGroups() = %v, want [Home Assistant Internet]", got)
|
||||
if !slices.Equal(got, []string{"Internet", "Security Office"}) {
|
||||
t.Fatalf("ChildGroups() = %v, want [Internet Security Office]", got)
|
||||
}
|
||||
if !state.Dirty {
|
||||
t.Fatal("Dirty = false, want true after DeleteCurrentGroup")
|
||||
@@ -1300,11 +1544,11 @@ func TestMoveSelectedEntryPersistsPathChangeAndMarksDirty(t *testing.T) {
|
||||
SelectedEntryID: "bellagio",
|
||||
}
|
||||
|
||||
if err := state.MoveSelectedEntry([]string{"Root", "Home Assistant"}); err != nil {
|
||||
if err := state.MoveSelectedEntry([]string{"Root", "Security Office"}); err != nil {
|
||||
t.Fatalf("MoveSelectedEntry() error = %v", err)
|
||||
}
|
||||
|
||||
state.NavigateToPath([]string{"Root", "Home Assistant"})
|
||||
state.NavigateToPath([]string{"Root", "Security Office"})
|
||||
got, err := state.VisibleEntries()
|
||||
if err != nil {
|
||||
t.Fatalf("VisibleEntries() error = %v", err)
|
||||
@@ -1512,7 +1756,7 @@ func testVaultModel() vault.Model {
|
||||
return vault.Model{
|
||||
Entries: []vault.Entry{
|
||||
{ID: "bellagio", Title: "Bellagio", Path: []string{"Root", "Internet"}},
|
||||
{ID: "surveillance-console", Title: "Surveillance Console", Path: []string{"Root", "Home Assistant"}},
|
||||
{ID: "surveillance-console", Title: "Surveillance Console", Path: []string{"Root", "Security Office"}},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1553,15 +1797,17 @@ func (s *saveableStubSession) Save() error {
|
||||
}
|
||||
|
||||
type lifecycleStubSession struct {
|
||||
createCalls int
|
||||
openPath string
|
||||
saveAsPath string
|
||||
remotePath string
|
||||
changedKey vault.MasterKey
|
||||
createCalls int
|
||||
model vault.Model
|
||||
openPath string
|
||||
saveAsPath string
|
||||
remoteClient webdav.Client
|
||||
remotePath string
|
||||
changedKey vault.MasterKey
|
||||
}
|
||||
|
||||
func (s *lifecycleStubSession) Current() (vault.Model, error) {
|
||||
return vault.Model{}, nil
|
||||
return s.model, nil
|
||||
}
|
||||
|
||||
func (s *lifecycleStubSession) Create(_ vault.Model, _ vault.MasterKey) error {
|
||||
@@ -1579,7 +1825,8 @@ func (s *lifecycleStubSession) SaveAs(path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *lifecycleStubSession) OpenRemote(_ webdav.Client, path string, _ vault.MasterKey) error {
|
||||
func (s *lifecycleStubSession) OpenRemote(client webdav.Client, path string, _ vault.MasterKey) error {
|
||||
s.remoteClient = client
|
||||
s.remotePath = path
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user