Files
kopia/cli/command_policy_edit.go
Erkki Seppälä 6a93e4d5b9 Added support for scanning only one filesystem via files policy (#676)
The new files policy oneFileSystem ignores files that are mounted to
other filesystems similarly to tar's --one-file-system switch. For
example, if this is enabled, backing up / should now automatically
ignore /dev, /proc, etc, so the directory entries themselves don't
appear in the backup. The value of the policy is 'false' by default.

This is implemented by adding a non-windows-field Device (of type
DeviceInfo, reflecting the implementation of Owner) to the Entry
interface. DeviceInfo holds the dev and rdev acquired with stat (same
way as with Owner), but in addition to that it also holds the same
values for the parent directory. It would seem that doing this in some
other way, ie. in ReadDir, would require modifying the ReadDir
interface which seems a too large modification for a feature this
small.

This change introduces a duplication of 'stat' call to the files, as
the Owner feature already does a separate call. I doubt the
performance implications are noticeable, though with some refactoring
both Owner and Device fields could be filled in in one go.

Filling in the field has been placed in fs/localfs/localfs.go where
entryFromChildFileInfo has acquired a third parameter giving the the
parent entry. From that information the Device of the parent is
retrieved, to be passed off to platformSpecificDeviceInfo which does
the rest of the paperwork. Other fs implementations just put in the
default values.

The Dev and Rdev fields returned by the 'stat' call have different
sizes on different platforms, but for convenience they are internally
handled the same. The conversion is done with local_fs_32bit.go and
local_fs_64bit.go which are conditionally compiled on different
platforms.

Finally the actual check of the condition is in ignorefs.go function
shouldIncludeByDevice which is analoguous to the other similarly named
functions.

Co-authored-by: Erkki Seppälä <flux@inside.org>
2020-10-14 22:45:32 -07:00

136 lines
3.5 KiB
Go

package cli
import (
"bytes"
"context"
"encoding/json"
"fmt"
"strings"
"github.com/pkg/errors"
"github.com/kopia/kopia/internal/editor"
"github.com/kopia/kopia/repo"
"github.com/kopia/kopia/snapshot/policy"
)
const policyEditHelpText = `
# Editing policy for '%v'
# Make changes to the policy, save your file and exit the editor.
# The output must be valid JSON.
# Lines starting with # are comments and automatically removed.
`
const policyEditRetentionHelpText = ` # Retention for snapshots of this directory. Options include:
# "keepLatest": number
# "keepDaily": number
# "keepHourly": number
# "keepWeekly": number
# "keepMonthly": number
# "keepAnnual": number
`
const policyEditFilesHelpText = `
# Which files to include in snapshots. Options include:
# "ignore": ["*.ext", "*.ext2"]
# "dotIgnoreFiles": [".gitignore", ".kopiaignore"]
# "maxFileSize": number
# "noParentDotFiles": true
# "noParentIgnore": true
# "oneFileSystem": false
`
const policyEditSchedulingHelpText = `
# Snapshot scheduling options. Options include:
# "intervalSeconds": number /* 86400-day, 3600-hour, 60-minute */
# "timesOfDay": [{"hour":H,"min":M},{"hour":H,"min":M}]
`
var (
policyEditCommand = policyCommands.Command("edit", "Set snapshot policy for a single directory, user@host or a global policy.")
policyEditTargets = policyEditCommand.Arg("target", "Target of a policy ('global','user@host','@host') or a path").Strings()
policyEditGlobal = policyEditCommand.Flag("global", "Set global policy").Bool()
)
func init() {
policyEditCommand.Action(repositoryAction(editPolicy))
}
func editPolicy(ctx context.Context, rep repo.Repository) error {
targets, err := policyTargets(ctx, rep, policyEditGlobal, policyEditTargets)
if err != nil {
return err
}
for _, target := range targets {
original, err := policy.GetDefinedPolicy(ctx, rep, target)
if errors.Is(err, policy.ErrPolicyNotFound) {
original = &policy.Policy{}
}
log(ctx).Infof("Editing policy for %v using external editor...", target)
s := policyEditHelpText + prettyJSON(original)
s = insertHelpText(s, ` "retention": {`, policyEditRetentionHelpText)
s = insertHelpText(s, ` "files": {`, policyEditFilesHelpText)
s = insertHelpText(s, ` "scheduling": {`, policyEditSchedulingHelpText)
var updated *policy.Policy
if err := editor.EditLoop(ctx, "policy.conf", s, func(edited string) error {
updated = &policy.Policy{}
d := json.NewDecoder(bytes.NewBufferString(edited))
d.DisallowUnknownFields()
return d.Decode(updated)
}); err != nil {
return err
}
if jsonEqual(updated, original) {
log(ctx).Infof("Policy for %v unchanged", target)
continue
}
log(ctx).Infof("Updated policy for %v\n%v", target, prettyJSON(updated))
fmt.Print("Save updated policy? (y/N) ")
var shouldSave string
fmt.Scanf("%v", &shouldSave)
if strings.HasPrefix(strings.ToLower(shouldSave), "y") {
if err := policy.SetPolicy(ctx, rep, target, updated); err != nil {
return errors.Wrapf(err, "can't save policy for %v", target)
}
}
}
return nil
}
func prettyJSON(v interface{}) string {
var b bytes.Buffer
e := json.NewEncoder(&b)
e.SetIndent("", " ")
e.Encode(v) //nolint:errcheck
return b.String()
}
func jsonEqual(v1, v2 interface{}) bool {
return prettyJSON(v1) == prettyJSON(v2)
}
func insertHelpText(s, lookFor, help string) string {
p := strings.Index(s, lookFor)
if p < 0 {
return s
}
return s[0:p] + help + s[p:]
}