2023-01-26 08:13:07 +08:00
#! /usr/bin/env python
# -*- coding: utf-8 -*-
#
# This file is part of npbackup
__intname__ = " npbackup "
__author__ = " Orsiris de Jong "
__site__ = " https://www.netperfect.fr/npbackup "
__description__ = " NetPerfect Backup Client "
__copyright__ = " Copyright (C) 2022-2023 NetInvent "
__license__ = " GPL-3.0-only "
2023-03-26 21:37:31 +08:00
__build__ = " 2023032601 "
__version__ = " 2.2.0-rc9 "
2023-01-26 08:13:07 +08:00
import os
import sys
import atexit
from argparse import ArgumentParser
import dateutil . parser
from datetime import datetime
import tempfile
import pidfile
import ofunctions . logger_utils
from ofunctions . process import kill_childs
2023-03-14 02:37:16 +08:00
2023-03-14 02:14:29 +08:00
# This is needed so we get no GUI version messages
try :
import PySimpleGUI as sg
import _tkinter
2023-03-21 22:52:43 +08:00
2023-03-21 22:35:56 +08:00
_NO_GUI_ERROR = None
2023-03-14 02:14:29 +08:00
_NO_GUI = False
2023-03-21 22:35:56 +08:00
except ImportError as exc :
_NO_GUI_ERROR = str ( exc )
2023-03-14 02:14:29 +08:00
_NO_GUI = True
2023-01-28 02:33:19 +08:00
2023-01-27 18:42:05 +08:00
from npbackup . customization import (
PYSIMPLEGUI_THEME ,
OEM_ICON ,
LICENSE_TEXT ,
LICENSE_FILE ,
)
2023-01-29 02:42:38 +08:00
from npbackup import configuration
2023-01-27 18:30:06 +08:00
from npbackup . windows . task import create_scheduled_task
from npbackup . core . runner import NPBackupRunner
from npbackup . core . i18n_helper import _t
from npbackup . path_helper import CURRENT_DIR , CURRENT_EXECUTABLE
2023-02-02 02:01:39 +08:00
from npbackup . upgrade_client . upgrader import need_upgrade
from npbackup . core . upgrade_runner import run_upgrade
2023-03-14 02:37:16 +08:00
2023-03-14 02:14:29 +08:00
if not _NO_GUI :
from npbackup . gui . config import config_gui
from npbackup . gui . main import main_gui
from npbackup . gui . minimize_window import minimize_current_window
2023-03-14 02:37:16 +08:00
2023-03-14 02:14:29 +08:00
sg . theme ( PYSIMPLEGUI_THEME )
sg . SetOptions ( icon = OEM_ICON )
2023-01-27 18:30:06 +08:00
2023-01-26 08:13:07 +08:00
# Nuitka compat, see https://stackoverflow.com/a/74540217
try :
2023-01-29 02:42:38 +08:00
# pylint: disable=W0611 (unused-import)
from charset_normalizer import md__mypyc # noqa
2023-01-26 08:13:07 +08:00
except ImportError :
pass
_DEBUG = False
_VERBOSE = False
LOG_FILE = os . path . join ( CURRENT_DIR , " {} .log " . format ( __intname__ ) )
CONFIG_FILE = os . path . join ( CURRENT_DIR , " {} .conf " . format ( __intname__ ) )
PID_FILE = os . path . join ( tempfile . gettempdir ( ) , " {} .pid " . format ( __intname__ ) )
logger = ofunctions . logger_utils . logger_get_logger ( LOG_FILE )
def execution_logs ( start_time : datetime ) - > None :
"""
Try to know if logger . warning or worse has been called
logger . _cache contains a dict of values like { 10 : boolean , 20 : boolean , 30 : boolean , 40 : boolean , 50 : boolean }
where
10 = debug , 20 = info , 30 = warning , 40 = error , 50 = critical
so " if 30 in logger._cache " checks if warning has been triggered
ATTENTION : logger . _cache does only contain cache of current main , not modules , deprecated in favor of
ofunctions . ContextFilterWorstLevel
"""
end_time = datetime . utcnow ( )
logger_worst_level = 0
for flt in logger . filters :
if isinstance ( flt , ofunctions . logger_utils . ContextFilterWorstLevel ) :
logger_worst_level = flt . worst_level
log_level_reached = " success "
try :
if logger_worst_level > = 40 :
log_level_reached = " errors "
elif logger_worst_level > = 30 :
log_level_reached = " warnings "
2023-01-29 02:42:38 +08:00
except AttributeError as exc :
logger . error ( " Cannot get worst log level reached: {} " . format ( exc ) )
2023-01-26 08:13:07 +08:00
logger . info (
2023-01-26 08:26:08 +08:00
" ExecTime = {} , finished, state is: {} . " . format (
end_time - start_time , log_level_reached
)
2023-01-26 08:13:07 +08:00
)
# using sys.exit(code) in a atexit function will swallow the exitcode and render 0
2023-01-26 08:22:02 +08:00
def interface ( ) :
2023-01-26 08:13:07 +08:00
global _DEBUG
global _VERBOSE
global CONFIG_FILE
parser = ArgumentParser (
2023-01-26 08:26:08 +08:00
prog = " {} {} - {} " . format ( __description__ , __copyright__ , __site__ ) ,
2023-01-26 08:13:07 +08:00
description = """ Portable Network Backup Client \n
This program is distributed under the GNU General Public License and comes with ABSOLUTELY NO WARRANTY . \n
This is free software , and you are welcome to redistribute it under certain conditions ; Please type - - license for more info . """ ,
)
parser . add_argument (
" --check " , action = " store_true " , help = " Check if a recent backup exists "
)
parser . add_argument ( " -b " , " --backup " , action = " store_true " , help = " Run a backup " )
parser . add_argument (
" --force " ,
action = " store_true " ,
default = False ,
help = " Force running a backup regardless of existing backups " ,
)
parser . add_argument (
" -c " ,
" --config-file " ,
dest = " config_file " ,
type = str ,
default = None ,
required = False ,
help = " Path to alternative configuration file " ,
)
parser . add_argument (
" --config-gui " ,
action = " store_true " ,
default = False ,
2023-01-26 08:26:08 +08:00
help = " Show configuration GUI " ,
2023-01-26 08:13:07 +08:00
)
parser . add_argument (
" -l " , " --list " , action = " store_true " , help = " Show current snapshots "
)
parser . add_argument (
" --ls " ,
type = str ,
default = None ,
required = False ,
2023-01-26 08:26:08 +08:00
help = ' Show content given snapshot. Use " latest " for most recent snapshot. ' ,
2023-01-26 08:13:07 +08:00
)
parser . add_argument (
" -f " ,
" --find " ,
type = str ,
default = None ,
required = False ,
help = " Find full path of given file / directory " ,
)
parser . add_argument (
" -r " ,
" --restore " ,
type = str ,
default = None ,
required = False ,
help = " Restore to path given by --restore " ,
)
parser . add_argument (
" --restore-include " ,
type = str ,
default = None ,
required = False ,
help = " Restore only paths within include path " ,
)
parser . add_argument (
" --restore-from-snapshot " ,
type = str ,
default = " latest " ,
required = False ,
help = " Choose which snapshot to restore from. Defaults to latest " ,
)
parser . add_argument (
" --forget " , type = str , default = None , required = False , help = " Forget snapshot "
)
parser . add_argument (
" --raw " , type = str , default = None , required = False , help = " Raw commands "
)
parser . add_argument (
" -v " , " --verbose " , action = " store_true " , help = " Show verbose output "
)
parser . add_argument ( " -d " , " --debug " , action = " store_true " , help = " Run with debugging " )
parser . add_argument (
" -V " , " --version " , action = " store_true " , help = " Show program version "
)
parser . add_argument (
2023-01-26 08:26:08 +08:00
" --dry-run " ,
action = " store_true " ,
help = " Run operations in test mode (no actual modifications " ,
2023-01-26 08:13:07 +08:00
)
parser . add_argument (
2023-01-26 08:26:08 +08:00
" --create-scheduled-task " ,
type = str ,
default = None ,
required = False ,
help = " Create task that runs every n minutes " ,
2023-01-26 08:13:07 +08:00
)
2023-02-01 22:57:28 +08:00
parser . add_argument ( " --license " , action = " store_true " , help = " Show license " )
2023-02-01 08:51:15 +08:00
parser . add_argument (
2023-02-02 02:06:52 +08:00
" --auto-upgrade " , action = " store_true " , help = " Auto upgrade NPBackup "
)
2023-02-01 22:57:28 +08:00
parser . add_argument (
2023-02-02 02:06:52 +08:00
" --upgrade-conf " ,
action = " store_true " ,
help = " Add new configuration elements after upgrade " ,
)
2023-03-21 22:35:56 +08:00
parser . add_argument (
" --gui-status " ,
action = " store_true " ,
2023-03-21 22:52:43 +08:00
help = " Show status of required modules for GUI to work " ,
2023-03-21 22:35:56 +08:00
)
2023-01-26 08:13:07 +08:00
args = parser . parse_args ( )
2023-03-21 22:52:43 +08:00
2023-03-28 00:23:54 +08:00
version_string = " {} v {} {} {} {} - {} - {} " . format (
2023-03-21 22:52:43 +08:00
__intname__ ,
__version__ ,
2023-03-27 21:30:38 +08:00
" -PRIV " if configuration . IS_PRIV_BUILD else " " ,
2023-03-28 00:23:54 +08:00
" -P {} " . format ( sys . version_info [ 1 ] ) ,
2023-03-21 22:52:43 +08:00
__build__ ,
" GUI disabled " if _NO_GUI else " GUI enabled " ,
2023-03-28 00:24:38 +08:00
__copyright__ ,
2023-03-21 22:52:43 +08:00
)
2023-01-26 08:13:07 +08:00
if args . version :
2023-03-21 22:35:56 +08:00
print ( version_string )
2023-01-26 08:13:07 +08:00
sys . exit ( 0 )
2023-03-21 22:35:56 +08:00
logger . info ( version_string )
2023-01-26 08:13:07 +08:00
if args . license :
try :
2023-03-14 02:36:11 +08:00
with open ( LICENSE_FILE , " r " , encoding = " utf-8 " ) as file_handle :
2023-01-26 08:13:07 +08:00
print ( file_handle . read ( ) )
except OSError :
print ( LICENSE_TEXT )
sys . exit ( 0 )
2023-03-21 22:35:56 +08:00
if args . gui_status :
logger . info ( " Can run GUI: {} , errors= {} " . format ( not _NO_GUI , _NO_GUI_ERROR ) )
2023-03-25 21:33:40 +08:00
# Don't bother to talk about package manager when compiled with Nuitka
is_nuitka = " __compiled__ " in globals ( )
if _NO_GUI and not is_nuitka :
logger . info (
' You need tcl/tk 8.6+ and python-tkinter installed for GUI to work. Please use your package manager (example " yum install python-tkinter " or " apt install python3-tk " ) to install missing dependencies. '
)
2023-03-21 22:35:56 +08:00
sys . exit ( 0 )
2023-01-26 08:26:08 +08:00
if args . debug or os . environ . get ( " _DEBUG " , " False " ) . capitalize ( ) == " True " :
2023-01-26 08:13:07 +08:00
_DEBUG = True
logger . setLevel ( ofunctions . logger_utils . logging . DEBUG )
if args . verbose :
_VERBOSE = True
# Make sure we log execution time and error state at the end of the program
if args . backup or args . restore or args . find or args . list or args . check :
atexit . register (
execution_logs ,
datetime . utcnow ( ) ,
)
if args . config_file :
if not os . path . isfile ( args . config_file ) :
logger . critical ( " Given file {} cannot be read. " . format ( args . config_file ) )
CONFIG_FILE = args . config_file
# Program entry
if args . config_gui :
try :
config_dict = configuration . load_config ( CONFIG_FILE )
2023-02-02 21:13:06 +08:00
if not config_dict :
logger . error ( " Cannot load config file " )
sys . exit ( 24 )
2023-01-26 08:13:07 +08:00
except FileNotFoundError :
2023-01-26 08:26:08 +08:00
logger . warning (
' No configuration file found. Please use --config-file " path " to specify one or put a config file into current directory. Will create fresh config file in current directory. '
)
2023-01-26 08:13:07 +08:00
config_dict = configuration . empty_config_dict
2023-01-26 08:26:08 +08:00
2023-01-26 08:13:07 +08:00
config_dict = config_gui ( config_dict , CONFIG_FILE )
sys . exit ( 0 )
if args . create_scheduled_task :
try :
2023-01-26 08:26:08 +08:00
result = create_scheduled_task (
executable_path = CURRENT_EXECUTABLE ,
interval_minutes = int ( args . create_scheduled_task ) ,
)
2023-01-26 08:13:07 +08:00
if result :
sys . exit ( 0 )
else :
sys . exit ( 22 )
except ValueError :
sys . exit ( 23 )
try :
2023-02-03 05:03:43 +08:00
config_dict = configuration . load_config ( CONFIG_FILE )
2023-01-26 08:13:07 +08:00
except FileNotFoundError :
2023-02-02 21:13:06 +08:00
config_dict = None
if not config_dict :
2023-01-26 08:26:08 +08:00
message = _t ( " config_gui.no_config_available " )
2023-01-26 08:13:07 +08:00
logger . error ( message )
2023-03-14 02:14:29 +08:00
if config_dict is None and not _NO_GUI :
2023-02-02 21:13:06 +08:00
config_dict = configuration . empty_config_dict
# If no arguments are passed, assume we are launching the GUI
if len ( sys . argv ) == 1 :
2023-02-08 21:40:45 +08:00
minimize_current_window ( )
2023-03-14 02:14:29 +08:00
try :
result = sg . Popup (
" {} \n \n {} " . format ( message , _t ( " config_gui.create_new_config " ) ) ,
custom_text = ( _t ( " generic._yes " ) , _t ( " generic._no " ) ) ,
keep_on_top = True ,
)
if result == _t ( " generic._yes " ) :
config_dict = config_gui ( config_dict , CONFIG_FILE )
sg . Popup ( _t ( " config_gui.saved_initial_config " ) )
else :
logger . error ( " No configuration created via GUI " )
sys . exit ( 7 )
2023-03-21 22:35:56 +08:00
except _tkinter . TclError as exc :
2023-03-21 22:52:43 +08:00
logger . info (
' Tkinter error: " {} " . Is this a headless server ? ' . format ( exc )
)
2023-03-14 02:14:29 +08:00
parser . print_help ( sys . stderr )
sys . exit ( 1 )
2023-03-14 02:22:13 +08:00
sys . exit ( 7 )
elif not config_dict :
if len ( sys . argv ) == 1 and not _NO_GUI :
2023-02-02 21:13:06 +08:00
sg . Popup ( _t ( " config_gui.bogus_config_file " , config_file = CONFIG_FILE ) )
sys . exit ( 7 )
2023-01-26 08:13:07 +08:00
2023-02-01 22:57:28 +08:00
if args . upgrade_conf :
# Whatever we need to add here for future releases
# Eg:
logger . info ( " Upgrading configuration file to version %s " , __version__ )
try :
2023-02-03 05:03:43 +08:00
config_dict [ " identity " ]
2023-02-01 22:57:28 +08:00
except KeyError :
2023-02-03 05:03:43 +08:00
# Create new section identity, as per upgrade 2.2.0rc2
config_dict [ " identity " ] = { " machine_id " : " $ {HOSTNAME} " }
2023-02-01 22:57:28 +08:00
configuration . save_config ( CONFIG_FILE , config_dict )
sys . exit ( 0 )
2023-02-01 08:50:45 +08:00
# Try to perform an auto upgrade if needed
try :
2023-02-01 08:51:15 +08:00
auto_upgrade = config_dict [ " options " ] [ " auto_upgrade " ]
2023-02-01 08:50:45 +08:00
except KeyError :
auto_upgrade = True
try :
2023-02-02 02:32:25 +08:00
auto_upgrade_interval = config_dict [ " options " ] [ " interval " ]
2023-02-01 08:50:45 +08:00
except KeyError :
auto_upgrade_interval = 10
if ( auto_upgrade and need_upgrade ( auto_upgrade_interval ) ) or args . auto_upgrade :
2023-02-02 02:01:39 +08:00
if args . auto_upgrade :
logger . info ( " Running user initiated auto upgrade " )
2023-02-01 06:14:02 +08:00
else :
2023-02-02 02:01:39 +08:00
logger . info ( " Running program initiated auto upgrade " )
result = run_upgrade ( config_dict )
if result :
sys . exit ( 0 )
elif args . auto_upgrade :
sys . exit ( 23 )
2023-02-01 06:14:02 +08:00
2023-01-26 08:13:07 +08:00
dry_run = False
if args . dry_run :
dry_run = True
2023-01-26 08:22:02 +08:00
npbackup_runner = NPBackupRunner ( config_dict = config_dict )
npbackup_runner . dry_run = dry_run
npbackup_runner . verbose = _VERBOSE
2023-01-26 08:13:07 +08:00
if args . check :
2023-01-26 08:22:02 +08:00
if npbackup_runner . check_recent_backups ( ) :
2023-01-26 08:13:07 +08:00
sys . exit ( 0 )
else :
sys . exit ( 2 )
if args . list :
2023-01-26 08:22:02 +08:00
result = npbackup_runner . list ( )
2023-01-26 08:13:07 +08:00
if result :
for snapshot in result :
try :
tags = snapshot [ " tags " ]
except KeyError :
tags = None
logger . info (
" ID: {} Hostname: {} , Username: {} , Tags: {} , source: {} , time: {} " . format (
snapshot [ " short_id " ] ,
snapshot [ " hostname " ] ,
snapshot [ " username " ] ,
tags ,
snapshot [ " paths " ] ,
dateutil . parser . parse ( snapshot [ " time " ] ) ,
)
)
sys . exit ( 0 )
else :
sys . exit ( 2 )
if args . ls :
2023-01-26 08:22:02 +08:00
result = npbackup_runner . ls ( snapshot = args . ls )
2023-01-26 08:13:07 +08:00
if result :
2023-01-26 08:22:02 +08:00
logger . info ( " Snapshot content: " )
2023-01-26 08:13:07 +08:00
for entry in result :
logger . info ( entry )
sys . exit ( 0 )
else :
logger . error ( " Snapshot could not be listed. " )
sys . exit ( 2 )
if args . find :
2023-01-26 08:22:02 +08:00
result = npbackup_runner . find ( path = args . find )
2023-01-26 08:13:07 +08:00
if result :
sys . exit ( 0 )
else :
sys . exit ( 2 )
try :
with pidfile . PIDFile ( PID_FILE ) :
if args . backup :
2023-01-26 08:22:02 +08:00
result = npbackup_runner . backup ( force = args . force )
2023-01-26 08:13:07 +08:00
if result :
logger . info ( " Backup finished. " )
sys . exit ( 0 )
else :
logger . error ( " Backup operation failed. " )
sys . exit ( 2 )
if args . restore :
2023-01-26 08:26:08 +08:00
result = npbackup_runner . restore (
snapshot = args . restore_from_snapshot ,
target = args . restore ,
restore_includes = args . restore_include ,
)
2023-01-26 08:13:07 +08:00
if result :
sys . exit ( 0 )
else :
sys . exit ( 2 )
if args . forget :
2023-01-26 08:22:02 +08:00
result = npbackup_runner . forget ( snapshot = args . forget )
2023-01-26 08:13:07 +08:00
if result :
sys . exit ( 0 )
else :
sys . exit ( 2 )
if args . raw :
2023-01-26 08:22:02 +08:00
result = npbackup_runner . raw ( command = args . raw )
2023-01-26 08:13:07 +08:00
if result :
sys . exit ( 0 )
else :
sys . exit ( 2 )
except pidfile . AlreadyRunningError :
logger . warning ( " Backup process already running. Will not continue. " )
# EXIT_CODE 21 = current backup process already running
sys . exit ( 21 )
2023-03-14 02:14:29 +08:00
if not _NO_GUI :
# When no argument is given, let's run the GUI
# Also, let's minimize the commandline window so the GUI user isn't distracted
minimize_current_window ( )
logger . info ( " Running GUI " )
try :
with pidfile . PIDFile ( PID_FILE ) :
try :
main_gui ( config_dict , CONFIG_FILE , version_string )
2023-03-21 22:35:56 +08:00
except _tkinter . TclError as exc :
2023-03-21 22:52:43 +08:00
logger . info (
' Tkinter error: " {} " . Is this a headless server ? ' . format ( exc )
)
2023-03-14 02:14:29 +08:00
parser . print_help ( sys . stderr )
sys . exit ( 1 )
except pidfile . AlreadyRunningError :
logger . warning ( " Backup GUI already running. Will not continue " )
# EXIT_CODE 21 = current backup process already running
sys . exit ( 21 )
else :
parser . print_help ( sys . stderr )
2023-01-26 08:13:07 +08:00
2023-01-26 08:22:02 +08:00
def main ( ) :
2023-01-26 08:13:07 +08:00
try :
2023-01-26 08:22:02 +08:00
# kill_childs normally would not be necessary, but let's just be foolproof here (kills restic subprocess in all cases)
2023-01-26 08:26:08 +08:00
atexit . register (
kill_childs ,
os . getpid ( ) ,
)
2023-01-26 08:22:02 +08:00
interface ( )
2023-01-26 08:13:07 +08:00
except KeyboardInterrupt as exc :
logger . error ( " Program interrupted by keyboard. {} " . format ( exc ) )
logger . info ( " Trace: " , exc_info = True )
# EXIT_CODE 200 = keyboard interrupt
sys . exit ( 200 )
except Exception as exc :
logger . error ( " Program interrupted by error. {} " . format ( exc ) )
logger . info ( " Trace: " , exc_info = True )
# EXIT_CODE 201 = Non handled exception
sys . exit ( 201 )
2023-01-26 08:22:02 +08:00
if __name__ == " __main__ " :
main ( )