veyron/services/mgmt: Implement optional packages
This change implements optional packages in the mgmt services. The
binary repository now saves the media type and encoding of the objects,
e.g type=application/x-tar,encoding=gzip, or type=application/zip.
The node manager uses this information to download and install the
packages requested in the application envelopes.
TODO: Test the node manager implementation. This will require either using the
real binaryd, or significantly improving the mock binaryd.
Change-Id: Idc7d0ee7663ccd1816a271289de8f61403d8dcce
diff --git a/services/mgmt/lib/binary/impl.go b/services/mgmt/lib/binary/impl.go
index a1bedb8..07bedd9 100644
--- a/services/mgmt/lib/binary/impl.go
+++ b/services/mgmt/lib/binary/impl.go
@@ -18,6 +18,8 @@
"veyron.io/veyron/veyron2/services/mgmt/repository"
"veyron.io/veyron/veyron2/verror"
"veyron.io/veyron/veyron2/vlog"
+
+ "veyron.io/veyron/veyron/services/mgmt/lib/packages"
)
var (
@@ -41,17 +43,16 @@
return nil
}
-func download(ctx context.T, w io.WriteSeeker, von string) error {
+func download(ctx context.T, w io.WriteSeeker, von string) (repository.MediaInfo, error) {
client := repository.BinaryClient(von)
- // TODO(rthellend): Use the media type.
- parts, _, err := client.Stat(ctx)
+ parts, mediaInfo, err := client.Stat(ctx)
if err != nil {
vlog.Errorf("Stat() failed: %v", err)
- return err
+ return repository.MediaInfo{}, err
}
for _, part := range parts {
if part.Checksum == binary.MissingChecksum {
- return errNotExist
+ return repository.MediaInfo{}, errNotExist
}
}
offset, whence := int64(0), 0
@@ -102,33 +103,34 @@
success = true
}
if !success {
- return errOperationFailed
+ return repository.MediaInfo{}, errOperationFailed
}
offset += part.Size
}
- return nil
+ return mediaInfo, nil
}
-func Download(ctx context.T, von string) ([]byte, error) {
+func Download(ctx context.T, von string) ([]byte, repository.MediaInfo, error) {
dir, prefix := "", ""
file, err := ioutil.TempFile(dir, prefix)
if err != nil {
vlog.Errorf("TempFile(%v, %v) failed: %v", dir, prefix, err)
- return nil, errOperationFailed
+ return nil, repository.MediaInfo{}, errOperationFailed
}
defer os.Remove(file.Name())
defer file.Close()
ctx, cancel := ctx.WithTimeout(time.Minute)
defer cancel()
- if err := download(ctx, file, von); err != nil {
- return nil, errOperationFailed
+ mediaInfo, err := download(ctx, file, von)
+ if err != nil {
+ return nil, repository.MediaInfo{}, errOperationFailed
}
bytes, err := ioutil.ReadFile(file.Name())
if err != nil {
vlog.Errorf("ReadFile(%v) failed: %v", file.Name(), err)
- return nil, errOperationFailed
+ return nil, repository.MediaInfo{}, errOperationFailed
}
- return bytes, nil
+ return bytes, mediaInfo, nil
}
func DownloadToFile(ctx context.T, von, path string) error {
@@ -141,13 +143,14 @@
defer file.Close()
ctx, cancel := ctx.WithTimeout(time.Minute)
defer cancel()
- if err := download(ctx, file, von); err != nil {
+ mediaInfo, err := download(ctx, file, von)
+ if err != nil {
if err := os.Remove(file.Name()); err != nil {
vlog.Errorf("Remove(%v) failed: %v", file.Name(), err)
}
return errOperationFailed
}
- perm := os.FileMode(0700)
+ perm := os.FileMode(0600)
if err := file.Chmod(perm); err != nil {
vlog.Errorf("Chmod(%v) failed: %v", perm, err)
if err := os.Remove(file.Name()); err != nil {
@@ -162,10 +165,17 @@
}
return errOperationFailed
}
+ if err := packages.SaveMediaInfo(path, mediaInfo); err != nil {
+ vlog.Errorf("packages.SaveMediaInfo(%v, %v) failed: %v", path, mediaInfo, err)
+ if err := os.Remove(path); err != nil {
+ vlog.Errorf("Remove(%v) failed: %v", path, err)
+ }
+ return errOperationFailed
+ }
return nil
}
-func upload(ctx context.T, r io.ReadSeeker, von string) error {
+func upload(ctx context.T, r io.ReadSeeker, mediaInfo repository.MediaInfo, von string) error {
client := repository.BinaryClient(von)
offset, whence := int64(0), 2
size, err := r.Seek(offset, whence)
@@ -174,9 +184,7 @@
return errOperationFailed
}
nparts := (size-1)/partSize + 1
- // TODO(rthellend): Determine the actual media type.
- mediaType := "application/octet-stream"
- if err := client.Create(ctx, int32(nparts), mediaType); err != nil {
+ if err := client.Create(ctx, int32(nparts), mediaInfo); err != nil {
vlog.Errorf("Create() failed: %v", err)
return err
}
@@ -258,11 +266,11 @@
return nil
}
-func Upload(ctx context.T, von string, data []byte) error {
+func Upload(ctx context.T, von string, data []byte, mediaInfo repository.MediaInfo) error {
buffer := bytes.NewReader(data)
ctx, cancel := ctx.WithTimeout(time.Minute)
defer cancel()
- return upload(ctx, buffer, von)
+ return upload(ctx, buffer, mediaInfo, von)
}
func UploadFromFile(ctx context.T, von, path string) error {
@@ -274,5 +282,6 @@
}
ctx, cancel := ctx.WithTimeout(time.Minute)
defer cancel()
- return upload(ctx, file, von)
+ mediaInfo := packages.MediaInfoForFileName(path)
+ return upload(ctx, file, mediaInfo, von)
}
diff --git a/services/mgmt/lib/binary/impl_test.go b/services/mgmt/lib/binary/impl_test.go
index ce66250..ca0a76f 100644
--- a/services/mgmt/lib/binary/impl_test.go
+++ b/services/mgmt/lib/binary/impl_test.go
@@ -5,11 +5,13 @@
"io/ioutil"
"os"
"path/filepath"
+ "reflect"
"testing"
"veyron.io/veyron/veyron2"
"veyron.io/veyron/veyron2/naming"
"veyron.io/veyron/veyron2/rt"
+ "veyron.io/veyron/veyron2/services/mgmt/repository"
"veyron.io/veyron/veyron2/vlog"
"veyron.io/veyron/veyron/lib/testutil"
@@ -85,21 +87,25 @@
von, cleanup := setupRepository(t)
defer cleanup()
data := testutil.RandomBytes(testutil.Rand.Intn(10 << 20))
- if err := Upload(runtime.NewContext(), von, data); err != nil {
+ mediaInfo := repository.MediaInfo{Type: "application/octet-stream"}
+ if err := Upload(runtime.NewContext(), von, data, mediaInfo); err != nil {
t.Fatalf("Upload(%v) failed: %v", von, err)
}
- output, err := Download(runtime.NewContext(), von)
+ output, outInfo, err := Download(runtime.NewContext(), von)
if err != nil {
t.Fatalf("Download(%v) failed: %v", von, err)
}
if bytes.Compare(data, output) != 0 {
- t.Fatalf("Data mismatch:\nexpected %v %v\ngot %v %v", len(data), data[:100], len(output), output[:100])
+ t.Errorf("Data mismatch:\nexpected %v %v\ngot %v %v", len(data), data[:100], len(output), output[:100])
}
if err := Delete(runtime.NewContext(), von); err != nil {
- t.Fatalf("Delete(%v) failed: %v", von, err)
+ t.Errorf("Delete(%v) failed: %v", von, err)
}
- if _, err := Download(runtime.NewContext(), von); err == nil {
- t.Fatalf("Download(%v) did not fail", von)
+ if _, _, err := Download(runtime.NewContext(), von); err == nil {
+ t.Errorf("Download(%v) did not fail", von)
+ }
+ if !reflect.DeepEqual(mediaInfo, outInfo) {
+ t.Errorf("unexpected media info: expected %v, got %v", mediaInfo, outInfo)
}
}
@@ -117,11 +123,15 @@
}
defer os.Remove(src.Name())
defer src.Close()
- dst, err := ioutil.TempFile(dir, prefix)
+ dstdir, err := ioutil.TempDir(dir, prefix)
if err != nil {
- t.Fatalf("TempFile(%v, %v) failed: %v", dir, prefix, err)
+ t.Fatalf("TempDir(%v, %v) failed: %v", dir, prefix, err)
}
- defer os.Remove(dst.Name())
+ defer os.RemoveAll(dstdir)
+ dst, err := ioutil.TempFile(dstdir, prefix)
+ if err != nil {
+ t.Fatalf("TempFile(%v, %v) failed: %v", dstdir, prefix, err)
+ }
defer dst.Close()
if _, err := src.Write(data); err != nil {
t.Fatalf("Write() failed: %v", err)
@@ -134,12 +144,19 @@
}
output, err := ioutil.ReadFile(dst.Name())
if err != nil {
- t.Fatalf("ReadFile(%v) failed: %v", dst.Name(), err)
+ t.Errorf("ReadFile(%v) failed: %v", dst.Name(), err)
}
if bytes.Compare(data, output) != 0 {
- t.Fatalf("Data mismatch:\nexpected %v %v\ngot %v %v", len(data), data[:100], len(output), output[:100])
+ t.Errorf("Data mismatch:\nexpected %v %v\ngot %v %v", len(data), data[:100], len(output), output[:100])
+ }
+ jMediaInfo, err := ioutil.ReadFile(dst.Name() + ".__info")
+ if err != nil {
+ t.Errorf("ReadFile(%v) failed: %v", dst.Name()+".__info", err)
+ }
+ if expected := `{"Type":"application/octet-stream","Encoding":""}`; string(jMediaInfo) != expected {
+ t.Errorf("unexpected media info: expected %q, got %q", expected, string(jMediaInfo))
}
if err := Delete(runtime.NewContext(), von); err != nil {
- t.Fatalf("Delete(%v) failed: %v", von, err)
+ t.Errorf("Delete(%v) failed: %v", von, err)
}
}
diff --git a/services/mgmt/lib/packages/packages.go b/services/mgmt/lib/packages/packages.go
new file mode 100644
index 0000000..57f9dde
--- /dev/null
+++ b/services/mgmt/lib/packages/packages.go
@@ -0,0 +1,192 @@
+// Package packages provides functionality to install ZIP and TAR packages.
+package packages
+
+import (
+ "archive/tar"
+ "archive/zip"
+ "compress/bzip2"
+ "compress/gzip"
+ "encoding/json"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "veyron.io/veyron/veyron2/services/mgmt/repository"
+)
+
+const defaultType = "application/octet-stream"
+
+var typemap = map[string]repository.MediaInfo{
+ ".zip": repository.MediaInfo{Type: "application/zip"},
+ ".tar": repository.MediaInfo{Type: "application/x-tar"},
+ ".tgz": repository.MediaInfo{Type: "application/x-tar", Encoding: "gzip"},
+ ".tar.gz": repository.MediaInfo{Type: "application/x-tar", Encoding: "gzip"},
+ ".tbz2": repository.MediaInfo{Type: "application/x-tar", Encoding: "bzip2"},
+ ".tb2": repository.MediaInfo{Type: "application/x-tar", Encoding: "bzip2"},
+ ".tbz": repository.MediaInfo{Type: "application/x-tar", Encoding: "bzip2"},
+ ".tar.bz2": repository.MediaInfo{Type: "application/x-tar", Encoding: "bzip2"},
+}
+
+// MediaInfoForFileName returns the MediaInfo based on the file's extension.
+func MediaInfoForFileName(fileName string) repository.MediaInfo {
+ fileName = strings.ToLower(fileName)
+ for k, v := range typemap {
+ if strings.HasSuffix(fileName, k) {
+ return v
+ }
+ }
+ return repository.MediaInfo{Type: defaultType}
+}
+
+// Install installs a package in the given directory. If the package is a TAR or
+// ZIP archive, its content is extracted in the destination directory.
+// Otherwise, the package file itself is copied to the destination directory.
+func Install(pkgFile, dir string) error {
+ mediaInfo, err := LoadMediaInfo(pkgFile)
+ if err != nil {
+ return err
+ }
+ switch mediaInfo.Type {
+ case "application/x-tar":
+ return extractTar(pkgFile, mediaInfo, dir)
+ case "application/zip":
+ return extractZip(pkgFile, dir)
+ default:
+ return fmt.Errorf("unsupported media type: %v", mediaInfo.Type)
+ }
+}
+
+// LoadMediaInfo returns the MediaInfo for the given package file.
+func LoadMediaInfo(pkgFile string) (repository.MediaInfo, error) {
+ jInfo, err := ioutil.ReadFile(pkgFile + ".__info")
+ if err != nil {
+ return repository.MediaInfo{}, err
+ }
+ var info repository.MediaInfo
+ if err := json.Unmarshal(jInfo, &info); err != nil {
+ return repository.MediaInfo{}, err
+ }
+ return info, nil
+}
+
+// SaveMediaInfo saves the media info for a package.
+func SaveMediaInfo(pkgFile string, mediaInfo repository.MediaInfo) error {
+ jInfo, err := json.Marshal(mediaInfo)
+ if err != nil {
+ return err
+ }
+ infoFile := pkgFile + ".__info"
+ if err := ioutil.WriteFile(infoFile, jInfo, os.FileMode(0600)); err != nil {
+ return err
+ }
+ return nil
+}
+
+func extractZip(zipFile, installDir string) error {
+ zr, err := zip.OpenReader(zipFile)
+ if err != nil {
+ return err
+ }
+ for _, file := range zr.File {
+ fi := file.FileInfo()
+ name := filepath.Join(installDir, file.Name)
+ if !strings.HasPrefix(name, installDir) {
+ return fmt.Errorf("failed to extract file %q outside of install directory", file.Name)
+ }
+ if fi.IsDir() {
+ if err := os.MkdirAll(name, os.FileMode(fi.Mode()&0700)); err != nil && !os.IsExist(err) {
+ return err
+ }
+ continue
+ }
+ in, err := file.Open()
+ if err != nil {
+ return err
+ }
+ parentName := filepath.Dir(name)
+ if err := os.MkdirAll(parentName, os.FileMode(0700)); err != nil {
+ return err
+ }
+ out, err := os.OpenFile(name, os.O_CREATE|os.O_WRONLY, os.FileMode(fi.Mode()&0700))
+ if err != nil {
+ in.Close()
+ return err
+ }
+ nbytes, err := io.Copy(out, in)
+ in.Close()
+ out.Close()
+ if err != nil {
+ return err
+ }
+ if nbytes != fi.Size() {
+ return fmt.Errorf("file size doesn't match for %q: %d != %d", fi.Name(), nbytes, fi.Size())
+ }
+ }
+ return nil
+}
+
+func extractTar(pkgFile string, mediaInfo repository.MediaInfo, installDir string) error {
+ f, err := os.Open(pkgFile)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ var reader io.Reader
+ switch enc := mediaInfo.Encoding; enc {
+ case "":
+ reader = f
+ case "gzip":
+ var err error
+ if reader, err = gzip.NewReader(f); err != nil {
+ return err
+ }
+ case "bzip2":
+ reader = bzip2.NewReader(f)
+ default:
+ return fmt.Errorf("unsupported encoding: %q", enc)
+ }
+
+ tr := tar.NewReader(reader)
+ for {
+ hdr, err := tr.Next()
+ if err == io.EOF {
+ return nil
+ }
+ if err != nil {
+ return err
+ }
+ name := filepath.Join(installDir, hdr.Name)
+ if !strings.HasPrefix(name, installDir) {
+ return fmt.Errorf("failed to extract file %q outside of install directory", hdr.Name)
+ }
+ // Regular file
+ if hdr.Typeflag == tar.TypeReg {
+ out, err := os.OpenFile(name, os.O_CREATE|os.O_WRONLY, os.FileMode(hdr.Mode&0700))
+ if err != nil {
+ return err
+ }
+ nbytes, err := io.Copy(out, tr)
+ out.Close()
+ if err != nil {
+ return err
+ }
+ if nbytes != hdr.Size {
+ return fmt.Errorf("file size doesn't match for %q: %d != %d", hdr.Name, nbytes, hdr.Size)
+ }
+ continue
+ }
+ // Directory
+ if hdr.Typeflag == tar.TypeDir {
+ if err := os.Mkdir(name, os.FileMode(hdr.Mode&0700)); err != nil && !os.IsExist(err) {
+ return err
+ }
+ continue
+ }
+ // Skip unsupported types
+ // TODO(rthellend): Consider adding support for Symlink.
+ }
+}
diff --git a/services/mgmt/lib/packages/packages_test.go b/services/mgmt/lib/packages/packages_test.go
new file mode 100644
index 0000000..8182b44
--- /dev/null
+++ b/services/mgmt/lib/packages/packages_test.go
@@ -0,0 +1,242 @@
+package packages_test
+
+import (
+ "archive/tar"
+ "archive/zip"
+ "compress/gzip"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "reflect"
+ "sort"
+ "testing"
+
+ "veyron.io/veyron/veyron2/services/mgmt/repository"
+
+ "veyron.io/veyron/veyron/services/mgmt/lib/packages"
+)
+
+func TestInstall(t *testing.T) {
+ workdir, err := ioutil.TempDir("", "packages-test-")
+ if err != nil {
+ t.Fatalf("ioutil.TempDir failed: %v", err)
+ }
+ defer os.RemoveAll(workdir)
+ srcdir := filepath.Join(workdir, "src")
+ dstdir := filepath.Join(workdir, "dst")
+ createFiles(t, srcdir)
+
+ zipfile := filepath.Join(workdir, "archivezip")
+ tarfile := filepath.Join(workdir, "archivetar")
+ tgzfile := filepath.Join(workdir, "archivetgz")
+
+ makeZip(t, zipfile, srcdir)
+ makeTar(t, tarfile, srcdir)
+ doGzip(t, tarfile, tgzfile)
+
+ binfile := filepath.Join(workdir, "binfile")
+ ioutil.WriteFile(binfile, []byte("This is a binary file"), os.FileMode(0644))
+ ioutil.WriteFile(binfile+".__info", []byte(`{"type":"application/octet-stream"}`), os.FileMode(0644))
+
+ expected := []string{
+ "a perm:700",
+ "a/b perm:700",
+ "a/b/xyzzy.txt perm:600",
+ "a/bar.txt perm:600",
+ "a/foo.txt perm:600",
+ }
+ for _, file := range []string{zipfile, tarfile, tgzfile} {
+ setupDstDir(t, dstdir)
+ if err := packages.Install(file, dstdir); err != nil {
+ t.Errorf("packages.Install failed for %q: %v", file, err)
+ }
+ files := scanDir(dstdir)
+ if !reflect.DeepEqual(files, expected) {
+ t.Errorf("unexpected result for %q: Got %q, want %q", file, files, expected)
+ }
+ }
+
+ setupDstDir(t, dstdir)
+ if err := packages.Install(binfile, dstdir); err == nil {
+ t.Errorf("expected packages.Install to fail %q", binfile)
+ }
+}
+
+func TestMediaInfo(t *testing.T) {
+ testcases := []struct {
+ filename string
+ expected repository.MediaInfo
+ }{
+ {"foo.zip", repository.MediaInfo{Type: "application/zip"}},
+ {"foo.ZIP", repository.MediaInfo{Type: "application/zip"}},
+ {"foo.tar", repository.MediaInfo{Type: "application/x-tar"}},
+ {"foo.TAR", repository.MediaInfo{Type: "application/x-tar"}},
+ {"foo.tgz", repository.MediaInfo{Type: "application/x-tar", Encoding: "gzip"}},
+ {"FOO.TAR.GZ", repository.MediaInfo{Type: "application/x-tar", Encoding: "gzip"}},
+ {"foo.tbz2", repository.MediaInfo{Type: "application/x-tar", Encoding: "bzip2"}},
+ {"foo.tar.bz2", repository.MediaInfo{Type: "application/x-tar", Encoding: "bzip2"}},
+ {"foo", repository.MediaInfo{Type: "application/octet-stream"}},
+ }
+ for _, tc := range testcases {
+ if got := packages.MediaInfoForFileName(tc.filename); !reflect.DeepEqual(got, tc.expected) {
+ t.Errorf("unexpected result for %q: Got %v, want %v", tc.filename, got, tc.expected)
+ }
+ }
+}
+
+func createFiles(t *testing.T, dir string) {
+ if err := os.Mkdir(dir, os.FileMode(0755)); err != nil {
+ t.Fatalf("os.Mkdir(%q) failed: %v", dir, err)
+ }
+ dirs := []string{"a", "a/b"}
+ for _, d := range dirs {
+ fullname := filepath.Join(dir, d)
+ if err := os.Mkdir(fullname, os.FileMode(0755)); err != nil {
+ t.Fatalf("os.Mkdir(%q) failed: %v", fullname, err)
+ }
+ }
+ files := []string{"a/foo.txt", "a/bar.txt", "a/b/xyzzy.txt"}
+ for _, f := range files {
+ fullname := filepath.Join(dir, f)
+ if err := ioutil.WriteFile(fullname, []byte(f), os.FileMode(0644)); err != nil {
+ t.Fatalf("ioutil.WriteFile(%q) failed: %v", fullname, err)
+ }
+ }
+}
+
+func makeZip(t *testing.T, zipfile, dir string) {
+ z, err := os.OpenFile(zipfile, os.O_CREATE|os.O_WRONLY, os.FileMode(0644))
+ if err != nil {
+ t.Fatalf("os.OpenFile(%q) failed: %v", zipfile, err)
+ }
+ defer z.Close()
+ w := zip.NewWriter(z)
+ filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ t.Fatalf("Walk(%q) error: %v", dir, err)
+ }
+ if dir == path {
+ return nil
+ }
+ fh, err := zip.FileInfoHeader(info)
+ if err != nil {
+ t.Fatalf("FileInfoHeader failed: %v", err)
+ }
+ fh.Name, _ = filepath.Rel(dir, path)
+ hdr, err := w.CreateHeader(fh)
+ if err != nil {
+ t.Fatalf("w.CreateHeader failed: %v", err)
+ }
+ if !info.IsDir() {
+ content, err := ioutil.ReadFile(path)
+ if err != nil {
+ t.Fatalf("ioutil.ReadFile(%q) failed: %v", path, err)
+ }
+ if _, err = hdr.Write(content); err != nil {
+ t.Fatalf("hdr.Write(%q) failed: %v", content, err)
+ }
+ }
+ return nil
+ })
+ if err := w.Close(); err != nil {
+ t.Fatalf("w.Close() failed: %v", err)
+ }
+ if err := ioutil.WriteFile(zipfile+".__info", []byte(`{"type":"application/zip"}`), os.FileMode(0644)); err != nil {
+ t.Fatalf("ioutil.WriteFile() failed: %v", err)
+ }
+}
+
+func makeTar(t *testing.T, tarfile, dir string) {
+ tf, err := os.OpenFile(tarfile, os.O_CREATE|os.O_WRONLY, os.FileMode(0644))
+ if err != nil {
+ t.Fatalf("os.OpenFile(%q) failed: %v", tarfile, err)
+ }
+ defer tf.Close()
+
+ tw := tar.NewWriter(tf)
+ filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ t.Fatalf("Walk(%q) error: %v", dir, err)
+ }
+ if dir == path {
+ return nil
+ }
+ hdr, err := tar.FileInfoHeader(info, "")
+ if err != nil {
+ t.Fatalf("tar.FileInfoHeader failed: %v", err)
+ }
+ hdr.Name, _ = filepath.Rel(dir, path)
+ if err := tw.WriteHeader(hdr); err != nil {
+ t.Fatalf("tw.WriteHeader failed: %v", err)
+ }
+ if !info.IsDir() {
+ content, err := ioutil.ReadFile(path)
+ if err != nil {
+ t.Fatalf("ioutil.ReadFile(%q) failed: %v", path, err)
+ }
+ if _, err := tw.Write(content); err != nil {
+ t.Fatalf("tw.Write failed: %v", err)
+ }
+ }
+ return nil
+ })
+ if err := tw.Close(); err != nil {
+ t.Fatalf("tw.Close failed: %v", err)
+ }
+ if err := ioutil.WriteFile(tarfile+".__info", []byte(`{"type":"application/x-tar"}`), os.FileMode(0644)); err != nil {
+ t.Fatalf("ioutil.WriteFile() failed: %v", err)
+ }
+}
+
+func doGzip(t *testing.T, infile, outfile string) {
+ in, err := os.Open(infile)
+ if err != nil {
+ t.Fatalf("os.Open(%q) failed: %v", infile, err)
+ }
+ defer in.Close()
+ out, err := os.OpenFile(outfile, os.O_CREATE|os.O_WRONLY, os.FileMode(0644))
+ if err != nil {
+ t.Fatalf("os.OpenFile(%q) failed: %v", outfile, err)
+ }
+ defer out.Close()
+ writer := gzip.NewWriter(out)
+ defer writer.Close()
+ if _, err := io.Copy(writer, in); err != nil {
+ t.Fatalf("io.Copy() failed: %v", err)
+ }
+
+ info, err := packages.LoadMediaInfo(infile)
+ if err != nil {
+ t.Fatalf("LoadMediaInfo(%q) failed: %v", infile, err)
+ }
+ info.Encoding = "gzip"
+ if err := packages.SaveMediaInfo(outfile, info); err != nil {
+ t.Fatalf("SaveMediaInfo(%v) failed: %v", outfile, err)
+ }
+}
+
+func scanDir(root string) []string {
+ files := []string{}
+ filepath.Walk(root, func(path string, info os.FileInfo, _ error) error {
+ if root == path {
+ return nil
+ }
+ rel, _ := filepath.Rel(root, path)
+ perm := info.Mode() & 0700
+ files = append(files, fmt.Sprintf("%s perm:%o", rel, perm))
+ return nil
+ })
+ sort.Strings(files)
+ return files
+}
+
+func setupDstDir(t *testing.T, dst string) {
+ if err := os.RemoveAll(dst); err != nil {
+ t.Fatalf("os.RemoveAll(%q) failed: %v", dst, err)
+ }
+ if err := os.Mkdir(dst, os.FileMode(0755)); err != nil {
+ t.Fatalf("os.Mkdir(%q) failed: %v", dst, err)
+ }
+}