source.go 2.57 KB
Newer Older
ale's avatar
ale committed
1
2
3
4
5
6
7
package tabacco

import (
	"bufio"
	"bytes"
	"context"
	"errors"
ale's avatar
ale committed
8
	"fmt"
ale's avatar
ale committed
9
10
11
12
13
	"os/exec"
)

// SourceSpec defines the configuration for a data source.
type SourceSpec struct {
ale's avatar
ale committed
14
15
	Name    string `yaml:"name"`
	Handler string `yaml:"handler"`
ale's avatar
ale committed
16
17

	// Schedule to run the backup on.
ale's avatar
ale committed
18
	Schedule string `yaml:"schedule"`
ale's avatar
ale committed
19
20
21

	// Define atoms statically, or use a script to generate them
	// dynamically on every new backup.
ale's avatar
ale committed
22
23
	Atoms        []Atom `yaml:"atoms"`
	AtomsCommand string `yaml:"atoms_command"`
ale's avatar
ale committed
24
25
26
27
28
29

	//Params map[string]interface{} `json:"params"`
}

// Parse a SourceSpec and return a Dataset instance.
func (spec *SourceSpec) Parse(ctx context.Context) (ds Dataset, err error) {
ale's avatar
ale committed
30
	// Invoke the atoms_command if necessary.
ale's avatar
ale committed
31
	atoms := spec.Atoms
ale's avatar
ale committed
32
33
	if spec.AtomsCommand != "" {
		atoms, err = runAtomsCommand(ctx, spec.AtomsCommand)
ale's avatar
ale committed
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
		if err != nil {
			return
		}
	}

	// Verify that the dataset is syntactically correct: one case
	// we want to catch is that of multiple atoms with empty
	// RelativePaths.
	if len(atoms) > 1 {
		for _, atom := range atoms {
			if atom.RelativePath == "" {
				err = errors.New("can't have atoms with empty relative paths in multi-atom datasets")
				return
			}
		}
	}

ale's avatar
ale committed
51
	ds = normalizeDataset(Dataset{
ale's avatar
ale committed
52
53
54
		Name:    spec.Name,
		Handler: spec.Handler,
		Atoms:   atoms,
ale's avatar
ale committed
55
	})
ale's avatar
ale committed
56
57
58
	return
}

ale's avatar
ale committed
59
60
61
62
// Check that the configuration is valid. Not an alternative to
// validation at usage time, but it provides an early warning to the
// user. Checks the handler name against a string set of handler
// names.
ale's avatar
ale committed
63
func (spec *SourceSpec) Check(handlers map[string]Handler) error {
ale's avatar
ale committed
64
65
66
67
68
69
	if spec.Name == "" {
		return errors.New("name is empty")
	}
	if _, ok := handlers[spec.Handler]; !ok {
		return fmt.Errorf("unknown handler '%s'", spec.Handler)
	}
ale's avatar
ale committed
70
71
	if len(spec.Atoms) > 0 && spec.AtomsCommand != "" {
		return errors.New("can't specify both 'atoms' and 'atoms_command'")
ale's avatar
ale committed
72
73
74
75
	}
	return nil
}

ale's avatar
ale committed
76
func runAtomsCommand(ctx context.Context, cmd string) ([]Atom, error) {
ale's avatar
ale committed
77
78
79
80
81
82
83
84
85
86
87
88
89
90
	c := exec.Command("/bin/sh", "-c", cmd) // #nosec
	stdout, err := c.StdoutPipe()
	if err != nil {
		return nil, err
	}
	defer stdout.Close() // nolint: errcheck
	if err := c.Start(); err != nil {
		return nil, err
	}

	var atoms []Atom
	scanner := bufio.NewScanner(stdout)
	for scanner.Scan() {
		parts := bytes.Fields(scanner.Bytes())
ale's avatar
ale committed
91
		atom := Atom{Name: string(parts[0])}
ale's avatar
ale committed
92
		if len(parts) == 2 {
ale's avatar
ale committed
93
			atom.RelativePath = string(parts[1])
ale's avatar
ale committed
94
		}
ale's avatar
ale committed
95
		atoms = append(atoms, atom)
ale's avatar
ale committed
96
97
98
	}
	return atoms, scanner.Err()
}
ale's avatar
ale committed
99
100
101
102
103
104
105
106

func normalizeDataset(ds Dataset) Dataset {
	// If the Dataset has no atoms, add an empty one.
	if len(ds.Atoms) == 0 {
		ds.Atoms = []Atom{Atom{}}
	}
	return ds
}