github.com/YousefHaggyHeroku/pack@v1.5.5/internal/archive/archive.go (about) 1 package archive 2 3 import ( 4 "archive/tar" 5 "archive/zip" 6 "bytes" 7 "io" 8 "io/ioutil" 9 "os" 10 "path" 11 "path/filepath" 12 "time" 13 14 "github.com/docker/docker/pkg/ioutils" 15 "github.com/pkg/errors" 16 ) 17 18 var NormalizedDateTime time.Time 19 20 func init() { 21 NormalizedDateTime = time.Date(1980, time.January, 1, 0, 0, 1, 0, time.UTC) 22 } 23 24 type TarWriter interface { 25 WriteHeader(hdr *tar.Header) error 26 Write(b []byte) (int, error) 27 Close() error 28 } 29 30 type TarWriterFactory interface { 31 NewWriter(io.Writer) TarWriter 32 } 33 34 type defaultTarWriterFactory struct{} 35 36 func DefaultTarWriterFactory() TarWriterFactory { 37 return defaultTarWriterFactory{} 38 } 39 40 func (defaultTarWriterFactory) NewWriter(w io.Writer) TarWriter { 41 return tar.NewWriter(w) 42 } 43 44 func ReadDirAsTar(srcDir, basePath string, uid, gid int, mode int64, normalizeModTime bool, fileFilter func(string) bool) io.ReadCloser { 45 return GenerateTar(func(tw TarWriter) error { 46 return WriteDirToTar(tw, srcDir, basePath, uid, gid, mode, normalizeModTime, fileFilter) 47 }) 48 } 49 50 func ReadZipAsTar(srcPath, basePath string, uid, gid int, mode int64, normalizeModTime bool, fileFilter func(string) bool) io.ReadCloser { 51 return GenerateTar(func(tw TarWriter) error { 52 return WriteZipToTar(tw, srcPath, basePath, uid, gid, mode, normalizeModTime, fileFilter) 53 }) 54 } 55 56 func GenerateTar(genFn func(TarWriter) error) io.ReadCloser { 57 return GenerateTarWithWriter(genFn, DefaultTarWriterFactory()) 58 } 59 60 // GenerateTarWithTar returns a reader to a tar from a generator function using a writer from the provided factory. 61 // Note that the generator will not fully execute until the reader is fully read from. Any errors returned by the 62 // generator will be returned when reading the reader. 63 func GenerateTarWithWriter(genFn func(TarWriter) error, twf TarWriterFactory) io.ReadCloser { 64 errChan := make(chan error) 65 pr, pw := io.Pipe() 66 67 go func() { 68 tw := twf.NewWriter(pw) 69 defer func() { 70 if r := recover(); r != nil { 71 tw.Close() 72 pw.CloseWithError(errors.Errorf("panic: %v", r)) 73 } 74 }() 75 76 err := genFn(tw) 77 78 closeErr := tw.Close() 79 closeErr = aggregateError(closeErr, pw.CloseWithError(err)) 80 81 errChan <- closeErr 82 }() 83 84 closed := false 85 return ioutils.NewReadCloserWrapper(pr, func() error { 86 if closed { 87 return errors.New("reader already closed") 88 } 89 90 var completeErr error 91 92 // closing the reader ensures that if anything attempts 93 // further reading it doesn't block waiting for content 94 if err := pr.Close(); err != nil { 95 completeErr = aggregateError(completeErr, err) 96 } 97 98 // wait until everything closes properly 99 if err := <-errChan; err != nil { 100 completeErr = aggregateError(completeErr, err) 101 } 102 103 closed = true 104 return completeErr 105 }) 106 } 107 108 func aggregateError(base, addition error) error { 109 if addition == nil { 110 return base 111 } 112 113 if base == nil { 114 return addition 115 } 116 117 return errors.Wrap(addition, base.Error()) 118 } 119 120 func CreateSingleFileTarReader(path, txt string) io.ReadCloser { 121 tarBuilder := TarBuilder{} 122 tarBuilder.AddFile(path, 0644, NormalizedDateTime, []byte(txt)) 123 return tarBuilder.Reader(DefaultTarWriterFactory()) 124 } 125 126 func CreateSingleFileTar(tarFile, path, txt string) error { 127 tarBuilder := TarBuilder{} 128 tarBuilder.AddFile(path, 0644, NormalizedDateTime, []byte(txt)) 129 return tarBuilder.WriteToPath(tarFile, DefaultTarWriterFactory()) 130 } 131 132 // ErrEntryNotExist is an error returned if an entry path doesn't exist 133 var ErrEntryNotExist = errors.New("not exist") 134 135 // IsEntryNotExist detects whether a given error is of type ErrEntryNotExist 136 func IsEntryNotExist(err error) bool { 137 return err == ErrEntryNotExist || errors.Cause(err) == ErrEntryNotExist 138 } 139 140 // ReadTarEntry reads and returns a tar file 141 func ReadTarEntry(rc io.Reader, entryPath string) (*tar.Header, []byte, error) { 142 tr := tar.NewReader(rc) 143 for { 144 header, err := tr.Next() 145 if err == io.EOF { 146 break 147 } 148 if err != nil { 149 return nil, nil, errors.Wrap(err, "failed to get next tar entry") 150 } 151 152 if path.Clean(header.Name) == entryPath { 153 buf, err := ioutil.ReadAll(tr) 154 if err != nil { 155 return nil, nil, errors.Wrapf(err, "failed to read contents of '%s'", entryPath) 156 } 157 158 return header, buf, nil 159 } 160 } 161 162 return nil, nil, errors.Wrapf(ErrEntryNotExist, "could not find entry path '%s'", entryPath) 163 } 164 165 // WriteDirToTar writes the contents of a directory to a tar writer. `basePath` is the "location" in the tar the 166 // contents will be placed. 167 func WriteDirToTar(tw TarWriter, srcDir, basePath string, uid, gid int, mode int64, normalizeModTime bool, fileFilter func(string) bool) error { 168 return filepath.Walk(srcDir, func(file string, fi os.FileInfo, err error) error { 169 if fileFilter != nil && !fileFilter(file) { 170 return nil 171 } 172 if err != nil { 173 return err 174 } 175 176 if fi.Mode()&os.ModeSocket != 0 { 177 return nil 178 } 179 180 var header *tar.Header 181 if fi.Mode()&os.ModeSymlink != 0 { 182 target, err := os.Readlink(file) 183 if err != nil { 184 return err 185 } 186 187 // Ensure that symlinks have Linux link names, independent of source OS 188 header, err = tar.FileInfoHeader(fi, filepath.ToSlash(target)) 189 if err != nil { 190 return err 191 } 192 } else { 193 header, err = tar.FileInfoHeader(fi, fi.Name()) 194 if err != nil { 195 return err 196 } 197 } 198 199 relPath, err := filepath.Rel(srcDir, file) 200 if err != nil { 201 return err 202 } else if relPath == "." { 203 return nil 204 } 205 206 header.Name = filepath.ToSlash(filepath.Join(basePath, relPath)) 207 finalizeHeader(header, uid, gid, mode, normalizeModTime) 208 209 if err := tw.WriteHeader(header); err != nil { 210 return err 211 } 212 213 if fi.Mode().IsRegular() { 214 f, err := os.Open(file) 215 if err != nil { 216 return err 217 } 218 defer f.Close() 219 220 if _, err := io.Copy(tw, f); err != nil { 221 return err 222 } 223 } 224 225 return nil 226 }) 227 } 228 229 // WriteZipToTar writes the contents of a zip file to a tar writer. 230 func WriteZipToTar(tw TarWriter, srcZip, basePath string, uid, gid int, mode int64, normalizeModTime bool, fileFilter func(string) bool) error { 231 zipReader, err := zip.OpenReader(srcZip) 232 if err != nil { 233 return err 234 } 235 defer zipReader.Close() 236 237 var fileMode int64 238 for _, f := range zipReader.File { 239 if fileFilter != nil && !fileFilter(f.Name) { 240 continue 241 } 242 243 fileMode = mode 244 if isFatFile(f.FileHeader) { 245 fileMode = 0777 246 } 247 248 var header *tar.Header 249 if f.Mode()&os.ModeSymlink != 0 { 250 target, err := func() (string, error) { 251 r, err := f.Open() 252 if err != nil { 253 return "", nil 254 } 255 defer r.Close() 256 257 // contents is the target of the symlink 258 target, err := ioutil.ReadAll(r) 259 if err != nil { 260 return "", err 261 } 262 263 return string(target), nil 264 }() 265 266 if err != nil { 267 return err 268 } 269 270 header, err = tar.FileInfoHeader(f.FileInfo(), target) 271 if err != nil { 272 return err 273 } 274 } else { 275 header, err = tar.FileInfoHeader(f.FileInfo(), f.Name) 276 if err != nil { 277 return err 278 } 279 } 280 281 header.Name = filepath.ToSlash(filepath.Join(basePath, f.Name)) 282 finalizeHeader(header, uid, gid, fileMode, normalizeModTime) 283 284 if err := tw.WriteHeader(header); err != nil { 285 return err 286 } 287 288 if f.Mode().IsRegular() { 289 err := func() error { 290 fi, err := f.Open() 291 if err != nil { 292 return err 293 } 294 defer fi.Close() 295 296 _, err = io.Copy(tw, fi) 297 return err 298 }() 299 300 if err != nil { 301 return err 302 } 303 } 304 } 305 306 return nil 307 } 308 309 func isFatFile(header zip.FileHeader) bool { 310 var ( 311 creatorFAT uint16 = 0 312 creatorVFAT uint16 = 14 313 ) 314 315 // This identifies FAT files, based on the `zip` source: https://golang.org/src/archive/zip/struct.go 316 firstByte := header.CreatorVersion >> 8 317 return firstByte == creatorFAT || firstByte == creatorVFAT 318 } 319 320 func finalizeHeader(header *tar.Header, uid, gid int, mode int64, normalizeModTime bool) { 321 NormalizeHeader(header, normalizeModTime) 322 if mode != -1 { 323 header.Mode = mode 324 } 325 header.Uid = uid 326 header.Gid = gid 327 } 328 329 // NormalizeHeader normalizes a tar.Header 330 // 331 // Normalizes the following: 332 // - ModTime 333 // - GID 334 // - UID 335 // - User Name 336 // - Group Name 337 func NormalizeHeader(header *tar.Header, normalizeModTime bool) { 338 if normalizeModTime { 339 header.ModTime = NormalizedDateTime 340 } 341 header.Uid = 0 342 header.Gid = 0 343 header.Uname = "" 344 header.Gname = "" 345 } 346 347 // IsZip detects whether or not a File is a zip directory 348 func IsZip(file io.Reader) (bool, error) { 349 b := make([]byte, 4) 350 _, err := file.Read(b) 351 if err != nil && err != io.EOF { 352 return false, err 353 } else if err == io.EOF { 354 return false, nil 355 } 356 357 return bytes.Equal(b, []byte("\x50\x4B\x03\x04")), nil 358 }