fhem-mirror/lib/FHEM/Core/Utils/FHEMbackup.pm
2021-04-25 08:55:00 +00:00

316 lines
8.7 KiB
Perl

################################################################
# Developed with Kate
#
# (c) 2012-2021 Copyright: Martin Fischer (m_fischer at gmx dot de)
# Rewrite and Maintained by Marko Oldenburg since 2019
# All rights reserved
#
# Contributors:
# - Marko Oldenburg (CoolTux - fhemdevelopment at cooltux dot net)
#
# This script 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 2 of the License, or
# (at your option) any later version.
#
# The GNU General Public License can be found at
# http://www.gnu.org/copyleft/gpl.html.
# A copy is found in the textfile GPL.txt and important notices to the license
# from the author is found in LICENSE.txt distributed with these scripts.
#
# This script 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.
#
#
# $Id$
#
################################################################
package FHEM::Core::Utils::FHEMbackup;
use strict;
use warnings;
my @pathname;
sub CommandBackup {
my $cl = shift;
my $param = shift;
my $byUpdate = ( $param && $param eq 'startedByUpdate' );
my $modpath = ::AttrVal( 'global', 'modpath', '.' );
my $configfile = ::AttrVal( 'global', 'configfile', $modpath . '/fhem.cfg' );
my $statefile = ::AttrVal( 'global', 'statefile', $modpath . '/log/fhem.save' );
my $dir = ::AttrVal( 'global', 'backupdir', $modpath . '/backup');
my $now = ::gettimeofday();
my @t = localtime($now);
my $dateTime = dateTime();
$statefile = ::ResolveDateWildcards( $statefile, @t );
# prevent duplicate entries in backup list for default config, forum #54826
$configfile = '' if ( $configfile eq 'fhem.cfg' || ::configDBUsed() );
$statefile = '' if ( $statefile eq './log/fhem.save' );
my $msg;
my $ret;
my ($err,$backupdir) = createBackupDir( $dir, $modpath );
return ::Log(1, q(ERROR: if create backup directory!))
if ( defined($err) && $err );
::Log(1, q(NOTE: make sure you have a database backup!))
if ( ::configDBUsed() );
$ret = addConfDBFiles( $configfile, $statefile );
return ::Log(1, qq(Backup ERROR - addConfDBFiles: $ret))
if ( defined($ret)
&& $ret =~ m{\ACan\'t\sopen.*:\s.*}xms);
$ret = readModpath( $modpath, $backupdir );
return ::Log(1, qq(Backup ERROR - readModpath: $ret))
if ( defined($ret)
&& $ret =~ m{\ACan\'t\sopen\s\$modpath:\s.*}xms);
## add all logfile path to pathname array
$ret = addLogPathToPathnameArray();
### remove double entries from pathname array
my %all=();
@all{@pathname}=1;
@pathname = keys %all;
### create archiv
$ret = createArchiv( $backupdir, $cl, $byUpdate, $dateTime );
### support for backupToStorage Modul
::readingsSingleUpdate($::defs{join(' ',
::devspec2array('TYPE=backupToStorage'))}
, 'fhemBackupFile'
, "$backupdir/FHEM-$dateTime.tar.gz"
, 0
)
if ( ::devspec2array('TYPE=backupToStorage') > 0 );
@pathname = [];
undef @pathname;
return $ret;
}
sub addConfDBFiles {
my $configfile = shift;
my $statefile = shift;
my $ret;
if ( ::configDBUsed() ) {
# add ::configDB configuration file
push( @pathname, 'configDB.conf' );
::Log(2, q(backup include: 'configDB.conf'));
## check if sqlite db file outside of modpath
if ( $::configDB{type} eq 'SQLITE'
&& defined($::configDB{filename})
&& $::configDB{filename} !~ m{\A[a-zA-Z].*|^\.\/[a-zA-Z].*}xms )
{
## backup sqlite db file
::Log(2, qq(backup include SQLite DB File: $::configDB{filename}));
push( @pathname, $::configDB{filename} );
}
}
else {
# get pathnames to archiv
push( @pathname, $configfile ) if ($configfile);
::Log(2, qq(backup include: $configfile))
if ($configfile);
$ret = parseConfig($configfile);
push( @pathname, $statefile ) if ($statefile);
::Log(2, qq(backup include: $statefile))
if ($statefile);
}
return $ret;
}
sub createBackupDir {
my $dir = shift;
my $modpath = shift;
my $msg;
my $ret;
my $backupdir = $dir =~ m{\A\.(\/.*)\z}xms ? $modpath.$1 : $dir =~ m{\A\.\.\/}xms ? $modpath.'/'.$dir : $dir;
# create backupdir if not exists
if ( !-d $backupdir ) {
::Log(4, qq(backup create backupdir: $backupdir));
$ret = `(mkdir -p $backupdir) 2>&1`;
if ($ret) {
chomp($ret);
$msg = 'backup: ' . $ret;
return ($msg,undef);
}
}
return (undef,$backupdir);
}
sub parseConfig {
my $configfile = shift;
# we need default value to read included files
$configfile = $configfile ? $configfile : 'fhem.cfg';
my $fh;
my $msg;
my $ret;
if ( !open( $fh, $configfile ) ) {
$msg = 'Can\'t open ' . $configfile . ': ' . $!;
::Log(1, qq(backup $msg));
return $msg;
}
while ( my $l = <$fh> ) {
$l =~ s/[\r\n]//g;
if ( $l =~ m{\A\s*include\s+(\S+)\s*.*\z}xms ) {
if ( -e $1 ) {
push @pathname, $1;
::Log(4, qq(backup include: $1));
$ret = parseConfig($1);
}
else {
::Log(1, qq(backup configfile: $1 does not exists! File not included.));
}
}
}
close $fh;
return $ret;
}
sub readModpath {
my $modpath = shift;
my $backupdir = shift;
my $msg;
my $ret;
if ( !opendir( DH, $modpath ) ) {
$msg = qq(Can't open \$modpath: $!);
::Log(1, qq(backup $msg));
return $msg;
}
my @files = <$modpath/*>;
foreach my $file (@files) {
if ( $file eq $backupdir && ( -d $file || -l $file ) ) {
::Log(4, qq(backup exclude: $file));
}
else {
::Log(4, qq(backup include: $file));
push @pathname, $file;
}
}
return $ret;
}
sub dateTime {
my $dateTime = ::TimeNow();
$dateTime =~ s/ /_/g;
$dateTime =~ s/(:|-)//g;
return $dateTime;
}
sub createArchiv {
my ($backupdir, $cl, $byUpdate, $dateTime) = @_;
my $backupcmd = ::AttrVal('global','backupcmd',undef);
my $symlink = ::AttrVal('global','backupsymlink','no');
my $tarOpts;
my $msg;
my $ret;
my $pathlist = join( '" "', @pathname );
my $cmd = '';
if ( !defined($backupcmd) ) {
if ( lc($symlink) eq 'no' ) {
$tarOpts = 'czf';
}
else {
$tarOpts = 'chzf';
}
# prevents tar's output of "Removing leading /" and return total bytes of
# archive
$cmd = "tar $tarOpts $backupdir/FHEM-$dateTime.tar.gz \"$pathlist\"";
}
else {
$cmd = $backupcmd . ' \"' . $pathlist . '\"';
}
::Log(2, qq(Backup with command: $cmd));
if ( !$::fhemForked && !$byUpdate ) {
require Blocking;
::BC_searchTelnet('backup');
my $tp = $::defs{$::BC_telnetDevice}{PORT};
system( "($cmd; echo Backup done;"
. "$^X $0 localhost:$tp 'trigger global backup done')2>&1 &" );
return
"Started the backup in the background, watch the log for details";
}
$ret = `($cmd) 2>&1`;
if ($ret) {
chomp $ret;
::Log(1, qq(backup $ret));
}
if ( !defined($backupcmd) && -e "$backupdir/FHEM-$dateTime.tar.gz" ) {
my $size = -s "$backupdir/FHEM-$dateTime.tar.gz";
$msg = "backup done: FHEM-$dateTime.tar.gz ($size Bytes)";
::DoTrigger( 'global', $msg );
::Log(1, $msg);
$ret .= "\n" . $msg;
}
return $ret;
}
sub addLogPathToPathnameArray {
my $ret;
my @logpathname;
my $extlogpath;
::Log(4, q(addLogPathToPathnameArray));
foreach my $logFile (::devspec2array('TYPE=FileLog')) {
::Log(5, qq(found logFiles: $logFile));
my $logpath = ::InternalVal($logFile,'currentlogfile','');
::Log(4, qq(found logpath: $logpath));
if ( $logpath =~ m{\A(.+?)\/[\_|\-|\w]+\.log\z}xms ) {
$extlogpath = $1;
::Log(4, qq(found extlogpath: $extlogpath));
if ( $1 =~ m{\A\/[A-Za-z]}xms ) {
push( @logpathname, $extlogpath ) ;
::Log(4, qq(external logpath include: $extlogpath));
}
}
}
push( @pathname, @logpathname);
return $ret;
}
1;