Add base code

This commit is contained in:
Sebastien Clement 2019-12-19 15:08:47 +01:00
parent e008c62c32
commit 6e609f387c
32 changed files with 2813 additions and 0 deletions

1
.gitignore vendored
View File

@ -102,3 +102,4 @@ dist
# TernJS port file
.tern-port
.vscode

View File

@ -0,0 +1,67 @@
# Community Hass.io Add-ons: Nextcloud Backup
[![Release][release-shield]][release] ![Project Stage][project-stage-shield] ![Project Maintenance][maintenance-shield]
[![Discord][discord-shield]][discord] [![Community Forum][forum-shield]][forum]
[![Buy me a coffee][buymeacoffee-shield]][buymeacoffee]
Hass.io snapshot backup to Nextcloud
## About
Easily backup you'r Hass.io snapshots to Nextcloud.
Auto backup can be configure with the web interface.
[Click here for the full documentation][docs]
{% if channel == "edge" %}
## WARNING! THIS IS AN EDGE VERSION!
This Hass.io Add-ons repository contains edge builds of add-ons. Edge builds
add-ons are based upon the latest development version.
- They may not work at all.
- They might stop working at any time.
- They could have a negative impact on your system.
This repository was created for:
- Anybody willing to test.
- Anybody interested in trying out upcoming add-ons or add-on features.
- Developers.
If you are more interested in stable releases of our add-ons:
<https://github.com/hassio-addons/repository>
{% endif %}
{% if channel == "beta" %}
## WARNING! THIS IS A BETA VERSION!
This Hass.io Add-ons repository contains beta releases of add-ons.
- They might stop working at any time.
- They could have a negative impact on your system.
This repository was created for:
- Anybody willing to test.
- Anybody interested in trying out upcoming add-ons or add-on features.
If you are more interested in stable releases of our add-ons:
<https://github.com/hassio-addons/repository>
{% endif %}
[buymeacoffee-shield]: https://www.buymeacoffee.com/assets/img/guidelines/download-assets-sm-2.svg
[buymeacoffee]: https://www.buymeacoffee.com/seb6596
[discord-shield]: https://img.shields.io/discord/478094546522079232.svg
[discord]: https://discord.me/hassioaddons
[docs]: {{ repo }}/blob/{{ version }}/README.md
[forum-shield]: https://img.shields.io/badge/community-forum-brightgreen.svg
[forum]: https://community.home-assistant.io/
[maintenance-shield]: https://img.shields.io/maintenance/yes/2019.svg
[project-stage-shield]: https://img.shields.io/badge/project%20stage-developpement-yellow.svg
[release-shield]: https://img.shields.io/badge/version-{{ version }}-blue.svg
[release]: {{ repo }}/tree/{{ version }}

View File

@ -0,0 +1,39 @@
ARG BUILD_FROM=hassioaddons/base:5.0.2
# hadolint ignore=DL3006
FROM ${BUILD_FROM}
# Copy root filesystem
COPY rootfs /
# Setup base
RUN apk add --no-cache \
nodejs \
npm
WORKDIR /opt/nextcloud_backup
RUN npm install
# Build arguments
ARG BUILD_ARCH
ARG BUILD_DATE
ARG BUILD_REF
ARG BUILD_VERSION
# Labels
LABEL \
io.hass.name="Nextcloud Backup" \
io.hass.description="Addon that backup your snapshot to a Nextcloud server" \
io.hass.arch="${BUILD_ARCH}" \
io.hass.type="addon" \
io.hass.version=${BUILD_VERSION} \
maintainer="Sebclem" \
org.label-schema.description="Addon that backup your snapshot to a Nextcloud server" \
org.label-schema.build-date=${BUILD_DATE} \
org.label-schema.name="Nextcloud Backup" \
org.label-schema.schema-version="1.0" \
org.label-schema.url="https://addons.community" \
org.label-schema.usage="https://github.com/hassio-addons/addon-example/tree/master/README.md" \
org.label-schema.vcs-ref=${BUILD_REF} \
org.label-schema.vcs-url="https://github.com/hassio-addons/addon-example" \
org.label-schema.vendor="Sebclem"

View File

@ -0,0 +1,11 @@
{
"squash": false,
"build_from": {
"aarch64": "hassioaddons/base-aarch64:5.0.2",
"amd64": "hassioaddons/base-amd64:5.0.2",
"armhf": "hassioaddons/base-armhf:5.0.2",
"armv7": "hassioaddons/base-armv7:5.0.2",
"i386": "hassioaddons/base-i386:5.0.2"
},
"args": {}
}

View File

@ -0,0 +1,37 @@
{
"name": "Nextcloud Backup",
"version": "dev",
"slug": "nextcloud_backup",
"description": "Addon that backup your snapshot to a Nextcloud server",
"url": "https://github.com/hassio-addons/addon-example",
"webui": "[PROTO:ssl]://[HOST]:[PORT:3000]/",
"ingress": true,
"ingress_port": 3000,
"startup": "application",
"arch": [
"aarch64",
"amd64",
"armhf",
"armv7",
"i386"
],
"boot": "auto",
"hassio_api": true,
"hassio_role": "backup",
"options": {
"ssl": false,
"certfile": "fullchain.pem",
"keyfile": "privkey.pem"
},
"schema": {
"log_level": "match(^(trace|debug|info|notice|warning|error|fatal)$)?",
"ssl": "bool",
"certfile": "str",
"keyfile": "str",
"leave_front_door_open": "bool?"
},
"ports": {
"3000/tcp": 3000,
"9226/tcp": 9226
}
}

BIN
nextcloud_backup/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
nextcloud_backup/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

View File

@ -0,0 +1 @@
/usr/bin/run.sh false root 0755 0755

View File

@ -0,0 +1,10 @@
#!/usr/bin/with-contenv bashio
# ==============================================================================
# Community Hass.io Add-ons: Example
# Runs example1 script
# ==============================================================================
bashio::log.info "Starting Node..."
exec /usr/bin/nextcloud_backup.sh

View File

@ -0,0 +1,20 @@
module.exports = {
"env": {
"browser": true,
"commonjs": true,
"es6": true,
"node": true
},
"extends": "eslint:recommended",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018
},
"rules": {
"no-unused-vars": "warn"
}
};

View File

@ -0,0 +1,69 @@
const createError = require('http-errors');
const express = require('express');
const path = require('path');
const cookieParser = require('cookie-parser');
const logger = require('morgan');
const indexRouter = require('./routes/index');
const apiRouter = require('./routes/api');
const app = express();
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
app.use(logger('dev', { skip: function(req, res) { return res.statusCode = 304 } }));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter);
app.use('/api', apiRouter);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
const statusTools = require('./tools/status');
statusTools.init();
console.log("Satus : \x1b[32mGo !\x1b[0m")
const hassioApiTools = require('./tools/hassioApiTools');
hassioApiTools.getSnapshots().then(
() => {
console.log("Hassio API : \x1b[32mGo !\x1b[0m")
}, (err) => {
console.log("Hassio API : \x1b[31;1mFAIL !\x1b[0m")
console.log("... " + err);
});
const WebdavTools = require('./tools/webdavTools');
const webdav = new WebdavTools().getInstance();
webdav.confIsValid().then(
() => {
console.log("Nextcloud connection : \x1b[32mGo !\x1b[0m")
}, (err) => {
console.log("Nextcloud connection : \x1b[31;1mFAIL !\x1b[0m")
console.log("... " + err);
}
)
module.exports = app;

View File

@ -0,0 +1 @@
{"status":"idle","last_upload":null,"next_upload":null,"message":null,"error_code":null}

View File

@ -0,0 +1,90 @@
#!/usr/bin/env node
/**
* Module dependencies.
*/
var app = require('../app');
var debug = require('debug')('nexcloud-backup:server');
var http = require('http');
/**
* Get port from environment and store in Express.
*/
var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
/**
* Create HTTP server.
*/
var server = http.createServer(app);
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val) {
var port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
}

View File

@ -0,0 +1 @@
{"hostname":"test","username":"test","password":"test"}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,23 @@
{
"name": "nexcloud-backup",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node --inspect=0.0.0.0:9226 ./bin/www "
},
"dependencies": {
"cookie-parser": "~1.4.4",
"debug": "~2.6.9",
"ejs": "~2.6.1",
"express": "~4.16.1",
"http-errors": "~1.6.3",
"moment": "^2.24.0",
"morgan": "~1.9.1",
"superagent": "^5.1.2",
"webdav": "^2.10.0"
},
"devDependencies": {
"eslint": "^6.7.2",
"eslint-config-google": "^0.14.0"
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,61 @@
const express = require('express');
const router = express.Router();
const moment = require('moment');
const statusTools = require('../tools/status');
const WebdavTools = require('../tools/webdavTools')
const webdav = new WebdavTools().getInstance();
const hassioApiTools = require('../tools/hassioApiTools');
const settingsTools = require('../tools/settingsTools');
router.get('/status', (req, res, next) => {
let status = statusTools.getStatus();
res.json(status);
});
router.get('/formated-local-snap', function(req, res, next) {
hassioApiTools.getSnapshots().then(
(snaps) => {
res.render('localSnaps', { snaps: snaps, moment: moment });
},
(err) => {
console.log(err);
res.status(500);
res.send('');
})
});
router.get('/formated-remote-manual', function(req, res, next) {
webdav.init(true, 'cloud.seb6596.ovh', 'admin', 'WPHRG-4jwCw-i8eqg-mtiao-Kmwrw').then(() => {
console.log('success');
}, (err) => {
console.log('failure');
console.log(err);
})
});
router.post('/nextcloud-settings', function(req, res, next){
console.log("ok");
let settings = req.body;
if(settings.host !== null && settings.host !== "" && settings.username !== null && settings.password !== null){
settingsTools.setSettings(settings);
res.status(201);
res.send();
}
else{
res.status(400);
res.send();
}
});
module.exports = router;

View File

@ -0,0 +1,11 @@
const express = require('express');
const router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index');
});
module.exports = router;

View File

@ -0,0 +1 @@
{"status":"Error","last_upload":null,"next_upload":null,"message":"Nextcloud config not found !","error_code":2}

View File

@ -0,0 +1,41 @@
const superagent = require('superagent');
const statusTools = require('./status');
// !!! FOR DEV PURPOSE ONLY !!!
//put token here for dev (ssh port tunelling 'sudo ssh -L 80:hassio:80 root@`hassoi_ip`' + put 127.0.0.1 hassio into host)
const fallbackToken = "75c7a712290f47a7513ee75a24072f2a5f44745d9b9c4e1f9fe6d44e55da2715e7c4341de239ec1c79a5f7178dd4376e27a98ebb7b4b029a"
function getSnapshots() {
return new Promise((resolve, reject) => {
let token = process.env.HASSIO_TOKEN;
if (token == null) {
token = fallbackToken
}
let status = statusTools.getStatus();
superagent.get('http://hassio/snapshots')
.set('X-HASSIO-KEY', token)
.then(data => {
if (status.error_code == 1) {
status.status = "idle";
status.message = null;
status.error_code = null;
statusTools.setStatus(status);
}
let snaps = data.body.data.snapshots;
// console.log(snaps);
resolve(snaps);
})
.catch(err => {
status.status = "error";
status.message = "Fail to fetch Hassio snapshot (" + err + ")";
status.error_code = 1;
statusTools.setStatus(status);
reject(err);
});
});
}
exports.getSnapshots = getSnapshots;

View File

@ -0,0 +1,26 @@
const fs = require('fs');
const settingsPath = "./config.json"
function getSettings(){
if (!fs.existsSync(settingsPath)) {
return {};
}
else{
let rawSettings = fs.readFileSync(settingsPath);
return JSON.parse(rawSettings);
}
}
function setSettings(settings){
fs.writeFileSync(settingsPath, JSON.stringify(settings));
}
exports.getSettings = getSettings;
exports.setSettings = setSettings;

View File

@ -0,0 +1,45 @@
const fs = require('fs');
const statusPath = './status.json'
let baseStatus = {
status: "idle",
last_upload: null,
next_upload: null
}
function init() {
if (!fs.existsSync(statusPath)) {
fs.writeFileSync(statusPath, JSON.stringify(baseStatus));
}
else{
let content = getStatus();
if(content.status !== "idle"){
content.status = "idle";
content.message = null;
setStatus(content)
}
}
}
function getStatus(){
if (!fs.existsSync(statusPath)) {
fs.writeFileSync(statusPath, JSON.stringify(baseStatus));
}
let content = fs.readFileSync(statusPath);
return JSON.parse(content);
}
function setStatus(state){
fs.writeFileSync(statusPath, JSON.stringify(state));
}
exports.init = init;
exports.getStatus = getStatus;
exports.setStatus = setStatus;

View File

@ -0,0 +1,104 @@
const { createClient } = require("webdav");
const fs = require("fs");
const statusTools = require('./status');
const endpoint = "/remote.php/webdav"
const configPath = "./webdav_conf.json"
class WebdavTools {
constructor() {
this.client = null;
}
init(ssl, host, username, password) {
return new Promise((resolve, reject) => {
console.log("Initilizing and checking webdav client...")
let url = (ssl ? "https" : "http") + "://" + host + endpoint;
try {
this.client = createClient(url, { username: username, password: password });
this.client.getDirectoryContents("/").then(() => {
if (status.error_code == 3) {
status.status = "idle";
status.message = null;
status.error_code = null;
statusTools.setStatus(status);
}
resolve();
}).catch((error) => {
status.status = "Error";
status.error_code = 3;
status.message = "Can't connect to Nextcloud (" + error + ") !"
statusTools.setStatus(status);
this.client = null;
reject("Can't connect to Nextcloud (" + error + ") !");
});
} catch (err) {
status.status = "Error";
status.error_code = 3;
status.message = "Can't connect to Nextcloud (" + err + ") !"
statusTools.setStatus(status);
this.client = null;
reject("Can't connect to Nextcloud (" + err + ") !");
}
});
}
confIsValid() {
return new Promise((resolve, reject) => {
let status = statusTools.getStatus();
let conf = this.loadConf();
if (conf !== null) {
if (conf.ssl !== null && conf.host !== null && conf !== null && conf !== null) {
if (status.error_code == 2) {
status.status = "idle";
status.message = null;
status.error_code = null;
statusTools.setStatus(status);
}
//TODO init connection
resolve();
}
else {
status.status = "Error";
status.error_code = 2;
status.message = "Nextcloud config invalid !"
statusTools.setStatus(status);
reject("Nextcloud config invalid !");
}
}
else {
status.status = "Error";
status.error_code = 2;
status.message = "Nextcloud config not found !"
statusTools.setStatus(status);
reject("Nextcloud config not found !");
}
});
}
loadConf() {
if (fs.existsSync(configPath)) {
let content = JSON.parse(fs.readFileSync(configPath));
return content;
}
else
return null;
}
}
class Singleton {
constructor() {
if (!Singleton.instance) {
Singleton.instance = new WebdavTools();
}
}
getInstance() {
return Singleton.instance;
}
}
module.exports = Singleton;

View File

@ -0,0 +1,3 @@
<h1><%= message %></h1>
<h2><%= error.status %></h2>
<pre><%= error.stack %></pre>

View File

@ -0,0 +1,284 @@
<!DOCTYPE html>
<html>
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Nextcloud Backup</title>
<!-- <link rel='stylesheet' href='/stylesheets/style.css' /> -->
<link rel="stylesheet" href="./css/materialize.min.css">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<style>
.modal input[disabled] {
color: #757575 !important;
border-color: #616161 !important;
}
.modal .input-field span.helper-text {
color: #9e9e9e;
}
.modal div.row:last-child {
margin-bottom: 0;
}
.modal div.col:last-child {
margin-bottom: 0;
}
.header-box {
height: 150px;
}
.header-box .col {
height: 100%;
}
.header-box .col .card {
height: 100%;
}
.header-box .card-content {
padding-top: 10px;
}
.header-box .card-content h5 {
margin-top: 10px;
}
ul.dropdown-content a:hover {
background-color: #101619 !important;
}
ul.dropdown-content li:hover {
background-color: #101619 !important;
}
/* change autocomplete color */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active {
-webkit-box-shadow: 0 0 0 30px #263238 inset !important;
-webkit-text-fill-color: white !important;
}
</style>
</head>
<body class="blue-grey darken-4">
<nav class=" light-blue accent-4">
<div class="nav-wrapper container">
<a href="#" class="brand-logo"><img src="https://cloud.seb6596.ovh/svg/core/logo/logo?color=fff&v=1" height="54"
style="margin: 5px"></a>
<ul class="right">
<li id="setting-trigger"><a class="dropdown-trigger" href="#" data-target="dropdown-settings"><i
class="material-icons">settings</i></a></li>
</ul>
<div style="height: 64px; display: table; margin-left: 130px;">
<h4 style="display: table-cell; vertical-align: middle;">Nextcloud Backup</h2>
</div>
</div>
</nav>
<ul id="dropdown-settings" class="dropdown-content blue-grey darken-4">
<li><a href="#modal-settings-nextcloud" class="modal-trigger center">Nextcloud</a></li>
<li><a href="#modal-settings-backup" class="modal-trigger center">Backup</a></li>
</ul>
<div class="container ">
<div class="row header-box">
<div class="col s12 m3">
<div class="card cyan darken-3">
<div class="card-content">
<span class="card-title white-text" style="font-weight: bold;">Status </span>
<div class="divider"></div>
<h5 id="status" class="white-text"></h5>
<div id="status-second-line" class="truncate tooltipped" data-position="bottom" data-tooltip=""></div>
</div>
</div>
</div>
<div class="col s12 m3">
<div class="card cyan darken-3">
<div class="card-content">
<span class="card-title white-text" style="font-weight: bold;">Last Backup</span>
<div class="divider"></div>
<h5 id="last_back_status"></h5>
</div>
</div>
</div>
<div class="col s12 m3">
<div class="card cyan darken-3 ">
<div class="card-content">
<span class="card-title white-text" style="font-weight: bold;">Next Backup </span>
<div class="divider"></div>
<h5 id="next_back_status"></h5>
</div>
</div>
</div>
<div class="col s12 m3">
<div class="card cyan darken-3">
<div class="card-content center">
<a class="btn green">Backup Now</a>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col s12 m12 l6 ">
<div class="card cyan darken-3">
<div class="card-content">
<span class="card-title center white-text">Local Snapshots</span>
<div id="local_snaps"></div>
</div>
</div>
</div>
</div>
</div>
<div id="modal-settings-nextcloud" class="modal modal-fixed-footer blue-grey darken-4 white-text">
<div class="modal-content">
<div class="row">
<div class="col s12 center">
<h4>Nextcloud Settings</h2>
</div>
</div>
<div class="row">
<div class="col s12 center divider">
</div>
</div>
<div class="row">
<div class="input-field col s12">
<input id="hostname" type="text" class="white-text">
<label for="hostname">Hostname</label>
<span class="helper-text">exemple.com:8080</span>
</div>
</div>
<div class="row">
<div class="input-field col s6">
<input id="username" type="text" class="white-text">
<label for="username">Username</label>
</div>
<div class="input-field col s6">
<input id="password" type="password" class="white-text">
<label for="password">Password</label>
</div>
</div>
</div>
<div class="modal-footer blue-grey darken-4">
<a href="#" class="modal-close waves-effect btn red"><b>Cancel</b></a>
<a href="#" class="btn green waves-effect" style="margin-left: 5px;" id="save-nextcloud-settings"><b>Save</b></a>
</div>
</div>
<div id="modal-loading" class="modal blue-grey darken-4 white-text">
<div class="modal-content">
</div>
</div>
</body>
<script src="./js/materialize.min.js"></script>
<script src="./js/jquery-3.4.1.min.js"></script>
<script src="./js/index.js"></script>
<script>
var last_status = "";
var loadingModal = null;
document.addEventListener('DOMContentLoaded', function() {
updateLocalSnaps();
update_status();
let tooltips = document.querySelectorAll('.tooltipped');
M.Tooltip.init(tooltips, {});
let drops = document.querySelectorAll('.dropdown-trigger');
M.Dropdown.init(drops, { constrainWidth: false, coverTrigger: false, alignment: 'right', onOpenStart: () => $('#setting-trigger').addClass('active'), onCloseEnd: () => $('#setting-trigger').removeClass('active') });
setInterval(update_status, 500);
listeners();
});
function updateLocalSnaps() {
$.get('./api/formated-local-snap', (data) => {
$('#local_snaps').empty();
$('#local_snaps').html(data);
var elems = document.querySelectorAll('.collapsible');
M.Collapsible.init(elems, { accordion: true });
var modals = document.querySelectorAll('.modal:not(#modal-loading)');
M.Modal.init(modals, {});
let loadingModals = document.querySelectorAll('#modal-loading');
M.Modal.init( loadingModals, {dismissible: false});
loadingModal = M.Modal.getInstance(document.querySelector('#modal-loading'));
debugger;
$('.local-snap-listener').click(function() {
let id = this.getAttribute('data-id');
console.log(id);
});
});
}
function update_status() {
$.get('./api/status', (data) => {
if (JSON.stringify(data) !== last_status) {
last_status = JSON.stringify(data);
switch (data.status) {
case "error":
printStatus('Error', data.message);
break;
case "idle":
printStatus('Idle', "Waiting for next backup.")
}
}
});
}
function printStatus(status, secondLine) {
$('#status').empty();
$('#status').html(status);
$('#status-second-line').empty();
$('#status-second-line').html(secondLine);
$('#status-second-line').attr('data-tooltip', secondLine)
}
function listeners() {
$('#save-nextcloud-settings').click(sendNextcloudSettings);
}
function sendNextcloudSettings() {
loadingModal.open();
let hostname = $('#hostname').val();
let username = $('#username').val();
let password = $('#password').val();
$.post('./api/nextcloud-settings', { hostname: hostname, username: username, password: password })
.done((data) => {
console.log('Saved');
}).fail((data) => {
console.log('Fail');
}).always(()=>{
loadingModal.close();
})
}
</script>
</html>

View File

@ -0,0 +1,61 @@
<% if (locals.snaps) { %>
<div class="collection">
<% for(const index in snaps) { %>
<a class="collection-item local-snap-listener modal-trigger" href="#modal-<%=snaps[index].slug%>"
data-id="<%= snaps[index].slug %>">
<div><%= snaps[index].name ? snaps[index].name : 'No name' %><div class="secondary-content">
<%= moment(snaps[index].date).format('lll') %></div>
</div>
</a>
<div id="modal-<%=snaps[index].slug%>" class="modal modal-fixed-footer blue-grey darken-4 white-text">
<div class="modal-content">
<div class="row">
<div class="col s12 center">
<h4>Snapshot Detail</h2>
</div>
</div>
<div class="row">
<div class="col s12 center divider">
</div>
</div>
<div class="row">
<div class="input-field col s12">
<input disabled type="text" id="name-<%=snaps[index].slug%>" value="<%= snaps[index].name ? snaps[index].name : 'No name' %>" />
<label for="name-<%=snaps[index].slug%>" class="white-text active">Name</label>
</div>
<div class="input-field col s12">
<input disabled type="text" id="date-<%=snaps[index].slug%>"
value="<%=moment(snaps[index].date).format('lll')%>" />
<label for="date-<%=snaps[index].slug%>" class="white-text active">Date</label>
</div>
<div class="input-field col s12">
<input disabled type="text" id="protected-<%=snaps[index].slug%>"
value="<%=snaps[index].protected%>" />
<label for="protected-<%=snaps[index].slug%>" class="white-text active">Protected</label>
</div>
<div class="input-field col s12">
<input disabled type="text" id="type-<%=snaps[index].slug%>" value="<%=snaps[index].type%>" />
<label for="type-<%=snaps[index].slug%>" class="white-text active">Type</label>
</div>
</div>
</div>
<div class="modal-footer blue-grey darken-4">
<a href="#!" class="waves-effect waves-green btn light-blue accent-4">Backup now</a>
<a href="#!" class="modal-close waves-effect waves-green btn red">Close</a>
</div>
</div>
<% } %>
</div>
<% } %>

View File

@ -0,0 +1,12 @@
#!/usr/bin/with-contenv bashio
# ==============================================================================
#
# Community Hass.io Add-ons: Example
#
# Example add-on for Hass.io.
# This add-on displays a random quote every X seconds.
#
# ==============================================================================
cd /opt/nextcloud_backup/
npm start

1
nextcloud_backup/run.sh Normal file
View File

@ -0,0 +1 @@