lib/model, lib/protocol, lib/scanner: Include symlink target in index, pull symlinks synchronously
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3792
This commit is contained in:
committed by
Audrius Butkevicius
parent
f6a2b6252a
commit
7b07ed6580
@@ -9,7 +9,6 @@ package model
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -63,6 +62,7 @@ const (
|
||||
dbUpdateHandleFile
|
||||
dbUpdateDeleteFile
|
||||
dbUpdateShortcutFile
|
||||
dbUpdateHandleSymlink
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -394,7 +394,7 @@ func (f *rwFolder) pullerIteration(ignores *ignore.Matcher) int {
|
||||
dirDeletions := []protocol.FileInfo{}
|
||||
buckets := map[string][]protocol.FileInfo{}
|
||||
|
||||
handleFile := func(fi protocol.FileInfo) bool {
|
||||
handleItem := func(fi protocol.FileInfo) bool {
|
||||
switch {
|
||||
case fi.IsDeleted():
|
||||
// A deleted file, directory or symlink
|
||||
@@ -413,10 +413,18 @@ func (f *rwFolder) pullerIteration(ignores *ignore.Matcher) int {
|
||||
buckets[key] = append(buckets[key], df)
|
||||
}
|
||||
}
|
||||
changed++
|
||||
|
||||
case fi.IsDirectory() && !fi.IsSymlink():
|
||||
// A new or changed directory
|
||||
l.Debugln("Creating directory", fi.Name)
|
||||
l.Debugln("Handling directory", fi.Name)
|
||||
f.handleDir(fi)
|
||||
changed++
|
||||
|
||||
case fi.IsSymlink():
|
||||
l.Debugln("Handling symlink", fi.Name)
|
||||
f.handleSymlink(fi)
|
||||
changed++
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
@@ -442,7 +450,7 @@ func (f *rwFolder) pullerIteration(ignores *ignore.Matcher) int {
|
||||
|
||||
file := intf.(protocol.FileInfo)
|
||||
l.Debugln(f, "handling", file.Name)
|
||||
if !handleFile(file) {
|
||||
if !handleItem(file) {
|
||||
// A new or changed file or symlink. This is the only case where
|
||||
// we do stuff concurrently in the background. We only queue
|
||||
// files where we are connected to at least one device that has
|
||||
@@ -504,35 +512,35 @@ nextFile:
|
||||
// Handles races where an index update arrives changing what the file
|
||||
// is between queueing and retrieving it from the queue, effectively
|
||||
// changing how the file should be handled.
|
||||
if handleFile(fi) {
|
||||
if handleItem(fi) {
|
||||
continue
|
||||
}
|
||||
|
||||
if !fi.IsSymlink() {
|
||||
key := string(fi.Blocks[0].Hash)
|
||||
for i, candidate := range buckets[key] {
|
||||
if scanner.BlocksEqual(candidate.Blocks, fi.Blocks) {
|
||||
// Remove the candidate from the bucket
|
||||
lidx := len(buckets[key]) - 1
|
||||
buckets[key][i] = buckets[key][lidx]
|
||||
buckets[key] = buckets[key][:lidx]
|
||||
// Check our list of files to be removed for a match, in which case
|
||||
// we can just do a rename instead.
|
||||
key := string(fi.Blocks[0].Hash)
|
||||
for i, candidate := range buckets[key] {
|
||||
if scanner.BlocksEqual(candidate.Blocks, fi.Blocks) {
|
||||
// Remove the candidate from the bucket
|
||||
lidx := len(buckets[key]) - 1
|
||||
buckets[key][i] = buckets[key][lidx]
|
||||
buckets[key] = buckets[key][:lidx]
|
||||
|
||||
// candidate is our current state of the file, where as the
|
||||
// desired state with the delete bit set is in the deletion
|
||||
// map.
|
||||
desired := fileDeletions[candidate.Name]
|
||||
// Remove the pending deletion (as we perform it by renaming)
|
||||
delete(fileDeletions, candidate.Name)
|
||||
// candidate is our current state of the file, where as the
|
||||
// desired state with the delete bit set is in the deletion
|
||||
// map.
|
||||
desired := fileDeletions[candidate.Name]
|
||||
// Remove the pending deletion (as we perform it by renaming)
|
||||
delete(fileDeletions, candidate.Name)
|
||||
|
||||
f.renameFile(desired, fi)
|
||||
f.renameFile(desired, fi)
|
||||
|
||||
f.queue.Done(fileName)
|
||||
continue nextFile
|
||||
}
|
||||
f.queue.Done(fileName)
|
||||
continue nextFile
|
||||
}
|
||||
}
|
||||
|
||||
// Not a rename or a symlink, deal with it.
|
||||
// Handle the file normally, by coping and pulling, etc.
|
||||
f.handleFile(fi, copyChan, finisherChan)
|
||||
}
|
||||
|
||||
@@ -569,7 +577,10 @@ nextFile:
|
||||
|
||||
// handleDir creates or updates the given directory
|
||||
func (f *rwFolder) handleDir(file protocol.FileInfo) {
|
||||
// Used in the defer closure below, updated by the function body. Take
|
||||
// care not declare another err.
|
||||
var err error
|
||||
|
||||
events.Default.Log(events.ItemStarted, map[string]string{
|
||||
"folder": f.folderID,
|
||||
"item": file.Name,
|
||||
@@ -666,15 +677,93 @@ func (f *rwFolder) handleDir(file protocol.FileInfo) {
|
||||
}
|
||||
}
|
||||
|
||||
// handleSymlink creates or updates the given symlink
|
||||
func (f *rwFolder) handleSymlink(file protocol.FileInfo) {
|
||||
// Used in the defer closure below, updated by the function body. Take
|
||||
// care not declare another err.
|
||||
var err error
|
||||
|
||||
events.Default.Log(events.ItemStarted, map[string]string{
|
||||
"folder": f.folderID,
|
||||
"item": file.Name,
|
||||
"type": "symlink",
|
||||
"action": "update",
|
||||
})
|
||||
|
||||
defer func() {
|
||||
events.Default.Log(events.ItemFinished, map[string]interface{}{
|
||||
"folder": f.folderID,
|
||||
"item": file.Name,
|
||||
"error": events.Error(err),
|
||||
"type": "symlink",
|
||||
"action": "update",
|
||||
})
|
||||
}()
|
||||
|
||||
realName, err := rootedJoinedPath(f.dir, file.Name)
|
||||
if err != nil {
|
||||
f.newError(file.Name, err)
|
||||
return
|
||||
}
|
||||
|
||||
if shouldDebug() {
|
||||
curFile, _ := f.model.CurrentFolderFile(f.folderID, file.Name)
|
||||
l.Debugf("need symlink\n\t%v\n\t%v", file, curFile)
|
||||
}
|
||||
|
||||
if len(file.SymlinkTarget) == 0 {
|
||||
// Index entry from a Syncthing predating the support for including
|
||||
// the link target in the index entry. We log this as an error.
|
||||
err = errors.New("incompatible symlink entry; rescan with newer Syncthing on source")
|
||||
l.Infof("Puller (folder %q, dir %q): %v", f.folderID, file.Name, err)
|
||||
f.newError(file.Name, err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = f.mtimeFS.Lstat(realName); err == nil {
|
||||
// There is already something under that name. Remove it to replace
|
||||
// with the symlink. This also handles the "change symlink type"
|
||||
// path.
|
||||
err = osutil.InWritableDir(os.Remove, realName)
|
||||
if err != nil {
|
||||
l.Infof("Puller (folder %q, dir %q): %v", f.folderID, file.Name, err)
|
||||
f.newError(file.Name, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
tt := symlinks.TargetFile
|
||||
if file.IsDirectory() {
|
||||
tt = symlinks.TargetDirectory
|
||||
}
|
||||
|
||||
// We declare a function that acts on only the path name, so
|
||||
// we can pass it to InWritableDir.
|
||||
createLink := func(path string) error {
|
||||
return symlinks.Create(path, file.SymlinkTarget, tt)
|
||||
}
|
||||
|
||||
if err = osutil.InWritableDir(createLink, realName); err == nil {
|
||||
f.dbUpdates <- dbUpdateJob{file, dbUpdateHandleSymlink}
|
||||
} else {
|
||||
l.Infof("Puller (folder %q, dir %q): %v", f.folderID, file.Name, err)
|
||||
f.newError(file.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
// deleteDir attempts to delete the given directory
|
||||
func (f *rwFolder) deleteDir(file protocol.FileInfo, matcher *ignore.Matcher) {
|
||||
// Used in the defer closure below, updated by the function body. Take
|
||||
// care not declare another err.
|
||||
var err error
|
||||
|
||||
events.Default.Log(events.ItemStarted, map[string]string{
|
||||
"folder": f.folderID,
|
||||
"item": file.Name,
|
||||
"type": "dir",
|
||||
"action": "delete",
|
||||
})
|
||||
|
||||
defer func() {
|
||||
events.Default.Log(events.ItemFinished, map[string]interface{}{
|
||||
"folder": f.folderID,
|
||||
@@ -721,13 +810,17 @@ func (f *rwFolder) deleteDir(file protocol.FileInfo, matcher *ignore.Matcher) {
|
||||
|
||||
// deleteFile attempts to delete the given file
|
||||
func (f *rwFolder) deleteFile(file protocol.FileInfo) {
|
||||
// Used in the defer closure below, updated by the function body. Take
|
||||
// care not declare another err.
|
||||
var err error
|
||||
|
||||
events.Default.Log(events.ItemStarted, map[string]string{
|
||||
"folder": f.folderID,
|
||||
"item": file.Name,
|
||||
"type": "file",
|
||||
"action": "delete",
|
||||
})
|
||||
|
||||
defer func() {
|
||||
events.Default.Log(events.ItemFinished, map[string]interface{}{
|
||||
"folder": f.folderID,
|
||||
@@ -775,7 +868,10 @@ func (f *rwFolder) deleteFile(file protocol.FileInfo) {
|
||||
// renameFile attempts to rename an existing file to a destination
|
||||
// and set the right attributes on it.
|
||||
func (f *rwFolder) renameFile(source, target protocol.FileInfo) {
|
||||
// Used in the defer closure below, updated by the function body. Take
|
||||
// care not declare another err.
|
||||
var err error
|
||||
|
||||
events.Default.Log(events.ItemStarted, map[string]string{
|
||||
"folder": f.folderID,
|
||||
"item": source.Name,
|
||||
@@ -788,6 +884,7 @@ func (f *rwFolder) renameFile(source, target protocol.FileInfo) {
|
||||
"type": "file",
|
||||
"action": "update",
|
||||
})
|
||||
|
||||
defer func() {
|
||||
events.Default.Log(events.ItemFinished, map[string]interface{}{
|
||||
"folder": f.folderID,
|
||||
@@ -912,13 +1009,7 @@ func (f *rwFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocks
|
||||
|
||||
f.queue.Done(file.Name)
|
||||
|
||||
var err error
|
||||
if file.IsSymlink() {
|
||||
err = f.shortcutSymlink(file)
|
||||
} else {
|
||||
err = f.shortcutFile(file)
|
||||
}
|
||||
|
||||
err := f.shortcutFile(file)
|
||||
events.Default.Log(events.ItemFinished, map[string]interface{}{
|
||||
"folder": f.folderID,
|
||||
"item": file.Name,
|
||||
@@ -1089,20 +1180,6 @@ func (f *rwFolder) shortcutFile(file protocol.FileInfo) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// shortcutSymlink changes the symlinks type if necessary.
|
||||
func (f *rwFolder) shortcutSymlink(file protocol.FileInfo) (err error) {
|
||||
tt := symlinks.TargetFile
|
||||
if file.IsDirectory() {
|
||||
tt = symlinks.TargetDirectory
|
||||
}
|
||||
err = symlinks.ChangeType(filepath.Join(f.dir, file.Name), tt)
|
||||
if err != nil {
|
||||
l.Infof("Puller (folder %q, file %q): symlink shortcut: %v", f.folderID, file.Name, err)
|
||||
f.newError(file.Name, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// copierRoutine reads copierStates until the in channel closes and performs
|
||||
// the relevant copies when possible, or passes it to the puller routine.
|
||||
func (f *rwFolder) copierRoutine(in <-chan copyBlocksState, pullChan chan<- pullBlockState, out chan<- *sharedPullerState) {
|
||||
@@ -1332,27 +1409,6 @@ func (f *rwFolder) performFinish(state *sharedPullerState) error {
|
||||
// Set the correct timestamp on the new file
|
||||
f.mtimeFS.Chtimes(state.realName, state.file.ModTime(), state.file.ModTime()) // never fails
|
||||
|
||||
// If it's a symlink, the target of the symlink is inside the file.
|
||||
if state.file.IsSymlink() {
|
||||
content, err := ioutil.ReadFile(state.realName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove the file, and replace it with a symlink.
|
||||
err = osutil.InWritableDir(func(path string) error {
|
||||
os.Remove(path)
|
||||
tt := symlinks.TargetFile
|
||||
if state.file.IsDirectory() {
|
||||
tt = symlinks.TargetDirectory
|
||||
}
|
||||
return symlinks.Create(path, string(content), tt)
|
||||
}, state.realName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Record the updated file in the index
|
||||
f.dbUpdates <- dbUpdateJob{state.file, dbUpdateHandleFile}
|
||||
return nil
|
||||
@@ -1441,17 +1497,14 @@ func (f *rwFolder) dbUpdaterRoutine() {
|
||||
// collect changed files and dirs
|
||||
switch job.jobType {
|
||||
case dbUpdateHandleFile, dbUpdateShortcutFile:
|
||||
// fsyncing symlinks is only supported by MacOS
|
||||
if !job.file.IsSymlink() {
|
||||
changedFiles = append(changedFiles,
|
||||
filepath.Join(f.dir, job.file.Name))
|
||||
}
|
||||
changedFiles = append(changedFiles, filepath.Join(f.dir, job.file.Name))
|
||||
case dbUpdateHandleDir:
|
||||
changedDirs = append(changedDirs, filepath.Join(f.dir, job.file.Name))
|
||||
case dbUpdateHandleSymlink:
|
||||
// fsyncing symlinks is only supported by MacOS, ignore
|
||||
}
|
||||
if job.jobType != dbUpdateShortcutFile {
|
||||
changedDirs = append(changedDirs,
|
||||
filepath.Dir(filepath.Join(f.dir, job.file.Name)))
|
||||
changedDirs = append(changedDirs, filepath.Dir(filepath.Join(f.dir, job.file.Name)))
|
||||
}
|
||||
}
|
||||
if job.file.IsInvalid() || (job.file.IsDirectory() && !job.file.IsSymlink()) {
|
||||
|
||||
Reference in New Issue
Block a user