diff --git a/gui/default/index.html b/gui/default/index.html index 7d05d39c9..9a1b2cdc1 100644 --- a/gui/default/index.html +++ b/gui/default/index.html @@ -314,6 +314,8 @@ + + diff --git a/gui/default/syncthing/core/syncthingController.js b/gui/default/syncthing/core/syncthingController.js index 161dd7fdd..25f7afc8a 100755 --- a/gui/default/syncthing/core/syncthingController.js +++ b/gui/default/syncthing/core/syncthingController.js @@ -824,7 +824,7 @@ angular.module('syncthing.core') if (status == 'paused') { return 'default'; } - if (status === 'syncing' || status === 'sync-preparing' || status === 'scanning') { + if (status === 'syncing' || status === 'sync-preparing' || status === 'scanning' || status === 'cleaning') { return 'primary'; } if (status === 'unknown') { @@ -833,7 +833,7 @@ angular.module('syncthing.core') if (status === 'stopped' || status === 'outofsync' || status === 'error' || status === 'faileditems') { return 'danger'; } - if (status === 'unshared' || status === 'scan-waiting' || status === 'sync-waiting') { + if (status === 'unshared' || status === 'scan-waiting' || status === 'sync-waiting' || status === 'clean-waiting') { return 'warning'; } diff --git a/lib/config/folderconfiguration.go b/lib/config/folderconfiguration.go index da31376e6..ebd41d86a 100644 --- a/lib/config/folderconfiguration.go +++ b/lib/config/folderconfiguration.go @@ -226,6 +226,11 @@ func (f *FolderConfiguration) prepare() { if f.Versioning.Params == nil { f.Versioning.Params = make(map[string]string) } + if f.Versioning.CleanupIntervalS > MaxRescanIntervalS { + f.Versioning.CleanupIntervalS = MaxRescanIntervalS + } else if f.Versioning.CleanupIntervalS < 0 { + f.Versioning.CleanupIntervalS = 0 + } if f.WeakHashThresholdPct == 0 { f.WeakHashThresholdPct = 25 diff --git a/lib/config/versioningconfiguration.go b/lib/config/versioningconfiguration.go index b9922ca32..90cd496db 100644 --- a/lib/config/versioningconfiguration.go +++ b/lib/config/versioningconfiguration.go @@ -7,21 +7,28 @@ package config import ( + "encoding/json" "encoding/xml" "sort" + + "github.com/syncthing/syncthing/lib/util" ) +// VersioningConfiguration is used in the code and for JSON serialization type VersioningConfiguration struct { - Type string `xml:"type,attr" json:"type"` - Params map[string]string `json:"params"` + Type string `json:"type"` + Params map[string]string `json:"params"` + CleanupIntervalS int `json:"cleanupIntervalS" default:"3600"` } -type InternalVersioningConfiguration struct { - Type string `xml:"type,attr,omitempty"` - Params []InternalParam `xml:"param"` +// internalVersioningConfiguration is used in XML serialization +type internalVersioningConfiguration struct { + Type string `xml:"type,attr,omitempty"` + Params []internalParam `xml:"param"` + CleanupIntervalS int `xml:"cleanupIntervalS" default:"3600"` } -type InternalParam struct { +type internalParam struct { Key string `xml:"key,attr"` Val string `xml:"val,attr"` } @@ -35,31 +42,45 @@ func (c VersioningConfiguration) Copy() VersioningConfiguration { return cp } +func (c *VersioningConfiguration) UnmarshalJSON(data []byte) error { + util.SetDefaults(c) + type noCustomUnmarshal VersioningConfiguration + ptr := (*noCustomUnmarshal)(c) + return json.Unmarshal(data, ptr) +} + +func (c *VersioningConfiguration) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + var intCfg internalVersioningConfiguration + util.SetDefaults(&intCfg) + if err := d.DecodeElement(&intCfg, &start); err != nil { + return err + } + c.fromInternal(intCfg) + return nil +} + func (c *VersioningConfiguration) MarshalXML(e *xml.Encoder, start xml.StartElement) error { - var tmp InternalVersioningConfiguration + return e.Encode(c.toInternal()) +} + +func (c *VersioningConfiguration) toInternal() internalVersioningConfiguration { + var tmp internalVersioningConfiguration tmp.Type = c.Type + tmp.CleanupIntervalS = c.CleanupIntervalS for k, v := range c.Params { - tmp.Params = append(tmp.Params, InternalParam{k, v}) + tmp.Params = append(tmp.Params, internalParam{k, v}) } sort.Slice(tmp.Params, func(a, b int) bool { return tmp.Params[a].Key < tmp.Params[b].Key }) - - return e.EncodeElement(tmp, start) - + return tmp } -func (c *VersioningConfiguration) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { - var tmp InternalVersioningConfiguration - err := d.DecodeElement(&tmp, &start) - if err != nil { - return err - } - - c.Type = tmp.Type - c.Params = make(map[string]string, len(tmp.Params)) - for _, p := range tmp.Params { +func (c *VersioningConfiguration) fromInternal(intCfg internalVersioningConfiguration) { + c.Type = intCfg.Type + c.CleanupIntervalS = intCfg.CleanupIntervalS + c.Params = make(map[string]string, len(intCfg.Params)) + for _, p := range intCfg.Params { c.Params[p.Key] = p.Val } - return nil } diff --git a/lib/model/folder.go b/lib/model/folder.go index d2a29a8c4..808b7d7d4 100644 --- a/lib/model/folder.go +++ b/lib/model/folder.go @@ -29,6 +29,7 @@ import ( "github.com/syncthing/syncthing/lib/stats" "github.com/syncthing/syncthing/lib/sync" "github.com/syncthing/syncthing/lib/util" + "github.com/syncthing/syncthing/lib/versioner" "github.com/syncthing/syncthing/lib/watchaggregator" "github.com/thejerf/suture" @@ -49,12 +50,14 @@ type folder struct { ignores *ignore.Matcher ctx context.Context - scanInterval time.Duration - scanTimer *time.Timer - scanDelay chan time.Duration - initialScanFinished chan struct{} - scanErrors []FileError - scanErrorsMut sync.Mutex + scanInterval time.Duration + scanTimer *time.Timer + scanDelay chan time.Duration + initialScanFinished chan struct{} + scanErrors []FileError + scanErrorsMut sync.Mutex + versionCleanupInterval time.Duration + versionCleanupTimer *time.Timer pullScheduled chan struct{} pullPause time.Duration @@ -72,7 +75,8 @@ type folder struct { watchErr error watchMut sync.Mutex - puller puller + puller puller + versioner versioner.Versioner } type syncRequest struct { @@ -81,10 +85,10 @@ type syncRequest struct { } type puller interface { - pull() bool // true when successfull and should not be retried + pull() bool // true when successful and should not be retried } -func newFolder(model *model, fset *db.FileSet, ignores *ignore.Matcher, cfg config.FolderConfiguration, evLogger events.Logger, ioLimiter *byteSemaphore) folder { +func newFolder(model *model, fset *db.FileSet, ignores *ignore.Matcher, cfg config.FolderConfiguration, evLogger events.Logger, ioLimiter *byteSemaphore, ver versioner.Versioner) folder { f := folder{ stateTracker: newStateTracker(cfg.ID, evLogger), FolderConfiguration: cfg, @@ -96,11 +100,13 @@ func newFolder(model *model, fset *db.FileSet, ignores *ignore.Matcher, cfg conf fset: fset, ignores: ignores, - scanInterval: time.Duration(cfg.RescanIntervalS) * time.Second, - scanTimer: time.NewTimer(time.Millisecond), // The first scan should be done immediately. - scanDelay: make(chan time.Duration), - initialScanFinished: make(chan struct{}), - scanErrorsMut: sync.NewMutex(), + scanInterval: time.Duration(cfg.RescanIntervalS) * time.Second, + scanTimer: time.NewTimer(0), // The first scan should be done immediately. + scanDelay: make(chan time.Duration), + initialScanFinished: make(chan struct{}), + scanErrorsMut: sync.NewMutex(), + versionCleanupInterval: time.Duration(cfg.Versioning.CleanupIntervalS) * time.Second, + versionCleanupTimer: time.NewTimer(time.Duration(cfg.Versioning.CleanupIntervalS) * time.Second), pullScheduled: make(chan struct{}, 1), // This needs to be 1-buffered so that we queue a pull if we're busy when it comes. @@ -113,6 +119,8 @@ func newFolder(model *model, fset *db.FileSet, ignores *ignore.Matcher, cfg conf watchCancel: func() {}, restartWatchChan: make(chan struct{}, 1), watchMut: sync.NewMutex(), + + versioner: ver, } f.pullPause = f.pullBasePause() f.pullFailTimer = time.NewTimer(0) @@ -131,6 +139,7 @@ func (f *folder) serve(ctx context.Context) { defer func() { f.scanTimer.Stop() + f.versionCleanupTimer.Stop() f.setState(FolderIdle) }() @@ -138,6 +147,14 @@ func (f *folder) serve(ctx context.Context) { f.startWatch() } + // If we're configured to not do version cleanup, or we don't have a + // versioner, cancel and drain that timer now. + if f.versionCleanupInterval == 0 || f.versioner == nil { + if !f.versionCleanupTimer.Stop() { + <-f.versionCleanupTimer.C + } + } + initialCompleted := f.initialScanFinished for { @@ -181,6 +198,10 @@ func (f *folder) serve(ctx context.Context) { case <-f.restartWatchChan: l.Debugln(f, "Restart watcher") f.restartWatch() + + case <-f.versionCleanupTimer.C: + l.Debugln(f, "Doing version cleanup") + f.versionCleanupTimerFired() } } } @@ -701,6 +722,24 @@ func (f *folder) scanTimerFired() { f.Reschedule() } +func (f *folder) versionCleanupTimerFired() { + f.setState(FolderCleanWaiting) + defer f.setState(FolderIdle) + + if err := f.ioLimiter.takeWithContext(f.ctx, 1); err != nil { + return + } + defer f.ioLimiter.give(1) + + f.setState(FolderCleaning) + + if err := f.versioner.Clean(f.ctx); err != nil { + l.Infoln("Failed to clean versions in %s: %v", f.Description(), err) + } + + f.versionCleanupTimer.Reset(f.versionCleanupInterval) +} + func (f *folder) WatchError() error { f.watchMut.Lock() defer f.watchMut.Unlock() diff --git a/lib/model/folder_sendonly.go b/lib/model/folder_sendonly.go index 17fb7c321..a5dcc96a4 100644 --- a/lib/model/folder_sendonly.go +++ b/lib/model/folder_sendonly.go @@ -27,7 +27,7 @@ type sendOnlyFolder struct { func newSendOnlyFolder(model *model, fset *db.FileSet, ignores *ignore.Matcher, cfg config.FolderConfiguration, _ versioner.Versioner, _ fs.Filesystem, evLogger events.Logger, ioLimiter *byteSemaphore) service { f := &sendOnlyFolder{ - folder: newFolder(model, fset, ignores, cfg, evLogger, ioLimiter), + folder: newFolder(model, fset, ignores, cfg, evLogger, ioLimiter, nil), } f.folder.puller = f f.folder.Service = util.AsService(f.serve, f.String()) diff --git a/lib/model/folder_sendrecv.go b/lib/model/folder_sendrecv.go index cfb7a83b5..8fe3ece20 100644 --- a/lib/model/folder_sendrecv.go +++ b/lib/model/folder_sendrecv.go @@ -123,8 +123,7 @@ type dbUpdateJob struct { type sendReceiveFolder struct { folder - fs fs.Filesystem - versioner versioner.Versioner + fs fs.Filesystem queue *jobQueue blockPullReorderer blockPullReorderer @@ -137,9 +136,8 @@ type sendReceiveFolder struct { func newSendReceiveFolder(model *model, fset *db.FileSet, ignores *ignore.Matcher, cfg config.FolderConfiguration, ver versioner.Versioner, fs fs.Filesystem, evLogger events.Logger, ioLimiter *byteSemaphore) service { f := &sendReceiveFolder{ - folder: newFolder(model, fset, ignores, cfg, evLogger, ioLimiter), + folder: newFolder(model, fset, ignores, cfg, evLogger, ioLimiter, ver), fs: fs, - versioner: ver, queue: newJobQueue(), blockPullReorderer: newBlockPullReorderer(cfg.BlockPullOrder, model.id, cfg.DeviceIDs()), writeLimiter: newByteSemaphore(cfg.MaxConcurrentWrites), diff --git a/lib/model/folderstate.go b/lib/model/folderstate.go index 6a273029d..ec7150c90 100644 --- a/lib/model/folderstate.go +++ b/lib/model/folderstate.go @@ -22,6 +22,8 @@ const ( FolderSyncWaiting FolderSyncPreparing FolderSyncing + FolderCleaning + FolderCleanWaiting FolderError ) @@ -39,6 +41,10 @@ func (s folderState) String() string { return "sync-preparing" case FolderSyncing: return "syncing" + case FolderCleaning: + return "cleaning" + case FolderCleanWaiting: + return "clean-waiting" case FolderError: return "error" default: diff --git a/lib/model/model.go b/lib/model/model.go index de9de227f..78167e31c 100644 --- a/lib/model/model.go +++ b/lib/model/model.go @@ -133,15 +133,15 @@ type model struct { folderIOLimiter *byteSemaphore // fields protected by fmut - fmut sync.RWMutex - folderCfgs map[string]config.FolderConfiguration // folder -> cfg - folderFiles map[string]*db.FileSet // folder -> files - deviceStatRefs map[protocol.DeviceID]*stats.DeviceStatisticsReference // deviceID -> statsRef - folderIgnores map[string]*ignore.Matcher // folder -> matcher object - folderRunners map[string]service // folder -> puller or scanner - folderRunnerTokens map[string][]suture.ServiceToken // folder -> tokens for puller or scanner - folderRestartMuts syncMutexMap // folder -> restart mutex - folderVersioners map[string]versioner.Versioner // folder -> versioner (may be nil) + fmut sync.RWMutex + folderCfgs map[string]config.FolderConfiguration // folder -> cfg + folderFiles map[string]*db.FileSet // folder -> files + deviceStatRefs map[protocol.DeviceID]*stats.DeviceStatisticsReference // deviceID -> statsRef + folderIgnores map[string]*ignore.Matcher // folder -> matcher object + folderRunners map[string]service // folder -> puller or scanner + folderRunnerToken map[string]suture.ServiceToken // folder -> token for folder runner + folderRestartMuts syncMutexMap // folder -> restart mutex + folderVersioners map[string]versioner.Versioner // folder -> versioner (may be nil) // fields protected by pmut pmut sync.RWMutex @@ -207,14 +207,14 @@ func NewModel(cfg config.Wrapper, id protocol.DeviceID, clientName, clientVersio folderIOLimiter: newByteSemaphore(cfg.Options().MaxFolderConcurrency()), // fields protected by fmut - fmut: sync.NewRWMutex(), - folderCfgs: make(map[string]config.FolderConfiguration), - folderFiles: make(map[string]*db.FileSet), - deviceStatRefs: make(map[protocol.DeviceID]*stats.DeviceStatisticsReference), - folderIgnores: make(map[string]*ignore.Matcher), - folderRunners: make(map[string]service), - folderRunnerTokens: make(map[string][]suture.ServiceToken), - folderVersioners: make(map[string]versioner.Versioner), + fmut: sync.NewRWMutex(), + folderCfgs: make(map[string]config.FolderConfiguration), + folderFiles: make(map[string]*db.FileSet), + deviceStatRefs: make(map[protocol.DeviceID]*stats.DeviceStatisticsReference), + folderIgnores: make(map[string]*ignore.Matcher), + folderRunners: make(map[string]service), + folderRunnerToken: make(map[string]suture.ServiceToken), + folderVersioners: make(map[string]versioner.Versioner), // fields protected by pmut pmut: sync.NewRWMutex(), @@ -346,13 +346,6 @@ func (m *model) addAndStartFolderLockedWithIgnores(cfg config.FolderConfiguratio if err != nil { panic(fmt.Errorf("creating versioner: %w", err)) } - if service, ok := ver.(suture.Service); ok { - // The versioner implements the suture.Service interface, so - // expects to be run in the background in addition to being called - // when files are going to be archived. - token := m.Add(service) - m.folderRunnerTokens[folder] = append(m.folderRunnerTokens[folder], token) - } } m.folderVersioners[folder] = ver @@ -362,8 +355,7 @@ func (m *model) addAndStartFolderLockedWithIgnores(cfg config.FolderConfiguratio m.warnAboutOverwritingProtectedFiles(cfg, ignores) - token := m.Add(p) - m.folderRunnerTokens[folder] = append(m.folderRunnerTokens[folder], token) + m.folderRunnerToken[folder] = m.Add(p) l.Infof("Ready to synchronize %s (%s)", cfg.Description(), cfg.Type) } @@ -430,11 +422,11 @@ func (m *model) stopFolder(cfg config.FolderConfiguration, err error) { // Stop the services running for this folder and wait for them to finish // stopping to prevent races on restart. m.fmut.RLock() - tokens := m.folderRunnerTokens[cfg.ID] + token, ok := m.folderRunnerToken[cfg.ID] m.fmut.RUnlock() - for _, id := range tokens { - m.RemoveAndWait(id, 0) + if ok { + m.RemoveAndWait(token, 0) } // Wait for connections to stop to ensure that no more calls to methods @@ -449,7 +441,7 @@ func (m *model) cleanupFolderLocked(cfg config.FolderConfiguration) { delete(m.folderFiles, cfg.ID) delete(m.folderIgnores, cfg.ID) delete(m.folderRunners, cfg.ID) - delete(m.folderRunnerTokens, cfg.ID) + delete(m.folderRunnerToken, cfg.ID) delete(m.folderVersioners, cfg.ID) } diff --git a/lib/versioner/external.go b/lib/versioner/external.go index 5abe59170..f786f7e40 100644 --- a/lib/versioner/external.go +++ b/lib/versioner/external.go @@ -7,6 +7,7 @@ package versioner import ( + "context" "errors" "os" "os/exec" @@ -115,3 +116,7 @@ func (v external) GetVersions() (map[string][]FileVersion, error) { func (v external) Restore(filePath string, versionTime time.Time) error { return ErrRestorationNotSupported } + +func (v external) Clean(_ context.Context) error { + return nil +} diff --git a/lib/versioner/simple.go b/lib/versioner/simple.go index ae3437154..d8cbcc504 100644 --- a/lib/versioner/simple.go +++ b/lib/versioner/simple.go @@ -7,6 +7,7 @@ package versioner import ( + "context" "strconv" "time" @@ -73,3 +74,7 @@ func (v simple) GetVersions() (map[string][]FileVersion, error) { func (v simple) Restore(filepath string, versionTime time.Time) error { return restoreFile(v.copyRangeMethod, v.versionsFs, v.folderFs, filepath, versionTime, TagFilename) } + +func (v simple) Clean(_ context.Context) error { + return nil +} diff --git a/lib/versioner/staggered.go b/lib/versioner/staggered.go index 41e102867..53b53a368 100644 --- a/lib/versioner/staggered.go +++ b/lib/versioner/staggered.go @@ -13,12 +13,8 @@ import ( "strconv" "time" - "github.com/thejerf/suture" - "github.com/syncthing/syncthing/lib/config" "github.com/syncthing/syncthing/lib/fs" - "github.com/syncthing/syncthing/lib/sync" - "github.com/syncthing/syncthing/lib/util" ) func init() { @@ -32,15 +28,10 @@ type interval struct { } type staggered struct { - suture.Service - cleanInterval int64 folderFs fs.Filesystem versionsFs fs.Filesystem interval [4]interval copyRangeMethod fs.CopyRangeMethod - mutex sync.Mutex - - testCleanDone chan struct{} } func newStaggered(cfg config.FolderConfiguration) Versioner { @@ -49,19 +40,14 @@ func newStaggered(cfg config.FolderConfiguration) Versioner { if err != nil { maxAge = 31536000 // Default: ~1 year } - cleanInterval, err := strconv.ParseInt(params["cleanInterval"], 10, 0) - if err != nil { - cleanInterval = 3600 // Default: clean once per hour - } // Backwards compatibility params["fsPath"] = params["versionsPath"] versionsFs := versionerFsFromFolderCfg(cfg) s := &staggered{ - cleanInterval: cleanInterval, - folderFs: cfg.Filesystem(), - versionsFs: versionsFs, + folderFs: cfg.Filesystem(), + versionsFs: versionsFs, interval: [4]interval{ {30, 60 * 60}, // first hour -> 30 sec between versions {60 * 60, 24 * 60 * 60}, // next day -> 1 h between versions @@ -69,41 +55,18 @@ func newStaggered(cfg config.FolderConfiguration) Versioner { {7 * 24 * 60 * 60, maxAge}, // next year -> 1 week between versions }, copyRangeMethod: cfg.CopyRangeMethod, - mutex: sync.NewMutex(), } - s.Service = util.AsService(s.serve, s.String()) l.Debugf("instantiated %#v", s) return s } -func (v *staggered) serve(ctx context.Context) { - v.clean() - if v.testCleanDone != nil { - close(v.testCleanDone) - } - - tck := time.NewTicker(time.Duration(v.cleanInterval) * time.Second) - defer tck.Stop() - for { - select { - case <-tck.C: - v.clean() - case <-ctx.Done(): - return - } - } -} - -func (v *staggered) clean() { - l.Debugln("Versioner clean: Waiting for lock on", v.versionsFs) - v.mutex.Lock() - defer v.mutex.Unlock() +func (v *staggered) Clean(ctx context.Context) error { l.Debugln("Versioner clean: Cleaning", v.versionsFs) if _, err := v.versionsFs.Stat("."); fs.IsNotExist(err) { // There is no need to clean a nonexistent dir. - return + return nil } versionsPerFile := make(map[string][]string) @@ -113,6 +76,11 @@ func (v *staggered) clean() { if err != nil { return err } + select { + case <-ctx.Done(): + return ctx.Err() + default: + } if f.IsDir() && !f.IsSymlink() { dirTracker.addDir(path) @@ -134,16 +102,22 @@ func (v *staggered) clean() { if err := v.versionsFs.Walk(".", walkFn); err != nil { l.Warnln("Versioner: error scanning versions dir", err) - return + return err } for _, versionList := range versionsPerFile { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } v.expire(versionList) } dirTracker.deleteEmptyDirs(v.versionsFs) l.Debugln("Cleaner: Finished cleaning", v.versionsFs) + return nil } func (v *staggered) expire(versions []string) { @@ -216,10 +190,6 @@ func (v *staggered) toRemove(versions []string, now time.Time) []string { // Archive moves the named file away to a version archive. If this function // returns nil, the named file does not exist any more (has been archived). func (v *staggered) Archive(filePath string) error { - l.Debugln("Waiting for lock on ", v.versionsFs) - v.mutex.Lock() - defer v.mutex.Unlock() - if err := archiveFile(v.copyRangeMethod, v.folderFs, v.versionsFs, filePath, TagFilename); err != nil { return err } diff --git a/lib/versioner/trashcan.go b/lib/versioner/trashcan.go index ca059fd22..b56fc6b50 100644 --- a/lib/versioner/trashcan.go +++ b/lib/versioner/trashcan.go @@ -12,11 +12,8 @@ import ( "strconv" "time" - "github.com/thejerf/suture" - "github.com/syncthing/syncthing/lib/config" "github.com/syncthing/syncthing/lib/fs" - "github.com/syncthing/syncthing/lib/util" ) func init() { @@ -25,7 +22,6 @@ func init() { } type trashcan struct { - suture.Service folderFs fs.Filesystem versionsFs fs.Filesystem cleanoutDays int @@ -42,7 +38,6 @@ func newTrashcan(cfg config.FolderConfiguration) Versioner { cleanoutDays: cleanoutDays, copyRangeMethod: cfg.CopyRangeMethod, } - s.Service = util.AsService(s.serve, s.String()) l.Debugf("instantiated %#v", s) return s @@ -56,37 +51,16 @@ func (t *trashcan) Archive(filePath string) error { }) } -func (t *trashcan) serve(ctx context.Context) { - l.Debugln(t, "starting") - defer l.Debugln(t, "stopping") - - // Do the first cleanup one minute after startup. - timer := time.NewTimer(time.Minute) - defer timer.Stop() - - for { - select { - case <-ctx.Done(): - return - - case <-timer.C: - if t.cleanoutDays > 0 { - if err := t.cleanoutArchive(); err != nil { - l.Infoln("Cleaning trashcan:", err) - } - } - - // Cleanups once a day should be enough. - timer.Reset(24 * time.Hour) - } - } -} - func (t *trashcan) String() string { return fmt.Sprintf("trashcan@%p", t) } -func (t *trashcan) cleanoutArchive() error { +func (t *trashcan) Clean(ctx context.Context) error { + if t.cleanoutDays <= 0 { + // no cleanout requested + return nil + } + if _, err := t.versionsFs.Lstat("."); fs.IsNotExist(err) { return nil } @@ -98,6 +72,11 @@ func (t *trashcan) cleanoutArchive() error { if err != nil { return err } + select { + case <-ctx.Done(): + return ctx.Err() + default: + } if info.IsDir() && !info.IsSymlink() { dirTracker.addDir(path) diff --git a/lib/versioner/trashcan_test.go b/lib/versioner/trashcan_test.go index 5bf3264db..94143a4b4 100644 --- a/lib/versioner/trashcan_test.go +++ b/lib/versioner/trashcan_test.go @@ -7,13 +7,14 @@ package versioner import ( - "github.com/syncthing/syncthing/lib/config" + "context" "io/ioutil" "os" "path/filepath" "testing" "time" + "github.com/syncthing/syncthing/lib/config" "github.com/syncthing/syncthing/lib/fs" ) @@ -65,7 +66,7 @@ func TestTrashcanCleanout(t *testing.T) { } versioner := newTrashcan(cfg).(*trashcan) - if err := versioner.cleanoutArchive(); err != nil { + if err := versioner.Clean(context.Background()); err != nil { t.Fatal(err) } diff --git a/lib/versioner/versioner.go b/lib/versioner/versioner.go index eabd8f07d..372488294 100644 --- a/lib/versioner/versioner.go +++ b/lib/versioner/versioner.go @@ -9,6 +9,7 @@ package versioner import ( + "context" "errors" "fmt" "time" @@ -20,6 +21,7 @@ type Versioner interface { Archive(filePath string) error GetVersions() (map[string][]FileVersion, error) Restore(filePath string, versionTime time.Time) error + Clean(context.Context) error } type FileVersion struct {