summaryrefslogtreecommitdiff
path: root/pkgs/btclone/btclone.nix
blob: 68673a770060953a57b2b8c0fb276f1c3fde14f5 (plain)
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
''
  usage() { echo "Usage: $0 <subvolume> <target> [<up-to>]" >&2; exit 1; }

  [[ $# -ge 2 && $# -le 3 && ( $# -ne 3 || $(("$3" + 0)) = "$3" ) ]] || usage
  [[ $UID -eq 0 ]] || { echo "$0: requires root privileges" >&2; exit 1; }

  SOURCE="$1"
  TARGET="$2"
  [ $# -ge 3 ] && UP_TO="$3" || UP_TO=

  get_snapshots() {
      FILTER="\$1 > 0"
      [[ -n "$UP_TO" ]] && FILTER="\$1 > 0 && \$1 <= $UP_TO"

      find "$1" -follow -maxdepth 1 -printf '%f\n' \
          | sed 's@/$@@g' | awk "$FILTER { print }" | sort -n
  }

  clone() {
      if [[ -n "$2" ]]; then
          echo "Cloning $1 (incremental from $2)..." >&2
          PARENT_ARG="-p$SOURCE/$2/snapshot"
      else
          echo "Cloning $1 (non-incremental)..." >&2
          PARENT_ARG="--"
      fi

      FROM="$SOURCE/$1"
      INTO="$TARGET/$1"

      [[ -d "$INTO" ]] || mkdir "$INTO"
      btrfs send "$PARENT_ARG" "$FROM/snapshot" | pv | btrfs receive "$INTO"
      cp -a "$FROM/info.xml" "$INTO/"
  }

  cleanup() {
      echo "Removing $1..." >&2

      AT="$TARGET/$1"
      [[ -d "$AT/snapshot" ]] && btrfs subvolume delete "$AT/snapshot"
      rm -f "$AT/info.xml"
      rmdir "$AT"
  }

  while true; do
      ls -- "$SOURCE/" "$TARGET/" >/dev/null
      mapfile -t SNAPSHOTS < <(get_snapshots "$SOURCE")
      [[ ''${#SNAPSHOTS} -eq 0 ]] && { echo "$0: nothing to do" >&2; exit; }
      mapfile -t EXISTENT < <(get_snapshots "$TARGET")

      RESET=0
      PARENT=
      ALL_ARE_BAD=0
      for SNAPSHOT in "''${SNAPSHOTS[@]}"; do
          if [[ ! -e "$SOURCE/$SNAPSHOT" ]]; then
              echo "Notice: resetting listings due to time lapse" >&2

              RESET=1
              break
          elif [[ ! -s "$SOURCE/$SNAPSHOT/info.xml" ]]; then
              echo "Warning: skipping source $SNAPSHOT due to invalid info.xml" >&2
              continue
          fi

          CLONE=0
          if [[ ! -e "$TARGET/$SNAPSHOT" ]]; then
              CLONE=1
          elif [[ $ALL_ARE_BAD -eq 1 || ! -f "$TARGET/$SNAPSHOT/info.xml" ]]; then
              echo -n "Warning: bad copy $SNAPSHOT (" >&2
              [[ $ALL_ARE_BAD -eq 1 ]] && echo -n "bad chain" >&2 || echo -n "incomplete" >&2
              echo "), recreating..." >&2

              cleanup "$SNAPSHOT"

              CLONE=1
              ALL_ARE_BAD=1
          fi

          if [[ $CLONE -eq 1 ]]; then
              clone "$SNAPSHOT" "$PARENT"
          elif [[ -z "$PARENT" ]]; then
              echo "Initial snapshot present, all copies will be incremental" >&2
          fi

          chmod 750 "$TARGET/$SNAPSHOT"
          PARENT="$SNAPSHOT"
      done

      [[ $RESET -eq 1 ]] && continue

      echo "Copies updated, cleaning up..." >&2
      for SNAPSHOT in "''${EXISTENT[@]}"; do
          FOUND=""
          for KEEP in "''${SNAPSHOTS[@]}"; do
              if [ "$KEEP" = "$SNAPSHOT" ]; then
                  FOUND=1
                  break
              fi
          done

          [ -n "$FOUND" ] || cleanup "$SNAPSHOT"
      done

      break
  done

  echo "Done" >&2
''