mv internal lib
This commit is contained in:
101
lib/osutil/atomic.go
Normal file
101
lib/osutil/atomic.go
Normal file
@@ -0,0 +1,101 @@
|
||||
// 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 (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrClosed = errors.New("write to closed writer")
|
||||
TempPrefix = ".syncthing.tmp."
|
||||
)
|
||||
|
||||
// An AtomicWriter is an *os.File that writes to a temporary file in the same
|
||||
// directory as the final path. On successfull Close the file is renamed to
|
||||
// it's final path. Any error on Write or during Close is accumulated and
|
||||
// returned on Close, so a lazy user can ignore errors until Close.
|
||||
type AtomicWriter struct {
|
||||
path string
|
||||
next *os.File
|
||||
err error
|
||||
}
|
||||
|
||||
// CreateAtomic is like os.Create with a FileMode, except a temporary file
|
||||
// name is used instead of the given name.
|
||||
func CreateAtomic(path string, mode os.FileMode) (*AtomicWriter, error) {
|
||||
fd, err := ioutil.TempFile(filepath.Dir(path), TempPrefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := os.Chmod(fd.Name(), mode); err != nil {
|
||||
fd.Close()
|
||||
os.Remove(fd.Name())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
w := &AtomicWriter{
|
||||
path: path,
|
||||
next: fd,
|
||||
}
|
||||
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// Write is like io.Writer, but is a no-op on an already failed AtomicWriter.
|
||||
func (w *AtomicWriter) Write(bs []byte) (int, error) {
|
||||
if w.err != nil {
|
||||
return 0, w.err
|
||||
}
|
||||
n, err := w.next.Write(bs)
|
||||
if err != nil {
|
||||
w.err = err
|
||||
w.next.Close()
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Close closes the temporary file and renames it to the final path. It is
|
||||
// invalid to call Write() or Close() after Close().
|
||||
func (w *AtomicWriter) Close() error {
|
||||
if w.err != nil {
|
||||
return w.err
|
||||
}
|
||||
|
||||
// Try to not leave temp file around, but ignore error.
|
||||
defer os.Remove(w.next.Name())
|
||||
|
||||
if err := w.next.Close(); err != nil {
|
||||
w.err = err
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove the destination file, on Windows only. If it fails, and not due
|
||||
// to the file not existing, we won't be able to complete the rename
|
||||
// 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) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.Rename(w.next.Name(), w.path); err != nil {
|
||||
w.err = err
|
||||
return err
|
||||
}
|
||||
|
||||
// Set w.err to return appropriately for any future operations.
|
||||
w.err = ErrClosed
|
||||
|
||||
return nil
|
||||
}
|
||||
85
lib/osutil/atomic_test.go
Normal file
85
lib/osutil/atomic_test.go
Normal file
@@ -0,0 +1,85 @@
|
||||
// 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"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCreateAtomicCreate(t *testing.T) {
|
||||
os.RemoveAll("testdata")
|
||||
defer os.RemoveAll("testdata")
|
||||
|
||||
if err := os.Mkdir("testdata", 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
w, err := CreateAtomic("testdata/file", 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
n, err := w.Write([]byte("hello"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n != 5 {
|
||||
t.Fatal("written bytes", n, "!= 5")
|
||||
}
|
||||
|
||||
if _, err := ioutil.ReadFile("testdata/file"); err == nil {
|
||||
t.Fatal("file should not exist")
|
||||
}
|
||||
|
||||
if err := w.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
bs, err := ioutil.ReadFile("testdata/file")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(bs, []byte("hello")) {
|
||||
t.Error("incorrect data")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateAtomicReplace(t *testing.T) {
|
||||
os.RemoveAll("testdata")
|
||||
defer os.RemoveAll("testdata")
|
||||
|
||||
if err := os.Mkdir("testdata", 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile("testdata/file", []byte("some old data"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
w, err := CreateAtomic("testdata/file", 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := w.Write([]byte("hello")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
bs, err := ioutil.ReadFile("testdata/file")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(bs, []byte("hello")) {
|
||||
t.Error("incorrect data")
|
||||
}
|
||||
}
|
||||
17
lib/osutil/filenames_darwin.go
Normal file
17
lib/osutil/filenames_darwin.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// 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 osutil
|
||||
|
||||
import "golang.org/x/text/unicode/norm"
|
||||
|
||||
func NormalizedFilename(s string) string {
|
||||
return norm.NFC.String(s)
|
||||
}
|
||||
|
||||
func NativeFilename(s string) string {
|
||||
return norm.NFD.String(s)
|
||||
}
|
||||
19
lib/osutil/filenames_unix.go
Normal file
19
lib/osutil/filenames_unix.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/.
|
||||
|
||||
// +build !windows,!darwin
|
||||
|
||||
package osutil
|
||||
|
||||
import "golang.org/x/text/unicode/norm"
|
||||
|
||||
func NormalizedFilename(s string) string {
|
||||
return norm.NFC.String(s)
|
||||
}
|
||||
|
||||
func NativeFilename(s string) string {
|
||||
return s
|
||||
}
|
||||
21
lib/osutil/filenames_windows.go
Normal file
21
lib/osutil/filenames_windows.go
Normal file
@@ -0,0 +1,21 @@
|
||||
// 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 osutil
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"golang.org/x/text/unicode/norm"
|
||||
)
|
||||
|
||||
func NormalizedFilename(s string) string {
|
||||
return norm.NFC.String(filepath.ToSlash(s))
|
||||
}
|
||||
|
||||
func NativeFilename(s string) string {
|
||||
return filepath.FromSlash(s)
|
||||
}
|
||||
17
lib/osutil/glob_unix.go
Normal file
17
lib/osutil/glob_unix.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// 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/.
|
||||
|
||||
// +build !windows
|
||||
|
||||
package osutil
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func Glob(pattern string) (matches []string, err error) {
|
||||
return filepath.Glob(pattern)
|
||||
}
|
||||
93
lib/osutil/glob_windows.go
Normal file
93
lib/osutil/glob_windows.go
Normal file
@@ -0,0 +1,93 @@
|
||||
// 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/.
|
||||
|
||||
// +build windows
|
||||
|
||||
package osutil
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"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(pattern)
|
||||
switch dir {
|
||||
case "":
|
||||
dir = "."
|
||||
case string(filepath.Separator):
|
||||
// nothing
|
||||
default:
|
||||
dir = dir[0 : len(dir)-1] // chop off trailing separator
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
19
lib/osutil/hidden_unix.go
Normal file
19
lib/osutil/hidden_unix.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/.
|
||||
|
||||
// +build !windows
|
||||
|
||||
package osutil
|
||||
|
||||
func HideFile(path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func ShowFile(path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func HideConsole() {}
|
||||
52
lib/osutil/hidden_windows.go
Normal file
52
lib/osutil/hidden_windows.go
Normal file
@@ -0,0 +1,52 @@
|
||||
// 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/.
|
||||
|
||||
// +build windows
|
||||
|
||||
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")
|
||||
if getConsoleWindow.Find() == nil && showWindow.Find() == nil {
|
||||
hwnd, _, _ := getConsoleWindow.Call()
|
||||
if hwnd != 0 {
|
||||
showWindow.Call(hwnd, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
30
lib/osutil/lan_unix.go
Normal file
30
lib/osutil/lan_unix.go
Normal file
@@ -0,0 +1,30 @@
|
||||
// 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/.
|
||||
|
||||
// +build !windows
|
||||
|
||||
package osutil
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
func GetLans() ([]*net.IPNet, error) {
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nets := make([]*net.IPNet, 0, len(addrs))
|
||||
|
||||
for _, addr := range addrs {
|
||||
net, ok := addr.(*net.IPNet)
|
||||
if ok {
|
||||
nets = append(nets, net)
|
||||
}
|
||||
}
|
||||
return nets, nil
|
||||
}
|
||||
83
lib/osutil/lan_windows.go
Normal file
83
lib/osutil/lan_windows.go
Normal file
@@ -0,0 +1,83 @@
|
||||
// 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/.
|
||||
|
||||
// +build windows
|
||||
|
||||
package osutil
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// Modified version of:
|
||||
// http://stackoverflow.com/questions/23529663/how-to-get-all-addresses-and-masks-from-local-interfaces-in-go
|
||||
// v4 only!
|
||||
|
||||
func getAdapterList() (*syscall.IpAdapterInfo, error) {
|
||||
b := make([]byte, 10240)
|
||||
l := uint32(len(b))
|
||||
a := (*syscall.IpAdapterInfo)(unsafe.Pointer(&b[0]))
|
||||
// TODO(mikio): GetAdaptersInfo returns IP_ADAPTER_INFO that
|
||||
// contains IPv4 address list only. We should use another API
|
||||
// for fetching IPv6 stuff from the kernel.
|
||||
err := syscall.GetAdaptersInfo(a, &l)
|
||||
if err == syscall.ERROR_BUFFER_OVERFLOW {
|
||||
b = make([]byte, l)
|
||||
a = (*syscall.IpAdapterInfo)(unsafe.Pointer(&b[0]))
|
||||
err = syscall.GetAdaptersInfo(a, &l)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, os.NewSyscallError("GetAdaptersInfo", err)
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func GetLans() ([]*net.IPNet, error) {
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nets := make([]*net.IPNet, 0, len(ifaces))
|
||||
|
||||
aList, err := getAdapterList()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, ifi := range ifaces {
|
||||
for ai := aList; ai != nil; ai = ai.Next {
|
||||
index := ai.Index
|
||||
|
||||
if ifi.Index == int(index) {
|
||||
ipl := &ai.IpAddressList
|
||||
for ; ipl != nil; ipl = ipl.Next {
|
||||
ipStr := strings.Trim(string(ipl.IpAddress.String[:]), "\x00")
|
||||
maskStr := strings.Trim(string(ipl.IpMask.String[:]), "\x00")
|
||||
ip := net.ParseIP(ipStr)
|
||||
maskip := net.ParseIP(maskStr)
|
||||
if ip.IsUnspecified() || maskip.IsUnspecified() {
|
||||
continue
|
||||
}
|
||||
nets = append(nets, &net.IPNet{
|
||||
IP: ip,
|
||||
Mask: net.IPv4Mask(
|
||||
maskip[net.IPv6len-net.IPv4len],
|
||||
maskip[net.IPv6len-net.IPv4len+1],
|
||||
maskip[net.IPv6len-net.IPv4len+2],
|
||||
maskip[net.IPv6len-net.IPv4len+3],
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nets, err
|
||||
}
|
||||
29
lib/osutil/lstat_broken.go
Normal file
29
lib/osutil/lstat_broken.go
Normal file
@@ -0,0 +1,29 @@
|
||||
// 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/.
|
||||
|
||||
// +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
|
||||
}
|
||||
15
lib/osutil/lstat_ok.go
Normal file
15
lib/osutil/lstat_ok.go
Normal file
@@ -0,0 +1,15 @@
|
||||
// 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/.
|
||||
|
||||
// +build !linux,!android
|
||||
|
||||
package osutil
|
||||
|
||||
import "os"
|
||||
|
||||
func Lstat(name string) (fi os.FileInfo, err error) {
|
||||
return os.Lstat(name)
|
||||
}
|
||||
17
lib/osutil/mkdirall.go
Normal file
17
lib/osutil/mkdirall.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// 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/.
|
||||
|
||||
// +build !windows
|
||||
|
||||
package osutil
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
func MkdirAll(path string, perm os.FileMode) error {
|
||||
return os.MkdirAll(path, perm)
|
||||
}
|
||||
65
lib/osutil/mkdirall_windows.go
Normal file
65
lib/osutil/mkdirall_windows.go
Normal file
@@ -0,0 +1,65 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// 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{"mkdir", path, 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
|
||||
}
|
||||
212
lib/osutil/osutil.go
Normal file
212
lib/osutil/osutil.go
Normal file
@@ -0,0 +1,212 @@
|
||||
// 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 osutil implements utilities for native OS support.
|
||||
package osutil
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"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()
|
||||
|
||||
// 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 {
|
||||
renameLock.Lock()
|
||||
defer renameLock.Unlock()
|
||||
|
||||
return withPreparedTarget(from, to, func() error {
|
||||
return os.Rename(from, to)
|
||||
})
|
||||
}
|
||||
|
||||
// Rename moves a temporary file to it's final place.
|
||||
// Will make sure to delete the from file if the operation fails, so use only
|
||||
// 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 {
|
||||
// Don't leave a dangling temp file in case of rename error
|
||||
if !(runtime.GOOS == "windows" && strings.EqualFold(from, to)) {
|
||||
defer os.Remove(from)
|
||||
}
|
||||
return TryRename(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)
|
||||
})
|
||||
}
|
||||
|
||||
// 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 {
|
||||
dir := filepath.Dir(path)
|
||||
info, err := os.Stat(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return errors.New("Not a directory: " + path)
|
||||
}
|
||||
if info.Mode()&0200 == 0 {
|
||||
// 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)
|
||||
if err == nil {
|
||||
defer func() {
|
||||
err = os.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
|
||||
// change it back.
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
return fn(path)
|
||||
}
|
||||
|
||||
// Remove removes the given path. On Windows, removes the read-only attribute
|
||||
// from the target prior to deletion.
|
||||
func Remove(path string) error {
|
||||
if runtime.GOOS == "windows" {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.Mode()&0200 == 0 {
|
||||
os.Chmod(path, 0700)
|
||||
}
|
||||
}
|
||||
return os.Remove(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 {
|
||||
// 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())
|
||||
}
|
||||
|
||||
// On Windows, make sure the destination file is writeable (or we can't delete it)
|
||||
if runtime.GOOS == "windows" {
|
||||
os.Chmod(to, 0666)
|
||||
if !strings.EqualFold(from, to) {
|
||||
err := os.Remove(to)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return f()
|
||||
}
|
||||
|
||||
// copyFileContents copies the contents of the file named src to the file named
|
||||
// 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)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer in.Close()
|
||||
out, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
cerr := out.Close()
|
||||
if err == nil {
|
||||
err = cerr
|
||||
}
|
||||
}()
|
||||
if _, err = io.Copy(out, in); err != nil {
|
||||
return
|
||||
}
|
||||
err = out.Sync()
|
||||
return
|
||||
}
|
||||
|
||||
var execExts map[string]bool
|
||||
|
||||
func init() {
|
||||
// PATHEXT contains a list of executable file extensions, on Windows
|
||||
pathext := filepath.SplitList(os.Getenv("PATHEXT"))
|
||||
// We want the extensions in execExts to be lower case
|
||||
execExts = make(map[string]bool, len(pathext))
|
||||
for _, ext := range pathext {
|
||||
execExts[strings.ToLower(ext)] = true
|
||||
}
|
||||
}
|
||||
|
||||
// IsWindowsExecutable returns true if the given path has an extension that is
|
||||
// in the list of executable extensions.
|
||||
func IsWindowsExecutable(path string) bool {
|
||||
return execExts[strings.ToLower(filepath.Ext(path))]
|
||||
}
|
||||
166
lib/osutil/osutil_test.go
Normal file
166
lib/osutil/osutil_test.go
Normal file
@@ -0,0 +1,166 @@
|
||||
// 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 osutil_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
)
|
||||
|
||||
func TestInWriteableDir(t *testing.T) {
|
||||
err := os.RemoveAll("testdata")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll("testdata")
|
||||
|
||||
os.Mkdir("testdata", 0700)
|
||||
os.Mkdir("testdata/rw", 0700)
|
||||
os.Mkdir("testdata/ro", 0500)
|
||||
|
||||
create := func(name string) error {
|
||||
fd, err := os.Create(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fd.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// These should succeed
|
||||
|
||||
err = osutil.InWritableDir(create, "testdata/file")
|
||||
if err != nil {
|
||||
t.Error("testdata/file:", err)
|
||||
}
|
||||
err = osutil.InWritableDir(create, "testdata/rw/foo")
|
||||
if err != nil {
|
||||
t.Error("testdata/rw/foo:", err)
|
||||
}
|
||||
err = osutil.InWritableDir(os.Remove, "testdata/rw/foo")
|
||||
if err != nil {
|
||||
t.Error("testdata/rw/foo:", err)
|
||||
}
|
||||
|
||||
err = osutil.InWritableDir(create, "testdata/ro/foo")
|
||||
if err != nil {
|
||||
t.Error("testdata/ro/foo:", err)
|
||||
}
|
||||
err = osutil.InWritableDir(os.Remove, "testdata/ro/foo")
|
||||
if err != nil {
|
||||
t.Error("testdata/ro/foo:", err)
|
||||
}
|
||||
|
||||
// These should not
|
||||
|
||||
err = osutil.InWritableDir(create, "testdata/nonexistent/foo")
|
||||
if err == nil {
|
||||
t.Error("testdata/nonexistent/foo returned nil error")
|
||||
}
|
||||
err = osutil.InWritableDir(create, "testdata/file/foo")
|
||||
if err == nil {
|
||||
t.Error("testdata/file/foo returned nil error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInWritableDirWindowsRemove(t *testing.T) {
|
||||
if runtime.GOOS != "windows" {
|
||||
t.Skipf("Tests not required")
|
||||
return
|
||||
}
|
||||
|
||||
err := os.RemoveAll("testdata")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll("testdata")
|
||||
|
||||
create := func(name string) error {
|
||||
fd, err := os.Create(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fd.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
os.Mkdir("testdata", 0700)
|
||||
|
||||
os.Mkdir("testdata/windows", 0500)
|
||||
os.Mkdir("testdata/windows/ro", 0500)
|
||||
create("testdata/windows/ro/readonly")
|
||||
os.Chmod("testdata/windows/ro/readonly", 0500)
|
||||
|
||||
for _, path := range []string{"testdata/windows/ro/readonly", "testdata/windows/ro", "testdata/windows"} {
|
||||
err := os.Remove(path)
|
||||
if err == nil {
|
||||
t.Errorf("Expected error %s", path)
|
||||
}
|
||||
}
|
||||
|
||||
for _, path := range []string{"testdata/windows/ro/readonly", "testdata/windows/ro", "testdata/windows"} {
|
||||
err := osutil.InWritableDir(osutil.Remove, path)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error %s: %s", path, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInWritableDirWindowsRename(t *testing.T) {
|
||||
if runtime.GOOS != "windows" {
|
||||
t.Skipf("Tests not required")
|
||||
return
|
||||
}
|
||||
|
||||
err := os.RemoveAll("testdata")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll("testdata")
|
||||
|
||||
create := func(name string) error {
|
||||
fd, err := os.Create(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fd.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
os.Mkdir("testdata", 0700)
|
||||
|
||||
os.Mkdir("testdata/windows", 0500)
|
||||
os.Mkdir("testdata/windows/ro", 0500)
|
||||
create("testdata/windows/ro/readonly")
|
||||
os.Chmod("testdata/windows/ro/readonly", 0500)
|
||||
|
||||
for _, path := range []string{"testdata/windows/ro/readonly", "testdata/windows/ro", "testdata/windows"} {
|
||||
err := os.Rename(path, path+"new")
|
||||
if err == nil {
|
||||
t.Skipf("seem like this test doesn't work here")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
rename := func(path string) error {
|
||||
return osutil.Rename(path, path+"new")
|
||||
}
|
||||
|
||||
for _, path := range []string{"testdata/windows/ro/readonly", "testdata/windows/ro", "testdata/windows"} {
|
||||
err := osutil.InWritableDir(rename, path)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error %s: %s", path, err)
|
||||
}
|
||||
_, err = os.Stat(path + "new")
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error %s: %s", path, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
48
lib/osutil/replacingwriter.go
Normal file
48
lib/osutil/replacingwriter.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 osutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
)
|
||||
|
||||
type ReplacingWriter struct {
|
||||
Writer io.Writer
|
||||
From byte
|
||||
To []byte
|
||||
}
|
||||
|
||||
func (w ReplacingWriter) Write(bs []byte) (int, error) {
|
||||
var n, written int
|
||||
var err error
|
||||
|
||||
newlineIdx := bytes.IndexByte(bs, w.From)
|
||||
for newlineIdx >= 0 {
|
||||
n, err = w.Writer.Write(bs[:newlineIdx])
|
||||
written += n
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if len(w.To) > 0 {
|
||||
n, err := w.Writer.Write(w.To)
|
||||
if n == len(w.To) {
|
||||
written++
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
bs = bs[newlineIdx+1:]
|
||||
newlineIdx = bytes.IndexByte(bs, w.From)
|
||||
}
|
||||
|
||||
n, err = w.Writer.Write(bs)
|
||||
written += n
|
||||
|
||||
return written, err
|
||||
}
|
||||
44
lib/osutil/replacingwriter_test.go
Normal file
44
lib/osutil/replacingwriter_test.go
Normal file
@@ -0,0 +1,44 @@
|
||||
// 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 osutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var testcases = []struct {
|
||||
from byte
|
||||
to []byte
|
||||
a, b string
|
||||
}{
|
||||
{'\n', []byte{'\r', '\n'}, "", ""},
|
||||
{'\n', []byte{'\r', '\n'}, "foo", "foo"},
|
||||
{'\n', []byte{'\r', '\n'}, "foo\n", "foo\r\n"},
|
||||
{'\n', []byte{'\r', '\n'}, "foo\nbar", "foo\r\nbar"},
|
||||
{'\n', []byte{'\r', '\n'}, "foo\nbar\nbaz", "foo\r\nbar\r\nbaz"},
|
||||
{'\n', []byte{'\r', '\n'}, "\nbar", "\r\nbar"},
|
||||
{'o', []byte{'x', 'l', 'r'}, "\nfoo", "\nfxlrxlr"},
|
||||
{'o', nil, "\nfoo", "\nf"},
|
||||
{'f', []byte{}, "\nfoo", "\noo"},
|
||||
}
|
||||
|
||||
func TestReplacingWriter(t *testing.T) {
|
||||
for _, tc := range testcases {
|
||||
var buf bytes.Buffer
|
||||
w := ReplacingWriter{
|
||||
Writer: &buf,
|
||||
From: tc.from,
|
||||
To: tc.to,
|
||||
}
|
||||
fmt.Fprint(w, tc.a)
|
||||
if buf.String() != tc.b {
|
||||
t.Errorf("%q != %q", buf.String(), tc.b)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user