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
		if err != nil {
			return
		}
	}

ale's avatar
ale committed
39
	ds = normalizeDataset(Dataset{
ale's avatar
ale committed
40 41 42
		Name:    spec.Name,
		Handler: spec.Handler,
		Atoms:   atoms,
ale's avatar
ale committed
43
	})
ale's avatar
ale committed
44 45 46
	return
}

ale's avatar
ale committed
47 48 49 50
// 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
51
func (spec *SourceSpec) Check(handlers map[string]Handler) error {
ale's avatar
ale committed
52 53 54 55 56 57
	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
58 59
	if len(spec.Atoms) > 0 && spec.AtomsCommand != "" {
		return errors.New("can't specify both 'atoms' and 'atoms_command'")
ale's avatar
ale committed
60 61 62 63
	}
	return nil
}

ale's avatar
ale committed
64
func runAtomsCommand(ctx context.Context, cmd string) ([]Atom, error) {
ale's avatar
ale committed
65 66 67 68 69 70 71 72 73 74 75 76 77 78
	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
79
		atom := Atom{Name: string(parts[0])}
ale's avatar
ale committed
80
		if len(parts) == 2 {
ale's avatar
ale committed
81
			atom.RelativePath = string(parts[1])
ale's avatar
ale committed
82
		}
ale's avatar
ale committed
83
		atoms = append(atoms, atom)
ale's avatar
ale committed
84 85 86
	}
	return atoms, scanner.Err()
}
ale's avatar
ale committed
87 88 89 90 91 92

func normalizeDataset(ds Dataset) Dataset {
	// If the Dataset has no atoms, add an empty one.
	if len(ds.Atoms) == 0 {
		ds.Atoms = []Atom{Atom{}}
	}
ale's avatar
ale committed
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107

	// If there are multiple atoms, and some (or all) have empty
	// RelativePaths, just set their RelativePath equal to their
	// Name.
	if len(ds.Atoms) > 1 {
		var atoms []Atom
		for _, atom := range ds.Atoms {
			if atom.RelativePath == "" {
				atom.RelativePath = atom.Name
			}
			atoms = append(atoms, atom)
		}
		ds.Atoms = atoms
	}

ale's avatar
ale committed
108 109
	return ds
}