Compare commits
25 Commits
repo-reque
...
main
Author | SHA1 | Date | |
---|---|---|---|
a57097d504 | |||
2c7b0d2ced | |||
efb4719914 | |||
b03b4fadce | |||
cde6c1d288 | |||
53568e360f | |||
c81e018d97 | |||
71d911c660 | |||
2fb284a142 | |||
c79af4b675 | |||
8c8465df52 | |||
fba957a6bc | |||
c506d9fcfa | |||
b4a1d99ae1 | |||
02c10e0c78 | |||
938be8c808 | |||
29d5b51752 | |||
3031454682 | |||
aa4f31df23 | |||
1f5c2b9de6 | |||
b743b7919c | |||
9ec5b178ac | |||
1651b2f4ef | |||
b6f204a15e | |||
f16a0d5b65 |
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -35,6 +35,7 @@ borgweb/borgweb/secrets.py
|
||||||
|
|
||||||
# db
|
# db
|
||||||
*.sqlite
|
*.sqlite
|
||||||
|
*.db
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from .repoform import RepoForm
|
from .repoform import RepoForm, ToggleVisibility
|
||||||
from .archiveform import ArchiveForm
|
from .archiveform import ArchiveForm
|
||||||
from .errorform import ErrorForm
|
from .errorform import ErrorForm
|
||||||
from .locationform import LocationForm
|
from .locationform import LocationForm
|
||||||
|
|
|
@ -6,3 +6,7 @@ class RepoForm(forms.Form):
|
||||||
fingerprint = forms.CharField(label='Fingerprint')
|
fingerprint = forms.CharField(label='Fingerprint')
|
||||||
location = forms.CharField(label='Location')
|
location = forms.CharField(label='Location')
|
||||||
last_modified = forms.DateTimeField(label='Last Modified', input_formats=["%Y-%m-%dT%H:%M:%S.%z"])
|
last_modified = forms.DateTimeField(label='Last Modified', input_formats=["%Y-%m-%dT%H:%M:%S.%z"])
|
||||||
|
|
||||||
|
|
||||||
|
class ToggleVisibility(forms.Form):
|
||||||
|
label = forms.CharField(label='Label')
|
||||||
|
|
18
borgweb/borg/migrations/0003_label_visible.py
Normal file
18
borgweb/borg/migrations/0003_label_visible.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 4.0.6 on 2022-07-26 15:24
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('borg', '0002_location'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='label',
|
||||||
|
name='visible',
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,63 @@
|
||||||
|
# Generated by Django 4.0.6 on 2022-10-05 18:27
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('borg', '0003_label_visible'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='archive',
|
||||||
|
name='compressed_size',
|
||||||
|
field=models.BigIntegerField(),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='archive',
|
||||||
|
name='deduplicated_size',
|
||||||
|
field=models.BigIntegerField(),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='archive',
|
||||||
|
name='file_count',
|
||||||
|
field=models.BigIntegerField(),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='archive',
|
||||||
|
name='original_size',
|
||||||
|
field=models.BigIntegerField(),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cache',
|
||||||
|
name='total_chunks',
|
||||||
|
field=models.BigIntegerField(),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cache',
|
||||||
|
name='total_csize',
|
||||||
|
field=models.BigIntegerField(),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cache',
|
||||||
|
name='total_size',
|
||||||
|
field=models.BigIntegerField(),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cache',
|
||||||
|
name='total_unique_chunks',
|
||||||
|
field=models.BigIntegerField(),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cache',
|
||||||
|
name='unique_csize',
|
||||||
|
field=models.BigIntegerField(),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cache',
|
||||||
|
name='unique_size',
|
||||||
|
field=models.BigIntegerField(),
|
||||||
|
),
|
||||||
|
]
|
|
@ -8,9 +8,9 @@ class Archive(models.Model):
|
||||||
name = models.TextField()
|
name = models.TextField()
|
||||||
start = models.DateTimeField()
|
start = models.DateTimeField()
|
||||||
end = models.DateTimeField()
|
end = models.DateTimeField()
|
||||||
file_count = models.IntegerField()
|
file_count = models.BigIntegerField()
|
||||||
original_size = models.IntegerField()
|
original_size = models.BigIntegerField()
|
||||||
compressed_size = models.IntegerField()
|
compressed_size = models.BigIntegerField()
|
||||||
deduplicated_size = models.IntegerField()
|
deduplicated_size = models.BigIntegerField()
|
||||||
cache = models.OneToOneField(Cache, on_delete=models.CASCADE)
|
cache = models.OneToOneField(Cache, on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
|
|
@ -2,9 +2,9 @@ from django.db import models
|
||||||
|
|
||||||
|
|
||||||
class Cache(models.Model):
|
class Cache(models.Model):
|
||||||
total_chunks = models.IntegerField()
|
total_chunks = models.BigIntegerField()
|
||||||
total_csize = models.IntegerField()
|
total_csize = models.BigIntegerField()
|
||||||
total_size = models.IntegerField()
|
total_size = models.BigIntegerField()
|
||||||
total_unique_chunks = models.IntegerField()
|
total_unique_chunks = models.BigIntegerField()
|
||||||
unique_csize = models.IntegerField()
|
unique_csize = models.BigIntegerField()
|
||||||
unique_size = models.IntegerField()
|
unique_size = models.BigIntegerField()
|
||||||
|
|
|
@ -3,6 +3,7 @@ from django.db import models
|
||||||
|
|
||||||
class Label(models.Model):
|
class Label(models.Model):
|
||||||
label = models.TextField(blank=True, unique=True)
|
label = models.TextField(blank=True, unique=True)
|
||||||
|
visible = models.BooleanField(default=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.label
|
return self.label
|
||||||
|
|
|
@ -11,10 +11,10 @@ class Repo(models.Model):
|
||||||
last_modified = models.DateTimeField()
|
last_modified = models.DateTimeField()
|
||||||
label = models.OneToOneField(Label, on_delete=models.CASCADE, unique=True)
|
label = models.OneToOneField(Label, on_delete=models.CASCADE, unique=True)
|
||||||
|
|
||||||
def warning(self, hours_ago=2):
|
def warning(self, hours_ago=4):
|
||||||
return not self.latest_archive().start > datetime.utcnow() - timedelta(hours=hours_ago)
|
return not self.latest_archive().start > datetime.utcnow() - timedelta(hours=hours_ago)
|
||||||
|
|
||||||
def error(self, hours_ago=4):
|
def error(self, hours_ago=12):
|
||||||
latest_archive = self.latest_archive()
|
latest_archive = self.latest_archive()
|
||||||
if latest_archive is None or not self.archive_after_latest_error():
|
if latest_archive is None or not self.archive_after_latest_error():
|
||||||
return True
|
return True
|
||||||
|
@ -102,23 +102,23 @@ class Repo(models.Model):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def series_csize(archives, units=None):
|
def series_csize(archives, units=None):
|
||||||
return [convert_bytes(archive.cache.unique_csize, units)[0]
|
return [convert_bytes(archive.cache.unique_csize, units)[0]
|
||||||
if archive is not None else None for archive in archives]
|
if archive is not None else 0 for archive in archives]
|
||||||
|
|
||||||
def hourly_archive_string(self):
|
def hourly_archive_string(self):
|
||||||
return ''.join(['H' if archive is not None else '-' for archive in self.hourly_archives(8)])
|
return ''.join(['H' if archive is not None else '-' for archive in self.hourly_archives(8)])
|
||||||
|
|
||||||
def monthly_archives(self, n_months: int = 12):
|
def monthly_archives(self, n_months: int = 12):
|
||||||
archives = []
|
archives = []
|
||||||
for month in range(n_months):
|
archive_set = self.archive_set.all().order_by('-start')
|
||||||
current_date = subtract_months(datetime.utcnow(), month)
|
|
||||||
archive_current_month = self.archive_set.all() \
|
current_date = datetime.utcnow()
|
||||||
.filter(start__year=current_date.year,
|
for archive in archive_set:
|
||||||
start__month=current_date.month) \
|
if len(archives) >= n_months:
|
||||||
.order_by('-start')
|
break
|
||||||
if len(archive_current_month) > 0:
|
if archive.start.year == current_date.year and archive.start.month == current_date.month:
|
||||||
archives.append(archive_current_month[0])
|
archives.append(archive)
|
||||||
else:
|
current_date = subtract_months(current_date, 1)
|
||||||
archives.append(None)
|
|
||||||
return archives[::-1]
|
return archives[::-1]
|
||||||
|
|
||||||
def archives_on_dates(self, dates: list):
|
def archives_on_dates(self, dates: list):
|
||||||
|
@ -136,9 +136,9 @@ class Repo(models.Model):
|
||||||
archives = []
|
archives = []
|
||||||
for hour in range(n_hours):
|
for hour in range(n_hours):
|
||||||
current_hour = datetime.utcnow() - timedelta(hours=hour)
|
current_hour = datetime.utcnow() - timedelta(hours=hour)
|
||||||
archives_hour = self.archive_set.all()\
|
archives_hour = self.archive_set.all() \
|
||||||
.filter(start__date=current_hour.date())\
|
.filter(start__date=current_hour.date()) \
|
||||||
.filter(start__hour=current_hour.hour)\
|
.filter(start__hour=current_hour.hour) \
|
||||||
.order_by('-start')
|
.order_by('-start')
|
||||||
if len(archives_hour) > 0:
|
if len(archives_hour) > 0:
|
||||||
archives.append(archives_hour[0])
|
archives.append(archives_hour[0])
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
function draw_time_series_graph(chartID, repo, dateLabels, sizeUnits) {
|
function draw_time_series_graph(canvas, data) {
|
||||||
let datasets = [{
|
let datasets = [{
|
||||||
label: repo.label,
|
label: data.label,
|
||||||
data: repo.size,
|
data: data.size,
|
||||||
fill: false,
|
fill: false,
|
||||||
borderColor: 'rgb(7, 59, 76)'
|
borderColor: 'rgb(7, 59, 76)'
|
||||||
}]
|
}]
|
||||||
|
|
||||||
const data = {
|
const graphData = {
|
||||||
labels: dateLabels,
|
labels: data.dates,
|
||||||
datasets: datasets
|
datasets: datasets
|
||||||
};
|
};
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data,
|
data: graphData,
|
||||||
options: {
|
options: {
|
||||||
plugins: {
|
plugins: {
|
||||||
tooltip: {
|
tooltip: {
|
||||||
|
@ -21,13 +21,16 @@ function draw_time_series_graph(chartID, repo, dateLabels, sizeUnits) {
|
||||||
label: function (context) {
|
label: function (context) {
|
||||||
const yValue = context.parsed.y
|
const yValue = context.parsed.y
|
||||||
if (yValue !== null) {
|
if (yValue !== null) {
|
||||||
return `${yValue} ${sizeUnits}`
|
return `${yValue} ${data.units}`
|
||||||
} else {
|
} else {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
y: {
|
y: {
|
||||||
|
@ -41,7 +44,7 @@ function draw_time_series_graph(chartID, repo, dateLabels, sizeUnits) {
|
||||||
},
|
},
|
||||||
ticks: {
|
ticks: {
|
||||||
callback: function (value, index, values) {
|
callback: function (value, index, values) {
|
||||||
return `${value} ${sizeUnits}`
|
return `${value} ${data.units}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,69 +52,8 @@ function draw_time_series_graph(chartID, repo, dateLabels, sizeUnits) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var newChart = new Chart(
|
const newGraph = new Chart(
|
||||||
document.getElementById(chartID),
|
canvas,
|
||||||
config
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function draw_time_graph(chartID, repos, dateLabels, sizeUnits) {
|
|
||||||
let datasets = []
|
|
||||||
repos.forEach(function (repo) {
|
|
||||||
datasets.push({
|
|
||||||
label: repo.label,
|
|
||||||
data: repo.size,
|
|
||||||
fill: false,
|
|
||||||
borderColor: 'rgb(7, 59, 76)'
|
|
||||||
});
|
|
||||||
})
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
labels: dateLabels,
|
|
||||||
datasets: datasets
|
|
||||||
};
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
type: 'line',
|
|
||||||
data,
|
|
||||||
options: {
|
|
||||||
plugins: {
|
|
||||||
tooltip: {
|
|
||||||
callbacks: {
|
|
||||||
label: function (context) {
|
|
||||||
const yValue = context.parsed.y
|
|
||||||
if (yValue !== null) {
|
|
||||||
return `${yValue} ${sizeUnits}`
|
|
||||||
} else {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
y: {
|
|
||||||
min: 0,
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: "Compressed Size",
|
|
||||||
font: {
|
|
||||||
size: 18
|
|
||||||
}
|
|
||||||
},
|
|
||||||
ticks: {
|
|
||||||
callback: function (value, index, values) {
|
|
||||||
return `${value} ${sizeUnits}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var newChart = new Chart(
|
|
||||||
document.getElementById(chartID),
|
|
||||||
config
|
config
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,29 +12,37 @@ function colourRepo(repo_json, label, container_id) {
|
||||||
$(container_id).find(repoLabel).addClass(bg_class);
|
$(container_id).find(repoLabel).addClass(bg_class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stringRequests() {
|
||||||
window.addEventListener("DOMContentLoaded", function () {
|
|
||||||
// todo: inflate each repo and colour background accordingly
|
|
||||||
const container = $('#repo-container');
|
|
||||||
|
|
||||||
$('[data-json-string-request]').each(function (index, element) {
|
$('[data-json-string-request]').each(function (index, element) {
|
||||||
$.getJSON($(this).attr("data-json-string-request"), function (data) {
|
$.getJSON($(this).attr("data-json-string-request"), function (data) {
|
||||||
$(element).html(data['data']);
|
$(element).html(data['data']);
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
$.getJSON(`/repo-list.json`, function (repo_list) {
|
|
||||||
repo_list.labels.forEach(function (repo_label) {
|
|
||||||
$.getJSON(`/repo/${repo_label}.json`, function (repo_json) {
|
|
||||||
colourRepo(repo_json, repo_label, container);
|
|
||||||
})
|
|
||||||
|
|
||||||
$.getJSON(`/repo/${repo_label}/monthly-size.json`, function (repo_size_json) {
|
|
||||||
draw_time_series_graph(`repo-${repo_label}-size-graph`, repo_size_json.repo,
|
|
||||||
repo_size_json.dates, repo_size_json.units);
|
|
||||||
})
|
|
||||||
|
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function graphRequests() {
|
||||||
|
$('[data-json-graph-request]').each(function (index, element) {
|
||||||
|
$.getJSON($(this).attr("data-json-graph-request"), function (data) {
|
||||||
|
let newGraph = $('<canvas/>').width(400).height(200);
|
||||||
|
draw_time_series_graph(newGraph, data)
|
||||||
|
$(element).html(newGraph);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function colourRepos(repo_list) {
|
||||||
|
const container = $('#repo-container');
|
||||||
|
repo_list.labels.forEach(function (repo_label) {
|
||||||
|
$.getJSON(`/repo/${repo_label}.json`, function (repo_json) {
|
||||||
|
colourRepo(repo_json, repo_label, container);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("DOMContentLoaded", function () {
|
||||||
|
setTimeout(stringRequests, 0);
|
||||||
|
setTimeout(graphRequests, 0);
|
||||||
|
$.getJSON(`/repo-list.json`, function (repo_list) {
|
||||||
|
setTimeout(colourRepos.bind(null, repo_list), 0);
|
||||||
|
});
|
||||||
}, false);
|
}, false);
|
||||||
|
|
35
borgweb/borg/templates/borg/errors.html
Normal file
35
borgweb/borg/templates/borg/errors.html
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
{% extends "borg/base.html" %}
|
||||||
|
{% load cache %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}{{ repo.label }} errors{% endblock %}
|
||||||
|
{% block script %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block style %}
|
||||||
|
{{ block.super }}
|
||||||
|
.error-container {
|
||||||
|
padding: 8px;
|
||||||
|
margin: 8px;
|
||||||
|
}
|
||||||
|
{% endblock %}
|
||||||
|
{% block body %}
|
||||||
|
{% if errors %}
|
||||||
|
<div class="error-container" class="grid justify-content-left">
|
||||||
|
<ul class="att-label row ps-3 col-11">
|
||||||
|
{% for error in errors %}
|
||||||
|
<li class="shadow rounded overflow-hidden bg-primary m-1">
|
||||||
|
<span>{{ error.error }}</span>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<div style="width: 600px;" class="error-container shadow rounded bg-primary overflow-hidden">
|
||||||
|
<div style="width: 600px;" class="error-container bg-primary overflow-hidden">
|
||||||
|
<h2 class="h2">No errors found</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
{% block script %}
|
{% block script %}
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.4.1/dist/chart.min.js"
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.4.1/dist/chart.min.js"
|
||||||
integrity="sha256-GMN9UIJeUeOsn/Uq4xDheGItEeSpI5Hcfp/63GclDZk=" crossorigin="anonymous"></script>
|
integrity="sha256-GMN9UIJeUeOsn/Uq4xDheGItEeSpI5Hcfp/63GclDZk=" crossorigin="anonymous"></script>
|
||||||
{% include "borg/repo-template.html" %}
|
|
||||||
<script src="{% static 'borg/js/chart.js' %}"></script>
|
<script src="{% static 'borg/js/chart.js' %}"></script>
|
||||||
<script src="{% static 'borg/js/index.js' %}"></script>
|
<script src="{% static 'borg/js/index.js' %}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -38,7 +37,7 @@
|
||||||
<dt class="col-4">Latest backup:</dt>
|
<dt class="col-4">Latest backup:</dt>
|
||||||
<dd class="repo-latest-backup col-8"
|
<dd class="repo-latest-backup col-8"
|
||||||
data-json-string-request="/repo/{{ repo.label }}/latest-backup.json">
|
data-json-string-request="/repo/{{ repo.label }}/latest-backup.json">
|
||||||
<div class="spinner-border" role="status">
|
<div class="spinner-border spinner-border-sm" role="status">
|
||||||
</div>
|
</div>
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
@ -46,7 +45,7 @@
|
||||||
<dt class="col-4">Size:</dt>
|
<dt class="col-4">Size:</dt>
|
||||||
<dd class="repo-size col-8"
|
<dd class="repo-size col-8"
|
||||||
data-json-string-request="/repo/{{ repo.label }}/size.json">
|
data-json-string-request="/repo/{{ repo.label }}/size.json">
|
||||||
<div class="spinner-border" role="status">
|
<div class="spinner-border spinner-border-sm" role="status">
|
||||||
</div>
|
</div>
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
@ -54,12 +53,16 @@
|
||||||
<dt class="col-4">Recent errors:</dt>
|
<dt class="col-4">Recent errors:</dt>
|
||||||
<dd class="repo-recent-errors col-8"
|
<dd class="repo-recent-errors col-8"
|
||||||
data-json-string-request="/repo/{{ repo.label }}/recent-errors.json">
|
data-json-string-request="/repo/{{ repo.label }}/recent-errors.json">
|
||||||
<div class="spinner-border" role="status">
|
<div class="spinner-border spinner-border-sm" role="status">
|
||||||
</div>
|
</div>
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
<canvas id="repo-{{ repo.label }}-size-graph" width="400" height="200"></canvas>
|
<div id="repo-{{ repo.label }}-size-graph"
|
||||||
</dl>
|
class="d-flex justify-content-center"
|
||||||
|
data-json-graph-request="/repo/{{ repo.label }}/monthly-size.json">
|
||||||
|
<div class="spinner-border" role="status">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
14
borgweb/borg/templates/borg/post/toggle.html
Normal file
14
borgweb/borg/templates/borg/post/toggle.html
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Toggle repo visibility</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<form action="toggle" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form }}
|
||||||
|
<input type="submit" value="Submit">
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,23 +0,0 @@
|
||||||
<script id="repo-template" type="text/x-custom-template">
|
|
||||||
<div style="width: 600px;" class="repo-container shadow rounded overflow-hidden">
|
|
||||||
<div class="row me-1 overflow-hidden text-truncate">
|
|
||||||
<h2 class="h2"> <span class="repo-label"></span>
|
|
||||||
<small class="repo-location text-muted"></small>
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<dl class="att-label row ps-3">
|
|
||||||
<dt class="col-4">Latest backup:</dt>
|
|
||||||
<dd class="repo-latest-backup col-8"></dd>
|
|
||||||
</dl>
|
|
||||||
<dl class="att-label row ps-3">
|
|
||||||
<dt class="col-4">Size:</dt>
|
|
||||||
<dd class="repo-size col-8"></dd>
|
|
||||||
</dl>
|
|
||||||
<dl class="att-label row ps-3">
|
|
||||||
<dt class="col-4">Recent errors:</dt>
|
|
||||||
<dd class="repo-recent-errors col-8"></dd>
|
|
||||||
</dl>
|
|
||||||
<canvas class="repo-size-graph" width="400" height="200"></canvas>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</script>
|
|
|
@ -10,7 +10,7 @@ urlpatterns = [
|
||||||
path('repo-list.json', cache_page(60)(views.repo_list_json), name='repo list'),
|
path('repo-list.json', cache_page(60)(views.repo_list_json), name='repo list'),
|
||||||
|
|
||||||
# Repo
|
# Repo
|
||||||
path('repo/<str:repo_label>/monthly-size.json', cache_page(3600)(views.repo_monthly_size_json),
|
path('repo/<str:repo_label>/monthly-size.json', cache_page(60)(views.repo_monthly_size_json),
|
||||||
name='repo size time series'),
|
name='repo size time series'),
|
||||||
path('repo/<str:repo_label>.json', cache_page(60)(views.repo_json), name='repo json'),
|
path('repo/<str:repo_label>.json', cache_page(60)(views.repo_json), name='repo json'),
|
||||||
path('repo/<str:repo_label>/latest-backup.json', cache_page(60)(views.repo_latest_backup_json), name='repo json'),
|
path('repo/<str:repo_label>/latest-backup.json', cache_page(60)(views.repo_latest_backup_json), name='repo json'),
|
||||||
|
@ -20,9 +20,11 @@ urlpatterns = [
|
||||||
|
|
||||||
# Repo page
|
# Repo page
|
||||||
path('repo/<str:repo_label>', cache_page(60)(views.repo), name='repo'),
|
path('repo/<str:repo_label>', cache_page(60)(views.repo), name='repo'),
|
||||||
|
path('repo/<str:repo_label>/errors', cache_page(60)(views.repo_errors), name='repo'),
|
||||||
|
|
||||||
# POST
|
# POST
|
||||||
path('post/repo', views.post_repo, name='post repo'),
|
path('post/repo', views.post_repo, name='post repo'),
|
||||||
|
path('post/toggle', views.toggle_visibility, name='toggle repo visibility'),
|
||||||
path('post/archive', views.post_archive, name='post archive'),
|
path('post/archive', views.post_archive, name='post archive'),
|
||||||
path('post/error', views.post_error, name='post error'),
|
path('post/error', views.post_error, name='post error'),
|
||||||
path('post/location', views.post_location, name='post location'),
|
path('post/location', views.post_location, name='post location'),
|
||||||
|
|
|
@ -35,14 +35,10 @@ def repo_monthly_size_json(request, repo_label, months_ago: int = 12):
|
||||||
|
|
||||||
max_unit = get_units([repo])
|
max_unit = get_units([repo])
|
||||||
|
|
||||||
repo_dict = {"id": repo.id,
|
|
||||||
"label": repo.label.label,
|
|
||||||
"size": repo.size_on_months(max_unit, months_ago)}
|
|
||||||
|
|
||||||
response_dict = {
|
response_dict = {
|
||||||
"dates": date_labels,
|
"dates": date_labels,
|
||||||
"repo": repo_dict,
|
"units": max_unit,
|
||||||
"units": max_unit
|
"size": repo.size_on_months(max_unit, months_ago)
|
||||||
}
|
}
|
||||||
|
|
||||||
return JsonResponse(response_dict)
|
return JsonResponse(response_dict)
|
||||||
|
|
|
@ -3,9 +3,34 @@ from django.http import HttpResponseRedirect
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.contrib.auth.decorators import permission_required
|
from django.contrib.auth.decorators import permission_required
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache as django_cache
|
||||||
from ..models import Repo, Label, Archive, Cache, Error, Location
|
from ..models import Repo, Label, Archive, Cache, Error, Location
|
||||||
from ..forms import RepoForm, ArchiveForm, ErrorForm, LocationForm
|
from ..forms import RepoForm, ArchiveForm, ErrorForm, LocationForm, ToggleVisibility
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__file__)
|
||||||
|
|
||||||
|
|
||||||
|
@permission_required("borg.change_repo")
|
||||||
|
def toggle_visibility(request):
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = ToggleVisibility(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
cdata = form.cleaned_data
|
||||||
|
|
||||||
|
label = get_object_or_404(Label, label=cdata['label'])
|
||||||
|
|
||||||
|
label.visible = not label.visible
|
||||||
|
|
||||||
|
label.save()
|
||||||
|
django_cache.clear()
|
||||||
|
|
||||||
|
return HttpResponseRedirect(reverse('index'))
|
||||||
|
else:
|
||||||
|
form = ToggleVisibility()
|
||||||
|
|
||||||
|
return render(request, 'borg/post/toggle.html', {'form': form})
|
||||||
|
|
||||||
|
|
||||||
@permission_required("borg.add_repo")
|
@permission_required("borg.add_repo")
|
||||||
|
@ -28,7 +53,7 @@ def post_repo(request):
|
||||||
'last_modified': cdata['last_modified'],
|
'last_modified': cdata['last_modified'],
|
||||||
'label': label})
|
'label': label})
|
||||||
repo.save()
|
repo.save()
|
||||||
cache.clear()
|
django_cache.clear()
|
||||||
|
|
||||||
return HttpResponseRedirect(reverse('index'))
|
return HttpResponseRedirect(reverse('index'))
|
||||||
else:
|
else:
|
||||||
|
@ -42,22 +67,25 @@ def post_archive(request):
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
form = ArchiveForm(request.POST)
|
form = ArchiveForm(request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
cdata = form.cleaned_data
|
try:
|
||||||
|
cdata = form.cleaned_data
|
||||||
|
|
||||||
repo = get_object_or_404(Repo, label__label=cdata['label'])
|
repo = get_object_or_404(Repo, label__label=cdata['label'])
|
||||||
|
|
||||||
cache_dict = {k: cdata[k] for k in ('total_chunks', 'total_csize', 'total_size',
|
cache_dict = {k: cdata[k] for k in ('total_chunks', 'total_csize', 'total_size',
|
||||||
'total_unique_chunks', 'unique_csize', 'unique_size')}
|
'total_unique_chunks', 'unique_csize', 'unique_size')}
|
||||||
|
|
||||||
cache = Cache(**cache_dict)
|
cache = Cache(**cache_dict)
|
||||||
cache.save()
|
cache.save()
|
||||||
|
|
||||||
archive_dict = {k: cdata[k] for k in ('fingerprint', 'name', 'start', 'end', 'file_count',
|
archive_dict = {k: cdata[k] for k in ('fingerprint', 'name', 'start', 'end', 'file_count',
|
||||||
'original_size', 'compressed_size', 'deduplicated_size')}
|
'original_size', 'compressed_size', 'deduplicated_size')}
|
||||||
|
|
||||||
archive = Archive(**archive_dict, repo=repo, cache=cache)
|
archive = Archive(**archive_dict, repo=repo, cache=cache)
|
||||||
archive.save()
|
archive.save()
|
||||||
cache.clear()
|
django_cache.clear()
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Archive post failed")
|
||||||
|
|
||||||
return HttpResponseRedirect(reverse('index'))
|
return HttpResponseRedirect(reverse('index'))
|
||||||
else:
|
else:
|
||||||
|
@ -76,7 +104,7 @@ def post_error(request):
|
||||||
|
|
||||||
error = Error(label=label, error=cdata['error'], time=cdata['time'])
|
error = Error(label=label, error=cdata['error'], time=cdata['time'])
|
||||||
error.save()
|
error.save()
|
||||||
cache.clear()
|
django_cache.clear()
|
||||||
|
|
||||||
return HttpResponseRedirect(reverse('index'))
|
return HttpResponseRedirect(reverse('index'))
|
||||||
else:
|
else:
|
||||||
|
@ -94,7 +122,7 @@ def post_location(request):
|
||||||
label, _ = Location.objects.get_or_create(label=cdata['label'],
|
label, _ = Location.objects.get_or_create(label=cdata['label'],
|
||||||
defaults={"path": cdata["path"]})
|
defaults={"path": cdata["path"]})
|
||||||
label.save()
|
label.save()
|
||||||
cache.clear()
|
django_cache.clear()
|
||||||
|
|
||||||
return HttpResponseRedirect(reverse('index'))
|
return HttpResponseRedirect(reverse('index'))
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
|
from django.contrib.auth.decorators import permission_required
|
||||||
from django.shortcuts import render, get_object_or_404
|
from django.shortcuts import render, get_object_or_404
|
||||||
from ..models import Repo
|
from ..models import Repo
|
||||||
|
|
||||||
|
|
||||||
def index(request):
|
def index(request):
|
||||||
repo_list = Repo.objects.all()
|
repo_list = Repo.objects.filter(label__visible=True)
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'repo_list': repo_list,
|
'repo_list': repo_list,
|
||||||
|
@ -16,5 +17,11 @@ def repo(request, repo_label: str):
|
||||||
return render(request, 'borg/repo.html', {'repo': s_repo})
|
return render(request, 'borg/repo.html', {'repo': s_repo})
|
||||||
|
|
||||||
|
|
||||||
|
@permission_required("borg.view_error")
|
||||||
|
def repo_errors(request, repo_label: str):
|
||||||
|
s_repo = get_object_or_404(Repo, label__label=repo_label)
|
||||||
|
return render(request, 'borg/errors.html', {'errors': s_repo.label.errors.all().order_by('-time')})
|
||||||
|
|
||||||
|
|
||||||
def axes(request, credentials, *args, **kwargs):
|
def axes(request, credentials, *args, **kwargs):
|
||||||
return render(request, 'error/axes.html', {})
|
return render(request, 'error/axes.html', {})
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
from .secrets import SECRET_KEY
|
from .secrets import SECRET_KEY, DATABASE_PASSWORD
|
||||||
|
|
|
@ -2,6 +2,7 @@ import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from . import SECRET_KEY as __SECRET_KEY
|
from . import SECRET_KEY as __SECRET_KEY
|
||||||
|
from . import DATABASE_PASSWORD as __DATABASE_PASSWORD
|
||||||
|
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
@ -9,9 +10,11 @@ SECRET_KEY = __SECRET_KEY
|
||||||
|
|
||||||
DEBUG = False
|
DEBUG = False
|
||||||
|
|
||||||
|
AXES_META_PRECEDENCE_ORDER = ('HTTP_X_FORWARDED_FOR', 'X_FORWARDED_FOR', 'REMOTE_ADDR')
|
||||||
|
|
||||||
AXES_LOCKOUT_CALLABLE = "borg.views.axes"
|
AXES_LOCKOUT_CALLABLE = "borg.views.axes"
|
||||||
|
|
||||||
ALLOWED_HOSTS = ['127.0.0.1', 'borg.george.ooo', 'george.ooo', 'www.george.ooo']
|
ALLOWED_HOSTS = ['127.0.0.1', 'borg.george.ooo', 'george.ooo', 'www.george.ooo', '10.10.10.100', 'proxy.george.ooo']
|
||||||
|
|
||||||
AUTHENTICATION_BACKENDS = [
|
AUTHENTICATION_BACKENDS = [
|
||||||
'axes.backends.AxesBackend',
|
'axes.backends.AxesBackend',
|
||||||
|
@ -65,8 +68,12 @@ WSGI_APPLICATION = 'borgweb.wsgi.application'
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
'ENGINE': 'django.db.backends.postgresql_psycopg2',
|
||||||
'NAME': "/home/web/sites/db/borg.db",
|
'NAME': 'borgweb',
|
||||||
|
'USER': 'borgweb',
|
||||||
|
'PASSWORD': __DATABASE_PASSWORD,
|
||||||
|
'HOST': 'db.george.ooo',
|
||||||
|
'PORT': '5432',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,7 +127,7 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
CACHES = {
|
CACHES = {
|
||||||
"default": {
|
"default": {
|
||||||
"BACKEND": "django_redis.cache.RedisCache",
|
"BACKEND": "django_redis.cache.RedisCache",
|
||||||
"LOCATION": "redis://127.0.0.1:6379/1",
|
"LOCATION": "redis://cache.george.ooo:6379/1",
|
||||||
"OPTIONS": {
|
"OPTIONS": {
|
||||||
"CLIENT_CLASS": "django_redis.client.DefaultClient"
|
"CLIENT_CLASS": "django_redis.client.DefaultClient"
|
||||||
},
|
},
|
||||||
|
@ -128,6 +135,36 @@ CACHES = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LOGGING = {
|
||||||
|
"version": 1,
|
||||||
|
"disable_existing_loggers": False,
|
||||||
|
"root": {"level": "INFO", "handlers": ["file"]},
|
||||||
|
"handlers": {
|
||||||
|
"file": {
|
||||||
|
"level": "INFO",
|
||||||
|
"class": "logging.FileHandler",
|
||||||
|
"filename": "/var/log/django/borgweb.log",
|
||||||
|
"formatter": "app",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"loggers": {
|
||||||
|
"django": {
|
||||||
|
"handlers": ["file"],
|
||||||
|
"level": "INFO",
|
||||||
|
"propagate": True
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"formatters": {
|
||||||
|
"app": {
|
||||||
|
"format": (
|
||||||
|
u"%(asctime)s [%(levelname)-8s] "
|
||||||
|
"(%(module)s.%(funcName)s) %(message)s"
|
||||||
|
),
|
||||||
|
"datefmt": "%Y-%m-%d %H:%M:%S",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
# security
|
# security
|
||||||
SESSION_COOKIE_SECURE = True
|
SESSION_COOKIE_SECURE = True
|
||||||
SECURE_SSL_REDIRECT = True
|
SECURE_SSL_REDIRECT = True
|
||||||
|
|
|
@ -80,7 +80,7 @@ WSGI_APPLICATION = 'borgweb.wsgi.application'
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
'NAME': BASE_DIR / 'borg.sqlite',
|
'NAME': BASE_DIR / 'borg.db',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ print_action "Installing pip packages, this may take a while..."
|
||||||
|
|
||||||
# install required pip packages
|
# install required pip packages
|
||||||
yes | python -m pip install --upgrade wheel
|
yes | python -m pip install --upgrade wheel
|
||||||
yes | python -m pip install django gunicorn django-libsass django-compressor django-axes django-redis
|
yes | python -m pip install django gunicorn django-libsass django-compressor django-axes django-redis psycopg2-binary
|
||||||
|
|
||||||
print_action "Setting up static files and database"
|
print_action "Setting up static files and database"
|
||||||
|
|
||||||
|
|
9
borgweb/update.sh
Normal file
9
borgweb/update.sh
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
cd "${0%/*}"
|
||||||
|
|
||||||
|
source ./venv/bin/activate
|
||||||
|
python ./manage.py collectstatic --noinput
|
||||||
|
python ./manage.py compress
|
||||||
|
python ./manage.py migrate --noinput
|
||||||
|
deactivate
|
Loading…
Reference in New Issue
Block a user