sync: the order the providers were put in on the Providers tab goes with them, so a computer that syncs lists and tries them as the other does (ARNO on Discord: 发现webdav同步的时候没有同步provider的顺序) — since #499 the order is kept apart from the providers, as providers.json's "order" (ids, the built-in signed-in accounts among them), and the list is sorted by it when read; the providers themselves stay in the file in the order they were added. The sync's bundle (backup.Bundle) carried the providers, icons, groups and search APIs but not that order, and the providers part was hashed without it, so arranging the list was not even a change to sync, and a computer bringing the providers in (provider.Mirror) kept its own order, or none. The bundle now has Order (omitempty, from provider.StoredOrder), hashed with the providers part only when there is one, so the hashes of a setup never arranged are as before and an older magpie's file still compares the same; take carries it with the providers, and bring applies it after Mirror with the new provider.MirrorOrder: the other computer's ids first as it has them, then the ones named here alone (an account only this computer has) in the order they have here; none, from a magpie before this or a setup never arranged, leaves the order here as it is. This goes for WebDAV and S3 alike (one path). A backup restored by hand still merges providers without changing the order. TestSyncProviderOrder (a arranges three providers, b joins and lists them in a's order and pushes nothing back, b arranging only is pushed and a follows; against an httptest WebDAV server) fails without the change ("b after joining: [one two three]"); TestMirrorOrder covers the merge. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... (temp HOME) pass.

This commit is contained in:
yetone committed 2026-10-02 15:01:24 +08:00
1 parent bb5e8a2b61
commit 7b4065b12a
5 files changed
+144 -1

No files matched your search

+6
View File
@@ -62,6 +62,9 @@ type Bundle struct {
// Searches are the web search APIs (#419), their keys with the
// providers'; nil from a magpie before them.
Searches *[]provider.SearchAPI `json:"searches,omitempty"`
// Order is the order the user put the providers in (#499), by id;
// none from a magpie before it went, or when they were never arranged.
Order []string `json:"order,omitempty"`
}
type envelope struct {
@@ -92,6 +95,9 @@ func Collect(keys bool, app string) (Bundle, error) {
if b.Groups, err = provider.StoredGroups(); err != nil {
return b, err
}
if b.Order, err = provider.StoredOrder(); err != nil {
return b, err
}
if keys {
access.MigrateLegacyLANKeyBestEffort()
if _, err := os.Stat(access.Path()); err == nil {
+7 -1
View File
@@ -537,6 +537,9 @@ func hashes(b backup.Bundle) map[string]string {
if b.Searches != nil && len(*b.Searches) > 0 { // as before them, without one
providers = append(providers, *b.Searches)
}
if len(b.Order) > 0 { // as before it, when never arranged
providers = append(providers, map[string][]string{"order": b.Order})
}
return map[string]string{
"providers": h(providers),
"settings": h(s),
@@ -587,7 +590,7 @@ func take(to *backup.Bundle, from backup.Bundle, part string) {
}
searches = &ss
}
to.Providers, to.Icons, to.Groups, to.Searches = ps, from.Icons, from.Groups, searches
to.Providers, to.Icons, to.Groups, to.Searches, to.Order = ps, from.Icons, from.Groups, searches, from.Order
to.Keys = to.Keys || from.Keys
case "settings":
to.Settings = from.Settings
@@ -647,6 +650,9 @@ func bring(b backup.Bundle, part string) error {
if err := provider.Mirror(b.Providers, b.Groups); err != nil {
return err
}
if err := provider.MirrorOrder(b.Order); err != nil {
return err
}
if b.Searches == nil { // from a magpie before them: the ones here stay
return nil
}
+59
View File
@@ -360,6 +360,65 @@ func TestSyncNutstore(t *testing.T) {
}
}
// The order the providers were put in on the Providers tab (#499) goes
// with them (ARNO on Discord: 发现webdav同步的时候没有同步provider的顺序):
// b joins and lists them as a does; b arranges them again and a follows;
// a moving one alone is a change that syncs.
func TestSyncProviderOrder(t *testing.T) {
fake := &fakeDAV{files: map[string][]byte{}, etags: map[string]string{}, dirs: map[string]bool{"/dav": true}}
srv := httptest.NewServer(fake)
defer srv.Close()
cfg := Config{URL: srv.URL + "/dav/", User: "me", Password: "pw", Passphrase: "correct horse", Keys: true}
now := func(t *testing.T) {
t.Helper()
if err := Now(context.Background()); err != nil {
t.Fatal(err)
}
}
listed := func() []string {
var out []string
for _, p := range provider.All() {
out = append(out, p.ID)
}
return out
}
a, b := newComputer(t), newComputer(t)
a.use(t)
for _, id := range []string{"one", "two", "three"} {
provider.Save(provider.Provider{ID: id, Name: id, Chat: "https://" + id + ".example.com/v1", Key: "k-" + id})
}
if err := provider.SetOrder([]string{"three", "one", "two"}); err != nil {
t.Fatal(err)
}
Configure(cfg)
now(t)
b.use(t)
Configure(cfg)
now(t)
if got := listed(); !slices.Equal(got, []string{"three", "one", "two"}) {
t.Fatalf("b after joining: %v", got)
}
puts := fake.puts
now(t)
if fake.puts != puts {
t.Fatal("b pushed back the order it brought in")
}
// only the order changes on b: a gets it
if err := provider.SetOrder([]string{"two", "three", "one"}); err != nil {
t.Fatal(err)
}
now(t)
if fake.puts != puts+1 {
t.Fatalf("b's new order not pushed (%d puts)", fake.puts-puts)
}
a.use(t)
now(t)
if got := listed(); !slices.Equal(got, []string{"two", "three", "one"}) {
t.Fatalf("a after b arranged: %v", got)
}
}
func TestSyncLibrary(t *testing.T) {
fake := &fakeDAV{files: map[string][]byte{}, etags: map[string]string{}, dirs: map[string]bool{"/dav": true}}
srv := httptest.NewServer(fake)
+35
View File
@@ -59,3 +59,38 @@ func SetOrder(ids []string) error {
f.Order = order
return store(f)
}
// StoredOrder is the order the user put the providers in, as saved; as
// with Stored, a file that can't be read is an error.
func StoredOrder() ([]string, error) {
f, err := read()
return f.Order, err
}
// MirrorOrder makes the providers' order the one another computer has, as
// sync brings it: its ids first, as it has them, then the ones named here
// alone (a signed-in account only this computer has), in the order they
// have here. None, as from a magpie before the order was synced or one
// never arranged, leaves the order here as it is.
func MirrorOrder(order []string) error {
if len(order) == 0 {
return nil
}
f, err := read()
if err != nil {
return err
}
out := []string{}
seen := map[string]bool{}
for _, id := range slices.Concat(order, f.Order) {
if id != "" && !seen[id] {
seen[id] = true
out = append(out, id)
}
}
if slices.Equal(out, f.Order) {
return nil
}
f.Order = out
return store(f)
}
+37
View File
@@ -63,3 +63,40 @@ func TestProviderOrder(t *testing.T) {
}
}
}
// Sync's order (MirrorOrder): the other computer's ids first, as it has
// them, then those named here alone (an account only this computer has)
// in the order they have here; none from the other leaves this one's.
func TestMirrorOrder(t *testing.T) {
t.Setenv("HOME", t.TempDir())
t.Setenv("USERPROFILE", t.TempDir())
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
t.Setenv("XDG_CACHE_HOME", t.TempDir())
for _, id := range []string{"a", "b", "c", "d"} {
if err := Save(Provider{ID: id, Name: id, Key: "sk-" + id, Chat: "https://" + id + ".example.com/v1"}); err != nil {
t.Fatal(err)
}
}
if err := SetOrder([]string{"d", "a", "c", "b"}); err != nil {
t.Fatal(err)
}
if err := MirrorOrder(nil); err != nil {
t.Fatal(err)
}
if got, _ := StoredOrder(); !slices.Equal(got, []string{"d", "a", "c", "b"}) {
t.Fatalf("none from the other: %v", got)
}
if err := MirrorOrder([]string{"c", "a", "elsewhere"}); err != nil {
t.Fatal(err)
}
if got, _ := StoredOrder(); !slices.Equal(got, []string{"c", "a", "elsewhere", "d", "b"}) {
t.Fatalf("mirrored: %v", got)
}
var listed []string
for _, p := range All() {
listed = append(listed, p.ID)
}
if !slices.Equal(listed, []string{"c", "a", "d", "b"}) {
t.Fatalf("listed: %v", listed)
}
}