| package main |
| |
| import ( |
| "archive/tar" |
| "archive/zip" |
| "bytes" |
| "compress/gzip" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "net/http" |
| "net/url" |
| "os" |
| "strings" |
| ) |
| |
| type Archive interface { |
| Next() (*File, error) |
| Close() error |
| } |
| |
| type File struct { |
| io.ReadCloser |
| Name string |
| Size int64 |
| } |
| |
| type tarArchive struct { |
| io.Closer |
| tr *tar.Reader |
| } |
| |
| func (a *tarArchive) Next() (*File, error) { |
| for { |
| hdr, err := a.tr.Next() |
| if err != nil { |
| return nil, err |
| } |
| |
| // We only care about files |
| if hdr.Typeflag != tar.TypeReg && hdr.Typeflag != tar.TypeRegA { |
| continue |
| } |
| |
| return &File{ |
| ReadCloser: ioutil.NopCloser(a.tr), |
| Name: hdr.Name, |
| Size: hdr.Size, |
| }, nil |
| } |
| } |
| |
| type zipArchive struct { |
| io.Closer |
| files []*zip.File |
| } |
| |
| func (a *zipArchive) Next() (*File, error) { |
| if len(a.files) == 0 { |
| return nil, io.EOF |
| } |
| |
| f := a.files[0] |
| a.files = a.files[1:] |
| |
| r, err := f.Open() |
| if err != nil { |
| return nil, err |
| } |
| |
| return &File{ |
| ReadCloser: r, |
| Name: f.Name, |
| Size: int64(f.UncompressedSize64), |
| }, nil |
| } |
| |
| func newZipArchive(r io.Reader, closer io.Closer) (*zipArchive, error) { |
| f, ok := r.(interface { |
| io.ReaderAt |
| Stat() (os.FileInfo, error) |
| }) |
| if !ok { |
| return nil, fmt.Errorf("streaming zip files not supported") |
| } |
| |
| fi, err := f.Stat() |
| if err != nil { |
| return nil, err |
| } |
| |
| zr, err := zip.NewReader(f, fi.Size()) |
| if err != nil { |
| return nil, err |
| } |
| |
| // Filter out non files |
| files := zr.File[:0] |
| for _, f := range zr.File { |
| if f.Mode().IsRegular() { |
| files = append(files, f) |
| } |
| } |
| |
| return &zipArchive{ |
| Closer: closer, |
| files: files, |
| }, nil |
| } |
| |
| func detectContentType(r io.Reader) (string, io.Reader, error) { |
| var buf [512]byte |
| n, err := io.ReadFull(r, buf[:]) |
| if err != nil && err != io.ErrUnexpectedEOF { |
| return "", nil, err |
| } |
| |
| ct := http.DetectContentType(buf[:n]) |
| |
| // If we are a seeker, we can just undo our read |
| if s, ok := r.(io.Seeker); ok { |
| _, err := s.Seek(int64(-n), io.SeekCurrent) |
| return ct, r, err |
| } |
| |
| // Otherwise return a new reader which merges in the read bytes |
| return ct, io.MultiReader(bytes.NewReader(buf[:n]), r), nil |
| } |
| |
| func openReader(u string) (io.ReadCloser, error) { |
| if strings.HasPrefix(u, "https://") || strings.HasPrefix(u, "http://") { |
| resp, err := http.Get(u) |
| if err != nil { |
| return nil, err |
| } |
| if resp.StatusCode < 200 || resp.StatusCode >= 300 { |
| b, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1024)) |
| _ = resp.Body.Close() |
| if err != nil { |
| return nil, err |
| } |
| return nil, &url.Error{ |
| Op: "Get", |
| URL: u, |
| Err: fmt.Errorf("%s: %s", resp.Status, string(b)), |
| } |
| } |
| return resp.Body, nil |
| } else if u == "-" { |
| return ioutil.NopCloser(os.Stdin), nil |
| } |
| |
| return os.Open(u) |
| } |
| |
| // openArchive opens the tar at the URL or filepath u. Also supported is tgz |
| // files over http. |
| func openArchive(u string) (ar Archive, err error) { |
| readCloser, err := openReader(u) |
| if err != nil { |
| return nil, err |
| } |
| defer func() { |
| if err != nil { |
| _ = readCloser.Close() |
| } |
| }() |
| |
| ct, r, err := detectContentType(readCloser) |
| if err != nil { |
| return nil, err |
| } |
| switch ct { |
| case "application/x-gzip": |
| r, err = gzip.NewReader(r) |
| if err != nil { |
| return nil, err |
| } |
| |
| case "application/zip": |
| return newZipArchive(r, readCloser) |
| } |
| |
| return &tarArchive{ |
| Closer: readCloser, |
| tr: tar.NewReader(r), |
| }, nil |
| } |