all: Convert folders to use filesystem abstraction
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/4228
This commit is contained in:
committed by
Jakob Borg
parent
ab8c2fb5c7
commit
3d8b4a42b7
@@ -8,10 +8,10 @@ package osutil
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/fs"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -25,7 +25,8 @@ var (
|
||||
// returned on Close, so a lazy user can ignore errors until Close.
|
||||
type AtomicWriter struct {
|
||||
path string
|
||||
next *os.File
|
||||
next fs.File
|
||||
fs fs.Filesystem
|
||||
err error
|
||||
}
|
||||
|
||||
@@ -33,11 +34,19 @@ type AtomicWriter struct {
|
||||
// instead of the given name. The file is created with secure (0600)
|
||||
// permissions.
|
||||
func CreateAtomic(path string) (*AtomicWriter, error) {
|
||||
fs := fs.NewFilesystem(fs.FilesystemTypeBasic, filepath.Dir(path))
|
||||
return CreateAtomicFilesystem(fs, filepath.Base(path))
|
||||
}
|
||||
|
||||
// CreateAtomicFilesystem is like os.Create, except a temporary file name is used
|
||||
// instead of the given name. The file is created with secure (0600)
|
||||
// permissions.
|
||||
func CreateAtomicFilesystem(filesystem fs.Filesystem, path string) (*AtomicWriter, error) {
|
||||
// The security of this depends on the tempfile having secure
|
||||
// permissions, 0600, from the beginning. This is what ioutil.TempFile
|
||||
// does. We have a test that verifies that that is the case, should this
|
||||
// ever change in the standard library in the future.
|
||||
fd, err := ioutil.TempFile(filepath.Dir(path), TempPrefix)
|
||||
fd, err := TempFile(filesystem, filepath.Dir(path), TempPrefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -45,6 +54,7 @@ func CreateAtomic(path string) (*AtomicWriter, error) {
|
||||
w := &AtomicWriter{
|
||||
path: path,
|
||||
next: fd,
|
||||
fs: filesystem,
|
||||
}
|
||||
|
||||
return w, nil
|
||||
@@ -71,7 +81,7 @@ func (w *AtomicWriter) Close() error {
|
||||
}
|
||||
|
||||
// Try to not leave temp file around, but ignore error.
|
||||
defer os.Remove(w.next.Name())
|
||||
defer w.fs.Remove(w.next.Name())
|
||||
|
||||
if err := w.next.Sync(); err != nil {
|
||||
w.err = err
|
||||
@@ -88,17 +98,21 @@ func (w *AtomicWriter) Close() error {
|
||||
// either. Return this error because it may be more informative. On non-
|
||||
// Windows we want the atomic rename behavior so we don't attempt remove.
|
||||
if runtime.GOOS == "windows" {
|
||||
if err := os.Remove(w.path); err != nil && !os.IsNotExist(err) {
|
||||
if err := w.fs.Remove(w.path); err != nil && !fs.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.Rename(w.next.Name(), w.path); err != nil {
|
||||
if err := w.fs.Rename(w.next.Name(), w.path); err != nil {
|
||||
w.err = err
|
||||
return err
|
||||
}
|
||||
|
||||
SyncDir(filepath.Dir(w.next.Name()))
|
||||
// fsync the directory too
|
||||
if fd, err := w.fs.Open(filepath.Dir(w.next.Name())); err == nil {
|
||||
fd.Sync()
|
||||
fd.Close()
|
||||
}
|
||||
|
||||
// Set w.err to return appropriately for any future operations.
|
||||
w.err = ErrClosed
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
// Copyright (C) 2016 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 https://mozilla.org/MPL/2.0/.
|
||||
|
||||
// +build !windows
|
||||
|
||||
package osutil
|
||||
|
||||
func GetFilesystemRoots() ([]string, error) {
|
||||
return []string{"/"}, nil
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
// Copyright (C) 2016 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 https://mozilla.org/MPL/2.0/.
|
||||
|
||||
// +build windows
|
||||
|
||||
package osutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func GetFilesystemRoots() ([]string, error) {
|
||||
kernel32, err := syscall.LoadDLL("kernel32.dll")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
getLogicalDriveStringsHandle, err := kernel32.FindProc("GetLogicalDriveStringsA")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buffer := [1024]byte{}
|
||||
bufferSize := uint32(len(buffer))
|
||||
|
||||
hr, _, _ := getLogicalDriveStringsHandle.Call(uintptr(unsafe.Pointer(&bufferSize)), uintptr(unsafe.Pointer(&buffer)))
|
||||
if hr == 0 {
|
||||
return nil, fmt.Errorf("Syscall failed")
|
||||
}
|
||||
|
||||
var drives []string
|
||||
parts := bytes.Split(buffer[:], []byte{0})
|
||||
for _, part := range parts {
|
||||
if len(part) == 0 {
|
||||
break
|
||||
}
|
||||
drives = append(drives, string(part))
|
||||
}
|
||||
|
||||
return drives, nil
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
// 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 https://mozilla.org/MPL/2.0/.
|
||||
|
||||
// +build !windows
|
||||
|
||||
package osutil
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func Glob(pattern string) (matches []string, err error) {
|
||||
return filepath.Glob(pattern)
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
// 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 https://mozilla.org/MPL/2.0/.
|
||||
|
||||
// +build windows
|
||||
|
||||
package osutil
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Glob implements filepath.Glob, but works with Windows long path prefixes.
|
||||
// Deals with https://github.com/golang/go/issues/10577
|
||||
func Glob(pattern string) (matches []string, err error) {
|
||||
if !hasMeta(pattern) {
|
||||
if _, err = os.Lstat(pattern); err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
return []string{pattern}, nil
|
||||
}
|
||||
|
||||
dir, file := filepath.Split(filepath.Clean(pattern))
|
||||
switch dir {
|
||||
case "":
|
||||
dir = "."
|
||||
case string(filepath.Separator):
|
||||
// nothing
|
||||
default:
|
||||
if runtime.GOOS != "windows" || len(dir) < 2 || dir[len(dir)-2] != ':' {
|
||||
dir = dir[0 : len(dir)-1] // chop off trailing separator, if it's not after the drive letter
|
||||
}
|
||||
}
|
||||
|
||||
if !hasMeta(dir) {
|
||||
return glob(dir, file, nil)
|
||||
}
|
||||
|
||||
var m []string
|
||||
m, err = Glob(dir)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, d := range m {
|
||||
matches, err = glob(d, file, matches)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func hasMeta(path string) bool {
|
||||
// Strip off Windows long path prefix if it exists.
|
||||
if strings.HasPrefix(path, "\\\\?\\") {
|
||||
path = path[4:]
|
||||
}
|
||||
// TODO(niemeyer): Should other magic characters be added here?
|
||||
return strings.IndexAny(path, "*?[") >= 0
|
||||
}
|
||||
|
||||
func glob(dir, pattern string, matches []string) (m []string, e error) {
|
||||
m = matches
|
||||
fi, err := os.Stat(dir)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
return
|
||||
}
|
||||
d, err := os.Open(dir)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer d.Close()
|
||||
|
||||
names, _ := d.Readdirnames(-1)
|
||||
sort.Strings(names)
|
||||
|
||||
for _, n := range names {
|
||||
matched, err := filepath.Match(pattern, n)
|
||||
if err != nil {
|
||||
return m, err
|
||||
}
|
||||
if matched {
|
||||
m = append(m, filepath.Join(dir, n))
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
// 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 https://mozilla.org/MPL/2.0/.
|
||||
|
||||
// +build windows
|
||||
|
||||
package osutil_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
)
|
||||
|
||||
func TestGlob(t *testing.T) {
|
||||
testcases := []string{
|
||||
`C:\*`,
|
||||
`\\?\C:\*`,
|
||||
`\\?\C:\Users`,
|
||||
`\\?\\\?\C:\Users`,
|
||||
}
|
||||
for _, tc := range testcases {
|
||||
if _, err := osutil.Glob(tc); err != nil {
|
||||
t.Fatalf("pattern %s failed: %v", tc, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,12 +8,4 @@
|
||||
|
||||
package osutil
|
||||
|
||||
func HideFile(path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func ShowFile(path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func HideConsole() {}
|
||||
|
||||
@@ -10,36 +10,6 @@ package osutil
|
||||
|
||||
import "syscall"
|
||||
|
||||
func HideFile(path string) error {
|
||||
p, err := syscall.UTF16PtrFromString(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
attrs, err := syscall.GetFileAttributes(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
attrs |= syscall.FILE_ATTRIBUTE_HIDDEN
|
||||
return syscall.SetFileAttributes(p, attrs)
|
||||
}
|
||||
|
||||
func ShowFile(path string) error {
|
||||
p, err := syscall.UTF16PtrFromString(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
attrs, err := syscall.GetFileAttributes(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
attrs &^= syscall.FILE_ATTRIBUTE_HIDDEN
|
||||
return syscall.SetFileAttributes(p, attrs)
|
||||
}
|
||||
|
||||
func HideConsole() {
|
||||
getConsoleWindow := syscall.NewLazyDLL("kernel32.dll").NewProc("GetConsoleWindow")
|
||||
showWindow := syscall.NewLazyDLL("user32.dll").NewProc("ShowWindow")
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
// 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 https://mozilla.org/MPL/2.0/.
|
||||
|
||||
// +build linux android
|
||||
|
||||
package osutil
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Lstat is like os.Lstat, except lobotomized for Android. See
|
||||
// https://forum.syncthing.net/t/2395
|
||||
func Lstat(name string) (fi os.FileInfo, err error) {
|
||||
for i := 0; i < 10; i++ { // We have to draw the line somewhere
|
||||
fi, err = os.Lstat(name)
|
||||
if err, ok := err.(*os.PathError); ok && err.Err == syscall.EINTR {
|
||||
time.Sleep(time.Duration(i+1) * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
// 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 https://mozilla.org/MPL/2.0/.
|
||||
|
||||
// +build !linux,!android
|
||||
|
||||
package osutil
|
||||
|
||||
import "os"
|
||||
|
||||
func Lstat(name string) (fi os.FileInfo, err error) {
|
||||
return os.Lstat(name)
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
// 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 https://mozilla.org/MPL/2.0/.
|
||||
|
||||
// +build !windows
|
||||
|
||||
package osutil
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
func MkdirAll(path string, perm os.FileMode) error {
|
||||
return os.MkdirAll(path, perm)
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are
|
||||
// met:
|
||||
//
|
||||
// * Redistributions of source code must retain the above copyright
|
||||
// notice, this list of conditions and the following disclaimer.
|
||||
// * Redistributions in binary form must reproduce the above
|
||||
// copyright notice, this list of conditions and the following disclaimer
|
||||
// in the documentation and/or other materials provided with the
|
||||
// distribution.
|
||||
// * Neither the name of Google Inc. nor the names of its
|
||||
// contributors may be used to endorse or promote products derived from
|
||||
// this software without specific prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
//
|
||||
// Modified by Zillode to fix https://github.com/syncthing/syncthing/issues/1822
|
||||
// Sync with https://github.com/golang/go/blob/master/src/os/path.go
|
||||
// See https://github.com/golang/go/issues/10900
|
||||
|
||||
package osutil
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// MkdirAll creates a directory named path, along with any necessary parents,
|
||||
// and returns nil, or else returns an error.
|
||||
// The permission bits perm are used for all directories that MkdirAll creates.
|
||||
// If path is already a directory, MkdirAll does nothing and returns nil.
|
||||
func MkdirAll(path string, perm os.FileMode) error {
|
||||
// Fast path: if we can tell whether path is a directory or file, stop with success or error.
|
||||
dir, err := os.Stat(path)
|
||||
if err == nil {
|
||||
if dir.IsDir() {
|
||||
return nil
|
||||
}
|
||||
return &os.PathError{
|
||||
Op: "mkdir",
|
||||
Path: path,
|
||||
Err: syscall.ENOTDIR,
|
||||
}
|
||||
}
|
||||
|
||||
// Slow path: make sure parent exists and then call Mkdir for path.
|
||||
i := len(path)
|
||||
for i > 0 && os.IsPathSeparator(path[i-1]) { // Skip trailing path separator.
|
||||
i--
|
||||
}
|
||||
|
||||
j := i
|
||||
for j > 0 && !os.IsPathSeparator(path[j-1]) { // Scan backward over element.
|
||||
j--
|
||||
}
|
||||
|
||||
if j > 1 {
|
||||
// Create parent
|
||||
parent := path[0 : j-1]
|
||||
if parent != filepath.VolumeName(parent) {
|
||||
err = MkdirAll(parent, perm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parent now exists; invoke Mkdir and use its result.
|
||||
err = os.Mkdir(path, perm)
|
||||
if err != nil {
|
||||
// Handle arguments like "foo/." by
|
||||
// double-checking that directory doesn't exist.
|
||||
dir, err1 := os.Lstat(path)
|
||||
if err1 == nil && dir.IsDir() {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
56
lib/osutil/net.go
Normal file
56
lib/osutil/net.go
Normal file
@@ -0,0 +1,56 @@
|
||||
// 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 osutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net"
|
||||
)
|
||||
|
||||
// ResolveInterfaceAddresses returns available addresses of the given network
|
||||
// type for a given interface.
|
||||
func ResolveInterfaceAddresses(network, nameOrMac string) []string {
|
||||
intf, err := net.InterfaceByName(nameOrMac)
|
||||
if err == nil {
|
||||
return interfaceAddresses(network, intf)
|
||||
}
|
||||
|
||||
mac, err := net.ParseMAC(nameOrMac)
|
||||
if err != nil {
|
||||
return []string{nameOrMac}
|
||||
}
|
||||
|
||||
intfs, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return []string{nameOrMac}
|
||||
}
|
||||
|
||||
for _, intf := range intfs {
|
||||
if bytes.Equal(intf.HardwareAddr, mac) {
|
||||
return interfaceAddresses(network, &intf)
|
||||
}
|
||||
}
|
||||
|
||||
return []string{nameOrMac}
|
||||
}
|
||||
|
||||
func interfaceAddresses(network string, intf *net.Interface) []string {
|
||||
var out []string
|
||||
addrs, err := intf.Addrs()
|
||||
if err != nil {
|
||||
return out
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
ipnet, ok := addr.(*net.IPNet)
|
||||
if ok && (network == "tcp" || (network == "tcp4" && len(ipnet.IP) == net.IPv4len) || (network == "tcp6" && len(ipnet.IP) == net.IPv6len)) {
|
||||
out = append(out, ipnet.IP.String())
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
@@ -9,19 +9,16 @@ package osutil
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/calmh/du"
|
||||
"github.com/syncthing/syncthing/lib/fs"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
)
|
||||
|
||||
var errNoHome = errors.New("no home directory found - set $HOME (or the platform equivalent)")
|
||||
|
||||
// Try to keep this entire operation atomic-like. We shouldn't be doing this
|
||||
// often enough that there is any contention on this lock.
|
||||
var renameLock = sync.NewMutex()
|
||||
@@ -29,12 +26,12 @@ var renameLock = sync.NewMutex()
|
||||
// TryRename renames a file, leaving source file intact in case of failure.
|
||||
// Tries hard to succeed on various systems by temporarily tweaking directory
|
||||
// permissions and removing the destination file when necessary.
|
||||
func TryRename(from, to string) error {
|
||||
func TryRename(filesystem fs.Filesystem, from, to string) error {
|
||||
renameLock.Lock()
|
||||
defer renameLock.Unlock()
|
||||
|
||||
return withPreparedTarget(from, to, func() error {
|
||||
return os.Rename(from, to)
|
||||
return withPreparedTarget(filesystem, from, to, func() error {
|
||||
return filesystem.Rename(from, to)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -43,28 +40,28 @@ func TryRename(from, to string) error {
|
||||
// for situations like committing a temp file to it's final location.
|
||||
// Tries hard to succeed on various systems by temporarily tweaking directory
|
||||
// permissions and removing the destination file when necessary.
|
||||
func Rename(from, to string) error {
|
||||
func Rename(filesystem fs.Filesystem, from, to string) error {
|
||||
// Don't leave a dangling temp file in case of rename error
|
||||
if !(runtime.GOOS == "windows" && strings.EqualFold(from, to)) {
|
||||
defer os.Remove(from)
|
||||
defer filesystem.Remove(from)
|
||||
}
|
||||
return TryRename(from, to)
|
||||
return TryRename(filesystem, from, to)
|
||||
}
|
||||
|
||||
// Copy copies the file content from source to destination.
|
||||
// Tries hard to succeed on various systems by temporarily tweaking directory
|
||||
// permissions and removing the destination file when necessary.
|
||||
func Copy(from, to string) (err error) {
|
||||
return withPreparedTarget(from, to, func() error {
|
||||
return copyFileContents(from, to)
|
||||
func Copy(filesystem fs.Filesystem, from, to string) (err error) {
|
||||
return withPreparedTarget(filesystem, from, to, func() error {
|
||||
return copyFileContents(filesystem, from, to)
|
||||
})
|
||||
}
|
||||
|
||||
// InWritableDir calls fn(path), while making sure that the directory
|
||||
// containing `path` is writable for the duration of the call.
|
||||
func InWritableDir(fn func(string) error, path string) error {
|
||||
func InWritableDir(fn func(string) error, fs fs.Filesystem, path string) error {
|
||||
dir := filepath.Dir(path)
|
||||
info, err := os.Stat(dir)
|
||||
info, err := fs.Stat(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -75,10 +72,10 @@ func InWritableDir(fn func(string) error, path string) error {
|
||||
// A non-writeable directory (for this user; we assume that's the
|
||||
// relevant part). Temporarily change the mode so we can delete the
|
||||
// file or directory inside it.
|
||||
err = os.Chmod(dir, 0755)
|
||||
err = fs.Chmod(dir, 0755)
|
||||
if err == nil {
|
||||
defer func() {
|
||||
err = os.Chmod(dir, info.Mode())
|
||||
err = fs.Chmod(dir, info.Mode())
|
||||
if err != nil {
|
||||
// We managed to change the permission bits like a
|
||||
// millisecond ago, so it'd be bizarre if we couldn't
|
||||
@@ -92,59 +89,22 @@ func InWritableDir(fn func(string) error, path string) error {
|
||||
return fn(path)
|
||||
}
|
||||
|
||||
func ExpandTilde(path string) (string, error) {
|
||||
if path == "~" {
|
||||
return getHomeDir()
|
||||
}
|
||||
|
||||
path = filepath.FromSlash(path)
|
||||
if !strings.HasPrefix(path, fmt.Sprintf("~%c", os.PathSeparator)) {
|
||||
return path, nil
|
||||
}
|
||||
|
||||
home, err := getHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(home, path[2:]), nil
|
||||
}
|
||||
|
||||
func getHomeDir() (string, error) {
|
||||
var home string
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
home = filepath.Join(os.Getenv("HomeDrive"), os.Getenv("HomePath"))
|
||||
if home == "" {
|
||||
home = os.Getenv("UserProfile")
|
||||
}
|
||||
default:
|
||||
home = os.Getenv("HOME")
|
||||
}
|
||||
|
||||
if home == "" {
|
||||
return "", errNoHome
|
||||
}
|
||||
|
||||
return home, nil
|
||||
}
|
||||
|
||||
// Tries hard to succeed on various systems by temporarily tweaking directory
|
||||
// permissions and removing the destination file when necessary.
|
||||
func withPreparedTarget(from, to string, f func() error) error {
|
||||
func withPreparedTarget(filesystem fs.Filesystem, from, to string, f func() error) error {
|
||||
// Make sure the destination directory is writeable
|
||||
toDir := filepath.Dir(to)
|
||||
if info, err := os.Stat(toDir); err == nil && info.IsDir() && info.Mode()&0200 == 0 {
|
||||
os.Chmod(toDir, 0755)
|
||||
defer os.Chmod(toDir, info.Mode())
|
||||
if info, err := filesystem.Stat(toDir); err == nil && info.IsDir() && info.Mode()&0200 == 0 {
|
||||
filesystem.Chmod(toDir, 0755)
|
||||
defer filesystem.Chmod(toDir, info.Mode())
|
||||
}
|
||||
|
||||
// On Windows, make sure the destination file is writeable (or we can't delete it)
|
||||
if runtime.GOOS == "windows" {
|
||||
os.Chmod(to, 0666)
|
||||
filesystem.Chmod(to, 0666)
|
||||
if !strings.EqualFold(from, to) {
|
||||
err := os.Remove(to)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
err := filesystem.Remove(to)
|
||||
if err != nil && !fs.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -156,13 +116,13 @@ func withPreparedTarget(from, to string, f func() error) error {
|
||||
// by dst. The file will be created if it does not already exist. If the
|
||||
// destination file exists, all it's contents will be replaced by the contents
|
||||
// of the source file.
|
||||
func copyFileContents(src, dst string) (err error) {
|
||||
in, err := os.Open(src)
|
||||
func copyFileContents(filesystem fs.Filesystem, src, dst string) (err error) {
|
||||
in, err := filesystem.Open(src)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer in.Close()
|
||||
out, err := os.Create(dst)
|
||||
out, err := filesystem.Create(dst)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -193,13 +153,3 @@ func init() {
|
||||
func IsWindowsExecutable(path string) bool {
|
||||
return execExts[strings.ToLower(filepath.Ext(path))]
|
||||
}
|
||||
|
||||
func DiskFreeBytes(path string) (free int64, err error) {
|
||||
u, err := du.Get(path)
|
||||
return u.FreeBytes, err
|
||||
}
|
||||
|
||||
func DiskFreePercentage(path string) (freePct float64, err error) {
|
||||
u, err := du.Get(path)
|
||||
return (float64(u.FreeBytes) / float64(u.TotalBytes)) * 100, err
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/fs"
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
)
|
||||
|
||||
@@ -21,6 +22,8 @@ func TestInWriteableDir(t *testing.T) {
|
||||
}
|
||||
defer os.RemoveAll("testdata")
|
||||
|
||||
fs := fs.NewFilesystem(fs.FilesystemTypeBasic, ".")
|
||||
|
||||
os.Mkdir("testdata", 0700)
|
||||
os.Mkdir("testdata/rw", 0700)
|
||||
os.Mkdir("testdata/ro", 0500)
|
||||
@@ -36,35 +39,35 @@ func TestInWriteableDir(t *testing.T) {
|
||||
|
||||
// These should succeed
|
||||
|
||||
err = osutil.InWritableDir(create, "testdata/file")
|
||||
err = osutil.InWritableDir(create, fs, "testdata/file")
|
||||
if err != nil {
|
||||
t.Error("testdata/file:", err)
|
||||
}
|
||||
err = osutil.InWritableDir(create, "testdata/rw/foo")
|
||||
err = osutil.InWritableDir(create, fs, "testdata/rw/foo")
|
||||
if err != nil {
|
||||
t.Error("testdata/rw/foo:", err)
|
||||
}
|
||||
err = osutil.InWritableDir(os.Remove, "testdata/rw/foo")
|
||||
err = osutil.InWritableDir(os.Remove, fs, "testdata/rw/foo")
|
||||
if err != nil {
|
||||
t.Error("testdata/rw/foo:", err)
|
||||
}
|
||||
|
||||
err = osutil.InWritableDir(create, "testdata/ro/foo")
|
||||
err = osutil.InWritableDir(create, fs, "testdata/ro/foo")
|
||||
if err != nil {
|
||||
t.Error("testdata/ro/foo:", err)
|
||||
}
|
||||
err = osutil.InWritableDir(os.Remove, "testdata/ro/foo")
|
||||
err = osutil.InWritableDir(os.Remove, fs, "testdata/ro/foo")
|
||||
if err != nil {
|
||||
t.Error("testdata/ro/foo:", err)
|
||||
}
|
||||
|
||||
// These should not
|
||||
|
||||
err = osutil.InWritableDir(create, "testdata/nonexistent/foo")
|
||||
err = osutil.InWritableDir(create, fs, "testdata/nonexistent/foo")
|
||||
if err == nil {
|
||||
t.Error("testdata/nonexistent/foo returned nil error")
|
||||
}
|
||||
err = osutil.InWritableDir(create, "testdata/file/foo")
|
||||
err = osutil.InWritableDir(create, fs, "testdata/file/foo")
|
||||
if err == nil {
|
||||
t.Error("testdata/file/foo returned nil error")
|
||||
}
|
||||
@@ -101,8 +104,10 @@ func TestInWritableDirWindowsRemove(t *testing.T) {
|
||||
create("testdata/windows/ro/readonly")
|
||||
os.Chmod("testdata/windows/ro/readonly", 0500)
|
||||
|
||||
fs := fs.NewFilesystem(fs.FilesystemTypeBasic, ".")
|
||||
|
||||
for _, path := range []string{"testdata/windows/ro/readonly", "testdata/windows/ro", "testdata/windows"} {
|
||||
err := osutil.InWritableDir(os.Remove, path)
|
||||
err := osutil.InWritableDir(os.Remove, fs, path)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error %s: %s", path, err)
|
||||
}
|
||||
@@ -174,6 +179,8 @@ func TestInWritableDirWindowsRename(t *testing.T) {
|
||||
create("testdata/windows/ro/readonly")
|
||||
os.Chmod("testdata/windows/ro/readonly", 0500)
|
||||
|
||||
fs := fs.NewFilesystem(fs.FilesystemTypeBasic, ".")
|
||||
|
||||
for _, path := range []string{"testdata/windows/ro/readonly", "testdata/windows/ro", "testdata/windows"} {
|
||||
err := os.Rename(path, path+"new")
|
||||
if err == nil {
|
||||
@@ -183,11 +190,11 @@ func TestInWritableDirWindowsRename(t *testing.T) {
|
||||
}
|
||||
|
||||
rename := func(path string) error {
|
||||
return osutil.Rename(path, path+"new")
|
||||
return osutil.Rename(fs, path, path+"new")
|
||||
}
|
||||
|
||||
for _, path := range []string{"testdata/windows/ro/readonly", "testdata/windows/ro", "testdata/windows"} {
|
||||
err := osutil.InWritableDir(rename, path)
|
||||
err := osutil.InWritableDir(rename, fs, path)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error %s: %s", path, err)
|
||||
}
|
||||
@@ -197,18 +204,3 @@ func TestInWritableDirWindowsRename(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiskUsage(t *testing.T) {
|
||||
free, err := osutil.DiskFreePercentage(".")
|
||||
if err != nil {
|
||||
if runtime.GOOS == "netbsd" ||
|
||||
runtime.GOOS == "openbsd" ||
|
||||
runtime.GOOS == "solaris" {
|
||||
t.Skip()
|
||||
}
|
||||
t.Errorf("Unexpected error: %s", err)
|
||||
}
|
||||
if free < 1 {
|
||||
t.Error("Disk is full?", free)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
// Copyright (C) 2016 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 https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package osutil
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func SyncFile(path string) error {
|
||||
flag := 0
|
||||
if runtime.GOOS == "windows" {
|
||||
flag = os.O_WRONLY
|
||||
}
|
||||
fd, err := os.OpenFile(path, flag, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fd.Close()
|
||||
// MacOS and Windows do not flush the disk cache
|
||||
return fd.Sync()
|
||||
}
|
||||
|
||||
func SyncDir(path string) error {
|
||||
if runtime.GOOS == "windows" {
|
||||
// not supported by Windows
|
||||
return nil
|
||||
}
|
||||
return SyncFile(path)
|
||||
}
|
||||
63
lib/osutil/tempfile.go
Normal file
63
lib/osutil/tempfile.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// 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 https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package osutil
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/fs"
|
||||
)
|
||||
|
||||
var rand uint32
|
||||
var randmu sync.Mutex
|
||||
|
||||
func reseed() uint32 {
|
||||
return uint32(time.Now().UnixNano() + int64(os.Getpid()))
|
||||
}
|
||||
|
||||
func nextSuffix() string {
|
||||
randmu.Lock()
|
||||
r := rand
|
||||
if r == 0 {
|
||||
r = reseed()
|
||||
}
|
||||
r = r*1664525 + 1013904223 // constants from Numerical Recipes
|
||||
rand = r
|
||||
randmu.Unlock()
|
||||
return strconv.Itoa(int(1e9 + r%1e9))[1:]
|
||||
}
|
||||
|
||||
// TempFile creates a new temporary file in the directory dir
|
||||
// with a name beginning with prefix, opens the file for reading
|
||||
// and writing, and returns the resulting *os.File.
|
||||
// If dir is the empty string, TempFile uses the default directory
|
||||
// for temporary files (see os.TempDir).
|
||||
// Multiple programs calling TempFile simultaneously
|
||||
// will not choose the same file. The caller can use f.Name()
|
||||
// to find the pathname of the file. It is the caller's responsibility
|
||||
// to remove the file when no longer needed.
|
||||
func TempFile(filesystem fs.Filesystem, dir, prefix string) (f fs.File, err error) {
|
||||
nconflict := 0
|
||||
for i := 0; i < 10000; i++ {
|
||||
name := filepath.Join(dir, prefix+nextSuffix())
|
||||
f, err = filesystem.OpenFile(name, fs.OptReadWrite|fs.OptCreate|fs.OptExclusive, 0600)
|
||||
if fs.IsExist(err) {
|
||||
if nconflict++; nconflict > 10 {
|
||||
randmu.Lock()
|
||||
rand = reseed()
|
||||
randmu.Unlock()
|
||||
}
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -8,9 +8,10 @@ package osutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/fs"
|
||||
)
|
||||
|
||||
// TraversesSymlinkError is an error indicating symlink traversal
|
||||
@@ -34,9 +35,10 @@ func (e NotADirectoryError) Error() string {
|
||||
// TraversesSymlink returns an error if base and any path component of name up to and
|
||||
// including filepath.Join(base, name) traverses a symlink.
|
||||
// Base and name must both be clean and name must be relative to base.
|
||||
func TraversesSymlink(base, name string) error {
|
||||
func TraversesSymlink(filesystem fs.Filesystem, name string) error {
|
||||
base := "."
|
||||
path := base
|
||||
info, err := Lstat(path)
|
||||
info, err := filesystem.Lstat(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -51,17 +53,17 @@ func TraversesSymlink(base, name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
parts := strings.Split(name, string(os.PathSeparator))
|
||||
parts := strings.Split(name, string(fs.PathSeparator))
|
||||
for _, part := range parts {
|
||||
path = filepath.Join(path, part)
|
||||
info, err := Lstat(path)
|
||||
info, err := filesystem.Lstat(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if fs.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
if info.IsSymlink() {
|
||||
return &TraversesSymlinkError{
|
||||
path: strings.TrimPrefix(path, base),
|
||||
}
|
||||
|
||||
@@ -12,17 +12,20 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/fs"
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
)
|
||||
|
||||
func TestTraversesSymlink(t *testing.T) {
|
||||
os.RemoveAll("testdata")
|
||||
defer os.RemoveAll("testdata")
|
||||
os.MkdirAll("testdata/a/b/c", 0755)
|
||||
os.Symlink("b", "testdata/a/l")
|
||||
|
||||
fs := fs.NewFilesystem(fs.FilesystemTypeBasic, "testdata")
|
||||
fs.MkdirAll("a/b/c", 0755)
|
||||
fs.CreateSymlink("b", "a/l")
|
||||
|
||||
// a/l -> b, so a/l/c should resolve by normal stat
|
||||
info, err := osutil.Lstat("testdata/a/l/c")
|
||||
info, err := fs.Lstat("a/l/c")
|
||||
if err != nil {
|
||||
t.Fatal("unexpected error", err)
|
||||
}
|
||||
@@ -52,7 +55,7 @@ func TestTraversesSymlink(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
if res := osutil.TraversesSymlink("testdata", tc.name); tc.traverses == (res == nil) {
|
||||
if res := osutil.TraversesSymlink(fs, tc.name); tc.traverses == (res == nil) {
|
||||
t.Errorf("TraversesSymlink(%q) = %v, should be %v", tc.name, res, tc.traverses)
|
||||
}
|
||||
}
|
||||
@@ -63,10 +66,11 @@ var traversesSymlinkResult error
|
||||
func BenchmarkTraversesSymlink(b *testing.B) {
|
||||
os.RemoveAll("testdata")
|
||||
defer os.RemoveAll("testdata")
|
||||
os.MkdirAll("testdata/a/b/c", 0755)
|
||||
fs := fs.NewFilesystem(fs.FilesystemTypeBasic, "testdata")
|
||||
fs.MkdirAll("a/b/c", 0755)
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
traversesSymlinkResult = osutil.TraversesSymlink("testdata", "a/b/c")
|
||||
traversesSymlinkResult = osutil.TraversesSymlink(fs, "a/b/c")
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
|
||||
Reference in New Issue
Block a user