kubesphere.io/s2irun@v3.2.1+incompatible/pkg/tar/tar.go (about) 1 package tar 2 3 import ( 4 "archive/tar" 5 "fmt" 6 "io" 7 "io/ioutil" 8 "os" 9 "path/filepath" 10 "regexp" 11 "strings" 12 "time" 13 14 "github.com/kubesphere/s2irun/pkg/utils/fs" 15 utilglog "github.com/kubesphere/s2irun/pkg/utils/glog" 16 17 s2ierr "github.com/kubesphere/s2irun/pkg/errors" 18 "github.com/kubesphere/s2irun/pkg/utils" 19 ) 20 21 var glog = utilglog.StderrLog 22 23 // defaultTimeout is the amount of time that the untar will wait for a tar 24 // stream to extract a single file. A timeout is needed to guard against broken 25 // connections in which it would wait for a long time to untar and nothing would happen 26 const defaultTimeout = 30 * time.Second 27 28 // DefaultExclusionPattern is the pattern of files that will not be included in a tar 29 // file when creating one. By default it is any file inside a .git metadata directory 30 var DefaultExclusionPattern = regexp.MustCompile(`(^|/)\.git(/|$)`) 31 32 // Tar can create and extract tar files used in an STI build 33 type Tar interface { 34 // SetExclusionPattern sets the exclusion pattern for tar 35 // creation 36 SetExclusionPattern(*regexp.Regexp) 37 38 // CreateTarFile creates a tar file in the base directory 39 // using the contents of dir directory 40 // The name of the new tar file is returned if successful 41 CreateTarFile(base, dir string) (string, error) 42 43 // CreateTarStreamToTarWriter creates a tar from the given directory 44 // and streams it to the given writer. 45 // An error is returned if an error occurs during streaming. 46 // Archived file names are written to the logger if provided 47 CreateTarStreamToTarWriter(dir string, includeDirInPath bool, writer Writer, logger io.Writer) error 48 49 // CreateTarStream creates a tar from the given directory 50 // and streams it to the given writer. 51 // An error is returned if an error occurs during streaming. 52 CreateTarStream(dir string, includeDirInPath bool, writer io.Writer) error 53 54 // CreateTarStreamReader returns an io.ReadCloser from which a tar stream can be 55 // read. The tar stream is created using CreateTarStream. 56 CreateTarStreamReader(dir string, includeDirInPath bool) io.ReadCloser 57 58 // ExtractTarStream extracts files from a given tar stream. 59 // Times out if reading from the stream for any given file 60 // exceeds the value of timeout. 61 ExtractTarStream(dir string, reader io.Reader) error 62 63 // ExtractTarStreamWithLogging extracts files from a given tar stream. 64 // Times out if reading from the stream for any given file 65 // exceeds the value of timeout. 66 // Extracted file names are written to the logger if provided. 67 ExtractTarStreamWithLogging(dir string, reader io.Reader, logger io.Writer) error 68 69 // ExtractTarStreamFromTarReader extracts files from a given tar stream. 70 // Times out if reading from the stream for any given file 71 // exceeds the value of timeout. 72 // Extracted file names are written to the logger if provided. 73 ExtractTarStreamFromTarReader(dir string, tarReader Reader, logger io.Writer) error 74 } 75 76 // Reader is an interface which tar.Reader implements. 77 type Reader interface { 78 io.Reader 79 Next() (*tar.Header, error) 80 } 81 82 // Writer is an interface which tar.Writer implements. 83 type Writer interface { 84 io.WriteCloser 85 Flush() error 86 WriteHeader(hdr *tar.Header) error 87 } 88 89 // ChmodAdapter changes the mode of files and directories inline as a tarfile is 90 // being written 91 type ChmodAdapter struct { 92 Writer 93 NewFileMode int64 94 NewExecFileMode int64 95 NewDirMode int64 96 } 97 98 // WriteHeader changes the mode of files and directories inline as a tarfile is 99 // being written 100 func (a ChmodAdapter) WriteHeader(hdr *tar.Header) error { 101 if hdr.FileInfo().Mode()&os.ModeSymlink == 0 { 102 newMode := hdr.Mode &^ 0777 103 if hdr.FileInfo().IsDir() { 104 newMode |= a.NewDirMode 105 } else if hdr.FileInfo().Mode()&0010 != 0 { // S_IXUSR 106 newMode |= a.NewExecFileMode 107 } else { 108 newMode |= a.NewFileMode 109 } 110 hdr.Mode = newMode 111 } 112 return a.Writer.WriteHeader(hdr) 113 } 114 115 // RenameAdapter renames files and directories inline as a tarfile is being 116 // written 117 type RenameAdapter struct { 118 Writer 119 Old string 120 New string 121 } 122 123 // WriteHeader renames files and directories inline as a tarfile is being 124 // written 125 func (a RenameAdapter) WriteHeader(hdr *tar.Header) error { 126 if hdr.Name == a.Old { 127 hdr.Name = a.New 128 } else if strings.HasPrefix(hdr.Name, a.Old+"/") { 129 hdr.Name = a.New + hdr.Name[len(a.Old):] 130 } 131 132 return a.Writer.WriteHeader(hdr) 133 } 134 135 // New creates a new Tar 136 func New(fs fs.FileSystem) Tar { 137 return &stiTar{ 138 FileSystem: fs, 139 exclude: DefaultExclusionPattern, 140 timeout: defaultTimeout, 141 } 142 } 143 144 // NewParanoid creates a new Tar that has restrictions 145 // on what it can do while extracting files. 146 func NewParanoid(fs fs.FileSystem) Tar { 147 return &stiTar{ 148 FileSystem: fs, 149 exclude: DefaultExclusionPattern, 150 timeout: defaultTimeout, 151 disallowOverwrite: true, 152 disallowOutsidePaths: true, 153 disallowSpecialFiles: true, 154 } 155 } 156 157 // stiTar is an implementation of the Tar interface 158 type stiTar struct { 159 fs.FileSystem 160 timeout time.Duration 161 exclude *regexp.Regexp 162 includeDirInPath bool 163 disallowOverwrite bool 164 disallowOutsidePaths bool 165 disallowSpecialFiles bool 166 } 167 168 // SetExclusionPattern sets the exclusion pattern for tar creation. The 169 // exclusion pattern always uses UNIX-style (/) path separators, even on 170 // Windows. 171 func (t *stiTar) SetExclusionPattern(p *regexp.Regexp) { 172 t.exclude = p 173 } 174 175 // CreateTarFile creates a tar file from the given directory 176 // while excluding files that match the given exclusion pattern 177 // It returns the name of the created file 178 func (t *stiTar) CreateTarFile(base, dir string) (string, error) { 179 tarFile, err := ioutil.TempFile(base, "tar") 180 defer tarFile.Close() 181 if err != nil { 182 return "", err 183 } 184 if err = t.CreateTarStream(dir, false, tarFile); err != nil { 185 return "", err 186 } 187 return tarFile.Name(), nil 188 } 189 190 func (t *stiTar) shouldExclude(path string) bool { 191 return t.exclude != nil && t.exclude.String() != "" && t.exclude.MatchString(filepath.ToSlash(path)) 192 } 193 194 // CreateTarStream calls CreateTarStreamToTarWriter with a nil logger 195 func (t *stiTar) CreateTarStream(dir string, includeDirInPath bool, writer io.Writer) error { 196 tarWriter := tar.NewWriter(writer) 197 defer tarWriter.Close() 198 199 return t.CreateTarStreamToTarWriter(dir, includeDirInPath, tarWriter, nil) 200 } 201 202 // CreateTarStreamReader returns an io.ReadCloser from which a tar stream can be 203 // read. The tar stream is created using CreateTarStream. 204 func (t *stiTar) CreateTarStreamReader(dir string, includeDirInPath bool) io.ReadCloser { 205 r, w := io.Pipe() 206 go func() { 207 w.CloseWithError(t.CreateTarStream(dir, includeDirInPath, w)) 208 }() 209 return r 210 } 211 212 // CreateTarStreamToTarWriter creates a tar stream on the given writer from 213 // the given directory while excluding files that match the given 214 // exclusion pattern. 215 func (t *stiTar) CreateTarStreamToTarWriter(dir string, includeDirInPath bool, tarWriter Writer, logger io.Writer) error { 216 dir = filepath.Clean(dir) // remove relative paths and extraneous slashes 217 glog.V(5).Infof("Adding %q to tar ...", dir) 218 err := t.Walk(dir, func(path string, info os.FileInfo, err error) error { 219 if err != nil { 220 return err 221 } 222 // on Windows, directory symlinks report as a directory and as a symlink. 223 // They should be treated as symlinks. 224 if !t.shouldExclude(path) { 225 // if file is a link just writing header info is enough 226 if info.Mode()&os.ModeSymlink != 0 { 227 if dir == path { 228 return nil 229 } 230 if err = t.writeTarHeader(tarWriter, dir, path, info, includeDirInPath, logger); err != nil { 231 glog.Errorf("Error writing header for %q: %v", info.Name(), err) 232 } 233 // on Windows, filepath.Walk recurses into directory symlinks when it 234 // shouldn't. https://github.com/golang/go/issues/17540 235 if err == nil && info.Mode()&os.ModeDir != 0 { 236 return filepath.SkipDir 237 } 238 return err 239 } 240 if info.IsDir() { 241 if dir == path { 242 return nil 243 } 244 if err = t.writeTarHeader(tarWriter, dir, path, info, includeDirInPath, logger); err != nil { 245 glog.Errorf("Error writing header for %q: %v", info.Name(), err) 246 } 247 return err 248 } 249 250 // regular files are copied into tar, if accessible 251 file, err := os.Open(path) 252 if err != nil { 253 glog.Errorf("Ignoring file %s: %v", path, err) 254 return nil 255 } 256 defer file.Close() 257 if err = t.writeTarHeader(tarWriter, dir, path, info, includeDirInPath, logger); err != nil { 258 glog.Errorf("Error writing header for %q: %v", info.Name(), err) 259 return err 260 } 261 if _, err = io.Copy(tarWriter, file); err != nil { 262 glog.Errorf("Error copying file %q to tar: %v", path, err) 263 return err 264 } 265 } 266 return nil 267 }) 268 269 if err != nil { 270 glog.Errorf("Error writing tar: %v", err) 271 return err 272 } 273 274 return nil 275 } 276 277 // writeTarHeader writes tar header for given file, returns error if operation fails 278 func (t *stiTar) writeTarHeader(tarWriter Writer, dir string, path string, info os.FileInfo, includeDirInPath bool, logger io.Writer) error { 279 var ( 280 link string 281 err error 282 ) 283 if info.Mode()&os.ModeSymlink != 0 { 284 link, err = os.Readlink(path) 285 if err != nil { 286 return err 287 } 288 } 289 header, err := tar.FileInfoHeader(info, link) 290 if err != nil { 291 return err 292 } 293 // on Windows, tar.FileInfoHeader incorrectly interprets directory symlinks 294 // as directories. https://github.com/golang/go/issues/17541 295 if info.Mode()&os.ModeSymlink != 0 && info.Mode()&os.ModeDir != 0 { 296 header.Typeflag = tar.TypeSymlink 297 header.Mode &^= 040000 // c_ISDIR 298 header.Mode |= 0120000 // c_ISLNK 299 header.Linkname = link 300 } 301 prefix := dir 302 if includeDirInPath { 303 prefix = filepath.Dir(prefix) 304 } 305 fileName := path 306 if prefix != "." { 307 fileName = path[1+len(prefix):] 308 } 309 header.Name = filepath.ToSlash(fileName) 310 header.Linkname = filepath.ToSlash(header.Linkname) 311 logFile(logger, header.Name) 312 glog.V(5).Infof("Adding to tar: %s as %s", path, header.Name) 313 return tarWriter.WriteHeader(header) 314 } 315 316 // ExtractTarStream calls ExtractTarStreamFromTarReader with a default reader and nil logger 317 func (t *stiTar) ExtractTarStream(dir string, reader io.Reader) error { 318 tarReader := tar.NewReader(reader) 319 return t.ExtractTarStreamFromTarReader(dir, tarReader, nil) 320 } 321 322 // ExtractTarStreamWithLogging calls ExtractTarStreamFromTarReader with a default reader 323 func (t *stiTar) ExtractTarStreamWithLogging(dir string, reader io.Reader, logger io.Writer) error { 324 tarReader := tar.NewReader(reader) 325 return t.ExtractTarStreamFromTarReader(dir, tarReader, logger) 326 } 327 328 // ExtractTarStreamFromTarReader extracts files from a given tar stream. 329 // Times out if reading from the stream for any given file 330 // exceeds the value of timeout 331 func (t *stiTar) ExtractTarStreamFromTarReader(dir string, tarReader Reader, logger io.Writer) error { 332 err := utils.TimeoutAfter(t.timeout, "", func(timeoutTimer *time.Timer) error { 333 for { 334 header, err := tarReader.Next() 335 if !timeoutTimer.Stop() { 336 return &utils.TimeoutError{} 337 } 338 timeoutTimer.Reset(t.timeout) 339 if err == io.EOF { 340 return nil 341 } 342 if err != nil { 343 glog.Errorf("Error reading next tar header: %v", err) 344 return err 345 } 346 347 if t.disallowSpecialFiles { 348 switch header.Typeflag { 349 case tar.TypeReg, tar.TypeRegA, tar.TypeLink, tar.TypeSymlink, tar.TypeDir, tar.TypeGNUSparse: 350 default: 351 glog.Warningf("Skipping special file %s, type: %v", header.Name, header.Typeflag) 352 continue 353 } 354 } 355 356 p := header.Name 357 if t.disallowOutsidePaths { 358 p = filepath.Clean(filepath.Join(dir, p)) 359 if !strings.HasPrefix(p, dir) { 360 glog.Warningf("Skipping relative path file in tar: %s", header.Name) 361 continue 362 } 363 } 364 365 if header.FileInfo().IsDir() { 366 dirPath := filepath.Join(dir, filepath.Clean(header.Name)) 367 glog.V(3).Infof("Creating directory %s", dirPath) 368 if err = os.MkdirAll(dirPath, 0700); err != nil { 369 glog.Errorf("Error creating dir %q: %v", dirPath, err) 370 return err 371 } 372 t.Chmod(dirPath, header.FileInfo().Mode()) 373 } else { 374 fileDir := filepath.Dir(header.Name) 375 dirPath := filepath.Join(dir, filepath.Clean(fileDir)) 376 glog.V(3).Infof("Creating directory %s", dirPath) 377 if err = os.MkdirAll(dirPath, 0700); err != nil { 378 glog.Errorf("Error creating dir %q: %v", dirPath, err) 379 return err 380 } 381 if header.Typeflag == tar.TypeSymlink { 382 if err := t.extractLink(dir, header, tarReader); err != nil { 383 glog.Errorf("Error extracting link %q: %v", header.Name, err) 384 return err 385 } 386 continue 387 } 388 logFile(logger, header.Name) 389 if err := t.extractFile(dir, header, tarReader); err != nil { 390 glog.Errorf("Error extracting file %q: %v", header.Name, err) 391 return err 392 } 393 } 394 } 395 }) 396 397 if err != nil { 398 glog.Error("Error extracting tar stream") 399 } else { 400 glog.V(2).Info("Done extracting tar stream") 401 } 402 403 if utils.IsTimeoutError(err) { 404 err = s2ierr.NewTarTimeoutError() 405 } 406 407 return err 408 } 409 410 func (t *stiTar) extractLink(dir string, header *tar.Header, tarReader io.Reader) error { 411 dest := filepath.Join(dir, header.Name) 412 source := header.Linkname 413 414 if t.disallowOutsidePaths { 415 target := filepath.Clean(filepath.Join(dest, "..", source)) 416 if !strings.HasPrefix(target, dir) { 417 glog.Warningf("Skipping symlink that points to relative path: %s", header.Linkname) 418 return nil 419 } 420 } 421 422 if t.disallowOverwrite { 423 if _, err := os.Stat(dest); !os.IsNotExist(err) { 424 glog.Warningf("Refusing to overwrite existing file: %s", dest) 425 return nil 426 } 427 } 428 429 glog.V(3).Infof("Creating symbolic link from %q to %q", dest, source) 430 431 // TODO: set mtime for symlink (unfortunately we can't use os.Chtimes() and probably should use syscall) 432 return os.Symlink(source, dest) 433 } 434 435 func (t *stiTar) extractFile(dir string, header *tar.Header, tarReader io.Reader) error { 436 path := filepath.Join(dir, header.Name) 437 if t.disallowOverwrite { 438 if _, err := os.Stat(path); !os.IsNotExist(err) { 439 glog.Warningf("Refusing to overwrite existing file: %s", path) 440 return nil 441 } 442 } 443 444 glog.V(3).Infof("Creating %s", path) 445 446 file, err := os.Create(path) 447 if err != nil { 448 return err 449 } 450 // The file times need to be modified after it's been closed thus this function 451 // is deferred after the file close (LIFO order for defer) 452 defer os.Chtimes(path, time.Now(), header.FileInfo().ModTime()) 453 defer file.Close() 454 glog.V(3).Infof("Extracting/writing %s", path) 455 written, err := io.Copy(file, tarReader) 456 if err != nil { 457 return err 458 } 459 if written != header.Size { 460 return fmt.Errorf("wrote %d bytes, expected to write %d", written, header.Size) 461 } 462 return t.Chmod(path, header.FileInfo().Mode()) 463 } 464 465 func logFile(logger io.Writer, name string) { 466 if logger == nil { 467 return 468 } 469 fmt.Fprintf(logger, "%s\n", name) 470 }