github.com/vmware/govmomi@v0.37.2/toolbox/hgfs/archive.go (about)

     1  /*
     2  Copyright (c) 2017 VMware, Inc. All Rights Reserved.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package hgfs
    18  
    19  import (
    20  	"archive/tar"
    21  	"bufio"
    22  	"bytes"
    23  	"compress/gzip"
    24  	"io"
    25  	"log"
    26  	"math"
    27  	"net/url"
    28  	"os"
    29  	"path/filepath"
    30  	"strings"
    31  	"sync"
    32  	"time"
    33  
    34  	"github.com/vmware/govmomi/toolbox/vix"
    35  )
    36  
    37  // ArchiveScheme is the default scheme used to register the archive FileHandler
    38  var ArchiveScheme = "archive"
    39  
    40  // ArchiveHandler implements a FileHandler for transferring directories.
    41  type ArchiveHandler struct {
    42  	Read  func(*url.URL, *tar.Reader) error
    43  	Write func(*url.URL, *tar.Writer) error
    44  }
    45  
    46  // NewArchiveHandler returns a FileHandler implementation for transferring directories using gzip'd tar files.
    47  func NewArchiveHandler() FileHandler {
    48  	return &ArchiveHandler{
    49  		Read:  archiveRead,
    50  		Write: archiveWrite,
    51  	}
    52  }
    53  
    54  // Stat implements FileHandler.Stat
    55  func (*ArchiveHandler) Stat(u *url.URL) (os.FileInfo, error) {
    56  	switch u.Query().Get("format") {
    57  	case "", "tar", "tgz":
    58  		// ok
    59  	default:
    60  		log.Printf("unknown archive format: %q", u)
    61  		return nil, vix.Error(vix.InvalidArg)
    62  	}
    63  
    64  	return &archive{
    65  		name: u.Path,
    66  		size: math.MaxInt64,
    67  	}, nil
    68  }
    69  
    70  // Open implements FileHandler.Open
    71  func (h *ArchiveHandler) Open(u *url.URL, mode int32) (File, error) {
    72  	switch mode {
    73  	case OpenModeReadOnly:
    74  		return h.newArchiveFromGuest(u)
    75  	case OpenModeWriteOnly:
    76  		return h.newArchiveToGuest(u)
    77  	default:
    78  		return nil, os.ErrNotExist
    79  	}
    80  }
    81  
    82  // archive implements the hgfs.File and os.FileInfo interfaces.
    83  type archive struct {
    84  	name string
    85  	size int64
    86  	done func() error
    87  
    88  	io.Reader
    89  	io.Writer
    90  }
    91  
    92  // Name implementation of the os.FileInfo interface method.
    93  func (a *archive) Name() string {
    94  	return a.name
    95  }
    96  
    97  // Size implementation of the os.FileInfo interface method.
    98  func (a *archive) Size() int64 {
    99  	return a.size
   100  }
   101  
   102  // Mode implementation of the os.FileInfo interface method.
   103  func (a *archive) Mode() os.FileMode {
   104  	return 0600
   105  }
   106  
   107  // ModTime implementation of the os.FileInfo interface method.
   108  func (a *archive) ModTime() time.Time {
   109  	return time.Now()
   110  }
   111  
   112  // IsDir implementation of the os.FileInfo interface method.
   113  func (a *archive) IsDir() bool {
   114  	return false
   115  }
   116  
   117  // Sys implementation of the os.FileInfo interface method.
   118  func (a *archive) Sys() interface{} {
   119  	return nil
   120  }
   121  
   122  // The trailer is required since TransferFromGuest requires a Content-Length,
   123  // which toolbox doesn't know ahead of time as the gzip'd tarball never touches the disk.
   124  // HTTP clients need to be aware of this and stop reading when they see the 2nd gzip header.
   125  var gzipHeader = []byte{0x1f, 0x8b, 0x08} // rfc1952 {ID1, ID2, CM}
   126  
   127  var gzipTrailer = true
   128  
   129  // newArchiveFromGuest returns an hgfs.File implementation to read a directory as a gzip'd tar.
   130  func (h *ArchiveHandler) newArchiveFromGuest(u *url.URL) (File, error) {
   131  	r, w := io.Pipe()
   132  
   133  	a := &archive{
   134  		name:   u.Path,
   135  		done:   r.Close,
   136  		Reader: r,
   137  		Writer: w,
   138  	}
   139  
   140  	var z io.Writer = w
   141  	var c io.Closer = io.NopCloser(nil)
   142  
   143  	switch u.Query().Get("format") {
   144  	case "tgz":
   145  		gz := gzip.NewWriter(w)
   146  		z = gz
   147  		c = gz
   148  	}
   149  
   150  	tw := tar.NewWriter(z)
   151  
   152  	go func() {
   153  		err := h.Write(u, tw)
   154  
   155  		_ = tw.Close()
   156  		_ = c.Close()
   157  		if gzipTrailer {
   158  			_, _ = w.Write(gzipHeader)
   159  		}
   160  		_ = w.CloseWithError(err)
   161  	}()
   162  
   163  	return a, nil
   164  }
   165  
   166  // newArchiveToGuest returns an hgfs.File implementation to expand a gzip'd tar into a directory.
   167  func (h *ArchiveHandler) newArchiveToGuest(u *url.URL) (File, error) {
   168  	r, w := io.Pipe()
   169  
   170  	buf := bufio.NewReader(r)
   171  
   172  	a := &archive{
   173  		name:   u.Path,
   174  		Reader: buf,
   175  		Writer: w,
   176  	}
   177  
   178  	var cerr error
   179  	var wg sync.WaitGroup
   180  
   181  	a.done = func() error {
   182  		_ = w.Close()
   183  		// We need to wait for unpack to finish to complete its work
   184  		// and to propagate the error if any to Close.
   185  		wg.Wait()
   186  		return cerr
   187  	}
   188  
   189  	wg.Add(1)
   190  	go func() {
   191  		defer wg.Done()
   192  
   193  		c := func() error {
   194  			// Drain the pipe of tar trailer data (two null blocks)
   195  			if cerr == nil {
   196  				_, _ = io.Copy(io.Discard, a.Reader)
   197  			}
   198  			return nil
   199  		}
   200  
   201  		header, _ := buf.Peek(len(gzipHeader))
   202  
   203  		if bytes.Equal(header, gzipHeader) {
   204  			gz, err := gzip.NewReader(a.Reader)
   205  			if err != nil {
   206  				_ = r.CloseWithError(err)
   207  				cerr = err
   208  				return
   209  			}
   210  
   211  			c = gz.Close
   212  			a.Reader = gz
   213  		}
   214  
   215  		tr := tar.NewReader(a.Reader)
   216  
   217  		cerr = h.Read(u, tr)
   218  
   219  		_ = c()
   220  		_ = r.CloseWithError(cerr)
   221  	}()
   222  
   223  	return a, nil
   224  }
   225  
   226  func (a *archive) Close() error {
   227  	return a.done()
   228  }
   229  
   230  // archiveRead writes the contents of the given tar.Reader to the given directory.
   231  func archiveRead(u *url.URL, tr *tar.Reader) error {
   232  	for {
   233  		header, err := tr.Next()
   234  		if err != nil {
   235  			if err == io.EOF {
   236  				return nil
   237  			}
   238  			return err
   239  		}
   240  
   241  		name := filepath.Join(u.Path, header.Name)
   242  		mode := os.FileMode(header.Mode)
   243  
   244  		switch header.Typeflag {
   245  		case tar.TypeDir:
   246  			err = os.MkdirAll(name, mode)
   247  		case tar.TypeReg:
   248  			_ = os.MkdirAll(filepath.Dir(name), 0750)
   249  
   250  			var f *os.File
   251  
   252  			f, err = os.OpenFile(name, os.O_CREATE|os.O_RDWR|os.O_TRUNC, mode)
   253  			if err == nil {
   254  				_, cerr := io.Copy(f, tr)
   255  				err = f.Close()
   256  				if cerr != nil {
   257  					err = cerr
   258  				}
   259  			}
   260  		case tar.TypeSymlink:
   261  			err = os.Symlink(header.Linkname, name)
   262  		}
   263  
   264  		// TODO: Uid/Gid may not be meaningful here without some mapping.
   265  		// The other option to consider would be making use of the guest auth user ID.
   266  		// os.Lchown(name, header.Uid, header.Gid)
   267  
   268  		if err != nil {
   269  			return err
   270  		}
   271  	}
   272  }
   273  
   274  // archiveWrite writes the contents of the given source directory to the given tar.Writer.
   275  func archiveWrite(u *url.URL, tw *tar.Writer) error {
   276  	info, err := os.Stat(u.Path)
   277  	if err != nil {
   278  		return err
   279  	}
   280  
   281  	// Note that the VMX will trim any trailing slash.  For example:
   282  	// "/foo/bar/?prefix=bar/" will end up here as "/foo/bar/?prefix=bar"
   283  	// Escape to avoid this: "/for/bar/?prefix=bar%2F"
   284  	prefix := u.Query().Get("prefix")
   285  
   286  	dir := u.Path
   287  
   288  	f := func(file string, fi os.FileInfo, err error) error {
   289  		if err != nil {
   290  			return filepath.SkipDir
   291  		}
   292  
   293  		name := strings.TrimPrefix(file, dir)
   294  		name = strings.TrimPrefix(name, "/")
   295  
   296  		if name == "" {
   297  			return nil // this is u.Path itself (which may or may not have a trailing "/")
   298  		}
   299  
   300  		if prefix != "" {
   301  			name = prefix + name
   302  		}
   303  
   304  		header, _ := tar.FileInfoHeader(fi, name)
   305  
   306  		header.Name = name
   307  
   308  		if header.Typeflag == tar.TypeDir {
   309  			header.Name += "/"
   310  		}
   311  
   312  		var f *os.File
   313  
   314  		if header.Typeflag == tar.TypeReg && fi.Size() != 0 {
   315  			f, err = os.Open(filepath.Clean(file))
   316  			if err != nil {
   317  				if os.IsPermission(err) {
   318  					return nil
   319  				}
   320  				return err
   321  			}
   322  		}
   323  
   324  		_ = tw.WriteHeader(header)
   325  
   326  		if f != nil {
   327  			_, err = io.Copy(tw, f)
   328  			_ = f.Close()
   329  		}
   330  
   331  		return err
   332  	}
   333  
   334  	if info.IsDir() {
   335  		return filepath.Walk(u.Path, f)
   336  	}
   337  
   338  	dir = filepath.Dir(dir)
   339  
   340  	return f(u.Path, info, nil)
   341  }