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