Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions internal/gdrive/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,15 @@ func syncWith(ctx context.Context, d drive, local string, st *State) (Report, er
if err != nil {
return rep, err
}
// Local files under a forgotten folder mean it was re-added. A failed
// Drive delete must not exempt its new backup from the mass-loss guard.
readded := map[string]bool{}
for k := range here {
if id := st.forgotten(k); id != "" {
readded[id] = true
}
}
st.Forgotten = slices.DeleteFunc(st.Forgotten, func(id string) bool { return readded[id] })
// One side empty while the last sync saw files: it was lost or not
// there yet, not emptied on purpose. Copy instead of deleting.
if len(st.Files) > 0 && (len(here) == 0 || len(remote) == 0) {
Expand Down
54 changes: 54 additions & 0 deletions internal/gdrive/sync_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,60 @@ func TestSyncForgottenFolderPartlyDeleted(t *testing.T) {
}
}

// A failed delete must not keep a re-added game's backup outside the
// mass-loss guard when its local folder later disappears.
func TestSyncReaddedForgottenFolderMassLoss(t *testing.T) {
d := newFake()
a := newPC(t)
t0 := time.Now().Add(-time.Hour)
for i := 0; i < 20; i++ {
a.write(fmt.Sprintf("g/%d.sav", i), "old", t0)
}
a.write(".versions/g/old/slot.sav", "history", t0)
a.write("other/1.sav", "other", t0)
a.sync(d)
failing := d.path(".versions/g/old/slot.sav")
d.failDelete = func(id string) bool { return id == failing }
for _, rel := range []string{"g", ".versions/g"} {
if err := os.RemoveAll(filepath.Join(a.local, filepath.FromSlash(rel))); err != nil {
t.Fatal(err)
}
}
a.forget("g")
if r := a.sync(d); r.DeletedThere != 20 || len(r.Errors) != 1 {
t.Fatalf("partial delete: %+v", r)
}
if st := a.saved(); len(st.Forgotten) != 1 {
t.Fatalf("forgotten after failed delete: %v", st.Forgotten)
}

for i := 0; i < 20; i++ {
a.write(fmt.Sprintf("g/%d.sav", i), "re-added", t0.Add(time.Minute))
}
if r := a.sync(d); r.Up != 20 || len(r.Errors) != 1 {
t.Fatalf("re-added game: %+v", r)
}
if st := a.saved(); len(st.Forgotten) != 0 {
t.Errorf("re-added game still forgotten: %v", st.Forgotten)
}
ids := make([]string, 20)
for i := range ids {
ids[i] = d.path(fmt.Sprintf("g/%d.sav", i))
}
if err := os.RemoveAll(filepath.Join(a.local, "g")); err != nil {
t.Fatal(err)
}
if r := a.sync(d); r.DeletedThere != 0 || r.Down != 20 || len(r.Notes) == 0 || len(r.Errors) != 1 {
t.Errorf("re-added backup lost: %+v", r)
}
for i, id := range ids {
rel := fmt.Sprintf("g/%d.sav", i)
if d.path(rel) != id || string(d.data[id]) != "re-added" || a.read(rel) != "re-added" {
t.Errorf("%s was not preserved in Drive and restored locally", rel)
}
}
}

// Two PCs made the same folder at once: files in either copy count.
func TestSyncDuplicateFolders(t *testing.T) {
d := newFake()
Expand Down
4 changes: 3 additions & 1 deletion internal/paths/real.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,4 +194,6 @@ var shortNameForm = regexp.MustCompile(`~[0-9]+(\.[^.]*)?$`)
// short name, which may stand for a folder of another name. Other names
// with a "~" are fine: a real short name elsewhere is caught on disk by
// CheckSyncable.
func LooksShort(name string) bool { return shortNameForm.MatchString(name) }
func LooksShort(name string) bool {
return shortNameForm.MatchString(strings.TrimRight(name, ". "))
}
2 changes: 1 addition & 1 deletion internal/paths/real_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func TestResolveShortNameForm(t *testing.T) {
t.Errorf("Resolve(home, %q) refused", rel)
}
}
for _, rel := range []string{"PROGRA~1", "MICROS~1.TXT", "Games/SAVEGA~12", "a~1."} {
for _, rel := range []string{"PROGRA~1", "MICROS~1.TXT", "Games/SAVEGA~12", "a~1.", "PROGRA~1 ", "PROGRA~1..", "MICROS~1.TXT."} {
if p, ok := Resolve(Home, rel); ok {
t.Errorf("Resolve(home, %q) = %q, want refused", rel, p)
}
Expand Down
Loading