mirror of
https://github.com/ep1cman/unifi-protect-backup.git
synced 2025-12-05 23:53:30 +00:00
Compare commits
16 Commits
restructur
...
v0.8.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34bc37bd0b | ||
|
|
f15cdf9a9b | ||
|
|
63d368f14c | ||
|
|
ee01edf55c | ||
|
|
4e10e0f10e | ||
|
|
385f115eab | ||
|
|
b4062d3b53 | ||
|
|
7bfcb548e2 | ||
|
|
a74e4b042d | ||
|
|
2c5308aa20 | ||
|
|
9d375d4e7b | ||
|
|
df4390688b | ||
|
|
3acfd1f543 | ||
|
|
49c11c1872 | ||
|
|
93cf297371 | ||
|
|
8baa413a23 |
@@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 0.7.4
|
||||
current_version = 0.8.2
|
||||
commit = True
|
||||
tag = True
|
||||
|
||||
@@ -12,5 +12,5 @@ search = __version__ = '{current_version}'
|
||||
replace = __version__ = '{new_version}'
|
||||
|
||||
[bumpversion:file:Dockerfile]
|
||||
search = COPY dist/unifi-protect-backup-{current_version}.tar.gz sdist.tar.gz
|
||||
replace = COPY dist/unifi-protect-backup-{new_version}.tar.gz sdist.tar.gz
|
||||
search = COPY dist/unifi_protect_backup-{current_version}.tar.gz sdist.tar.gz
|
||||
replace = COPY dist/unifi_protect_backup-{new_version}.tar.gz sdist.tar.gz
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -52,7 +52,7 @@ jobs:
|
||||
|
||||
- name: show temporary files
|
||||
run: >-
|
||||
ls -l
|
||||
ls -lR
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
@@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.8.2] - 2022-12-05
|
||||
### Fixed
|
||||
- Fixed issue where command output was being returned with added indentation intended for logging only
|
||||
- Fixed issue where some command logging was not indented
|
||||
- Fixed issue where the tool could crash when run in a container if /config/database didn't exist
|
||||
|
||||
## [0.8.1] - 2022-12-04
|
||||
version 0.8.0 was used by accident previously and PyPI would not accept it so bumping by one patch version
|
||||
|
||||
## [0.8.0] - 2022-12-03
|
||||
Major internal refactoring. Each task is now its own class and asyncio task.
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ LABEL maintainer="ep1cman"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY dist/unifi-protect-backup-0.7.4.tar.gz sdist.tar.gz
|
||||
COPY dist/unifi_protect_backup-0.8.2.tar.gz sdist.tar.gz
|
||||
|
||||
RUN \
|
||||
echo "**** install build packages ****" && \
|
||||
@@ -45,8 +45,11 @@ ENV RCLONE_DESTINATION=local:/data
|
||||
ENV VERBOSITY="v"
|
||||
ENV TZ=UTC
|
||||
ENV IGNORE_CAMERAS=""
|
||||
ENV SQLITE_PATH=/config/database/events.sqlite
|
||||
|
||||
COPY docker_root/ /
|
||||
|
||||
RUN mkdir -p /config/database /config/rclone
|
||||
|
||||
VOLUME [ "/config" ]
|
||||
VOLUME [ "/data" ]
|
||||
|
||||
@@ -165,6 +165,9 @@ You can optionally format the `event.start`/`event.end` timestamps as per the [`
|
||||
You can run this tool as a container if you prefer with the following command.
|
||||
Remember to change the variable to make your setup.
|
||||
|
||||
> **Note**
|
||||
> As of version 0.8.0, the event database needs to be persisted for the tool to function properly
|
||||
> please see the updated commands below
|
||||
|
||||
### Backing up locally
|
||||
By default, if no rclone config is provided clips will be backed up to `/data`.
|
||||
@@ -176,6 +179,7 @@ docker run \
|
||||
-e UFP_ADDRESS='UNIFI_PROTECT_IP' \
|
||||
-e UFP_SSL_VERIFY='false' \
|
||||
-v '/path/to/save/clips':'/data' \
|
||||
-v '/path/to/save/database':/config/database/ \
|
||||
ghcr.io/ep1cman/unifi-protect-backup
|
||||
```
|
||||
|
||||
@@ -198,7 +202,8 @@ docker run \
|
||||
-e UFP_SSL_VERIFY='false' \
|
||||
-e RCLONE_DESTINATION='my_remote:/unifi_protect_backup' \
|
||||
-v '/path/to/save/clips':'/data' \
|
||||
-v `/path/to/rclone.conf':'/config/rclone/rclone.conf'
|
||||
-v `/path/to/rclone.conf':'/config/rclone/rclone.conf' \
|
||||
-v '/path/to/save/database':/config/database/ \
|
||||
ghcr.io/ep1cman/unifi-protect-backup
|
||||
```
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[tool]
|
||||
[tool.poetry]
|
||||
name = "unifi-protect-backup"
|
||||
version = "0.7.4"
|
||||
name = "unifi_protect_backup"
|
||||
version = "0.8.2"
|
||||
homepage = "https://github.com/ep1cman/unifi-protect-backup"
|
||||
description = "Python tool to backup unifi event clips in realtime."
|
||||
authors = ["sebastian.goscik <sebastian@goscik.com>"]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
__author__ = """sebastian.goscik"""
|
||||
__email__ = 'sebastian@goscik.com'
|
||||
__version__ = '0.7.4'
|
||||
__version__ = '0.8.2'
|
||||
|
||||
# from .unifi_protect_backup import UnifiProtectBackup
|
||||
from .downloader import VideoDownloader
|
||||
|
||||
@@ -10,30 +10,28 @@ from pyunifiprotect import ProtectApiClient
|
||||
from pyunifiprotect.data.nvr import Event
|
||||
from pyunifiprotect.data.types import EventType
|
||||
|
||||
from unifi_protect_backup.utils import SubprocessException, VideoQueue, get_camera_name, human_readable_size
|
||||
from unifi_protect_backup.utils import (
|
||||
SubprocessException,
|
||||
VideoQueue,
|
||||
get_camera_name,
|
||||
human_readable_size,
|
||||
run_command,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def get_video_length(video: bytes) -> float:
|
||||
"""Uses ffprobe to get the length of the video file passed in as a byte stream"""
|
||||
cmd = 'ffprobe -v quiet -show_streams -select_streams v:0 -of json -'
|
||||
proc = await asyncio.create_subprocess_shell(
|
||||
cmd,
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
returncode, stdout, stderr = await run_command(
|
||||
'ffprobe -v quiet -show_streams -select_streams v:0 -of json -', video
|
||||
)
|
||||
stdout, stderr = await proc.communicate(video)
|
||||
if proc.returncode == 0:
|
||||
logger.extra_debug(f"stdout:\n{stdout.decode()}") # type: ignore
|
||||
logger.extra_debug(f"stderr:\n{stderr.decode()}") # type: ignore
|
||||
|
||||
json_data = json.loads(stdout.decode())
|
||||
return float(json_data['streams'][0]['duration'])
|
||||
if returncode != 0:
|
||||
raise SubprocessException(stdout, stderr, returncode)
|
||||
|
||||
else:
|
||||
raise SubprocessException(stdout.decode(), stderr.decode(), proc.returncode)
|
||||
json_data = json.loads(stdout)
|
||||
return float(json_data['streams'][0]['duration'])
|
||||
|
||||
|
||||
class VideoDownloader:
|
||||
@@ -139,4 +137,3 @@ class VideoDownloader:
|
||||
logger.debug(msg)
|
||||
except SubprocessException as e:
|
||||
logger.warn(" `ffprobe` failed")
|
||||
logger.exception(e)
|
||||
|
||||
@@ -8,7 +8,7 @@ import aiosqlite
|
||||
from pyunifiprotect.data.nvr import Event
|
||||
from pyunifiprotect import ProtectApiClient
|
||||
|
||||
from unifi_protect_backup.utils import get_camera_name, SubprocessException, VideoQueue
|
||||
from unifi_protect_backup.utils import get_camera_name, SubprocessException, VideoQueue, run_command
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -74,19 +74,9 @@ class VideoUploader:
|
||||
Raises:
|
||||
RuntimeError: If rclone returns a non-zero exit code
|
||||
"""
|
||||
cmd = f'rclone rcat -vv {rclone_args} "{destination}"'
|
||||
proc = await asyncio.create_subprocess_shell(
|
||||
cmd,
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
stdout, stderr = await proc.communicate(video)
|
||||
if proc.returncode == 0:
|
||||
logger.extra_debug(f"stdout:\n{stdout.decode()}") # type: ignore
|
||||
logger.extra_debug(f"stderr:\n{stderr.decode()}") # type: ignore
|
||||
else:
|
||||
raise SubprocessException(stdout.decode(), stderr.decode(), proc.returncode)
|
||||
returncode, stdout, stderr = await run_command(f'rclone rcat -vv {rclone_args} "{destination}"', video)
|
||||
if returncode != 0:
|
||||
logger.warn(f" Failed to upload file: '{destination}'")
|
||||
|
||||
async def _update_database(self, event: Event, destination: str):
|
||||
"""
|
||||
|
||||
@@ -86,26 +86,29 @@ def parse_rclone_retention(retention: str) -> relativedelta:
|
||||
)
|
||||
|
||||
|
||||
async def run_command(cmd: str):
|
||||
async def run_command(cmd: str, data=None):
|
||||
"""
|
||||
Runs the given command returning the exit code, stdout and stderr
|
||||
"""
|
||||
proc = await asyncio.create_subprocess_shell(
|
||||
cmd,
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
stdout, stderr = await proc.communicate()
|
||||
stdout = stdout.decode().replace('\n', '\n\t').strip()
|
||||
stderr = stderr.decode().replace('\n', '\n\t').strip()
|
||||
stdout, stderr = await proc.communicate(data)
|
||||
stdout = stdout.decode()
|
||||
stdout_indented = '\t' + stdout.replace('\n', '\n\t').strip()
|
||||
stderr = stderr.decode()
|
||||
stderr_indented = '\t' + stderr.replace('\n', '\n\t').strip()
|
||||
|
||||
if proc.returncode != 0:
|
||||
logger.warn(f"Failed to run: '{cmd}")
|
||||
logger.warn(f"stdout:\n{stdout}")
|
||||
logger.warn(f"stderr:\n{stderr}")
|
||||
logger.warn(f"stdout:\n{stdout_indented}")
|
||||
logger.warn(f"stderr:\n{stderr_indented}")
|
||||
else:
|
||||
logger.extra_debug(f"stdout:\n{stdout}")
|
||||
logger.extra_debug(f"stderr:\n{stderr}")
|
||||
logger.extra_debug(f"stdout:\n{stdout_indented}")
|
||||
logger.extra_debug(f"stderr:\n{stderr_indented}")
|
||||
|
||||
return proc.returncode, stdout, stderr
|
||||
|
||||
|
||||
Reference in New Issue
Block a user