github.com/vmware/govmomi@v0.43.0/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 }