mirror of
https://github.com/seriousm4x/UpSnap.git
synced 2026-03-31 06:24:09 -04:00
implement backup/restore
This commit is contained in:
@@ -36,7 +36,7 @@ class WSConsumer(AsyncWebsocketConsumer):
|
||||
}
|
||||
)
|
||||
|
||||
async def disconnect(self, code):
|
||||
async def disconnect(self, _):
|
||||
await self.channel_layer.group_discard("wol", self.channel_name)
|
||||
await self.remove_visitor()
|
||||
await self.channel_layer.group_send(
|
||||
@@ -86,6 +86,11 @@ class WSConsumer(AsyncWebsocketConsumer):
|
||||
"type": "scan_network",
|
||||
"message": await self.scan_network()
|
||||
}))
|
||||
elif received["type"] == "backup":
|
||||
await self.send(text_data=json.dumps({
|
||||
"type": "backup",
|
||||
"message": await self.get_all_devices()
|
||||
}))
|
||||
|
||||
async def send_group(self, event):
|
||||
await self.send(json.dumps(event["message"]))
|
||||
@@ -170,31 +175,32 @@ class WSConsumer(AsyncWebsocketConsumer):
|
||||
p = Port.objects.get(number=port["number"])
|
||||
obj.port.remove(p)
|
||||
|
||||
if data["cron"]["enabled"]:
|
||||
cron_value = data["cron"]["value"].strip().split(" ")
|
||||
if not len(cron_value) == 5:
|
||||
return
|
||||
minute, hour, dom, month, dow = cron_value
|
||||
schedule, _ = CrontabSchedule.objects.get_or_create(
|
||||
minute=minute,
|
||||
hour=hour,
|
||||
day_of_week=dow,
|
||||
day_of_month=dom,
|
||||
month_of_year=month
|
||||
)
|
||||
PeriodicTask.objects.update_or_create(
|
||||
name=data["name"],
|
||||
defaults={
|
||||
"crontab": schedule,
|
||||
"task": "wol.tasks.scheduled_wake",
|
||||
"args": json.dumps([data["id"]]),
|
||||
"enabled": True
|
||||
}
|
||||
)
|
||||
else:
|
||||
for task in PeriodicTask.objects.filter(name=data["name"], task="wol.tasks.scheduled_wake"):
|
||||
task.enabled = False
|
||||
task.save()
|
||||
if data.get("cron"):
|
||||
if data["cron"]["enabled"]:
|
||||
cron_value = data["cron"]["value"].strip().split(" ")
|
||||
if not len(cron_value) == 5:
|
||||
return
|
||||
minute, hour, dom, month, dow = cron_value
|
||||
schedule, _ = CrontabSchedule.objects.get_or_create(
|
||||
minute=minute,
|
||||
hour=hour,
|
||||
day_of_week=dow,
|
||||
day_of_month=dom,
|
||||
month_of_year=month
|
||||
)
|
||||
PeriodicTask.objects.update_or_create(
|
||||
name=data["name"],
|
||||
defaults={
|
||||
"crontab": schedule,
|
||||
"task": "wol.tasks.scheduled_wake",
|
||||
"args": json.dumps([data["id"]]),
|
||||
"enabled": True
|
||||
}
|
||||
)
|
||||
else:
|
||||
for task in PeriodicTask.objects.filter(name=data["name"], task="wol.tasks.scheduled_wake"):
|
||||
task.enabled = False
|
||||
task.save()
|
||||
|
||||
@database_sync_to_async
|
||||
def update_port(self, data):
|
||||
|
||||
@@ -59,6 +59,15 @@
|
||||
btnScan.disabled = false;
|
||||
btnScanSpinner.classList.add("d-none");
|
||||
btnScanText.innerText = "Scan";
|
||||
} else if (currentMessage.type == "backup") {
|
||||
// download backup file
|
||||
const now = new Date();
|
||||
const fileName = `upsnap_backup_${now.toISOString()}.json`
|
||||
const a = document.createElement("a");
|
||||
const file = new Blob([JSON.stringify(currentMessage.message)], { type: "text/plain" });
|
||||
a.href = URL.createObjectURL(file);
|
||||
a.download = fileName;
|
||||
a.click();
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -182,7 +182,7 @@
|
||||
* /4 * * * (Wake every 4 hours)
|
||||
0 9 * * 1-5 (Wake from Mo-Fr at 9 a.m.)
|
||||
</pre>
|
||||
<p class="mb-0">Read more about <a href="https://linux.die.net/man/5/crontab" target="_blank">valid syntax</a> here or <a href="https://crontab.guru/" target="_blank">generate</a> it. Expressions starting with "@..." are not supported.</p>
|
||||
<p class="mb-0">Read more about <a href="https://linux.die.net/man/5/crontab" target="_blank">valid syntax here</a> or <a href="https://crontab.guru/" target="_blank">generate</a> it. Expressions starting with "@..." are not supported.</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -44,6 +44,34 @@
|
||||
const dev = settings.scan_network[i];
|
||||
updateDevice(dev);
|
||||
}
|
||||
|
||||
const restoreFromFile = (e) => {
|
||||
let file = e.target.files[0];
|
||||
let reader = new FileReader();
|
||||
reader.readAsText(file);
|
||||
reader.onload = e => {
|
||||
let data = JSON.parse(e.target.result);
|
||||
if (Array.isArray(data)) {
|
||||
// v2 file restore
|
||||
data.forEach((device) => {
|
||||
updateDevice(device)
|
||||
})
|
||||
} else {
|
||||
// v1 file restore
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
value["mac"] = key
|
||||
updateDevice(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function backupToFile() {
|
||||
store.sendMessage({
|
||||
type: "backup"
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<nav class="navbar navbar-expand-sm navbar-light bg-light">
|
||||
@@ -196,20 +224,34 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<label class="form-check-label" for="flexCheckDefault">
|
||||
Enable notifications
|
||||
</label>
|
||||
<input class="form-check-input" type="checkbox" value="" id="flexCheckDefault" bind:checked={settings.notifications}>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<label class="form-check-label" for="flexCheckDefault">
|
||||
Enable notifications
|
||||
</label>
|
||||
<input class="form-check-input" type="checkbox" value="" id="flexCheckDefault" bind:checked={settings.notifications}>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" form="settingsForm" class="btn btn-outline-success">Save</button>
|
||||
<div class="row mb-3">
|
||||
<div class="col-auto ms-auto">
|
||||
<button type="submit" form="settingsForm" class="btn btn-outline-success">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
<h5 class="fw-bold">Backup/Restore</h5>
|
||||
<div class="callout callout-info">
|
||||
<p class="mb-0">Backup file structure has changed in v2. You can still restore both versions with this file upload.</p>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="inputRestore" class="form-label">Restore from .json</label>
|
||||
<input id="inputRestore" type="file" class="form-control" accept=".json" on:change={(e) => restoreFromFile(e)}>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<button type="button" class="btn btn-secondary" on:click={backupToFile}>
|
||||
<i class="fa-solid fa-download me-2"></i>
|
||||
Export .json
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -242,6 +284,10 @@
|
||||
border-left-width: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
|
||||
&.callout-info {
|
||||
border-left-color: $info;
|
||||
}
|
||||
|
||||
&.callout-danger {
|
||||
border-left-color: $danger;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user