diff --git a/internal/caches/archive/archive.go b/internal/caches/archive/archive.go new file mode 100644 index 0000000..bc75218 --- /dev/null +++ b/internal/caches/archive/archive.go @@ -0,0 +1,86 @@ +// Package archive provides a read-only cache, backed by a zip archive. +package archive + +import ( + "archive/zip" + "errors" + "io" + "log/slog" + "os" + + "github.com/AlekSi/hardcache/internal/go/cache" + "github.com/AlekSi/lazyerrors" +) + +// Cache represents a read-only [cache.Cache], backed by a zip archive. +type Cache struct { + zr *zip.Reader + c io.Closer + d + l *slog.Logger +} + +// New creates a new [Cache]. +func New(r io.ReaderAt, size int64, l *slog.Logger) (*Cache, error) { + zr, err := zip.NewReader(r, size) + if err != nil { + return nil, lazyerrors.Error(err) + } + + os.CreateTemp() + + return &Cache{ + zr: zr, + l: l, + }, nil +} + +func Open(file string, l *slog.Logger) (*Cache, error) { + rc, err := zip.OpenReader(file) + if err != nil { + return nil, lazyerrors.Error(err) + } + + return &Cache{ + zr: &rc.Reader, + c: rc, + l: l, + }, nil +} + +// Get implements [cache.Cache]. +func (c *Cache) Get(id cache.ActionID) (cache.Entry, error) { + return c.dc.Get(id) +} + +// Put implements [cache.Cache] by returning an error. +func (c *Cache) Put(id cache.ActionID, rs io.ReadSeeker) (_ cache.OutputID, _ int64, err error) { + err = errors.New("archive cache is read-only") + return +} + +// Close implements [cache.Cache]. +func (c *Cache) Close() error { + if c.c != nil { + if err := c.c.Close(); err != nil { + return lazyerrors.Error(err) + } + } + + return nil +} + +// OutputFile implements [cache.Cache]. +func (c *Cache) OutputFile(id cache.OutputID) string { + return c.dc.OutputFile(id) +} + +// FuzzDir implements [cache.Cache]. +func (c *Cache) FuzzDir() string { + return +} + +// check interfaces +var ( + _ cache.Cache = (*Cache)(nil) +) diff --git a/internal/caches/local/local.go b/internal/caches/local/local.go index 300cfad..04c8c32 100644 --- a/internal/caches/local/local.go +++ b/internal/caches/local/local.go @@ -1,4 +1,4 @@ -// Package local provides local Go build cache. +// Package local provides local cache, fully compatible with the built-in one. package local import ( @@ -11,7 +11,7 @@ import ( "github.com/AlekSi/hardcache/internal/go/cache" ) -// Cache represents a local Go build cache, compatible with a built-in one. +// Cache represents a local [cache.Cache], fully compatible with the built-in one. // It provides more configuration options for trimming. type Cache struct { dc *cache.DiskCache diff --git a/internal/pack/pack.go b/internal/pack/pack.go new file mode 100644 index 0000000..631628d --- /dev/null +++ b/internal/pack/pack.go @@ -0,0 +1,167 @@ +// Package pack provides functionality for packing and unpacking directories. +package pack + +import ( + "archive/zip" + "compress/flate" + "errors" + "io" + "io/fs" + "os" + + "github.com/AlekSi/lazyerrors" +) + +// Pack compresses the contents of the specified directory +// and writes it to the provided writer in ZIP format with the given comment. +func Pack(dir, comment string, w io.Writer) (resErr error) { + zw := zip.NewWriter(w) + defer func() { + if e := zw.Close(); resErr == nil { + resErr = lazyerrors.Error(e) + } + }() + + zw.RegisterCompressor(zip.Deflate, func(w io.Writer) (io.WriteCloser, error) { + return flate.NewWriter(w, flate.BestCompression) + }) + + root, err := os.OpenRoot(dir) + if err != nil { + resErr = lazyerrors.Error(err) + return + } + + defer func() { + if e := root.Close(); resErr == nil { + resErr = lazyerrors.Error(e) + } + }() + + if err = zw.AddFS(root.FS()); err != nil { + resErr = lazyerrors.Error(err) + return + } + + if err = zw.SetComment(comment); err != nil { + resErr = lazyerrors.Error(err) + return + } + + return +} + +// putFile writes the contents of src file to the dst path. +// If dst already exists, it will be touched, but not overwritten. +func putFile(dst string, src fs.File) (resErr error) { + srcFI, err := src.Stat() + if err != nil { + resErr = lazyerrors.Error(err) + return + } + + dstFI, err := os.Stat(dst) + if err == nil { + if err = os.Chtimes(dst, dstFI.ModTime(), srcFI.ModTime()); err != nil { + resErr = lazyerrors.Error(err) + } + + return + } + + if !errors.Is(err, fs.ErrNotExist) { + resErr = lazyerrors.Error(err) + return + } + + f, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_EXCL, srcFI.Mode()) + if err != nil { + if errors.Is(err, fs.ErrExist) { + // another process created the file in the meantime + return + } + + resErr = lazyerrors.Error(err) + return + } + + defer func() { + if e := f.Close(); resErr == nil { + resErr = lazyerrors.Error(e) + } + }() + + if _, err = io.Copy(f, src); err != nil { + resErr = lazyerrors.Error(err) + return + } + + return +} + +// Unpack extracts the contents of a ZIP archive from the provided reader +// and writes it to the specified directory. +// It also returns the archive comment. +func Unpack(r io.ReaderAt, size int64, dir string) (comment string, resErr error) { + zr, err := zip.NewReader(r, size) + if err != nil { + resErr = lazyerrors.Error(err) + return + } + + comment = zr.Comment + + root, err := os.OpenRoot(dir) + if err != nil { + resErr = lazyerrors.Error(err) + return + } + defer func() { + if e := root.Close(); resErr == nil { + resErr = lazyerrors.Error(e) + } + }() + + for _, f := range zr.File { + fp := f.Name + + if f.FileInfo().IsDir() { + err = root.MkdirAll(fp, f.Mode()) + if err != nil { + resErr = lazyerrors.Error(err) + return + } + + continue + } + + // err = root.MkdirAll(fp[:len(fp)-len(f.FileInfo().Name())], 0o755) + // if err != nil { + // resErr = lazyerrors.Error(err) + // return + // } + + dst, err := root.OpenFile(fp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + resErr = lazyerrors.Error(err) + return + } + + rc, err := f.Open() + if err != nil { + dst.Close() + resErr = lazyerrors.Error(err) + return + } + + _, err = io.Copy(dst, rc) + rc.Close() + dst.Close() + if err != nil { + resErr = lazyerrors.Error(err) + return + } + } + + return +}