github.com/mhilton/juju-juju@v0.0.0-20150901100907-a94dd2c73455/state/backups/archive.go (about)

     1  // Copyright 2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package backups
     5  
     6  import (
     7  	"bytes"
     8  	"compress/gzip"
     9  	"io"
    10  	"io/ioutil"
    11  	"os"
    12  	"path"
    13  	"path/filepath"
    14  
    15  	"github.com/juju/errors"
    16  	"github.com/juju/utils/tar"
    17  
    18  	"github.com/juju/juju/version"
    19  )
    20  
    21  const (
    22  	contentDir   = "juju-backup"
    23  	filesBundle  = "root.tar"
    24  	dbDumpDir    = "dump"
    25  	metadataFile = "metadata.json"
    26  )
    27  
    28  var legacyVersion = version.Number{Major: 1, Minor: 20}
    29  
    30  // ArchivePaths holds the paths to the files and directories in a
    31  // backup archive.
    32  type ArchivePaths struct {
    33  	// ContentDir is the path to the directory within the archive
    34  	// containing all the contents. It is the only file or directory at
    35  	// the top-level of the archive and everything else in the archive
    36  	// is contained in the content directory.
    37  	ContentDir string
    38  
    39  	// FilesBundle is the path to the tar file inside the archive
    40  	// containing all the state-related files (with the exception of the
    41  	// DB dump files) gathered in by the backup machinery.
    42  	FilesBundle string
    43  
    44  	// DBDumpDir is the path to the directory within the archive
    45  	// contents that contains all the files dumped from the juju state
    46  	// database.
    47  	DBDumpDir string
    48  
    49  	// MetadataFile is the path to the metadata file.
    50  	MetadataFile string
    51  }
    52  
    53  // NewCanonicalArchivePaths composes a new ArchivePaths with default
    54  // values set. These values are relative (un-rooted) and the canonical
    55  // slash ("/") is the path separator. Thus the paths are suitable for
    56  // resolving the paths in a backup archive file (which is a tar file).
    57  func NewCanonicalArchivePaths() ArchivePaths {
    58  	return ArchivePaths{
    59  		ContentDir:   contentDir,
    60  		FilesBundle:  path.Join(contentDir, filesBundle),
    61  		DBDumpDir:    path.Join(contentDir, dbDumpDir),
    62  		MetadataFile: path.Join(contentDir, metadataFile),
    63  	}
    64  }
    65  
    66  // NonCanonicalArchivePaths builds a new ArchivePaths using default
    67  // values, rooted at the provided rootDir. The path separator used is
    68  // platform-dependent. The resulting paths are suitable for locating
    69  // backup archive contents in a directory into which an archive has
    70  // been unpacked.
    71  func NewNonCanonicalArchivePaths(rootDir string) ArchivePaths {
    72  	return ArchivePaths{
    73  		ContentDir:   filepath.Join(rootDir, contentDir),
    74  		FilesBundle:  filepath.Join(rootDir, contentDir, filesBundle),
    75  		DBDumpDir:    filepath.Join(rootDir, contentDir, dbDumpDir),
    76  		MetadataFile: filepath.Join(rootDir, contentDir, metadataFile),
    77  	}
    78  }
    79  
    80  // ArchiveWorkspace is a wrapper around backup archive info that has a
    81  // concrete root directory and an archive unpacked in it.
    82  type ArchiveWorkspace struct {
    83  	ArchivePaths
    84  	RootDir string
    85  }
    86  
    87  func newArchiveWorkspace() (*ArchiveWorkspace, error) {
    88  	rootdir, err := ioutil.TempDir("", "juju-backups-")
    89  	if err != nil {
    90  		return nil, errors.Annotate(err, "while creating workspace dir")
    91  	}
    92  
    93  	ws := ArchiveWorkspace{
    94  		ArchivePaths: NewNonCanonicalArchivePaths(rootdir),
    95  		RootDir:      rootdir,
    96  	}
    97  	return &ws, nil
    98  }
    99  
   100  // NewArchiveWorkspaceReader returns a new archive workspace with a new
   101  // workspace dir populated from the archive. Note that this involves
   102  // unpacking the entire archive into a directory under the host's
   103  // "temporary" directory. For relatively large archives this could have
   104  // adverse effects on hosts with little disk space.
   105  func NewArchiveWorkspaceReader(archive io.Reader) (*ArchiveWorkspace, error) {
   106  	ws, err := newArchiveWorkspace()
   107  	if err != nil {
   108  		return nil, errors.Trace(err)
   109  	}
   110  	err = unpackCompressedReader(ws.RootDir, archive)
   111  	return ws, errors.Trace(err)
   112  }
   113  
   114  func unpackCompressedReader(targetDir string, tarFile io.Reader) error {
   115  	tarFile, err := gzip.NewReader(tarFile)
   116  	if err != nil {
   117  		return errors.Annotate(err, "while uncompressing archive file")
   118  	}
   119  	err = tar.UntarFiles(tarFile, targetDir)
   120  	return errors.Trace(err)
   121  }
   122  
   123  // Close cleans up the workspace dir.
   124  func (ws *ArchiveWorkspace) Close() error {
   125  	err := os.RemoveAll(ws.RootDir)
   126  	return errors.Trace(err)
   127  }
   128  
   129  // UnpackFilesBundle unpacks the archived files bundle into the targeted dir.
   130  func (ws *ArchiveWorkspace) UnpackFilesBundle(targetRoot string) error {
   131  	tarFile, err := os.Open(ws.FilesBundle)
   132  	if err != nil {
   133  		return errors.Trace(err)
   134  	}
   135  	defer tarFile.Close()
   136  
   137  	err = tar.UntarFiles(tarFile, targetRoot)
   138  	return errors.Trace(err)
   139  }
   140  
   141  // OpenBundledFile returns an open ReadCloser for the corresponding file in
   142  // the archived files bundle.
   143  func (ws *ArchiveWorkspace) OpenBundledFile(filename string) (io.Reader, error) {
   144  	if filepath.IsAbs(filename) {
   145  		return nil, errors.Errorf("filename must be relative, got %q", filename)
   146  	}
   147  
   148  	tarFile, err := os.Open(ws.FilesBundle)
   149  	if err != nil {
   150  		return nil, errors.Trace(err)
   151  	}
   152  
   153  	_, file, err := tar.FindFile(tarFile, filename)
   154  	if err != nil {
   155  		tarFile.Close()
   156  		return nil, errors.Trace(err)
   157  	}
   158  	return file, nil
   159  }
   160  
   161  // Metadata returns the metadata derived from the JSON file in the archive.
   162  func (ws *ArchiveWorkspace) Metadata() (*Metadata, error) {
   163  	metaFile, err := os.Open(ws.MetadataFile)
   164  	if err != nil {
   165  		return nil, errors.Trace(err)
   166  	}
   167  	defer metaFile.Close()
   168  
   169  	meta, err := NewMetadataJSONReader(metaFile)
   170  	return meta, errors.Trace(err)
   171  }
   172  
   173  // ArchiveData is a wrapper around a the uncompressed data in a backup
   174  // archive file. It provides access to the content of the archive. While
   175  // ArchiveData provides useful functionality, it may not be appropriate
   176  // for large archives. The contents of the archive are kept in-memory,
   177  // so large archives could be too taxing on the host. In that case
   178  // consider using ArchiveWorkspace instead.
   179  type ArchiveData struct {
   180  	ArchivePaths
   181  	data []byte
   182  }
   183  
   184  // NewArchiveData builds a new archive data wrapper for the given
   185  // uncompressed data.
   186  func NewArchiveData(data []byte) *ArchiveData {
   187  	return &ArchiveData{
   188  		ArchivePaths: NewCanonicalArchivePaths(),
   189  		data:         data,
   190  	}
   191  }
   192  
   193  // NewArchiveReader returns a new archive data wrapper for the data in
   194  // the provided reader. Note that the entire archive will be read into
   195  // memory and kept there. So for relatively large archives it will often
   196  // be more appropriate to use ArchiveWorkspace instead.
   197  func NewArchiveDataReader(r io.Reader) (*ArchiveData, error) {
   198  	gzr, err := gzip.NewReader(r)
   199  	if err != nil {
   200  		return nil, errors.Trace(err)
   201  	}
   202  	defer gzr.Close()
   203  
   204  	data, err := ioutil.ReadAll(gzr)
   205  	if err != nil {
   206  		return nil, errors.Trace(err)
   207  	}
   208  
   209  	return NewArchiveData(data), nil
   210  }
   211  
   212  // NewBuffer wraps the archive data in a Buffer.
   213  func (ad *ArchiveData) NewBuffer() *bytes.Buffer {
   214  	return bytes.NewBuffer(ad.data)
   215  }
   216  
   217  // Metadata returns the metadata stored in the backup archive.  If no
   218  // metadata is there, errors.NotFound is returned.
   219  func (ad *ArchiveData) Metadata() (*Metadata, error) {
   220  	buf := ad.NewBuffer()
   221  	_, metaFile, err := tar.FindFile(buf, ad.MetadataFile)
   222  	if err != nil {
   223  		return nil, errors.Trace(err)
   224  	}
   225  
   226  	meta, err := NewMetadataJSONReader(metaFile)
   227  	return meta, errors.Trace(err)
   228  }
   229  
   230  // Version returns the juju version under which the backup archive
   231  // was created.  If no version is found in the archive, it must come
   232  // from before backup archives included the version.  In that case we
   233  // return version 1.20.
   234  func (ad *ArchiveData) Version() (*version.Number, error) {
   235  	meta, err := ad.Metadata()
   236  	if errors.IsNotFound(err) {
   237  		return &legacyVersion, nil
   238  	}
   239  	if err != nil {
   240  		return nil, errors.Trace(err)
   241  	}
   242  
   243  	return &meta.Origin.Version, nil
   244  }