1
0
mirror of https://github.com/moparisthebest/SickRage synced 2024-12-13 11:32:20 -05:00

Added backup and restore feature, this allows you to backup your config.ini and sickbeard.db files into a zipfile and save it to a destination of your choice and as well you can restore the same zip file later on then perform a restart to have the changes take affect automatically. Backups are saved date/time stamped.

This commit is contained in:
echel0n 2014-06-19 07:31:44 -07:00
parent ddbcfb40ba
commit 2b7df8e67d
8 changed files with 268 additions and 36 deletions

View File

@ -21,6 +21,7 @@
from __future__ import with_statement from __future__ import with_statement
import sys import sys
import shutil
if sys.version_info < (2, 6): if sys.version_info < (2, 6):
print "Sorry, requires Python 2.6 or 2.7." print "Sorry, requires Python 2.6 or 2.7."
@ -133,6 +134,20 @@ def daemonize():
dev_null = file('/dev/null', 'r') dev_null = file('/dev/null', 'r')
os.dup2(dev_null.fileno(), sys.stdin.fileno()) os.dup2(dev_null.fileno(), sys.stdin.fileno())
def restore(srcDir, dstDir):
try:
for file in os.listdir(srcDir):
srcFile = os.path.join(srcDir, file)
dstFile = os.path.join(dstDir, file)
bakFile = os.path.join(dstDir, file + '.bak')
shutil.move(dstFile, bakFile)
shutil.move(srcFile, dstFile)
os.rmdir(srcDir)
return True
except:
return False
def main(): def main():
""" """
TV for me TV for me
@ -177,6 +192,14 @@ def main():
# Rename the main thread # Rename the main thread
threading.currentThread().name = "MAIN" threading.currentThread().name = "MAIN"
# Check if we need to perform a restore first
restoreDir = os.path.join(sickbeard.PROG_DIR, 'restore')
if os.path.exists(restoreDir):
if restore(restoreDir, sickbeard.PROG_DIR):
print "Restore successful..."
else:
print "Restore FAILED!"
try: try:
opts, args = getopt.getopt(sys.argv[1:], "qfdp::", opts, args = getopt.getopt(sys.argv[1:], "qfdp::",
['quiet', 'forceupdate', 'daemon', 'port=', 'pidfile=', 'nolaunch', 'config=', ['quiet', 'forceupdate', 'daemon', 'port=', 'pidfile=', 'nolaunch', 'config=',

View File

@ -0,0 +1,83 @@
#import os.path
#import datetime
#import locale
#import sickbeard
#from sickbeard.common import *
#from sickbeard.sbdatetime import *
#from sickbeard import config
#from sickbeard import metadata
#from sickbeard.metadata.generic import GenericMetadata
#set global $title = "Config - Backup/Restore"
#set global $header = "Backup/Restore"
#set global $sbPath="../.."
#set global $topmenu="config"#
#include $os.path.join($sickbeard.PROG_DIR, "gui/slick/interfaces/default/inc_top.tmpl")
<script type="text/javascript" src="$sbRoot/js/configBackupRestore.js?$sbPID"></script>
#if $varExists('header')
<h1 class="header">$header</h1>
#else
<h1 class="title">$title</h1>
#end if
#set $indexer = 0
#if $sickbeard.INDEXER_DEFAULT
#set $indexer = $sickbeard.INDEXER_DEFAULT
#end if
<script type="text/javascript" src="$sbRoot/js/config.js?$sbPID"></script>
<div id="config">
<div id="config-content">
<form name="configForm" method="post" action="backuprestore" style="line-height: 44px">
<div id="config-components">
<ul>
<li><a href="#core-component-group1">Backup</a></li>
<li><a href="#core-component-group2">Restore</a></li>
</ul>
<div id="core-component-group1" class="component-group clearfix">
<div class="component-group-desc">
<h3>Backup</h3>
<p><b>Backup your main database file and config.</b></p>
</div>
<b>Select the folder you wish to save your backup file to:</b>
<br/>
<input type="text" name="backupDir" id="backupDir" size="50" /><br/>
<br/>
<div class="Backup" id="Backup-result"></div>
<input class="btn" type="button" value="Backup" id="Backup" />
</div><!-- /component-group1 //-->
<div id="core-component-group2" class="component-group clearfix">
<div class="component-group-desc">
<h3>Restore</h3>
<p><b>Restore your main database file and config.</b></p>
</div>
<b>Select the backup file you wish to restore:</b>
<br/>
<input type="text" name="backupFile" id="backupFile" size="50" /><br/>
<br/>
<div class="Restore" id="Restore-result"></div>
<input class="btn" type="button" value="Restore" id="Restore" />
</div><!-- /component-group2 //-->
</div><!-- /config-components -->
</form>
</div></div>
<div class="clearfix"></div>
<script type="text/javascript" charset="utf-8">
<!--
jQuery('#backupDir').fileBrowser({ title: 'Select backup folder to save to', key: 'backupPath' });
jQuery('#backupFile').fileBrowser({ title: 'Select backup files to restore', key: 'backupFile', includeFiles: 1 });
jQuery('#config-components').tabs();
//-->
</script>
#include $os.path.join($sickbeard.PROG_DIR,"gui/slick/interfaces/default/inc_bottom.tmpl")

View File

@ -161,6 +161,7 @@ a > i.icon-question-sign { background-image: url("$sbRoot/images/glyphicons-half
\$("#SubMenu a:contains('Anime')").addClass('btn').html('<span class="ui-icon ui-icon-anime pull-left"></span> Anime'); \$("#SubMenu a:contains('Anime')").addClass('btn').html('<span class="ui-icon ui-icon-anime pull-left"></span> Anime');
\$("#SubMenu a:contains('Settings')").addClass('btn').html('<span class="ui-icon ui-icon-search pull-left"></span> Search Settings'); \$("#SubMenu a:contains('Settings')").addClass('btn').html('<span class="ui-icon ui-icon-search pull-left"></span> Search Settings');
\$("#SubMenu a:contains('Provider')").addClass('btn').html('<span class="ui-icon ui-icon-search pull-left"></span> Search Providers'); \$("#SubMenu a:contains('Provider')").addClass('btn').html('<span class="ui-icon ui-icon-search pull-left"></span> Search Providers');
\$("#SubMenu a:contains('Backup/Restore')").addClass('btn').html('<span class="ui-icon ui-icon-gear pull-left"></span> Backup/Restore');
\$("#SubMenu a:contains('General')").addClass('btn').html('<span class="ui-icon ui-icon-gear pull-left"></span> General'); \$("#SubMenu a:contains('General')").addClass('btn').html('<span class="ui-icon ui-icon-gear pull-left"></span> General');
\$("#SubMenu a:contains('Episode Status')").addClass('btn').html('<span class="ui-icon ui-icon-transferthick-e-w pull-left"></span> Episode Status Management'); \$("#SubMenu a:contains('Episode Status')").addClass('btn').html('<span class="ui-icon ui-icon-transferthick-e-w pull-left"></span> Episode Status Management');
\$("#SubMenu a:contains('Missed Subtitle')").addClass('btn').html('<span class="ui-icon ui-icon-transferthick-e-w pull-left"></span> Missed Subtitles'); \$("#SubMenu a:contains('Missed Subtitle')").addClass('btn').html('<span class="ui-icon ui-icon-transferthick-e-w pull-left"></span> Missed Subtitles');
@ -272,6 +273,7 @@ a > i.icon-question-sign { background-image: url("$sbRoot/images/glyphicons-half
<ul> <ul>
<li><a href="$sbRoot/config/"><i class="icon-question-sign" style=" margin-left: -21px;margin-right: 8px;position: absolute;"></i>Help &amp; Info</a></li> <li><a href="$sbRoot/config/"><i class="icon-question-sign" style=" margin-left: -21px;margin-right: 8px;position: absolute;"></i>Help &amp; Info</a></li>
<li><a href="$sbRoot/config/general/"><img src="$sbRoot/images/menu/config16.png" alt="" width="16" height="16" />General</a></li> <li><a href="$sbRoot/config/general/"><img src="$sbRoot/images/menu/config16.png" alt="" width="16" height="16" />General</a></li>
<li><a href="$sbRoot/config/backuprestore/"><img src="$sbRoot/images/menu/config16.png" alt="" width="16" height="16" />Backup/Restore</a></li>
<li><a href="$sbRoot/config/search/"><img src="$sbRoot/images/menu/config16.png" alt="" width="16" height="16" />Search Settings</a></li> <li><a href="$sbRoot/config/search/"><img src="$sbRoot/images/menu/config16.png" alt="" width="16" height="16" />Search Settings</a></li>
<li><a href="$sbRoot/config/providers/"><img src="$sbRoot/images/menu/config16.png" alt="" width="16" height="16" />Search Providers</a></li> <li><a href="$sbRoot/config/providers/"><img src="$sbRoot/images/menu/config16.png" alt="" width="16" height="16" />Search Providers</a></li>
<li><a href="$sbRoot/config/subtitles/"><img src="$sbRoot/images/menu/config16.png" alt="" width="16" height="16" />Subtitles Settings</a></li> <li><a href="$sbRoot/config/subtitles/"><img src="$sbRoot/images/menu/config16.png" alt="" width="16" height="16" />Subtitles Settings</a></li>

View File

@ -5,13 +5,14 @@
defaults: { defaults: {
title: 'Choose Directory', title: 'Choose Directory',
url: sbRoot + '/browser/', url: sbRoot + '/browser/',
autocompleteURL: sbRoot + '/browser/complete' autocompleteURL: sbRoot + '/browser/complete',
includeFiles: 0
} }
}; };
var fileBrowserDialog, currentBrowserPath, currentRequest = null; var fileBrowserDialog, currentBrowserPath, currentRequest = null;
function browse(path, endpoint) { function browse(path, endpoint, includeFiles) {
if (currentBrowserPath == path) { if (currentBrowserPath == path) {
return; return;
@ -25,7 +26,7 @@
fileBrowserDialog.dialog('option', 'dialogClass', 'browserDialog busy'); fileBrowserDialog.dialog('option', 'dialogClass', 'browserDialog busy');
currentRequest = $.getJSON(endpoint, { path: path }, function (data) { currentRequest = $.getJSON(endpoint, { path: path, includeFiles: includeFiles }, function (data) {
fileBrowserDialog.empty(); fileBrowserDialog.empty();
var first_val = data[0]; var first_val = data[0];
var i = 0; var i = 0;
@ -36,7 +37,7 @@
$('<h2>').text(first_val.current_path).appendTo(fileBrowserDialog); $('<h2>').text(first_val.current_path).appendTo(fileBrowserDialog);
list = $('<ul>').appendTo(fileBrowserDialog); list = $('<ul>').appendTo(fileBrowserDialog);
$.each(data, function (i, entry) { $.each(data, function (i, entry) {
link = $("<a href='javascript:void(0)' />").click(function () { browse(entry.path, endpoint); }).text(entry.name); link = $("<a href='javascript:void(0)' />").click(function () { browse(entry.path, endpoint, includeFiles); }).text(entry.name);
$('<span class="ui-icon ui-icon-folder-collapsed"></span>').prependTo(link); $('<span class="ui-icon ui-icon-folder-collapsed"></span>').prependTo(link);
link.hover( link.hover(
function () {$("span", this).addClass("ui-icon-folder-open"); }, function () {$("span", this).addClass("ui-icon-folder-open"); },
@ -93,7 +94,8 @@
if (options.initialDir) { if (options.initialDir) {
initialDir = options.initialDir; initialDir = options.initialDir;
} }
browse(initialDir, options.url);
browse(initialDir, options.url, options.includeFiles);
fileBrowserDialog.dialog('open'); fileBrowserDialog.dialog('open');
return false; return false;
@ -110,7 +112,7 @@
position: { my : "top", at: "bottom", collision: "flipfit" }, position: { my : "top", at: "bottom", collision: "flipfit" },
source: function (request, response) { source: function (request, response) {
//keep track of user submitted search term //keep track of user submitted search term
query = $.ui.autocomplete.escapeRegex(request.term); query = $.ui.autocomplete.escapeRegex(request.term, options.includeFiles);
$.ajax({ $.ajax({
url: options.autocompleteURL, url: options.autocompleteURL,
data: request, data: request,

View File

@ -0,0 +1,24 @@
$(document).ready(function(){
var loading = '<img src="' + sbRoot + '/images/loading16.gif" height="16" width="16" />';
$('#Backup').click(function() {
$("#Backup").attr("disabled", true);
$('#Backup-result').html(loading);
var backupDir = $("#backupDir").val();
$.get(sbRoot + "/config/backup", {'backupDir': backupDir})
.done(function (data) {
$('#Backup-result').html(data);
$("#Backup").attr("disabled", false);
});
});
$('#Restore').click(function() {
$("#Restore").attr("disabled", true);
$('#Restore-result').html(loading);
var backupFile = $("#backupFile").val();
$.get(sbRoot + "/config/restore", {'backupFile': backupFile})
.done(function (data) {
$('#Restore-result').html(data);
$("#Restore").attr("disabled", false);
});
});
});

View File

@ -48,7 +48,7 @@ def getWinDrives():
return drives return drives
def foldersAtPath(path, includeParent=False): def foldersAtPath(path, includeParent=False, includeFiles=False):
""" Returns a list of dictionaries with the folders contained at the given path """ Returns a list of dictionaries with the folders contained at the given path
Give the empty string as the path to list the contents of the root path Give the empty string as the path to list the contents of the root path
under Unix this means "/", on Windows this will be a list of drive letters) under Unix this means "/", on Windows this will be a list of drive letters)
@ -81,7 +81,8 @@ def foldersAtPath(path, includeParent=False):
parentPath = "" parentPath = ""
fileList = [{'name': filename, 'path': ek.ek(os.path.join, path, filename)} for filename in ek.ek(os.listdir, path)] fileList = [{'name': filename, 'path': ek.ek(os.path.join, path, filename)} for filename in ek.ek(os.listdir, path)]
fileList = filter(lambda entry: ek.ek(os.path.isdir, entry['path']), fileList) if not includeFiles:
fileList = filter(lambda entry: ek.ek(os.path.isdir, entry['path']), fileList)
# prune out directories to proect the user from doing stupid things (already lower case the dir to reduce calls) # prune out directories to proect the user from doing stupid things (already lower case the dir to reduce calls)
hideList = ["boot", "bootmgr", "cache", "msocache", "recovery", "$recycle.bin", "recycler", hideList = ["boot", "bootmgr", "cache", "msocache", "recovery", "$recycle.bin", "recycler",
@ -101,11 +102,11 @@ def foldersAtPath(path, includeParent=False):
class WebFileBrowser(RequestHandler): class WebFileBrowser(RequestHandler):
def index(self, path='', *args, **kwargs): def index(self, path='', includeFiles=False, *args, **kwargs):
self.set_header("Content-Type", "application/json") self.set_header("Content-Type", "application/json")
return json.dumps(foldersAtPath(path, True)) return json.dumps(foldersAtPath(path, True, bool(int(includeFiles))))
def complete(self, term): def complete(self, term, includeFiles=0):
self.set_header("Content-Type", "application/json") self.set_header("Content-Type", "application/json")
paths = [entry['path'] for entry in foldersAtPath(os.path.dirname(term)) if 'path' in entry] paths = [entry['path'] for entry in foldersAtPath(os.path.dirname(term), includeFiles=bool(int(includeFiles))) if 'path' in entry]
return json.dumps(paths) return json.dumps(paths)

View File

@ -32,6 +32,7 @@ import urlparse
import uuid import uuid
import base64 import base64
import string import string
import zipfile
from lib import requests from lib import requests
from lib.requests import exceptions from lib.requests import exceptions
@ -529,7 +530,7 @@ def rename_ep_file(cur_path, new_path, old_path_length=0):
# Extract subtitle language from filename # Extract subtitle language from filename
sublang = os.path.splitext(cur_file_name)[1][1:] sublang = os.path.splitext(cur_file_name)[1][1:]
#Check if the language extracted from filename is a valid language # Check if the language extracted from filename is a valid language
try: try:
language = subliminal.language.Language(sublang, strict=True) language = subliminal.language.Language(sublang, strict=True)
cur_file_ext = '.' + sublang + cur_file_ext cur_file_ext = '.' + sublang + cur_file_ext
@ -679,6 +680,7 @@ def is_anime_in_show_list():
def update_anime_support(): def update_anime_support():
sickbeard.ANIMESUPPORT = is_anime_in_show_list() sickbeard.ANIMESUPPORT = is_anime_in_show_list()
def get_absolute_number_from_season_and_episode(show, season, episode): def get_absolute_number_from_season_and_episode(show, season, episode):
with db.DBConnection() as myDB: with db.DBConnection() as myDB:
sql = "SELECT * FROM tv_episodes WHERE showid = ? and season = ? and episode = ?" sql = "SELECT * FROM tv_episodes WHERE showid = ? and season = ? and episode = ?"
@ -692,10 +694,12 @@ def get_absolute_number_from_season_and_episode(show, season, episode):
return absolute_number return absolute_number
else: else:
logger.log( logger.log(
"No entries for absolute number in show: " + show.name + " found using " + str(season) + "x" + str(episode),logger.DEBUG) "No entries for absolute number in show: " + show.name + " found using " + str(season) + "x" + str(episode),
logger.DEBUG)
return None return None
def get_all_episodes_from_absolute_number(show, indexer_id, absolute_numbers): def get_all_episodes_from_absolute_number(show, indexer_id, absolute_numbers):
if len(absolute_numbers) == 0: if len(absolute_numbers) == 0:
raise EpisodeNotFoundByAbsoluteNumberException() raise EpisodeNotFoundByAbsoluteNumberException()
@ -885,6 +889,7 @@ def backupVersionedFile(old_file, version):
return True return True
def restoreVersionedFile(backup_file, version): def restoreVersionedFile(backup_file, version):
numTries = 0 numTries = 0
@ -896,10 +901,14 @@ def restoreVersionedFile(backup_file, version):
return False return False
try: try:
logger.log(u"Trying to backup " + new_file + " to " + new_file + "." + "r" + str(version) + " before restoring backup", logger.DEBUG) logger.log(
u"Trying to backup " + new_file + " to " + new_file + "." + "r" + str(version) + " before restoring backup",
logger.DEBUG)
shutil.move(new_file, new_file + '.' + 'r' + str(version)) shutil.move(new_file, new_file + '.' + 'r' + str(version))
except Exception, e: except Exception, e:
logger.log(u"Error while trying to backup DB file " + restore_file + " before proceeding with restore: " + ex(e), logger.WARNING) logger.log(
u"Error while trying to backup DB file " + restore_file + " before proceeding with restore: " + ex(e),
logger.WARNING)
return False return False
while not ek.ek(os.path.isfile, new_file): while not ek.ek(os.path.isfile, new_file):
@ -919,11 +928,13 @@ def restoreVersionedFile(backup_file, version):
logger.log(u"Trying again.", logger.DEBUG) logger.log(u"Trying again.", logger.DEBUG)
if numTries >= 10: if numTries >= 10:
logger.log(u"Unable to restore " + restore_file + " to " + new_file + " please do it manually.", logger.ERROR) logger.log(u"Unable to restore " + restore_file + " to " + new_file + " please do it manually.",
logger.ERROR)
return False return False
return True return True
# try to convert to int, if it fails the default will be returned # try to convert to int, if it fails the default will be returned
def tryInt(s, s_default=0): def tryInt(s, s_default=0):
try: try:
@ -1044,7 +1055,6 @@ def full_sanitizeSceneName(name):
def _check_against_names(nameInQuestion, show, season=-1): def _check_against_names(nameInQuestion, show, season=-1):
showNames = [] showNames = []
if season in [-1, 1]: if season in [-1, 1]:
showNames = [show.name] showNames = [show.name]
@ -1069,7 +1079,8 @@ def get_show_by_name(name, useIndexer=False):
return showObj return showObj
if not showObj and sickbeard.showList: if not showObj and sickbeard.showList:
if name in sickbeard.scene_exceptions.exceptionIndexerCache: if name in sickbeard.scene_exceptions.exceptionIndexerCache:
showObj = findCertainShow(sickbeard.showList, int(sickbeard.scene_exceptions.exceptionIndexerCache[name])) showObj = findCertainShow(sickbeard.showList,
int(sickbeard.scene_exceptions.exceptionIndexerCache[name]))
if useIndexer and not showObj: if useIndexer and not showObj:
(sn, idx, id) = searchIndexerForShowID(name, ui=classes.ShowListUI) (sn, idx, id) = searchIndexerForShowID(name, ui=classes.ShowListUI)
@ -1084,6 +1095,7 @@ def get_show_by_name(name, useIndexer=False):
return showObj return showObj
def is_hidden_folder(folder): def is_hidden_folder(folder):
""" """
Returns True if folder is hidden. Returns True if folder is hidden.
@ -1144,4 +1156,45 @@ def set_up_anidb_connection():
else: else:
return True return True
return sickbeard.ADBA_CONNECTION.authed() return sickbeard.ADBA_CONNECTION.authed()
def makeZip(fileList, archive):
"""
'fileList' is a list of file names - full path each name
'archive' is the file name for the archive with a full path
"""
try:
a = zipfile.ZipFile(archive, 'w', zipfile.ZIP_DEFLATED)
for f in fileList:
a.write(f)
a.close()
return True
except:
return False
def extractZip(archive, targetDir):
"""
'fileList' is a list of file names - full path each name
'archive' is the file name for the archive with a full path
"""
try:
if not os.path.exists(targetDir):
os.mkdir(targetDir)
with zipfile.ZipFile(archive) as zip_file:
for member in zip_file.namelist():
filename = os.path.basename(member)
# skip directories
if not filename:
continue
# copy file (taken from zipfile's extract)
source = zip_file.open(member)
target = file(os.path.join(targetDir, filename), "wb")
with source, target:
shutil.copyfileobj(source, target)
return True
except:
return False

View File

@ -18,13 +18,12 @@
from __future__ import with_statement from __future__ import with_statement
import base64 import base64
import functools
import inspect import inspect
import zipfile
import os.path import os.path
import time import time
import traceback
import urllib import urllib
import re import re
import threading import threading
@ -84,6 +83,8 @@ from tornado import gen
from tornado.web import RequestHandler, HTTPError, asynchronous from tornado.web import RequestHandler, HTTPError, asynchronous
req_headers = None req_headers = None
def authenticated(handler_class): def authenticated(handler_class):
def wrap_execute(handler_execute): def wrap_execute(handler_execute):
def basicauth(handler, transforms, *args, **kwargs): def basicauth(handler, transforms, *args, **kwargs):
@ -98,7 +99,7 @@ def authenticated(handler_class):
if not (sickbeard.WEB_USERNAME and sickbeard.WEB_PASSWORD): if not (sickbeard.WEB_USERNAME and sickbeard.WEB_PASSWORD):
return True return True
elif handler.request.uri.startswith('/calendar') or ( elif handler.request.uri.startswith('/calendar') or (
handler.request.uri.startswith('/api') and '/api/builder' not in handler.request.uri): handler.request.uri.startswith('/api') and '/api/builder' not in handler.request.uri):
return True return True
auth_hdr = handler.request.headers.get('Authorization') auth_hdr = handler.request.headers.get('Authorization')
@ -205,7 +206,7 @@ class MainHandler(RequestHandler):
return func(**args) return func(**args)
raise HTTPError(404) raise HTTPError(404)
def redirect(self, url, permanent=False, status=None): def redirect(self, url, permanent=False, status=None):
self._transforms = [] self._transforms = []
super(MainHandler, self).redirect(sickbeard.WEB_ROOT + url, permanent, status) super(MainHandler, self).redirect(sickbeard.WEB_ROOT + url, permanent, status)
@ -456,6 +457,7 @@ class MainHandler(RequestHandler):
browser = WebFileBrowser browser = WebFileBrowser
class PageTemplate(Template): class PageTemplate(Template):
def __init__(self, *args, **KWs): def __init__(self, *args, **KWs):
KWs['file'] = os.path.join(sickbeard.PROG_DIR, "gui/" + sickbeard.GUI_NAME + "/interfaces/default/", KWs['file'] = os.path.join(sickbeard.PROG_DIR, "gui/" + sickbeard.GUI_NAME + "/interfaces/default/",
@ -728,7 +730,8 @@ class Manage(MainHandler):
all_eps = [str(x["season"]) + 'x' + str(x["episode"]) for x in all_eps_results] all_eps = [str(x["season"]) + 'x' + str(x["episode"]) for x in all_eps_results]
to_change[cur_indexer_id] = all_eps to_change[cur_indexer_id] = all_eps
Home(self.application, self.request).setStatus(cur_indexer_id, '|'.join(to_change[cur_indexer_id]), newStatus, direct=True) Home(self.application, self.request).setStatus(cur_indexer_id, '|'.join(to_change[cur_indexer_id]),
newStatus, direct=True)
return self.redirect('/manage/episodeStatuses/') return self.redirect('/manage/episodeStatuses/')
@ -1319,6 +1322,7 @@ class History(MainHandler):
ConfigMenu = [ ConfigMenu = [
{'title': 'General', 'path': 'config/general/'}, {'title': 'General', 'path': 'config/general/'},
{'title': 'Backup/Restore', 'path': 'config/backuprestore/'},
{'title': 'Search Settings', 'path': 'config/search/'}, {'title': 'Search Settings', 'path': 'config/search/'},
{'title': 'Search Providers', 'path': 'config/providers/'}, {'title': 'Search Providers', 'path': 'config/providers/'},
{'title': 'Subtitles Settings', 'path': 'config/subtitles/'}, {'title': 'Subtitles Settings', 'path': 'config/subtitles/'},
@ -1474,6 +1478,55 @@ class ConfigGeneral(MainHandler):
ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE)) ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE))
class ConfigBackupRestore(MainHandler):
def index(self, *args, **kwargs):
t = PageTemplate(file="config_backuprestore.tmpl")
t.submenu = ConfigMenu
return _munge(t)
def backup(self, backupDir=None):
self.set_header('Cache-Control', "max-age=0,no-cache,no-store")
finalResult = ''
if backupDir:
source = [os.path.join(sickbeard.PROG_DIR, 'sickbeard.db'), os.path.join(sickbeard.PROG_DIR, 'config.ini')]
target = os.path.join(backupDir, 'sickrage-' + time.strftime('%Y%m%d%H%M%S') + '.zip')
if helpers.makeZip(source, target):
finalResult += "Successful backup to " + target
else:
finalResult += "Backup FAILED"
else:
finalResult += "You need to choose a folder to save your backup to!"
finalResult += "<br />\n"
return finalResult
def restore(self, backupFile=None):
self.set_header('Cache-Control', "max-age=0,no-cache,no-store")
finalResult = ''
if backupFile:
source = backupFile
target_dir = os.path.join(sickbeard.PROG_DIR, 'restore')
if helpers.extractZip(source, target_dir):
finalResult += "Successfully extracted restore files to " + target_dir
finalResult += "<br>Restart sickrage to complete the restore."
else:
finalResult += "Restore FAILED"
else:
finalResult += "You need to select a backup file to restore!"
finalResult += "<br />\n"
return finalResult
class ConfigSearch(MainHandler): class ConfigSearch(MainHandler):
def index(self, *args, **kwargs): def index(self, *args, **kwargs):
@ -1558,7 +1611,6 @@ class ConfigSearch(MainHandler):
class ConfigPostProcessing(MainHandler): class ConfigPostProcessing(MainHandler):
def index(self, *args, **kwargs): def index(self, *args, **kwargs):
t = PageTemplate(file="config_postProcessing.tmpl") t = PageTemplate(file="config_postProcessing.tmpl")
@ -1730,7 +1782,6 @@ class ConfigPostProcessing(MainHandler):
class ConfigProviders(MainHandler): class ConfigProviders(MainHandler):
def index(self, *args, **kwargs): def index(self, *args, **kwargs):
t = PageTemplate(file="config_providers.tmpl") t = PageTemplate(file="config_providers.tmpl")
t.submenu = ConfigMenu t.submenu = ConfigMenu
@ -2101,7 +2152,6 @@ class ConfigProviders(MainHandler):
class ConfigNotifications(MainHandler): class ConfigNotifications(MainHandler):
def index(self, *args, **kwargs): def index(self, *args, **kwargs):
t = PageTemplate(file="config_notifications.tmpl") t = PageTemplate(file="config_notifications.tmpl")
t.submenu = ConfigMenu t.submenu = ConfigMenu
@ -2303,7 +2353,6 @@ class ConfigNotifications(MainHandler):
class ConfigSubtitles(MainHandler): class ConfigSubtitles(MainHandler):
def index(self, *args, **kwargs): def index(self, *args, **kwargs):
t = PageTemplate(file="config_subtitles.tmpl") t = PageTemplate(file="config_subtitles.tmpl")
t.submenu = ConfigMenu t.submenu = ConfigMenu
@ -2361,7 +2410,6 @@ class ConfigSubtitles(MainHandler):
class ConfigAnime(MainHandler): class ConfigAnime(MainHandler):
def index(self, *args, **kwargs): def index(self, *args, **kwargs):
t = PageTemplate(file="config_anime.tmpl") t = PageTemplate(file="config_anime.tmpl")
@ -2407,7 +2455,6 @@ class ConfigAnime(MainHandler):
class Config(MainHandler): class Config(MainHandler):
def index(self, *args, **kwargs): def index(self, *args, **kwargs):
t = PageTemplate(file="config.tmpl") t = PageTemplate(file="config.tmpl")
t.submenu = ConfigMenu t.submenu = ConfigMenu
@ -2416,6 +2463,7 @@ class Config(MainHandler):
# map class names to urls # map class names to urls
general = ConfigGeneral general = ConfigGeneral
backuprestore = ConfigBackupRestore
search = ConfigSearch search = ConfigSearch
providers = ConfigProviders providers = ConfigProviders
subtitles = ConfigSubtitles subtitles = ConfigSubtitles
@ -2454,7 +2502,6 @@ def HomeMenu():
class HomePostProcess(MainHandler): class HomePostProcess(MainHandler):
def index(self, *args, **kwargs): def index(self, *args, **kwargs):
t = PageTemplate(file="home_postprocess.tmpl") t = PageTemplate(file="home_postprocess.tmpl")
@ -2501,7 +2548,6 @@ class HomePostProcess(MainHandler):
class NewHomeAddShows(MainHandler): class NewHomeAddShows(MainHandler):
def index(self, *args, **kwargs): def index(self, *args, **kwargs):
t = PageTemplate(file="home_addShows.tmpl") t = PageTemplate(file="home_addShows.tmpl")
@ -2887,7 +2933,6 @@ ErrorLogsMenu = [
class ErrorLogs(MainHandler): class ErrorLogs(MainHandler):
def index(self, *args, **kwargs): def index(self, *args, **kwargs):
t = PageTemplate(file="errorlogs.tmpl") t = PageTemplate(file="errorlogs.tmpl")
@ -2956,7 +3001,6 @@ class ErrorLogs(MainHandler):
class Home(MainHandler): class Home(MainHandler):
def is_alive(self, *args, **kwargs): def is_alive(self, *args, **kwargs):
if 'callback' in kwargs and '_' in kwargs: if 'callback' in kwargs and '_' in kwargs:
callback, _ = kwargs['callback'], kwargs['_'] callback, _ = kwargs['callback'], kwargs['_']