From 3936cf233ea37c63865c05588d888b647d0d9aba Mon Sep 17 00:00:00 2001 From: Patrick Pacher Date: Wed, 13 May 2020 08:15:27 +0200 Subject: [PATCH] Add support for packed resource files --- updater/file.go | 61 ++++++++++++++++++++++++++- updater/get.go | 3 +- updater/packers.go | 13 ++++++ utils/atomic.go | 101 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 updater/packers.go create mode 100644 utils/atomic.go diff --git a/updater/file.go b/updater/file.go index 4ed412f..88e044a 100644 --- a/updater/file.go +++ b/updater/file.go @@ -1,6 +1,13 @@ package updater -import "github.com/safing/portbase/log" +import ( + "io" + "os" + "strings" + + "github.com/safing/portbase/log" + "github.com/safing/portbase/utils" +) // File represents a file from the update system. type File struct { @@ -42,3 +49,55 @@ func (file *File) markActiveWithLocking() { file.resource.ActiveVersion = file.version } } + +// Unpacker describes the function that is passed to +// File.Unpack. It receives a reader to the compressed/packed +// file and should return a reader that provides +// unpacked file contents. If the returned reader implements +// io.Closer it's close method is invoked when an error +// or io.EOF is returned from Read(). +type Unpacker func(io.Reader) (io.Reader, error) + +// Unpack returns the path to the unpacked version of file and +// unpacks it on demand using unpacker. +func (file *File) Unpack(suffix string, unpacker Unpacker) (string, error) { + path := strings.TrimSuffix(file.Path(), suffix) + + if suffix == "" { + path += "-unpacked" + } + + _, err := os.Stat(path) + if err == nil { + return path, nil + } + + if !os.IsNotExist(err) { + return "", err + } + + f, err := os.Open(file.Path()) + if err != nil { + return "", err + } + defer f.Close() + + r, err := unpacker(f) + if err != nil { + return "", err + } + + ioErr := utils.CreateAtomic(path, r, &utils.AtomicFileOptions{ + TempDir: file.resource.registry.TmpDir().Path, + }) + + if c, ok := r.(io.Closer); ok { + if err := c.Close(); err != nil && ioErr == nil { + // if ioErr is already set we ignore the error from + // closing the unpacker. + ioErr = err + } + } + + return path, ioErr +} diff --git a/updater/get.go b/updater/get.go index 2659034..9b92124 100644 --- a/updater/get.go +++ b/updater/get.go @@ -13,7 +13,8 @@ var ( ErrNotAvailableLocally = errors.New("the requested file is not available locally") ) -// GetFile returns the selected (mostly newest) file with the given identifier or an error, if it fails. +// GetFile returns the selected (mostly newest) file with the given +// identifier or an error, if it fails. func (reg *ResourceRegistry) GetFile(identifier string) (*File, error) { reg.RLock() res, ok := reg.resources[identifier] diff --git a/updater/packers.go b/updater/packers.go new file mode 100644 index 0000000..f7d9d14 --- /dev/null +++ b/updater/packers.go @@ -0,0 +1,13 @@ +package updater + +import ( + "compress/gzip" + "io" +) + +// UnpackGZIP unpacks a GZIP compressed reader r +// and returns a new reader. It's suitable to be +// used with registry.GetPackedFile. +func UnpackGZIP(r io.Reader) (io.Reader, error) { + return gzip.NewReader(r) +} diff --git a/utils/atomic.go b/utils/atomic.go new file mode 100644 index 0000000..45fd66d --- /dev/null +++ b/utils/atomic.go @@ -0,0 +1,101 @@ +package utils + +import ( + "fmt" + "io" + "os" + + "github.com/google/renameio" +) + +// AtomicFileOptions holds additional options for manipulating +// the behavior of CreateAtomic and friends. +type AtomicFileOptions struct { + // Mode is the file mode for the new file. If + // 0, the file mode will be set to 0600. + Mode os.FileMode + + // TempDir is the path to the temp-directory + // that should be used. If empty, it defaults + // to the system temp. + TempDir string +} + +// CreateAtomic creates or overwrites a file at dest atomically using +// data from r. Atomic means that even in case of a power outage, +// dest will never be a zero-length file. It will always either contain +// the previous data (or not exist) or the new data but never anything +// in between. +func CreateAtomic(dest string, r io.Reader, opts *AtomicFileOptions) error { + if opts == nil { + opts = &AtomicFileOptions{} + } + + tmpFile, err := renameio.TempFile(opts.TempDir, dest) + if err != nil { + return fmt.Errorf("failed to create temp file: %w", err) + } + defer tmpFile.Cleanup() //nolint:errcheck + + if opts.Mode != 0 { + if err := tmpFile.Chmod(opts.Mode); err != nil { + return fmt.Errorf("failed to update mode bits of temp file: %w", err) + } + } + + if _, err := io.Copy(tmpFile, r); err != nil { + return fmt.Errorf("failed to copy source file: %w", err) + } + + if err := tmpFile.CloseAtomicallyReplace(); err != nil { + return fmt.Errorf("failed to rename temp file to %q", dest) + } + + return nil +} + +// CopyFileAtomic is like CreateAtomic but copies content from +// src to dest. If opts.Mode is 0 CopyFileAtomic tries to set +// the file mode of src to dest. +func CopyFileAtomic(dest string, src string, opts *AtomicFileOptions) error { + if opts == nil { + opts = &AtomicFileOptions{} + } + + if opts.Mode == 0 { + stat, err := os.Stat(src) + if err != nil { + return err + } + opts.Mode = stat.Mode() + } + + f, err := os.Open(src) + if err != nil { + return err + } + defer f.Close() + + return CreateAtomic(dest, f, opts) +} + +// ReplaceFileAtomic replaces the file at dest with the content from src. +// If dest exists it's file mode copied and used for the replacement. If +// not, dest will get the same file mode as src. See CopyFileAtomic and +// CreateAtomic for more information. +func ReplaceFileAtomic(dest string, src string, opts *AtomicFileOptions) error { + if opts == nil { + opts = &AtomicFileOptions{} + } + + if opts.Mode == 0 { + stat, err := os.Stat(dest) + if err == nil { + opts.Mode = stat.Mode() + } else if !os.IsNotExist(err) { + return err + } + } + + return CopyFileAtomic(dest, src, opts) +}