2014-11-04 00:05:58 +00:00
|
|
|
package base
|
|
|
|
|
|
|
|
import (
|
|
|
|
"archive/tar"
|
|
|
|
"compress/gzip"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
2017-09-09 19:58:57 +01:00
|
|
|
"io/ioutil"
|
|
|
|
"log"
|
2017-09-10 20:35:34 +01:00
|
|
|
"os"
|
|
|
|
"reflect"
|
2014-11-04 00:05:58 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2017-09-09 19:58:57 +01:00
|
|
|
SerializationFormatVersion = "golearn 1.0"
|
2014-11-04 00:05:58 +00:00
|
|
|
)
|
|
|
|
|
2017-09-09 19:58:57 +01:00
|
|
|
// FunctionalTarReader allows you to read anything in a tar file in any order, rather than just
|
|
|
|
// sequentially.
|
|
|
|
type FunctionalTarReader struct {
|
2017-09-10 20:35:34 +01:00
|
|
|
Regenerate func() *tar.Reader
|
2014-11-04 00:05:58 +00:00
|
|
|
}
|
|
|
|
|
2017-09-09 19:58:57 +01:00
|
|
|
// NewFunctionalTarReader creates a new FunctionalTarReader using a function that it can call
|
|
|
|
// to get a tar.Reader at the beginning of the file.
|
2017-09-10 20:35:34 +01:00
|
|
|
func NewFunctionalTarReader(regenFunc func() *tar.Reader) *FunctionalTarReader {
|
|
|
|
return &FunctionalTarReader{
|
2017-09-09 19:58:57 +01:00
|
|
|
regenFunc,
|
2014-11-04 00:05:58 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-09 19:58:57 +01:00
|
|
|
// GetNamedFile returns a file named a given thing from the tar file. If there's more than one
|
|
|
|
// entry, the most recent is returned.
|
2017-09-10 20:35:34 +01:00
|
|
|
func (f *FunctionalTarReader) GetNamedFile(name string) ([]byte, error) {
|
2017-09-09 19:58:57 +01:00
|
|
|
tr := f.Regenerate()
|
2014-11-04 00:05:58 +00:00
|
|
|
|
2017-09-09 19:58:57 +01:00
|
|
|
var returnCandidate []byte = nil
|
2014-11-04 00:05:58 +00:00
|
|
|
for {
|
|
|
|
hdr, err := tr.Next()
|
|
|
|
if err == io.EOF {
|
|
|
|
break
|
|
|
|
} else if err != nil {
|
2017-09-09 19:58:57 +01:00
|
|
|
return nil, err
|
2014-11-04 00:05:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if hdr.Name == name {
|
2017-09-10 21:10:54 +01:00
|
|
|
ret, err := ioutil.ReadAll(tr)
|
|
|
|
if err != nil {
|
|
|
|
return nil, WrapError(err)
|
|
|
|
}
|
2021-01-08 00:22:00 -03:00
|
|
|
|
2017-09-10 21:10:54 +01:00
|
|
|
if int64(len(ret)) != hdr.Size {
|
|
|
|
if int64(len(ret)) < hdr.Size {
|
|
|
|
log.Printf("Size mismatch, got %d byte(s) for %s, expected %d (err was %s)", len(ret), hdr.Name, hdr.Size, err)
|
2017-09-10 19:30:02 +01:00
|
|
|
} else {
|
2017-09-10 21:10:54 +01:00
|
|
|
return nil, WrapError(fmt.Errorf("Size mismatch, expected %d byte(s) for %s, got %d", len(ret), hdr.Name, hdr.Size))
|
2017-09-10 19:30:02 +01:00
|
|
|
}
|
2014-11-04 00:05:58 +00:00
|
|
|
}
|
2021-01-08 00:22:00 -03:00
|
|
|
|
2017-09-09 19:58:57 +01:00
|
|
|
returnCandidate = ret
|
2021-01-08 00:22:00 -03:00
|
|
|
break
|
2014-11-04 00:05:58 +00:00
|
|
|
}
|
|
|
|
}
|
2017-09-09 19:58:57 +01:00
|
|
|
if returnCandidate == nil {
|
|
|
|
return nil, WrapError(fmt.Errorf("Not found (looking for %s)", name))
|
2014-11-04 00:05:58 +00:00
|
|
|
}
|
2017-09-09 19:58:57 +01:00
|
|
|
return returnCandidate, nil
|
2017-09-09 12:31:32 +01:00
|
|
|
}
|
|
|
|
|
2017-09-09 19:58:57 +01:00
|
|
|
func tarPrefix(prefix string, suffix string) string {
|
|
|
|
if prefix == "" {
|
|
|
|
return suffix
|
2014-11-04 00:05:58 +00:00
|
|
|
}
|
2018-03-23 23:39:55 +00:00
|
|
|
return fmt.Sprintf("%s/%s", prefix, suffix)
|
2014-11-04 00:05:58 +00:00
|
|
|
}
|
|
|
|
|
2017-08-07 14:43:21 +01:00
|
|
|
// ClassifierMetadataV1 is what gets written into METADATA
|
|
|
|
// in a classification file format.
|
|
|
|
type ClassifierMetadataV1 struct {
|
|
|
|
// FormatVersion should always be 1 for this structure
|
|
|
|
FormatVersion int `json:"format_version"`
|
|
|
|
// Uses the classifier name (provided by the classifier)
|
|
|
|
ClassifierName string `json:"classifier"`
|
|
|
|
// ClassifierVersion is also provided by the classifier
|
|
|
|
// and checks whether this version of GoLearn can read what's
|
|
|
|
// be written.
|
|
|
|
ClassifierVersion string `json"classifier_version"`
|
|
|
|
// This is a custom metadata field, provided by the classifier
|
|
|
|
ClassifierMetadata map[string]interface{} `json:"classifier_metadata"`
|
|
|
|
}
|
|
|
|
|
2017-09-09 19:58:57 +01:00
|
|
|
// ClassifierDeserializer attaches helper functions useful for reading classificatiers. (UNSTABLE).
|
2017-08-07 14:43:21 +01:00
|
|
|
type ClassifierDeserializer struct {
|
|
|
|
gzipReader io.Reader
|
2017-08-26 14:56:17 +01:00
|
|
|
fileReader io.ReadCloser
|
2017-09-10 20:35:34 +01:00
|
|
|
tarReader *FunctionalTarReader
|
2017-08-07 14:43:21 +01:00
|
|
|
Metadata *ClassifierMetadataV1
|
|
|
|
}
|
|
|
|
|
2017-09-09 19:58:57 +01:00
|
|
|
// Prefix outputs a string in the right format for TAR
|
|
|
|
func (c *ClassifierDeserializer) Prefix(prefix string, suffix string) string {
|
|
|
|
if prefix == "" {
|
|
|
|
return suffix
|
|
|
|
}
|
|
|
|
return fmt.Sprintf("%s/%s", prefix, suffix)
|
|
|
|
}
|
|
|
|
|
|
|
|
// ReadMetadataAtPrefix reads the METADATA file after prefix. If an error is returned, the first value is undefined.
|
|
|
|
func (c *ClassifierDeserializer) ReadMetadataAtPrefix(prefix string) (ClassifierMetadataV1, error) {
|
|
|
|
var ret ClassifierMetadataV1
|
|
|
|
err := c.GetJSONForKey(c.Prefix(prefix, "METADATA"), &ret)
|
|
|
|
return ret, err
|
|
|
|
}
|
|
|
|
|
2017-08-26 14:56:17 +01:00
|
|
|
// ReadSerializedClassifierStub is the counterpart of CreateSerializedClassifierStub.
|
|
|
|
// It's used inside SaveableClassifiers to read information from a perviously saved
|
|
|
|
// model file.
|
2017-08-07 14:43:21 +01:00
|
|
|
func ReadSerializedClassifierStub(filePath string) (*ClassifierDeserializer, error) {
|
|
|
|
|
|
|
|
f, err := os.Open(filePath)
|
|
|
|
if err != nil {
|
2017-09-09 19:58:57 +01:00
|
|
|
return nil, DescribeError("Can't open file", err)
|
2017-08-07 14:43:21 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
gzr, err := gzip.NewReader(f)
|
|
|
|
if err != nil {
|
2017-09-09 19:58:57 +01:00
|
|
|
return nil, DescribeError("Can't decompress", err)
|
2017-08-07 14:43:21 +01:00
|
|
|
}
|
|
|
|
|
2017-09-09 19:58:57 +01:00
|
|
|
regenerateFunc := func() *tar.Reader {
|
|
|
|
f.Seek(0, os.SEEK_SET)
|
|
|
|
gzr.Reset(f)
|
|
|
|
tz := tar.NewReader(gzr)
|
|
|
|
return tz
|
|
|
|
}
|
|
|
|
|
|
|
|
tz := NewFunctionalTarReader(regenerateFunc)
|
2017-08-07 14:43:21 +01:00
|
|
|
|
|
|
|
// Check that the serialization format is right
|
|
|
|
// Retrieve the MANIFEST and verify
|
2017-09-09 19:58:57 +01:00
|
|
|
manifestBytes, err := tz.GetNamedFile("CLS_MANIFEST")
|
|
|
|
if err != nil {
|
|
|
|
return nil, DescribeError("Error reading CLS_MANIFEST", err)
|
|
|
|
}
|
2017-08-07 14:43:21 +01:00
|
|
|
if !reflect.DeepEqual(manifestBytes, []byte(SerializationFormatVersion)) {
|
2017-09-09 19:58:57 +01:00
|
|
|
return nil, fmt.Errorf("Unsupported CLS_MANIFEST: %s", string(manifestBytes))
|
2017-08-07 14:43:21 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
//
|
|
|
|
// Parse METADATA
|
|
|
|
//
|
|
|
|
var metadata ClassifierMetadataV1
|
2017-08-26 14:56:17 +01:00
|
|
|
ret := &ClassifierDeserializer{
|
|
|
|
f,
|
|
|
|
gzr,
|
|
|
|
tz,
|
|
|
|
&metadata,
|
|
|
|
}
|
|
|
|
|
2017-09-09 19:58:57 +01:00
|
|
|
metadata, err = ret.ReadMetadataAtPrefix("")
|
2017-08-07 14:43:21 +01:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("Error whilst reading METADATA: %s", err)
|
|
|
|
}
|
2017-09-09 19:58:57 +01:00
|
|
|
ret.Metadata = &metadata
|
2017-08-07 14:43:21 +01:00
|
|
|
|
|
|
|
// Check that we can understand this archive
|
|
|
|
if metadata.FormatVersion != 1 {
|
|
|
|
return nil, fmt.Errorf("METADATA: wrong format_version for this version of golearn")
|
|
|
|
}
|
|
|
|
|
|
|
|
return ret, nil
|
|
|
|
}
|
|
|
|
|
2017-08-26 14:56:17 +01:00
|
|
|
// GetBytesForKey returns the bytes at a given location in the output.
|
2017-08-07 14:43:21 +01:00
|
|
|
func (c *ClassifierDeserializer) GetBytesForKey(key string) ([]byte, error) {
|
2017-09-09 19:58:57 +01:00
|
|
|
return c.tarReader.GetNamedFile(key)
|
2017-08-07 14:43:21 +01:00
|
|
|
}
|
|
|
|
|
2017-09-10 16:59:05 +01:00
|
|
|
func (c *ClassifierDeserializer) GetStringForKey(key string) (string, error) {
|
|
|
|
b, err := c.GetBytesForKey(key)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
return string(b), err
|
|
|
|
}
|
|
|
|
|
2017-08-26 14:56:17 +01:00
|
|
|
// GetJSONForKey deserializes a JSON key in the output file.
|
|
|
|
func (c *ClassifierDeserializer) GetJSONForKey(key string, v interface{}) error {
|
|
|
|
b, err := c.GetBytesForKey(key)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return json.Unmarshal(b, v)
|
|
|
|
}
|
2017-08-07 17:26:11 +01:00
|
|
|
|
2017-09-09 19:58:57 +01:00
|
|
|
// GetInstancesForKey deserializes some instances stored in a classifier output file
|
|
|
|
func (c *ClassifierDeserializer) GetInstancesForKey(key string) (FixedDataGrid, error) {
|
|
|
|
return DeserializeInstancesFromTarReader(c.tarReader, key)
|
|
|
|
}
|
|
|
|
|
2017-09-09 12:31:32 +01:00
|
|
|
// GetUInt64ForKey returns a int64 stored at a given key
|
|
|
|
func (c *ClassifierDeserializer) GetU64ForKey(key string) (uint64, error) {
|
|
|
|
b, err := c.GetBytesForKey(key)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
return UnpackBytesToU64(b), nil
|
|
|
|
}
|
|
|
|
|
2017-09-10 16:59:05 +01:00
|
|
|
// GetAttributeForKey returns an Attribute stored at a given key
|
|
|
|
func (c *ClassifierDeserializer) GetAttributeForKey(key string) (Attribute, error) {
|
|
|
|
b, err := c.GetBytesForKey(key)
|
|
|
|
if err != nil {
|
|
|
|
return nil, WrapError(err)
|
|
|
|
}
|
|
|
|
attr, err := DeserializeAttribute(b)
|
|
|
|
if err != nil {
|
|
|
|
return nil, WrapError(err)
|
|
|
|
}
|
|
|
|
return attr, nil
|
|
|
|
}
|
|
|
|
|
2017-09-10 20:10:53 +01:00
|
|
|
// GetAttributesForKey returns an Attribute list stored at a given key
|
|
|
|
func (c *ClassifierDeserializer) GetAttributesForKey(key string) ([]Attribute, error) {
|
|
|
|
|
|
|
|
attrCountKey := c.Prefix(key, "ATTR_COUNT")
|
|
|
|
attrCount, err := c.GetU64ForKey(attrCountKey)
|
|
|
|
if err != nil {
|
|
|
|
return nil, DescribeError("Unable to read ATTR_COUNT", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
ret := make([]Attribute, attrCount)
|
|
|
|
|
|
|
|
for i := range ret {
|
|
|
|
attrKey := c.Prefix(key, fmt.Sprintf("%d", i))
|
|
|
|
ret[i], err = c.GetAttributeForKey(attrKey)
|
|
|
|
if err != nil {
|
|
|
|
return nil, DescribeError("Unable to read Attribute", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ret, nil
|
|
|
|
}
|
|
|
|
|
2017-08-26 14:56:17 +01:00
|
|
|
// Close cleans up everything.
|
|
|
|
func (c *ClassifierDeserializer) Close() {
|
|
|
|
c.fileReader.Close()
|
2017-08-07 17:26:11 +01:00
|
|
|
}
|
|
|
|
|
2017-08-26 14:56:17 +01:00
|
|
|
// ClassifierSerializer is an object used by SaveableClassifiers.
|
2017-08-07 14:43:21 +01:00
|
|
|
type ClassifierSerializer struct {
|
|
|
|
gzipWriter *gzip.Writer
|
2017-09-10 21:10:54 +01:00
|
|
|
fileWriter *os.File
|
2017-08-07 14:43:21 +01:00
|
|
|
tarWriter *tar.Writer
|
2017-09-10 20:35:34 +01:00
|
|
|
f *os.File
|
|
|
|
filePath string
|
2017-08-07 14:43:21 +01:00
|
|
|
}
|
|
|
|
|
2017-08-26 14:56:17 +01:00
|
|
|
// Close finalizes the Classifier serialization session.
|
2017-08-07 14:43:21 +01:00
|
|
|
func (c *ClassifierSerializer) Close() error {
|
|
|
|
|
|
|
|
// Finally, close and flush the various levels
|
|
|
|
if err := c.tarWriter.Flush(); err != nil {
|
|
|
|
return fmt.Errorf("Could not flush tar: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := c.tarWriter.Close(); err != nil {
|
|
|
|
return fmt.Errorf("Could not close tar: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := c.gzipWriter.Flush(); err != nil {
|
|
|
|
return fmt.Errorf("Could not flush gz: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := c.gzipWriter.Close(); err != nil {
|
|
|
|
return fmt.Errorf("Could not close gz: %s", err)
|
|
|
|
}
|
|
|
|
|
2017-09-10 21:10:54 +01:00
|
|
|
if err := c.fileWriter.Sync(); err != nil {
|
|
|
|
return fmt.Errorf("Could not close file writer: %s", err)
|
|
|
|
}
|
|
|
|
|
2017-08-26 14:56:17 +01:00
|
|
|
if err := c.fileWriter.Close(); err != nil {
|
2017-09-09 19:58:57 +01:00
|
|
|
return fmt.Errorf("Could not close file writer: %s", err)
|
2017-08-26 14:56:17 +01:00
|
|
|
}
|
2017-08-07 14:43:21 +01:00
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2017-08-26 14:56:17 +01:00
|
|
|
// WriteBytesForKey creates a new entry in the serializer file with some user-defined bytes.
|
2017-08-07 14:43:21 +01:00
|
|
|
func (c *ClassifierSerializer) WriteBytesForKey(key string, b []byte) error {
|
|
|
|
|
|
|
|
//
|
|
|
|
// Write header for key
|
|
|
|
//
|
|
|
|
hdr := &tar.Header{
|
|
|
|
Name: key,
|
|
|
|
Size: int64(len(b)),
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := c.tarWriter.WriteHeader(hdr); err != nil {
|
2017-08-26 14:56:17 +01:00
|
|
|
return fmt.Errorf("Could not write header for '%s': %s", key, err)
|
2017-08-07 14:43:21 +01:00
|
|
|
}
|
|
|
|
//
|
|
|
|
// Write data
|
|
|
|
//
|
|
|
|
if _, err := c.tarWriter.Write(b); err != nil {
|
2017-08-26 14:56:17 +01:00
|
|
|
return fmt.Errorf("Could not write data for '%s': %s", key, err)
|
2017-08-07 14:43:21 +01:00
|
|
|
}
|
|
|
|
|
2017-09-10 21:10:54 +01:00
|
|
|
c.tarWriter.Flush()
|
|
|
|
c.gzipWriter.Flush()
|
|
|
|
c.fileWriter.Sync()
|
2017-08-07 14:43:21 +01:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2017-09-10 16:59:05 +01:00
|
|
|
// WriteU64ForKey creates a new entry in the serializer file with the bytes of a uint64
|
|
|
|
func (c *ClassifierSerializer) WriteU64ForKey(key string, v uint64) error {
|
|
|
|
b := PackU64ToBytes(v)
|
|
|
|
return c.WriteBytesForKey(key, b)
|
|
|
|
}
|
|
|
|
|
2017-08-26 14:56:17 +01:00
|
|
|
// WriteJSONForKey creates a new entry in the file with an interface serialized as JSON.
|
|
|
|
func (c *ClassifierSerializer) WriteJSONForKey(key string, v interface{}) error {
|
|
|
|
|
|
|
|
b, err := json.Marshal(v)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return c.WriteBytesForKey(key, b)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2017-09-10 16:59:05 +01:00
|
|
|
// WriteAttributeForKey creates a new entry in the file containing a serialized representation of Attribute
|
|
|
|
func (c *ClassifierSerializer) WriteAttributeForKey(key string, a Attribute) error {
|
|
|
|
b, err := SerializeAttribute(a)
|
|
|
|
if err != nil {
|
|
|
|
return WrapError(err)
|
|
|
|
}
|
|
|
|
return c.WriteBytesForKey(key, b)
|
|
|
|
}
|
|
|
|
|
2017-09-10 20:10:53 +01:00
|
|
|
// WriteAttributesForKey does the same as WriteAttributeForKey, just with more than one Attribute.
|
|
|
|
func (c *ClassifierSerializer) WriteAttributesForKey(key string, attrs []Attribute) error {
|
|
|
|
|
|
|
|
attrCountKey := c.Prefix(key, "ATTR_COUNT")
|
|
|
|
err := c.WriteU64ForKey(attrCountKey, uint64(len(attrs)))
|
|
|
|
if err != nil {
|
|
|
|
return DescribeError("Unable to write ATTR_COUNT", err)
|
|
|
|
}
|
|
|
|
for i, a := range attrs {
|
|
|
|
attrKey := c.Prefix(key, fmt.Sprintf("%d", i))
|
|
|
|
err = c.WriteAttributeForKey(attrKey, a)
|
|
|
|
if err != nil {
|
|
|
|
return DescribeError("Unable to write Attribute", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2017-09-09 19:58:57 +01:00
|
|
|
// WriteInstances for key creates a new entry in the file containing some training instances
|
2017-09-09 12:31:32 +01:00
|
|
|
func (c *ClassifierSerializer) WriteInstancesForKey(key string, g FixedDataGrid, includeData bool) error {
|
2017-09-09 19:58:57 +01:00
|
|
|
fmt.Sprintf("%v", c)
|
|
|
|
return SerializeInstancesToTarWriter(g, c.tarWriter, key, includeData)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Prefix outputs a string in the right format for TAR
|
|
|
|
func (c *ClassifierSerializer) Prefix(prefix string, suffix string) string {
|
|
|
|
if prefix == "" {
|
|
|
|
return suffix
|
|
|
|
}
|
|
|
|
return fmt.Sprintf("%s/%s", prefix, suffix)
|
|
|
|
}
|
|
|
|
|
|
|
|
// WriteMetadataAtPrefix outputs a METADATA entry in the right place
|
|
|
|
func (c *ClassifierSerializer) WriteMetadataAtPrefix(prefix string, metadata ClassifierMetadataV1) error {
|
|
|
|
return c.WriteJSONForKey(c.Prefix(prefix, "METADATA"), &metadata)
|
2017-09-09 12:31:32 +01:00
|
|
|
}
|
|
|
|
|
2017-08-07 14:43:21 +01:00
|
|
|
// CreateSerializedClassifierStub generates a file to serialize into
|
|
|
|
// and writes the METADATA header.
|
|
|
|
func CreateSerializedClassifierStub(filePath string, metadata ClassifierMetadataV1) (*ClassifierSerializer, error) {
|
|
|
|
|
2018-01-27 18:00:52 +00:00
|
|
|
// Open the filePath
|
2018-01-28 22:55:17 +00:00
|
|
|
f, err := os.OpenFile(filePath, os.O_RDWR|os.O_TRUNC|os.O_CREATE, 0600)
|
2017-08-07 14:43:21 +01:00
|
|
|
if err != nil {
|
2017-09-09 19:58:57 +01:00
|
|
|
return nil, err
|
2017-08-07 14:43:21 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
var hdr *tar.Header
|
|
|
|
gzWriter := gzip.NewWriter(f)
|
|
|
|
tw := tar.NewWriter(gzWriter)
|
|
|
|
|
2017-09-09 19:58:57 +01:00
|
|
|
ret := ClassifierSerializer{
|
2017-08-07 14:43:21 +01:00
|
|
|
gzipWriter: gzWriter,
|
|
|
|
fileWriter: f,
|
2017-09-10 20:35:34 +01:00
|
|
|
tarWriter: tw,
|
2017-08-07 14:43:21 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
//
|
|
|
|
// Write the MANIFEST entry
|
|
|
|
//
|
|
|
|
hdr = &tar.Header{
|
2017-09-09 19:58:57 +01:00
|
|
|
Name: "CLS_MANIFEST",
|
2017-08-07 14:43:21 +01:00
|
|
|
Size: int64(len(SerializationFormatVersion)),
|
|
|
|
}
|
|
|
|
if err := tw.WriteHeader(hdr); err != nil {
|
2017-09-09 19:58:57 +01:00
|
|
|
return nil, fmt.Errorf("Could not write CLS_MANIFEST header: %s", err)
|
2017-08-07 14:43:21 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if _, err := tw.Write([]byte(SerializationFormatVersion)); err != nil {
|
2017-09-09 19:58:57 +01:00
|
|
|
return nil, fmt.Errorf("Could not write CLS_MANIFEST contents: %s", err)
|
2017-08-07 14:43:21 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
//
|
|
|
|
// Write the METADATA entry
|
|
|
|
//
|
2017-09-09 19:58:57 +01:00
|
|
|
err = ret.WriteMetadataAtPrefix("", metadata)
|
2017-08-07 14:43:21 +01:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("JSON marshal error: %s", err)
|
|
|
|
}
|
|
|
|
|
2017-09-09 19:58:57 +01:00
|
|
|
return &ret, nil
|
2017-08-07 14:43:21 +01:00
|
|
|
|
2014-11-04 00:05:58 +00:00
|
|
|
}
|