Add unarchiving to file downloader
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
parent
ff8ed902ea
commit
e785c6b53d
2
go.mod
2
go.mod
@ -19,6 +19,7 @@ require (
|
|||||||
github.com/schollz/progressbar/v3 v3.13.0
|
github.com/schollz/progressbar/v3 v3.13.0
|
||||||
github.com/twitchtv/twirp v8.1.3+incompatible
|
github.com/twitchtv/twirp v8.1.3+incompatible
|
||||||
github.com/urfave/cli/v2 v2.23.7
|
github.com/urfave/cli/v2 v2.23.7
|
||||||
|
github.com/vmihailenco/msgpack/v5 v5.3.5
|
||||||
go.arsenm.dev/logger v0.0.0-20230126004036-a8cbbe3b6fe6
|
go.arsenm.dev/logger v0.0.0-20230126004036-a8cbbe3b6fe6
|
||||||
go.arsenm.dev/translate v0.0.0-20230113025904-5ad1ec0ed296
|
go.arsenm.dev/translate v0.0.0-20230113025904-5ad1ec0ed296
|
||||||
golang.org/x/exp v0.0.0-20220916125017-b168a2c6b86b
|
golang.org/x/exp v0.0.0-20220916125017-b168a2c6b86b
|
||||||
@ -86,6 +87,7 @@ require (
|
|||||||
github.com/sirupsen/logrus v1.9.0 // indirect
|
github.com/sirupsen/logrus v1.9.0 // indirect
|
||||||
github.com/therootcompany/xz v1.0.1 // indirect
|
github.com/therootcompany/xz v1.0.1 // indirect
|
||||||
github.com/ulikunitz/xz v0.5.10 // indirect
|
github.com/ulikunitz/xz v0.5.10 // indirect
|
||||||
|
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||||
github.com/xanzy/ssh-agent v0.3.1 // indirect
|
github.com/xanzy/ssh-agent v0.3.1 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||||
|
4
go.sum
4
go.sum
@ -251,6 +251,10 @@ github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
|
|||||||
github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
github.com/urfave/cli/v2 v2.23.7 h1:YHDQ46s3VghFHFf1DdF+Sh7H4RqhcM+t0TmZRJx4oJY=
|
github.com/urfave/cli/v2 v2.23.7 h1:YHDQ46s3VghFHFf1DdF+Sh7H4RqhcM+t0TmZRJx4oJY=
|
||||||
github.com/urfave/cli/v2 v2.23.7/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
|
github.com/urfave/cli/v2 v2.23.7/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
|
||||||
|
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
|
||||||
|
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
|
||||||
|
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||||
|
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||||
github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
|
github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
|
||||||
github.com/xanzy/ssh-agent v0.3.1 h1:AmzO1SSWxw73zxFZPRwaMN1MohDw8UyHnmuxyceTEGo=
|
github.com/xanzy/ssh-agent v0.3.1 h1:AmzO1SSWxw73zxFZPRwaMN1MohDw8UyHnmuxyceTEGo=
|
||||||
github.com/xanzy/ssh-agent v0.3.1/go.mod h1:QIE4lCeL7nkC25x+yA3LBIYfwCc1TFziCtG7cBAac6w=
|
github.com/xanzy/ssh-agent v0.3.1/go.mod h1:QIE4lCeL7nkC25x+yA3LBIYfwCc1TFziCtG7cBAac6w=
|
||||||
|
@ -2,15 +2,22 @@ package dl
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/vmihailenco/msgpack/v5"
|
||||||
"go.arsenm.dev/logger/log"
|
"go.arsenm.dev/logger/log"
|
||||||
"go.arsenm.dev/lure/internal/dlcache"
|
"go.arsenm.dev/lure/internal/dlcache"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const manifestFileName = ".lure_cache_manifest"
|
||||||
|
|
||||||
|
var ErrChecksumMismatch = errors.New("dl: checksums did not match")
|
||||||
|
|
||||||
var Downloaders = []Downloader{
|
var Downloaders = []Downloader{
|
||||||
|
GitDownloader{},
|
||||||
FileDownloader{},
|
FileDownloader{},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,48 +39,85 @@ func (t Type) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
ID string
|
SHA256 []byte
|
||||||
Name string
|
Name string
|
||||||
URL string
|
URL string
|
||||||
Destination string
|
Destination string
|
||||||
|
CacheDisabled bool
|
||||||
|
PostprocDisabled bool
|
||||||
Progress io.Writer
|
Progress io.Writer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Manifest struct {
|
||||||
|
Type Type
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
type Downloader interface {
|
type Downloader interface {
|
||||||
Name() string
|
Name() string
|
||||||
Type() Type
|
|
||||||
MatchURL(string) bool
|
MatchURL(string) bool
|
||||||
Download(Options) error
|
Download(Options) (Type, string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdatingDownloader interface {
|
type UpdatingDownloader interface {
|
||||||
Downloader
|
Downloader
|
||||||
Update(Options) error
|
Update(Options) (bool, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Download(ctx context.Context, opts Options) error {
|
func Download(ctx context.Context, opts Options) (err error) {
|
||||||
d := getDownloader(opts.URL)
|
d := getDownloader(opts.URL)
|
||||||
cacheDir, ok := dlcache.Get(opts.ID)
|
|
||||||
|
if opts.CacheDisabled {
|
||||||
|
_, _, err = d.Download(opts)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var t Type
|
||||||
|
cacheDir, ok := dlcache.Get(opts.URL)
|
||||||
if ok {
|
if ok {
|
||||||
ok, err := handleCache(cacheDir, opts.Destination, d.Type())
|
var updated bool
|
||||||
|
if d, ok := d.(UpdatingDownloader); ok {
|
||||||
|
log.Info("Source can be updated, updating if required").Str("source", opts.Name).Str("downloader", d.Name()).Send()
|
||||||
|
|
||||||
|
updated, err = d.Update(Options{
|
||||||
|
Name: opts.Name,
|
||||||
|
URL: opts.URL,
|
||||||
|
Destination: cacheDir,
|
||||||
|
Progress: opts.Progress,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := getManifest(cacheDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t = m.Type
|
||||||
|
|
||||||
|
dest := filepath.Join(opts.Destination, m.Name)
|
||||||
|
ok, err := handleCache(cacheDir, dest, t)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if ok {
|
if ok && !updated {
|
||||||
log.Info("Source found in cache, linked to destination").Str("source", opts.Name).Stringer("type", d.Type()).Send()
|
log.Info("Source found in cache, linked to destination").Str("source", opts.Name).Stringer("type", t).Send()
|
||||||
|
return nil
|
||||||
|
} else if ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info("Downloading source").Str("source", opts.Name).Str("downloader", d.Name()).Send()
|
log.Info("Downloading source").Str("source", opts.Name).Str("downloader", d.Name()).Send()
|
||||||
|
|
||||||
cacheDir, err := dlcache.New(opts.ID)
|
cacheDir, err = dlcache.New(opts.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = d.Download(Options{
|
t, name, err := d.Download(Options{
|
||||||
Name: opts.Name,
|
Name: opts.Name,
|
||||||
URL: opts.URL,
|
URL: opts.URL,
|
||||||
Destination: cacheDir,
|
Destination: cacheDir,
|
||||||
@ -83,10 +127,36 @@ func Download(ctx context.Context, opts Options) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = handleCache(cacheDir, opts.Destination, d.Type())
|
err = writeManifest(cacheDir, Manifest{t, name})
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dest := filepath.Join(opts.Destination, name)
|
||||||
|
_, err = handleCache(cacheDir, dest, t)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeManifest(cacheDir string, m Manifest) error {
|
||||||
|
fl, err := os.Create(filepath.Join(cacheDir, manifestFileName))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer fl.Close()
|
||||||
|
return msgpack.NewEncoder(fl).Encode(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getManifest(cacheDir string) (m Manifest, err error) {
|
||||||
|
fl, err := os.Open(filepath.Join(cacheDir, manifestFileName))
|
||||||
|
if err != nil {
|
||||||
|
return Manifest{}, err
|
||||||
|
}
|
||||||
|
defer fl.Close()
|
||||||
|
|
||||||
|
err = msgpack.NewDecoder(fl).Decode(&m)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func handleCache(cacheDir, dest string, t Type) (bool, error) {
|
func handleCache(cacheDir, dest string, t Type) (bool, error) {
|
||||||
switch t {
|
switch t {
|
||||||
case TypeFile:
|
case TypeFile:
|
||||||
@ -95,24 +165,28 @@ func handleCache(cacheDir, dest string, t Type) (bool, error) {
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
names, err := cd.Readdirnames(1)
|
names, err := cd.Readdirnames(2)
|
||||||
if err != nil && err != io.EOF {
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
} else if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the cache dir contains no files,
|
cd.Close()
|
||||||
// assume there is no cache entry
|
|
||||||
if len(names) == 0 {
|
for _, name := range names {
|
||||||
break
|
if name == manifestFileName {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.Link(filepath.Join(cacheDir, names[0]), filepath.Join(dest, filepath.Base(names[0])))
|
err = os.Link(filepath.Join(cacheDir, names[0]), filepath.Join(dest, filepath.Base(names[0])))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
case TypeDir:
|
case TypeDir:
|
||||||
err := os.Link(cacheDir, dest)
|
err := linkDir(cacheDir, dest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -121,6 +195,30 @@ func handleCache(cacheDir, dest string, t Type) (bool, error) {
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func linkDir(src, dest string) error {
|
||||||
|
return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.Name() == manifestFileName {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rel, err := filepath.Rel(src, path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
newPath := filepath.Join(dest, rel)
|
||||||
|
if info.IsDir() {
|
||||||
|
return os.Mkdir(newPath, info.Mode())
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.Link(path, newPath)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func getDownloader(u string) Downloader {
|
func getDownloader(u string) Downloader {
|
||||||
for _, d := range Downloaders {
|
for _, d := range Downloaders {
|
||||||
if d.MatchURL(u) {
|
if d.MatchURL(u) {
|
||||||
|
@ -1,14 +1,21 @@
|
|||||||
package dl
|
package dl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/mholt/archiver/v4"
|
||||||
"github.com/schollz/progressbar/v3"
|
"github.com/schollz/progressbar/v3"
|
||||||
|
"go.arsenm.dev/lure/internal/shutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FileDownloader struct{}
|
type FileDownloader struct{}
|
||||||
@ -25,28 +32,156 @@ func (FileDownloader) MatchURL(string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (FileDownloader) Download(opts Options) error {
|
func (FileDownloader) Download(opts Options) (Type, string, error) {
|
||||||
res, err := http.Get(opts.URL)
|
res, err := http.Get(opts.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return 0, "", err
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
|
||||||
|
|
||||||
name := getFilename(res)
|
name := getFilename(res)
|
||||||
fl, err := os.Create(filepath.Join(opts.Destination, name))
|
path := filepath.Join(opts.Destination, name)
|
||||||
|
fl, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return 0, "", err
|
||||||
|
}
|
||||||
|
defer fl.Close()
|
||||||
|
|
||||||
|
var bar io.WriteCloser
|
||||||
|
if opts.Progress != nil {
|
||||||
|
bar = progressbar.NewOptions64(
|
||||||
|
res.ContentLength,
|
||||||
|
progressbar.OptionSetDescription(name),
|
||||||
|
progressbar.OptionSetWriter(opts.Progress),
|
||||||
|
progressbar.OptionShowBytes(true),
|
||||||
|
progressbar.OptionSetWidth(10),
|
||||||
|
progressbar.OptionThrottle(65*time.Millisecond),
|
||||||
|
progressbar.OptionShowCount(),
|
||||||
|
progressbar.OptionOnCompletion(func() {
|
||||||
|
_, _ = io.WriteString(opts.Progress, "\n")
|
||||||
|
}),
|
||||||
|
progressbar.OptionSpinnerType(14),
|
||||||
|
progressbar.OptionFullWidth(),
|
||||||
|
progressbar.OptionSetRenderBlankState(true),
|
||||||
|
)
|
||||||
|
defer bar.Close()
|
||||||
|
} else {
|
||||||
|
bar = shutils.NopRWC{}
|
||||||
|
}
|
||||||
|
|
||||||
|
h := sha256.New()
|
||||||
|
|
||||||
|
var w io.Writer
|
||||||
|
if opts.SHA256 != nil {
|
||||||
|
w = io.MultiWriter(fl, h, bar)
|
||||||
|
} else {
|
||||||
|
w = io.MultiWriter(fl, bar)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(w, res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return 0, "", err
|
||||||
|
}
|
||||||
|
res.Body.Close()
|
||||||
|
|
||||||
|
if opts.SHA256 != nil {
|
||||||
|
sum := h.Sum(nil)
|
||||||
|
if !bytes.Equal(sum, opts.SHA256) {
|
||||||
|
return 0, "", ErrChecksumMismatch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.PostprocDisabled {
|
||||||
|
return TypeFile, name, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = fl.Seek(0, io.SeekStart)
|
||||||
|
if err != nil {
|
||||||
|
return 0, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
format, r, err := archiver.Identify(name, fl)
|
||||||
|
if err == archiver.ErrNoMatch {
|
||||||
|
return TypeFile, name, nil
|
||||||
|
} else if err != nil {
|
||||||
|
return 0, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = extractFile(r, format, name, opts)
|
||||||
|
if err != nil {
|
||||||
|
return 0, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.Remove(path)
|
||||||
|
return TypeDir, strings.TrimSuffix(name, format.Name()), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractFile(r io.Reader, format archiver.Format, name string, opts Options) (err error) {
|
||||||
|
fname := format.Name()
|
||||||
|
|
||||||
|
switch format := format.(type) {
|
||||||
|
case archiver.Extractor:
|
||||||
|
err = format.Extract(context.Background(), r, nil, func(ctx context.Context, f archiver.File) error {
|
||||||
|
fr, err := f.Open()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer fr.Close()
|
||||||
|
fi, err := f.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fm := fi.Mode()
|
||||||
|
|
||||||
|
path := filepath.Join(opts.Destination, f.NameInArchive)
|
||||||
|
|
||||||
|
err = os.MkdirAll(filepath.Dir(path), 0o755)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
bar := progressbar.DefaultBytes(
|
if f.IsDir() {
|
||||||
res.ContentLength,
|
err = os.Mkdir(path, 0o755)
|
||||||
"downloading "+name,
|
if err != nil {
|
||||||
)
|
|
||||||
defer bar.Close()
|
|
||||||
|
|
||||||
_, err = io.Copy(io.MultiWriter(fl, bar), res.Body)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
outFl, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fm.Perm())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer outFl.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(outFl, fr)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case archiver.Decompressor:
|
||||||
|
rc, err := format.OpenReader(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer rc.Close()
|
||||||
|
|
||||||
|
path := filepath.Join(opts.Destination, name)
|
||||||
|
path = strings.TrimSuffix(path, fname)
|
||||||
|
|
||||||
|
outFl, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(outFl, rc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var cdHeaderRgx = regexp.MustCompile(`filename="(.+)"`)
|
var cdHeaderRgx = regexp.MustCompile(`filename="(.+)"`)
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ func (NopRWC) Read([]byte) (int, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (NopRWC) Write([]byte) (int, error) {
|
func (NopRWC) Write([]byte) (int, error) {
|
||||||
return 0, io.EOF
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (NopRWC) Close() error {
|
func (NopRWC) Close() error {
|
||||||
|
Loading…
Reference in New Issue
Block a user