github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/state/backups/testing/file.go (about)

     1  // Copyright 2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package testing
     5  
     6  import (
     7  	"archive/tar"
     8  	"bytes"
     9  	"compress/gzip"
    10  	"encoding/json"
    11  	"io"
    12  	"path"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/juju/collections/set"
    17  	"github.com/juju/errors"
    18  	"github.com/juju/version/v2"
    19  
    20  	"github.com/juju/juju/state/backups"
    21  )
    22  
    23  // File represents a file during testing.
    24  type File struct {
    25  	// Name is the path to which the file will be identified in the archive.
    26  	Name string
    27  	// Content is the data that will be written to the archive for the file.
    28  	Content string
    29  	// IsDir determines if the file is a regular file or a directory.
    30  	IsDir bool
    31  }
    32  
    33  // AddToArchive adds the file to the tar archive.
    34  func (f *File) AddToArchive(archive *tar.Writer) error {
    35  	hdr := &tar.Header{
    36  		Name: f.Name,
    37  	}
    38  	if f.IsDir {
    39  		hdr.Typeflag = tar.TypeDir
    40  		hdr.Mode = 0777
    41  	} else {
    42  		hdr.Size = int64(len(f.Content))
    43  		hdr.Mode = 0666
    44  	}
    45  
    46  	if err := archive.WriteHeader(hdr); err != nil {
    47  		return errors.Trace(err)
    48  	}
    49  
    50  	if !f.IsDir {
    51  		if _, err := archive.Write([]byte(f.Content)); err != nil {
    52  			return errors.Trace(err)
    53  		}
    54  	}
    55  
    56  	return nil
    57  }
    58  
    59  // NewArchive returns a new archive file containing the files.
    60  func NewArchive(meta *backups.Metadata, files, dump []File) (*bytes.Buffer, error) {
    61  	topFiles, err := internalTopFiles(files, dump)
    62  	if err != nil {
    63  		return nil, errors.Trace(err)
    64  	}
    65  
    66  	if meta != nil {
    67  		metaFile, err := meta.AsJSONBuffer()
    68  		if err != nil {
    69  			return nil, errors.Trace(err)
    70  		}
    71  		topFiles = append(topFiles,
    72  			File{
    73  				Name:    "juju-backup/metadata.json",
    74  				Content: metaFile.(*bytes.Buffer).String(),
    75  			},
    76  		)
    77  	}
    78  	return internalCompress(topFiles)
    79  }
    80  
    81  func internalTopFiles(files, dump []File) ([]File, error) {
    82  	dirs := set.NewStrings()
    83  	var sysFiles []File
    84  	for _, file := range files {
    85  		var parent string
    86  		for _, p := range strings.Split(path.Dir(file.Name), "/") {
    87  			if parent == "" {
    88  				parent = p
    89  			} else {
    90  				parent = path.Join(parent, p)
    91  			}
    92  			if !dirs.Contains(parent) {
    93  				sysFiles = append(sysFiles, File{
    94  					Name:  parent,
    95  					IsDir: true,
    96  				})
    97  				dirs.Add(parent)
    98  			}
    99  		}
   100  		if file.IsDir {
   101  			if !dirs.Contains(file.Name) {
   102  				sysFiles = append(sysFiles, file)
   103  				dirs.Add(file.Name)
   104  			}
   105  		} else {
   106  			sysFiles = append(sysFiles, file)
   107  		}
   108  	}
   109  
   110  	var rootFile bytes.Buffer
   111  	if err := writeToTar(&rootFile, sysFiles); err != nil {
   112  		return nil, errors.Trace(err)
   113  	}
   114  
   115  	topFiles := []File{{
   116  		Name:  "juju-backup",
   117  		IsDir: true,
   118  	}}
   119  
   120  	topFiles = append(topFiles, File{
   121  		Name:  "juju-backup/dump",
   122  		IsDir: true,
   123  	})
   124  	for _, dumpFile := range dump {
   125  		topFiles = append(topFiles, File{
   126  			Name:    "juju-backup/dump/" + dumpFile.Name,
   127  			Content: dumpFile.Content,
   128  			IsDir:   dumpFile.IsDir,
   129  		})
   130  	}
   131  
   132  	topFiles = append(topFiles,
   133  		File{
   134  			Name:    "juju-backup/root.tar",
   135  			Content: rootFile.String(),
   136  		},
   137  	)
   138  	return topFiles, nil
   139  }
   140  
   141  // NewArchiveV0 returns a new archive file containing the files, in v0 format.
   142  func NewArchiveV0(meta *backups.Metadata, files, dump []File) (*bytes.Buffer, error) {
   143  	topFiles, err := internalTopFiles(files, dump)
   144  	if err != nil {
   145  		return nil, errors.Trace(err)
   146  	}
   147  	if meta != nil {
   148  		metaFile, err := asJSONBufferV0(meta)
   149  		if err != nil {
   150  			return nil, errors.Trace(err)
   151  		}
   152  		topFiles = append(topFiles,
   153  			File{
   154  				Name:    "juju-backup/metadata.json",
   155  				Content: metaFile.(*bytes.Buffer).String(),
   156  			},
   157  		)
   158  	}
   159  	return internalCompress(topFiles)
   160  }
   161  
   162  func internalCompress(topFiles []File) (*bytes.Buffer, error) {
   163  	var arFile bytes.Buffer
   164  	compressed := gzip.NewWriter(&arFile)
   165  	defer compressed.Close()
   166  	if err := writeToTar(compressed, topFiles); err != nil {
   167  		return nil, errors.Trace(err)
   168  	}
   169  	return &arFile, nil
   170  }
   171  
   172  func asJSONBufferV0(m *backups.Metadata) (io.Reader, error) {
   173  	var outfile bytes.Buffer
   174  	if err := json.NewEncoder(&outfile).Encode(flatV0(m)); err != nil {
   175  		return nil, errors.Trace(err)
   176  	}
   177  	return &outfile, nil
   178  }
   179  
   180  type flatMetadataV0 struct {
   181  	ID string
   182  
   183  	// file storage
   184  
   185  	Checksum       string
   186  	ChecksumFormat string
   187  	Size           int64
   188  	Stored         time.Time
   189  
   190  	// backup
   191  
   192  	Started     time.Time
   193  	Finished    time.Time
   194  	Notes       string
   195  	Environment string
   196  	Machine     string
   197  	Hostname    string
   198  	Version     version.Number
   199  	Base        string
   200  }
   201  
   202  func flatV0(m *backups.Metadata) flatMetadataV0 {
   203  	flat := flatMetadataV0{
   204  		ID:             m.ID(),
   205  		Checksum:       m.Checksum(),
   206  		ChecksumFormat: m.ChecksumFormat(),
   207  		Size:           m.Size(),
   208  		Started:        m.Started,
   209  		Notes:          m.Notes,
   210  		Environment:    m.Origin.Model,
   211  		Machine:        m.Origin.Machine,
   212  		Hostname:       m.Origin.Hostname,
   213  		Version:        m.Origin.Version,
   214  		Base:           m.Origin.Base,
   215  	}
   216  	stored := m.Stored()
   217  	if stored != nil {
   218  		flat.Stored = *stored
   219  	}
   220  
   221  	if m.Finished != nil {
   222  		flat.Finished = *m.Finished
   223  	}
   224  	return flat
   225  }
   226  
   227  // NewArchiveBasic returns a new archive file with a few files provided.
   228  func NewArchiveBasic(meta *backups.Metadata) (*bytes.Buffer, error) {
   229  	files := []File{
   230  		{
   231  			Name:    "var/lib/juju/tools/1.21-alpha2.1-trusty-amd64/jujud",
   232  			Content: "<some binary data goes here>",
   233  		},
   234  		{
   235  			Name:    "var/lib/juju/system-identity",
   236  			Content: "<an ssh key goes here>",
   237  		},
   238  	}
   239  	dump := []File{
   240  		{
   241  			Name:    "juju/machines.bson",
   242  			Content: "<BSON data goes here>",
   243  		},
   244  		{
   245  			Name:    "oplog.bson",
   246  			Content: "<BSON data goes here>",
   247  		},
   248  	}
   249  
   250  	arFile, err := NewArchive(meta, files, dump)
   251  	if err != nil {
   252  		return nil, errors.Trace(err)
   253  	}
   254  	return arFile, nil
   255  }
   256  
   257  func writeToTar(archive io.Writer, files []File) error {
   258  	tarw := tar.NewWriter(archive)
   259  	defer tarw.Close()
   260  
   261  	for _, file := range files {
   262  		if err := file.AddToArchive(tarw); err != nil {
   263  			return errors.Trace(err)
   264  		}
   265  	}
   266  	return nil
   267  }