diff --git a/SickBeard.py b/SickBeard.py
index cc01be9a..cd0f7fb6 100755
--- a/SickBeard.py
+++ b/SickBeard.py
@@ -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=',
diff --git a/gui/slick/interfaces/default/config_backuprestore.tmpl b/gui/slick/interfaces/default/config_backuprestore.tmpl
new file mode 100644
index 00000000..cf907ac0
--- /dev/null
+++ b/gui/slick/interfaces/default/config_backuprestore.tmpl
@@ -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")
+
+
+#if $varExists('header')
+
+#else
+ $title
+#end if
+
+#set $indexer = 0
+#if $sickbeard.INDEXER_DEFAULT
+ #set $indexer = $sickbeard.INDEXER_DEFAULT
+#end if
+
+
+
+
+
+
+
+
+
+#include $os.path.join($sickbeard.PROG_DIR,"gui/slick/interfaces/default/inc_bottom.tmpl")
diff --git a/gui/slick/interfaces/default/inc_top.tmpl b/gui/slick/interfaces/default/inc_top.tmpl
index ea43ecc1..d78597d2 100644
--- a/gui/slick/interfaces/default/inc_top.tmpl
+++ b/gui/slick/interfaces/default/inc_top.tmpl
@@ -161,6 +161,7 @@ a > i.icon-question-sign { background-image: url("$sbRoot/images/glyphicons-half
\$("#SubMenu a:contains('Anime')").addClass('btn').html(' Anime');
\$("#SubMenu a:contains('Settings')").addClass('btn').html(' Search Settings');
\$("#SubMenu a:contains('Provider')").addClass('btn').html(' Search Providers');
+ \$("#SubMenu a:contains('Backup/Restore')").addClass('btn').html(' Backup/Restore');
\$("#SubMenu a:contains('General')").addClass('btn').html(' General');
\$("#SubMenu a:contains('Episode Status')").addClass('btn').html(' Episode Status Management');
\$("#SubMenu a:contains('Missed Subtitle')").addClass('btn').html(' Missed Subtitles');
@@ -272,6 +273,7 @@ a > i.icon-question-sign { background-image: url("$sbRoot/images/glyphicons-half
- Help & Info
- General
+ - Backup/Restore
- Search Settings
- Search Providers
- Subtitles Settings
diff --git a/gui/slick/js/browser.js b/gui/slick/js/browser.js
index 36c0c10c..000888d3 100644
--- a/gui/slick/js/browser.js
+++ b/gui/slick/js/browser.js
@@ -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 @@
$('').text(first_val.current_path).appendTo(fileBrowserDialog);
list = $('').appendTo(fileBrowserDialog);
$.each(data, function (i, entry) {
- link = $("").click(function () { browse(entry.path, endpoint); }).text(entry.name);
+ link = $("").click(function () { browse(entry.path, endpoint, includeFiles); }).text(entry.name);
$('').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,
diff --git a/gui/slick/js/configBackupRestore.js b/gui/slick/js/configBackupRestore.js
new file mode 100644
index 00000000..38cc076e
--- /dev/null
+++ b/gui/slick/js/configBackupRestore.js
@@ -0,0 +1,24 @@
+$(document).ready(function(){
+ var loading = '';
+
+ $('#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);
+ });
+ });
+});
\ No newline at end of file
diff --git a/sickbeard/browser.py b/sickbeard/browser.py
index aec2923e..86fb54ba 100644
--- a/sickbeard/browser.py
+++ b/sickbeard/browser.py
@@ -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)
\ No newline at end of file
diff --git a/sickbeard/helpers.py b/sickbeard/helpers.py
index 2b7b3ee0..7ff9c1e6 100644
--- a/sickbeard/helpers.py
+++ b/sickbeard/helpers.py
@@ -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()
\ No newline at end of file
+ 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
\ No newline at end of file
diff --git a/sickbeard/webserve.py b/sickbeard/webserve.py
index 9024c8c1..07bd99e5 100644
--- a/sickbeard/webserve.py
+++ b/sickbeard/webserve.py
@@ -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 += "
\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 += "
Restart sickrage to complete the restore."
+ else:
+ finalResult += "Restore FAILED"
+ else:
+ finalResult += "You need to select a backup file to restore!"
+
+ finalResult += "
\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['_']