diff --git a/plexpy/config.py b/plexpy/config.py index 3e9862ff..41039914 100644 --- a/plexpy/config.py +++ b/plexpy/config.py @@ -80,6 +80,14 @@ _CONFIG_DEFINITIONS = { 'BACKUP_DAYS': (int, 'General', 3), 'BACKUP_DIR': (str, 'General', ''), 'BACKUP_INTERVAL': (int, 'General', 6), + 'S3_BACKUP_ENABLED': (int, 'S3 Backup', 0), + 'S3_ENDPOINT': (str, 'S3 Backup', ''), + 'S3_ACCESS_KEY': (str, 'S3 Backup', ''), + 'S3_SECRET_KEY': (str, 'S3 Backup', ''), + 'S3_BUCKET_NAME': (str, 'S3 Backup', ''), + 'S3_REGION': (str, 'S3 Backup', 'us-east-1'), + 'S3_PREFIX': (str, 'S3 Backup', ''), + 'S3_SECURE': (bool_int, 'S3 Backup', 1), 'CACHE_DIR': (str, 'General', ''), 'CACHE_IMAGES': (int, 'General', 1), 'CACHE_SIZEMB': (int, 'Advanced', 32), @@ -292,6 +300,12 @@ SETTINGS = [ 'PMS_WEB_URL', 'REFRESH_LIBRARIES_INTERVAL', 'REFRESH_USERS_INTERVAL', + 'S3_ACCESS_KEY', + 'S3_BUCKET_NAME', + 'S3_ENDPOINT', + 'S3_PREFIX', + 'S3_REGION', + 'S3_SECRET_KEY', 'SHOW_ADVANCED_SETTINGS', 'TIME_FORMAT', 'TV_WATCHED_PERCENT', @@ -331,6 +345,8 @@ CHECKED_SETTINGS = [ 'PMS_URL_MANUAL', 'REFRESH_LIBRARIES_ON_STARTUP', 'REFRESH_USERS_ON_STARTUP', + 'S3_BACKUP_ENABLED', + 'S3_SECURE', 'SYS_TRAY_ICON', 'THEMOVIEDB_LOOKUP', 'TVMAZE_LOOKUP', @@ -425,7 +441,21 @@ def make_backup(cleanup=False, scheduler=False): except OSError as e: logger.error("Tautulli Config :: Failed to delete %s from the backup folder: %s" % (file_, e)) - if backup_file in os.listdir(backup_folder): + backup_success = backup_file in os.listdir(backup_folder) + + # Upload to S3 if enabled + if backup_success and plexpy.CONFIG.S3_BACKUP_ENABLED: + try: + from plexpy import s3_uploader + s3_success = s3_uploader.upload_file_to_s3(backup_file_fp) + if s3_success: + logger.debug("Tautulli Config :: Successfully uploaded backup to S3") + else: + logger.error("Tautulli Config :: Failed to upload backup to S3") + except Exception as e: + logger.error("Tautulli Config :: Failed to upload backup to S3: %s" % e) + + if backup_success: logger.debug("Tautulli Config :: Successfully backed up %s to %s" % (plexpy.CONFIG_FILE, backup_file)) return True else: diff --git a/plexpy/database.py b/plexpy/database.py index e70d6c77..a11c06ef 100644 --- a/plexpy/database.py +++ b/plexpy/database.py @@ -371,7 +371,21 @@ def make_backup(cleanup=False, scheduler=False): except OSError as e: logger.error("Tautulli Database :: Failed to delete %s from the backup folder: %s" % (file_, e)) - if backup_file in os.listdir(backup_folder): + backup_success = backup_file in os.listdir(backup_folder) + + # Upload to S3 if enabled + if backup_success and plexpy.CONFIG.S3_BACKUP_ENABLED: + try: + from plexpy import s3_uploader + s3_success = s3_uploader.upload_file_to_s3(backup_file_fp) + if s3_success: + logger.debug("Tautulli Database :: Successfully uploaded backup to S3") + else: + logger.error("Tautulli Database :: Failed to upload backup to S3") + except Exception as e: + logger.error("Tautulli Database :: Failed to upload backup to S3: %s" % e) + + if backup_success: logger.debug("Tautulli Database :: Successfully backed up %s to %s" % (db_filename(), backup_file)) return True else: diff --git a/plexpy/s3_uploader.py b/plexpy/s3_uploader.py new file mode 100644 index 00000000..ba75163e --- /dev/null +++ b/plexpy/s3_uploader.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# This file is part of Tautulli. +# +# Tautulli is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Tautulli is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Tautulli. If not, see . + +import os +import sys + +import plexpy +from plexpy import logger + +# Check if boto3 is installed +try: + import boto3 + from botocore.exceptions import ClientError, NoCredentialsError, EndpointConnectionError + BOTO3_AVAILABLE = True +except ImportError: + BOTO3_AVAILABLE = False + logger.error("Tautulli S3 Uploader :: Failed to import boto3. S3 backup functionality is not available.") + + +def check_s3_enabled(): + """Check if S3 backup is enabled and properly configured.""" + if not BOTO3_AVAILABLE: + logger.error("Tautulli S3 Uploader :: S3 backup is unavailable as boto3 is not installed.") + return False + + if not plexpy.CONFIG.S3_BACKUP_ENABLED: + logger.debug("Tautulli S3 Uploader :: S3 backup is disabled.") + return False + + required_config = { + 'S3_ENDPOINT': plexpy.CONFIG.S3_ENDPOINT, + 'S3_ACCESS_KEY': plexpy.CONFIG.S3_ACCESS_KEY, + 'S3_SECRET_KEY': plexpy.CONFIG.S3_SECRET_KEY, + 'S3_BUCKET_NAME': plexpy.CONFIG.S3_BUCKET_NAME + } + + missing = [k for k, v in required_config.items() if not v] + if missing: + logger.error(f"Tautulli S3 Uploader :: S3 backup is missing required configuration: {', '.join(missing)}") + return False + + return True + + +def get_s3_client(): + """Create and return an S3 client.""" + if not BOTO3_AVAILABLE: + return None + + # Check for environment variables first + access_key = os.environ.get('TAUTULLI_S3_ACCESS_KEY', plexpy.CONFIG.S3_ACCESS_KEY) + secret_key = os.environ.get('TAUTULLI_S3_SECRET_KEY', plexpy.CONFIG.S3_SECRET_KEY) + + # Configure the S3 client + session = boto3.session.Session() + + # Set custom endpoint (for MinIO, etc.) + endpoint_url = plexpy.CONFIG.S3_ENDPOINT if plexpy.CONFIG.S3_ENDPOINT else None + region = plexpy.CONFIG.S3_REGION if plexpy.CONFIG.S3_REGION else None + + try: + s3_client = session.client( + service_name='s3', + aws_access_key_id=access_key, + aws_secret_access_key=secret_key, + endpoint_url=endpoint_url, + region_name=region, + use_ssl=bool(plexpy.CONFIG.S3_SECURE) + ) + return s3_client + except Exception as e: + logger.error(f"Tautulli S3 Uploader :: Failed to create S3 client: {e}") + return None + + +def upload_file_to_s3(file_path, object_name=None): + """Upload a file to an S3 bucket. + + Args: + file_path (str): The path to the file to upload + object_name (str, optional): The S3 object name. If not specified, file_name is used + + Returns: + bool: True if file was uploaded, False otherwise + """ + # Check if S3 backup is enabled and configured + if not check_s3_enabled(): + return False + + # If object_name was not specified, use file_path + if not object_name: + object_name = os.path.basename(file_path) + + # Add prefix if configured + if plexpy.CONFIG.S3_PREFIX: + prefix = plexpy.CONFIG.S3_PREFIX.strip('/') + object_name = f"{prefix}/{object_name}" + + # Get S3 client + s3_client = get_s3_client() + if not s3_client: + return False + + # Upload the file + try: + logger.debug(f"Tautulli S3 Uploader :: Uploading file {file_path} to S3 bucket {plexpy.CONFIG.S3_BUCKET_NAME}/{object_name}") + with open(file_path, 'rb') as file_data: + s3_client.upload_fileobj( + file_data, + plexpy.CONFIG.S3_BUCKET_NAME, + object_name + ) + logger.info(f"Tautulli S3 Uploader :: Successfully uploaded file to {plexpy.CONFIG.S3_BUCKET_NAME}/{object_name}") + return True + except FileNotFoundError: + logger.error(f"Tautulli S3 Uploader :: File {file_path} not found for S3 upload") + return False + except NoCredentialsError: + logger.error("Tautulli S3 Uploader :: Credentials not available for S3 upload") + return False + except EndpointConnectionError: + logger.error(f"Tautulli S3 Uploader :: Could not connect to S3 endpoint: {plexpy.CONFIG.S3_ENDPOINT}") + return False + except ClientError as e: + logger.error(f"Tautulli S3 Uploader :: S3 error: {e}") + return False + except Exception as e: + logger.error(f"Tautulli S3 Uploader :: Unexpected error during S3 upload: {e}") + return False \ No newline at end of file