Skip to content

Instantly share code, notes, and snippets.

@jamesWalker55
Created January 6, 2026 16:04
Show Gist options
  • Select an option

  • Save jamesWalker55/6179caebab33af2ff9c72b881a63e7f0 to your computer and use it in GitHub Desktop.

Select an option

Save jamesWalker55/6179caebab33af2ff9c72b881a63e7f0 to your computer and use it in GitHub Desktop.
Python script to delete Reaper project backups according to a retention policy. Please edit `expected_working_dir`, `KEEP_LAST_X` and `POLICY` to your use case.
# /// script
# dependencies = [
# "retention-rules",
# ]
# ///
import logging
import os
import re
import sys
from typing import NamedTuple, Self
from datetime import datetime
from retention_rules import PolicyBuilder
log = logging.getLogger(__name__)
del logging
# failsafe check, to make sure you are executing in the right directory
expected_working_dir = R"D:\Audio Projects (Reaper)"
if os.getcwd() != expected_working_dir:
log.warning("unexpected working directory, aborting")
print(
f"Current working directory is {os.getcwd()} instead of {expected_working_dir}, are you sure you ran this script in the right folder?"
)
sys.exit(1)
KEEP_LAST_X = 15
POLICY = PolicyBuilder().build(
{
"rules": [
{"applies_for": "1D", "retain_every": "H"},
{"applies_for": "3M", "retain_every": "D/8"},
{"applies_for": "6M", "retain_every": "D"},
{"applies_for": "Y", "retain_every": "W/2"},
],
"reuse": True,
"retain": "newest",
}
)
class RppBak(NamedTuple):
filename: str
title: str
time: datetime
def __str__(self) -> str:
return f"{self.time_repr()} {self.title!r}"
def __repr__(self) -> str:
return f"RppBak('{self.time_repr()}', {self.title!r})"
def time_repr(self):
return self.time.strftime(r"%Y-%m-%d %H:%M:%S")
@classmethod
def try_from_filename(cls, filename: str) -> Self | None:
stem, ext = os.path.splitext(filename)
if ext.lower() != ".rpp-bak":
return None
match = re.match(r"\A(.+)-(\d\d\d\d)-(\d\d)-(\d\d)_(\d\d)(\d\d)(\d\d)?\Z", stem)
if match is None:
return None
# `s` may be None
title, year, month, day, h, m, s = match.groups()
time = datetime(
int(year),
int(month),
int(day),
int(h),
int(m),
0 if s is None else int(s),
)
return cls(filename, title, time)
def scan_rpp_bak(root: str | os.PathLike):
result: list[tuple[str, dict[str, list[RppBak]]]] = []
for dirpath, dirnames, filenames in os.walk(root):
# filter only .rpp-bak files
rppbaks = [RppBak.try_from_filename(x) for x in filenames]
rppbaks = [x for x in rppbaks if x is not None]
if len(rppbaks) == 0:
continue
# group the files by their title
# (a single folder may have multiple project's backups)
title_group: dict[str, list[RppBak]] = {}
for rb in rppbaks:
if rb.title not in title_group:
title_group[rb.title] = []
title_group[rb.title].append(rb)
result.append((dirpath, title_group))
return result
class RppBakKeep(NamedTuple):
item: RppBak
keep: bool
def decide_rppbak_keep(rppbak: list[RppBak]):
if len(rppbak) == 0:
raise RuntimeError("TODO")
# retention checking is based on latest backup time
latest_time = max(rppbak, key=lambda x: x.time).time
# check files to delete
retention = POLICY.check_retention(
rppbak,
key=lambda x: x.time,
now=latest_time,
)
# the most recent 15 files are always kept
retention.sort(key=lambda x: x.item.time)
for x in retention[-KEEP_LAST_X:]:
x.retain = True
return [RppBakKeep(x.item, x.retain) for x in retention]
# Function to set up logging
def setup_logging():
import logging
log.setLevel(logging.DEBUG)
# Custom log output format
msg_fmt = "[%(asctime)s %(levelname)s %(name)s.%(funcName)s] %(message)s"
fmt = logging.Formatter(fmt=msg_fmt)
# Log to a file
log_filename = f"trim-backups.log"
h = logging.FileHandler(log_filename, "w", encoding="utf8")
h.setLevel(logging.DEBUG)
h.setFormatter(fmt)
log.addHandler(h)
# Log to stderr
h = logging.StreamHandler()
h.setLevel(logging.WARNING)
h.setFormatter(fmt)
log.addHandler(h)
def main():
setup_logging()
log.info("scanning .rpp-bak in: %s", expected_working_dir)
result = scan_rpp_bak(expected_working_dir)
log.info("found .rpp-bak in %d folders", len(result))
for dirname, title_group in result:
for title, rppbaks in title_group.items():
keeps = decide_rppbak_keep(rppbaks)
keep_count = len([x for x in keeps if x.keep is True])
delete_count = len([x for x in keeps if x.keep is False])
if delete_count == 0:
continue
log.info("dir=%s title=%s", dirname, title)
log.info("keep %d files:", keep_count)
log.info("delete %d files:", delete_count)
for k in keeps:
if k.keep is False:
path = os.path.join(dirname, k.item.filename)
log.info("%s", path)
os.remove(path)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment