summaryrefslogtreecommitdiffstats
path: root/lib/model/model.go
diff options
context:
space:
mode:
Diffstat (limited to 'lib/model/model.go')
-rw-r--r--lib/model/model.go158
1 files changed, 143 insertions, 15 deletions
diff --git a/lib/model/model.go b/lib/model/model.go
index 5ecaeb6485..5527ca664f 100644
--- a/lib/model/model.go
+++ b/lib/model/model.go
@@ -38,6 +38,16 @@ import (
"github.com/thejerf/suture"
)
+var locationLocal *time.Location
+
+func init() {
+ var err error
+ locationLocal, err = time.LoadLocation("Local")
+ if err != nil {
+ panic(err.Error())
+ }
+}
+
// How many files to send in each Index/IndexUpdate message.
const (
maxBatchSizeBytes = 250 * 1024 // Aim for making index messages no larger than 250 KiB (uncompressed)
@@ -232,21 +242,13 @@ func (m *Model) startFolderLocked(folder string) config.FolderType {
}
}
- var ver versioner.Versioner
- if len(cfg.Versioning.Type) > 0 {
- versionerFactory, ok := versioner.Factories[cfg.Versioning.Type]
- if !ok {
- l.Fatalf("Requested versioning type %q that does not exist", cfg.Versioning.Type)
- }
-
- ver = versionerFactory(folder, cfg.Filesystem(), cfg.Versioning.Params)
- 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)
- }
+ ver := cfg.Versioner()
+ 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)
}
ffs := fs.MtimeFS()
@@ -2376,6 +2378,132 @@ func (m *Model) GlobalDirectoryTree(folder, prefix string, levels int, dirsonly
return output
}
+func (m *Model) GetFolderVersions(folder string) (map[string][]versioner.FileVersion, error) {
+ fcfg, ok := m.cfg.Folder(folder)
+ if !ok {
+ return nil, errFolderMissing
+ }
+
+ files := make(map[string][]versioner.FileVersion)
+
+ filesystem := fcfg.Filesystem()
+ err := filesystem.Walk(".stversions", func(path string, f fs.FileInfo, err error) error {
+ // Skip root (which is ok to be a symlink)
+ if path == ".stversions" {
+ return nil
+ }
+
+ // Ignore symlinks
+ if f.IsSymlink() {
+ return fs.SkipDir
+ }
+
+ // No records for directories
+ if f.IsDir() {
+ return nil
+ }
+
+ // Strip .stversions prefix.
+ path = strings.TrimPrefix(path, ".stversions"+string(fs.PathSeparator))
+
+ name, tag := versioner.UntagFilename(path)
+ // Something invalid
+ if name == "" || tag == "" {
+ return nil
+ }
+
+ name = osutil.NormalizedFilename(name)
+
+ versionTime, err := time.ParseInLocation(versioner.TimeFormat, tag, locationLocal)
+ if err != nil {
+ return nil
+ }
+
+ files[name] = append(files[name], versioner.FileVersion{
+ VersionTime: versionTime.Truncate(time.Second),
+ ModTime: f.ModTime().Truncate(time.Second),
+ Size: f.Size(),
+ })
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ return files, nil
+}
+
+func (m *Model) RestoreFolderVersions(folder string, versions map[string]time.Time) (map[string]string, error) {
+ fcfg, ok := m.cfg.Folder(folder)
+ if !ok {
+ return nil, errFolderMissing
+ }
+
+ filesystem := fcfg.Filesystem()
+ ver := fcfg.Versioner()
+
+ restore := make(map[string]string)
+ errors := make(map[string]string)
+
+ // Validation
+ for file, version := range versions {
+ file = osutil.NativeFilename(file)
+ tag := version.In(locationLocal).Truncate(time.Second).Format(versioner.TimeFormat)
+ versionedTaggedFilename := filepath.Join(".stversions", versioner.TagFilename(file, tag))
+ // Check that the thing we've been asked to restore is actually a file
+ // and that it exists.
+ if info, err := filesystem.Lstat(versionedTaggedFilename); err != nil {
+ errors[file] = err.Error()
+ continue
+ } else if !info.IsRegular() {
+ errors[file] = "not a file"
+ continue
+ }
+
+ // Check that the target location of where we are supposed to restore
+ // either does not exist, or is actually a file.
+ if info, err := filesystem.Lstat(file); err == nil && !info.IsRegular() {
+ errors[file] = "cannot replace a non-file"
+ continue
+ } else if err != nil && !fs.IsNotExist(err) {
+ errors[file] = err.Error()
+ continue
+ }
+
+ restore[file] = versionedTaggedFilename
+ }
+
+ // Execution
+ var err error
+ for target, source := range restore {
+ err = nil
+ if _, serr := filesystem.Lstat(target); serr == nil {
+ if ver != nil {
+ err = osutil.InWritableDir(ver.Archive, filesystem, target)
+ } else {
+ err = osutil.InWritableDir(filesystem.Remove, filesystem, target)
+ }
+ }
+
+ filesystem.MkdirAll(filepath.Dir(target), 0755)
+ if err == nil {
+ err = osutil.Copy(filesystem, source, target)
+ }
+
+ if err != nil {
+ errors[target] = err.Error()
+ continue
+ }
+ }
+
+ // Trigger scan
+ if !fcfg.FSWatcherEnabled {
+ m.ScanFolder(folder)
+ }
+
+ return errors, nil
+}
+
func (m *Model) Availability(folder, file string, version protocol.Vector, block protocol.BlockInfo) []Availability {
// The slightly unusual locking sequence here is because we need to hold
// pmut for the duration (as the value returned from foldersFiles can