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
import sys
import shutil
if sys.version_info < (2, 6):
print "Sorry, requires Python 2.6 or 2.7."
@ -133,6 +134,20 @@ def daemonize():
dev_null = file('/dev/null', 'r')
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():
"""
TV for me
@ -177,6 +192,14 @@ def main():
# Rename the main thread
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:
opts, args = getopt.getopt(sys.argv[1:], "qfdp::",
['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('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('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('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');
@ -272,6 +273,7 @@ a > i.icon-question-sign { background-image: url("$sbRoot/images/glyphicons-half
<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/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/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>

View File

@ -5,13 +5,14 @@
defaults: {
title: 'Choose Directory',
url: sbRoot + '/browser/',
autocompleteURL: sbRoot + '/browser/complete'
autocompleteURL: sbRoot + '/browser/complete',
includeFiles: 0
}
};
var fileBrowserDialog, currentBrowserPath, currentRequest = null;
function browse(path, endpoint) {
function browse(path, endpoint, includeFiles) {
if (currentBrowserPath == path) {
return;
@ -25,7 +26,7 @@
fileBrowserDialog.dialog('option', 'dialogClass', 'browserDialog busy');
currentRequest = $.getJSON(endpoint, { path: path }, function (data) {
currentRequest = $.getJSON(endpoint, { path: path, includeFiles: includeFiles }, function (data) {
fileBrowserDialog.empty();
var first_val = data[0];
var i = 0;
@ -36,7 +37,7 @@
$('<h2>').text(first_val.current_path).appendTo(fileBrowserDialog);
list = $('<ul>').appendTo(fileBrowserDialog);
$.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);
link.hover(
function () {$("span", this).addClass("ui-icon-folder-open"); },
@ -93,7 +94,8 @@
if (options.initialDir) {
initialDir = options.initialDir;
}
browse(initialDir, options.url);
browse(initialDir, options.url, options.includeFiles);
fileBrowserDialog.dialog('open');
return false;
@ -110,7 +112,7 @@
position: { my : "top", at: "bottom", collision: "flipfit" },
source: function (request, response) {
//keep track of user submitted search term
query = $.ui.autocomplete.escapeRegex(request.term);
query = $.ui.autocomplete.escapeRegex(request.term, options.includeFiles);
$.ajax({
url: options.autocompleteURL,
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
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
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)
@ -81,7 +81,8 @@ def foldersAtPath(path, includeParent=False):
parentPath = ""
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)
hideList = ["boot", "bootmgr", "cache", "msocache", "recovery", "$recycle.bin", "recycler",
@ -101,11 +102,11 @@ def foldersAtPath(path, includeParent=False):
class WebFileBrowser(RequestHandler):
def index(self, path='', *args, **kwargs):
def index(self, path='', includeFiles=False, *args, **kwargs):
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")
paths = [entry['path'] for entry in foldersAtPath(os.path.dirname(term)) if 'path' in entry]
return json.dumps(paths)
paths = [entry['path'] for entry in foldersAtPath(os.path.dirname(term), includeFiles=bool(int(includeFiles))) if 'path' in entry]
return json.dumps(paths)

View File

@ -32,6 +32,7 @@ import urlparse
import uuid
import base64
import string
import zipfile
from lib import requests
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
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:
language = subliminal.language.Language(sublang, strict=True)
cur_file_ext = '.' + sublang + cur_file_ext
@ -679,6 +680,7 @@ def is_anime_in_show_list():
def update_anime_support():
sickbeard.ANIMESUPPORT = is_anime_in_show_list()
def get_absolute_number_from_season_and_episode(show, season, episode):
with db.DBConnection() as myDB:
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
else:
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
def get_all_episodes_from_absolute_number(show, indexer_id, absolute_numbers):
if len(absolute_numbers) == 0:
raise EpisodeNotFoundByAbsoluteNumberException()
@ -885,6 +889,7 @@ def backupVersionedFile(old_file, version):
return True
def restoreVersionedFile(backup_file, version):
numTries = 0
@ -896,10 +901,14 @@ def restoreVersionedFile(backup_file, version):
return False
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))
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
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)
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 True
# try to convert to int, if it fails the default will be returned
def tryInt(s, s_default=0):
try:
@ -1044,7 +1055,6 @@ def full_sanitizeSceneName(name):
def _check_against_names(nameInQuestion, show, season=-1):
showNames = []
if season in [-1, 1]:
showNames = [show.name]
@ -1069,7 +1079,8 @@ def get_show_by_name(name, useIndexer=False):
return showObj
if not showObj and sickbeard.showList:
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:
(sn, idx, id) = searchIndexerForShowID(name, ui=classes.ShowListUI)
@ -1084,6 +1095,7 @@ def get_show_by_name(name, useIndexer=False):
return showObj
def is_hidden_folder(folder):
"""
Returns True if folder is hidden.
@ -1144,4 +1156,45 @@ def set_up_anidb_connection():
else:
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
import base64
import functools
import inspect
import zipfile
import os.path
import time
import traceback
import urllib
import re
import threading
@ -84,6 +83,8 @@ from tornado import gen
from tornado.web import RequestHandler, HTTPError, asynchronous
req_headers = None
def authenticated(handler_class):
def wrap_execute(handler_execute):
def basicauth(handler, transforms, *args, **kwargs):
@ -98,7 +99,7 @@ def authenticated(handler_class):
if not (sickbeard.WEB_USERNAME and sickbeard.WEB_PASSWORD):
return True
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
auth_hdr = handler.request.headers.get('Authorization')
@ -205,7 +206,7 @@ class MainHandler(RequestHandler):
return func(**args)
raise HTTPError(404)
def redirect(self, url, permanent=False, status=None):
self._transforms = []
super(MainHandler, self).redirect(sickbeard.WEB_ROOT + url, permanent, status)
@ -456,6 +457,7 @@ class MainHandler(RequestHandler):
browser = WebFileBrowser
class PageTemplate(Template):
def __init__(self, *args, **KWs):
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]
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/')
@ -1319,6 +1322,7 @@ class History(MainHandler):
ConfigMenu = [
{'title': 'General', 'path': 'config/general/'},
{'title': 'Backup/Restore', 'path': 'config/backuprestore/'},
{'title': 'Search Settings', 'path': 'config/search/'},
{'title': 'Search Providers', 'path': 'config/providers/'},
{'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))
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):
def index(self, *args, **kwargs):
@ -1558,7 +1611,6 @@ class ConfigSearch(MainHandler):
class ConfigPostProcessing(MainHandler):
def index(self, *args, **kwargs):
t = PageTemplate(file="config_postProcessing.tmpl")
@ -1730,7 +1782,6 @@ class ConfigPostProcessing(MainHandler):
class ConfigProviders(MainHandler):
def index(self, *args, **kwargs):
t = PageTemplate(file="config_providers.tmpl")
t.submenu = ConfigMenu
@ -2101,7 +2152,6 @@ class ConfigProviders(MainHandler):
class ConfigNotifications(MainHandler):
def index(self, *args, **kwargs):
t = PageTemplate(file="config_notifications.tmpl")
t.submenu = ConfigMenu
@ -2303,7 +2353,6 @@ class ConfigNotifications(MainHandler):
class ConfigSubtitles(MainHandler):
def index(self, *args, **kwargs):
t = PageTemplate(file="config_subtitles.tmpl")
t.submenu = ConfigMenu
@ -2361,7 +2410,6 @@ class ConfigSubtitles(MainHandler):
class ConfigAnime(MainHandler):
def index(self, *args, **kwargs):
t = PageTemplate(file="config_anime.tmpl")
@ -2407,7 +2455,6 @@ class ConfigAnime(MainHandler):
class Config(MainHandler):
def index(self, *args, **kwargs):
t = PageTemplate(file="config.tmpl")
t.submenu = ConfigMenu
@ -2416,6 +2463,7 @@ class Config(MainHandler):
# map class names to urls
general = ConfigGeneral
backuprestore = ConfigBackupRestore
search = ConfigSearch
providers = ConfigProviders
subtitles = ConfigSubtitles
@ -2454,7 +2502,6 @@ def HomeMenu():
class HomePostProcess(MainHandler):
def index(self, *args, **kwargs):
t = PageTemplate(file="home_postprocess.tmpl")
@ -2501,7 +2548,6 @@ class HomePostProcess(MainHandler):
class NewHomeAddShows(MainHandler):
def index(self, *args, **kwargs):
t = PageTemplate(file="home_addShows.tmpl")
@ -2887,7 +2933,6 @@ ErrorLogsMenu = [
class ErrorLogs(MainHandler):
def index(self, *args, **kwargs):
t = PageTemplate(file="errorlogs.tmpl")
@ -2956,7 +3001,6 @@ class ErrorLogs(MainHandler):
class Home(MainHandler):
def is_alive(self, *args, **kwargs):
if 'callback' in kwargs and '_' in kwargs:
callback, _ = kwargs['callback'], kwargs['_']