Files
kopia/cli/command_policy_set.go
Jarek Kowalski 6cb9b8fa4f repo: refactored public API (#318)
* This is 99% mechanical:

Extracted repo.Repository interface that only exposes high-level object and manifest management methods, but not blob nor content management.

Renamed old *repo.Repository to *repo.DirectRepository

Reviewed codebase to only depend on repo.Repository as much as possible, but added way for low-level CLI commands to use DirectRepository.

* PR fixes
2020-03-26 08:04:01 -07:00

427 lines
13 KiB
Go

package cli
import (
"context"
"sort"
"strconv"
"strings"
"github.com/pkg/errors"
"github.com/kopia/kopia/repo"
"github.com/kopia/kopia/repo/compression"
"github.com/kopia/kopia/snapshot/policy"
)
var booleanEnumValues = []string{"true", "false", "inherit"}
var (
policySetCommand = policyCommands.Command("set", "Set snapshot policy for a single directory, user@host or a global policy.")
policySetTargets = policySetCommand.Arg("target", "Target of a policy ('global','user@host','@host') or a path").Strings()
policySetGlobal = policySetCommand.Flag("global", "Set global policy").Bool()
// Frequency
policySetInterval = policySetCommand.Flag("snapshot-interval", "Interval between snapshots").DurationList()
policySetTimesOfDay = policySetCommand.Flag("snapshot-time", "Times of day when to take snapshot (HH:mm)").Strings()
// Expiration policies.
policySetKeepLatest = policySetCommand.Flag("keep-latest", "Number of most recent backups to keep per source (or 'inherit')").PlaceHolder("N").String()
policySetKeepHourly = policySetCommand.Flag("keep-hourly", "Number of most-recent hourly backups to keep per source (or 'inherit')").PlaceHolder("N").String()
policySetKeepDaily = policySetCommand.Flag("keep-daily", "Number of most-recent daily backups to keep per source (or 'inherit')").PlaceHolder("N").String()
policySetKeepWeekly = policySetCommand.Flag("keep-weekly", "Number of most-recent weekly backups to keep per source (or 'inherit')").PlaceHolder("N").String()
policySetKeepMonthly = policySetCommand.Flag("keep-monthly", "Number of most-recent monthly backups to keep per source (or 'inherit')").PlaceHolder("N").String()
policySetKeepAnnual = policySetCommand.Flag("keep-annual", "Number of most-recent annual backups to keep per source (or 'inherit')").PlaceHolder("N").String()
// Files to ignore.
policySetAddIgnore = policySetCommand.Flag("add-ignore", "List of paths to add to the ignore list").PlaceHolder("PATTERN").Strings()
policySetRemoveIgnore = policySetCommand.Flag("remove-ignore", "List of paths to remove from the ignore list").PlaceHolder("PATTERN").Strings()
policySetClearIgnore = policySetCommand.Flag("clear-ignore", "Clear list of paths in the ignore list").Bool()
// Name of compression algorithm.
policySetCompressionAlgorithm = policySetCommand.Flag("compression", "Compression algorithm").Enum(supportedCompressionAlgorithms()...)
policySetCompressionMinSize = policySetCommand.Flag("compression-min-size", "Min size of file to attempt compression for").String()
policySetCompressionMaxSize = policySetCommand.Flag("compression-max-size", "Max size of file to attempt compression for").String()
// Files to only compress.
policySetAddOnlyCompress = policySetCommand.Flag("add-only-compress", "List of extensions to add to the only-compress list").PlaceHolder("PATTERN").Strings()
policySetRemoveOnlyCompress = policySetCommand.Flag("remove-only-compress", "List of extensions to remove from the only-compress list").PlaceHolder("PATTERN").Strings()
policySetClearOnlyCompress = policySetCommand.Flag("clear-only-compress", "Clear list of extensions in the only-compress list").Bool()
// Files to never compress.
policySetAddNeverCompress = policySetCommand.Flag("add-never-compress", "List of extensions to add to the never compress list").PlaceHolder("PATTERN").Strings()
policySetRemoveNeverCompress = policySetCommand.Flag("remove-never-compress", "List of extensions to remove from the never compress list").PlaceHolder("PATTERN").Strings()
policySetClearNeverCompress = policySetCommand.Flag("clear-never-compress", "Clear list of extensions in the never compress list").Bool()
// Dot-ignore files to look at.
policySetAddDotIgnore = policySetCommand.Flag("add-dot-ignore", "List of paths to add to the dot-ignore list").PlaceHolder("FILENAME").Strings()
policySetRemoveDotIgnore = policySetCommand.Flag("remove-dot-ignore", "List of paths to remove from the dot-ignore list").PlaceHolder("FILENAME").Strings()
policySetClearDotIgnore = policySetCommand.Flag("clear-dot-ignore", "Clear list of paths in the dot-ignore list").Bool()
policySetMaxFileSize = policySetCommand.Flag("max-file-size", "Exclude files above given size").PlaceHolder("N").String()
// Error handling behavior.
policyIgnoreFileErrors = policySetCommand.Flag("ignore-file-errors", "Ignore errors reading files while traversing ('true', 'false', 'inherit')").Enum(booleanEnumValues...)
policyIgnoreDirectoryErrors = policySetCommand.Flag("ignore-dir-errors", "Ignore errors reading directories while traversing ('true', 'false', 'inherit").Enum(booleanEnumValues...)
// General policy.
policySetInherit = policySetCommand.Flag(inheritPolicyString, "Enable or disable inheriting policies from the parent").BoolList()
)
const (
inheritPolicyString = "inherit"
)
func init() {
policySetCommand.Action(repositoryAction(setPolicy))
}
func setPolicy(ctx context.Context, rep repo.Repository) error {
targets, err := policyTargets(ctx, rep, policySetGlobal, policySetTargets)
if err != nil {
return err
}
for _, target := range targets {
p, err := policy.GetDefinedPolicy(ctx, rep, target)
switch {
case err == policy.ErrPolicyNotFound:
p = &policy.Policy{}
case err != nil:
return errors.Wrap(err, "could not get defined policy")
}
printStderr("Setting policy for %v\n", target)
changeCount := 0
if err := setPolicyFromFlags(p, &changeCount); err != nil {
return err
}
if changeCount == 0 {
return errors.New("no changes specified")
}
if err := policy.SetPolicy(ctx, rep, target, p); err != nil {
return errors.Wrapf(err, "can't save policy for %v", target)
}
}
return nil
}
func setPolicyFromFlags(p *policy.Policy, changeCount *int) error {
if err := setRetentionPolicyFromFlags(&p.RetentionPolicy, changeCount); err != nil {
return errors.Wrap(err, "retention policy")
}
setFilesPolicyFromFlags(&p.FilesPolicy, changeCount)
if err := setErrorHandlingPolicyFromFlags(&p.ErrorHandlingPolicy, changeCount); err != nil {
return errors.Wrap(err, "error handling policy")
}
if err := setCompressionPolicyFromFlags(&p.CompressionPolicy, changeCount); err != nil {
return errors.Wrap(err, "compression policy")
}
if err := setSchedulingPolicyFromFlags(&p.SchedulingPolicy, changeCount); err != nil {
return errors.Wrap(err, "scheduling policy")
}
if err := applyPolicyNumber64("maximum file size", &p.FilesPolicy.MaxFileSize, *policySetMaxFileSize, changeCount); err != nil {
return errors.Wrap(err, "maximum file size")
}
// It's not really a list, just optional boolean, last one wins.
for _, inherit := range *policySetInherit {
*changeCount++
p.NoParent = !inherit
}
return nil
}
func setFilesPolicyFromFlags(fp *policy.FilesPolicy, changeCount *int) {
if *policySetClearDotIgnore {
*changeCount++
printStderr(" - removing all rules for dot-ignore files\n")
fp.DotIgnoreFiles = nil
} else {
fp.DotIgnoreFiles = addRemoveDedupeAndSort("dot-ignore files", fp.DotIgnoreFiles, *policySetAddDotIgnore, *policySetRemoveDotIgnore, changeCount)
}
if *policySetClearIgnore {
*changeCount++
fp.IgnoreRules = nil
printStderr(" - removing all ignore rules\n")
} else {
fp.IgnoreRules = addRemoveDedupeAndSort("ignored files", fp.IgnoreRules, *policySetAddIgnore, *policySetRemoveIgnore, changeCount)
}
}
func setErrorHandlingPolicyFromFlags(fp *policy.ErrorHandlingPolicy, changeCount *int) error {
switch {
case *policyIgnoreFileErrors == "":
case *policyIgnoreFileErrors == inheritPolicyString:
*changeCount++
fp.IgnoreFileErrors = nil
printStderr(" - inherit file read error behavior from parent\n")
default:
val, err := strconv.ParseBool(*policyIgnoreFileErrors)
if err != nil {
return err
}
*changeCount++
fp.IgnoreFileErrors = &val
printStderr(" - setting ignore file read errors to %v\n", val)
}
switch {
case *policyIgnoreDirectoryErrors == "":
case *policyIgnoreDirectoryErrors == inheritPolicyString:
*changeCount++
fp.IgnoreDirectoryErrors = nil
printStderr(" - inherit directory read error behavior from parent\n")
default:
val, err := strconv.ParseBool(*policyIgnoreDirectoryErrors)
if err != nil {
return err
}
*changeCount++
fp.IgnoreDirectoryErrors = &val
printStderr(" - setting ignore directory read errors to %v\n", val)
}
return nil
}
func setRetentionPolicyFromFlags(rp *policy.RetentionPolicy, changeCount *int) error {
cases := []struct {
desc string
max **int
flagValue *string
}{
{"number of annual backups to keep", &rp.KeepAnnual, policySetKeepAnnual},
{"number of monthly backups to keep", &rp.KeepMonthly, policySetKeepMonthly},
{"number of weekly backups to keep", &rp.KeepWeekly, policySetKeepWeekly},
{"number of daily backups to keep", &rp.KeepDaily, policySetKeepDaily},
{"number of hourly backups to keep", &rp.KeepHourly, policySetKeepHourly},
{"number of latest backups to keep", &rp.KeepLatest, policySetKeepLatest},
}
for _, c := range cases {
if err := applyPolicyNumber(c.desc, c.max, *c.flagValue, changeCount); err != nil {
return err
}
}
return nil
}
func setSchedulingPolicyFromFlags(sp *policy.SchedulingPolicy, changeCount *int) error {
// It's not really a list, just optional value.
for _, interval := range *policySetInterval {
*changeCount++
sp.SetInterval(interval)
printStderr(" - setting snapshot interval to %v\n", sp.Interval())
break
}
if len(*policySetTimesOfDay) > 0 {
var timesOfDay []policy.TimeOfDay
for _, tods := range *policySetTimesOfDay {
for _, tod := range strings.Split(tods, ",") {
if tod == inheritPolicyString {
timesOfDay = nil
break
}
var timeOfDay policy.TimeOfDay
if err := timeOfDay.Parse(tod); err != nil {
return errors.Wrap(err, "unable to parse time of day")
}
timesOfDay = append(timesOfDay, timeOfDay)
}
}
*changeCount++
sp.TimesOfDay = policy.SortAndDedupeTimesOfDay(timesOfDay)
if timesOfDay == nil {
printStderr(" - resetting snapshot times of day to default\n")
} else {
printStderr(" - setting snapshot times to %v\n", timesOfDay)
}
}
return nil
}
func setCompressionPolicyFromFlags(p *policy.CompressionPolicy, changeCount *int) error {
if err := applyPolicyNumber64("minimum file size subject to compression", &p.MinSize, *policySetCompressionMinSize, changeCount); err != nil {
return errors.Wrap(err, "minimum file size subject to compression")
}
if err := applyPolicyNumber64("maximum file size subject to compression", &p.MaxSize, *policySetCompressionMaxSize, changeCount); err != nil {
return errors.Wrap(err, "maximum file size subject to compression")
}
if v := *policySetCompressionAlgorithm; v != "" {
*changeCount++
if v == inheritPolicyString {
printStderr(" - resetting compression algorithm to default value inherited from parent\n")
p.CompressorName = ""
} else {
printStderr(" - setting compression algorithm to %v\n", v)
p.CompressorName = compression.Name(v)
}
}
if *policySetClearOnlyCompress {
*changeCount++
p.OnlyCompress = nil
printStderr(" - removing all only-compress extensions\n")
} else {
p.OnlyCompress = addRemoveDedupeAndSort("only-compress extensions",
p.OnlyCompress, *policySetAddOnlyCompress, *policySetRemoveOnlyCompress, changeCount)
}
if *policySetClearNeverCompress {
*changeCount++
p.NeverCompress = nil
printStderr(" - removing all never-compress extensions\n")
} else {
p.NeverCompress = addRemoveDedupeAndSort("never-compress extensions",
p.NeverCompress, *policySetAddNeverCompress, *policySetRemoveNeverCompress, changeCount)
}
return nil
}
func addRemoveDedupeAndSort(desc string, base, add, remove []string, changeCount *int) []string {
entries := map[string]bool{}
for _, b := range base {
entries[b] = true
}
for _, b := range add {
*changeCount++
printStderr(" - adding %v to %v\n", b, desc)
entries[b] = true
}
for _, b := range remove {
*changeCount++
printStderr(" - removing %v from %v\n", b, desc)
delete(entries, b)
}
var s []string
for k := range entries {
s = append(s, k)
}
sort.Strings(s)
return s
}
func applyPolicyNumber(desc string, val **int, str string, changeCount *int) error {
if str == "" {
// not changed
return nil
}
if str == inheritPolicyString || str == "default" {
*changeCount++
printStderr(" - resetting %v to a default value inherited from parent.\n", desc)
*val = nil
return nil
}
v, err := strconv.ParseInt(str, 10, 32)
if err != nil {
return errors.Wrapf(err, "can't parse the %v %q", desc, str)
}
i := int(v)
*changeCount++
printStderr(" - setting %v to %v.\n", desc, i)
*val = &i
return nil
}
func applyPolicyNumber64(desc string, val *int64, str string, changeCount *int) error {
if str == "" {
// not changed
return nil
}
if str == inheritPolicyString || str == "default" {
*changeCount++
printStderr(" - resetting %v to a default value inherited from parent.\n", desc)
*val = 0
return nil
}
v, err := strconv.ParseInt(str, 10, 64)
if err != nil {
return errors.Wrapf(err, "can't parse the %v %q", desc, str)
}
*changeCount++
printStderr(" - setting %v to %v.\n", desc, v)
*val = v
return nil
}
func supportedCompressionAlgorithms() []string {
var res []string
for name := range compression.ByName {
res = append(res, string(name))
}
sort.Strings(res)
return append([]string{inheritPolicyString, "none"}, res...)
}