diff --git a/API.md b/API.md index ec6581d1..b62b91d9 100644 --- a/API.md +++ b/API.md @@ -36,6 +36,10 @@ General optional parameters: Get to the chopper! +### backup_config +Create a manual backup of the `config.ini` file. + + ### backup_db Create a manual backup of the `plexpy.db` file. diff --git a/PlexPy.py b/PlexPy.py index e57cc8f7..5ea68130 100755 --- a/PlexPy.py +++ b/PlexPy.py @@ -33,7 +33,7 @@ import signal import time import plexpy -from plexpy import logger, web_socket, webstart +from plexpy import config, database, logger, web_socket, webstart # Register signals, such as CTRL + C @@ -148,7 +148,7 @@ def main(): if args.config: config_file = args.config else: - config_file = os.path.join(plexpy.DATA_DIR, 'config.ini') + config_file = os.path.join(plexpy.DATA_DIR, config.FILENAME) # Try to create the DATA_DIR if it doesn't exist if not os.path.exists(plexpy.DATA_DIR): @@ -164,7 +164,7 @@ def main(): 'Cannot write to the data directory: ' + plexpy.DATA_DIR + '. Exiting...') # Put the database in the DATA_DIR - plexpy.DB_FILE = os.path.join(plexpy.DATA_DIR, 'plexpy.db') + plexpy.DB_FILE = os.path.join(plexpy.DATA_DIR, database.FILENAME) if plexpy.DAEMON: plexpy.daemonize() diff --git a/data/interfaces/default/settings.html b/data/interfaces/default/settings.html index 1c9c5e48..f0d1de70 100644 --- a/data/interfaces/default/settings.html +++ b/data/interfaces/default/settings.html @@ -198,12 +198,11 @@
-
-
- - - - +
+ +
+ +
@@ -211,12 +210,11 @@
-
-
- - - - +
+ +
+ +
@@ -224,12 +222,10 @@
-
-
- - - - +
+ +
+
@@ -525,7 +521,7 @@ -

Store a hashed password in the config.ini file.
Warning: Your password cannot be recovered if forgotten!

+

Store a hashed password in the config file.
Warning: Your password cannot be recovered if forgotten!

@@ -2138,12 +2134,12 @@ $(document).ready(function() { } getSchedulerTable(); - $("#backup_database").click(function () { - $("#confirm-message").text("Are you sure you want to create a backup of the PlexPy database?"); + function confirmAjaxCall (url, msg) { + $("#confirm-message").text(msg); $('#confirm-modal').modal(); $('#confirm-modal').one('click', '#confirm-button', function () { $.ajax({ - url: 'backup_db', + url: url, type: 'POST', complete: function (xhr, status) { result = $.parseJSON(xhr.responseText); @@ -2156,46 +2152,36 @@ $(document).ready(function() { } }); }); + } + + $("#backup_config").click(function () { + var msg = 'Are you sure you want to create a backup of the PlexPy config?'; + var url = 'backup_config'; + confirmAjaxCall(url, msg); + }); + + $("#backup_database").click(function () { + var msg = 'Are you sure you want to create a backup of the PlexPy database?'; + var url = 'backup_db'; + confirmAjaxCall(url, msg); }); $("#clear_cache").click(function () { - $("#confirm-message").text("Are you sure you want to clear the PlexPy cache?"); - $('#confirm-modal').modal(); - $('#confirm-modal').one('click', '#confirm-button', function () { - $.ajax({ - url: 'delete_cache', - type: 'POST', - complete: function (xhr, status) { - result = $.parseJSON(xhr.responseText); - msg = result.message; - if (result.result == 'success') { - showMsg(' ' + msg, false, true, 5000) - } else { - showMsg(' ' + msg, false, true, 5000, true) - } - } - }); - }); + var msg = 'Are you sure you want to clear the PlexPy cache?'; + var url = 'delete_cache'; + confirmAjaxCall(url, msg); + }); + + $("#clear_image_cache").click(function () { + var msg = 'Are you sure you want to clear the PlexPy image cache?'; + var url = 'delete_image_cache'; + confirmAjaxCall(url, msg); }); $("#clear_logs").click(function () { - $("#confirm-message").text("Are you sure you want to clear the PlexPy logs?"); - $('#confirm-modal').modal(); - $('#confirm-modal').one('click', '#confirm-button', function () { - $.ajax({ - url: 'delete_logs', - type: 'POST', - complete: function (xhr, status) { - result = $.parseJSON(xhr.responseText); - msg = result.message; - if (result.result == 'success') { - showMsg(' ' + msg, false, true, 5000) - } else { - showMsg(' ' + msg, false, true, 5000, true) - } - } - }); - }); + var msg = 'Are you sure you want to clear the PlexPy logs?'; + var url = 'delete_logs'; + confirmAjaxCall(url, msg); }); diff --git a/plexpy/__init__.py b/plexpy/__init__.py index d07cd117..01877d16 100644 --- a/plexpy/__init__.py +++ b/plexpy/__init__.py @@ -345,6 +345,7 @@ def initialize_scheduler(): hours=hours, minutes=0, seconds=0) schedule_job(database.make_backup, 'Backup PlexPy database', hours=6, minutes=0, seconds=0, args=(True, True)) + schedule_job(config.make_backup, 'Backup PlexPy config', hours=6, minutes=0, seconds=0, args=(True, True)) # Start scheduler if start_jobs and len(SCHED.get_jobs()): diff --git a/plexpy/api2.py b/plexpy/api2.py index 4b9b1568..8e782810 100644 --- a/plexpy/api2.py +++ b/plexpy/api2.py @@ -31,6 +31,7 @@ import cherrypy import xmltodict import plexpy +import config import database import logger import plextv @@ -295,6 +296,18 @@ class API2: self.data = rows return rows + def backup_config(self): + """ Create a manual backup of the `config.ini` file. """ + + data = config.make_backup() + + if data: + self.result_type = 'success' + else: + self.result_type = 'failed' + + return data + def backup_db(self): """ Create a manual backup of the `plexpy.db` file. """ diff --git a/plexpy/common.py b/plexpy/common.py index 31797826..5c69c8c7 100644 --- a/plexpy/common.py +++ b/plexpy/common.py @@ -69,5 +69,6 @@ SCHEDULER_LIST = ['Check GitHub for updates', 'Refresh libraries list', 'Refresh Plex server URLs', 'Refresh Plex server name', - 'Backup PlexPy database' + 'Backup PlexPy database', + 'Backup PlexPy config' ] \ No newline at end of file diff --git a/plexpy/config.py b/plexpy/config.py index f2f3b012..bb03fdf3 100644 --- a/plexpy/config.py +++ b/plexpy/config.py @@ -13,10 +13,14 @@ # You should have received a copy of the GNU General Public License # along with PlexPy. If not, see . +import arrow +import os import re +import shutil from configobj import ConfigObj +import plexpy import logger @@ -29,6 +33,8 @@ def bool_int(value): value = 0 return int(bool(value)) +FILENAME = "config.ini" + _CONFIG_DEFINITIONS = { 'ALLOW_GUEST_ACCESS': (int, 'General', 0), 'DATE_FORMAT': (str, 'General', 'YYYY-MM-DD'), @@ -480,6 +486,43 @@ _BLACKLIST_KEYS = ['_APITOKEN', '_TOKEN', '_KEY', '_SECRET', '_PASSWORD', '_APIK _WHITELIST_KEYS = ['HTTPS_KEY', 'UPDATE_SECTION_IDS'] +def make_backup(cleanup=False, scheduler=False): + """ Makes a backup of config file, removes all but the last 5 backups """ + + if scheduler: + backup_file = 'config.backup-%s.sched.ini' % arrow.now().format('YYYYMMDDHHmmss') + else: + backup_file = 'config.backup-%s.ini' % arrow.now().format('YYYYMMDDHHmmss') + backup_folder = plexpy.CONFIG.BACKUP_DIR + backup_file_fp = os.path.join(backup_folder, backup_file) + + # In case the user has deleted it manually + if not os.path.exists(backup_folder): + os.makedirs(backup_folder) + + plexpy.CONFIG.write() + shutil.copyfile(plexpy.CONFIG_FILE, backup_file_fp) + + if cleanup: + # Delete all scheduled backup files except from the last 5. + for root, dirs, files in os.walk(backup_folder): + db_files = [os.path.join(root, f) for f in files if f.endswith('.sched.ini')] + if len(db_files) > 5: + backups_sorted_on_age = sorted(db_files, key=os.path.getctime, reverse=True) + for file_ in backups_sorted_on_age[5:]: + try: + os.remove(file_) + except OSError as e: + logger.error(u"PlexPy Config :: Failed to delete %s from the backup folder: %s" % (file_, e)) + + if backup_file in os.listdir(backup_folder): + logger.debug(u"PlexPy Config :: Successfully backed up %s to %s" % (plexpy.CONFIG_FILE, backup_file)) + return True + else: + logger.warn(u"PlexPy Config :: Failed to backup %s to %s" % (plexpy.CONFIG_FILE, backup_file)) + return False + + # pylint:disable=R0902 # it might be nice to refactor for fewer instance variables class Config(object): @@ -557,12 +600,12 @@ class Config(object): new_config[section][ini_key] = self._config[section][ini_key] # Write it to file - logger.info("Writing configuration to file") + logger.info(u"PlexPy Config :: Writing configuration to file") try: new_config.write() except IOError as e: - logger.error("Error writing configuration file: %s", e) + logger.error(u"PlexPy Config :: Error writing configuration file: %s", e) self._blacklist() diff --git a/plexpy/database.py b/plexpy/database.py index 2331d6ca..6771c1cf 100644 --- a/plexpy/database.py +++ b/plexpy/database.py @@ -23,6 +23,7 @@ import time import plexpy import logger +FILENAME = "plexpy.db" db_lock = threading.Lock() @@ -52,7 +53,7 @@ def delete_sessions(): logger.warn(u"PlexPy Database :: Unable to clear temporary sessions from database: %s." % e) return 'Unable to clear temporary sessions.' -def db_filename(filename="plexpy.db"): +def db_filename(filename=FILENAME): """ Returns the filepath to the db """ return os.path.join(plexpy.DATA_DIR, filename) @@ -115,7 +116,7 @@ def dict_factory(cursor, row): class MonitorDatabase(object): - def __init__(self, filename='plexpy.db'): + def __init__(self, filename=FILENAME): self.filename = filename self.connection = sqlite3.connect(db_filename(filename), timeout=20) # Don't wait for the disk to finish writing diff --git a/plexpy/webserve.py b/plexpy/webserve.py index 64ffd1b9..dbc1aff4 100644 --- a/plexpy/webserve.py +++ b/plexpy/webserve.py @@ -30,6 +30,7 @@ from mako import exceptions import plexpy import common +import config import database import datafactory import graphs @@ -2428,6 +2429,19 @@ class WebInterface(object): return {'result': 'success', 'message': 'Settings saved.'} + @cherrypy.expose + @cherrypy.tools.json_out() + @requireAuth(member_of("admin")) + def backup_config(self): + """ Creates a manual backup of the plexpy.db file """ + + result = config.make_backup() + + if result: + return {'result': 'success', 'message': 'Config backup successful.'} + else: + return {'result': 'error', 'message': 'Config backup failed.'} + @cherrypy.expose @requireAuth(member_of("admin")) def get_scheduler_table(self, **kwargs):