mv internal lib
This commit is contained in:
1
lib/versioner/.gitignore
vendored
Normal file
1
lib/versioner/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
testdata
|
||||
19
lib/versioner/debug.go
Normal file
19
lib/versioner/debug.go
Normal file
@@ -0,0 +1,19 @@
|
||||
// Copyright (C) 2014 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package versioner
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/calmh/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
debug = strings.Contains(os.Getenv("STTRACE"), "versioner") || os.Getenv("STTRACE") == "all"
|
||||
l = logger.DefaultLogger
|
||||
)
|
||||
89
lib/versioner/external.go
Normal file
89
lib/versioner/external.go
Normal file
@@ -0,0 +1,89 @@
|
||||
// Copyright (C) 2015 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package versioner
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Register the constructor for this type of versioner with the name "external"
|
||||
Factories["external"] = NewExternal
|
||||
}
|
||||
|
||||
type External struct {
|
||||
command string
|
||||
folderPath string
|
||||
}
|
||||
|
||||
func NewExternal(folderID, folderPath string, params map[string]string) Versioner {
|
||||
command := params["command"]
|
||||
|
||||
s := External{
|
||||
command: command,
|
||||
folderPath: folderPath,
|
||||
}
|
||||
|
||||
if debug {
|
||||
l.Debugf("instantiated %#v", s)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// 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 External) Archive(filePath string) error {
|
||||
_, err := osutil.Lstat(filePath)
|
||||
if os.IsNotExist(err) {
|
||||
if debug {
|
||||
l.Debugln("not archiving nonexistent file", filePath)
|
||||
}
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if debug {
|
||||
l.Debugln("archiving", filePath)
|
||||
}
|
||||
|
||||
inFolderPath, err := filepath.Rel(v.folderPath, filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if v.command == "" {
|
||||
return errors.New("Versioner: command is empty, please enter a valid command")
|
||||
}
|
||||
|
||||
cmd := exec.Command(v.command, v.folderPath, inFolderPath)
|
||||
env := os.Environ()
|
||||
// filter STGUIAUTH and STGUIAPIKEY from environment variables
|
||||
filteredEnv := []string{}
|
||||
for _, x := range env {
|
||||
if !strings.HasPrefix(x, "STGUIAUTH=") && !strings.HasPrefix(x, "STGUIAPIKEY=") {
|
||||
filteredEnv = append(filteredEnv, x)
|
||||
}
|
||||
}
|
||||
cmd.Env = filteredEnv
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// return error if the file was not removed
|
||||
if _, err = osutil.Lstat(filePath); os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return errors.New("Versioner: file was not removed by external script")
|
||||
}
|
||||
128
lib/versioner/simple.go
Normal file
128
lib/versioner/simple.go
Normal file
@@ -0,0 +1,128 @@
|
||||
// Copyright (C) 2014 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package versioner
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Register the constructor for this type of versioner with the name "simple"
|
||||
Factories["simple"] = NewSimple
|
||||
}
|
||||
|
||||
type Simple struct {
|
||||
keep int
|
||||
folderPath string
|
||||
}
|
||||
|
||||
func NewSimple(folderID, folderPath string, params map[string]string) Versioner {
|
||||
keep, err := strconv.Atoi(params["keep"])
|
||||
if err != nil {
|
||||
keep = 5 // A reasonable default
|
||||
}
|
||||
|
||||
s := Simple{
|
||||
keep: keep,
|
||||
folderPath: folderPath,
|
||||
}
|
||||
|
||||
if debug {
|
||||
l.Debugf("instantiated %#v", s)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// 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 Simple) Archive(filePath string) error {
|
||||
fileInfo, err := osutil.Lstat(filePath)
|
||||
if os.IsNotExist(err) {
|
||||
if debug {
|
||||
l.Debugln("not archiving nonexistent file", filePath)
|
||||
}
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
versionsDir := filepath.Join(v.folderPath, ".stversions")
|
||||
_, err = os.Stat(versionsDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if debug {
|
||||
l.Debugln("creating versions dir", versionsDir)
|
||||
}
|
||||
osutil.MkdirAll(versionsDir, 0755)
|
||||
osutil.HideFile(versionsDir)
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if debug {
|
||||
l.Debugln("archiving", filePath)
|
||||
}
|
||||
|
||||
file := filepath.Base(filePath)
|
||||
inFolderPath, err := filepath.Rel(v.folderPath, filepath.Dir(filePath))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dir := filepath.Join(versionsDir, inFolderPath)
|
||||
err = osutil.MkdirAll(dir, 0755)
|
||||
if err != nil && !os.IsExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
ver := taggedFilename(file, fileInfo.ModTime().Format(TimeFormat))
|
||||
dst := filepath.Join(dir, ver)
|
||||
if debug {
|
||||
l.Debugln("moving to", dst)
|
||||
}
|
||||
err = osutil.Rename(filePath, dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Glob according to the new file~timestamp.ext pattern.
|
||||
newVersions, err := osutil.Glob(filepath.Join(dir, taggedFilename(file, TimeGlob)))
|
||||
if err != nil {
|
||||
l.Warnln("globbing:", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Also according to the old file.ext~timestamp pattern.
|
||||
oldVersions, err := osutil.Glob(filepath.Join(dir, file+"~"+TimeGlob))
|
||||
if err != nil {
|
||||
l.Warnln("globbing:", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Use all the found filenames. "~" sorts after "." so all old pattern
|
||||
// files will be deleted before any new, which is as it should be.
|
||||
versions := uniqueSortedStrings(append(oldVersions, newVersions...))
|
||||
|
||||
if len(versions) > v.keep {
|
||||
for _, toRemove := range versions[:len(versions)-v.keep] {
|
||||
if debug {
|
||||
l.Debugln("cleaning out", toRemove)
|
||||
}
|
||||
err = os.Remove(toRemove)
|
||||
if err != nil {
|
||||
l.Warnln("removing old version:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
312
lib/versioner/staggered.go
Normal file
312
lib/versioner/staggered.go
Normal file
@@ -0,0 +1,312 @@
|
||||
// Copyright (C) 2014 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package versioner
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Register the constructor for this type of versioner with the name "staggered"
|
||||
Factories["staggered"] = NewStaggered
|
||||
}
|
||||
|
||||
type Interval struct {
|
||||
step int64
|
||||
end int64
|
||||
}
|
||||
|
||||
type Staggered struct {
|
||||
versionsPath string
|
||||
cleanInterval int64
|
||||
folderPath string
|
||||
interval [4]Interval
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
func NewStaggered(folderID, folderPath string, params map[string]string) Versioner {
|
||||
maxAge, err := strconv.ParseInt(params["maxAge"], 10, 0)
|
||||
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
|
||||
}
|
||||
|
||||
// Use custom path if set, otherwise .stversions in folderPath
|
||||
var versionsDir string
|
||||
if params["versionsPath"] == "" {
|
||||
if debug {
|
||||
l.Debugln("using default dir .stversions")
|
||||
}
|
||||
versionsDir = filepath.Join(folderPath, ".stversions")
|
||||
} else {
|
||||
if debug {
|
||||
l.Debugln("using dir", params["versionsPath"])
|
||||
}
|
||||
versionsDir = params["versionsPath"]
|
||||
}
|
||||
|
||||
s := Staggered{
|
||||
versionsPath: versionsDir,
|
||||
cleanInterval: cleanInterval,
|
||||
folderPath: folderPath,
|
||||
interval: [4]Interval{
|
||||
{30, 3600}, // first hour -> 30 sec between versions
|
||||
{3600, 86400}, // next day -> 1 h between versions
|
||||
{86400, 592000}, // next 30 days -> 1 day between versions
|
||||
{604800, maxAge}, // next year -> 1 week between versions
|
||||
},
|
||||
mutex: sync.NewMutex(),
|
||||
}
|
||||
|
||||
if debug {
|
||||
l.Debugf("instantiated %#v", s)
|
||||
}
|
||||
|
||||
go func() {
|
||||
s.clean()
|
||||
for _ = range time.Tick(time.Duration(cleanInterval) * time.Second) {
|
||||
s.clean()
|
||||
}
|
||||
}()
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (v Staggered) clean() {
|
||||
if debug {
|
||||
l.Debugln("Versioner clean: Waiting for lock on", v.versionsPath)
|
||||
}
|
||||
v.mutex.Lock()
|
||||
defer v.mutex.Unlock()
|
||||
if debug {
|
||||
l.Debugln("Versioner clean: Cleaning", v.versionsPath)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(v.versionsPath); os.IsNotExist(err) {
|
||||
// There is no need to clean a nonexistent dir.
|
||||
return
|
||||
}
|
||||
|
||||
versionsPerFile := make(map[string][]string)
|
||||
filesPerDir := make(map[string]int)
|
||||
|
||||
err := filepath.Walk(v.versionsPath, func(path string, f os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if f.Mode().IsDir() && f.Mode()&os.ModeSymlink == 0 {
|
||||
filesPerDir[path] = 0
|
||||
if path != v.versionsPath {
|
||||
dir := filepath.Dir(path)
|
||||
filesPerDir[dir]++
|
||||
}
|
||||
} else {
|
||||
// Regular file, or possibly a symlink.
|
||||
ext := filepath.Ext(path)
|
||||
versionTag := filenameTag(path)
|
||||
dir := filepath.Dir(path)
|
||||
withoutExt := path[:len(path)-len(ext)-len(versionTag)-1]
|
||||
name := withoutExt + ext
|
||||
|
||||
filesPerDir[dir]++
|
||||
versionsPerFile[name] = append(versionsPerFile[name], path)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
l.Warnln("Versioner: error scanning versions dir", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, versionList := range versionsPerFile {
|
||||
// List from filepath.Walk is sorted
|
||||
v.expire(versionList)
|
||||
}
|
||||
|
||||
for path, numFiles := range filesPerDir {
|
||||
if numFiles > 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if path == v.versionsPath {
|
||||
if debug {
|
||||
l.Debugln("Cleaner: versions dir is empty, don't delete", path)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if debug {
|
||||
l.Debugln("Cleaner: deleting empty directory", path)
|
||||
}
|
||||
err = os.Remove(path)
|
||||
if err != nil {
|
||||
l.Warnln("Versioner: can't remove directory", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
if debug {
|
||||
l.Debugln("Cleaner: Finished cleaning", v.versionsPath)
|
||||
}
|
||||
}
|
||||
|
||||
func (v Staggered) expire(versions []string) {
|
||||
if debug {
|
||||
l.Debugln("Versioner: Expiring versions", versions)
|
||||
}
|
||||
var prevAge int64
|
||||
firstFile := true
|
||||
for _, file := range versions {
|
||||
fi, err := osutil.Lstat(file)
|
||||
if err != nil {
|
||||
l.Warnln("versioner:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if fi.IsDir() {
|
||||
l.Infof("non-file %q is named like a file version", file)
|
||||
continue
|
||||
}
|
||||
|
||||
versionTime, err := time.Parse(TimeFormat, filenameTag(file))
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugf("Versioner: file name %q is invalid: %v", file, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
age := int64(time.Since(versionTime).Seconds())
|
||||
|
||||
// If the file is older than the max age of the last interval, remove it
|
||||
if lastIntv := v.interval[len(v.interval)-1]; lastIntv.end > 0 && age > lastIntv.end {
|
||||
if debug {
|
||||
l.Debugln("Versioner: File over maximum age -> delete ", file)
|
||||
}
|
||||
err = os.Remove(file)
|
||||
if err != nil {
|
||||
l.Warnf("Versioner: can't remove %q: %v", file, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// If it's the first (oldest) file in the list we can skip the interval checks
|
||||
if firstFile {
|
||||
prevAge = age
|
||||
firstFile = false
|
||||
continue
|
||||
}
|
||||
|
||||
// Find the interval the file fits in
|
||||
var usedInterval Interval
|
||||
for _, usedInterval = range v.interval {
|
||||
if age < usedInterval.end {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if prevAge-age < usedInterval.step {
|
||||
if debug {
|
||||
l.Debugln("too many files in step -> delete", file)
|
||||
}
|
||||
err = os.Remove(file)
|
||||
if err != nil {
|
||||
l.Warnf("Versioner: can't remove %q: %v", file, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
prevAge = age
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if debug {
|
||||
l.Debugln("Waiting for lock on ", v.versionsPath)
|
||||
}
|
||||
v.mutex.Lock()
|
||||
defer v.mutex.Unlock()
|
||||
|
||||
_, err := osutil.Lstat(filePath)
|
||||
if os.IsNotExist(err) {
|
||||
if debug {
|
||||
l.Debugln("not archiving nonexistent file", filePath)
|
||||
}
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := os.Stat(v.versionsPath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if debug {
|
||||
l.Debugln("creating versions dir", v.versionsPath)
|
||||
}
|
||||
osutil.MkdirAll(v.versionsPath, 0755)
|
||||
osutil.HideFile(v.versionsPath)
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if debug {
|
||||
l.Debugln("archiving", filePath)
|
||||
}
|
||||
|
||||
file := filepath.Base(filePath)
|
||||
inFolderPath, err := filepath.Rel(v.folderPath, filepath.Dir(filePath))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dir := filepath.Join(v.versionsPath, inFolderPath)
|
||||
err = osutil.MkdirAll(dir, 0755)
|
||||
if err != nil && !os.IsExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
ver := taggedFilename(file, time.Now().Format(TimeFormat))
|
||||
dst := filepath.Join(dir, ver)
|
||||
if debug {
|
||||
l.Debugln("moving to", dst)
|
||||
}
|
||||
err = osutil.Rename(filePath, dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Glob according to the new file~timestamp.ext pattern.
|
||||
newVersions, err := osutil.Glob(filepath.Join(dir, taggedFilename(file, TimeGlob)))
|
||||
if err != nil {
|
||||
l.Warnln("globbing:", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Also according to the old file.ext~timestamp pattern.
|
||||
oldVersions, err := osutil.Glob(filepath.Join(dir, file+"~"+TimeGlob))
|
||||
if err != nil {
|
||||
l.Warnln("globbing:", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Use all the found filenames.
|
||||
versions := append(oldVersions, newVersions...)
|
||||
v.expire(uniqueSortedStrings(versions))
|
||||
|
||||
return nil
|
||||
}
|
||||
187
lib/versioner/trashcan.go
Normal file
187
lib/versioner/trashcan.go
Normal file
@@ -0,0 +1,187 @@
|
||||
// Copyright (C) 2015 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package versioner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Register the constructor for this type of versioner
|
||||
Factories["trashcan"] = NewTrashcan
|
||||
}
|
||||
|
||||
type Trashcan struct {
|
||||
folderPath string
|
||||
cleanoutDays int
|
||||
stop chan struct{}
|
||||
}
|
||||
|
||||
func NewTrashcan(folderID, folderPath string, params map[string]string) Versioner {
|
||||
cleanoutDays, _ := strconv.Atoi(params["cleanoutDays"])
|
||||
// On error we default to 0, "do not clean out the trash can"
|
||||
|
||||
s := &Trashcan{
|
||||
folderPath: folderPath,
|
||||
cleanoutDays: cleanoutDays,
|
||||
stop: make(chan struct{}),
|
||||
}
|
||||
|
||||
if debug {
|
||||
l.Debugf("instantiated %#v", s)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// 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 (t *Trashcan) Archive(filePath string) error {
|
||||
_, err := osutil.Lstat(filePath)
|
||||
if os.IsNotExist(err) {
|
||||
if debug {
|
||||
l.Debugln("not archiving nonexistent file", filePath)
|
||||
}
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
versionsDir := filepath.Join(t.folderPath, ".stversions")
|
||||
if _, err := os.Stat(versionsDir); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
if debug {
|
||||
l.Debugln("creating versions dir", versionsDir)
|
||||
}
|
||||
if err := osutil.MkdirAll(versionsDir, 0777); err != nil {
|
||||
return err
|
||||
}
|
||||
osutil.HideFile(versionsDir)
|
||||
}
|
||||
|
||||
if debug {
|
||||
l.Debugln("archiving", filePath)
|
||||
}
|
||||
|
||||
relativePath, err := filepath.Rel(t.folderPath, filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
archivedPath := filepath.Join(versionsDir, relativePath)
|
||||
if err := osutil.MkdirAll(filepath.Dir(archivedPath), 0777); err != nil && !os.IsExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
if debug {
|
||||
l.Debugln("moving to", archivedPath)
|
||||
}
|
||||
|
||||
if err := osutil.Rename(filePath, archivedPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the mtime to the time the file was deleted. This is used by the
|
||||
// cleanout routine. If this fails things won't work optimally but there's
|
||||
// not much we can do about it so we ignore the error.
|
||||
os.Chtimes(archivedPath, time.Now(), time.Now())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Trashcan) Serve() {
|
||||
if debug {
|
||||
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 <-t.stop:
|
||||
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) Stop() {
|
||||
close(t.stop)
|
||||
}
|
||||
|
||||
func (t *Trashcan) String() string {
|
||||
return fmt.Sprintf("trashcan@%p", t)
|
||||
}
|
||||
|
||||
func (t *Trashcan) cleanoutArchive() error {
|
||||
versionsDir := filepath.Join(t.folderPath, ".stversions")
|
||||
if _, err := osutil.Lstat(versionsDir); os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
|
||||
cutoff := time.Now().Add(time.Duration(-24*t.cleanoutDays) * time.Hour)
|
||||
currentDir := ""
|
||||
filesInDir := 0
|
||||
walkFn := func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
// We have entered a new directory. Lets check if the previous
|
||||
// directory was empty and try to remove it. We ignore failure for
|
||||
// the time being.
|
||||
if currentDir != "" && filesInDir == 0 {
|
||||
osutil.Remove(currentDir)
|
||||
}
|
||||
currentDir = path
|
||||
filesInDir = 0
|
||||
return nil
|
||||
}
|
||||
|
||||
if info.ModTime().Before(cutoff) {
|
||||
// The file is too old; remove it.
|
||||
osutil.Remove(path)
|
||||
} else {
|
||||
// Keep this file, and remember it so we don't unnecessarily try
|
||||
// to remove this directory.
|
||||
filesInDir++
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := filepath.Walk(versionsDir, walkFn); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The last directory seen by the walkFn may not have been removed as it
|
||||
// should be.
|
||||
if currentDir != "" && filesInDir == 0 {
|
||||
osutil.Remove(currentDir)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
69
lib/versioner/trashcan_test.go
Normal file
69
lib/versioner/trashcan_test.go
Normal file
@@ -0,0 +1,69 @@
|
||||
// Copyright (C) 2015 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package versioner
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestTrashcanCleanout(t *testing.T) {
|
||||
// Verify that files older than the cutoff are removed, that files newer
|
||||
// than the cutoff are *not* removed, and that empty directories are
|
||||
// removed (best effort).
|
||||
|
||||
var testcases = []struct {
|
||||
file string
|
||||
shouldRemove bool
|
||||
}{
|
||||
{"testdata/.stversions/file1", false},
|
||||
{"testdata/.stversions/file2", true},
|
||||
{"testdata/.stversions/keep1/file1", false},
|
||||
{"testdata/.stversions/keep1/file2", false},
|
||||
{"testdata/.stversions/keep2/file1", false},
|
||||
{"testdata/.stversions/keep2/file2", true},
|
||||
{"testdata/.stversions/remove/file1", true},
|
||||
{"testdata/.stversions/remove/file2", true},
|
||||
}
|
||||
|
||||
os.RemoveAll("testdata")
|
||||
defer os.RemoveAll("testdata")
|
||||
|
||||
oldTime := time.Now().Add(-8 * 24 * time.Hour)
|
||||
for _, tc := range testcases {
|
||||
os.MkdirAll(filepath.Dir(tc.file), 0777)
|
||||
if err := ioutil.WriteFile(tc.file, []byte("data"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if tc.shouldRemove {
|
||||
if err := os.Chtimes(tc.file, oldTime, oldTime); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
versioner := NewTrashcan("default", "testdata", map[string]string{"cleanoutDays": "7"}).(*Trashcan)
|
||||
if err := versioner.cleanoutArchive(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
_, err := os.Lstat(tc.file)
|
||||
if tc.shouldRemove && !os.IsNotExist(err) {
|
||||
t.Error(tc.file, "should have been removed")
|
||||
} else if !tc.shouldRemove && err != nil {
|
||||
t.Error(tc.file, "should not have been removed")
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Lstat("testdata/.stversions/remove"); !os.IsNotExist(err) {
|
||||
t.Error("empty directory should have been removed")
|
||||
}
|
||||
}
|
||||
48
lib/versioner/util.go
Normal file
48
lib/versioner/util.go
Normal file
@@ -0,0 +1,48 @@
|
||||
// Copyright (C) 2014 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package versioner
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Inserts ~tag just before the extension of the filename.
|
||||
func taggedFilename(name, tag string) string {
|
||||
dir, file := filepath.Dir(name), filepath.Base(name)
|
||||
ext := filepath.Ext(file)
|
||||
withoutExt := file[:len(file)-len(ext)]
|
||||
return filepath.Join(dir, withoutExt+"~"+tag+ext)
|
||||
}
|
||||
|
||||
var tagExp = regexp.MustCompile(`.*~([^~.]+)(?:\.[^.]+)?$`)
|
||||
|
||||
// Returns the tag from a filename, whether at the end or middle.
|
||||
func filenameTag(path string) string {
|
||||
match := tagExp.FindStringSubmatch(path)
|
||||
// match is []string{"whole match", "submatch"} when successful
|
||||
|
||||
if len(match) != 2 {
|
||||
return ""
|
||||
}
|
||||
return match[1]
|
||||
}
|
||||
|
||||
func uniqueSortedStrings(strings []string) []string {
|
||||
seen := make(map[string]struct{}, len(strings))
|
||||
unique := make([]string, 0, len(strings))
|
||||
for _, str := range strings {
|
||||
_, ok := seen[str]
|
||||
if !ok {
|
||||
seen[str] = struct{}{}
|
||||
unique = append(unique, str)
|
||||
}
|
||||
}
|
||||
sort.Strings(unique)
|
||||
return unique
|
||||
}
|
||||
20
lib/versioner/versioner.go
Normal file
20
lib/versioner/versioner.go
Normal file
@@ -0,0 +1,20 @@
|
||||
// Copyright (C) 2014 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
// Package versioner implements common interfaces for file versioning and a
|
||||
// simple default versioning scheme.
|
||||
package versioner
|
||||
|
||||
type Versioner interface {
|
||||
Archive(filePath string) error
|
||||
}
|
||||
|
||||
var Factories = map[string]func(folderID string, folderDir string, params map[string]string) Versioner{}
|
||||
|
||||
const (
|
||||
TimeFormat = "20060102-150405"
|
||||
TimeGlob = "[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]" // glob pattern matching TimeFormat
|
||||
)
|
||||
90
lib/versioner/versioner_test.go
Normal file
90
lib/versioner/versioner_test.go
Normal file
@@ -0,0 +1,90 @@
|
||||
// Copyright (C) 2014 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package versioner
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestTaggedFilename(t *testing.T) {
|
||||
cases := [][3]string{
|
||||
{filepath.Join("foo", "bar.baz"), "tag", filepath.Join("foo", "bar~tag.baz")},
|
||||
{"bar.baz", "tag", "bar~tag.baz"},
|
||||
{"bar", "tag", "bar~tag"},
|
||||
{"~$ufheft2.docx", "20140612-200554", "~$ufheft2~20140612-200554.docx"},
|
||||
{"alle~4.mgz", "20141106-094415", "alle~4~20141106-094415.mgz"},
|
||||
|
||||
// Parsing test only
|
||||
{"", "tag-only", "foo/bar.baz~tag-only"},
|
||||
{"", "tag-only", "bar.baz~tag-only"},
|
||||
{"", "20140612-200554", "~$ufheft2.docx~20140612-200554"},
|
||||
{"", "20141106-094415", "alle~4.mgz~20141106-094415"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
if tc[0] != "" {
|
||||
// Test tagger
|
||||
tf := taggedFilename(tc[0], tc[1])
|
||||
if tf != tc[2] {
|
||||
t.Errorf("%s != %s", tf, tc[2])
|
||||
}
|
||||
}
|
||||
|
||||
// Test parser
|
||||
tag := filenameTag(tc[2])
|
||||
if tag != tc[1] {
|
||||
t.Errorf("%s != %s", tag, tc[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSimpleVersioningVersionCount(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Test takes some time, skipping.")
|
||||
}
|
||||
|
||||
dir, err := ioutil.TempDir("", "")
|
||||
defer os.RemoveAll(dir)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
v := NewSimple("", dir, map[string]string{"keep": "2"})
|
||||
versionDir := filepath.Join(dir, ".stversions")
|
||||
|
||||
path := filepath.Join(dir, "test")
|
||||
|
||||
for i := 1; i <= 3; i++ {
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
f.Close()
|
||||
v.Archive(path)
|
||||
|
||||
d, err := os.Open(versionDir)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
n, err := d.Readdirnames(-1)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if float64(len(n)) != math.Min(float64(i), 2) {
|
||||
t.Error("Wrong count")
|
||||
}
|
||||
d.Close()
|
||||
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user