github.com/kjk/siser@v0.0.0-20220410204903-1b1e84ea1397/pak/writer.go (about)

     1  package pak
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/sha1"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"os"
    11  	"strconv"
    12  	"time"
    13  
    14  	"github.com/kjk/siser"
    15  )
    16  
    17  const (
    18  	// MetaKeyPath is name of mandatory "Path" meta-value
    19  	MetaKeyPath = "Path"
    20  	// MetaKeySize is name of mandatory "Size" meta-value
    21  	MetaKeySize = "Size"
    22  	// MetaKeySha1 is name of mandatory "Sha1" meta-value
    23  	MetaKeySha1 = "Sha1"
    24  
    25  	archiveName      = "pak-archive2"
    26  	archiveEntryName = "pak-entry"
    27  )
    28  
    29  // Writer is for creating an archive
    30  type Writer struct {
    31  	// Entries is exposed so that we can re-arrange (e.g. sort)
    32  	// them before calling Write
    33  	Entries []*Entry
    34  
    35  	// TODO: add option to conserve memory when writing
    36  }
    37  
    38  // NewWriter creates a new archive writer
    39  func NewWriter() *Writer {
    40  	return &Writer{}
    41  }
    42  
    43  func getFileSize(path string) (int64, error) {
    44  	fi, err := os.Lstat(path)
    45  	if err != nil {
    46  		return 0, err
    47  	}
    48  	return fi.Size(), nil
    49  }
    50  
    51  func sha1HexOfBytes(d []byte) string {
    52  	h := sha1.New()
    53  	h.Write(d)
    54  	return fmt.Sprintf("%x", h.Sum(nil))
    55  }
    56  
    57  // AddFile adds a file from disk to the archive. If meta has "Path"
    58  // value, it'll over-write path of the file in meta-data
    59  func (w *Writer) AddFile(path string, meta Metadata) error {
    60  	size, err := getFileSize(path)
    61  	if err != nil {
    62  		return err
    63  	}
    64  
    65  	// TODO: an option that preserves memory i.e. doesn't keep
    66  	// data in memory. It'll be slower because it'll have to
    67  	// read files twice
    68  	d, err := ioutil.ReadFile(path)
    69  	if err != nil {
    70  		return err
    71  	}
    72  
    73  	e := &Entry{
    74  		srcFilePath: path,
    75  		data:        d,
    76  		Path:        path,
    77  		Size:        size,
    78  		Sha1:        sha1HexOfBytes(d),
    79  		Metadata:    meta,
    80  	}
    81  
    82  	// if "Path" is in meta-data, it over-writes physical path
    83  	if v, ok := meta.Get("Path"); ok {
    84  		if v == "" {
    85  			return ErrNoPath
    86  		}
    87  		e.Path = v
    88  	}
    89  
    90  	w.Entries = append(w.Entries, e)
    91  	return nil
    92  }
    93  
    94  // AddData adds a file from disk to the archive
    95  func (w *Writer) AddData(d []byte, path string, meta Metadata) error {
    96  	if path == "" {
    97  		return ErrNoPath
    98  	}
    99  	sha1 := sha1HexOfBytes(d)
   100  	e := &Entry{
   101  		data:     d,
   102  		Path:     path,
   103  		Size:     int64(len(d)),
   104  		Sha1:     sha1,
   105  		Metadata: meta,
   106  	}
   107  	w.Entries = append(w.Entries, e)
   108  	return nil
   109  }
   110  
   111  func serializeHeader(entries []*Entry) ([]byte, error) {
   112  	var buf bytes.Buffer
   113  	sw := siser.NewWriter(&buf)
   114  
   115  	var r siser.Record
   116  	for _, e := range entries {
   117  		r.Reset()
   118  
   119  		meta := e.Metadata
   120  		meta.Set(MetaKeyPath, e.Path)
   121  		meta.Set(MetaKeySize, strconv.FormatInt(e.Size, 10))
   122  		meta.Set(MetaKeySha1, e.Sha1)
   123  
   124  		for _, kv := range meta.Meta {
   125  			r.Write(kv.Key, kv.Value)
   126  		}
   127  
   128  		r.Name = archiveEntryName
   129  
   130  		_, err := sw.WriteRecord(&r)
   131  		if err != nil {
   132  			return nil, err
   133  		}
   134  	}
   135  	return buf.Bytes(), nil
   136  }
   137  
   138  // WriteToFile writes an archive to a file
   139  func (w *Writer) WriteToFile(path string) error {
   140  	f, err := os.Create(path)
   141  	if err != nil {
   142  		return err
   143  	}
   144  	err = w.Write(f)
   145  	err2 := f.Close()
   146  
   147  	if err != nil || err2 != nil {
   148  		os.Remove(path)
   149  		if err != nil {
   150  			return err
   151  		}
   152  		return err2
   153  	}
   154  	return nil
   155  }
   156  
   157  // Write writes an archive to a writer
   158  func (w *Writer) Write(wr io.Writer) (err error) {
   159  	if wr == nil {
   160  		return errors.New("must provide io.Writer to NewArchiveWriter")
   161  	}
   162  
   163  	if len(w.Entries) == 0 {
   164  		return errors.New("there are 0 entries to write")
   165  	}
   166  
   167  	hdr, err := serializeHeader(w.Entries)
   168  	if err != nil {
   169  		return err
   170  	}
   171  
   172  	sw := siser.NewWriter(wr)
   173  	if _, err = sw.Write(hdr, time.Now(), archiveName); err != nil {
   174  		return err
   175  	}
   176  
   177  	// write files at the end of the archive
   178  	for _, e := range w.Entries {
   179  		d := e.data
   180  		if d == nil && e.srcFilePath != "" {
   181  			d2, err := ioutil.ReadFile(e.srcFilePath)
   182  			if err != nil {
   183  				return err
   184  			}
   185  			d = d2
   186  		}
   187  
   188  		if len(d) == 0 {
   189  			// it's ok to have empty files
   190  			continue
   191  		}
   192  
   193  		if _, err = wr.Write(d); err != nil {
   194  			return err
   195  		}
   196  	}
   197  	return nil
   198  }