diff --git a/scripts/CMakeLists.txt b/scripts/CMakeLists.txt index f1bfa2ed1..e278c3eaf 100644 --- a/scripts/CMakeLists.txt +++ b/scripts/CMakeLists.txt @@ -10,6 +10,7 @@ configure_file(zmdc.pl.in "${CMAKE_CURRENT_BINARY_DIR}/zmdc.pl" @ONLY) configure_file(zmfilter.pl.in "${CMAKE_CURRENT_BINARY_DIR}/zmfilter.pl" @ONLY) configure_file(zmonvif-probe.pl.in "${CMAKE_CURRENT_BINARY_DIR}/zmonvif-probe.pl" @ONLY) configure_file(zmpkg.pl.in "${CMAKE_CURRENT_BINARY_DIR}/zmpkg.pl" @ONLY) +configure_file(zmrecover.pl.in "${CMAKE_CURRENT_BINARY_DIR}/zmrecover.pl" @ONLY) configure_file(zmtrack.pl.in "${CMAKE_CURRENT_BINARY_DIR}/zmtrack.pl" @ONLY) configure_file(zmtrigger.pl.in "${CMAKE_CURRENT_BINARY_DIR}/zmtrigger.pl" @ONLY) configure_file(zmupdate.pl.in "${CMAKE_CURRENT_BINARY_DIR}/zmupdate.pl" @ONLY) @@ -35,7 +36,7 @@ FOREACH(PERLSCRIPT ${perlscripts}) ENDFOREACH(PERLSCRIPT ${perlscripts}) # Install the perl scripts -install(FILES "${CMAKE_CURRENT_BINARY_DIR}/zmaudit.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmcontrol.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmdc.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmfilter.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmonvif-probe.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmpkg.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmtrack.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmtrigger.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmupdate.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmvideo.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmwatch.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmcamtool.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmtelemetry.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmstats.pl" DESTINATION "${CMAKE_INSTALL_FULL_BINDIR}" PERMISSIONS OWNER_WRITE OWNER_READ OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE) +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/zmaudit.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmcontrol.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmdc.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmfilter.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmonvif-probe.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmpkg.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmrecover.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmtrack.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmtrigger.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmupdate.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmvideo.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmwatch.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmcamtool.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmtelemetry.pl" "${CMAKE_CURRENT_BINARY_DIR}/zmstats.pl" DESTINATION "${CMAKE_INSTALL_FULL_BINDIR}" PERMISSIONS OWNER_WRITE OWNER_READ OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE) if(NOT ZM_NO_X10) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/zmx10.pl" DESTINATION "${CMAKE_INSTALL_FULL_BINDIR}" PERMISSIONS OWNER_WRITE OWNER_READ OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE) endif(NOT ZM_NO_X10) diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Event.pm b/scripts/ZoneMinder/lib/ZoneMinder/Event.pm index d52c9829e..a91b8c948 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Event.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/Event.pm @@ -52,7 +52,7 @@ use ZoneMinder::Logger qw(:all); use ZoneMinder::Database qw(:all); require Date::Parse; -use vars qw/ $table $primary_key %fields $serial @identified_by/; +use vars qw/ $table $primary_key %fields $serial @identified_by %defaults/; $table = 'Events'; @identified_by = ('Id'); $serial = $primary_key = 'Id'; @@ -84,6 +84,16 @@ $serial = $primary_key = 'Id'; Orientation DiskSpace ); +%defaults = ( + Cause => q`'Unknown'`, + TotScore => '0', + Archived => '0', + Videoed => '0', + Uploaded => '0', + Emailed => '0', + Messaged => '0', + Executed => '0', +); use POSIX; @@ -99,56 +109,10 @@ sub Time { return $_[0]{Time}; } -sub Name { - if ( @_ > 1 ) { - $_[0]{Name} = $_[1]; - } - return $_[0]{Name}; -} # end sub Name - -sub find { - shift if $_[0] eq 'ZoneMinder::Event'; - my %sql_filters = @_; - - my $sql = 'SELECT * FROM Events'; - my @sql_filters; - my @sql_values; - - if ( exists $sql_filters{Name} ) { - push @sql_filters , ' Name = ? '; - push @sql_values, $sql_filters{Name}; - } - if ( exists $sql_filters{Id} ) { - push @sql_filters , ' Id = ? '; - push @sql_values, $sql_filters{Id}; - } - - $sql .= ' WHERE ' . join(' AND ', @sql_filters ) if @sql_filters; - $sql .= ' LIMIT ' . $sql_filters{limit} if $sql_filters{limit}; - - my $sth = $ZoneMinder::Database::dbh->prepare_cached( $sql ) - or Fatal( "Can't prepare '$sql': ".$ZoneMinder::Database::dbh->errstr() ); - my $res = $sth->execute( @sql_values ) - or Fatal( "Can't execute '$sql': ".$sth->errstr() ); - - my @results; - - while( my $db_filter = $sth->fetchrow_hashref() ) { - my $filter = new ZoneMinder::Event( $$db_filter{Id}, $db_filter ); - push @results, $filter; - } # end while - $sth->finish(); - return @results; -} - -sub find_one { - my @results = find(@_); - return $results[0] if @results; -} - sub getPath { return Path( @_ ); } + sub Path { my $event = shift; @@ -168,6 +132,9 @@ sub Path { sub Scheme { my $self = shift; + + $$self{Scheme} = shift if @_; + if ( ! $$self{Scheme} ) { if ( $$self{RelativePath} ) { if ( $$self{RelativePath} =~ /^\d+\/\d{4}\-\d{2}\-\d{2}\/\d+$/ ) { @@ -182,9 +149,8 @@ sub Scheme { sub RelativePath { my $event = shift; - if ( @_ ) { - $$event{RelativePath} = $_[0]; - } + + $$event{RelativePath} = shift if @_; if ( ! $$event{RelativePath} ) { if ( $$event{Scheme} eq 'Deep' ) { @@ -203,7 +169,7 @@ sub RelativePath { if ( $event->Time() ) { $$event{RelativePath} = join('/', $event->{MonitorId}, - strftime( '%Y-%m-%d', localtime($event->Time())), + strftime('%Y-%m-%d', localtime($event->Time())), $event->{Id}, ); } else { @@ -223,9 +189,8 @@ sub RelativePath { sub LinkPath { my $event = shift; - if ( @_ ) { - $$event{LinkPath} = $_[0]; - } + + $$event{LinkPath} = shift if @_; if ( ! $$event{LinkPath} ) { if ( $$event{Scheme} eq 'Deep' ) { @@ -351,19 +316,19 @@ sub GenerateVideo { .$Config{ZM_FFMPEG_OUTPUT_OPTIONS} ." '$video_file' > ffmpeg.log 2>&1" ; - Debug( $command."\n" ); + Debug($command); my $output = qx($command); my $status = $? >> 8; if ( $status ) { - Error( "Unable to generate video, check $event_path/ffmpeg.log for details"); + Error("Unable to generate video, check $event_path/ffmpeg.log for details"); return; } - Info( "Finished $video_file\n" ); + Info("Finished $video_file"); return $event_path.'/'.$video_file; } else { - Info( "Video file $video_file already exists for event $self->{Id}\n" ); + Info("Video file $video_file already exists for event $self->{Id}"); return $event_path.'/'.$video_file; } return; @@ -373,14 +338,14 @@ sub delete { my $event = $_[0]; if ( ! ( $event->{Id} and $event->{MonitorId} and $event->{StartTime} ) ) { my ( $caller, undef, $line ) = caller; - Warning("Can't Delete event $event->{Id} from Monitor $event->{MonitorId} StartTime:$event->{StartTime} from $caller:$line\n"); + Warning("Can't delete event $event->{Id} from Monitor $event->{MonitorId} StartTime:$event->{StartTime} from $caller:$line"); return; } if ( ! -e $event->Storage()->Path() ) { Warning("Not deleting event because storage path doesn't exist"); return; } - Info("Deleting event $event->{Id} from Monitor $event->{MonitorId} StartTime:$event->{StartTime}\n"); + Info("Deleting event $event->{Id} from Monitor $event->{MonitorId} StartTime:$event->{StartTime}"); $ZoneMinder::Database::dbh->ping(); $ZoneMinder::Database::dbh->begin_work(); @@ -697,6 +662,63 @@ Debug("Done deleting files, returning"); return $error; } # end sub MoveTo +# Assumes $path is absolute +# +sub recover_timestamps { + my ( $Event, $path ) = @_; + $path = $Event->Path() if ! $path; + + if ( ! opendir(DIR, $path) ) { + Error("Can't open directory '$path': $!"); + next; + } + my @contents = readdir(DIR); + Debug('Have ' . @contents . " files in $path"); + closedir(DIR); + + my @mp4_files = grep( /^\d+\-video\.mp4$/, @contents); + my @capture_jpgs = grep( /^\d+\-capture\.jpg$/, @contents); + + if ( @capture_jpgs ) { + # can get start and end times from stat'ing first and last jpg + @capture_jpgs = sort { $a cmp $b } @capture_jpgs; + my $first_file = "$path/$capture_jpgs[0]"; + ( $first_file ) = $first_file =~ /^(.*)$/; + my $first_timestamp = (stat($first_file))[9]; + + my $last_file = "$path/$capture_jpgs[@capture_jpgs-1]"; + ( $last_file ) = $last_file =~ /^(.*)$/; + my $last_timestamp = (stat($last_file))[9]; + + my $duration = $last_timestamp - $first_timestamp; + $Event->Length($duration); + $Event->StartTime( Date::Format::time2str('%Y-%m-%d %H:%M:%S', $first_timestamp) ); + $Event->EndTime( Date::Format::time2str('%Y-%m-%d %H:%M:%S', $last_timestamp) ); + Debug("From capture Jpegs have duration $duration = $last_timestamp - $first_timestamp : $$Event{StartTime} to $$Event{EndTime}"); + } elsif ( @mp4_files ) { + my $file = "$path/$mp4_files[0]"; + ( $file ) = $file =~ /^(.*)$/; + + my $first_timestamp = (stat($file))[9]; + my $output = `ffprobe $file 2>&1`; + my ($duration) = $output =~ /Duration: [:\.0-9]+/gm; + Debug("From mp4 have duration $duration, start: $first_timestamp"); + + my ( $h, $m, $s, $u ); + if ( $duration =~ m/(\d+):(\d+):(\d+)\.(\d+)/ ) { + ( $h, $m, $s, $u ) = ($1, $2, $3, $4 ); + Debug("( $h, $m, $s, $u ) from /^(\\d{2}):(\\d{2}):(\\d{2})\.(\\d+)/"); + } + my $seconds = ($h*60*60)+($m*60)+$s; + $Event->Length($seconds.'.'.$u); + $Event->StartTime( Date::Format::time2str('%Y-%m-%d %H:%M:%S', $first_timestamp) ); + $Event->EndTime( Date::Format::time2str('%Y-%m-%d %H:%M:%S', $first_timestamp+$seconds) ); + } + if ( @mp4_files ) { + $Event->DefaultVideo($mp4_files[0]); + } +} + 1; __END__ diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Monitor.pm b/scripts/ZoneMinder/lib/ZoneMinder/Monitor.pm index 2d8400a31..47ec7c557 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Monitor.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/Monitor.pm @@ -36,17 +36,6 @@ require ZoneMinder::Server; #our @ISA = qw(Exporter ZoneMinder::Base); use parent qw(ZoneMinder::Object); -# ========================================================================== -# -# General Utility Functions -# -# ========================================================================== - -use ZoneMinder::Config qw(:all); -use ZoneMinder::Logger qw(:all); -use ZoneMinder::Database qw(:all); - -use POSIX; use vars qw/ $table $primary_key /; $table = 'Monitors'; $primary_key = 'Id'; diff --git a/scripts/ZoneMinder/lib/ZoneMinder/Object.pm b/scripts/ZoneMinder/lib/ZoneMinder/Object.pm index 8c28028a0..ee51d0aab 100644 --- a/scripts/ZoneMinder/lib/ZoneMinder/Object.pm +++ b/scripts/ZoneMinder/lib/ZoneMinder/Object.pm @@ -27,6 +27,8 @@ package ZoneMinder::Object; use 5.006; use strict; use warnings; +use Time::HiRes qw{ gettimeofday tv_interval }; +use Carp qw( cluck ); require ZoneMinder::Base; @@ -49,7 +51,7 @@ use vars qw/ $AUTOLOAD $log $dbh %cache $no_cache/; my $debug = 0; $no_cache = 0; -use constant DEBUG_ALL=>0; +use constant DEBUG_ALL=>1; sub init_cache { $no_cache = 0; @@ -167,17 +169,6 @@ sub lock_and_load { } # end sub lock_and_load -sub AUTOLOAD { - my ( $self, $newvalue ) = @_; - my $type = ref($_[0]); - my $name = $AUTOLOAD; - $name =~ s/.*://; - if ( @_ > 1 ) { - return $_[0]{$name} = $_[1]; - } - return $_[0]{$name}; -} - sub save { my ( $self, $data, $force_insert ) = @_; @@ -187,7 +178,12 @@ sub save { $log->error("No type in Object::save. self:$self from $caller:$line"); } my $local_dbh = eval '$'.$type.'::dbh'; - $local_dbh = $ZoneMinder::Database::dbh if ! $local_dbh; + if ( ! $local_dbh ) { + $local_dbh = $ZoneMinder::Database::dbh; + if ( $debug or DEBUG_ALL ) { + $log->debug("Using global dbh"); + } + } $self->set( $data ? $data : {} ); if ( $debug or DEBUG_ALL ) { if ( $data ) { @@ -196,7 +192,6 @@ sub save { } } } -#$debug = 0; my $table = eval '$'.$type.'::table'; my $fields = eval '\%'.$type.'::fields'; @@ -297,6 +292,7 @@ $log->debug("No serial") if $debug; if ( $need_serial ) { if ( $serial ) { + $log->debug("Getting auto_increments"); my $s = qq{SELECT `auto_increment` FROM INFORMATION_SCHEMA.TABLES WHERE table_name = '$table'}; @$self{@identified_by} = @sql{@$fields{@identified_by}} = $local_dbh->selectrow_array( $s ); #@$self{@identified_by} = @sql{@$fields{@identified_by}} = $local_dbh->selectrow_array( q{SELECT nextval('} . $serial . q{')} ); @@ -368,6 +364,7 @@ sub set { $log->warn("ZoneMinder::Object::set called on an object ($type) with no fields".$@); } # end if my %defaults = eval('%'.$type.'::defaults'); + if ( ref $params ne 'HASH' ) { my ( $caller, undef, $line ) = caller; $log->error("$type -> set called with non-hash params from $caller $line"); @@ -456,7 +453,420 @@ sub transform { sub to_string { my $type = ref($_[0]); my $fields = eval '\%'.$type.'::fields'; - return $type . ': '. join(' ' , map { $_[0]{$_} ? "$_ => $_[0]{$_}" : () } keys %$fields ); + if ( $fields and %{$fields} ) { + return $type . ': '. join(' ', map { $_[0]{$_} ? "$_ => $_[0]{$_}" : () } sort { $a cmp $b } keys %$fields ); + } + return $type . ': '. join(' ', map { $_ .' => '.(defined $_[0]{$_} ? $_[0]{$_} : 'undef') } sort { $a cmp $b } keys %{$_[0]} ); +} + +# We make this a separate function so that we can use it to generate the sql statements for each value in an OR +sub find_operators { + my ( $field, $type, $operator, $value ) = @_; +$log->debug("find_operators: field($field) type($type) op($operator) value($value)") if DEBUG_ALL; + +my $add_placeholder = ( ! ( $field =~ /\?/ ) ) ? 1 : 0; + + if ( sets::isin( $operator, [ '=', '!=', '<', '>', '<=', '>=', '<<=' ] ) ) { + return ( $field.$type.' ' . $operator . ( $add_placeholder ? ' ?' : '' ), $value ); + } elsif ( $operator eq 'not' ) { + return ( '( NOT ' . $field.$type.')', $value ); + } elsif ( sets::isin( $operator, [ '&&', '<@', '@>' ] ) ) { + if ( ref $value eq 'ARRAY' ) { + if ( $field =~ /^\(/ ) { + return ( 'ARRAY('.$field.$type.') ' . $operator . ' ?', $value ); + } else { + return ( $field.$type.' ' . $operator . ' ?', $value ); + } # emd of + } else { + return ( $field.$type.' ' . $operator . ' ?', [ $value ] ); + } # end if + } elsif ( $operator eq 'exists' ) { + return ( $value ? '' : 'NOT ' ) . 'EXISTS ' . $field.$type; + } elsif ( sets::isin( $operator, [ 'in', 'not in' ] ) ) { + if ( ref $value eq 'ARRAY' ) { + return ( $field.$type.' ' . $operator . ' ('. join(',', map { '?' } @{$value} ) . ')', @{$value} ); + } else { + return ( $field.$type.' ' . $operator . ' (?)', $value ); + } # end if + } elsif ( $operator eq 'contains' ) { + return ( '? IN '.$field.$type, $value ); + } elsif ( $operator eq 'does not contain' ) { + return ( '? NOT IN '.$field.$type, $value ); + } elsif ( sets::isin( $operator, [ 'like','ilike' ] ) ) { + return $field.'::text ' . $operator . ' ?', $value; + } elsif ( $operator eq 'null_or_<=' ) { + return '('.$field.$type.' IS NULL OR '.$field.$type.' <= ?)', $value; + } elsif ( $operator eq 'is null or <=' ) { + return '('.$field.$type.' IS NULL OR '.$field.$type.' <= ?)', $value; + } elsif ( $operator eq 'null_or_>=' ) { + return '('.$field.$type.' IS NULL OR '.$field.$type.' >= ?)', $value; + } elsif ( $operator eq 'is null or >=' ) { + return '('.$field.$type.' IS NULL OR '.$field.$type.' >= ?)', $value; + } elsif ( $operator eq 'null_or_>' or $operator eq 'is null or >' ) { + return '('.$field.$type.' IS NULL OR '.$field.$type.' > ?)', $value; + } elsif ( $operator eq 'null_or_<' or $operator eq 'is null or <' ) { + return '('.$field.$type.' IS NULL OR '.$field.$type.' < ?)', $value; + } elsif ( $operator eq 'null_or_=' or $operator eq 'is null or =' ) { + return '('.$field.$type.' IS NULL OR '.$field.$type.' = ?)', $value; + } elsif ( $operator eq 'null or in' or $operator eq 'is null or in' ) { + return '('.$field.$type.' IS NULL OR '.$field.$type.' IN ('.join(',', map { '?' } @{$value} ) . '))', @{$value}; + } elsif ( $operator eq 'null or not in' ) { + return '('.$field.$type.' IS NULL OR '.$field.$type.' NOT IN ('.join(',', map { '?' } @{$value} ) . '))', @{$value}; + } elsif ( $operator eq 'exists' ) { + return ( $value ? ' EXISTS ' : 'NOT EXISTS ' ).$field; + } elsif ( $operator eq 'lc' ) { + return 'lower('.$field.$type.') = ?', $value; + } elsif ( $operator eq 'uc' ) { + return 'upper('.$field.$type.') = ?', $value; + } elsif ( $operator eq 'trunc' ) { + return 'trunc('.$field.$type.') = ?', $value; + } elsif ( $operator eq 'any' ) { + if ( ref $value eq 'ARRAY' ) { + return '(' . join(',', map { '?' } @{$value} ).") = ANY($field)", @{$value}; + } else { + return "? = ANY($field)", $value; + } # end if + } elsif ( $operator eq 'not any' ) { + if ( ref $value eq 'ARRAY' ) { + return '(' . join(',', map { '?' } @{$value} ).") != ANY($field)", @{$value}; + } else { + return "? != ANY($field)", $value; + } # end if + } elsif ( $operator eq 'is null' ) { + if ( $value ) { + return $field.$type. ' is null'; + } else { + return $field.$type. ' is not null'; + } # end if + } elsif ( $operator eq 'is not null' ) { + if ( $value ) { + return $field.$type. ' is not null'; + } else { + return $field.$type. ' is null'; + } # end if + } else { +$log->warn("find_operators: op not found field($field) type($type) op($operator) value($value)"); + } # end if + return; +} # end sub find_operators + +sub get_fields_values { + my ( $object_type, $search, $param_keys ) = @_; + + my @used_fields; + my @where; + my @values; + no strict 'refs'; + + foreach my $k ( @$param_keys ) { + if ( $k eq 'or' ) { + my $or_ref = ref $$search{or}; + + if ( $or_ref eq 'HASH' ) { + my @keys = keys %{$$search{or}}; + if ( @keys ) { + my ( $where, $values, $used_fields ) = get_fields_values( $object_type, $$search{or}, \@keys ); + + push @where, '('.join(' OR ', @{$where} ).')'; + push @values, @{$values}; + } else { + $log->error("No keys in or"); + } + + } elsif ( $or_ref eq 'ARRAY' ) { + my %s = @{$$search{or}}; + my ( $where, $values, $used_fields ) = get_fields_values( $object_type, \%s, [ keys %s ] ); + push @where, '('.join(' OR ', @{$where} ).')'; + push @values, @{$values}; + + } else { + $log->error("Deprecated use of or $or_ref for $$search{or}"); + } # end if + push @used_fields, $k; + next; + } elsif ( $k eq 'and' ) { + my $and_ref = ref $$search{and}; + if ( $and_ref eq 'HASH' ) { + my @keys = keys %{$$search{and}}; + if ( @keys ) { + my ( $where, $values, $used_fields ) = get_fields_values( $object_type, $$search{and}, \@keys ); + + push @where, '('.join(' AND ', @{$where} ).')'; + push @values, @{$values}; + } else { + $log->error("No keys in and"); + } + } elsif ( $and_ref eq 'ARRAY' and @{$$search{and}} ) { + my @sub_where; + + for( my $p_index = 0; $p_index < @{$$search{and}}; $p_index += 2 ) { + my %p = ( $$search{and}[$p_index], $$search{and}[$p_index+1] ); + + my ( $where, $values, $used_fields ) = get_fields_values( $object_type, \%p, [ keys %p ] ); + push @sub_where, @{$where}; + push @values, @{$values}; + } + push @where, '('.join(' AND ', @sub_where ).')'; + } else { + $log->error("incorrect ref of and $and_ref"); + } + push @used_fields, $k; + next; + } + my ( $field, $type, $function ) = $k =~ /^([_\+\w\-]+)(::\w+\[?\]?)?[\s_]*(.*)?$/; + $type = '' if ! defined $type; +$log->debug("$object_type param $field($type) func($function) " . ( ref $$search{$k} eq 'ARRAY' ? join(',',@{$$search{$k}}) : $$search{$k} ) ) if DEBUG_ALL; + + foreach ( 'find_fields', 'fields' ) { + my $fields = \%{$object_type.'::'.$_}; + if ( ! $fields ) { + $log->debug("No $fields in $object_type") if DEBUG_ALL; + next; + } # end if + + if ( ! $$fields{$field} ) { + #$log->debug("No $field in $_ for $object_type") if DEBUG_ALL; + next; + } # end if + +# This allows mainly for find_fields to reference multiple values, opinion in Project, value + foreach my $db_field ( ref $$fields{$field} eq 'ARRAY' ? @{$$fields{$field}} : $$fields{$field} ) { + if ( ! $function ) { + $db_field .= $type; + + if ( ref $$search{$k} eq 'ARRAY' ) { +$log->debug("Have array for $k $$search{$k}") if DEBUG_ALL; + + if ( ! ( $db_field =~ /\?/ ) ) { + if ( @{$$search{$k}} != 1 ) { + push @where, $db_field .' IN ('.join(',', map {'?'} @{$$search{$k}} ) . ')'; + } else { + push @where, $db_field.'=?'; + } # end if + } else { +$log->debug("Have question ? for $k $$search{$k} $db_field") if DEBUG_ALL; + + $db_field =~ s/=/IN/g; + my $question_replacement = '('.join(',', map {'?'} @{$$search{$k}} ) . ')'; + $db_field =~ s/\?/$question_replacement/; + push @where, $db_field; + } + push @values, @{$$search{$k}}; + } elsif ( ref $$search{$k} eq 'HASH' ) { + foreach my $p_k ( keys %{$$search{$k}} ) { + my $v = $$search{$k}{$p_k}; + if ( ref $v eq 'ARRAY' ) { + push @where, $db_field.' IN ('.join(',', map {'?'} @{$v} ) . ')'; + push @values, $p_k, @{$v}; + } else { + push @where, $db_field.'=?'; + push @values, $p_k, $v; + } # end if + } # end foreach p_k + } elsif ( ! defined $$search{$k} ) { + push @where, $db_field.' IS NULL'; + } else { + if ( ! ( $db_field =~ /\?/ ) ) { + push @where, $db_field .'=?'; + } else { + push @where, $db_field; + } + push @values, $$search{$k}; + } # end if + push @used_fields, $k; + } else { + #my @w = +#ref $search{$k} eq 'ARRAY' ? + #map { find_operators( $field, $type, $function, $_ ); } @{$search{$k}} : + my ( $w, @v ) = find_operators( $db_field, $type, $function, $$search{$k} ); + if ( $w ) { + #push @where, '(' . join(' OR ', @w ) . ')'; + push @where, $w; + push @values, @v if @v; + push @used_fields, $k; + } # end if @w + } # end if has function or not + } # end foreach db_field + } # end foreach find_field + } # end foreach k + return ( \@where, \@values, \@used_fields ); +} + +sub find { + no strict 'refs'; + my $object_type = shift; + my $debug = ${$object_type.'::debug'}; + $debug = DEBUG_ALL if ! $debug; + + my $starttime = [gettimeofday] if $debug; + my $params; + if ( @_ == 1 ) { + $params = $_[0]; + if ( ref $params ne 'HASH' ) { + $log->error("params $params was not a has"); + } # end if + } else { + $params = { @_ }; + } # end if + + my $local_dbh = ${$object_type.'::dbh'}; + if ( $$params{dbh} ) { + $local_dbh = $$params{dbh}; + delete $$params{dbh}; + } elsif ( ! $local_dbh ) { + $local_dbh = $dbh if ! $local_dbh; + } # end if + + my $sql = find_sql( $object_type, $params); + + my $do_cache = $$sql{columns} ne '*' ? 0 : 1; + +#$log->debug( 'find prepare: ' . sprintf('%.4f', tv_interval($starttime)*1000) ." useconds") if $debug; + my $data = $local_dbh->selectall_arrayref($$sql{sql}, { Slice => {} }, @{$$sql{values}}); + if ( ! $data ) { + $log->error('Error ' . $local_dbh->errstr() . " loading $object_type ($$sql{sql}) (". join(',', map { ref $_ eq 'ARRAY' ? 'ARRAY('.join(',',@$_).')' : $_ } @{$$sql{values}} ) . ')' ); + return (); + #} elsif ( ( ! @$data ) and $debug ) { + #$log->debug("No $type ($sql) (@values) " ); + } elsif ( $debug ) { + $log->debug("Loading Debug:$debug $object_type ($$sql{sql}) (".join(',', map { ref $_ eq 'ARRAY' ? join(',', @{$_}) : $_ } @{$$sql{values}}).') # of results:' . @$data . ' in ' . sprintf('%.4f', tv_interval($starttime)*1000) .' useconds' ); + } # end if + + my $fields = \%{$object_type.'::fields'}; + my $primary_key = ${$object_type.'::primary_key'}; + if ( ! $primary_key ) { + Error( 'NO primary_key for type ' . $object_type ); + return; + } # end if + if ( ! ($fields and keys %{$fields}) ) { + return map { new($object_type, $$_{$primary_key}, $_ ) } @$data; + } elsif ( $$fields{$primary_key} ) { + return map { new($object_type, $_->{$$fields{$primary_key}}, $_) } @$data; + } else { + my @identified_by = eval '@'.$object_type.'::identified_by'; + if ( ! @identified_by ) { + $log->debug("Multi key object $object_type but no identified by $fields") if $debug; + } # end if + return map { new($object_type, \@identified_by, $_, !$do_cache) } @$data; + } # end if +} # end sub find + +sub find_one { + my $object_type = shift; + my $params; + if ( @_ == 1 ) { + $params = $_[0]; + } else { + %{$params} = @_; + } # end if + $$params{limit}=1; + my @Results = $object_type->find(%$params); + my ( $caller, undef, $line ) = caller; +$log->debug("returning to $caller:$line from find_one") if DEBUG_ALL; + return $Results[0] if @Results; +} # end sub find_one + +sub find_sql { + no strict 'refs'; + my $object_type = shift; + + my $debug = ${$object_type.'::debug'}; + $debug = DEBUG_ALL if ! $debug; + + my $params; + if ( @_ == 1 ) { + $params = $_[0]; + if ( ref $params ne 'HASH' ) { + $log->error("params $params was not a has"); + } # end if + } else { + $params = { @_ }; + } # end if + + my %sql = ( + ( distinct => ( exists $$params{distinct} ? 1:0 ) ), + ( columns => ( exists $$params{columns} ? $$params{columns} : '*' ) ), + ( table => ( exists $$params{table} ? $$params{table} : ${$object_type.'::table'} )), + 'group by'=> $$params{'group by'}, + limit => $$params{limit}, + offset => $$params{offset}, + ); + if ( exists $$params{order} ) { + $sql{order} = $$params{order}; + } else { + my $order = eval '$'.$object_type.'::default_sort'; +#$log->debug("default sort: $object_type :: default_sort = $order") if DEBUG_ALL; + $sql{order} = $order if $order; + } # end if + delete @$params{'distinct','columns','table','group by','limit','offset','order'}; + + my @where; + my @values; + if ( exists $$params{custom} ) { + push @where, '(' . (shift @{$$params{custom}}) . ')'; + push @values, @{$$params{custom}}; + delete $$params{custom}; + } # end if + + my @param_keys = keys %$params; + + # no operators, just which fields are being searched on. Mostly just useful for detetion of the deleted field. + my %used_fields; + + # We use this search hash so that we can mash it up and leave the params hash alone + my %search; + @search{@param_keys} = @$params{@param_keys}; + + my ( $where, $values, $used_fields ) = get_fields_values( $object_type, \%search, \@param_keys ); + delete @search{@{$used_fields}}; + @used_fields{ @{$used_fields} } = @{$used_fields}; + push @where, @{$where}; + push @values, @{$values}; + + my $fields = \%{$object_type.'::fields'}; + +#optimise this + if ( $$fields{deleted} and ! $used_fields{deleted} ) { + push @where, 'deleted=?'; + push @values, 0; + } # end if + $sql{where} = \@where; + $sql{values} = \@values; + $sql{used_fields} = \%used_fields; + + foreach my $k ( keys %search ) { + $log->error("Extra parameters in $object_type ::find $k => $search{$k}"); + Carp::cluck("Extra parameters in $object_type ::find $k => $search{$k}"); + } # end foreach + + $sql{sql} = join( ' ', + ( 'SELECT', ( $sql{distinct} ? ('DISTINCT') : () ) ), + ( $sql{columns}, 'FROM', $sql{table} ), + ( @{$sql{where}} ? ('WHERE', join(' AND ', @{$sql{where}})) : () ), + ( $sql{order} ? ( 'ORDER BY', $sql{order} ) : () ), + ( $sql{'group by'} ? ( 'GROUP BY', $sql{'group by'} ) : () ), + ( $sql{limit} ? ( 'LIMIT', $sql{limit}) : () ), + ( $sql{offset} ? ( 'OFFSET', $sql{offset} ) : () ), + ); + #$log->debug("Loading Debug:$debug $object_type ($sql) (".join(',', map { ref $_ eq 'ARRAY' ? join(',', @{$_}) : $_ } @values).')' ) if $debug; + return \%sql; +} # end sub find_sql + +sub AUTOLOAD { + my $type = ref($_[0]); + Carp::cluck("No type in autoload") if ! $type; + if ( DEBUG_ALL ) { + Carp::cluck("Using AUTOLOAD $AUTOLOAD"); + } + my $name = $AUTOLOAD; + $name =~ s/.*://; + if ( @_ > 1 ) { + return $_[0]{$name} = $_[1]; + } + return $_[0]{$name}; +} + +sub DESTROY { } 1; diff --git a/scripts/zmrecover.pl.in b/scripts/zmrecover.pl.in new file mode 100644 index 000000000..9b0f589ff --- /dev/null +++ b/scripts/zmrecover.pl.in @@ -0,0 +1,475 @@ +#!/usr/bin/perl -wT + +use strict; +use bytes; + +# ========================================================================== +# +# These are the elements you can edit to suit your installation +# +# ========================================================================== + +use constant RECOVER_TAG => '(r)'; # Tag to append to event name when recovered +use constant RECOVER_TEXT => 'Recovered.'; # Text to append to event notes when recovered + +# ========================================================================== +# +# You shouldn't need to change anything from here downwards +# +# ========================================================================== + +@EXTRA_PERL_LIB@ +use ZoneMinder; +use DBI; +use POSIX; +use File::Find; +use Time::HiRes qw/gettimeofday/; +use Getopt::Long; +use Date::Format; +use autouse 'Pod::Usage'=>qw(pod2usage); + +use constant ZM_RECOVER_PID => '@ZM_RUNDIR@/zmrecover.pid'; + + +$ENV{PATH} = '/bin:/usr/bin:/usr/local/bin'; +$ENV{SHELL} = '/bin/sh' if exists $ENV{SHELL}; +delete @ENV{qw(IFS CDPATH ENV BASH_ENV)}; + +my $report = 0; +my $interactive = 1; +my $monitor_id = 0; +my $version; +my $force = 0; +my $server_id = undef; +my $storage_id = undef; + +logInit(); + +GetOptions( + force =>\$force, + interactive =>\$interactive, + 'monitor_id=i' =>\$monitor_id, + report =>\$report, + 'server_id=i' =>\$server_id, + 'storage_id=i' =>\$storage_id, + version =>\$version + ) or pod2usage(-exitstatus => -1); + +if ( $version ) { + print( ZoneMinder::Base::ZM_VERSION . "\n"); + exit(0); +} +if ( ($report + $interactive) > 1 ) { + print( STDERR "Error, only one option may be specified\n" ); + pod2usage(-exitstatus => -1); +} + +if ( -e ZM_RECOVER_PID ) { + local $/ = undef; + open FILE, ZM_RECOVER_PID or die "Couldn't open file: $!"; + binmode FILE; + my $pid = ; + close FILE; + if ( $force ) { + Error("zmrecover.pl appears to already be running at pid $pid. Continuing." ); + } else { + Fatal("zmrecover.pl appears to already be running at pid $pid. If not, please delete " . + ZM_RECOVER_PID . " or use the --force command line option." ); + } +} # end if ZM_RECOVER_PID exists + +if ( open(my $PID, '>', ZM_RECOVER_PID) ) { + print($PID $$); + close($PID); +} else { + Error( "Can't open pid file at " . ZM_PID ); +} + +sub HupHandler { + Info('Received HUP, reloading'); + &ZoneMinder::Logger::logHupHandler(); +} +sub TermHandler { + Info('Received TERM, exiting'); + Term(); +} +sub Term { + unlink ZM_RECOVER_PID; + exit(0); +} +$SIG{HUP} = \&HupHandler; +$SIG{TERM} = \&TermHandler; +$SIG{INT} = \&TermHandler; + +my $dbh = zmDbConnect(); +if ( ! $dbh ) { + Error('Unable to connect to database'); + Term(); +} # end if + +$| = 1; + +require ZoneMinder::Monitor; +require ZoneMinder::Storage; +require ZoneMinder::Event; + + +my @Storage_Areas; +if ( defined $storage_id ) { + @Storage_Areas = ZoneMinder::Storage->find( Id=>$storage_id ); + if ( !@Storage_Areas ) { + Error("No Storage Area found with Id $storage_id"); + Term(); + } + Info("Auditing Storage Area $Storage_Areas[0]{Id} $Storage_Areas[0]{Name} at $Storage_Areas[0]{Path}"); +} elsif ( $server_id ) { + @Storage_Areas = ZoneMinder::Storage->find( ServerId => $server_id ); + if ( ! @Storage_Areas ) { + Error("No Storage Area found with ServerId =" . $server_id); + Term(); + } + foreach my $Storage ( @Storage_Areas ) { + Info('Auditing ' . $Storage->Name() . ' at ' . $Storage->Path() . ' on ' . $Storage->Server()->Name() ); + } +} else { + @Storage_Areas = ZoneMinder::Storage->find(); + Info("Auditing All Storage Areas"); +} + +my @Monitors = ZoneMinder::Monitor->find(); +Debug("@Monitors"); + foreach my $Monitor ( @Monitors ) { + Debug("Monitor " . $Monitor->to_string() ) + } + my %Monitors = map { $$_{Id} => $_ } @Monitors; + #ZoneMinder::Monitor->find( + + # ($monitor_id ? ( Id=>$monitor_id ) : () ), + + #); + Debug("Found " . (keys %Monitors) . " monitors"); + foreach my $id ( keys %Monitors ) { + Debug("Monitor $id $Monitors{$id}{Name}"); + } + + foreach my $Storage ( @Storage_Areas ) { + Debug('Checking events in ' . $Storage->Path() ); + if ( ! chdir($Storage->Path()) ) { + Error('Unable to change dir to ' . $Storage->Path()); + next; + } # end if + + # Please note that this glob will take all files beginning with a digit. + foreach my $monitor ( glob('[0-9]*') ) { + if ( $monitor =~ /\D/ ) { + Debug("Weird non digit characters in $monitor"); + next; + } + # De-taint + ( my $monitor_dir ) = ( $monitor =~ /^(\d+)$/ ); + if ( $monitor_id and ( $monitor_id != $monitor_dir ) ) { + Debug("Skipping monitor $monitor_dir because we are only interested in monitor $monitor_id"); + next; + } + if ( ! $Monitors{$monitor_dir} ) { + Warning("There is no monitor in the database for $$Storage{Path}/$monitor_dir. Skipping it."); + next; + } + my $Monitor = $Monitors{$monitor_dir}; + + Debug("Found filesystem monitor '$monitor_dir'"); + + { + my @day_dirs = glob("$monitor_dir/[0-9][0-9]/[0-9][0-9]/[0-9][0-9]"); + Debug(qq`Checking for Deep Events under $$Storage{Path} using glob("$monitor_dir/[0-9][0-9]/[0-9][0-9]/[0-9][0-9]") returned `. scalar @day_dirs . ' days with events'); + foreach my $day_dir ( @day_dirs ) { + Debug("Checking day dir $day_dir"); + ( $day_dir ) = ( $day_dir =~ /^(.*)$/ ); # De-taint + if ( !chdir($day_dir) ) { + Error("Can't chdir to '$$Storage{Path}/$day_dir': $!"); + next; + } + if ( ! opendir(DIR, '.') ) { + Error("Can't open directory '$$Storage{Path}/$day_dir': $!"); + next; + } + my %event_ids_by_path; + + my @event_links = sort { $b <=> $a } grep { -l $_ } readdir( DIR ); + Debug('Have ' . (scalar @event_links) . ' event links'); + closedir(DIR); + + my $count = 0; + foreach my $event_link ( @event_links ) { + # Event links start with a period and consist of the digits of the event id. + # Anything else is not an event link + my ($event_id) = $event_link =~ /^\.(\d+)$/; + if ( !$event_id ) { + Warning("Non-event link found $event_link in $day_dir, skipping"); + next; + } + Debug("Checking link $event_link"); + #Event path is hour/minute/sec + my $event_path = readlink($event_link); + + if ( !($event_path and -e $event_path) ) { + Warning("Event link $day_dir/$event_link does not point to valid target at $event_path"); + next; + } + if ( ! ZoneMinder::Event->find_one(Id=>$event_id) ) { + Info("Event not found in db for event data found at $$Storage{Path}/$day_dir/$event_path with Id=$event_id"); + if ( confirm() ) { + my $Event = new ZoneMinder::Event(); + $$Event{Id} = $event_id; + $$Event{Path} = join('/', $Storage->Path(), $day_dir, $event_path); + $$Event{RelativePath} = join('/', $day_dir, $event_path); + $$Event{Scheme} = 'Deep'; + $$Event{Name} = "Event $event_id recovered"; + $Event->MonitorId( $monitor_dir ); + $Event->StorageId( $Storage->Id() ); + $Event->DiskSpace( undef ); + $Event->Width( $Monitor->Width() ); + $Event->Height( $Monitor->Height() ); + $Event->Orientation( $Monitor->Orientation() ); + + $Event->recover_timestamps(); + + $Event->save({}, 1); + Debug("Event resurrected as " . $Event->to_string() ); + next; + } # end if resurrection + } # event path exists + } # end foreach event_link + + # Now check for events that have lost their link. We can determine event Id from .mp4 + + my @time_dirs = glob('[0-9][0-9]/[0-9][0-9]/[0-9][0-9]'); + foreach my $event_dir ( @time_dirs ) { + Debug("Checking time dir $event_dir"); + ( $event_dir ) = ( $event_dir =~ /^(.*)$/ ); # De-taint + + my $event_id = undef; + + if ( ! opendir(DIR, $event_dir) ) { + Error("Can't open directory '$$Storage{Path}/$day_dir': $!"); + next; + } + my @contents = readdir( DIR ); + Debug('Have ' . @contents . " files in $day_dir/$event_dir"); + closedir(DIR); + + my @mp4_files = grep( /^\d+\-video.mp4$/, @contents); + foreach my $mp4_file ( @mp4_files ) { + my ( $id ) = $mp4_file =~ /^([0-9]+)\-video\.mp4$/; + if ( $id ) { + $event_id = $id; + Debug("Got event id from mp4 file $mp4_file => $event_id"); + last; + } + } # end foreach mp4 + + if ( ! $event_id ) { + # Look for .id file + my @hidden_files = grep( /^\.\d+$/, @contents); + Debug('Have ' . @hidden_files . ' hidden files'); + if ( @hidden_files ) { + ( $event_id ) = $hidden_files[0] =~ /^.(\d+)$/; + } + } + + if ( $event_id and ! ZoneMinder::Event->find_one(Id=>$event_id) ) { + Info("Event not found in db for event data found at $$Storage{Path}/$monitor_dir/$day_dir/$event_dir"); + if ( confirm() ) { + my $Event = new ZoneMinder::Event(); + $$Event{Id} = $event_id; + $$Event{Path} = join('/', $Storage->Path(), $day_dir, $event_dir); + $$Event{RelativePath} = join('/', $day_dir, $event_dir); + $$Event{Scheme} = 'Deep'; + $$Event{Name} = "Event $event_id recovered"; + $Event->MonitorId( $monitor_dir ); + $Event->Width( $Monitor->Width() ); + $Event->Height( $Monitor->Height() ); + $Event->Orientation( $Monitor->Orientation() ); + $Event->StorageId( $Storage->Id() ); + $Event->DiskSpace( undef ); + $Event->recover_timestamps(); + $Event->save({}, 1); + Debug("Event resurrected as " . $Event->to_string() ); + next; + } + } # end if event found + + # Search in db for given timestamp? + + my ( undef, $year, $month, $day ) = split('/', $day_dir); + $year += 2000; + my ( $hour, $minute, $second ) = split('/', $event_dir); + my $StartTime =sprintf('%.4d-%.2d-%.2d %.2d:%.2d:%.2d', $year, $month, $day, $hour, $minute, $second); + my $Event = ZoneMinder::Event->find_one( + MonitorId=>$monitor_dir, + StartTime=>$StartTime, + ); + if ( $Event ) { + Debug("Found event matching starttime on monitor $monitor_dir at $StartTime: " . $Event->to_string()); + next; + } + + } # end foreach event_dir without link + chdir( $Storage->Path() ); + } # end foreach day dir + } + + Debug("Checking for Medium Scheme Events under $$Storage{Path}/$monitor_dir"); + { + my @event_dirs = glob("$monitor_dir/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/*"); + Debug(qq`glob("$monitor_dir/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/*") returned ` . scalar @event_dirs . " entries." ); + foreach my $event_dir ( @event_dirs ) { + if ( ! -d $event_dir ) { + Debug("$event_dir is not a dir. Skipping"); + next; + } + my ( $date, $event_id ) = $event_dir =~ /^$monitor_dir\/(\d{4}\-\d{2}\-\d{2})\/(\d+)$/; + if ( !$event_id ) { + Debug("Unable to parse date/event_id from $event_dir"); + next; + } + + my $Event = ZoneMinder::Event->find_one(Id=>$event_id); + if ( $Event ) { + Debug('Found event in the db, moving on.'); + next; + } + $Event = new ZoneMinder::Event(); + $$Event{Id} = $event_id; + $$Event{Path} = join('/', $Storage->Path(), $event_dir ); + Debug("Have event $$Event{Id} at $$Event{Path}"); + $$Event{Scheme} = 'Medium'; + $$Event{RelativePath} = $event_dir; + $$Event{Name} = "Event $event_id recovered"; + $Event->MonitorId( $monitor_dir ); + $Event->Width( $Monitor->Width() ); + $Event->Height( $Monitor->Height() ); + $Event->Orientation( $Monitor->Orientation() ); + $Event->StorageId( $Storage->Id() ); + $Event->recover_timestamps(); + if ( confirm() ) { + $Event->save({}, 1); + Debug("Event resurrected as " . $Event->to_string() ); + } + } # end foreach event + } # end search for Medium + + # Shallow + Debug("Checking for ShallowScheme Events under $$Storage{Path}/$monitor_dir"); + if ( ! chdir($monitor_dir) ) { + Error("Can't chdir directory '$$Storage{Path}/$monitor_dir': $!"); + next; + } + if ( ! opendir(DIR, '.') ) { + Error("Can't open directory '$$Storage{Path}/$monitor_dir': $!"); + next; + } + my @temp_events = sort { $b <=> $a } grep { -d $_ && $_ =~ /^\d+$/ } readdir( DIR ); + closedir(DIR); + foreach my $event ( @temp_events ) { + my $Event = ZoneMinder::Event->find_one(Id=>$event); + if ( $Event ) { + Debug("Found an event in db for $event"); + next; + } + $$Event{Id} = $event; + $$Event{Path} = join('/', $Storage->Path(), $event ); + Debug("Have event $$Event{Id} at $$Event{Path}"); + $$Event{Scheme} = 'Shallow'; + $$Event{Name} = "Event $event recovered"; + #$$Event{Path} = $event_path; + $Event->MonitorId( $monitor_dir ); + $Event->Width( $Monitor->Width() ); + $Event->Height( $Monitor->Height() ); + $Event->Orientation( $Monitor->Orientation() ); + $Event->StorageId( $Storage->Id() ); + $Event->recover_timestamps(); + $Event->save({}, 1); + Debug("Event resurrected as " . $Event->to_string() ); + } # end foreach event + chdir( $Storage->Path() ); + } # end foreach monitor + + } # end foreach Storage Area + +Term(); + +sub confirm { + my $prompt = shift || 'resurrect'; + my $action = shift || 'resurrecting'; + + my $yesno = 0; + if ( $report ) { + print( "\n" ); + } elsif ( $interactive ) { + print(", $prompt Y/n/q: "); + my $char = <>; + chomp( $char ); + if ( $char eq 'q' ) { + Term(); + } + if ( !$char ) { + $char = 'y'; + } + $yesno = ( $char =~ /[yY]/ ); + } else { + Info($action); + $yesno = 1; + } + return $yesno; +} + +1; +__END__ + +=head1 NAME + +zmrecover.pl - ZoneMinder event file system and database recovery checker + +=head1 SYNOPSIS + + zmrecover.pl [-r,-report|-i,-interactive] + +=head1 DESCRIPTION + +This script checks for consistency between the event filesystem and +the database. If events are found in one and not the other they are +deleted (optionally). Additionally any monitor event directories that +do not correspond to a database monitor are similarly disposed of. +However monitors in the database that don't have a directory are left +alone as this is valid if they are newly created and have no events +yet. + +=head1 OPTIONS + + -i, --interactive - Ask before applying any changes + -m, --monitor_id - Only consider the given monitor + -r, --report - Just report don't actually do anything + -s, --storage_id - Specify a storage area to recover instead of all + -v, --version - Print the installed version of ZoneMinder + +=head1 COPYRIGHT + +ZoneMinder Recover Script +Copyright (C) 2018 ZoneMinder LLC + +This program 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 2 +of the License, or (at your option) any later version. + +This program 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 this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +=cut