BTRFS Rotating Snapshots

Python Script for Rotating Snapshots

By Andrew Que

About

The BTRFS file system has the ability to do snapshots. This can be very useful for backups. Rotating snapshots is an application of snapshots where snapshots are taken at regular intervals and older snapshots are removed. This could be over time periods of hours, months or even years.

There are other programs that perform rotating snapshots, including ones that work with BTRFS such as timeshift. However, their snapshots are hard to navigate as they are just kept as numbers rather than dates. Such systems are designed to restore entire snapshots rather than restoring individual files/directories.

This snapshot system is designed to make a directory structure using the date/time of the snapshot, thus making it easy to navigate the resulting snapshots. Used with its sister program BTRFS Snapshots Browser allows files and directory changes to be traced over multiple snapshots, making it easy to find changes.

Download

Starting in 2025, archives are signed by DrQue.net.

Archives signed by Andrew Que. Keys are generated yearly and can be found on the MIT PGP Public Key Server by doing a search for Andrew Que. They can also be downloaded directly from DrQue.net.

Version 1.0.2

Released June 24, 2025

  • Bug fix for sending snapshots to destination over SSH.
Download
Version 1.0.2
File size
31,938
File date
2025-06-24 07:58:39.286
CRC-32
f4c04cf4
Source SHA256
59e067c8d025be18f17913d9ca87683d197a7a83cddd3c7449bd3bf0c1031a66
PGP signature
Source PGP signature
Show archived versions.

How it works

BTRFS Rotating Snapshots has three primary functions:

Typically snapshots are setup to be taken at regular intervals (hourly, daily monthly) using a cron job.

Taking Snapshots

There are two ways to specify commands: with inline parameters, and using a configuration file.

root@system:/# btrfsSnapshots.py take /path/to/subvolume /path/to/snapshots root@system:/# btrfsSnapshots.py --config /path/to/config.json take

This function will take a snapshot of the subvolume /path/to/subvolume and place the snapshot subvolume in /path/to/snapshots.

Timestamp format

Snapshots are named with the time the snapshot was taken. The date format is variable but typically is in the format YYYY-MM-DD HH:MM:SS.

The timestamp has three configuration variables:

date_format and snapshot_name_format often match one another. However, the snapshot name format might force some fields. For example, %Y-%m-%d %H:%M:00 forces the second field to zero. This can be desirable for cron jobs as they sometimes take a second or two to launch the scheduled process.

Another common timestamp is to omit the time of day completely. For example, %Y-%m-%d could be used if snapshots are taken at most once a day.

The regular expression is used to get a directory listing of existing snapshots. Other than the snapshot, BTRFS Rotation Snapshots is stateless. So to find the existing snapshots (needed during pruning) the existing listing of the destination directory is used and matched against a regular expression.

One side effect is that snapshots not following the normal pattern are not removed. This can be useful for named snapshots. However, this too can be overridden.

Checking for changes

By default new snapshots are checked against the previous snapshot for changes. If no changes have occurred, the new snapshot is removed. Checking for changes uses rsync.

Pruning Snapshots

Two forms of pruning: all configured by the command line, and from a configuration file.

root@system:/# btrfsSnapshots.py --keep-last-days=10 --keep-last-months=-1 prune /path/to/snapshots root@system:/# btrfsSnapshots.py --config /path/to/config.json prune

Periodically snapshots should be pruned so that unneeded snapshots are removed. There are four variables that control how long snapshots remain:

Configuration is saved in a JSON file. Anything that can be specified from the command line can be specified in the configuration file. Only difference is that underscores change to dashes.

{
  "keep_last_snapshots" : 11,
  "keep_last_days"      : 7,
  "keep_last_months"    : 5,
  "keep_last_years"     : 3
}

This configuration will yield a result like this:

root@system:/snapshots# ls -l
drwxr-xr-x 1 root root 54 Jan  1 00:00 '2019-12-31 17:45:00'
drwxr-xr-x 1 root root 54 Jan  1 00:00 '2020-12-31 17:45:00'
drwxr-xr-x 1 root root 54 Jan  1 00:00 '2021-12-31 17:45:00'
drwxr-xr-x 1 root root 54 Jan  1 00:00 '2022-05-31 17:45:00'
drwxr-xr-x 1 root root 54 Jan  1 00:00 '2022-06-30 17:45:00'
drwxr-xr-x 1 root root 54 Jan  1 00:00 '2022-07-31 17:45:00'
drwxr-xr-x 1 root root 54 Jan  1 00:00 '2022-08-31 17:45:00'
drwxr-xr-x 1 root root 54 Jan  1 00:00 '2022-09-30 17:45:00'
drwxr-xr-x 1 root root 54 Jan  1 00:00 '2022-10-20 17:45:00'
drwxr-xr-x 1 root root 54 Jan  1 00:00 '2022-10-21 17:45:00'
drwxr-xr-x 1 root root 54 Jan  1 00:00 '2022-10-22 17:45:00'
drwxr-xr-x 1 root root 54 Jan  1 00:00 '2022-10-23 17:45:00'
drwxr-xr-x 1 root root 54 Jan  1 00:00 '2022-10-24 17:45:00'
drwxr-xr-x 1 root root 54 Jan  1 00:00 '2022-10-25 17:45:00'
drwxr-xr-x 1 root root 54 Jan  1 00:00 '2022-10-26 17:45:00'
drwxr-xr-x 1 root root 54 Jan  1 00:00 '2022-10-27 15:15:00'
drwxr-xr-x 1 root root 54 Jan  1 00:00 '2022-10-27 15:30:00'
drwxr-xr-x 1 root root 54 Jan  1 00:00 '2022-10-27 15:45:00'
drwxr-xr-x 1 root root 54 Jan  1 00:00 '2022-10-27 16:00:00'
drwxr-xr-x 1 root root 54 Jan  1 00:00 '2022-10-27 16:15:00'
drwxr-xr-x 1 root root 54 Jan  1 00:00 '2022-10-27 16:30:00'
drwxr-xr-x 1 root root 54 Jan  1 00:00 '2022-10-27 16:45:00'
drwxr-xr-x 1 root root 54 Jan  1 00:00 '2022-10-27 17:00:00'
drwxr-xr-x 1 root root 54 Jan  1 00:00 '2022-10-27 17:15:00'
drwxr-xr-x 1 root root 54 Jan  1 00:00 '2022-10-27 17:30:00'
drwxr-xr-x 1 root root 54 Jan  1 00:00 '2022-10-27 17:45:00'
-rw-r--r-- 1 root root 32 Oct 27 17:45 lastSnapshot.txt

In this example snapshots are taken every 15 minutes. This is what remains after 5 years. The backgrounds are color coated:

A value of 0 in one of the fields means no snapshots in that interval are kept. A value of -1 means keep all records. For example, -1 in months will keep one snapshot for every month. All settings after this are moot. So if months are set to -1, it doesn't matter what is put in year because all months are saved anyway.

Cron job

Generally this script will be run using a cron job. There are two different jobs, taking a snapshot and pruning the snapshots.

# Hourly snapshots
0 * * * *     /usr/local/sbin/btrfsSnapshots.py --config=/etc/btrfsSnapshot/subvolumeA.json take
0 * * * *     /usr/local/sbin/btrfsSnapshots.py --config=/etc/btrfsSnapshot/subvolumeB.json take

# End-of-day snapshots
55 23 * * *   /usr/local/sbin/btrfsSnapshots.py --config=/etc/btrfsSnapshot/subvolumeA.json take -f

# Prune snapshots
0 0 * * *     /usr/local/sbin/btrfsSnapshots.py --config=/etc/btrfsSnapshot/subvolumeA.json prune
0 0 * * *     /usr/local/sbin/btrfsSnapshots.py --config=/etc/btrfsSnapshot/subvolumeB.json prune

In the example above, hourly snapshots are taken on two subvolumes.

BTRFS Setup Example

While snapshots can be used to make images of an entire hard drive, subvolumes can be used to effectively make backup images of individual directories. Typically, this is done with directories that contain regular changing data, and data that you may want to have for backup purposes if it has been changed.

For this example, say we want to setup snapshots on the following sets of data:

Assume the root file system is BTRFS and none of these directories currently exist. If they do, rename them so they can be created as subvolumes.

Create the subvolumes.

root@system:/# btrfs subvolume create /home/user/Documents
Create subvolume '/home/user/Documents'
root@system:/# btrfs subvolume create /home/user/Photos
Create subvolume '/home/user/Photos'
root@system:/# btrfs subvolume create /home/user/Recordings
Create subvolume '/home/user/Recordings'
root@system:/# btrfs subvolume create /home/user/Source
Create subvolume '/home/user/Source'

Now move your data into these directories. Once populate with data, you can use BTRFS Rotating Snapshots on those subvolumes. You will just need a path to place the snapshots. Some systems use a hidden snapshot directory like ~/.snapshots. If not wanting to hide the snapshots you can use ~/snapshots. To get the snapshots to show up as the first item in a directory listing, use ~/@snapshots. To make it the last item, use ~/_snapshots. For the example, we will make a directory in root called @snapshots.

root@system:/# mkdir /@snapshots

Now to create a schedule for when to take snapshots.

Documents
Changes often. Snapshot every hour. Keep last 24 snapshots, the last 60 days, last 8 months for 1 year. Backup daily.
Photos
Snapshot once a day. Keep last 90 days, 12 months for all time. Backup weekly.
Recordings (music/video)
Snapshot once a day. Keep last 30 days, 1 month for all time. Backup monthly.
Source (software source code)
Changes often. Snapshot every 15 minutes. Keep last 24 snapshots, the last 20 days, last 6 months for 2 years. Backup weekly.

To accomplish this, we will create configuration files in /etc/btrfs.

Configuration Files

Documents configuration

/etc/btrfs/documents.json

{
  "name"                    : "Documents",
  "source"                  : "/home/user/Documents",
  "destination"             : "/@snapshots/Documents",
  "keep_last_snapshots"     : 24,
  "keep_last_days"          : 60,
  "keep_last_months"        : 8,
  "keep_last_years"         : 1
}

Photos configuration

/etc/btrfs/photos.json

{
  "name"                    : "Photots",
  "source"                  : "/home/user/Pictures",
  "destination"             : "/@snapshots/Pictures",
  "keep_last_snapshots"     : 0,
  "keep_last_days"          : 90,
  "keep_last_months"        : 12,
  "keep_last_years"         : -1,
  "snapshot_name_format"    : "%Y-%m-%d",
  "date_regular_expression" : "^(\\d{4}-\\d{2}-\\d{2})$"
}

Recordings configuration

/etc/btrfs/recordings.json

{
  "name"                    : "Recordings",
  "source"                  : "/home/user/Music",
  "destination"             : "/@snapshots/Music",
  "keep_last_snapshots"     : 0,
  "keep_last_days"          : 30,
  "keep_last_months"        : 1,
  "keep_last_years"         : -1,
  "snapshot_name_format"    : "%Y-%m-%d",
  "date_regular_expression" : "^(\\d{4}-\\d{2}-\\d{2})$"
}

Source configuration

/etc/btrfs/source.json

{
  "name"                    : "Source",
  "source"                  : "/home/user/Source",
  "destination"             : "/@snapshots/Source",
  "keep_last_snapshots"     : 24,
  "keep_last_days"          : 20,
  "keep_last_months"        : 6,
  "keep_last_years"         : 2
}

Note how the snapshots that only take place daily have a specified date format. Both the date, and the regular expression are given to specify the date in year (4 characters), month and day. The default date format is year (4 characters), month, day, hour, minute and second. That is fine for periodic snapshots.

Cron jobs

Now the crontab configuration.

Crontab

# Periodic Snapshots
0 * * * *     /opt/btrfsSnapshots.py --config=/etc/btrfs/documents.json take
*/15 * * * *  /opt/btrfsSnapshots.py --config=/etc/btrfs/source.json    take

# End-of-day snapshots
55 23 * * *   /opt/btrfsSnapshots.py --config=/etc/btrfs/documents.json  --snapshot-name-format '\%Y-\%m-\%d \%H:59:59' take -f
55 23 * * *   /opt/btrfsSnapshots.py --config=/etc/btrfs/photos.json     take -f
55 23 * * *   /opt/btrfsSnapshots.py --config=/etc/btrfs/recordings.json take -f
55 23 * * *   /opt/btrfsSnapshots.py --config=/etc/btrfs/source.json     --snapshot-name-format '\%Y-\%m-\%d \%H:59:59' take -f

# Prune snapshots
0 0 * * *     /opt/btrfsSnapshots.py --config=/etc/btrfs/documents.json  prune
5 0 * * *    /opt/btrfsSnapshots.py --config=/etc/btrfs/photos.json     prune
10 0 * * *    /opt/btrfsSnapshots.py --config=/etc/btrfs/recordings.json prune
15 0 * * *    /opt/btrfsSnapshots.py --config=/etc/btrfs/source.json     prune
# Backup snapshots
0  2 * * *    /opt/btrfsSnapshots.py --config=/etc/btrfs/documents.json  send  /@snapshots/Documents   /remote/@snapshots/Documents/  remote-computer 30 2 * 0 3    /opt/btrfsSnapshots.py --config=/etc/btrfs/photos.json     send  /@snapshots/Pictures    /remote/@snapshots/Pictures/   remote-computer 0  3 * 1 *    /opt/btrfsSnapshots.py --config=/etc/btrfs/recordings.json send  /@snapshots/Music       /remote/@snapshots/Music/      remote-computer 30 3 * * 0    /opt/btrfsSnapshots.py --config=/etc/btrfs/source.json     send  /@snapshots/Source      /remote/@snapshots/Source/     remote-computer

The periodic snapshots just use the take parameter. Nothing more is needed.

End of the day snapshots have a forced take. That is, the snapshot is not removed even if nothing in the snapshot has changed. This is useful for having a record of end-of-day snapshots in your history. Notice how the documents and source force a snapshot name. In this case, the snapshot forced the minutes and seconds to be constant. Cron usually takes a second or two to start a job, and this forces the minutes and second of the timestamp to always remain the same.

Pruning snapshots takes place daily. This will remove old snapshots. It should be done after the daily snapshot has been taken. They are spread out with 5 minutes between them. This may not be necessary, but something to consider.

The backups are all sent to a remote computer called remote-computer, connected via SSH. Backups begin at 2:10 am. They are (and should be) staggered to account for network traffic, especially if the receiving computer is not on an interanet. The staggering time depends on the particular application. Here they are just

User comments

Feel free to leave some feedback. A subset of Markdown is supported.

Opinions are not censored, but all advertisements will unceremoniously be deleted.

Comments are Javascript-based. Without Javascript enabled, this message is all that appears.

Copyright

BTRFS Rotating Snapshots is free, open-source software released under the GNU General Public License v3.

Author

BTRFS Rotating Snapshots is written and maintained by Andrew Que. To get in touch with Andrew Que, visit his contact page.