From 93da0603da86097af0fd1f0a23d478b36b897b0d Mon Sep 17 00:00:00 2001 From: George Lacey Date: Sun, 9 May 2021 15:58:53 +0100 Subject: [PATCH] Show daily repo size changes as graph --- borgweb/borg/models/repo.py | 83 +++++++++++++++++++------- borgweb/borg/static/js/index.js | 47 +++++++++++++-- borgweb/borg/templates/borg/index.html | 7 ++- borgweb/borg/utility/data.py | 27 ++++++--- borgweb/borg/views.py | 21 ++++++- 5 files changed, 148 insertions(+), 37 deletions(-) diff --git a/borgweb/borg/models/repo.py b/borgweb/borg/models/repo.py index 94ad1f7..904a6e5 100644 --- a/borgweb/borg/models/repo.py +++ b/borgweb/borg/models/repo.py @@ -1,7 +1,7 @@ from django.db import models from datetime import datetime, timedelta from ..utility.time import seconds_to_string -from ..utility.data import bytes_to_string +from ..utility.data import bytes_to_string, convert_bytes from . import Label @@ -46,12 +46,15 @@ class Repo(models.Model): def size(self): if self.archive_set.all().exists(): cache = self.latest_archive().cache - return f"{bytes_to_string(cache.unique_csize)}" + return cache.unique_csize else: - return "No archives stored" + return 0 - def recent_errors(self): - days = 7 + def size_string(self): + size = self.size() + return bytes_to_string(size) + + def recent_errors(self, days: int = 7): days_ago = (datetime.utcnow() - timedelta(days=days)) errors = self.label.errors.all().filter(time__gt=days_ago) return errors @@ -71,21 +74,57 @@ class Repo(models.Model): days.append(len(cday_archives) > 0) return days - def get_archive_hours_dict(self): - if self.archive_set.all().exists(): - return {"id": self.id, - "label": self.label.label, - "hours": self.get_archive_hours()} - else: - return {"id": self.id, - "label": self.label.label, - "hours": []} + def daily_dict(self, units, n_hours: int = 24): + archives = self.daily_archives(n_hours) + return { + "id": self.id, + "label": self.label.label, + "daily_size": list(reversed(self.series_csize(archives, units))) + } - def get_archive_hours(self): - hours = [] - for hour in range(24): - chour = datetime.utcnow() - timedelta(hours=hour) - cday_archives = self.archive_set.all().filter(start__date=chour.date()).filter(start__hour=chour.hour) - hours.append(len(cday_archives) > 0) - hours = ''.join(['H' if hour is True else '-' for hour in hours]) - return hours + @staticmethod + def series_times(archives): + return [archive.start if archive is not None else None for archive in archives] + + @staticmethod + def series_csize(archives, units=None): + return [convert_bytes(archive.cache.unique_csize, units)[0] + if archive is not None else None for archive in archives] + + @staticmethod + def series_csize_pretty(archives): + return [archive.cache.unique_csize if archive is not None else None for archive in archives] + + @staticmethod + def series_success_string(archives): + return ''.join(['H' if archive is not None else '-' for archive in archives]) + + def hourly_archive_string(self): + return ''.join(['H' if archive is not None else '-' for archive in self.hourly_archives(24)]) + + def daily_archives(self, n_days: int = 24): + archives = [] + for day in range(n_days): + current_date = (datetime.utcnow() - timedelta(days=day)).date() + archive_current_date = self.archive_set.all()\ + .filter(start__date=current_date)\ + .order_by('-start') + if len(archive_current_date) > 0: + archives.append(archive_current_date[0]) + else: + archives.append(None) + return archives + + def hourly_archives(self, n_hours: int = 24): + archives = [] + for hour in range(n_hours): + current_hour = datetime.utcnow() - timedelta(hours=hour) + archives_hour = self.archive_set.all()\ + .filter(start__date=current_hour.date())\ + .filter(start__hour=current_hour.hour)\ + .order_by('-start') + if len(archives_hour) > 0: + archives.append(archives_hour[0]) + else: + archives.append(None) + return archives diff --git a/borgweb/borg/static/js/index.js b/borgweb/borg/static/js/index.js index a329642..a89abc7 100644 --- a/borgweb/borg/static/js/index.js +++ b/borgweb/borg/static/js/index.js @@ -1,7 +1,46 @@ window.addEventListener("DOMContentLoaded", function () { + const repoDict = JSON.parse(document.getElementById('hour_list').textContent); + set_daily_graph(repoDict) +}, false); - const hour_json = JSON.parse(document.getElementById('hour_list').textContent); - hour_json.forEach(function (repo) { - console.log(repo.hours); +function set_daily_graph(repoDict) { + const labels = repoDict.date_labels; + const y_units = repoDict.units + + var datasets = [] + repoDict.repos.forEach(function (repo) { + datasets.push({label: repo.label, + data: repo.daily_size, + fill: false, + tension: 0.1, + borderColor: 'rgb(75, 192, 192)'}); }) -}, false); \ No newline at end of file + + const data = { + labels: labels, + datasets: datasets + }; + + const config = { + type: 'line', + data, + options: { + scales: { + y: { + ticks: { + display: true, + grid: false, + callback: function(value, index, values) { + return value + " " + y_units; + } + } + } + } + } + }; + + var myChart = new Chart( + document.getElementById('backup_csize_hourly'), + config + ); +} \ No newline at end of file diff --git a/borgweb/borg/templates/borg/index.html b/borgweb/borg/templates/borg/index.html index 1f50022..7055a99 100644 --- a/borgweb/borg/templates/borg/index.html +++ b/borgweb/borg/templates/borg/index.html @@ -31,11 +31,11 @@
Hourly backups:
-
{{ repo.get_archive_hours }}
+
{{ repo.hourly_archive_string }}
Size:
-
{{ repo.size }}
+
{{ repo.size_string }}
{% if repo.recent_errors|length > 0 %}
@@ -53,6 +53,9 @@ {% else %}

No repos found.

{% endif %} +
+ +
\ No newline at end of file diff --git a/borgweb/borg/utility/data.py b/borgweb/borg/utility/data.py index 8862f58..e9e0802 100644 --- a/borgweb/borg/utility/data.py +++ b/borgweb/borg/utility/data.py @@ -1,11 +1,24 @@ from math import floor, log +bytes_in_unit = 1024 # Kibibyte +units = ("B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB", "HiB") +# bytes_in_unit = 1000 # Kilobyte +# units = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB", "HB") -def bytes_to_string(bytes: int): - suffixes = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB", "HB") - if bytes == 0: - return f"0{suffixes[0]}" + +def convert_bytes(c_bytes: int, unit: str = None) -> (float, str): + if c_bytes == 0: + return 0, units[0] if unit is None else unit + + if unit is None: + index = int(floor(log(c_bytes, bytes_in_unit))) else: - index = int(floor(log(bytes, 1024))) - s = round(bytes / pow(1024, index), 2) - return f"{s}{suffixes[index]}" + index = units.index(unit) + + result = round(c_bytes / pow(bytes_in_unit, index), 2) + return result, units[index] + + +def bytes_to_string(c_bytes: int, unit: str = None) -> str: + n_bytes, unit = convert_bytes(c_bytes, unit) + return f"{n_bytes} {unit}" diff --git a/borgweb/borg/views.py b/borgweb/borg/views.py index 18d0ad5..18dd5bb 100644 --- a/borgweb/borg/views.py +++ b/borgweb/borg/views.py @@ -5,19 +5,36 @@ from .models import Repo, Label, Archive, Cache, Error from django.urls import reverse from .forms import RepoForm, ArchiveForm, ErrorForm from django.contrib.auth.decorators import permission_required +from .utility import data +from datetime import datetime, timedelta def index(request): repo_list = Repo.objects.all() - hour_list = [repo.get_archive_hours_dict() for repo in repo_list] + repo_dict = repo_daily_dict(repo_list, 24) + context = { 'repo_list': repo_list, - 'hour_list': hour_list + 'hour_list': repo_dict } return render(request, 'borg/index.html', context) +def repo_daily_dict(repo_list, n_days=14): + date_labels = list(reversed([(datetime.utcnow() - timedelta(days=day)).strftime("%d %b") for day in range(n_days)])) + max_repo_size = max(repo.latest_archive().cache.unique_csize for repo in repo_list) + _, max_unit = data.convert_bytes(max_repo_size) + + repo_dicts = [repo.daily_dict(max_unit, n_days) for repo in repo_list] + + return { + "date_labels": date_labels, + "repos": repo_dicts, + "units": max_unit + } + + @permission_required("borg.add_repo") def get_repo(request): if request.method == 'POST':