repository_restic.go 4.49 KB
Newer Older
ale's avatar
ale committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
package tabacco

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"
	"time"
)

type resticRepository struct {
	uri          string
	passwordFile string
	shell        *Shell
	excludes     []string
	excludeFiles []string
}

func (r *resticRepository) resticOptions() string {
	opts := []string{"-r", r.uri}
	if r.passwordFile != "" {
		opts = append(opts, "--password-file", r.passwordFile)
	}
	for _, x := range r.excludes {
		opts = append(opts, "--exclude", x)
	}
	for _, x := range r.excludeFiles {
		opts = append(opts, "--exclude-file", x)
	}
	return strings.Join(opts, " ")
}

// newResticRepository returns a restic repository.
func newResticRepository(params map[string]interface{}, shell *Shell) (Repository, error) {
	uri, ok := params["uri"].(string)
	if !ok || uri == "" {
		return nil, errors.New("missing uri")
	}
	password, ok := params["password"].(string)
	if !ok || password == "" {
		return nil, errors.New("missing password")
	}

	ex, _ := params["exclude"].([]string)
	exf, _ := params["exclude_files"].([]string)

	tmpf, err := ioutil.TempFile("", "restic-pw-")
	if err != nil {
		return nil, err
	}
	if _, err := io.WriteString(tmpf, password); err != nil {
		os.Remove(tmpf.Name()) // nolint: errcheck
		return nil, err
	}
	if err := tmpf.Close(); err != nil {
		os.Remove(tmpf.Name()) // nolint: errcheck
		return nil, err
	}

	return &resticRepository{
		uri:          uri,
		passwordFile: tmpf.Name(),
		excludes:     ex,
		excludeFiles: exf,
		shell:        shell,
	}, nil
}

func (r *resticRepository) Close() error {
	return os.Remove(r.passwordFile)
}

func (r *resticRepository) Init(ctx context.Context) error {
	return r.shell.Run(ctx, fmt.Sprintf(
		"restic %s init --quiet",
		r.resticOptions(),
	))
}

func (r *resticRepository) Prepare(ctx context.Context, backup Backup) error {
	return r.shell.Run(ctx, fmt.Sprintf(
		"restic %s forget --host %s --keep-last 10 --prune",
		r.resticOptions(),
		backup.Host,
	))
}

func (r *resticRepository) Backup(ctx context.Context, backup Backup, ds Dataset, sourcePath string) error {
	cmd := fmt.Sprintf(
		"restic %s backup --cleanup-cache --exclude-caches --one-file-system --tag %s --tag backup_id=%s",
		r.resticOptions(),
		ds.Name,
		backup.ID,
	)
	for _, atom := range ds.Atoms {
		if atom.SourcePath == "" {
			return errors.New("atom without source path")
		}
		cmd += fmt.Sprintf(" %s", atom.SourcePath)
	}
	return r.shell.Run(ctx, cmd)
}

func (r *resticRepository) getSnapshotID(ctx context.Context, backup Backup, ds Dataset) (string, error) {
	data, err := r.shell.Output(ctx, fmt.Sprintf(
		"restic %s snapshots --json --tag backup_id=%s --tag %s",
		r.resticOptions(),
		backup.ID,
		ds.Name,
	))
	if err != nil {
		return "", err
	}
	snaps, err := parseResticSnapshots(data)
	if err != nil {
		return "", err
	}
	if len(snaps) < 1 {
		return "", fmt.Errorf("could not find snapshot for backup id %s", backup.ID)
	}
	return snaps[0].ShortID, nil
}

func (r *resticRepository) Restore(ctx context.Context, backup Backup, ds Dataset, target string) error {
	snap, err := r.getSnapshotID(ctx, backup, ds)
	if err != nil {
		return err
	}
	cmd := fmt.Sprintf(
		"restic %s restore %s",
		r.resticOptions(),
		snap,
	)
	for _, atom := range ds.Atoms {
		cmd += fmt.Sprintf(" --include %s", filepath.Join(atom.SourcePath))
	}
	cmd += fmt.Sprintf(" --target %s", target)
	return r.shell.Run(ctx, cmd)
}

func (r *resticRepository) BackupStream(ctx context.Context, backup Backup, ds Dataset, input io.Reader) error {
	// Try to do the obvious thing with naming.
	name := ds.Name
	if len(ds.Atoms) == 1 {
		name = fmt.Sprintf("%s/%s", ds.Name, ds.Atoms[0].Name)
	}
	return r.shell.Run(ctx, fmt.Sprintf(
		"restic %s backup --stdin --stdin-name %s",
		r.resticOptions(),
		name,
	))
}

func (r *resticRepository) RestoreStream(_ context.Context, backup Backup, ds Dataset, target string, output io.Writer) error {
	// TODO.
	return nil
}

// Data about a snapshot, obtained from 'restic snapshots --json'.
type resticSnapshot struct {
	ID       string    `json:"id"`
	ShortID  string    `json:"short_id"`
	Time     time.Time `json:"time"`
	Parent   string    `json:"parent"`
	Tree     string    `json:"tree"`
	Hostname string    `json:"hostname"`
	Username string    `json:"username"`
	UID      int       `json:"uid"`
	GID      int       `json:"gid"`
	Paths    []string  `json:"paths"`
	Tags     []string  `json:"tags"`
}

func parseResticSnapshots(output []byte) (snapshots []resticSnapshot, err error) {
	err = json.Unmarshal(output, &snapshots)
	return
}