blob: c1afe5b88e41830fc1825277531a2c6898c9405c [file] [log] [blame]
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
}