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 }