func TestWalkSymlink(t *testing.T) { if !symlinks.Supported { t.Skip("skipping unsupported symlink test") return } // Create a folder with a symlink in it os.RemoveAll("_symlinks") defer os.RemoveAll("_symlinks") os.Mkdir("_symlinks", 0755) symlinks.Create("_symlinks/link", "destination", symlinks.TargetUnknown) // Scan it fchan, err := Walk(Config{ Dir: "_symlinks", BlockSize: 128 * 1024, }) if err != nil { t.Fatal(err) } var files []protocol.FileInfo for f := range fchan { files = append(files, f) } // Verify that we got one symlink and with the correct block contents if len(files) != 1 { t.Errorf("expected 1 symlink, not %d", len(files)) } if len(files[0].Blocks) != 1 { t.Errorf("expected 1 block, not %d", len(files[0].Blocks)) } if files[0].Blocks[0].Size != int32(len("destination")) { t.Errorf("expected block length %d, not %d", len("destination"), files[0].Blocks[0].Size) } // echo -n "destination" | openssl dgst -sha256 hash := "b5c755aaab1038b3d5627bbde7f47ca80c5f5c0481c6d33f04139d07aa1530e7" if fmt.Sprintf("%x", files[0].Blocks[0].Hash) != hash { t.Errorf("incorrect hash") } }
func TestTraversesSymlink(t *testing.T) { if !symlinks.Supported { t.Skip("pointless test") return } os.RemoveAll("testdata") defer os.RemoveAll("testdata") os.MkdirAll("testdata/a/b/c", 0755) symlinks.Create("testdata/a/l", "b", symlinks.TargetDirectory) // a/l -> b, so a/l/c should resolve by normal stat info, err := osutil.Lstat("testdata/a/l/c") if err != nil { t.Fatal("unexpected error", err) } if !info.IsDir() { t.Fatal("error in setup, a/l/c should be a directory") } cases := []struct { name string traverses bool }{ // Exist {".", false}, {"a", false}, {"a/b", false}, {"a/b/c", false}, // Don't exist {"x", false}, {"a/x", false}, {"a/b/x", false}, {"a/x/c", false}, // Symlink or behind symlink {"a/l", true}, {"a/l/c", true}, // Non-existing behind a symlink {"a/l/x", true}, } for _, tc := range cases { if res := osutil.TraversesSymlink("testdata", tc.name); tc.traverses == (res == nil) { t.Errorf("TraversesSymlink(%q) = %v, should be %v", tc.name, res, tc.traverses) } } }
func TestWalkSymlink(t *testing.T) { if !symlinks.Supported { t.Skip("skipping unsupported symlink test") return } // Create a folder with a symlink in it os.RemoveAll("_symlinks") defer os.RemoveAll("_symlinks") os.Mkdir("_symlinks", 0755) symlinks.Create("_symlinks/link", "destination", symlinks.TargetUnknown) // Scan it fchan, err := Walk(Config{ Dir: "_symlinks", BlockSize: 128 * 1024, }) if err != nil { t.Fatal(err) } var files []protocol.FileInfo for f := range fchan { files = append(files, f) } // Verify that we got one symlink and with the correct attributes if len(files) != 1 { t.Errorf("expected 1 symlink, not %d", len(files)) } if len(files[0].Blocks) != 0 { t.Errorf("expected zero blocks for symlink, not %d", len(files[0].Blocks)) } if files[0].SymlinkTarget != "destination" { t.Errorf("expected symlink to have target destination, not %q", files[0].SymlinkTarget) } }
// 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) } }
func (p *rwFolder) performFinish(state *sharedPullerState) error { // Set the correct permission bits on the new file if !p.ignorePermissions(state.file) { if err := os.Chmod(state.tempName, os.FileMode(state.file.Flags&0777)); err != nil { return err } } // Set the correct timestamp on the new file t := time.Unix(state.file.Modified, 0) if err := os.Chtimes(state.tempName, t, t); err != nil { // Try using virtual mtimes instead info, err := os.Stat(state.tempName) if err != nil { return err } p.virtualMtimeRepo.UpdateMtime(state.file.Name, info.ModTime(), t) } if stat, err := osutil.Lstat(state.realName); err == nil { // There is an old file or directory already in place. We need to // handle that. switch { case stat.IsDir() || stat.Mode()&os.ModeSymlink != 0: // It's a directory or a symlink. These are not versioned or // archived for conflicts, only removed (which of course fails for // non-empty directories). // TODO: This is the place where we want to remove temporary files // and future hard ignores before attempting a directory delete. // Should share code with p.deletDir(). if err = osutil.InWritableDir(osutil.Remove, state.realName); err != nil { return err } case p.inConflict(state.version, state.file.Version): // The new file has been changed in conflict with the existing one. We // should file it away as a conflict instead of just removing or // archiving. Also merge with the version vector we had, to indicate // we have resolved the conflict. state.file.Version = state.file.Version.Merge(state.version) if err = osutil.InWritableDir(moveForConflict, state.realName); err != nil { return err } case p.versioner != nil: // If we should use versioning, let the versioner archive the old // file before we replace it. Archiving a non-existent file is not // an error. if err = p.versioner.Archive(state.realName); err != nil { return err } } } // Replace the original content with the new one if err := osutil.Rename(state.tempName, state.realName); err != nil { return err } // 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) return symlinks.Create(path, string(content), state.file.Flags) }, state.realName) if err != nil { return err } } // Record the updated file in the index p.dbUpdates <- dbUpdateJob{state.file, dbUpdateHandleFile} return nil }
func TestIgnores(t *testing.T) { // Clean and start a syncthing instance log.Println("Cleaning...") err := removeAll("s1", "h1/index*") if err != nil { t.Fatal(err) } p := startInstance(t, 1) defer checkedStop(t, p) // Create eight empty files and directories dirs := []string{"d1", "d2", "d3", "d4", "d11", "d12", "d13", "d14"} files := []string{"f1", "f2", "f3", "f4", "f11", "f12", "f13", "f14", "d1/f1.TXT"} all := append(files, dirs...) for _, dir := range dirs { err := os.Mkdir(filepath.Join("s1", dir), 0755) if err != nil { t.Fatal(err) } } for _, file := range files { fd, err := os.Create(filepath.Join("s1", file)) if err != nil { t.Fatal(err) } fd.Close() } var syms []string if symlinksSupported() { syms = []string{"s1", "s2", "s3", "s4", "s11", "s12", "s13", "s14"} for _, sym := range syms { p := filepath.Join("s1", sym) symlinks.Create(p, p, 0) } all = append(all, syms...) } // Rescan and verify that we see them all if err := p.Rescan("default"); err != nil { t.Fatal(err) } m, err := p.Model("default") if err != nil { t.Fatal(err) } expected := len(all) // nothing is ignored if m.LocalFiles != expected { t.Fatalf("Incorrect number of files after initial scan, %d != %d", m.LocalFiles, expected) } // Add some of them to an ignore file fd, err := os.Create("s1/.stignore") if err != nil { t.Fatal(err) } _, err = fd.WriteString("f1*\nf2\nd1*\nd2\ns1*\ns2\n(?i)*.txt") // [fds][34] only non-ignored items if err != nil { t.Fatal(err) } err = fd.Close() if err != nil { t.Fatal(err) } // Rescan and verify that we see them if err := p.Rescan("default"); err != nil { t.Fatal(err) } m, err = p.Model("default") if err != nil { t.Fatal(err) } expected = len(all) * 2 / 8 // two out of eight items of each type should remain if m.LocalFiles != expected { t.Fatalf("Incorrect number of files after first ignore, %d != %d", m.LocalFiles, expected) } // Change the pattern to include some of the files and dirs previously ignored fd, err = os.Create("s1/.stignore") if err != nil { t.Fatal(err) } _, err = fd.WriteString("f2\nd2\ns2\n") if err != nil { t.Fatal(err) } err = fd.Close() if err != nil { t.Fatal(err) } // Rescan and verify that we see them if err := p.Rescan("default"); err != nil { t.Fatal(err) } m, err = p.Model("default") if err != nil { t.Fatal(err) } expected = len(all)*7/8 + 1 // seven out of eight items of each type should remain, plus the foo.TXT if m.LocalFiles != expected { t.Fatalf("Incorrect number of files after second ignore, %d != %d", m.LocalFiles, expected) } }
func testSymlinks(t *testing.T) { log.Println("Cleaning...") err := removeAll("s1", "s2", "h1/index*", "h2/index*") if err != nil { t.Fatal(err) } log.Println("Generating files...") err = generateFiles("s1", 100, 20, "../LICENSE") if err != nil { t.Fatal(err) } // A file that we will replace with a symlink later fd, err := os.Create("s1/fileToReplace") if err != nil { t.Fatal(err) } fd.Close() // A directory that we will replace with a symlink later err = os.Mkdir("s1/dirToReplace", 0755) if err != nil { t.Fatal(err) } // A file and a symlink to that file fd, err = os.Create("s1/file") if err != nil { t.Fatal(err) } fd.Close() err = symlinks.Create("s1/fileLink", "file", 0) if err != nil { log.Fatal(err) } // A directory and a symlink to that directory err = os.Mkdir("s1/dir", 0755) if err != nil { t.Fatal(err) } err = symlinks.Create("s1/dirLink", "dir", 0) if err != nil { log.Fatal(err) } // A link to something in the repo that does not exist err = symlinks.Create("s1/noneLink", "does/not/exist", 0) if err != nil { log.Fatal(err) } // A link we will replace with a file later err = symlinks.Create("s1/repFileLink", "does/not/exist", 0) if err != nil { log.Fatal(err) } // A link we will replace with a directory later err = symlinks.Create("s1/repDirLink", "does/not/exist", 0) if err != nil { log.Fatal(err) } // A link we will remove later err = symlinks.Create("s1/removeLink", "does/not/exist", 0) if err != nil { log.Fatal(err) } // Verify that the files and symlinks sync to the other side sender := startInstance(t, 1) defer checkedStop(t, sender) receiver := startInstance(t, 2) defer checkedStop(t, receiver) log.Println("Syncing...") rc.AwaitSync("default", sender, receiver) log.Println("Comparing directories...") err = compareDirectories("s1", "s2") if err != nil { t.Fatal(err) } log.Println("Making some changes...") // Remove one symlink err = os.Remove("s1/fileLink") if err != nil { log.Fatal(err) } // Change the target of another err = os.Remove("s1/dirLink") if err != nil { log.Fatal(err) } err = symlinks.Create("s1/dirLink", "file", 0) if err != nil { log.Fatal(err) } // Replace one with a file err = os.Remove("s1/repFileLink") if err != nil { log.Fatal(err) } fd, err = os.Create("s1/repFileLink") if err != nil { log.Fatal(err) } fd.Close() // Replace one with a directory err = os.Remove("s1/repDirLink") if err != nil { log.Fatal(err) } err = os.Mkdir("s1/repDirLink", 0755) if err != nil { log.Fatal(err) } // Replace a file with a symlink err = os.Remove("s1/fileToReplace") if err != nil { log.Fatal(err) } err = symlinks.Create("s1/fileToReplace", "somewhere/non/existent", 0) if err != nil { log.Fatal(err) } // Replace a directory with a symlink err = os.RemoveAll("s1/dirToReplace") if err != nil { log.Fatal(err) } err = symlinks.Create("s1/dirToReplace", "somewhere/non/existent", 0) if err != nil { log.Fatal(err) } // Remove a broken symlink err = os.Remove("s1/removeLink") if err != nil { log.Fatal(err) } // Sync these changes and recheck log.Println("Syncing...") if err := sender.Rescan("default"); err != nil { t.Fatal(err) } rc.AwaitSync("default", sender, receiver) log.Println("Comparing directories...") err = compareDirectories("s1", "s2") if err != nil { t.Fatal(err) } }
func (p *rwFolder) performFinish(state *sharedPullerState) error { // Set the correct permission bits on the new file if !p.ignorePermissions(state.file) { if err := os.Chmod(state.tempName, os.FileMode(state.file.Flags&0777)); err != nil { return err } } // Set the correct timestamp on the new file t := time.Unix(state.file.Modified, 0) if err := os.Chtimes(state.tempName, t, t); err != nil { // Try using virtual mtimes instead info, err := os.Stat(state.tempName) if err != nil { return err } p.virtualMtimeRepo.UpdateMtime(state.file.Name, info.ModTime(), t) } var err error if p.inConflict(state.version, state.file.Version) { // The new file has been changed in conflict with the existing one. We // should file it away as a conflict instead of just removing or // archiving. Also merge with the version vector we had, to indicate // we have resolved the conflict. state.file.Version = state.file.Version.Merge(state.version) err = osutil.InWritableDir(moveForConflict, state.realName) } else if p.versioner != nil { // If we should use versioning, let the versioner archive the old // file before we replace it. Archiving a non-existent file is not // an error. err = p.versioner.Archive(state.realName) } else { err = nil } if err != nil { return err } // If the target path is a symlink or a directory, we cannot copy // over it, hence remove it before proceeding. stat, err := osutil.Lstat(state.realName) if err == nil && (stat.IsDir() || stat.Mode()&os.ModeSymlink != 0) { osutil.InWritableDir(osutil.Remove, state.realName) } // Replace the original content with the new one if err = osutil.Rename(state.tempName, state.realName); err != nil { return err } // 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) return symlinks.Create(path, string(content), state.file.Flags) }, state.realName) if err != nil { return err } } // Record the updated file in the index p.dbUpdates <- dbUpdateJob{state.file, dbUpdateHandleFile} return nil }