Here's a backlog of patches. This fails CI because of the golanglintci issue, but it does work. The folder mapping patch was tested by Bence originally. Tim Culverhouse (7): imap: download emails in batches cmds: add 'offmap check' command config: implement folder mapping chore: remove unused tmp dir for downloads chore: reorder goroutine for fetch commands sync: add daemon mode chore: move quit goroutine outside of loop cmd/check.go | 39 ++++++++++++ cmd/offmap.go | 1 + cmd/sync.go | 140 ++++++++++++++++++++++++------------------ config.go | 15 ++--- doc/offmap.1.scd | 12 ++++ doc/offmap.5.scd | 17 ++++++ imap/capability.go | 30 +++++++++ imap/client.go | 1 - imap/imap.go | 147 ++++++++++++++++++++++----------------------- imap/imap_test.go | 35 +++++++++++ maildir/maildir.go | 2 + 11 files changed, 300 insertions(+), 139 deletions(-) create mode 100644 cmd/check.go create mode 100644 imap/capability.go create mode 100644 imap/imap_test.go -- 2.39.2
offmap/patches/.build.yml: FAILED in 1m41s [Backlog-o-patches][0] from [Tim Culverhouse][1] [0]: https://lists.sr.ht/~rockorager/offmap/patches/39503 [1]: mailto:tim@timculverhouse.com ✗ #951712 FAILED offmap/patches/.build.yml https://builds.sr.ht/~rockorager/job/951712
Copy & paste the following snippet into your terminal to import this patchset into git:
curl -s https://lists.sr.ht/~rockorager/offmap/patches/39503/mbox | git am -3Learn more about email & git
Download email in batches (of size 2000) to prevent issues with Office 365. Requests can be too large for O365 to initiate the download. Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> --- imap/imap.go | 78 +++++++++++++++++++++++++++------------------------- 1 file changed, 40 insertions(+), 38 deletions(-) diff --git a/imap/imap.go b/imap/imap.go index 228669db260e..791248b04615 100644 --- a/imap/imap.go +++ b/imap/imap.go @@ -162,15 +162,11 @@ func (s *Store) DownloadEmail(mbox string, emls []*offmap.Email) chan *offmap.Fu var dled int32 = 0 total := len(emls) emlMu := sync.Mutex{} - seq := &imap.SeqSet{} - for _, eml := range emls { - seq.AddNum(eml.UID) - } client := s.getConn() go func() { defer client.done() defer close(ch) - if seq.Empty() { + if len(emls) == 0 { return } status, err := client.Select(mbox, true) @@ -180,41 +176,47 @@ func (s *Store) DownloadEmail(mbox string, emls []*offmap.Email) chan *offmap.Fu } section := &imap.BodySectionName{Peek: true} items := []imap.FetchItem{section.FetchItem()} - msgCh := make(chan *imap.Message) - msgWg := sync.WaitGroup{} - msgWg.Add(1) - go func() { - for msg := range msgCh { - atomic.AddInt32(&dled, 1) - cur := atomic.LoadInt32(&dled) - emlMu.Lock() - eml, ok := emlMap[msg.Uid] - emlMu.Unlock() - if !ok { - log.Errorf("imap: %s: eml not found: %d", status.Name, msg.Uid) - continue - } - r := msg.GetBody(section) - if r == nil { - log.Errorf("imap: no body") - continue - } - fe := &offmap.FullEmail{ - Email: eml, - Body: r, - } - ch <- fe - log.Tracef("imap: (client %d) downloaded %d of %d", client.id, cur, total) - } - msgWg.Done() - }() log.Debugf("imap (client %d): downloading uids in mailbox: %s", client.id, mbox) - err = client.UidFetch(seq, items, msgCh) - if err != nil { - log.Errorf("imap (client %d): %v", client.id, err) - return + seq := &imap.SeqSet{} + batch := 0 + for i, eml := range emls { + seq.AddNum(eml.UID) + batch = batch + 1 + if batch == 2000 || i == len(emls)-1 { + msgCh := make(chan *imap.Message) + go func() { + err = client.UidFetch(seq, items, msgCh) + if err != nil { + log.Errorf("imap (client %d): %v", client.id, err) + return + } + }() + for msg := range msgCh { + atomic.AddInt32(&dled, 1) + cur := atomic.LoadInt32(&dled) + emlMu.Lock() + eml, ok := emlMap[msg.Uid] + emlMu.Unlock() + if !ok { + log.Errorf("imap: %s: eml not found: %d", status.Name, msg.Uid) + continue + } + r := msg.GetBody(section) + if r == nil { + log.Errorf("imap: no body") + continue + } + fe := &offmap.FullEmail{ + Email: eml, + Body: r, + } + ch <- fe + log.Tracef("imap: (client %d) downloaded %d of %d", client.id, cur, total) + } + seq.Clear() + batch = 0 + } } - msgWg.Wait() }() return ch } -- 2.39.2
Add command 'check' which reports capabilities which will be used by offmap. Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> --- cmd/check.go | 39 +++++++++++++++++++++++++++++++++++++++ cmd/offmap.go | 1 + imap/capability.go | 30 ++++++++++++++++++++++++++++++ imap/client.go | 1 - 4 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 cmd/check.go create mode 100644 imap/capability.go diff --git a/cmd/check.go b/cmd/check.go new file mode 100644 index 000000000000..b7cba265136c --- /dev/null +++ b/cmd/check.go @@ -0,0 +1,39 @@ +package main + +import ( + "fmt" + + "git.sr.ht/~rockorager/offmap" + "git.sr.ht/~rockorager/offmap/imap" + "github.com/spf13/cobra" +) + +func newCheckCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "check", + Short: "check reports which capabilities of the imap server will be used", + RunE: check, + } + cmd.Flags().StringSliceP("account", "a", nil, "only sync the specified account(s)") + return cmd +} + +func check(cmd *cobra.Command, args []string) error { + accts, err := cmd.Flags().GetStringSlice("account") + if err != nil { + return fmt.Errorf("offmap: could not parse account: %v", err) + } + cfgs, err := offmap.LoadConfig(accts) + if err != nil { + return fmt.Errorf("could not read config: %v", err) + } + for _, cfg := range cfgs { + fmt.Printf("Account %s\n", cfg.Name) + remote, err := imap.NewStore(cfg) + if err != nil { + return fmt.Errorf("could not create store: %v", err) + } + remote.Capabilities() + } + return nil +} diff --git a/cmd/offmap.go b/cmd/offmap.go index 8a2176f28164..3fff3ccd4490 100644 --- a/cmd/offmap.go +++ b/cmd/offmap.go @@ -26,6 +26,7 @@ func main() { cmd.AddCommand(newSyncCommand()) cmd.AddCommand(newDiffCommand()) + cmd.AddCommand(newCheckCommand()) if err := cmd.ExecuteContext(ctx); err != nil { log.Fatalf("%v", err) } diff --git a/imap/capability.go b/imap/capability.go new file mode 100644 index 000000000000..fbd2f41238ed --- /dev/null +++ b/imap/capability.go @@ -0,0 +1,30 @@ +package imap + +import ( + "fmt" + + "git.sr.ht/~rockorager/offmap/log" +) + +func (s *Store) Capabilities() { + capsUsed := []string{ + "UIDPLUS", + "CONDSTORE", + "LIST-STATUS", + } + client := s.getConn() + caps, err := client.Capability() + if err != nil { + log.Errorf("could not get capabilities: %v", err) + return + } + for _, cap := range capsUsed { + _, has := caps[cap] + switch has { + case true: + fmt.Printf("Capability found: %s\n", cap) + case false: + fmt.Printf("Your server doesn't support '%s'\n", cap) + } + } +} diff --git a/imap/client.go b/imap/client.go index c410993950d4..e45f87b68c89 100644 --- a/imap/client.go +++ b/imap/client.go @@ -89,7 +89,6 @@ func (s *Store) initClients() { conn.lsc = liststatus.NewClient(c) } } - s.Lock() s.clients[n] = conn s.Unlock() -- 2.39.2
Implement folder mapping from remote folder name to local folder name. Add test for mappings, update docs. Maps must be of the form: REMOTE = LOCAL And it is recommended in the docs that the key should be quoted as well, specifically when the remote uses '.' delimiters, as this will break toml parsing. Quoted keys with '.' are parsed as raw strings and will not break toml. Example: "INBOX.test" = "Inbox/test" // Works INBOX.test = "Inbox/test" // Doesn't work Implements: https://todo.sr.ht/~rockorager/offmap/19 Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> --- config.go | 15 ++++++++------- doc/offmap.5.scd | 17 +++++++++++++++++ imap/imap.go | 26 ++++++++++++++++++++++---- imap/imap_test.go | 35 +++++++++++++++++++++++++++++++++++ maildir/maildir.go | 2 ++ 5 files changed, 84 insertions(+), 11 deletions(-) create mode 100644 imap/imap_test.go diff --git a/config.go b/config.go index e6fc2016d5d0..2c6bd07d4cc8 100644 --- a/config.go +++ b/config.go @@ -11,13 +11,14 @@ import ( type Config struct { Name string - Maildir string `toml:"maildir"` - Host string `toml:"host"` - Username string `toml:"username"` - Password string `toml:"password"` - PasswordCmd string `toml:"password-cmd"` - AuthMech string `toml:"auth-mech"` - MaxConnections int `toml:"max-connections"` + Maildir string `toml:"maildir"` + Host string `toml:"host"` + Username string `toml:"username"` + Password string `toml:"password"` + PasswordCmd string `toml:"password-cmd"` + AuthMech string `toml:"auth-mech"` + MaxConnections int `toml:"max-connections"` + Map map[string]string `toml:"map"` } func LoadConfig(accts []string) ([]*Config, error) { diff --git a/doc/offmap.5.scd b/doc/offmap.5.scd index da4ee3b7a0b0..e715439ecef0 100644 --- a/doc/offmap.5.scd +++ b/doc/offmap.5.scd @@ -68,3 +68,20 @@ auth-mech = "oauth2" effect for IMAP servers who *do not* support LIST-STATUS. Default: 1 + +*map* + A table of remote mailbox names mapped to local mailbox names. The key + should always be the remote mailbox name, and is recommended to be in + quotes. IMAP servers with '.' delimiters require quotes. Maps will apply + to any children of the mapped mailbox. + + Example: + ``` + [account-name.map] + "INBOX" = "inbox" + "Lists/offmap" = "offmap" + ``` + + In the above example, a child of "INBOX" would be mapped from + "INBOX/child" to "inbox/child". However, siblings of "Lists/offmap" + would not be mapped, and would still exist under "Lists/*". diff --git a/imap/imap.go b/imap/imap.go index 791248b04615..db1f7a853097 100644 --- a/imap/imap.go +++ b/imap/imap.go @@ -292,10 +292,7 @@ func (s *Store) listMailboxes() ([]*offmap.Mailbox, error) { // Process the statuses for _, status := range statuses { - local := status.Name - if s.Delimiter() != "/" { - local = strings.ReplaceAll(local, s.Delimiter(), "/") - } + local := s.localName(status.Name) mailbox := offmap.NewMailbox(status.UidValidity, local, status.Name) mailbox.UIDNext = status.UidNext if s.SupportCondstore() { @@ -306,6 +303,27 @@ func (s *Store) listMailboxes() ([]*offmap.Mailbox, error) { return s.mailboxes, nil } +// Finds the local name by replacing delimiters and checking the configured map +func (s *Store) localName(remote string) string { + local := remote + nodes := strings.Split(remote, s.Delimiter()) + // Find mapped paths by starting at most specific, and moving up the + // tree. The first mapped path breaks the loop, as this is the most + // specific mapping and the only one we will apply + for i := len(nodes); i > 0; i -= 1 { + path := strings.Join(nodes[:i], s.Delimiter()) + mapped, ok := s.cfg.Map[path] + if ok { + local = strings.Replace(local, path, mapped, 1) + break + } + } + if s.Delimiter() != "/" { + local = strings.ReplaceAll(local, s.Delimiter(), "/") + } + return local +} + // Returns the delimiter used on the server func (s *Store) Delimiter() string { if s.delim == "" { diff --git a/imap/imap_test.go b/imap/imap_test.go new file mode 100644 index 000000000000..1ed10cb0ee4c --- /dev/null +++ b/imap/imap_test.go @@ -0,0 +1,35 @@ +package imap + +import ( + "testing" + + "git.sr.ht/~rockorager/offmap" + "github.com/stretchr/testify/assert" +) + +func TestLocalName(t *testing.T) { + cfg := &offmap.Config{} + cfg.Map = map[string]string{ + "INBOX": "inbox", + "INBOX/child": "child", + "[GMAIL]/All Mail": "Archive", + } + store := Store{cfg: cfg} + store.delim = "/" + assert := assert.New(t) + assert.Equal("inbox", store.localName("INBOX")) + assert.Equal("child", store.localName("INBOX/child")) + assert.Equal("child/grandchild", store.localName("INBOX/child/grandchild")) + assert.Equal("inbox/sibling", store.localName("INBOX/sibling")) + assert.Equal("Archive", store.localName("[GMAIL]/All Mail")) + + store.delim = "." + cfg.Map = map[string]string{ + "INBOX": "inbox", + "INBOX.child": "child", + } + assert.Equal("inbox", store.localName("INBOX")) + assert.Equal("child", store.localName("INBOX.child")) + assert.Equal("child/grandchild", store.localName("INBOX.child.grandchild")) + assert.Equal("inbox/sibling", store.localName("INBOX.sibling")) +} diff --git a/maildir/maildir.go b/maildir/maildir.go index a3ed66496a54..7743f40841d8 100644 --- a/maildir/maildir.go +++ b/maildir/maildir.go @@ -21,6 +21,7 @@ type Store struct { state *offmap.State // map of relpath to mailbox mailboxes map[string]*offmap.Mailbox + dirmap map[string]string } func NewStore(cfg *offmap.Config) (*Store, error) { @@ -35,6 +36,7 @@ func NewStore(cfg *offmap.Config) (*Store, error) { if err != nil { return nil, fmt.Errorf("maildir: error creating root: %v", err) } + store.dirmap = cfg.Map return store, nil } -- 2.39.2
A previous commit changed the download strategy. The tmp dir is no longer needed. Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> --- imap/imap.go | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/imap/imap.go b/imap/imap.go index db1f7a853097..731f726544e9 100644 --- a/imap/imap.go +++ b/imap/imap.go @@ -2,9 +2,7 @@ package imap import ( "fmt" - "os" "os/exec" - "path" "strconv" "strings" "sync" @@ -24,8 +22,6 @@ type Store struct { // delim is the imap server's hierarchical delimiter. Offmap uses "/", // so a server which uses "." must have the "."s replaced delim string - // tmp is the temp directory where files are located when downloaded - tmp string cache map[uint32]*offmap.Mailbox mailboxes []*offmap.Mailbox @@ -56,17 +52,6 @@ func NewStore(cfg *offmap.Config) (*Store, error) { store.clients = make(map[int]*conn, store.cfg.MaxConnections) store.initClients() - var err error - cdir, err := os.UserCacheDir() - if err != nil { - return nil, fmt.Errorf("imap: could not convert uid: %v", err) - } - store.tmp = path.Join(cdir, "offmap") - err = os.MkdirAll(store.tmp, 0o700) - if err != nil { - return nil, fmt.Errorf("imap: could not convert uid: %v", err) - } - return store, nil } @@ -334,7 +319,6 @@ func (s *Store) Delimiter() string { func (s *Store) Cleanup() { s.logout() - os.RemoveAll(s.tmp) } func parseHighestModSeq(status *imap.MailboxStatus) uint64 { -- 2.39.2
Reorder for better readability, and remove a waitgroup Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> --- imap/imap.go | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/imap/imap.go b/imap/imap.go index 731f726544e9..4db1dff51160 100644 --- a/imap/imap.go +++ b/imap/imap.go @@ -106,26 +106,21 @@ func (s *Store) getCurrentState() ([]*offmap.Mailbox, error) { seq.AddRange(1, 0) items := []imap.FetchItem{imap.FetchFlags} msgCh := make(chan *imap.Message) - msgWg := sync.WaitGroup{} - msgWg.Add(1) go func() { - for msg := range msgCh { - flags := flagIMAPToMaildir(msg.Flags) - eml := offmap.NewEmail(mbox, msg.Uid, "", flags) - mbox.Emails = append(mbox.Emails, eml) - atomic.AddInt32(&emails, 1) + log.Tracef("imap (client %d): fetching uids in mailbox: %s", client.id, mbox.RemoteName) + err = client.UidFetch(seq, items, msgCh) + if err != nil { + log.Errorf("imap (client %d): %v", client.id, err) + client.done() + return } - msgWg.Done() }() - log.Tracef("imap (client %d): fetching uids in mailbox: %s", client.id, mbox.RemoteName) - err = client.UidFetch(seq, items, msgCh) - if err != nil { - log.Errorf("imap (client %d): %v", client.id, err) - client.done() - wg.Done() - return + for msg := range msgCh { + flags := flagIMAPToMaildir(msg.Flags) + eml := offmap.NewEmail(mbox, msg.Uid, "", flags) + mbox.Emails = append(mbox.Emails, eml) + atomic.AddInt32(&emails, 1) } - msgWg.Wait() client.done() wg.Done() }(client, mbox) -- 2.39.2
Implement a daemon mode, which persists IMAP connections and triggers a sync every specified duration. The duration is implemented as a wait period after the last sync, therefore the total round trip is duration + sync time. This prevents a particularly long sync (initial sync, many emails to download, etc) from overlapping. Implements: https://todo.sr.ht/~rockorager/offmap/6 Signed-off-by: Tim Culverhouse <tim@timculverhouse.com> --- cmd/sync.go | 139 ++++++++++++++++++++++++++++------------------- doc/offmap.1.scd | 12 ++++ 2 files changed, 95 insertions(+), 56 deletions(-) diff --git a/cmd/sync.go b/cmd/sync.go index fbaa48cd34e9..ea51ee15f0a8 100644 --- a/cmd/sync.go +++ b/cmd/sync.go @@ -18,6 +18,7 @@ func newSyncCommand() *cobra.Command { Short: "sync synchronizes mail stores", Run: syncRun, } + cmd.Flags().DurationP("daemon", "d", 0*time.Second, "run in daemon mode, syncing at the specified interval") cmd.Flags().StringSliceP("account", "a", nil, "only sync the specified account(s)") return cmd } @@ -30,6 +31,11 @@ func syncRun(cmd *cobra.Command, args []string) { log.Errorf("offmap: could not parse account: %v", err) return } + interval, err := cmd.Flags().GetDuration("daemon") + if err != nil { + log.Errorf("offmap: could not parse duration: %v", err) + return + } cfgs, err := offmap.LoadConfig(accts) if err != nil { log.Debugf("%v", err) @@ -60,75 +66,96 @@ func syncRun(cmd *cobra.Command, args []string) { log.Fatalf("could not create store: %v", err) } - // Get the diffs - localDiff, err := local.Diff(state.Mailboxes) - if err != nil { - log.Fatalf("%v", err) - } - remoteDiff, err := remote.Diff(state.Mailboxes) - if err != nil { - log.Fatalf("%v", err) - } + var ( + diffWg sync.WaitGroup + localDiff *offmap.Diff + remoteDiff *offmap.Diff + ) + for { + // Get the diffs + diffWg.Add(2) + go func() { + defer diffWg.Done() + localDiff, err = local.Diff(state.Mailboxes) + if err != nil { + log.Fatalf("%v", err) + } + }() + go func() { + defer diffWg.Done() + remoteDiff, err = remote.Diff(state.Mailboxes) + if err != nil { + log.Fatalf("%v", err) + } + }() + diffWg.Wait() - var quit atomic.Bool - go func() { - // Save our state if we cancel the command - <-cmd.Context().Done() - quit.Store(true) - }() + var quit atomic.Bool + go func() { + // Save our state if we cancel the command + <-cmd.Context().Done() + quit.Store(true) + }() - // Do mailboxes first - err = remote.ApplyMailboxDiff(state, localDiff) - if err != nil { - // Save any changes we've made. log.Fatalf calls os.Exit(1). - // Defers are bypassed with this call - if err := state.Save(); err != nil { - log.Errorf("offmap: could not save state: %v", err) - } - log.Fatalf("imap: could not apply diff: %v", err) - } - - // Apply remote changes to local - err = local.ApplyMailboxDiff(state, remoteDiff) - if err != nil { - // Save any changes we've made. log.Fatalf calls os.Exit(1). - // Defers are bypassed with this call - if err := state.Save(); err != nil { - log.Errorf("offmap: could not save state: %v", err) - } - log.Fatalf("%v", err) - } - - // Now the state has an up to date picture of mailboxes. Sync - // each mailbox, if it has changes - for _, mbox := range state.Mailboxes { - if quit.Load() { - break - } - ok1, err := remote.ApplyEmailDiff(state, mbox.RemoteName, localDiff) + // Do mailboxes first + err = remote.ApplyMailboxDiff(state, localDiff) if err != nil { - log.Errorf("%v", err) - continue + // Save any changes we've made. log.Fatalf calls os.Exit(1). + // Defers are bypassed with this call + if err := state.Save(); err != nil { + log.Errorf("offmap: could not save state: %v", err) + } + log.Fatalf("imap: could not apply diff: %v", err) } - ok2, err := local.ApplyEmailDiff(state, mbox.LocalName, remoteDiff) + + // Apply remote changes to local + err = local.ApplyMailboxDiff(state, remoteDiff) if err != nil { - log.Errorf("%v", err) - continue + // Save any changes we've made. log.Fatalf calls os.Exit(1). + // Defers are bypassed with this call + if err := state.Save(); err != nil { + log.Errorf("offmap: could not save state: %v", err) + } + log.Fatalf("%v", err) } - if ok1 || ok2 { - err = remote.UpdateMailboxStatus(state, mbox.LocalName) + + // Now the state has an up to date picture of mailboxes. Sync + // each mailbox, if it has changes + for _, mbox := range state.Mailboxes { + if quit.Load() { + break + } + ok1, err := remote.ApplyEmailDiff(state, mbox.RemoteName, localDiff) if err != nil { log.Errorf("%v", err) + continue + } + ok2, err := local.ApplyEmailDiff(state, mbox.LocalName, remoteDiff) + if err != nil { + log.Errorf("%v", err) + continue + } + if ok1 || ok2 { + err = remote.UpdateMailboxStatus(state, mbox.LocalName) + if err != nil { + log.Errorf("%v", err) + } } } - } - // Save and clean up - if err := state.Save(); err != nil { - log.Errorf("offmap: could not save state: %v", err) + // Save and clean up + if err := state.Save(); err != nil { + log.Errorf("offmap: could not save state: %v", err) + } + + log.Infof("offmap (%s): sync completed in %s", cfg.Name, time.Since(start).Round(1*time.Millisecond)) + if interval == 0 || quit.Load() { + break + } + time.Sleep(interval) + start = time.Now() } remote.Cleanup() - log.Infof("offmap (%s): sync completed in %s", cfg.Name, time.Since(start).Round(1*time.Millisecond)) }(cfg) } diff --git a/doc/offmap.1.scd b/doc/offmap.1.scd index bdfcee241a23..565d940a234c 100644 --- a/doc/offmap.1.scd +++ b/doc/offmap.1.scd @@ -27,6 +27,18 @@ offmap - an email synchronizer offmap sync -a personal -a work ``` +*-d, --daemon* <duration> + Run offmap in daemon mode, syncing at the specified duration. The + duration is applied after the last sync is complete. The IMAP connection + is persisted; it is recommended to keep a low number of max-connections + when using daemon mode, particularly if your IMAP server supports + LIST-STATUS. + + Example: + ``` + offmap sync -a personal -d 30s + ``` + *-v, --verbose* Increase verbosity level. Can be used multiple times -- 2.39.2
Move the quit goroutine outside of the main loop to prevent creating it several times during daemon mode --- cmd/sync.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/cmd/sync.go b/cmd/sync.go index ea51ee15f0a8..f4533be89cc8 100644 --- a/cmd/sync.go +++ b/cmd/sync.go @@ -40,6 +40,12 @@ func syncRun(cmd *cobra.Command, args []string) { if err != nil { log.Debugf("%v", err) } + var quit atomic.Bool + go func() { + // Save our state if we cancel the command + <-cmd.Context().Done() + quit.Store(true) + }() var wg sync.WaitGroup for _, cfg := range cfgs { wg.Add(1) @@ -90,13 +96,6 @@ func syncRun(cmd *cobra.Command, args []string) { }() diffWg.Wait() - var quit atomic.Bool - go func() { - // Save our state if we cancel the command - <-cmd.Context().Done() - quit.Store(true) - }() - // Do mailboxes first err = remote.ApplyMailboxDiff(state, localDiff) if err != nil { -- 2.39.2
builds.sr.ht <builds@sr.ht>offmap/patches/.build.yml: FAILED in 1m41s [Backlog-o-patches][0] from [Tim Culverhouse][1] [0]: https://lists.sr.ht/~rockorager/offmap/patches/39503 [1]: mailto:tim@timculverhouse.com ✗ #951712 FAILED offmap/patches/.build.yml https://builds.sr.ht/~rockorager/job/951712