├── borgweb ├── _tests │ └── __init__.py ├── static │ ├── i18n │ │ ├── fr.json │ │ ├── de-DE.json │ │ └── fr-FR.json │ ├── favicon.ico │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ ├── glyphicons-halflings-regular.woff2 │ │ └── glyphicons-halflings-regular.svg │ ├── style.css │ └── bootstrap │ │ └── bootstrap-theme.min.css ├── __init__.py ├── views │ ├── __init__.py │ ├── index.py │ ├── backup.py │ └── logs.py ├── wsgi.py ├── app.py ├── templates │ ├── error.html │ ├── _layout.html │ ├── _nav.html │ └── index.html └── config.py ├── js ├── static ├── templates ├── src │ ├── env.js │ ├── util.js │ ├── i18n.js │ ├── backup.js │ └── viewer.js ├── gulpfile.js ├── gulpfile.lib.js ├── package.json └── index.js ├── .gitattributes ├── docs ├── _static │ ├── logo.png │ ├── favicon.ico │ └── logo_font.txt ├── authors.rst ├── index.rst ├── borg_theme │ └── css │ │ └── borg.css ├── global.rst.inc ├── internals.rst ├── usage.rst ├── faq.rst ├── support.rst ├── installation.rst ├── Makefile └── conf.py ├── setup.cfg ├── .gitignore ├── AUTHORS ├── tox.ini ├── MANIFEST.in ├── logging.conf ├── README.rst ├── LICENSE ├── CHANGES └── setup.py /borgweb/_tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /js/static: -------------------------------------------------------------------------------- 1 | ../borgweb/static -------------------------------------------------------------------------------- /js/templates: -------------------------------------------------------------------------------- 1 | ../borgweb/templates -------------------------------------------------------------------------------- /borgweb/static/i18n/fr.json: -------------------------------------------------------------------------------- 1 | fr-FR.json -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | borgweb/_version.py export-subst 2 | -------------------------------------------------------------------------------- /borgweb/__init__.py: -------------------------------------------------------------------------------- 1 | from ._version import version as __version__ 2 | -------------------------------------------------------------------------------- /docs/_static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbackup/borgweb/HEAD/docs/_static/logo.png -------------------------------------------------------------------------------- /docs/_static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbackup/borgweb/HEAD/docs/_static/favicon.ico -------------------------------------------------------------------------------- /borgweb/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbackup/borgweb/HEAD/borgweb/static/favicon.ico -------------------------------------------------------------------------------- /borgweb/views/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | blueprint = Blueprint('borgweb', __name__) 4 | 5 | from . import index, logs, backup 6 | -------------------------------------------------------------------------------- /docs/_static/logo_font.txt: -------------------------------------------------------------------------------- 1 | Black Ops One 2 | James Grieshaber 3 | SIL Open Font License, 1.1 4 | 5 | https://www.google.com/fonts/specimen/Black+Ops+One 6 | -------------------------------------------------------------------------------- /borgweb/static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbackup/borgweb/HEAD/borgweb/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /borgweb/static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbackup/borgweb/HEAD/borgweb/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /borgweb/static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbackup/borgweb/HEAD/borgweb/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /borgweb/static/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgbackup/borgweb/HEAD/borgweb/static/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /docs/authors.rst: -------------------------------------------------------------------------------- 1 | .. include:: global.rst.inc 2 | 3 | .. include:: ../AUTHORS 4 | 5 | License 6 | ======= 7 | 8 | .. _license: 9 | 10 | .. include:: ../LICENSE 11 | :literal: 12 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E123,E126,E127,E129,E203,E221,E226,E231,E241,E265,E301,E302,E303,E713,F401,F403,W291,W293,W391 3 | max-line-length = 120 4 | exclude = docs/conf.py,borgweb/_version.py 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | MANIFEST 2 | docs/_build 3 | build 4 | dist 5 | env 6 | .tox 7 | *.egg-info 8 | *.pyc 9 | *.pyo 10 | *.so 11 | .idea/ 12 | js/node_modules 13 | logs 14 | repo 15 | borgweb/_version.py 16 | -------------------------------------------------------------------------------- /borgweb/wsgi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | DEBUG = False 4 | 5 | from .app import create_app 6 | 7 | application = create_app() 8 | 9 | if __name__ == '__main__': 10 | application.run(debug=DEBUG) 11 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | BorgWeb Developers / Contributors ("The Borg Collective") 2 | ````````````````````````````````````````````````````````` 3 | - Thomas Waldmann 4 | - Per Guth (http://perguth.de) 5 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{34,35,36} 3 | 4 | [testenv] 5 | # Change dir to avoid import problem 6 | changedir = {toxworkdir} 7 | deps = 8 | pytest 9 | commands = py.test --pyargs {posargs:borgweb._tests} 10 | passenv = * 11 | -------------------------------------------------------------------------------- /borgweb/views/index.py: -------------------------------------------------------------------------------- 1 | """ 2 | index / main view 3 | """ 4 | 5 | from flask import render_template 6 | 7 | from . import blueprint 8 | 9 | 10 | @blueprint.route('/') 11 | def index(): 12 | return render_template('index.html') 13 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # stuff we need to include into the sdist is handled automatically by 2 | # setuptools_scm - it includes all git-committed files. 3 | # but we want to exclude some committed files/dirs not needed in the sdist: 4 | exclude .gitattributes .gitignore 5 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: global.rst.inc 2 | 3 | 4 | BorgWeb Documentation 5 | ===================== 6 | 7 | .. include:: ../README.rst 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | 12 | installation 13 | usage 14 | faq 15 | support 16 | internals 17 | authors 18 | -------------------------------------------------------------------------------- /borgweb/static/i18n/de-DE.json: -------------------------------------------------------------------------------- 1 | { 2 | "Start Backup": "Backup starten", 3 | "Start a backup": "Ein Backup starten", 4 | "Documentation": "Dokumentation", 5 | "Open docs in new window": "Dokumentation in neuem Fenster öffnen", 6 | "See source code on Github": "Sourcecode auf Github anzeigen", 7 | "No logs found": "Keine Logs gefunden", 8 | "IRC Channel": "IRC-Kanal", 9 | "Mailinglist": "Mailingliste", 10 | "Beginning…": "Anfang…", 11 | "End of file": "Ende der Datei" 12 | } 13 | -------------------------------------------------------------------------------- /borgweb/static/i18n/fr-FR.json: -------------------------------------------------------------------------------- 1 | { 2 | "Start Backup": "Lancer la sauvegarde", 3 | "Start a backup": "Lancer une sauvegarde", 4 | "Documentation": "Documentation", 5 | "Open docs in new window": "Ouvrir la documentation dans une nouvelle fenêtre", 6 | "See source code on Github": "Voir le code source sur Github", 7 | "No logs found": "Aucun de fichier de logs trouvé", 8 | "IRC Channel": "Canal IRC", 9 | "Mailinglist": "List de diffusion", 10 | "Beginning…": "Début…", 11 | "End of file": "Fin de fichier" 12 | } 13 | -------------------------------------------------------------------------------- /docs/borg_theme/css/borg.css: -------------------------------------------------------------------------------- 1 | @import url("theme.css"); 2 | 3 | /* The Return of the Borg. 4 | * 5 | * Have a bit green and grey and darkness (and if only in the upper left corner). 6 | */ 7 | 8 | .wy-side-nav-search { 9 | background-color: #000000 !important; 10 | } 11 | 12 | .wy-side-nav-search > a { 13 | color: rgba(255, 255, 255, 0.5); 14 | } 15 | 16 | .wy-side-nav-search > div.version { 17 | color: rgba(255, 255, 255, 0.5); 18 | } 19 | 20 | #usage dt code { 21 | font-weight: normal; 22 | } 23 | -------------------------------------------------------------------------------- /logging.conf: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | # Default loglevel, to adjust verbosity: DEBUG, INFO, WARNING, ERROR, CRITICAL 3 | loglevel=INFO 4 | 5 | [loggers] 6 | keys=root 7 | 8 | [handlers] 9 | keys=stderr 10 | 11 | [formatters] 12 | keys=logfile 13 | 14 | [logger_root] 15 | level=%(loglevel)s 16 | handlers=stderr 17 | 18 | [handler_stderr] 19 | class=StreamHandler 20 | formatter=logfile 21 | level=%(loglevel)s 22 | args=(sys.stderr, ) 23 | 24 | [formatter_logfile] 25 | format=%(asctime)s %(levelname)s %(message)s 26 | datefmt= 27 | class=logging.Formatter 28 | 29 | -------------------------------------------------------------------------------- /docs/global.rst.inc: -------------------------------------------------------------------------------- 1 | .. highlight:: bash 2 | .. |project_name| replace:: ``BorgWeb`` 3 | .. |project_name_backup| replace:: ``BorgBackup`` 4 | .. |package_dirname| replace:: borgweb-|version| 5 | .. |package_filename| replace:: |package_dirname|.tar.gz 6 | .. |package_url| replace:: https://pypi.python.org/packages/source/b/borgweb/|package_filename| 7 | .. |git_url| replace:: https://github.com/borgbackup/borgweb.git 8 | .. _github: https://github.com/borgbackup/borgweb 9 | .. _issue tracker: https://github.com/borgbackup/borgweb/issues 10 | .. _Python: http://www.python.org/ 11 | .. _virtualenv: https://pypi.python.org/pypi/virtualenv/ 12 | 13 | -------------------------------------------------------------------------------- /js/src/env.js: -------------------------------------------------------------------------------- 1 | /** 2 | ~~ Environment ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 3 | */ 4 | module.exports = { 5 | lastSelectedLog: null, 6 | pollFrequency: 300, 7 | transitionTime: 170, 8 | coolDownTime: 1000, 9 | icon: { 10 | success: ['ok-circle', '#5cb85c'], 11 | warning: ['ban-circle', '#f0ad4e'], 12 | danger: ['remove-circle', '#c9302c'] }, 13 | logLine: { 14 | success: null, 15 | warning: ['warning', '#fcf8e3'], 16 | danger: ['danger', '#f2dede'] }, 17 | 18 | lastRun: 0, 19 | lastLogID: null, 20 | lastDirection: null, 21 | nextOffset: null, 22 | lastRendering: 0, 23 | reRenderCoolDown: 300, 24 | fetchRecentLogsStatus: 10 25 | } 26 | -------------------------------------------------------------------------------- /docs/internals.rst: -------------------------------------------------------------------------------- 1 | .. include:: global.rst.inc 2 | .. _internals: 3 | 4 | Internals 5 | ========= 6 | 7 | This page documents the internal workings of |project_name|. 8 | 9 | What we use 10 | ----------- 11 | 12 | * Flask and Werkzeug - Python web micro-framework and http toolbox 13 | * Bootstrap and jQuery - CSS framework, Javascript library 14 | * |project_name_backup| - for doing the backups 15 | 16 | Develop JS 17 | ~~~~~~~~~~ 18 | #. Have NodeJS/io.js and NPM installed. 19 | #. ``git clone https://github.com/borgbackup/borgweb.git`` 20 | #. ``cd borgweb/js`` 21 | #. ``npm install`` 22 | #. ``gulp watch`` 23 | #. Edit JS files within ``js/``; files will automatically be bundle into ``borgweb/static/bundle.js`` 24 | -------------------------------------------------------------------------------- /borgweb/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from flask import Flask, render_template 4 | from flask import g as flaskg 5 | 6 | from .views import blueprint 7 | 8 | 9 | def err404(error): 10 | return render_template('error.html', error=error), 404 11 | 12 | 13 | def create_app(): 14 | app = Flask(__name__) 15 | 16 | app.config.from_object('borgweb.config.Config') 17 | if os.environ.get('BORGWEB_CONFIG'): 18 | app.config.from_envvar('BORGWEB_CONFIG') 19 | 20 | app.register_blueprint(blueprint) 21 | 22 | app.jinja_env.globals['flaskg'] = flaskg 23 | app.register_error_handler(404, err404) 24 | 25 | return app 26 | 27 | 28 | def main(): 29 | application = create_app() 30 | application.run(host=application.config['HOST'], 31 | port=application.config['PORT'], 32 | debug=application.config['DEBUG']) 33 | 34 | 35 | if __name__ == '__main__': 36 | main() 37 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | .. include:: global.rst.inc 2 | .. _usage: 3 | 4 | Usage 5 | ===== 6 | 7 | Start the server process 8 | ------------------------ 9 | 10 | For using |project_name|, you need to start its web service first. 11 | 12 | To start the builtin server, run "borgweb". 13 | 14 | You should see the builtin server starting and announcing the URL it serves. 15 | It will continue running, outputting log information until you stop it. 16 | 17 | To stop the builtin server, type Ctrl-C or close the window. 18 | 19 | Alternatively, experienced python web administrators can also use 20 | |project_name| as a WSGI app, see the borgweb.wsgi python module. 21 | 22 | Point your browser at the service URL 23 | ------------------------------------- 24 | 25 | Use a web browser like Firefox and visit the web service URL. 26 | 27 | For the builtin server it usually is: http://127.0.0.1:5000/ 28 | 29 | |project_name| requires Javascript, so make sure it is not disabled. 30 | -------------------------------------------------------------------------------- /js/gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp') 2 | var log = require('gulp-util').log 3 | var lib = require('./gulpfile.lib.js') 4 | var browserSync = require('browser-sync').create() 5 | 6 | var files = { 7 | js: './index.js', 8 | jsBndl: '../borgweb/static/bundle.js', 9 | css: '../borgweb/static/style.css', 10 | cssBndl: '../borgweb/static/bundle.css' 11 | } 12 | var buildID = 1 13 | var newBuildLog = function () { 14 | log('Build # ' + lib.log(buildID++, 1) + 15 | ' ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~') } 16 | 17 | gulp.task('watch', function () { 18 | newBuildLog(); browserSync.init({proxy: 'localhost:5000', open: false }) 19 | lib.generateBundle(files.js, files.jsBndl, true) 20 | 21 | gulp.watch([files.js, 'src/**/*.js'], function (ev) { 22 | if (ev.type === 'changed') { 23 | newBuildLog() 24 | log('Changed: ' + ev.path) 25 | var ret = lib.generateBundle(files.js, files.jsBndl) 26 | setTimeout(function () { browserSync.reload() }, 150) // todo 27 | return ret 28 | } 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /js/gulpfile.lib.js: -------------------------------------------------------------------------------- 1 | var babelify = require('babelify') 2 | var browserify = require('browserify') 3 | var fs = require('fs') 4 | var log = require('gulp-util').log 5 | 6 | var ex = {} 7 | 8 | ex.log = function (what, colorCode) { 9 | return ('\u001b[' + colorCode + 'm' + what + '\u001b[0m') 10 | } 11 | 12 | ex.generateBundle = function (input, output) { 13 | log('Running \'' + ex.log('babelify', 36) + '\'') 14 | browserify({ debug: true }) 15 | .transform("babelify", {presets: ["@babel/preset-env"]}) 16 | .require(input, { entry: true }) 17 | // .plugin('minifyify', { map: 'static/bundle.js.map', output: output + '.map' }) 18 | .plugin('prependify', '/**\n This file was autogenerated.\n' + 19 | ' See https://github.com/borgbackup/borgweb\n' + 20 | '*/\n') 21 | .bundle() // todo: Why double sourcemap? https://github.com/ben-ng/minifyify/issues/95#issuecomment-117227925 22 | .on('error', function (err) { log('Error: ' + err.message) }) 23 | .pipe(fs.createWriteStream(output)) 24 | } 25 | 26 | module.exports = ex 27 | -------------------------------------------------------------------------------- /borgweb/templates/error.html: -------------------------------------------------------------------------------- 1 | {% extends "_layout.html" %} 2 | 3 | {% block content %} 4 | 12 | 13 |
14 |

15 | error: 16 |
17 | {{ error }} 18 |

19 |
20 | Documentation 21 | IRC Channel 22 | Mailinglist 23 | Issue Tracker 24 |
25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "borgweb", 3 | "version": "0.0.1", 4 | "description": "BorgWeb is a browser-based user interface for Borg Backup.", 5 | "main": "static/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/borgbackup/borgweb.git" 12 | }, 13 | "keywords": [ 14 | "borg", 15 | "borgbackup", 16 | "attic", 17 | "backup", 18 | "logviewer" 19 | ], 20 | "author": "https://github.com/borgbackup/borgweb/blob/master/AUTHORS", 21 | "license": "BSD-3-Clause", 22 | "bugs": { 23 | "url": "https://github.com/borgbackup/borgweb/issues" 24 | }, 25 | "homepage": "https://github.com/borgbackup/borgweb#readme", 26 | "devDependencies": { 27 | "@babel/core": "^7.0.0-beta.51", 28 | "@babel/preset-env": "^7.0.0-beta.51", 29 | "babelify": "^9.0.0", 30 | "browser-sync": "^2.24.5", 31 | "browserify": "^16.2.2", 32 | "dateformat": "^1.0.11", 33 | "gulp": "^4.0.0", 34 | "gulp-util": "^3.0.5", 35 | "minifyify": "^7.0.1", 36 | "prependify": "^1.1.4" 37 | }, 38 | "dependencies": { 39 | "bootstrap": "^3.3.5", 40 | "jquery": "^3.3.1" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /js/index.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery') 2 | var backup = require('./src/backup') 3 | var env = require('./src/env') 4 | var i18n = require('./src/i18n') 5 | var log = require('./src/util').log 6 | var viewer = require('./src/viewer') 7 | 8 | /** 9 | ~~ UI callables ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 10 | All globally available variables should be declared here. 11 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 12 | */ 13 | window.startBackup = backup.startBackup 14 | window.switchToLog = viewer.switchToLog 15 | window.nextPage = viewer.nextPage 16 | window.previousPage = viewer.previousPage 17 | window.firstPage = viewer.firstPage 18 | window.lastPage = viewer.lastPage 19 | 20 | /** 21 | ~~ Site init ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 22 | */ 23 | viewer.updateLogFileList() 24 | viewer.render() 25 | i18n.translate() 26 | 27 | /** 28 | ~~ Event listeners ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 29 | */ 30 | $(window).resize(function () { 31 | if (Date.now() - env['lastRendering'] >= env['reRenderCoolDown']) { 32 | log('Re-rendering') 33 | env['lastRendering'] = Date.now() 34 | setTimeout(viewer.render, env['reRenderCoolDown'] / 2) 35 | } 36 | }) 37 | -------------------------------------------------------------------------------- /js/src/util.js: -------------------------------------------------------------------------------- 1 | let $ = require('jquery') 2 | let dateformat = require('dateformat') 3 | 4 | /** 5 | ~~ Utility ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 6 | */ 7 | 8 | function log () { 9 | var args = Array.prototype.slice.call(arguments) 10 | var time = '[' + dateformat(new Date(), 'HH:MM:ss') + ']' 11 | args.unshift(time) 12 | console.log.apply(console, args) 13 | return this 14 | } 15 | 16 | function isInt (n) { 17 | return n % 1 === 0 18 | } 19 | 20 | function success (data) { 21 | logFiles = data.log_files 22 | } 23 | 24 | function parseAnchor () { 25 | var anchor = window.location.hash.slice(1) 26 | if (anchor) { 27 | var parts = anchor.split(';') 28 | var partsParsed = {} 29 | parts.forEach(function (e) { 30 | var pair = e.split(':') 31 | partsParsed[pair[0]] = pair[1] 32 | }) 33 | return partsParsed 34 | } else { 35 | log('Anchor not available') 36 | return {'log': 0} 37 | } 38 | } 39 | 40 | function determineLineCount () { 41 | let availableLines = Math.floor($('#log-text').height() / 18) 42 | return availableLines 43 | } 44 | 45 | module.exports = { 46 | log: log, 47 | isInt: isInt, 48 | success: success, 49 | parseAnchor: parseAnchor, 50 | determineLineCount: determineLineCount 51 | } 52 | -------------------------------------------------------------------------------- /docs/faq.rst: -------------------------------------------------------------------------------- 1 | .. _faq: 2 | .. include:: global.rst.inc 3 | 4 | Frequently asked questions 5 | ========================== 6 | 7 | Which platforms are supported? 8 | For the |project_name| web service, we try to support the same platforms 9 | as for |project_name_backup|. 10 | 11 | Additionally, you will need some sane browser to access the web service, 12 | like Firefox or Chrome/Chromium. Using MS Internet Explorer [IE] (or other 13 | other browsers based on it) is discouraged and unsupported. 14 | If you run a sane browser, accessing the service should work from desktop 15 | and mobile platforms, we try to adapt to your screen resolution. 16 | 17 | Why a web-based / browser-based approach? 18 | We didn't implement a "normal" desktop application, but a web app because: 19 | 20 | - too many different desktop and mobile platforms to support (Linux, *BSD, 21 | Windows, Mac OS X, Android, iOS + a ton of different options per platform) 22 | - html5, css and js works (almost) everywhere and we even have same code / 23 | similar UI everywhere. 24 | - you can run the browser on the same machine as the backup software 25 | (typical desktop backup scenario), but you can also run it on another 26 | machine (server-without-GUI scenario) - more flexibility! 27 | -------------------------------------------------------------------------------- /borgweb/config.py: -------------------------------------------------------------------------------- 1 | class Config(object): 2 | """This is the basic configuration class for BorgWeb.""" 3 | 4 | #: builtin web server configuration 5 | HOST = '127.0.0.1' # use 0.0.0.0 to bind to all interfaces 6 | PORT = 5000 # ports < 1024 need root 7 | DEBUG = False # if True, enable reloader and debugger 8 | 9 | #: borg / borgweb configuration 10 | LOG_DIR = 'logs' 11 | REPOSITORY = 'repo' 12 | NAME = 'localhost' 13 | 14 | # when you click on "start backup", this command will be given to a OS 15 | # shell to execute it. 16 | # if you just need something simple (like "borg create ..."), just put 17 | # the command here. if you need something more complex, write a script and 18 | # call it from here. 19 | # commands will be executed as the same user as the one used for running 20 | # borgweb. for running commands as root, you'll need to use sudo (and 21 | # configure it in an appropriate and secure way). 22 | # template placeholders like {LOG_DIR} (and other stuff set in the config) 23 | # will be expanded to their value before the shell command is executed. 24 | BACKUP_CMD = "BORG_LOGGING_CONF=logging.conf borg create --list --stats --show-version --show-rc {REPOSITORY}::{NAME}-{LOCALTIME} /etc >{LOG_DIR}/{NAME}-{LOCALTIME} 2>&1 2 | 3 | 4 | {{ title }} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 21 | 22 | 23 |
24 | 25 | {% include '_nav.html' %} 26 | 27 | {% block content %}{% endblock %} 28 | 29 |
30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /js/src/i18n.js: -------------------------------------------------------------------------------- 1 | let $ = require('jquery') 2 | let log = require('./util').log 3 | 4 | let lang = navigator.language || navigator.userLanguage 5 | let notEnglish = (lang.indexOf('en') !== 0) 6 | 7 | function translateAttribute (i18nElements, attr, translations) { 8 | for (let i = 0; i18nElements[i]; i++) { 9 | let elem = i18nElements[i] 10 | let key = elem[attr] 11 | if (translations[key]) elem[attr] = translations[key] 12 | } 13 | } 14 | 15 | function replaceMarkedTexts (translations) { 16 | // replace text marked by `` tags 17 | let i18nElements = document.getElementsByTagName('x-i18n') 18 | translateAttribute(i18nElements, 'innerHTML', translations) 19 | } 20 | 21 | function replaceAttrTexts (attr, translations) { 22 | // replace `title` attribute texts 23 | let i18nElements = document.getElementsByClassName('i18n') 24 | translateAttribute(i18nElements, attr, translations) 25 | } 26 | 27 | function translate () { 28 | let url = 'static/i18n/' + lang + '.json' 29 | if (notEnglish) { 30 | $.get(url, translations => { 31 | replaceMarkedTexts(translations) 32 | replaceAttrTexts('title', translations) 33 | replaceAttrTexts('aria-label', translations) 34 | }).fail(() => { 35 | log(`Translation missing for '${ lang }', 36 | help out on Github!`) 37 | }) 38 | } 39 | } 40 | 41 | module.exports = { 42 | 'translate': translate 43 | } 44 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | What is BorgWeb? 2 | ================ 3 | 4 | BorgWeb is a browser-based user interface for `Borg Backup `_. 5 | 6 | The UI is intended to help with everyday tasks, it is not intended as a full UI to everything borg can do. 7 | 8 | You'll need help of an admin to install and configure it, as well as to restore backups. 9 | 10 | Main features 11 | ------------- 12 | 13 | - BorgWeb (the web service) usually runs on the machine that is backed up with 14 | BorgBackup. You can use a web browser to access BorgWeb from the same 15 | machine or from another machine. 16 | - BorgWeb can operate using a builtin web server or as WSGI app using an 17 | external web server (like e.g. apache + mod_wsgi). 18 | - using the web browser you can: 19 | 20 | * review backup log files 21 | * start a backup 22 | 23 | 24 | Links 25 | ===== 26 | 27 | * `Documentation `_ 28 | * `PyPI packages `_ 29 | * `Github `_ 30 | * `Issue Tracker `_ 31 | 32 | 33 | Notes 34 | ----- 35 | 36 | NOT RELEASED DEVELOPMENT VERSIONS HAVE UNKNOWN COMPATIBILITY PROPERTIES. 37 | 38 | THIS IS SOFTWARE IN DEVELOPMENT, DECIDE YOURSELF WHETHER IT FITS YOUR NEEDS. 39 | 40 | Please also see the `LICENSE `_ for more information. 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2015 The Borg Collective (see AUTHORS file) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in 12 | the documentation and/or other materials provided with the 13 | distribution. 14 | 3. The name of the author may not be used to endorse or promote 15 | products derived from this software without specific prior 16 | written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS 19 | OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 21 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 22 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE 24 | GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 26 | IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 27 | OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN 28 | IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | BorgWeb Changelog 2 | ================= 3 | 4 | Version 0.3.0 5 | ------------- 6 | 7 | Fixes: 8 | 9 | - fix flask>=1.0 compatibility, #116 10 | - disconnect stdin from console, so it does not show progress 11 | 12 | New features: 13 | 14 | - add builtin web server configurability 15 | - add some options to borg create command 16 | 17 | - --list so all files are listed 18 | - --stats to see some statistics 19 | - --show-version to see the borg version 20 | - --show-rc to see return code of borgbackup 21 | - use a logging configuration 22 | - adapt line and overall classifier for new log line format 23 | - overall classifier: just look at terminating with ... rc X, 24 | X = 0 -> SUCCESS, X = 1 -> WARNING, other -> DANGER 25 | 26 | Other changes: 27 | 28 | - drop python 3.3 support, add 3.5 and 3.6 support 29 | - manifest is maintained via setuptools_scm, #121 30 | - remove remainders of versioneer, #120 31 | - disable flask debugger 32 | - docs: 33 | 34 | - update website / docs link and mailing list info 35 | - update installation.rst about how to create borg logfiles, #110 36 | - refresh theme (use same style as borg docs) 37 | - move sidebar links into docs 38 | 39 | 40 | Version 0.2.0 41 | ------------- 42 | 43 | Fixes: 44 | - update path/status line after running a backup 45 | - highlight the currently shown log, fixes #87 46 | - when no logs are present, display URLs etc., fixes #82 47 | - removed unused dependency (cython) 48 | - don't hardcode the name `borg`, fixes #76 49 | - fixed `documentation` link 50 | 51 | New features: 52 | - UI improvements: 53 | - misc. UI/UX improvements 54 | - show backup status in log list 55 | - added scrollbar for logfile list 56 | - translation infrastructure + de_DE translation, #85 57 | - use setuptools_scm instead of versioneer 58 | 59 | Other changes: 60 | - updated and improved docs 61 | 62 | 63 | Version 0.1.4 64 | ------------- 65 | 66 | First (working) pypi release. 67 | 68 | -------------------------------------------------------------------------------- /docs/support.rst: -------------------------------------------------------------------------------- 1 | .. include:: global.rst.inc 2 | .. _support: 3 | 4 | Support 5 | ======= 6 | 7 | Please first read the docs, the existing issue tracker issues and mailing 8 | list posts -- a lot of stuff is already documented / explained / discussed / 9 | filed there. 10 | 11 | Issue Tracker 12 | ------------- 13 | 14 | If you've found a bug or have a concrete feature request, please create a new 15 | ticket on the project's `issue tracker`_. 16 | 17 | For more general questions or discussions, IRC or mailing list are preferred. 18 | 19 | IRC 20 | --- 21 | Join us on channel #borgbackup on chat.freenode.net. 22 | 23 | As usual on IRC, just ask or tell directly and then patiently wait for replies. 24 | Stay connected. 25 | 26 | Mailing list 27 | ------------ 28 | 29 | To find out about the mailing list, its topic, how to subscribe, how to 30 | unsubscribe and where you can find the archives of the list, see the 31 | `mailing list homepage 32 | `_. 33 | 34 | Bounties and Fundraisers 35 | ------------------------ 36 | 37 | We use `BountySource `_ to allow 38 | monetary contributions to the project and the developers, who push it forward. 39 | 40 | There, you can give general funds to the borgbackup members (the developers will 41 | then spend the funds as they deem fit). If you do not have some specific bounty 42 | (see below), you can use this as a general way to say "Thank You!" and support 43 | the software / project you like. 44 | 45 | If you want to encourage developers to fix some specific issue or implement some 46 | specific feature suggestion, you can post a new bounty or back an existing one 47 | (they always refer to an issue in our `issue tracker`_). 48 | 49 | As a developer, you can become a Bounty Hunter and win bounties (earn money) by 50 | contributing to |project_name|, a free and open source software project. 51 | 52 | We might also use BountySource to fund raise for some bigger goals. 53 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | .. include:: global.rst.inc 2 | .. _installation: 3 | 4 | Installation 5 | ============ 6 | 7 | |project_name| requires: 8 | 9 | * Python_ >= 3.4 10 | * some python dependencies, see install_requires in setup.py 11 | 12 | Currently, only installation from git repo checkout is supported. 13 | 14 | Use pip install -e . from the top-level |project_name| directory to install 15 | it into same virtualenv as you use for |project_name_backup|. 16 | 17 | To install the complete environment for development you can do the following: :: 18 | 19 | # Install Python code and dependencies: 20 | virtualenv --python=python3 borg-env 21 | source borg-env/bin/activate 22 | pip install tox pytest 23 | git clone https://github.com/borgbackup/borgweb.git 24 | cd borgweb 25 | pip install -e . 26 | 27 | # Install JS code and dependencies: 28 | cd js 29 | npm install 30 | 31 | # Start the local Flask webserver: 32 | cd ../../ 33 | mkdir logs 34 | ./borg-env/bin/borgweb 35 | 36 | # Start the watch process and Browsersync 37 | # First install gulp 38 | sudo npm install --global gulp-cli 39 | # In another shell navigate to `borgweb/js` and enter: 40 | gulp watch 41 | 42 | 43 | Configuration 44 | ============= 45 | The builtin default configuration expects a "repo/" directory and a "logs/" 46 | directory in the current working directory. This is mostly development and 47 | testing, we do not expect normal setups to use the default configuration. 48 | 49 | You can override this by pointing to a custom configuration file via the 50 | environment variable BORGWEB_CONFIG. 51 | 52 | The configuration file must only have lines like this (and NO indentation):: 53 | 54 | KEY = value # KEY must be all-uppercase, valid python syntax 55 | 56 | See borgweb/config.py for the currently supported keys and example values. 57 | 58 | Borg does by default not create log files. In order to create logs, the output of Borg must be redirected to a file. 59 | -------------------------------------------------------------------------------- /borgweb/views/backup.py: -------------------------------------------------------------------------------- 1 | """ 2 | backup view 3 | """ 4 | 5 | import subprocess 6 | import time 7 | 8 | from flask import current_app, render_template, jsonify 9 | 10 | from . import blueprint 11 | 12 | process = None 13 | 14 | 15 | @blueprint.route('/backup/start', methods=['POST']) 16 | def backup_start(): 17 | env = dict(current_app.config) 18 | now = time.time() 19 | fmt = '%Y-%m-%d-%H:%M:%S' 20 | env['LOCALTIME'] = time.strftime(fmt, time.localtime(now)) 21 | env['UTC'] = time.strftime(fmt, time.gmtime(now)) 22 | cmd = env['BACKUP_CMD'].format(**env) 23 | global process 24 | if process is None or process.returncode is not None: 25 | # no process ever run or process has terminated 26 | process = subprocess.Popen(cmd, shell=True, stdin=None, stdout=None, stderr=None) 27 | msg = "started, pid=%d" % process.pid 28 | else: 29 | msg = "already running, pid %d" % process.pid 30 | return jsonify(dict(msg=msg, pid=process.pid)) 31 | 32 | 33 | @blueprint.route('/backup/stop', methods=['POST']) 34 | def backup_stop(): 35 | global process 36 | if process is None: 37 | rc = -1 38 | msg = 'not running' 39 | else: 40 | pid = process.pid 41 | try: 42 | process.terminate() 43 | for t in range(10): 44 | rc = process.poll() 45 | if rc is not None: 46 | msg = 'terminated pid %d' % pid 47 | break 48 | time.sleep(1) 49 | else: 50 | process.kill() 51 | msg = 'killed pid %d' % pid 52 | rc = -1 53 | except ProcessLookupError: 54 | rc = -1 55 | msg = 'not running' 56 | return jsonify(dict(msg=msg, rc=rc)) 57 | 58 | 59 | @blueprint.route('/backup/status') 60 | def backup_rc(): 61 | global process 62 | if process is not None: 63 | rc = process.poll() 64 | if rc is None: 65 | msg = 'running, pid %d' % process.pid 66 | else: 67 | msg = 'not running, last rc=%d' % rc 68 | else: 69 | msg = 'not running' 70 | rc = -1 71 | return jsonify(dict(msg=msg, rc=rc)) 72 | 73 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 *-* 2 | import sys 3 | 4 | min_python = (3, 4) 5 | if sys.version_info < min_python: 6 | print("BorgWeb requires Python %d.%d or later" % min_python) 7 | sys.exit(1) 8 | 9 | from setuptools import setup, find_packages 10 | 11 | with open('README.rst', 'r') as fd: 12 | long_description = fd.read() 13 | 14 | setup( 15 | name='borgweb', 16 | use_scm_version=dict(write_to='borgweb/_version.py'), 17 | author='The Borg Collective (see AUTHORS file)', 18 | author_email='borgbackup@python.org', 19 | url='https://borgweb.readthedocs.io/', 20 | description='Browser-based user interface for BorgBackup', 21 | long_description=long_description, 22 | license='BSD', 23 | platforms=['Linux', 'MacOS X', 'FreeBSD', 'OpenBSD', 'NetBSD', ], 24 | classifiers=[ 25 | 'Development Status :: 4 - Beta', 26 | 'Environment :: Console', 27 | 'Intended Audience :: System Administrators', 28 | 'License :: OSI Approved :: BSD License', 29 | 'Operating System :: POSIX :: BSD :: FreeBSD', 30 | 'Operating System :: POSIX :: BSD :: OpenBSD', 31 | 'Operating System :: POSIX :: BSD :: NetBSD', 32 | 'Operating System :: MacOS :: MacOS X', 33 | 'Operating System :: POSIX :: Linux', 34 | 'Programming Language :: Python', 35 | 'Programming Language :: Python :: 3', 36 | 'Programming Language :: Python :: 3.4', 37 | 'Programming Language :: Python :: 3.5', 38 | 'Programming Language :: Python :: 3.6', 39 | 'Topic :: System :: Archiving :: Backup', 40 | ], 41 | packages=find_packages(), 42 | package_data={ 43 | 'borgweb': [ 44 | 'static/*.*', # does NOT match subdirectories! 45 | 'static/bootstrap/*', 46 | 'static/fonts/*', 47 | 'static/i18n/*', 48 | 'templates/*', 49 | ], 50 | }, 51 | include_package_data=True, 52 | zip_safe=False, 53 | entry_points={ 54 | 'console_scripts': [ 55 | 'borgweb = borgweb.app:main', 56 | ] 57 | }, 58 | setup_requires=['setuptools_scm>=1.7'], 59 | install_requires=[ 60 | 'flask>=0.7', # >= 0.10 required for python 3 61 | ], 62 | ) 63 | -------------------------------------------------------------------------------- /borgweb/templates/_nav.html: -------------------------------------------------------------------------------- 1 | 39 | -------------------------------------------------------------------------------- /js/src/backup.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery') 2 | var env = require('./env') 3 | var util = require('./util') 4 | var viewer = require('./viewer') 5 | 6 | /** 7 | ~~ Backup interaction ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 8 | */ 9 | 10 | function noBackupRunning (callback) { 11 | $.getJSON('backup/status', function (resp) { 12 | var backupRunning = resp.rc === null 13 | if (backupRunning) util.log(`▶ Backup in progress`) 14 | else util.log(`✖ No backup in progress`) 15 | callback(!backupRunning) 16 | }) 17 | } 18 | 19 | function pollBackupStatus (endpoint, ms, callback) { 20 | noBackupRunning(function (notRunning) { 21 | if (notRunning) { 22 | $('.navbar button[type=submit]').toggleClass('btn-success') 23 | $('.navbar button[type=submit]').toggleClass('btn-warning') 24 | $('.navbar button[type=submit]').text(`▶ Start Backup`) 25 | $.getJSON('logs', viewer.updateLogFileList) 26 | viewer.switchToLog(1) 27 | } else { 28 | util.log(`Polling backup status`) 29 | $.getJSON('backup/status', callback) 30 | setTimeout(function () { 31 | pollBackupStatus(endpoint, ms, callback) 32 | }, ms) 33 | } 34 | }) 35 | } 36 | 37 | function stopBackup () { 38 | util.log(`Terminating (eventually killing) the backup process`) 39 | $.post('backup/stop', {}, function (res) { 40 | util.log(`Message: '${ res.msg }', RC: '${ res.rc }'`) 41 | }) 42 | } 43 | 44 | function startBackup (force) { 45 | if (force) { 46 | util.log(`Sending backup start request`) 47 | if (Date.now() - env['lastRun'] >= env['coolDownTime']) { 48 | env['lastRun'] = Date.now() 49 | $.post('backup/start', {}, function () { 50 | $('.navbar button[type=submit]').toggleClass('btn-success') 51 | $('.navbar button[type=submit]').toggleClass('btn-warning') 52 | $('.navbar button[type=submit]').text(`✖ Stop Backup`) 53 | pollBackupStatus('backup/status', env['pollFrequency'], 54 | function (res) { 55 | util.log(`Received status update`) 56 | } 57 | ) 58 | }) 59 | } else util.log('Restarting backup too fast, ignoring') 60 | } else { 61 | if (force === undefined) noBackupRunning(startBackup) 62 | else stopBackup() 63 | } 64 | } 65 | 66 | module.exports = { 67 | noBackupRunning: noBackupRunning, 68 | pollBackupStatus: pollBackupStatus, 69 | stopBackup: stopBackup, 70 | startBackup: startBackup 71 | } 72 | -------------------------------------------------------------------------------- /borgweb/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "_layout.html" %} 2 | 3 | {% block content %} 4 |
5 | 6 |
7 |   8 |
9 |
10 |

11 |
12 |
13 | 14 |
15 | 16 |
17 | 18 |
19 |
20 |
21 |

22 | No logs found 23 |

24 |
25 | Documentation 26 | IRC Channel 27 | Mailinglist 28 | Mailinglist 29 |
30 |

31 |     
32 |
33 | 34 |
35 | 36 |
37 |
38 |
39 | 55 |
56 |
57 | 58 | 59 | {% endblock %} 60 | 61 | -------------------------------------------------------------------------------- /borgweb/static/style.css: -------------------------------------------------------------------------------- 1 | html { height: 100%; } 2 | body { 3 | padding-top: 66px; 4 | } 5 | #no-logs-error { 6 | display: none; 7 | } 8 | #content-row { 9 | height: calc(95vh - 140px); 10 | margin-bottom: 8px; 11 | } 12 | #log-viewer, 13 | #list-viewer { 14 | height: 100%; 15 | } 16 | 17 | #log-viewer pre { 18 | overflow-y: hidden; 19 | } 20 | 21 | #list-viewer { 22 | height: 100%; 23 | overflow-y: scroll; 24 | padding: 4px 0 0; 25 | border: 1px solid #ccc; 26 | border-left: 0; 27 | } 28 | #list-viewer li { 29 | border: 1px solid #ccc; 30 | border-right: 0; 31 | border-radius: 4px; 32 | border-top-right-radius: 0; 33 | border-bottom-right-radius: 0; 34 | } 35 | .list-status-indicator { 36 | font-size: 18px; 37 | color: #ccc; 38 | display: block; 39 | float: left; 40 | margin-left: 0; 41 | margin-top: 3px; 42 | padding-right: 6px; 43 | opacity: 0.8; 44 | } 45 | 46 | #log-files { 47 | font-size: 18px; 48 | } 49 | #log-files li:last-child { 50 | margin-bottom: 12px; 51 | } 52 | #log-files li a { 53 | color: #333; 54 | cursor: pointer; 55 | } 56 | #log-files li a:hover { 57 | background-color: #E6E6E6; 58 | } 59 | a.shown-log { 60 | background-color: #E6E6E6; 61 | } 62 | 63 | #log-path { 64 | height: 36px; 65 | margin-top: 0; 66 | font-size: 20px; 67 | color: #333 !important; 68 | padding-left: 9px; 69 | padding-bottom: 2px; 70 | border-bottom: 1px solid #ccc; 71 | border-style: hidden; 72 | } 73 | #log-path input { 74 | width: calc(100% - 46px); 75 | display: inline-block; 76 | font-size: 18px; 77 | } 78 | 79 | #log-text { 80 | height: 100%; 81 | color: black; 82 | white-space: pre-wrap; 83 | line-height: 18px; 84 | } 85 | #log-text mark { 86 | width: calc(100% + 19px); 87 | display: inline-block; 88 | padding-left: 9.5px; padding-right: 9.5px; 89 | margin-left: -9.5px; margin-right: -9.5px; 90 | } 91 | 92 | #pagination-row ul { 93 | margin: 2px 0 0; 94 | } 95 | #pagination-row a { 96 | color: #3c8b3c; 97 | } 98 | 99 | .btn-success:hover, 100 | .btn-warning:hover, 101 | .btn-success:focus, 102 | .btn-warning:focus { 103 | margin-left: 8px; 104 | color: white; 105 | text-decoration: none; 106 | background-position: 0; 107 | background-repeat: repeat; 108 | background-color: #419641; 109 | } 110 | .btn-success:hover { 111 | background-image: -webkit-gradient(linear,left top,left bottom,from(#419641),to(#5cb85c)); 112 | } 113 | .btn-warning:hover { 114 | background-image: -webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#eb9316)); 115 | } 116 | -------------------------------------------------------------------------------- /borgweb/views/logs.py: -------------------------------------------------------------------------------- 1 | """ 2 | logs view 3 | """ 4 | 5 | import os 6 | 7 | from flask import current_app, render_template, jsonify, abort 8 | 9 | from . import blueprint 10 | 11 | SUCCESS, INFO, WARNING, DANGER = 'success', 'info', 'warning', 'danger' 12 | 13 | 14 | def overall_classifier(f): 15 | end = f.seek(0, os.SEEK_END) 16 | f.seek(max(0, end - 250), os.SEEK_SET) # len(last log line) < 250 17 | f.seek(0, os.SEEK_SET) 18 | lines = f.readlines() 19 | try: 20 | line = lines[-1].rstrip('\n') 21 | except IndexError: 22 | return DANGER # something strange happened, empty log? 23 | try: 24 | line = line.split(" ", 3)[3] # get msg from "date time level msg" 25 | except IndexError: 26 | # unexpected log line format 27 | return DANGER 28 | if line.startswith('terminating with'): 29 | if line.endswith('rc 0'): 30 | return SUCCESS 31 | if line.endswith('rc 1'): 32 | return WARNING 33 | return DANGER # rc == 2 or other, or no '--show-rc' given. 34 | 35 | 36 | def line_classifier(line): 37 | try: 38 | level = line.split(" ", 3)[2] # get level from "date time level msg" 39 | except IndexError: 40 | # unexpected log line format 41 | level = 'ERROR' 42 | if level == 'INFO': 43 | return INFO 44 | if level == 'WARNING': 45 | return WARNING 46 | return DANGER 47 | 48 | 49 | def _get_logs(): 50 | log_dir = current_app.config['LOG_DIR'] 51 | log_dir = os.path.abspath(log_dir) 52 | try: 53 | log_files = os.listdir(log_dir) 54 | except OSError: 55 | log_files = [] 56 | return log_dir, sorted(log_files, reverse=True) 57 | 58 | 59 | def _get_log_lines(log_dir, log_file, offset, linecount=None, direction=1): 60 | log_file = os.path.join(log_dir, log_file) 61 | with open(log_file, 'r') as f: 62 | if direction == 1: # forwards 63 | f.seek(offset) 64 | if linecount is None: # read all, starting from offset 65 | log_lines = f.readlines() 66 | else: # read n lines, starting from offset 67 | log_lines = [] 68 | for i in range(linecount): 69 | line = f.readline() 70 | if not line: 71 | break 72 | log_lines.append(line) 73 | offset = f.tell() 74 | elif direction == -1: # backwards 75 | log_lines = [] 76 | if linecount is None: # read all, up to offset 77 | start = 0 78 | else: # read n lines, up to offset 79 | # we do not expect medium line length bigger than 1024 80 | start = max(0, offset - linecount * 1024) 81 | f.seek(start) 82 | current = 0 83 | while current < offset: 84 | line = f.readline() 85 | if not line: 86 | break 87 | current = f.tell() 88 | log_lines.append((current, line)) 89 | if linecount is None: 90 | offset = 0 91 | log_lines = [line for _, line in log_lines] 92 | else: 93 | try: 94 | offset = log_lines[-linecount-1][0] 95 | except IndexError: 96 | offset = 0 97 | log_lines = [line for _, line in log_lines[-linecount:]] 98 | else: 99 | raise ValueError("give direction == 1 (forwards) or -1 (backwards)") 100 | log_lines = [line.rstrip('\n') for line in log_lines] 101 | return log_file, offset, log_lines 102 | 103 | 104 | @blueprint.route('/logs//::') 105 | def get_log_fragment(index, offset, linecount, direction): 106 | try: 107 | offset = int(offset) 108 | except ValueError: 109 | offset = 0 110 | try: 111 | linecount = int(linecount) 112 | except ValueError: 113 | linecount = None 114 | try: 115 | direction = int(direction) 116 | if direction not in (-1, 1): 117 | raise ValueError 118 | except ValueError: 119 | direction = 1 120 | log_dir, log_files = _get_logs() 121 | try: 122 | log_file = log_files[index] 123 | except IndexError: 124 | abort(404) 125 | log_file, offset, log_lines = _get_log_lines(log_dir, log_file, offset, linecount, direction) 126 | log_lines = [(line_classifier(line), line) for line in log_lines] 127 | return jsonify(dict(lines=log_lines, offset=offset)) 128 | 129 | 130 | @blueprint.route('/logs/') 131 | def get_log(index): 132 | log_dir, log_files = _get_logs() 133 | try: 134 | log_file = log_files[index] 135 | except IndexError: 136 | abort(404) 137 | else: 138 | log_file = os.path.join(log_dir, log_file) 139 | with open(log_file, 'r') as f: 140 | length = f.seek(0, os.SEEK_END) 141 | status = overall_classifier(f) 142 | return jsonify(dict(filename=log_file, status=status, length=length)) 143 | 144 | 145 | @blueprint.route('/logs') 146 | def get_logs(): 147 | log_dir, log_files = _get_logs() 148 | return jsonify(dict(dir=log_dir, 149 | files=list(enumerate(log_files)))) 150 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/borgweb.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/borgweb.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/borgweb" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/borgweb" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | 132 | gh-io: html 133 | GH_IO_CLONE="`mktemp -d`" && \ 134 | git clone git@github.com:borgbackup/borgweb.github.io.git $$GH_IO_CLONE && \ 135 | (cd $$GH_IO_CLONE && git rm -r *) && \ 136 | cp -r _build/html/* $$GH_IO_CLONE && \ 137 | (cd $$GH_IO_CLONE && git add -A && git commit -m 'Updated borgweb.github.io' && git push) && \ 138 | rm -rf $$GH_IO_CLONE 139 | 140 | inotify: html 141 | while inotifywait -r . --exclude usage.rst --exclude '_build/*' ; do make html ; done 142 | -------------------------------------------------------------------------------- /js/src/viewer.js: -------------------------------------------------------------------------------- 1 | let $ = require('jquery') 2 | let env = require('./env') 3 | let util = require('./util') 4 | 5 | /** 6 | ~~ Log viewer frontend ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 7 | */ 8 | let logText = $('#log-text') 9 | let noLogsError = $('#no-logs-error') 10 | 11 | function highlightListEntry (id) { 12 | $('.shown-log').toggleClass('shown-log') 13 | $(`#log-${ id }`).toggleClass('shown-log') 14 | } 15 | 16 | function setListItemStatus () { 17 | for (let i = 0; i < env.fetchRecentLogsStatus; i++) { 18 | $.getJSON('logs/' + i, res => { 19 | let search = `#log-${i} .glyphicon` 20 | let elem = $(search) 21 | elem.css('color', env.icon[res.status][1]) 22 | elem.removeClass('glyphicon-time') 23 | elem.addClass(`glyphicon-${ env.icon[res.status][0]}`) 24 | }) 25 | } 26 | } 27 | 28 | function updateLogFileList () { 29 | let logFilesListHTML = [] 30 | $.getJSON('logs', res => { 31 | let i = 0 32 | $.each(res.files, (key, value) => { 33 | let insert = (i < env.fetchRecentLogsStatus) 34 | let indicatorHTML = insert ? ` 35 | ` : '' 37 | logFilesListHTML += ` 38 |
  • 39 | 40 | ${ indicatorHTML } 41 | ${ value[1] } 42 | 43 |
  • ` 44 | i++ 45 | }) 46 | setListItemStatus() 47 | $('#log-files').html(logFilesListHTML) 48 | highlightListEntry(0) 49 | updatePathAndStatus(0) 50 | }) 51 | } 52 | 53 | function getSetState (state) { 54 | state = state || {} 55 | let anchor = util.parseAnchor() 56 | anchor = { 57 | log: state.log || anchor.log || 1, 58 | offset: state.offset || anchor.offset || 1 59 | } 60 | document.location.hash = 61 | `#log:${ anchor.log };offset:${ anchor.offset }` 62 | return anchor 63 | } 64 | 65 | function updatePathAndStatus (id) { 66 | if ((id + 1) === env.lastLogID) ; 67 | else { 68 | $.getJSON('logs/' + id, function (res) { 69 | $('#log-path').html(` 70 | 71 | 77 | `) 78 | highlightListEntry(id) 79 | }) 80 | } 81 | } 82 | 83 | function insertLogData (linesArray) { 84 | let [html, lineStatus] = [``, ``] 85 | linesArray.forEach((val, index) => { 86 | lineStatus = env.logLine[val[0]] 87 | html = lineStatus ? `` : `` 89 | html += val[1] + '\n' 90 | html += lineStatus ? `` : `` 91 | logText.append(html) 92 | }) 93 | } 94 | 95 | function clearLog () { logText.html('') } 96 | 97 | let fadeLog = { 98 | out: x => { 99 | setTimeout(clearLog, env.transitionTime * 0.5) 100 | logText.fadeOut(env.transitionTime * 0.5) 101 | }, 102 | in: x => { 103 | logText.fadeIn(env.transitionTime * 0.5) 104 | } 105 | } 106 | 107 | function displayLogSection (state, availableLines) { 108 | let url = `logs/${ state.log - 1 }/${ state.offset - 1 }:${ availableLines }:1` 109 | $.get(url, res => { 110 | noLogsError.hide() 111 | if (state.log === env.lastLogID) { 112 | clearLog() 113 | insertLogData(res.lines) 114 | } else { 115 | env.lastLogID = state.log 116 | fadeLog.out() 117 | setTimeout(x => { 118 | insertLogData(res.lines) 119 | fadeLog.in() 120 | }, env.transitionTime * 0.5) 121 | } 122 | }).fail(err => { 123 | noLogsError.show() 124 | logText.hide() 125 | }) 126 | } 127 | 128 | function render (availableLines) { 129 | availableLines = availableLines || util.determineLineCount() 130 | let state = getSetState() 131 | updatePathAndStatus(state.log - 1) 132 | displayLogSection(state, availableLines) 133 | } 134 | 135 | function switchToLog (id) { 136 | getSetState({ log: id, offset: 1 }) 137 | render() 138 | } 139 | 140 | function getNextOffset (state, direction, availableLines, callback) { 141 | let url = `logs/${ state.log - 1 }/${ state.offset - 1 }` + 142 | `:${ availableLines }:${ direction }` 143 | $.get(url, res => { 144 | let subsequentUrl = `logs/${ state.log - 1 }/${ res.offset + 1 }` + 145 | `:${ availableLines }:${ direction }` 146 | $.get(subsequentUrl, subsequentRes => { 147 | if (subsequentRes.lines.length === 0) getSetState(state) 148 | else callback(state, res, availableLines) 149 | }) 150 | }) 151 | } 152 | 153 | function switchPage (direction) { 154 | var availableLines = util.determineLineCount() 155 | getNextOffset(getSetState(), direction, availableLines, 156 | (state, res, availableLines) => { 157 | getSetState({ log: state.log, offset: res.offset + 1 }) 158 | render(availableLines) 159 | } 160 | ) 161 | } 162 | 163 | function lastPage () { 164 | let state = getSetState() 165 | let url = `logs/${ state.log - 1 }` 166 | $.get(url, res => { 167 | let logLength = res.length 168 | state.offset = logLength 169 | getSetState(state) 170 | switchPage(-1) 171 | }) 172 | } 173 | 174 | function nextPage () { switchPage(1) } 175 | 176 | function previousPage () { switchPage(-1) } 177 | 178 | function firstPage () { 179 | let state = getSetState() 180 | state.offset = 1 181 | setTimeout(x => { getSetState(state) }, 1) // prevent anchor loss 182 | switchToLog(state.log) 183 | } 184 | 185 | module.exports = { 186 | render: render, 187 | switchToLog: switchToLog, 188 | nextPage: nextPage, 189 | previousPage: previousPage, 190 | updateLogFileList: updateLogFileList, 191 | firstPage: firstPage, 192 | lastPage: lastPage 193 | } 194 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # documentation build configuration file, created by 4 | # sphinx-quickstart on Sat Sep 10 18:18:25 2011. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | #from borgweb import __version__ as sw_version 16 | sw_version = "0.0.0" 17 | 18 | on_rtd = os.environ.get('READTHEDOCS', None) == 'True' 19 | 20 | # If extensions (or modules to document with autodoc) are in another directory, 21 | # add these directories to sys.path here. If the directory is relative to the 22 | # documentation root, use os.path.abspath to make it absolute, like shown here. 23 | #sys.path.insert(0, os.path.abspath('.')) 24 | 25 | # -- General configuration ----------------------------------------------------- 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | #needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be extensions 31 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 32 | extensions = [] 33 | 34 | # Add any paths that contain templates here, relative to this directory. 35 | templates_path = ['_templates'] 36 | 37 | # The suffix of source filenames. 38 | source_suffix = '.rst' 39 | 40 | # The encoding of source files. 41 | #source_encoding = 'utf-8-sig' 42 | 43 | # The master toctree document. 44 | master_doc = 'index' 45 | 46 | # General information about the project. 47 | project = 'BorgWeb - Web User Interface for BorgBackup' 48 | copyright = '2015-2016 The Borg Collective (see AUTHORS)' 49 | 50 | # The version info for the project you're documenting, acts as replacement for 51 | # |version| and |release|, also used in various other places throughout the 52 | # built documents. 53 | # 54 | # The short X.Y version. 55 | version = sw_version.split('-')[0] 56 | # The full version, including alpha/beta/rc tags. 57 | release = version 58 | 59 | # The language for content autogenerated by Sphinx. Refer to documentation 60 | # for a list of supported languages. 61 | #language = None 62 | 63 | # There are two options for replacing |today|: either, you set today to some 64 | # non-false value, then it is used: 65 | #today = '' 66 | # Else, today_fmt is used as the format for a strftime call. 67 | today_fmt = '%Y-%m-%d' 68 | 69 | # List of patterns, relative to source directory, that match files and 70 | # directories to ignore when looking for source files. 71 | exclude_patterns = ['_build'] 72 | 73 | # The reST default role (used for this markup: `text`) to use for all documents. 74 | #default_role = None 75 | 76 | # If true, '()' will be appended to :func: etc. cross-reference text. 77 | #add_function_parentheses = True 78 | 79 | # If true, the current module name will be prepended to all description 80 | # unit titles (such as .. function::). 81 | #add_module_names = True 82 | 83 | # If true, sectionauthor and moduleauthor directives will be shown in the 84 | # output. They are ignored by default. 85 | #show_authors = False 86 | 87 | # The name of the Pygments (syntax highlighting) style to use. 88 | pygments_style = 'sphinx' 89 | 90 | # A list of ignored prefixes for module index sorting. 91 | #modindex_common_prefix = [] 92 | 93 | 94 | # -- Options for HTML output --------------------------------------------------- 95 | 96 | # The theme to use for HTML and HTML Help pages. See the documentation for 97 | # a list of builtin themes. 98 | #html_theme = '' 99 | if not on_rtd: # only import and set the theme if we're building docs locally 100 | import sphinx_rtd_theme 101 | html_theme = 'sphinx_rtd_theme' 102 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 103 | html_style = 'css/borg.css' 104 | else: 105 | html_context = { 106 | 'css_files': [ 107 | 'https://media.readthedocs.org/css/sphinx_rtd_theme.css', 108 | 'https://media.readthedocs.org/css/readthedocs-doc-embed.css', 109 | '_static/css/borg.css', 110 | ], 111 | } 112 | 113 | # Theme options are theme-specific and customize the look and feel of a theme 114 | # further. For a list of options available for each theme, see the 115 | # documentation. 116 | #html_theme_options = {} 117 | 118 | # Add any paths that contain custom themes here, relative to this directory. 119 | #html_theme_path = ['_themes'] 120 | 121 | # The name for this set of Sphinx documents. If None, it defaults to 122 | # " v documentation". 123 | #html_title = None 124 | 125 | # A shorter title for the navigation bar. Default is the same as html_title. 126 | #html_short_title = None 127 | 128 | # The name of an image file (relative to this directory) to place at the top 129 | # of the sidebar. 130 | html_logo = '_static/logo.png' 131 | 132 | # The name of an image file (within the static path) to use as favicon of the 133 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 134 | # pixels large. 135 | html_favicon = '_static/favicon.ico' 136 | 137 | # Add any paths that contain custom static files (such as style sheets) here, 138 | # relative to this directory. They are copied after the builtin static files, 139 | # so a file named "default.css" will overwrite the builtin "default.css". 140 | html_static_path = ['borg_theme'] 141 | 142 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 143 | # using the given strftime format. 144 | html_last_updated_fmt = '%Y-%m-%d' 145 | 146 | # If true, SmartyPants will be used to convert quotes and dashes to 147 | # typographically correct entities. 148 | #html_use_smartypants = True 149 | 150 | # Custom sidebar templates, maps document names to template names. 151 | html_sidebars = { 152 | 'index': ['sidebarlogo.html', 'sidebarusefullinks.html', 'searchbox.html'], 153 | '**': ['sidebarlogo.html', 'localtoc.html', 'relations.html', 'sidebarusefullinks.html', 'searchbox.html'] 154 | } 155 | # Additional templates that should be rendered to pages, maps page names to 156 | # template names. 157 | #html_additional_pages = {} 158 | 159 | # If false, no module index is generated. 160 | #html_domain_indices = True 161 | 162 | # If false, no index is generated. 163 | html_use_index = False 164 | 165 | # If true, the index is split into individual pages for each letter. 166 | #html_split_index = False 167 | 168 | # If true, links to the reST sources are added to the pages. 169 | html_show_sourcelink = False 170 | 171 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 172 | html_show_sphinx = False 173 | 174 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 175 | html_show_copyright = False 176 | 177 | # If true, an OpenSearch description file will be output, and all pages will 178 | # contain a tag referring to it. The value of this option must be the 179 | # base URL from which the finished HTML is served. 180 | #html_use_opensearch = '' 181 | 182 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 183 | #html_file_suffix = None 184 | 185 | # Output file base name for HTML help builder. 186 | htmlhelp_basename = 'borgwebdoc' 187 | 188 | 189 | # -- Options for LaTeX output -------------------------------------------------- 190 | 191 | # The paper size ('letter' or 'a4'). 192 | #latex_paper_size = 'letter' 193 | 194 | # The font size ('10pt', '11pt' or '12pt'). 195 | #latex_font_size = '10pt' 196 | 197 | # Grouping the document tree into LaTeX files. List of tuples 198 | # (source start file, target name, title, author, documentclass [howto/manual]). 199 | latex_documents = [ 200 | ('index', 'BorgWeb.tex', 'BorgWeb Documentation', 201 | 'see "AUTHORS" file', 'manual'), 202 | ] 203 | 204 | # The name of an image file (relative to this directory) to place at the top of 205 | # the title page. 206 | #latex_logo = None 207 | 208 | # For "manual" documents, if this is true, then toplevel headings are parts, 209 | # not chapters. 210 | #latex_use_parts = False 211 | 212 | # If true, show page references after internal links. 213 | #latex_show_pagerefs = False 214 | 215 | # If true, show URL addresses after external links. 216 | #latex_show_urls = False 217 | 218 | # Additional stuff for the LaTeX preamble. 219 | #latex_preamble = '' 220 | 221 | # Documents to append as an appendix to all manuals. 222 | #latex_appendices = [] 223 | 224 | # If false, no module index is generated. 225 | #latex_domain_indices = True 226 | 227 | 228 | # -- Options for manual page output -------------------------------------------- 229 | 230 | # One entry per manual page. List of tuples 231 | # (source start file, name, description, authors, manual section). 232 | #man_pages = [ 233 | # ('man', 'borg', 'Borg', 234 | # ['see "AUTHORS" file'], 1) 235 | #] 236 | 237 | extensions = ['sphinx.ext.extlinks'] 238 | 239 | extlinks = { 240 | 'issue': ('https://github.com/borgbackup/borgweb/issues/%s', '#'), 241 | 'targz_url': ('https://pypi.python.org/packages/source/b/borgweb/%%s-%s.tar.gz' % version, None), 242 | } 243 | -------------------------------------------------------------------------------- /borgweb/static/bootstrap/bootstrap-theme.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.5 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */.btn-danger,.btn-default,.btn-info,.btn-primary,.btn-success,.btn-warning{text-shadow:0 -1px 0 rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075)}.btn-danger.active,.btn-danger:active,.btn-default.active,.btn-default:active,.btn-info.active,.btn-info:active,.btn-primary.active,.btn-primary:active,.btn-success.active,.btn-success:active,.btn-warning.active,.btn-warning:active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-danger.disabled,.btn-danger[disabled],.btn-default.disabled,.btn-default[disabled],.btn-info.disabled,.btn-info[disabled],.btn-primary.disabled,.btn-primary[disabled],.btn-success.disabled,.btn-success[disabled],.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-danger,fieldset[disabled] .btn-default,fieldset[disabled] .btn-info,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-success,fieldset[disabled] .btn-warning{-webkit-box-shadow:none;box-shadow:none}.btn-danger .badge,.btn-default .badge,.btn-info .badge,.btn-primary .badge,.btn-success .badge,.btn-warning .badge{text-shadow:none}.btn.active,.btn:active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-o-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e0e0e0));background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#dbdbdb;border-color:#ccc}.btn-default:focus,.btn-default:hover{background-color:#e0e0e0;background-position:0 -15px}.btn-default.active,.btn-default:active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-default.disabled,.btn-default.disabled.active,.btn-default.disabled.focus,.btn-default.disabled:active,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled],.btn-default[disabled].active,.btn-default[disabled].focus,.btn-default[disabled]:active,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default,fieldset[disabled] .btn-default.active,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:active,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#e0e0e0;background-image:none}.btn-primary{background-image:-webkit-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-o-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#265a88));background-image:linear-gradient(to bottom,#337ab7 0,#265a88 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#245580}.btn-primary:focus,.btn-primary:hover{background-color:#265a88;background-position:0 -15px}.btn-primary.active,.btn-primary:active{background-color:#265a88;border-color:#245580}.btn-primary.disabled,.btn-primary.disabled.active,.btn-primary.disabled.focus,.btn-primary.disabled:active,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled],.btn-primary[disabled].active,.btn-primary[disabled].focus,.btn-primary[disabled]:active,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-primary.active,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:active,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#265a88;background-image:none}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#419641));background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:focus,.btn-success:hover{background-color:#419641;background-position:0 -15px}.btn-success.active,.btn-success:active{background-color:#419641;border-color:#3e8f3e}.btn-success.disabled,.btn-success.disabled.active,.btn-success.disabled.focus,.btn-success.disabled:active,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled],.btn-success[disabled].active,.btn-success[disabled].focus,.btn-success[disabled]:active,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success,fieldset[disabled] .btn-success.active,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:active,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#419641;background-image:none}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#2aabd2));background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:focus,.btn-info:hover{background-color:#2aabd2;background-position:0 -15px}.btn-info.active,.btn-info:active{background-color:#2aabd2;border-color:#28a4c9}.btn-info.disabled,.btn-info.disabled.active,.btn-info.disabled.focus,.btn-info.disabled:active,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled],.btn-info[disabled].active,.btn-info[disabled].focus,.btn-info[disabled]:active,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info,fieldset[disabled] .btn-info.active,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:active,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#2aabd2;background-image:none}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#eb9316));background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:focus,.btn-warning:hover{background-color:#eb9316;background-position:0 -15px}.btn-warning.active,.btn-warning:active{background-color:#eb9316;border-color:#e38d13}.btn-warning.disabled,.btn-warning.disabled.active,.btn-warning.disabled.focus,.btn-warning.disabled:active,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled],.btn-warning[disabled].active,.btn-warning[disabled].focus,.btn-warning[disabled]:active,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning,fieldset[disabled] .btn-warning.active,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:active,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#eb9316;background-image:none}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c12e2a));background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:focus,.btn-danger:hover{background-color:#c12e2a;background-position:0 -15px}.btn-danger.active,.btn-danger:active{background-color:#c12e2a;border-color:#b92c28}.btn-danger.disabled,.btn-danger.disabled.active,.btn-danger.disabled.focus,.btn-danger.disabled:active,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled],.btn-danger[disabled].active,.btn-danger[disabled].focus,.btn-danger[disabled]:active,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger,fieldset[disabled] .btn-danger.active,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:active,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#c12e2a;background-image:none}.img-thumbnail,.thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{background-color:#e8e8e8;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{background-color:#2e6da4;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-o-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#f8f8f8));background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075)}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-o-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dbdbdb),to(#e2e2e2));background-image:linear-gradient(to bottom,#dbdbdb 0,#e2e2e2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.075);box-shadow:inset 0 3px 9px rgba(0,0,0,.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-o-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#3c3c3c),to(#222));background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-o-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#080808),to(#0f0f0f));background-image:linear-gradient(to bottom,#080808 0,#0f0f0f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.25);box-shadow:inset 0 3px 9px rgba(0,0,0,.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,.25)}.navbar-fixed-bottom,.navbar-fixed-top,.navbar-static-top{border-radius:0}@media (max-width:767px){.navbar .navbar-nav .open .dropdown-menu>.active>a,.navbar .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}}.alert{text-shadow:0 1px 0 rgba(255,255,255,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#c8e5bc));background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);background-repeat:repeat-x;border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#b9def0));background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);background-repeat:repeat-x;border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#f8efc0));background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);background-repeat:repeat-x;border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-o-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#e7c3c3));background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);background-repeat:repeat-x;border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f5f5f5));background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x}.progress-bar{background-image:-webkit-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-o-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#286090));background-image:linear-gradient(to bottom,#337ab7 0,#286090 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);background-repeat:repeat-x}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#449d44));background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);background-repeat:repeat-x}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#31b0d5));background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);background-repeat:repeat-x}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#ec971f));background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);background-repeat:repeat-x}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c9302c));background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);background-repeat:repeat-x}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{text-shadow:0 -1px 0 #286090;background-image:-webkit-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2b669a));background-image:linear-gradient(to bottom,#337ab7 0,#2b669a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);background-repeat:repeat-x;border-color:#2b669a}.list-group-item.active .badge,.list-group-item.active:focus .badge,.list-group-item.active:hover .badge{text-shadow:none}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.05);box-shadow:0 1px 2px rgba(0,0,0,.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#d0e9c6));background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);background-repeat:repeat-x}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#c4e3f3));background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);background-repeat:repeat-x}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#faf2cc));background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);background-repeat:repeat-x}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-o-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#ebcccc));background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);background-repeat:repeat-x}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#e8e8e8),to(#f5f5f5));background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x;border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1)} -------------------------------------------------------------------------------- /borgweb/static/fonts/glyphicons-halflings-regular.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | --------------------------------------------------------------------------------