mirror of
https://github.com/octoleo/syncthing.git
synced 2025-01-05 08:02:13 +00:00
lib/versioner: Clean the versions dir of symlinks, not the full folder
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/4289
This commit is contained in:
parent
a3c17f8f81
commit
fa5c890ff6
@ -17,6 +17,8 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -29,7 +31,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
OldestHandledVersion = 10
|
OldestHandledVersion = 10
|
||||||
CurrentVersion = 20
|
CurrentVersion = 21
|
||||||
MaxRescanIntervalS = 365 * 24 * 60 * 60
|
MaxRescanIntervalS = 365 * 24 * 60 * 60
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -314,6 +316,9 @@ func (cfg *Configuration) clean() error {
|
|||||||
if cfg.Version == 19 {
|
if cfg.Version == 19 {
|
||||||
convertV19V20(cfg)
|
convertV19V20(cfg)
|
||||||
}
|
}
|
||||||
|
if cfg.Version == 20 {
|
||||||
|
convertV20V21(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
// Build a list of available devices
|
// Build a list of available devices
|
||||||
existingDevices := make(map[protocol.DeviceID]bool)
|
existingDevices := make(map[protocol.DeviceID]bool)
|
||||||
@ -363,6 +368,32 @@ func (cfg *Configuration) clean() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func convertV20V21(cfg *Configuration) {
|
||||||
|
for _, folder := range cfg.Folders {
|
||||||
|
switch folder.Versioning.Type {
|
||||||
|
case "simple", "trashcan":
|
||||||
|
// Clean out symlinks in the known place
|
||||||
|
cleanSymlinks(filepath.Join(folder.Path(), ".stversions"))
|
||||||
|
case "staggered":
|
||||||
|
versionDir := folder.Versioning.Params["versionsPath"]
|
||||||
|
if versionDir == "" {
|
||||||
|
// default place
|
||||||
|
cleanSymlinks(filepath.Join(folder.Path(), ".stversions"))
|
||||||
|
} else if filepath.IsAbs(versionDir) {
|
||||||
|
// absolute
|
||||||
|
cleanSymlinks(versionDir)
|
||||||
|
} else {
|
||||||
|
// relative to folder
|
||||||
|
cleanSymlinks(filepath.Join(folder.Path(), versionDir))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// there is also a symlink recovery step in Model.StartFolder()
|
||||||
|
|
||||||
|
cfg.Version = 21
|
||||||
|
}
|
||||||
|
|
||||||
func convertV19V20(cfg *Configuration) {
|
func convertV19V20(cfg *Configuration) {
|
||||||
cfg.Options.MinHomeDiskFree = Size{Value: cfg.Options.DeprecatedMinHomeDiskFreePct, Unit: "%"}
|
cfg.Options.MinHomeDiskFree = Size{Value: cfg.Options.DeprecatedMinHomeDiskFreePct, Unit: "%"}
|
||||||
cfg.Options.DeprecatedMinHomeDiskFreePct = 0
|
cfg.Options.DeprecatedMinHomeDiskFreePct = 0
|
||||||
@ -640,3 +671,23 @@ loop:
|
|||||||
}
|
}
|
||||||
return devices[0:count]
|
return devices[0:count]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cleanSymlinks(dir string) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
// We don't do symlinks on Windows. Additionally, there may
|
||||||
|
// be things that look like symlinks that are not, which we
|
||||||
|
// should leave alone. Deduplicated files, for example.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if info.Mode()&os.ModeSymlink != 0 {
|
||||||
|
l.Infoln("Removing incorrectly versioned symlink", path)
|
||||||
|
os.Remove(path)
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
15
lib/config/testdata/v21.xml
vendored
Normal file
15
lib/config/testdata/v21.xml
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<configuration version="21">
|
||||||
|
<folder id="test" path="testdata" type="readonly" ignorePerms="false" rescanIntervalS="600" autoNormalize="true">
|
||||||
|
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR"></device>
|
||||||
|
<device id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2"></device>
|
||||||
|
<minDiskFree unit="%">1</minDiskFree>
|
||||||
|
<maxConflicts>-1</maxConflicts>
|
||||||
|
<fsync>true</fsync>
|
||||||
|
</folder>
|
||||||
|
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR" name="node one" compression="metadata">
|
||||||
|
<address>tcp://a</address>
|
||||||
|
</device>
|
||||||
|
<device id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2" name="node two" compression="metadata">
|
||||||
|
<address>tcp://b</address>
|
||||||
|
</device>
|
||||||
|
</configuration>
|
@ -178,8 +178,13 @@ func (m *Model) StartDeadlockDetector(timeout time.Duration) {
|
|||||||
func (m *Model) StartFolder(folder string) {
|
func (m *Model) StartFolder(folder string) {
|
||||||
m.fmut.Lock()
|
m.fmut.Lock()
|
||||||
m.pmut.Lock()
|
m.pmut.Lock()
|
||||||
folderType := m.startFolderLocked(folder)
|
|
||||||
folderCfg := m.folderCfgs[folder]
|
folderCfg := m.folderCfgs[folder]
|
||||||
|
|
||||||
|
if folderCfg.Versioning.Type != "" && m.cfg.RawCopy().OriginalVersion < 21 {
|
||||||
|
m.attemptSymlinkRecovery(folderCfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
folderType := m.startFolderLocked(folder)
|
||||||
m.pmut.Unlock()
|
m.pmut.Unlock()
|
||||||
m.fmut.Unlock()
|
m.fmut.Unlock()
|
||||||
|
|
||||||
@ -2721,3 +2726,76 @@ func rootedJoinedPath(root, rel string) (string, error) {
|
|||||||
|
|
||||||
return joined, nil
|
return joined, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Model) attemptSymlinkRecovery(fcfg config.FolderConfiguration) {
|
||||||
|
fs, ok := m.folderFiles[fcfg.ID]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// The window during which we had a broken release out, roughly.
|
||||||
|
startDate := time.Date(2017, 8, 8, 6, 0, 0, 0, time.UTC)
|
||||||
|
endDate := time.Date(2017, 8, 8, 12, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
// Look through all our files looking for deleted symlinks.
|
||||||
|
fs.WithHave(protocol.LocalDeviceID, func(intf db.FileIntf) bool {
|
||||||
|
if !intf.IsSymlink() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
symlinkPath, err := rootedJoinedPath(fcfg.Path(), intf.FileName())
|
||||||
|
if err != nil {
|
||||||
|
// odd
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Lstat(symlinkPath); err == nil {
|
||||||
|
// The symlink exists. Our work here is done.
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fi := intf.(protocol.FileInfo)
|
||||||
|
if !fi.Deleted && fi.SymlinkTarget != "" {
|
||||||
|
// We haven't noticed the delete and put it into the
|
||||||
|
// index yet. Great! We can restore the symlink.
|
||||||
|
l.Infoln("Restoring incorrectly deleted symlink", symlinkPath)
|
||||||
|
os.Symlink(fi.SymlinkTarget, symlinkPath)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// It's deleted. Check if it was deleted in the bad window.
|
||||||
|
if fi.ModTime().Before(startDate) || !fi.ModTime().Before(endDate) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find an older index entry.
|
||||||
|
for deviceID := range m.cfg.Devices() {
|
||||||
|
olderFI, ok := fs.Get(deviceID, fi.Name)
|
||||||
|
if !ok {
|
||||||
|
// This device doesn't have it.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if olderFI.Deleted || !olderFI.IsSymlink() {
|
||||||
|
// The device has something deleted or not a
|
||||||
|
// symlink, doesn't help us.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if olderFI.Version.GreaterEqual(fi.Version) {
|
||||||
|
// The device has something newer. We should
|
||||||
|
// chill and let the puller handle it. No
|
||||||
|
// need to look further for this specific
|
||||||
|
// symlink.
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if olderFI.SymlinkTarget != "" {
|
||||||
|
// It has symlink data. Restore the symlink.
|
||||||
|
l.Infoln("Restoring incorrectly deleted symlink", symlinkPath)
|
||||||
|
os.Symlink(olderFI.SymlinkTarget, symlinkPath)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -141,6 +141,67 @@ func TestRequest(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSymlinkRecovery(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("symlinks not supported on Windows")
|
||||||
|
}
|
||||||
|
|
||||||
|
ldb := db.OpenMemory()
|
||||||
|
|
||||||
|
fs := db.NewFileSet("default", ldb)
|
||||||
|
|
||||||
|
// device1 has an old entry
|
||||||
|
fs.Update(device1, []protocol.FileInfo{
|
||||||
|
{
|
||||||
|
Name: "symlink-to-restore",
|
||||||
|
Type: protocol.FileInfoTypeSymlink,
|
||||||
|
Version: protocol.Vector{Counters: []protocol.Counter{{ID: 1, Value: 42}}},
|
||||||
|
SymlinkTarget: "/tmp",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
badTime := time.Date(2017, 8, 8, 9, 0, 0, 0, time.UTC).Unix()
|
||||||
|
|
||||||
|
// we have deleted it
|
||||||
|
fs.Update(protocol.LocalDeviceID, []protocol.FileInfo{
|
||||||
|
{
|
||||||
|
Name: "symlink-to-restore",
|
||||||
|
Deleted: true,
|
||||||
|
ModifiedS: badTime,
|
||||||
|
Type: protocol.FileInfoTypeSymlink,
|
||||||
|
Version: protocol.Vector{Counters: []protocol.Counter{{ID: 1, Value: 42}, {ID: 2, Value: 1}}},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Ensure the symlink does in fact not exist
|
||||||
|
symlinkPath := filepath.Join(defaultFolderConfig.Path(), "symlink-to-restore")
|
||||||
|
os.Remove(symlinkPath)
|
||||||
|
defer os.Remove(symlinkPath)
|
||||||
|
if _, err := os.Lstat(symlinkPath); err == nil {
|
||||||
|
t.Fatal("symlink should not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start up
|
||||||
|
m := NewModel(defaultConfig, protocol.LocalDeviceID, "syncthing", "dev", ldb, nil)
|
||||||
|
|
||||||
|
folderCfg := defaultFolderConfig
|
||||||
|
folderCfg.Versioning = config.VersioningConfiguration{
|
||||||
|
Type: "simple",
|
||||||
|
}
|
||||||
|
|
||||||
|
m.AddFolder(folderCfg)
|
||||||
|
m.StartFolder("default")
|
||||||
|
m.ServeBackground()
|
||||||
|
defer m.Stop()
|
||||||
|
m.ScanFolder("default")
|
||||||
|
|
||||||
|
// The symlink should have been restored as part of the StartFolder()
|
||||||
|
|
||||||
|
if _, err := os.Lstat(symlinkPath); err != nil {
|
||||||
|
t.Error("should have restored symlink")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func genFiles(n int) []protocol.FileInfo {
|
func genFiles(n int) []protocol.FileInfo {
|
||||||
files := make([]protocol.FileInfo, n)
|
files := make([]protocol.FileInfo, n)
|
||||||
t := time.Now().Unix()
|
t := time.Now().Unix()
|
||||||
|
@ -27,8 +27,6 @@ type External struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewExternal(folderID, folderPath string, params map[string]string) Versioner {
|
func NewExternal(folderID, folderPath string, params map[string]string) Versioner {
|
||||||
cleanSymlinks(folderPath)
|
|
||||||
|
|
||||||
command := params["command"]
|
command := params["command"]
|
||||||
|
|
||||||
s := External{
|
s := External{
|
||||||
|
@ -26,8 +26,6 @@ type Simple struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewSimple(folderID, folderPath string, params map[string]string) Versioner {
|
func NewSimple(folderID, folderPath string, params map[string]string) Versioner {
|
||||||
cleanSymlinks(folderPath)
|
|
||||||
|
|
||||||
keep, err := strconv.Atoi(params["keep"])
|
keep, err := strconv.Atoi(params["keep"])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
keep = 5 // A reasonable default
|
keep = 5 // A reasonable default
|
||||||
|
@ -39,8 +39,6 @@ type Staggered struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewStaggered(folderID, folderPath string, params map[string]string) Versioner {
|
func NewStaggered(folderID, folderPath string, params map[string]string) Versioner {
|
||||||
cleanSymlinks(folderPath)
|
|
||||||
|
|
||||||
maxAge, err := strconv.ParseInt(params["maxAge"], 10, 0)
|
maxAge, err := strconv.ParseInt(params["maxAge"], 10, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
maxAge = 31536000 // Default: ~1 year
|
maxAge = 31536000 // Default: ~1 year
|
||||||
|
@ -28,8 +28,6 @@ type Trashcan struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewTrashcan(folderID, folderPath string, params map[string]string) Versioner {
|
func NewTrashcan(folderID, folderPath string, params map[string]string) Versioner {
|
||||||
cleanSymlinks(folderPath)
|
|
||||||
|
|
||||||
cleanoutDays, _ := strconv.Atoi(params["cleanoutDays"])
|
cleanoutDays, _ := strconv.Atoi(params["cleanoutDays"])
|
||||||
// On error we default to 0, "do not clean out the trash can"
|
// On error we default to 0, "do not clean out the trash can"
|
||||||
|
|
||||||
|
@ -8,12 +8,6 @@
|
|||||||
// simple default versioning scheme.
|
// simple default versioning scheme.
|
||||||
package versioner
|
package versioner
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Versioner interface {
|
type Versioner interface {
|
||||||
Archive(filePath string) error
|
Archive(filePath string) error
|
||||||
}
|
}
|
||||||
@ -24,23 +18,3 @@ const (
|
|||||||
TimeFormat = "20060102-150405"
|
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
|
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
|
||||||
)
|
)
|
||||||
|
|
||||||
func cleanSymlinks(dir string) {
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
// We don't do symlinks on Windows. Additionally, there may
|
|
||||||
// be things that look like symlinks that are not, which we
|
|
||||||
// should leave alone. Deduplicated files, for example.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if info.Mode()&os.ModeSymlink != 0 {
|
|
||||||
l.Infoln("Removing incorrectly versioned symlink", path)
|
|
||||||
os.Remove(path)
|
|
||||||
return filepath.SkipDir
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user