This uses sha256 which, after benchmarking, doesn't seem to be any faster or slower than sha1. md5 was surprisingly slower (on aarch64), maybe because there are some CPU accelerated things in sha* ?
252 lines
5.0 KiB
Go
252 lines
5.0 KiB
Go
// Copyright 2021 Clayton Craft <clayton@craftyguy.net>
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
package archive
|
|
|
|
import (
|
|
"bytes"
|
|
"log"
|
|
"os"
|
|
"strings"
|
|
"io"
|
|
"encoding/hex"
|
|
"path/filepath"
|
|
"compress/flate"
|
|
"crypto/sha256"
|
|
"github.com/cavaliercoder/go-cpio"
|
|
"github.com/klauspost/pgzip"
|
|
"github.com/google/renameio"
|
|
"gitlab.com/postmarketOS/mkinitfs/pkgs/misc"
|
|
)
|
|
|
|
type Archive struct {
|
|
Dirs misc.StringSet
|
|
Files misc.StringSet
|
|
cpioWriter *cpio.Writer
|
|
buf *bytes.Buffer
|
|
}
|
|
|
|
func New() (*Archive, error) {
|
|
buf := new(bytes.Buffer)
|
|
archive := &Archive{
|
|
cpioWriter: cpio.NewWriter(buf),
|
|
Files: make(misc.StringSet),
|
|
Dirs: make(misc.StringSet),
|
|
buf: buf,
|
|
}
|
|
|
|
return archive, nil
|
|
}
|
|
|
|
func (archive *Archive) Write(path string, mode os.FileMode) error {
|
|
|
|
if err := archive.writeCpio(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := archive.cpioWriter.Close(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := archive.writeCompressed(path, mode); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func checksum(path string) (string, error) {
|
|
var sum string
|
|
|
|
buf := make([]byte, 64*1024)
|
|
sha256 := sha256.New()
|
|
fd, err := os.Open(path)
|
|
defer fd.Close()
|
|
|
|
if err != nil {
|
|
log.Print("Unable to checksum: ", path)
|
|
return sum, err
|
|
}
|
|
|
|
// Read file in chunks
|
|
for {
|
|
bytes, err := fd.Read(buf)
|
|
if bytes > 0 {
|
|
_, err := sha256.Write(buf[:bytes])
|
|
if err != nil {
|
|
log.Print("Unable to checksum: ", path)
|
|
return sum, err
|
|
}
|
|
}
|
|
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
}
|
|
sum = hex.EncodeToString(sha256.Sum(nil))
|
|
return sum, nil
|
|
}
|
|
|
|
func (archive *Archive) AddFile(file string, dest string) error {
|
|
if err := archive.addDir(filepath.Dir(dest)); err != nil {
|
|
return err
|
|
}
|
|
|
|
if archive.Files[file] {
|
|
// Already written to cpio
|
|
return nil
|
|
}
|
|
|
|
fileStat, err := os.Lstat(file)
|
|
if err != nil {
|
|
log.Print("AddFile: failed to stat file: ", file)
|
|
return err
|
|
}
|
|
|
|
// Symlink: write symlink to archive then set 'file' to link target
|
|
if fileStat.Mode()&os.ModeSymlink != 0 {
|
|
// log.Printf("File %q is a symlink", file)
|
|
target, err := os.Readlink(file)
|
|
if err != nil {
|
|
log.Print("AddFile: failed to get symlink target: ", file)
|
|
return err
|
|
}
|
|
|
|
destFilename := strings.TrimPrefix(dest, "/")
|
|
hdr := &cpio.Header{
|
|
Name: destFilename,
|
|
Linkname: target,
|
|
Mode: 0644 | cpio.ModeSymlink,
|
|
Size: int64(len(target)),
|
|
// Checksum: 1,
|
|
}
|
|
if err := archive.cpioWriter.WriteHeader(hdr); err != nil {
|
|
return err
|
|
}
|
|
if _, err = archive.cpioWriter.Write([]byte(target)); err != nil {
|
|
return err
|
|
}
|
|
|
|
archive.Files[file] = true
|
|
if filepath.Dir(target) == "." {
|
|
target = filepath.Join(filepath.Dir(file), target)
|
|
}
|
|
// make sure target is an absolute path
|
|
if !filepath.IsAbs(target) {
|
|
target, err = misc.RelativeSymlinkTargetToDir(target, filepath.Dir(file))
|
|
}
|
|
// TODO: add verbose mode, print stuff like this:
|
|
// log.Printf("symlink: %q, target: %q", file, target)
|
|
// write symlink target
|
|
err = archive.AddFile(target, target)
|
|
return err
|
|
}
|
|
|
|
// log.Printf("writing file: %q", file)
|
|
|
|
fd, err := os.Open(file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer fd.Close()
|
|
|
|
destFilename := strings.TrimPrefix(dest, "/")
|
|
hdr := &cpio.Header{
|
|
Name: destFilename,
|
|
Mode: cpio.FileMode(fileStat.Mode().Perm()),
|
|
Size: fileStat.Size(),
|
|
// Checksum: 1,
|
|
}
|
|
if err := archive.cpioWriter.WriteHeader(hdr); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err = io.Copy(archive.cpioWriter, fd); err != nil {
|
|
return err
|
|
}
|
|
|
|
archive.Files[file] = true
|
|
|
|
return nil
|
|
}
|
|
|
|
func (archive *Archive) writeCompressed(path string, mode os.FileMode) error {
|
|
tFile, err := renameio.TempFile("", path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tFile.Cleanup()
|
|
|
|
// TODO: support other compression formats, based on deviceinfo
|
|
gz, err := pgzip.NewWriterLevel(tFile, flate.BestSpeed)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err = io.Copy(gz, archive.buf); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := gz.Close(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := tFile.CloseAtomicallyReplace(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := os.Chmod(path, mode); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (archive *Archive) writeCpio() error {
|
|
// Write any dirs added explicitly
|
|
for dir := range archive.Dirs {
|
|
archive.addDir(dir)
|
|
}
|
|
|
|
// Write files and any missing parent dirs
|
|
for file, imported := range archive.Files {
|
|
if imported {
|
|
continue
|
|
}
|
|
if err := archive.AddFile(file, file); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (archive *Archive) addDir(dir string) error {
|
|
if archive.Dirs[dir] {
|
|
// Already imported
|
|
return nil
|
|
}
|
|
if dir == "/" {
|
|
dir = "."
|
|
}
|
|
|
|
subdirs := strings.Split(strings.TrimPrefix(dir, "/"), "/")
|
|
for i, subdir := range subdirs {
|
|
path := filepath.Join(strings.Join(subdirs[:i], "/"), subdir)
|
|
if archive.Dirs[path] {
|
|
// Subdir already imported
|
|
continue
|
|
}
|
|
err := archive.cpioWriter.WriteHeader(&cpio.Header{
|
|
Name: path,
|
|
Mode: cpio.ModeDir | 0755,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
archive.Dirs[path] = true
|
|
// log.Print("wrote dir: ", path)
|
|
}
|
|
|
|
return nil
|
|
}
|