Windows: Fix long path handling for docker build
Signed-off-by: Stefan J. Wernli <swernli@microsoft.com>
This commit is contained in:
parent
5bebf3cbac
commit
fe637416e9
9 changed files with 214 additions and 10 deletions
|
@ -8,15 +8,14 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/docker/pkg/longpath"
|
||||||
)
|
)
|
||||||
|
|
||||||
// fixVolumePathPrefix does platform specific processing to ensure that if
|
// fixVolumePathPrefix does platform specific processing to ensure that if
|
||||||
// the path being passed in is not in a volume path format, convert it to one.
|
// the path being passed in is not in a volume path format, convert it to one.
|
||||||
func fixVolumePathPrefix(srcPath string) string {
|
func fixVolumePathPrefix(srcPath string) string {
|
||||||
if !strings.HasPrefix(srcPath, `\\?\`) {
|
return longpath.AddPrefix(srcPath)
|
||||||
srcPath = `\\?\` + srcPath
|
|
||||||
}
|
|
||||||
return srcPath
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// getWalkRoot calculates the root path when performing a TarWithOptions.
|
// getWalkRoot calculates the root path when performing a TarWithOptions.
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/docker/docker/pkg/archive"
|
"github.com/docker/docker/pkg/archive"
|
||||||
|
"github.com/docker/docker/pkg/longpath"
|
||||||
)
|
)
|
||||||
|
|
||||||
// chroot is not supported by Windows
|
// chroot is not supported by Windows
|
||||||
|
@ -17,5 +18,5 @@ func invokeUnpack(decompressedArchive io.ReadCloser,
|
||||||
// Windows is different to Linux here because Windows does not support
|
// Windows is different to Linux here because Windows does not support
|
||||||
// chroot. Hence there is no point sandboxing a chrooted process to
|
// chroot. Hence there is no point sandboxing a chrooted process to
|
||||||
// do the unpack. We call inline instead within the daemon process.
|
// do the unpack. We call inline instead within the daemon process.
|
||||||
return archive.Unpack(decompressedArchive, dest, options)
|
return archive.Unpack(decompressedArchive, longpath.AddPrefix(dest), options)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,9 +5,9 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/docker/docker/pkg/archive"
|
"github.com/docker/docker/pkg/archive"
|
||||||
|
"github.com/docker/docker/pkg/longpath"
|
||||||
)
|
)
|
||||||
|
|
||||||
// applyLayerHandler parses a diff in the standard layer format from `layer`, and
|
// applyLayerHandler parses a diff in the standard layer format from `layer`, and
|
||||||
|
@ -17,9 +17,7 @@ func applyLayerHandler(dest string, layer archive.Reader, decompress bool) (size
|
||||||
dest = filepath.Clean(dest)
|
dest = filepath.Clean(dest)
|
||||||
|
|
||||||
// Ensure it is a Windows-style volume path
|
// Ensure it is a Windows-style volume path
|
||||||
if !strings.HasPrefix(dest, `\\?\`) {
|
dest = longpath.AddPrefix(dest)
|
||||||
dest = `\\?\` + dest
|
|
||||||
}
|
|
||||||
|
|
||||||
if decompress {
|
if decompress {
|
||||||
decompressed, err := archive.DecompressStream(layer)
|
decompressed, err := archive.DecompressStream(layer)
|
||||||
|
|
|
@ -5,10 +5,18 @@ package directory
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/docker/pkg/longpath"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Size walks a directory tree and returns its total size in bytes.
|
// Size walks a directory tree and returns its total size in bytes.
|
||||||
func Size(dir string) (size int64, err error) {
|
func Size(dir string) (size int64, err error) {
|
||||||
|
fixedPath, err := filepath.Abs(dir)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fixedPath = longpath.AddPrefix(fixedPath)
|
||||||
err = filepath.Walk(dir, func(d string, fileInfo os.FileInfo, e error) error {
|
err = filepath.Walk(dir, func(d string, fileInfo os.FileInfo, e error) error {
|
||||||
// Ignore directory sizes
|
// Ignore directory sizes
|
||||||
if fileInfo == nil {
|
if fileInfo == nil {
|
||||||
|
|
21
longpath/longpath.go
Normal file
21
longpath/longpath.go
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
// longpath introduces some constants and helper functions for handling long paths
|
||||||
|
// in Windows, which are expected to be prepended with `\\?\` and followed by either
|
||||||
|
// a drive letter, a UNC server\share, or a volume identifier.
|
||||||
|
|
||||||
|
package longpath
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Prefix is the longpath prefix for Windows file paths.
|
||||||
|
const Prefix = `\\?\`
|
||||||
|
|
||||||
|
// AddPrefix will add the Windows long path prefix to the path provided if
|
||||||
|
// it does not already have it.
|
||||||
|
func AddPrefix(path string) string {
|
||||||
|
if !strings.HasPrefix(path, Prefix) {
|
||||||
|
path = Prefix + path
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
Package symlink implements EvalSymlinksInScope which is an extension of filepath.EvalSymlinks
|
Package symlink implements EvalSymlinksInScope which is an extension of filepath.EvalSymlinks,
|
||||||
|
as well as a Windows long-path aware version of filepath.EvalSymlinks
|
||||||
from the [Go standard library](https://golang.org/pkg/path/filepath).
|
from the [Go standard library](https://golang.org/pkg/path/filepath).
|
||||||
|
|
||||||
The code from filepath.EvalSymlinks has been adapted in fs.go.
|
The code from filepath.EvalSymlinks has been adapted in fs.go.
|
||||||
|
|
|
@ -132,3 +132,12 @@ func evalSymlinksInScope(path, root string) (string, error) {
|
||||||
// what's happening here
|
// what's happening here
|
||||||
return filepath.Clean(root + filepath.Clean(string(filepath.Separator)+b.String())), nil
|
return filepath.Clean(root + filepath.Clean(string(filepath.Separator)+b.String())), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EvalSymlinks returns the path name after the evaluation of any symbolic
|
||||||
|
// links.
|
||||||
|
// If path is relative the result will be relative to the current directory,
|
||||||
|
// unless one of the components is an absolute symbolic link.
|
||||||
|
// This version has been updated to support long paths prepended with `\\?\`.
|
||||||
|
func EvalSymlinks(path string) (string, error) {
|
||||||
|
return evalSymlinks(path)
|
||||||
|
}
|
||||||
|
|
11
symlink/fs_unix.go
Normal file
11
symlink/fs_unix.go
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package symlink
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func evalSymlinks(path string) (string, error) {
|
||||||
|
return filepath.EvalSymlinks(path)
|
||||||
|
}
|
156
symlink/fs_windows.go
Normal file
156
symlink/fs_windows.go
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
package symlink
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/docker/docker/pkg/longpath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func toShort(path string) (string, error) {
|
||||||
|
p, err := syscall.UTF16FromString(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
b := p // GetShortPathName says we can reuse buffer
|
||||||
|
n, err := syscall.GetShortPathName(&p[0], &b[0], uint32(len(b)))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if n > uint32(len(b)) {
|
||||||
|
b = make([]uint16, n)
|
||||||
|
n, err = syscall.GetShortPathName(&p[0], &b[0], uint32(len(b)))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return syscall.UTF16ToString(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func toLong(path string) (string, error) {
|
||||||
|
p, err := syscall.UTF16FromString(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
b := p // GetLongPathName says we can reuse buffer
|
||||||
|
n, err := syscall.GetLongPathName(&p[0], &b[0], uint32(len(b)))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if n > uint32(len(b)) {
|
||||||
|
b = make([]uint16, n)
|
||||||
|
n, err = syscall.GetLongPathName(&p[0], &b[0], uint32(len(b)))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b = b[:n]
|
||||||
|
return syscall.UTF16ToString(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func evalSymlinks(path string) (string, error) {
|
||||||
|
path, err := walkSymlinks(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := toShort(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
p, err = toLong(p)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
// syscall.GetLongPathName does not change the case of the drive letter,
|
||||||
|
// but the result of EvalSymlinks must be unique, so we have
|
||||||
|
// EvalSymlinks(`c:\a`) == EvalSymlinks(`C:\a`).
|
||||||
|
// Make drive letter upper case.
|
||||||
|
if len(p) >= 2 && p[1] == ':' && 'a' <= p[0] && p[0] <= 'z' {
|
||||||
|
p = string(p[0]+'A'-'a') + p[1:]
|
||||||
|
} else if len(p) >= 6 && p[5] == ':' && 'a' <= p[4] && p[4] <= 'z' {
|
||||||
|
p = p[:3] + string(p[4]+'A'-'a') + p[5:]
|
||||||
|
}
|
||||||
|
return filepath.Clean(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const utf8RuneSelf = 0x80
|
||||||
|
|
||||||
|
func walkSymlinks(path string) (string, error) {
|
||||||
|
const maxIter = 255
|
||||||
|
originalPath := path
|
||||||
|
// consume path by taking each frontmost path element,
|
||||||
|
// expanding it if it's a symlink, and appending it to b
|
||||||
|
var b bytes.Buffer
|
||||||
|
for n := 0; path != ""; n++ {
|
||||||
|
if n > maxIter {
|
||||||
|
return "", errors.New("EvalSymlinks: too many links in " + originalPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A path beginnging with `\\?\` represents the root, so automatically
|
||||||
|
// skip that part and begin processing the next segment.
|
||||||
|
if strings.HasPrefix(path, longpath.Prefix) {
|
||||||
|
b.WriteString(longpath.Prefix)
|
||||||
|
path = path[4:]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// find next path component, p
|
||||||
|
var i = -1
|
||||||
|
for j, c := range path {
|
||||||
|
if c < utf8RuneSelf && os.IsPathSeparator(uint8(c)) {
|
||||||
|
i = j
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var p string
|
||||||
|
if i == -1 {
|
||||||
|
p, path = path, ""
|
||||||
|
} else {
|
||||||
|
p, path = path[:i], path[i+1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
if p == "" {
|
||||||
|
if b.Len() == 0 {
|
||||||
|
// must be absolute path
|
||||||
|
b.WriteRune(filepath.Separator)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is the first segment after the long path prefix, accept the
|
||||||
|
// current segment as a volume root or UNC share and move on to the next.
|
||||||
|
if b.String() == longpath.Prefix {
|
||||||
|
b.WriteString(p)
|
||||||
|
b.WriteRune(filepath.Separator)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fi, err := os.Lstat(b.String() + p)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if fi.Mode()&os.ModeSymlink == 0 {
|
||||||
|
b.WriteString(p)
|
||||||
|
if path != "" || (b.Len() == 2 && len(p) == 2 && p[1] == ':') {
|
||||||
|
b.WriteRune(filepath.Separator)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// it's a symlink, put it at the front of path
|
||||||
|
dest, err := os.Readlink(b.String() + p)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if filepath.IsAbs(dest) || os.IsPathSeparator(dest[0]) {
|
||||||
|
b.Reset()
|
||||||
|
}
|
||||||
|
path = dest + string(filepath.Separator) + path
|
||||||
|
}
|
||||||
|
return filepath.Clean(b.String()), nil
|
||||||
|
}
|
Loading…
Reference in a new issue