Skip to content

Commit

Permalink
Windows: Fix long path handling for docker build
Browse files Browse the repository at this point in the history
Signed-off-by: Stefan J. Wernli <[email protected]>
  • Loading branch information
swernli committed Sep 15, 2015
1 parent 7ce270d commit 9b648df
Show file tree
Hide file tree
Showing 15 changed files with 272 additions and 16 deletions.
9 changes: 5 additions & 4 deletions builder/internals.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
"github.com/docker/docker/pkg/progressreader"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/docker/pkg/stringutils"
"github.com/docker/docker/pkg/symlink"
"github.com/docker/docker/pkg/system"
"github.com/docker/docker/pkg/tarsum"
"github.com/docker/docker/pkg/urlutil"
Expand All @@ -42,7 +43,7 @@ import (
)

func (b *builder) readContext(context io.Reader) (err error) {
tmpdirPath, err := ioutil.TempDir("", "docker-build")
tmpdirPath, err := getTempDir("", "docker-build")
if err != nil {
return
}
Expand Down Expand Up @@ -305,7 +306,7 @@ func calcCopyInfo(b *builder, cmdName string, cInfos *[]*copyInfo, origPath stri
}

// Create a tmp dir
tmpDirName, err := ioutil.TempDir(b.contextPath, "docker-remote")
tmpDirName, err := getTempDir(b.contextPath, "docker-remote")
if err != nil {
return err
}
Expand Down Expand Up @@ -684,14 +685,14 @@ func (b *builder) run(c *daemon.Container) error {

func (b *builder) checkPathForAddition(orig string) error {
origPath := filepath.Join(b.contextPath, orig)
origPath, err := filepath.EvalSymlinks(origPath)
origPath, err := symlink.EvalSymlinks(origPath)
if err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("%s: no such file or directory", orig)
}
return err
}
contextPath, err := filepath.EvalSymlinks(b.contextPath)
contextPath, err := symlink.EvalSymlinks(b.contextPath)
if err != nil {
return err
}
Expand Down
5 changes: 5 additions & 0 deletions builder/internals_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@
package builder

import (
"io/ioutil"
"os"
"path/filepath"
)

func getTempDir(dir, prefix string) (string, error) {
return ioutil.TempDir(dir, prefix)
}

func fixPermissions(source, destination string, uid, gid int, destExisted bool) error {
// If the destination didn't already exist, or the destination isn't a
// directory, then we should Lchown the destination. Otherwise, we shouldn't
Expand Down
14 changes: 14 additions & 0 deletions builder/internals_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@

package builder

import (
"io/ioutil"

"github.com/docker/docker/pkg/longpath"
)

func getTempDir(dir, prefix string) (string, error) {
tempDir, err := ioutil.TempDir(dir, prefix)
if err != nil {
return "", err
}
return longpath.AddPrefix(tempDir), nil
}

func fixPermissions(source, destination string, uid, gid int, destExisted bool) error {
// chown is not supported on Windows
return nil
Expand Down
7 changes: 3 additions & 4 deletions pkg/archive/archive_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,14 @@ import (
"os"
"path/filepath"
"strings"

"github.com/docker/docker/pkg/longpath"
)

// 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.
func fixVolumePathPrefix(srcPath string) string {
if !strings.HasPrefix(srcPath, `\\?\`) {
srcPath = `\\?\` + srcPath
}
return srcPath
return longpath.AddPrefix(srcPath)
}

// getWalkRoot calculates the root path when performing a TarWithOptions.
Expand Down
3 changes: 2 additions & 1 deletion pkg/chrootarchive/archive_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"io"

"github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/longpath"
)

// chroot is not supported by Windows
Expand All @@ -17,5 +18,5 @@ func invokeUnpack(decompressedArchive io.ReadCloser,
// Windows is different to Linux here because Windows does not support
// chroot. Hence there is no point sandboxing a chrooted process to
// 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)
}
6 changes: 2 additions & 4 deletions pkg/chrootarchive/diff_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import (
"io/ioutil"
"os"
"path/filepath"
"strings"

"github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/longpath"
)

// applyLayerHandler parses a diff in the standard layer format from `layer`, and
Expand All @@ -17,9 +17,7 @@ func applyLayerHandler(dest string, layer archive.Reader, decompress bool) (size
dest = filepath.Clean(dest)

// Ensure it is a Windows-style volume path
if !strings.HasPrefix(dest, `\\?\`) {
dest = `\\?\` + dest
}
dest = longpath.AddPrefix(dest)

if decompress {
decompressed, err := archive.DecompressStream(layer)
Expand Down
8 changes: 8 additions & 0 deletions pkg/directory/directory_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,18 @@ package directory
import (
"os"
"path/filepath"
"strings"

"github.com/docker/docker/pkg/longpath"
)

// Size walks a directory tree and returns its total size in bytes.
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 {
// Ignore directory sizes
if fileInfo == nil {
Expand Down
21 changes: 21 additions & 0 deletions pkg/longpath/longpath.go
Original file line number Diff line number Diff line change
@@ -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
}
3 changes: 2 additions & 1 deletion pkg/symlink/README.md
Original file line number Diff line number Diff line change
@@ -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).

The code from filepath.EvalSymlinks has been adapted in fs.go.
Expand Down
9 changes: 9 additions & 0 deletions pkg/symlink/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,12 @@ func evalSymlinksInScope(path, root string) (string, error) {
// what's happening here
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 changes: 11 additions & 0 deletions pkg/symlink/fs_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// +build !windows

package symlink

import (
"path/filepath"
)

func evalSymlinks(path string) (string, error) {
return filepath.EvalSymlinks(path)
}
156 changes: 156 additions & 0 deletions pkg/symlink/fs_windows.go
Original file line number Diff line number Diff line change
@@ -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
}
8 changes: 6 additions & 2 deletions utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,9 +199,13 @@ func ReplaceOrAppendEnvValues(defaults, overrides []string) []string {
// can be read and returns an error if some files can't be read
// symlinks which point to non-existing files don't trigger an error
func ValidateContextDirectory(srcPath string, excludes []string) error {
return filepath.Walk(filepath.Join(srcPath, "."), func(filePath string, f os.FileInfo, err error) error {
contextRoot, err := getContextRoot(srcPath)
if err != nil {
return err
}
return filepath.Walk(contextRoot, func(filePath string, f os.FileInfo, err error) error {
// skip this directory/file if it's not in the path, it won't get added to the context
if relFilePath, err := filepath.Rel(srcPath, filePath); err != nil {
if relFilePath, err := filepath.Rel(contextRoot, filePath); err != nil {
return err
} else if skip, err := fileutils.Matches(relFilePath, excludes); err != nil {
return err
Expand Down
11 changes: 11 additions & 0 deletions utils/utils_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// +build !windows

package utils

import (
"path/filepath"
)

func getContextRoot(srcPath string) (string, error) {
return filepath.Join(srcPath, "."), nil
}
Loading

0 comments on commit 9b648df

Please sign in to comment.