Split retention and missing range

This commit is contained in:
Dobby
2025-07-26 21:07:54 +01:00
committed by Sebastian Goscik
parent afe025be1d
commit 2bd48014a0
3 changed files with 43 additions and 9 deletions

View File

@@ -23,7 +23,7 @@ retention period.
## Features ## Features
- Listens to events in real-time via the Unifi Protect websocket API - Listens to events in real-time via the Unifi Protect websocket API
- Ensures any previous and/or missed events within the retention period are also backed up - Ensures any previous and/or missed events within the missing range are also backed up
- Supports uploading to a [wide range of storage systems using `rclone`](https://rclone.org/overview/) - Supports uploading to a [wide range of storage systems using `rclone`](https://rclone.org/overview/)
- Automatic pruning of old clips - Automatic pruning of old clips
@@ -123,6 +123,10 @@ Options:
`--max-age` argument of `rclone` `--max-age` argument of `rclone`
(https://rclone.org/filtering/#max-age-don-t-transfer-any-file- (https://rclone.org/filtering/#max-age-don-t-transfer-any-file-
older-than-this) [default: 7d] older-than-this) [default: 7d]
--missing-range TEXT How far back should missing events be checked for. Defaults to
the same as the retention time. Format as per the `--max-age`
argument of `rclone` (https://rclone.org/filtering/#max-age-don-
t-transfer-any-file-older-than-this)
--rclone-args TEXT Optional extra arguments to pass to `rclone rcat` directly. --rclone-args TEXT Optional extra arguments to pass to `rclone rcat` directly.
Common usage for this would be to set a bandwidth limit, for Common usage for this would be to set a bandwidth limit, for
example. example.
@@ -131,14 +135,21 @@ Options:
instead of using the recycle bin on a destination. Google Drive instead of using the recycle bin on a destination. Google Drive
example: `--drive-use-trash=false` example: `--drive-use-trash=false`
--detection-types TEXT A comma separated list of which types of detections to backup. --detection-types TEXT A comma separated list of which types of detections to backup.
Valid options are: `motion`, `person`, `vehicle`, `ring` Valid options are: `motion`, `ring`, `line`, `fingerprint`,
[default: motion,person,vehicle,ring] `nfc`, `person`, `animal`, `vehicle`, `licensePlate`, `package`,
`face`, `car`, `pet`, `alrmSmoke`, `alrmCmonx`, `smoke_cmonx`,
`alrmSiren`, `alrmBabyCry`, `alrmSpeak`, `alrmBark`,
`alrmBurglar`, `alrmCarHorn`, `alrmGlassBreak` [default: motion
,ring,line,fingerprint,nfc,person,animal,vehicle,licensePlate,pa
ckage,face,car,pet,alrmSmoke,alrmCmonx,smoke_cmonx,alrmSiren,alr
mBabyCry,alrmSpeak,alrmBark,alrmBurglar,alrmCarHorn,alrmGlassBre
ak]
--ignore-camera TEXT IDs of cameras for which events should not be backed up. Use --ignore-camera TEXT IDs of cameras for which events should not be backed up. Use
multiple times to ignore multiple IDs. If being set as an multiple times to ignore multiple IDs. If being set as an
environment variable the IDs should be separated by whitespace. environment variable the IDs should be separated by whitespace.
Alternatively, use a Unifi user with a role which has access Alternatively, use a Unifi user with a role which has access
restricted to the subset of cameras that you wish to backup. restricted to the subset of cameras that you wish to backup.
--camera TEXT IDs of *ONLY* cameras for which events should be backed up. Use --camera TEXT IDs of *ONLY* cameras for which events should be backed up. Use
multiple times to include multiple IDs. If being set as an multiple times to include multiple IDs. If being set as an
environment variable the IDs should be separated by whitespace. environment variable the IDs should be separated by whitespace.
Alternatively, use a Unifi user with a role which has access Alternatively, use a Unifi user with a role which has access
@@ -214,6 +225,7 @@ always take priority over environment variables):
- `UFP_PORT` - `UFP_PORT`
- `UFP_SSL_VERIFY` - `UFP_SSL_VERIFY`
- `RCLONE_RETENTION` - `RCLONE_RETENTION`
- `MISSING_RANGE`
- `RCLONE_DESTINATION` - `RCLONE_DESTINATION`
- `RCLONE_ARGS` - `RCLONE_ARGS`
- `RCLONE_PURGE_ARGS` - `RCLONE_PURGE_ARGS`

View File

@@ -30,8 +30,11 @@ def _parse_detection_types(ctx, param, value):
return types return types
def parse_rclone_retention(ctx, param, retention) -> relativedelta: def parse_rclone_retention(ctx, param, retention) -> relativedelta | None:
"""Parse the rclone `retention` parameter into a relativedelta which can then be used to calculate datetimes.""" """Parse the rclone `retention` parameter into a relativedelta which can then be used to calculate datetimes."""
if retention is None:
return None
matches = {k: int(v) for v, k in re.findall(r"([\d]+)(ms|s|m|h|d|w|M|y)", retention)} matches = {k: int(v) for v, k in re.findall(r"([\d]+)(ms|s|m|h|d|w|M|y)", retention)}
# Check that we matched the whole string # Check that we matched the whole string
@@ -79,6 +82,15 @@ def parse_rclone_retention(ctx, param, retention) -> relativedelta:
"(https://rclone.org/filtering/#max-age-don-t-transfer-any-file-older-than-this)", "(https://rclone.org/filtering/#max-age-don-t-transfer-any-file-older-than-this)",
callback=parse_rclone_retention, callback=parse_rclone_retention,
) )
@click.option(
"--missing-range",
default=None,
envvar="MISSING_RANGE",
help="How far back should missing events be checked for. Defaults to the same as the retention time. "
"Format as per the `--max-age` argument of `rclone` "
"(https://rclone.org/filtering/#max-age-don-t-transfer-any-file-older-than-this)",
callback=parse_rclone_retention,
)
@click.option( @click.option(
"--rclone-args", "--rclone-args",
default="", default="",
@@ -261,6 +273,9 @@ def main(**kwargs):
) )
raise SystemExit(200) # throw 200 = arg error, service will not be restarted (docker) raise SystemExit(200) # throw 200 = arg error, service will not be restarted (docker)
if kwargs.get("missing_range") is None:
kwargs["missing_range"] = kwargs.get("retention")
# Only create the event listener and run if validation passes # Only create the event listener and run if validation passes
event_listener = UnifiProtectBackup(**kwargs) event_listener = UnifiProtectBackup(**kwargs)
run(event_listener.start(), stop_on_unhandled_errors=True) run(event_listener.start(), stop_on_unhandled_errors=True)

View File

@@ -68,6 +68,7 @@ class UnifiProtectBackup:
verify_ssl: bool, verify_ssl: bool,
rclone_destination: str, rclone_destination: str,
retention: relativedelta, retention: relativedelta,
missing_range: relativedelta,
rclone_args: str, rclone_args: str,
rclone_purge_args: str, rclone_purge_args: str,
detection_types: List[str], detection_types: List[str],
@@ -98,9 +99,13 @@ class UnifiProtectBackup:
rclone_destination (str): `rclone` destination path in the format rclone_destination (str): `rclone` destination path in the format
{rclone remote}:{path on remote}. E.g. {rclone remote}:{path on remote}. E.g.
`gdrive:/backups/unifi_protect` `gdrive:/backups/unifi_protect`
retention (str): How long should event clips be backed up for. Format as per the retention (relativedelta): How long should event clips be backed up for. Format as per the
`--max-age` argument of `rclone` `--max-age` argument of `rclone`
(https://rclone.org/filtering/#max-age-don-t-transfer-any-file-older-than-this) (https://rclone.org/filtering/#max-age-don-t-transfer-any-file-older-than-this)
missing_range (relativedelta): How far back should missing events be checked for. Defaults to
the same as the retention time. Format as per the
`--max-age` argument of `rclone`
(https://rclone.org/filtering/#max-age-don-t-transfer-any-file-older-than-this)
rclone_args (str): A bandwidth limit which is passed to the `--bwlimit` argument of rclone_args (str): A bandwidth limit which is passed to the `--bwlimit` argument of
`rclone` (https://rclone.org/docs/#bwlimit-bandwidth-spec) `rclone` (https://rclone.org/docs/#bwlimit-bandwidth-spec)
rclone_purge_args (str): Optional extra arguments to pass to `rclone delete` directly. rclone_purge_args (str): Optional extra arguments to pass to `rclone delete` directly.
@@ -144,6 +149,7 @@ class UnifiProtectBackup:
logger.debug(f" {verify_ssl=}") logger.debug(f" {verify_ssl=}")
logger.debug(f" {rclone_destination=}") logger.debug(f" {rclone_destination=}")
logger.debug(f" {retention=}") logger.debug(f" {retention=}")
logger.debug(f" {missing_range=}")
logger.debug(f" {rclone_args=}") logger.debug(f" {rclone_args=}")
logger.debug(f" {rclone_purge_args=}") logger.debug(f" {rclone_purge_args=}")
logger.debug(f" {ignore_cameras=}") logger.debug(f" {ignore_cameras=}")
@@ -163,6 +169,7 @@ class UnifiProtectBackup:
self.rclone_destination = rclone_destination self.rclone_destination = rclone_destination
self.retention = retention self.retention = retention
self.missing_range = missing_range
self.rclone_args = rclone_args self.rclone_args = rclone_args
self.rclone_purge_args = rclone_purge_args self.rclone_purge_args = rclone_purge_args
self.file_structure_format = file_structure_format self.file_structure_format = file_structure_format
@@ -314,15 +321,15 @@ class UnifiProtectBackup:
tasks.append(purge.start()) tasks.append(purge.start())
# Create missing event task # Create missing event task
# This will check all the events within the retention period, if any have been missed and not backed up # This will check all the events within the missing_range period, if any have been missed and not
# they will be added to the event queue # backed up. they will be added to the event queue
missing = MissingEventChecker( missing = MissingEventChecker(
self._protect, self._protect,
self._db, self._db,
download_queue, download_queue,
downloader, downloader,
uploaders, uploaders,
self.retention, self.missing_range,
self.detection_types, self.detection_types,
self.ignore_cameras, self.ignore_cameras,
self.cameras, self.cameras,