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 }