/** * create() * * A function that creates an archive file * * The $excludes will be a list or relative path excludes * * @param string $zip Full path & filename of ZIP Archive file to create * @param string $dir Full path of directory to add to ZIP Archive file * @parame array $excludes List of either absolute path exclusions or relative exclusions * @param string $tempdir Full path of directory for temporary usage * @return bool True if the creation was successful, false otherwise * */ public function create($zip, $dir, $excludes, $tempdir) { $za = null; $result = false; $exitcode = 255; $output = array(); $temp_zip = ''; $excluding_additional = false; $exclude_count = 0; $exclusions = array(); $temp_file_compression_threshold = 5; $pre_add_func = ''; $have_zip_errors = false; $zip_errors_count = 0; $zip_errors = array(); $have_zip_warnings = false; $zip_warnings_count = 0; $zip_warnings = array(); $have_zip_additions = false; $zip_additions_count = 0; $zip_additions = array(); $have_zip_debug = false; $zip_debug_count = 0; $zip_debug = array(); $have_zip_other = false; $zip_other_count = 0; $zip_other = array(); $zip_skipped_count = 0; $logfile_name = ''; $contentfile_name = ''; $contentfile_fp = 0; $have_more_content = true; $zip_ignoring_symlinks = false; $zm = null; $lister = null; $visitor = null; $logger = null; $total_size = 0; $total_count = 0; $the_list = array(); $count_ignored_symdirs = 0; $saved_ignored_symdirs = array(); $zip_error_encountered = false; $zip_period_expired = false; // The basedir must have a trailing normalized directory separator $basedir = rtrim(trim($dir), self::DIRECTORY_SEPARATORS) . self::NORM_DIRECTORY_SEPARATOR; // Normalize platform specific directory separators in path $basedir = str_replace(DIRECTORY_SEPARATOR, self::NORM_DIRECTORY_SEPARATOR, $basedir); // Ensure no stale file information clearstatcache(); // Create the zip monitor function here // Zip monitor will inherit the logger from this object $zm = new pb_backupbuddy_zip_monitor($this); // $zm->set_burst_max_period( self::ZIP_PCLZIP_DEFAULT_BURST_MAX_PERIOD )->set_burst_threshold_period( 'auto' )->log_parameters(); $zm->set_burst_size_min($this->get_min_burst_content())->set_burst_size_max($this->get_max_burst_content())->set_burst_current_size_threshold($zm->get_burst_size_min())->log_parameters(); // Note: could enforce trailing directory separator for robustness if (empty($tempdir) || !file_exists($tempdir)) { // This breaks the rule of single point of exit (at end) but it's early enough to not be a problem $this->log('details', __('Zip process reported: Temporary working directory not available: ', 'it-l10n-backupbuddy') . '`' . $tempdir . '`'); return false; } // Log the temporary working directory so we might be able to spot problems $this->log('details', __('Zip process reported: Temporary working directory available: ', 'it-l10n-backupbuddy') . '`' . $tempdir . '`'); $this->log('message', __('Zip process reported: Using Compatibility Mode.', 'it-l10n-backupbuddy')); // Notify the start of the step $this->log('details', sprintf(__('Zip process reported: Zip archive initial step started with step period threshold: %1$ss', 'it-l10n-backupbuddy'), $this->get_step_period())); // Let's inform what we are excluding/including if (count($excludes) > 0) { $this->log('details', __('Zip process reported: Calculating directories/files to exclude from backup (relative to site root).', 'it-l10n-backupbuddy')); foreach ($excludes as $exclude) { if (!strstr($exclude, 'backupbuddy_backups')) { // Set variable to show we are excluding additional directories besides backup dir. $excluding_additional = true; } $this->log('details', __('Zip process reported: Excluding', 'it-l10n-backupbuddy') . ': ' . $exclude); $exclude_count++; } } if (true === $excluding_additional) { $this->log('message', __('Zip process reported: Excluding archives directory and additional directories defined in settings.', 'it-l10n-backupbuddy') . ' ' . $exclude_count . ' ' . __('total', 'it-l10n-backupbuddy') . '.'); } else { $this->log('message', __('Zip process reported: Only excluding archives directory based on settings.', 'it-l10n-backupbuddy') . ' ' . $exclude_count . ' ' . __('total', 'it-l10n-backupbuddy') . '.'); } $this->log('message', __('Zip process reported: Determining list of candidate files + directories to be added to the zip archive', 'it-l10n-backupbuddy')); // Now let's create the list of files and empty (vacant) directories to include in the backup. // Note: we can only include vacant directories (those that had no content in the first place). // An empty directory may have had content that was excluded but if we give this directory to // pclzip it automatically recurses down into it (we have no control over that) which would then // mess up the exclusions. $visitor = new pluginbuddy_zbdir_visitor_details(array('filename', 'directory', 'vacant', 'absolute_path', 'size')); $logger = new pluginbuddy_zipbuddy_logger('Zip process reported: '); $visitor->set_logger($logger); // Give the visitor our process monitor to be used to keep // the server alive as long as possible $visitor->set_process_monitor($this->get_process_monitor()); $options = array('exclusions' => $excludes, 'pattern_exclusions' => array(), 'inclusions' => array(), 'pattern_inclusions' => array(), 'keep_tree' => false, 'ignore_symlinks' => $this->get_ignore_symlinks(), 'visitor' => $visitor); try { $lister = new pluginbuddy_zbdir($basedir, $options); // As we are not keeping the tree we haev already done the visitor pass // as the tree was built so our visitor contains all the information we // need so we can destroy the lister object unset($lister); $result = true; $this->log('message', __('Zip process reported: Determined list of candidate files + directories to be added to the zip archive', 'it-l10n-backupbuddy')); } catch (Exception $e) { // We couldn't build the list as required so need to bail $error_string = $e->getMessage(); $this->log('details', sprintf(__('Zip process reported: Unable to determine list of candidates files + directories for backup - error reported: %1$s', 'it-l10n-backupbuddy'), $error_string)); // TODO: Should do some cleanup of any temporary directory, visitor, etc. but not for now $result = false; } // In case that took a while use the helper to try and keep the process alive $zm->burst_end(); $this->get_process_monitor()->checkpoint(); if (true === $result) { // Now we have our flat file/directory list from the visitor - remember we didn't // keep the tree as we shouldn't need it for anything else as we can get all we need // from the visitor. First create our list. We have to do this first because we need to // know if we are bypassing ignored symdirs (not including them in the list) so we can // add the number of these to the total number of items from our simple (vacant) directory // and file count total so that the final stats of what was actually added and the details // of what we didn't add will all add up - sounds convoluted, well that's because it is... // Main thing is to filter non-vacant directories $the_list = $visitor->get_as_array(array('filename', 'directory', 'vacant', 'absolute_path', 'size')); foreach ($the_list as $key => $value) { if (false === $value['directory']) { // Not a directory so must be a file (whether symlink or not) so always // keep it (don't remove from list) } elseif (true === $value['directory'] && (isset($value['vacant']) && true === $value['vacant'])) { // It's a directory and has the vacant attribute and it is vacant so we can // safely add it. } elseif (true === $value['directory'] && isset($value['vacant'])) { // It's a directory with the vacant attribute set but not set to true (so implying // false) in which case we need to remove it from the list. // We cannot add non-vacant directories because pclzip will recurse into them. // If there are any files within the directory included then these will cause // directory to be created on unzip so we do not need the actual directory entry. unset($the_list[$key]); } elseif (true === $value['directory'] && !isset($value['vacant'])) { // If the directory does not have the vacant attribute that is because it is // a symlink dir that wasn't followed because of configuration. If this is the // case then the list does not contain any files under this directory. // We will leave the item in the master list for now but we _must_ not pass // it to pclzip because pclzip will recurse down into it befre we have any // chance to stop it. For single-burst/single-step zip building we were able // to do fancy stuff with skipping symdirs and then remembering the prefix to // test against other files to skip those if under the symdir but with multi-burst/ // multi-step this becomes way too complicated to maintain that state information // so we'll now have to skip them at the point of adding to the burst list // and we can save and then log them at the end of the burst. } } // Save the total count of items to be added $total_count = count($the_list); $this->log('details', sprintf(__('Zip process reported: %1$s (directories + files) will be requested to be added to backup zip archive', 'it-l10n-backupbuddy'), $total_count)); //$zm->set_options( array( 'directory_count' => ( $visitor->count( 'directory' => true, 'vacant' => true ) + count( $saved_ignored_symdirs ), 'file_count' => $visitor->count( array( 'directory' => false ) ) ) ); // Find the sum total size of all non-directory (i.e., file) items // Make sure we can handle >2GB on a 32 bit PHP by using double // Note: Currently assuming no single item >2GB size as using the // basic size as returned by stat(). We'll likely need to change to // use our stat() to allow for up to 4GB item size on 32 bit PHP $total_size = (double) 0; foreach ($the_list as $the_item) { if (false === $the_item['directory']) { $total_size += (int) $the_item['size']; } } $this->log('details', sprintf(__('Zip process reported: %1$s bytes will be requested to be added to backup zip archive', 'it-l10n-backupbuddy'), number_format($total_size, 0, ".", ""))); //$zm->set_options( array( 'content_size' => $total_size ) ); // This is where we want to save the contents list $contentfile_name = $tempdir . self::ZIP_CONTENT_FILE_NAME; // Now push the list to a file $this->log('details', sprintf(__('Zip process reported: Writing zip content list to file: %1$s', 'it-l10n-backupbuddy'), $contentfile_name)); try { $contentfile = new SplFileObject($contentfile_name, "wb"); // Simple way to ensure we don't get a final empty line in file that messes up // the read and json_decode. We could later use different ways such as using // marker arrays at start/end so we can include other stuff maybe but this is // all we need for now. $prefix = ''; foreach ($the_list as $the_item) { $encoded_item = serialize($the_item); // Need to bail out if it looks like we failed to encode the data if (0 === strlen($encoded_item)) { throw new Exception('Serialization of content list data failed'); } $bytes_written = $contentfile->fwrite($prefix . $encoded_item); // Be very careful to make sure we had a valid write - in paticular // make sure we didn't write 0 bytes since even an empty line from the // array should have the PHP_EOL bytes written if (null === $bytes_written || 0 === $bytes_written || strlen($prefix) >= $bytes_written) { throw new Exception('Failed to append to content file during creation'); } $prefix = PHP_EOL; } } catch (Exception $e) { // Something fishy - we should have been able to open and // write to the content file... $error_string = $e->getMessage(); $this->log('details', sprintf(__('Zip process reported: Zip content list file could not be created/appended-to - error reported: %1$s', 'it-l10n-backupbuddy'), $error_string)); // Temporary measure for bailing out on problems creting/appending content file $result = false; } // We are done with populating the content file unset($contentfile); // Retain this for reference for now //file_put_contents( ( dirname( dirname( $tempdir ) ) . DIRECTORY_SEPARATOR . self::ZIP_CONTENT_FILE_NAME ), print_r( $the_list, true ) ); // Presently we don't need the visitor any longer so we can free up some // memory by deleting unset($visitor); // We need to force the pclzip library to load at this point if it is // not already loaded so that we can use defined constants it creates // but we don't actually want to create a zip archive at this point. // We can also use this as an early test of being able to use the library // as an exception will be raised if the class does not exist. // Note that this is only really required when zip method caching is // in use, if this is disabled then the library would already have been // loaded by the method testing. try { // Select to just load the pclzip library only and tell it the // temporary directory to use if required (this is only possible // if it hasn't already been loaded and the temp dir defined) $za = new pluginbuddy_PclZip("", true, $tempdir); // We have no purpose for this object any longer, the library // will remain loaded unset($za); $result = true; } catch (Exception $e) { // Something fishy - the methods indicated pclzip but we couldn't find the class $error_string = $e->getMessage(); $this->log('details', sprintf(__('Zip process reported: pclzip indicated as available method but error reported: %1$s', 'it-l10n-backupbuddy'), $error_string)); $result = false; } } // Only continue if we have a valid list // This isn't ideal at present but will suffice if (true === $result) { // Basic argument list (will be used for each burst) $arguments = array(); array_push($arguments, PCLZIP_OPT_REMOVE_PATH, $dir); if (true !== $this->get_compression()) { // Note: don't need to force use of temporary files for compression $this->log('details', __('Zip process reported: Zip archive creation compression disabled based on settings.', 'it-l10n-backupbuddy')); array_push($arguments, PCLZIP_OPT_NO_COMPRESSION); } else { // Note: force the use of temporary files for compression when file size exceeds given value. // This over-rides the "auto-sense" which is based on memory_limit and this _may_ indicate a // memory availability that is higher than reality leading to memory allocation failure if // trying to compress large files. Set the threshold low enough (specify in MB) so that except in // The tightest memory situations we should be ok. Could have option to force use of temporary // files regardless. $this->log('details', __('Zip process reported: Zip archive creation compression enabled based on settings.', 'it-l10n-backupbuddy')); array_push($arguments, PCLZIP_OPT_TEMP_FILE_THRESHOLD, $temp_file_compression_threshold); } // Check if ignoring (not following) symlinks if (true === $this->get_ignore_symlinks()) { // Want to not follow symlinks so set flag for later use $zip_ignoring_symlinks = true; $this->log('details', __('Zip process reported: Zip archive creation symbolic links will be ignored based on settings.', 'it-l10n-backupbuddy')); } else { $this->log('details', __('Zip process reported: Zip archive creation symbolic links will not be ignored based on settings.', 'it-l10n-backupbuddy')); } // Check if we are ignoring warnings - meaning can still get a backup even // if, e.g., some files cannot be read if (true === $this->get_ignore_warnings()) { // Note: warnings are being ignored but will still be gathered and logged $this->log('details', __('Zip process reported: Zip archive creation actionable warnings will be ignored based on settings.', 'it-l10n-backupbuddy')); } else { $this->log('details', __('Zip process reported: Zip archive creation actionable warnings will not be ignored based on settings.', 'it-l10n-backupbuddy')); } // Set up the log file - for each file added we'll append a log entry to the // log file that maps the result of the add to the nearest equivalent command // line zip log entry and this allows us to eventually process and present the // relevant log details in a consistent manner across different methods which // should cut down on confusion a bit. Note that we'll also try and map the // pclzip exit codes to equivalent zip utility codes but we may have to still // maintain our own code space for those that cannot be mapped - just have to // see how it goes. // This approach gives us a unified process and also makes it easy to handle // the log over multiple steps if required. $logfile_name = $tempdir . self::ZIP_LOG_FILE_NAME; // Temporary zip file is _always_ located in the temp dir now and we move it // to the final location after completion if it is a good completion $temp_zip = $tempdir . basename($zip); // Use anonymous function to weed out the unreadable and non-existent files (common reason for failure) // and possibly symlinks based on user settings. // PclZip will record these files as 'skipped' in the file status and we can post-process to determine // if we had any of these and hence either stop the backup or continue dependent on whether the user // has chosen to ignore warnings or not and/or ignore symlinks or not. // Unfortunately we cannot directly tag the file with the reason why it has been skipped so when we // have to process the skipped items we have to try and work out why it was skipped - but shouldn't // be too hard. // TODO: Consider moving this into the PclZip wrapper and have a method to set the various pre/post // functions or select predefined functions (such as this). if (true) { // Note: This could be simplified - it's written to be extensible but may not need to be $args = '$event, &$header'; $code = ''; // $code .= 'static $symlinks = array(); '; $code .= '$result = true; '; // Handle symlinks - keep the two cases of ignoring/not-ignoring separate for now to make logic more // apparent - but could be merged with different conditional handling // For a valid symlink: is_link() -> true; is_file()/is_dir() -> true; file_exists() -> true // For a broken symlink: is_link() -> true; is_file()/is_dir() -> false; file_exists() -> false // Note: pclzip first tests every file using file_exists() before ever trying to add the file so // for a broken symlink it will _always_ error out immediately it discovers a broken symlink so // we never have a chance to filter these out at this stage. // Note: now that we are generating the file list and not following symlinks at that stage we // never have the situation where we need to remember a symdir prefix to filter out dirs/files // under that symdir (once you have passed "through" a dir symlink the dirs/files under that // do not register as symlinks because they themselves are not so previously when pclzip was // generating the list internally we had to make sure we skipped such dirs/files based on // there being a dir symlink as a prefix to the dir/file path). if (true === $zip_ignoring_symlinks) { // If it's a symlink or it's neither a file nor a directory then ignore it. A broken symlink // will never get this far because pclzip will have choked on it $code .= 'if ( ( true === $result ) && !( @is_link( $header[\'filename\'] ) ) ) { '; $code .= ' if ( @is_file( $header[\'filename\'] ) || @is_dir( $header[\'filename\'] ) ) { '; $code .= ' $result = true; '; // $code .= ' foreach ( $symlinks as $prefix ) { '; // $code .= ' if ( !( false === strpos( $header[\'filename\'], $prefix ) ) ) { '; // $code .= ' $result = false; '; // $code .= ' break; '; // $code .= ' } '; // $code .= ' } '; $code .= ' } else { '; // $code .= ' error_log( "Neither a file nor a directory (ignoring): \'" . $header[\'filename\'] . "\'" ); '; $code .= ' $result = false; '; $code .= ' } '; $code .= '} else { '; // $code .= ' error_log( "File is a symlink (ignoring): \'" . $header[\'filename\'] . "\'" ); '; // $code .= ' $symlinks[] = $header[\'filename\']; '; // $code .= ' error_log( "Symlinks Array: \'" . print_r( $symlinks, true ) . "\'" ); '; $code .= ' $result = false; '; $code .= '} '; } else { // If it's neither a file nor directory then ignore it - a valid symlink will register as a file // or directory dependent on what it is pointing at. A broken symlink will never get this far. // because pclzip will have barfed on its file_exists() check before calling the pre-add. We may // choose later to catch this earlier during the list creation I think. $code .= 'if ( ( true === $result ) && ( @is_file( $header[\'filename\'] ) || @is_dir( $header[\'filename\'] ) ) ) { '; $code .= ' $result = true; '; $code .= '} else { '; // $code .= ' error_log( "Neither a file nor a directory (ignoring): \'" . $header[\'filename\'] . "\'" ); '; $code .= ' $result = false; '; $code .= '} '; } // Add the code block for ignoring unreadable files if (true) { $code .= 'if ( ( true === $result ) && ( @is_readable( $header[\'filename\'] ) ) ) { '; $code .= ' $result = true; '; $code .= '} else { '; // $code .= ' error_log( "File not readable: \'" . $header[\'filename\'] . "\'" ); '; $code .= ' $result = false; '; $code .= '} '; } // Return true (to include file) if file passes conditions otherwise false (to skip file) if not $code .= 'return ( ( true === $result ) ? 1 : 0 ); '; $pre_add_func = create_function($args, $code); } // If we had cause to create a pre add function then add it to the argument list here if (!empty($pre_add_func)) { array_push($arguments, PCLZIP_CB_PRE_ADD, $pre_add_func); } // Add a post-add function for progress monitoring, usage data monitoring, // burst handling and server tickling - using the zip helper object // we created earlier $post_add_func = ''; // if (true) { // // $args = '$event, &$header'; // $code = ''; // $code .= '$result = true; '; // $code .= '$zm = pb_backupbuddy_pclzip_helper::get_instance();'; // $code .= '$result = $zm->event_handler( $event, $header );'; // $code .= 'return $result;'; // // $post_add_func = create_function( $args, $code ); // // } // If we had cause to create a pre add function then add it to the argument list here if (!empty($post_add_func)) { array_push($arguments, PCLZIP_CB_POST_ADD, $post_add_func); } // Remember our "master" arguments $master_arguments = $arguments; // Use this to memorise the worst exit code we had (where we didn't immediately // bail out because it signalled a bad failure) $max_exitcode = 0; // Do this as close to when we actually want to start monitoring usage // Maybe this is redundant as we have already called this in the constructor. // If we want to do this then we have to call with true to reset monitoring to // start now. $this->get_process_monitor()->initialize_monitoring_usage(); // Now we have built our common arguments and we have the list defined we can // start on the bursts. Note that each burst will either succeed with an array // output or will fail and no array. When we get an array we will iterate over // it and generate log file entries. For case where we have a non-fatal warning // condition we change the actual pclzip exit code to be the sam eas the zip // utility exit code (18) and this lets us handle the outcome the same way. In // the case of no array but an error code we map that to an equivalent zip utility // exit code (as much as possible) and then we'll drop out with that and a // logged error that the log file processing will pick up. // Now we have our command prototype we can start bursting // Simply build a burst list based on content size. Currently no // look-ahead so the size will always exceed the current size threshold // by some amount. May consider using a look-ahead to see if the next // item would exceed the threshold in which case don't add it (unless it // would be the only content in which case have to add it but also log // a warning). // We'll stop either when noting more to add or we have exceeded our step // period or we have encountered an error. // Note: we might bail out immediately if previous processing has already // caused us to exceed the step period. while ($have_more_content && !($zip_period_expired = $this->exceeded_step_period($this->get_process_monitor()->get_elapsed_time())) && !$zip_error_encountered) { clearstatcache(); // Populate the content array for zip $ilist = array(); // Keep track of any symdirs that are being ignored $saved_ignored_symdirs = array(); // Tell helper that we are preparing a new burst $zm->burst_begin(); $this->log('details', sprintf(__('Zip process reported: Starting burst number: %1$s', 'it-l10n-backupbuddy'), $zm->get_burst_count())); $this->log('details', sprintf(__('Zip process reported: Current burst size threshold: %1$s bytes', 'it-l10n-backupbuddy'), number_format($zm->get_burst_current_size_threshold(), 0, ".", ""))); // Open the content list file and seek to the "current" position. This // will be initially zero and then updated after each burst. For multi-step // it will be zero on the first step and then would be passed back in // as a parameter on subsequent steps based on where in the file the previous // step reached. // TODO: Maybe a sanity check to make sure position seems tenable try { $contentfile = new SplFileObject($contentfile_name, "rb"); $contentfile->fseek($contentfile_fp); // Helper keeps track of what is being added to the burst content and will // tell us when the content is sufficient for this burst based on it's // criteria - this can adapt to how each successive burst goes. while (!$contentfile->eof() && false === $zm->burst_content_complete()) { // Should be at least one item to grab from the list and then move to next // and remember it for if we drop out because burst content complete, in // that case we'll return to that point in the file at the next burst start. // Check for unserialize failure and bail $item = @unserialize($contentfile->current()); if (false === $item) { throw new Exception('Unserialization of content list data failed: `' . $contentfile->current() . '`'); } $contentfile->next(); $file = $item['absolute_path'] . $item['filename']; // Filter out symdirs if we are ignoring symlinks and record them to log // Because of the way the list creation works this condition indicates // a symlink directory only in the case of ignorign symlinks. If we // were not ignoring symlinks then the "vacant" attribute would be set // if the directory were vacant or alternatively this entry would have // already been filtered out if the symlinked directory were not vacant. // So we must filter it out and move on if (true === $item['directory'] && !isset($item['vacant'])) { $saved_ignored_symdirs[] = $file; } else { // We shouldn't have any empty items here as we should have removed them // earlier, but just in case... if (!empty($file)) { $ilist[] = $file; // Call the helper event handler as we add each file to the list $zm->burst_content_added($item); } } } // Burst list is completed by way of end of content list file or size threshold if (!$contentfile->eof()) { // We haven't exhausted the content list yet so remember where we // are at for next burst $contentfile_fp = $contentfile->ftell(); } else { // Exhausted the content list so make sure we drop out after this burst // if we don't break out of the loop due to a zip error or reached step // duration limit $have_more_content = false; } // Finished one way or another so close content list file for this burst unset($contentfile); } catch (Exception $e) { // Something fishy - we should have been able to open the content file... $error_string = $e->getMessage(); $this->log('details', sprintf(__('Zip process reported: Zip content list file could not be opened/read - error reported: %1$s', 'it-l10n-backupbuddy'), $error_string)); $exitcode = 255; $zip_error_encountered = true; break; } // Retain this for reference for now //file_put_contents( ( dirname( $tempdir ) . DIRECTORY_SEPARATOR . self::ZIP_CONTENT_FILE_NAME ), print_r( $ilist, true ) ); // add() method will create archive file if it doesn't aleady exist //$command = 'add'; $command = 'grow'; // Now create our zip handler object for thsi burst // This should give us a new archive object, if not catch it and bail out // Note we previously loaded the library and defined the temporary directory try { $za = new pluginbuddy_PclZip($temp_zip); $result = true; } catch (Exception $e) { // Something fishy - the methods indicated pclzip but we couldn't find the class $error_string = $e->getMessage(); $this->log('details', sprintf(__('Zip process reported: pclzip indicated as available method but error reported: %1$s', 'it-l10n-backupbuddy'), $error_string)); $exitcode = 255; $zip_error_encountered = true; break; } // Allow helper to check how the burst goes $zm->burst_start(); // Create the argument list for this burst $arguments = array(); array_push($arguments, $ilist); $arguments = array_merge($arguments, $master_arguments); // Showing the "master" arguments // First implode any embedded array in the argument list and truncate the result if too long // Assume no arrays embedded in arrays - currently no reason for that // Make sure that there are no non-printable characters (such as in pre- or post-add function // names created by create_function()) by replacing with "*" using preg_replace() // TODO: Make the summary length configurable so that can see more if required // TODO: Consider mapping pclzip argument identifiers to string representations for clarity $args = '$item'; $code = 'if ( is_array( $item ) ) { $string_item = implode( ",", $item); return ( ( strlen( $string_item ) <= 50 ) ? preg_replace( "/[^[:print:]]/", "*", $string_item ) : "List: " . preg_replace( "/[^[:print:]]/", "*", substr( $string_item, 0, 50 ) ) . "..." ); } else { return preg_replace( "/[^[:print:]]/", "*", $item ); }; '; $imploder_func = create_function($args, $code); $imploded_arguments = array_map($imploder_func, $arguments); $this->log('details', sprintf(__('Zip process reported: Burst requests %1$s (directories + files) items with %2$s bytes of content to be added to backup zip archive', 'it-l10n-backupbuddy'), $zm->get_burst_content_count(), $zm->get_burst_content_size())); $this->log('details', __('Zip process reported: ') . $this->get_method_tag() . __(' command arguments', 'it-l10n-backupbuddy') . ': ' . implode(';', $imploded_arguments)); $zip_output = call_user_func_array(array(&$za, $command), $arguments); // And now we can analyse what happened and plan for next burst if any $zm->burst_stop(); // Wrap up the individual burst handling // Note: because we called exec we basically went into a wait condition and so (on Linux) // we didn't consume any max_execution_time so we never really have to bother about // resetting it. However, it is true that time will have elapsed so if this burst _does_ // take longer than our current burst threshold period then max_execution_time would be // reset - but what this doesn't cover is a _cumulative_ effect of bursts and so we might // consider reworking the mechanism to monitor this separately from the individual burst // period (the confusion relates to this having originally applied to the time based // burst handling fro pclzip rather than teh size based for exec). It could also be more // relevant for Windows that doesn't stop the clock when exec is called. $zm->burst_end(); $this->get_process_monitor()->checkpoint(); // If the output is an array then we need to do a quick iteration over the output // in order to determine whetehr we need to change the exit code from 0 to any other // value (essentially to 18). The alternative is some messy stuff with iterating // around and doing stuff based on whether the log file is available or not. By // doing the preprocessing we can simply bail out at any point if the file cannot be // opened or if a write fails. if (is_array($zip_output)) { // Something reasonable happened // For now we'll assume everything rosy but if we find unreadable // files we'll modify the exit code $exitcode = 0; foreach ($zip_output as $file) { switch ($file['status']) { case "ok": break; case "skipped": // For skipped files need to determine why it was skipped if (true === $zip_ignoring_symlinks && @is_link($file['filename'])) { // Skipped because we are ignoring symlinks and this is a symlink. // This just handles files as we have previously filtered out symdirs } else { // Skipped because probably unreadable or non-existent (catch-all for now) // Change the exit code as this is a warning we want to catch later $exitcode = 18; } break; case "filtered": // Log it and change exit code as this is a warning we want to catch later $exitcode = 18; break; case "filename_too_long": // Log it and change exit code as this is a warning we want to catch later $exitcode = 18; break; default: // Unknown status that we'll not consider for changing exit code } } } else { // Something really failed $exitcode = $za->errorCode(); } // This method never directly produces a log file so we need to append the $zip_output array // to the log file - first invocation will create the file. // We now have our exit code so this iteration is simply to log output if we can. // If we fail to open the log file or there is a falure writing we can just bail out $this->log('details', sprintf(__('Zip process reported: Appending zip burst log detail to zip log file: %1$s', 'it-l10n-backupbuddy'), $logfile_name)); try { $logfile = new SplFileObject($logfile_name, "ab"); // Now handle whether the outcome of the addition if (is_array($zip_output)) { // Something reasonable happened // Note if we have skipped any files $skipped_count = 0; // Now we need to put the log information to file // Need to process each status to determine how to log the outcome // for the item - in particular how to log skipped items as the item // status didn't allow us to give any particular reason for an item // being skipped, so we have to try and deduce that from information // about the item. // Our logs are mapped to format like zip utility uses so we can use // a common log processor subsequently. foreach ($zip_output as $file) { // Use this to amass what we want to write to log file $line = ''; switch ($file['status']) { case "ok": // Item was added ok $line = 'adding: ' . $file['filename']; break; case "skipped": // For skipped files need to determine why it was skipped if (true === $zip_ignoring_symlinks && @is_link($file['filename'])) { // Skipped because we are ignoring symlinks and this is a symlink. // This just handles files as we have previously filtered out symdirs // Just treat as an informational $line = 'zip info: ignored symlink: ' . $file['filename']; } else { // Skipped because probably unreadable or non-existent (catch-all for now) $line = 'zip warning: could not open for reading: ' . $file['filename']; } $skipped_count++; break; case "filtered": // Log that it was filtered for some reason $line = 'zip warning: filtered: ' . $file['filename']; // This counts as a skip because we didn't add it $skipped_count++; break; case "filename_too_long": // Log that the given name was too long $line = 'zip warning: filename too long: ' . $file['filename']; // This counts as a skip because we didn't add it $skipped_count++; break; default: // Hmm, have to assume something was not right so we'll log it as // a warning to be on the safe side $line = 'zip warning: unknown add status: ' . $file['status'] . ': ' . $file['filename']; } // Now try and commit the log line to file $bytes_written = $logfile->fwrite($line . PHP_EOL); // Be very careful to make sure we had a valid write - in paticular // make sure we didn't write 0 bytes since even an empty line from the // array should have the PHP_EOL bytes written if (null === $bytes_written || 0 === $bytes_written) { throw new Exception('Failed to append to zip log file during zip creation - zip log details will be incomplete but zip exit code will still be valid'); } } // Now assemble some optional lines $lines = array(); // Now also add in INFORMATIONALs for any ignored symdirs because these would not have // been included in the build list. They were not included because pclzip would have attempted // to follow them and then we would have had to "filter" them and all entries that pclzip // would have created under them which is just a wster of time - best to not include at all // at tell the user now that we didnt include them foreach ($saved_ignored_symdirs as $ignored_symdir) { $lines[] = 'zip info: ignored symlink: ' . $ignored_symdir . self::NORM_DIRECTORY_SEPARATOR; } // Now add log entry related to skiped files if we did skip any // Make this look like zip utility output to some extent if (0 != $skipped_count) { $lines[] = 'zip warning: Not all files were readable'; $lines[] = ' skipped: ' . $skipped_count; } foreach ($lines as $line) { $bytes_written = $logfile->fwrite($line . PHP_EOL); // Be very careful to make sure we had a valid write - in paticular // make sure we didn't write 0 bytes since even an empty line from the // array should have the PHP_EOL bytes written if (null === $bytes_written || 0 === $bytes_written) { throw new Exception('Failed to append to zip log file during zip creation - zip log details will be incomplete but zip exit code will still be valid'); } } } else { // Have to map exit code and warn that not all warnings/etc may be logged // Something really failed $bytes_written = $logfile->fwrite('zip error: ' . $za->errorInfo(true) . PHP_EOL); // Be very careful to make sure we had a valid write - in paticular // make sure we didn't write 0 bytes since even an empty line from the // array should have the PHP_EOL bytes written if (null === $bytes_written || 0 === $bytes_written) { throw new Exception('Failed to append to zip log file during zip creation - zip log details will be incomplete but zip exit code will still be valid'); } } // Put the log file away - safe even if we failed to get a logfile unset($logfile); // And throw away the output result as we have no further use for it unset($zip_output); } catch (Exception $e) { // Something fishy - we should have been able to open and // write to the log file... $error_string = $e->getMessage(); $this->log('details', sprintf(__('Zip process reported: Zip log file could not be opened/appended-to - error reported: %1$s', 'it-l10n-backupbuddy'), $error_string)); // Put the log file away - safe even if we failed to get a logfile unset($logfile); // And throw away the output result as we cannot use it unset($zip_output); } // Put the zip archive away unset($za); // Put the log file away - safe even if we failed to get a logfile unset($logfile); // Report progress at end of burst $this->log('details', sprintf(__('Zip process reported: Accumulated bursts requested %1$s (directories + files) items to be added to backup zip archive (end of burst)', 'it-l10n-backupbuddy'), $zm->get_added_dir_count() + $zm->get_added_file_count())); // Work out percentage progress on items if (0 < $total_count) { $percentage_complete = (int) (($zm->get_added_dir_count() + $zm->get_added_file_count()) / $total_count * 100); $this->log('details', sprintf(__('Zip process reported: Accumulated bursts requested %1$s%% of %2$s (directories + files) total items to be added to backup zip archive (end of burst)', 'it-l10n-backupbuddy'), $percentage_complete, $total_count)); } clearstatcache(); // Keep a running total of the backup file size (this is temporary code) // Using our stat() function in case file size exceeds 2GB on a 32 bit PHP system $temp_zip_stats = pluginbuddy_stat::stat($temp_zip); // Only log anything if we got some valid file stats if (false !== $temp_zip_stats) { $this->log('details', sprintf(__('Zip process reported: Accumulated zip archive file size: %1$s bytes', 'it-l10n-backupbuddy'), number_format($temp_zip_stats['dsize'], 0, ".", ""))); } $this->log('details', sprintf(__('Zip process reported: Ending burst number: %1$s', 'it-l10n-backupbuddy'), $zm->get_burst_count())); // Now work out the result of that burst and what to do // If it is an array then append to the cumulative array and continue // otherwise we have an error and we must bail out. So we don't need // the complexity of exec to handle non-fatal errors (as warnings) // Note: in the multi-burst case we will still have the results array // accumulated from previous bursts so we _could_ chose to handle that // but for now we'll just throw that away. At some point we can thnk about // handling the output array. // We have to check the exit code to decide whether to keep going ot bail out (break). // If we get a 0 exit code ot 18 exit code then keep going and remember we got the 18 // so that we can emit that as the final exit code if applicable. If we get any other // exit code then we must break out immediately. if (0 !== $exitcode && 18 !== $exitcode) { // Zip failure of some sort - must bail out with current exit code $zip_error_encountered = true; } else { // Make sure exit code is always the worst we've had so that when // we've done our last burst we drop out with the correct exit code set // This is really to make sure we drop out with exit code 18 if we had // this in _any_ burst as we would keep going and subsequent burst(s) may // return 0. If we had any other non-zero exit code it would be a "fatal" // error and we would have dropped out immediately anyway. $exitcode = $max_exitcode > $exitcode ? $max_exitcode : ($max_exitcode = $exitcode); } // Now inject a little delay until the next burst. This may be required to give the // server time to catch up with finalizing file creation and/or it may be required to // reduce the average load a little so there isn't a sustained "peak" // Theoretically a sleep could be interrupted by a signal and it would return some // non-zero value or false - but if that is the case it probably signals something // more troubling so there is little point in tryng to "handle" such a condition here. if (0 < ($burst_gap = $this->get_burst_gap())) { $this->log('details', sprintf(__('Zip process reported: Starting burst gap delay of: %1$ss', 'it-l10n-backupbuddy'), $burst_gap)); sleep($burst_gap); } } // Exited the loop for some reason so decide what to do now. // If we didn't exit because of exceeding the step period then it's a // normal exit and we'll process accordingly and end up returning true // or false. If we exited because of exceeding step period then we need // to return the current state array to enable next iteration to pick up // where we left off. // Note: we might consider having the zip helper give us a state to // restore on it when we create one again - but for now we'll not do that if ($zip_period_expired) { // Report progress at end of step $this->log('details', sprintf(__('Zip process reported: Accumulated bursts requested %1$s (directories + files) items to be added to backup zip archive (end of step)', 'it-l10n-backupbuddy'), $zm->get_added_dir_count() + $zm->get_added_file_count())); // Work out percentage progress on items if (0 < $total_count) { $percentage_complete = (int) (($zm->get_added_dir_count() + $zm->get_added_file_count()) / $total_count * 100); $this->log('details', sprintf(__('Zip process reported: Accumulated bursts requested %1$s%% of %2$s (directories + files) total items to be added to backup zip archive (end of step)', 'it-l10n-backupbuddy'), $percentage_complete, $total_count)); } $this->log('details', sprintf(__('Zip process reported: Zip archive build step terminated after %1$ss, continuation step will be scheduled', 'it-l10n-backupbuddy'), $this->get_process_monitor()->get_elapsed_time())); // Need to set up the state information we'll need to tell the next // loop how to set things up to continue. Next time around if another // step is required then some of these may be changed and others may // stay the same. // Note: the method tag 'mt' is used to tell zipbuddy exactly which // zipper to use, the one that was picked first time through. $state = array('name' => pluginbuddy_zipbuddy::STATE_NAME_IN_PROGRESS, 'id' => pluginbuddy_zipbuddy::STATE_ID_IN_PROGRESS, 'zipbuddy' => array('mt' => $this->get_method_tag()), 'zipper' => array('fp' => $contentfile_fp, 'mec' => $max_exitcode, 'sp' => $this->get_step_period(), 'root' => $dir, 'ts' => $total_size, 'tc' => $total_count), 'helper' => array('dc' => $zm->get_added_dir_count(), 'fc' => $zm->get_added_file_count())); // Now we can return directly as we haev nothing to clear up return $state; } // Convenience for handling different scanarios $result = false; // We can report how many dirs/files added $this->log('details', sprintf(__('Zip process reported: Accumulated bursts requested %1$s (directories + files) items to be added to backup zip archive (final)', 'it-l10n-backupbuddy'), $zm->get_added_dir_count() + $zm->get_added_file_count())); // Work out percentage progress on items if (0 < $total_count) { $percentage_complete = (int) (($zm->get_added_dir_count() + $zm->get_added_file_count()) / $total_count * 100); $this->log('details', sprintf(__('Zip process reported: Accumulated bursts requested %1$s%% of %2$s (directories + files) total items to be added to backup zip archive (final)', 'it-l10n-backupbuddy'), $percentage_complete, $total_count)); } // Always logging to file one way or another // Always scan the output/logfile for warnings, etc. and show warnings even if user has chosen to ignore them try { $logfile = new SplFileObject($logfile_name, "rb"); while (!$logfile->eof()) { $line = $logfile->current(); $id = $logfile->key(); // Use the line number as unique key for later sorting $logfile->next(); if (preg_match('/^\\s*(zip warning:)/i', $line)) { // Looking for specific types of warning - in particular want the warning that // indicates a file couldn't be read as we want to treat that as a "skipped" // warning that indicates that zip flagged this as a potential problem but // created the zip file anyway - but it would have generated the non-zero exit // code of 18 and we key off that later. All other warnings are not considered // reasons to return a non-zero exit code whilst still creating a zip file so // we'll follow the lead on that and not have other warning types halt the backup. // So we'll try and look for a warning output that looks like it is file related... if (preg_match('/^\\s*(zip warning:)\\s*([^:]*:)\\s*(.*)/i', $line, $matches)) { // Matched to what looks like a file related warning so check particular cases switch (strtolower($matches[2])) { case "could not open for reading:": $zip_warnings[self::ZIP_WARNING_SKIPPED][$id] = trim($line); $zip_warnings_count++; break; case "filtered:": $zip_warnings[self::ZIP_WARNING_FILTERED][$id] = trim($line); $zip_warnings_count++; break; case "filename too long:": $zip_warnings[self::ZIP_WARNING_LONGPATH][$id] = trim($line); $zip_warnings_count++; break; case "unknown add status:": $zip_warnings[self::ZIP_WARNING_GENERIC][$id] = trim($line); $zip_warnings_count++; break; case "name not matched:": $zip_other[self::ZIP_OTHER_GENERIC][$id] = trim($line); $zip_other_count++; break; default: $zip_warnings[self::ZIP_WARNING_GENERIC][$id] = trim($line); $zip_warnings_count++; } } else { // Didn't match to what would look like a file related warning so count it regardless $zip_warnings[self::ZIP_WARNING_GENERIC][$id] = trim($line); $zip_warnings_count++; } } elseif (preg_match('/^\\s*(zip info:)/i', $line)) { // An informational may have associated reason and filename so // check for that if (preg_match('/^\\s*(zip info:)\\s*([^:]*:)\\s*(.*)/i', $line, $matches)) { // Matched to what looks like a file related info so check particular cases switch (strtolower($matches[2])) { case "ignored symlink:": $zip_other[self::ZIP_OTHER_IGNORED_SYMLINK][$id] = trim($line); $zip_other_count++; break; default: $zip_other[self::ZIP_OTHER_GENERIC][$id] = trim($line); $zip_other_count++; } } else { // Didn't match to what would look like a file related info so count it regardless $zip_other[self::ZIP_OTHER_GENERIC][$id] = trim($line); $zip_other_count++; } } elseif (preg_match('/^\\s*(zip error:)/i', $line)) { $zip_errors[$id] = trim($line); $zip_errors_count++; } elseif (preg_match('/^\\s*(adding:)/i', $line)) { // Currently not processing additions entried //$zip_additions[] = trim( $line ); //$zip_additions_count++; } elseif (preg_match('/^\\s*(sd:)/i', $line)) { $zip_debug[$id] = trim($line); $zip_debug_count++; } elseif (preg_match('/^.*(skipped:)\\s*(?P<skipped>\\d+)/i', $line, $matches)) { // Each burst may have some skipped files and each will report separately if (isset($matches['skipped'])) { $zip_skipped_count += $matches['skipped']; } } else { // Currently not processing other entries //$zip_other[] = trim( $line ); //$zip_other_count++; } } unset($logfile); @unlink($logfile_name); } catch (Exception $e) { // Something fishy - we should have been able to open the log file... $error_string = $e->getMessage(); $this->log('details', sprintf(__('Zip process reported: Zip log file could not be opened - error reported: %1$s', 'it-l10n-backupbuddy'), $error_string)); } // Set convenience flags $have_zip_warnings = 0 < $zip_warnings_count; $have_zip_errors = 0 < $zip_errors_count; $have_zip_additions = 0 < $zip_additions_count; $have_zip_debug = 0 < $zip_debug_count; $have_zip_other = 0 < $zip_other_count; // Always report the exit code regardless of whether we might ignore it or not $this->log('details', __('Zip process reported: Zip process exit code: ', 'it-l10n-backupbuddy') . $exitcode); // Always report the number of warnings - even just to confirm that we didn't have any $this->log('details', sprintf(__('Zip process reported: %1$s warning%2$s', 'it-l10n-backupbuddy'), $zip_warnings_count, 1 == $zip_warnings_count ? '' : 's')); // Always report warnings regardless of whether user has selected to ignore them if (true === $have_zip_warnings) { $this->log_zip_reports($zip_warnings, self::$_warning_desc, "WARNING", self::MAX_WARNING_LINES_TO_SHOW, dirname(dirname($tempdir)) . DIRECTORY_SEPARATOR . 'pb_backupbuddy' . DIRECTORY_SEPARATOR . self::ZIP_WARNINGS_FILE_NAME); } // Always report other reports regardless if (true === $have_zip_other) { // Only report number of informationals if we have any as they are not that important $this->log('details', sprintf(__('Zip process reported: %1$s information%2$s', 'it-l10n-backupbuddy'), $zip_other_count, 1 == $zip_other_count ? 'al' : 'als')); $this->log_zip_reports($zip_other, self::$_other_desc, "INFORMATION", self::MAX_OTHER_LINES_TO_SHOW, dirname(dirname($tempdir)) . DIRECTORY_SEPARATOR . 'pb_backupbuddy' . DIRECTORY_SEPARATOR . self::ZIP_OTHERS_FILE_NAME); } // See if we can figure out what happened - note that $exitcode could be non-zero for actionable warning(s) or error // if ( (no zip file) or (fatal exit code) or (not ignoring warnable exit code) ) // TODO: Handle condition testing with function calls based on mapping exit codes to exit type (fatal vs non-fatal) if (!@file_exists($temp_zip) || 0 != $exitcode && 18 != $exitcode || 18 == $exitcode && !$this->get_ignore_warnings()) { // If we have any zip errors reported show them regardless if (true == $have_zip_errors) { $this->log('details', sprintf(__('Zip process reported: %1$s error%2$s', 'it-l10n-backupbuddy'), $zip_errors_count, 1 == $zip_errors_count ? '' : 's')); foreach ($zip_errors as $line) { $this->log('details', __('Zip process reported: ', 'it-l10n-backupbuddy') . $line); } } // Report whether or not the zip file was created (this will always be in the temporary location) if (!@file_exists($temp_zip)) { $this->log('details', __('Zip process reported: Zip Archive file not created - check process exit code.', 'it-l10n-backupbuddy')); } else { $this->log('details', __('Zip process reported: Zip Archive file created but with errors/actionable-warnings so will be deleted - check process exit code and warnings.', 'it-l10n-backupbuddy')); } // The operation has failed one way or another. Note that for pclzip the zip file is always created in the temporary // location regardless of whether the user selected to ignore errors or not (we can never guarantee to create a valid // zip file because the script might be terminated by the server so we must wait to produce a valid file and then // move it to the final location if it is valid). // Therefore if there is a zip file (produced but with warnings) it will not be visible and will be deleted when the // temporary directory is deleted below. $result = false; } else { // Got file with no error or warnings _or_ with warnings that the user has chosen to ignore // File always built in temporary location so always need to move it $this->log('details', __('Zip process reported: Moving Zip Archive file to local archive directory.', 'it-l10n-backupbuddy')); // Make sure no stale file information clearstatcache(); // Relocate the temporary zip file to final location @rename($temp_zip, $zip); // Check that we moved the file ok if (@file_exists($zip)) { $this->log('details', __('Zip process reported: Zip Archive file moved to local archive directory.', 'it-l10n-backupbuddy')); $this->log('message', __('Zip process reported: Zip Archive file successfully created with no errors (any actionable warnings ignored by user settings).', 'it-l10n-backupbuddy')); $this->log_archive_file_stats($zip, array('content_size' => $total_size)); // Temporary for now - try and incorporate into stats logging (makes the stats logging function part of the zip helper class?) $this->log('details', sprintf(__('Zip process reported: Zip Archive file size: %1$s of %2$s (directories + files) actually added', 'it-l10n-backupbuddy'), $zm->get_added_dir_count() + $zm->get_added_file_count() - $zip_skipped_count, $total_count)); // Work out percentage on items if (0 < $total_count) { $percentage_complete = (int) (($zm->get_added_dir_count() + $zm->get_added_file_count() - $zip_skipped_count) / $total_count * 100); $this->log('details', sprintf(__('Zip process reported: Zip archive file size: %1$s%% of %2$s (directories + files) actually added', 'it-l10n-backupbuddy'), $percentage_complete, $total_count)); } $result = true; } else { $this->log('details', __('Zip process reported: Zip Archive file could not be moved to local archive directory.', 'it-l10n-backupbuddy')); $result = false; } } } // Cleanup the temporary directory that will have all detritus and maybe incomplete zip file $this->log('details', __('Zip process reported: Removing temporary directory.', 'it-l10n-backupbuddy')); if (!$this->delete_directory_recursive($tempdir)) { $this->log('details', __('Zip process reported: Temporary directory could not be deleted: ', 'it-l10n-backupbuddy') . $tempdir); } // if ( null != $za ) { unset( $za ); } return $result; }
/** * create_generic() * * A function that creates an archive file * * The $excludes will be a list or relative path excludes if the $listmaker object is NULL otehrwise * will be absolute path excludes and relative path excludes can be had from the $listmaker object * * @param string $zip Full path & filename of ZIP Archive file to create * @param string $dir Full path of directory to add to ZIP Archive file * @parame array $excludes List of either absolute path exclusions or relative exclusions * @param string $tempdir [Optional] Full path of directory for temporary usage * @return bool True if the creation was successful, false otherwise * */ protected function create_generic($zip, $dir, $excludes, $tempdir) { $exitcode = 255; $output = array(); $zippath = ''; $command = ''; $temp_zip = ''; $excluding_additional = false; $exclude_count = 0; $exclusions = array(); $have_zip_errors = false; $zip_errors_count = 0; $zip_errors = array(); $have_zip_warnings = false; $zip_warnings_count = 0; $zip_warnings = array(); $have_zip_additions = false; $zip_additions_count = 0; $zip_additions = array(); $have_zip_debug = false; $zip_debug_count = 0; $zip_debug = array(); $have_zip_other = false; $zip_other_count = 0; $zip_other = array(); $zip_skipped_count = 0; $zip_using_log_file = false; $logfile_name = ''; $zip_ignoring_symlinks = false; $zh = NULL; $lister = NULL; $visitor = NULL; $total_size = 0; $the_list = array(); // The basedir must have a trailing normalized directory separator $basedir = rtrim(trim($dir), self::DIRECTORY_SEPARATORS) . self::NORM_DIRECTORY_SEPARATOR; // Normalize platform specific directory separators in path $basedir = str_replace(DIRECTORY_SEPARATOR, self::NORM_DIRECTORY_SEPARATOR, $basedir); // Ensure no stale file information clearstatcache(); // Create the helper function here so we can use it outside of the post-add // function. Using all defaults so includes multi-burst and server tickling // for now but with options we can modify this. $zh = new pb_backupbuddy_exec_helper(); // Note: could enforce trailing directory separator for robustness if (empty($tempdir) || !file_exists($tempdir)) { // This breaks the rule of single point of exit (at end) but it's early enough to not be a problem pb_backupbuddy::status('details', __('Temporary working directory must be available.', 'it-l10n-backupbuddy')); return false; } pb_backupbuddy::status('message', __('Using Exec Mode.', 'it-l10n-backupbuddy')); // Tell which zip version is being used $version = $this->get_zip_version(); if (true === is_array($version)) { 2 == $version['major'] && 0 == $version['minor'] ? $version['minor'] = 'X' : true; pb_backupbuddy::status('details', sprintf(__('Using zip version: %1$s.%2$s', 'it-l10n-backupbuddy'), $version['major'], $version['minor'])); } else { $version = array("major" => "X", "minor" => "Y"); pb_backupbuddy::status('details', sprintf(__('Using zip version: %1$s.%2$s', 'it-l10n-backupbuddy'), $version['major'], $version['minor'])); } // Get the command path for the zip command - should return a trimmed string $zippath = $this->get_command_path(self::COMMAND_ZIP_PATH); // Determine if we are using an absolute path if (!empty($zippath)) { pb_backupbuddy::status('details', __('Using absolute zip path: ', 'it-l10n-backupbuddy') . $zippath); } // Add the trailing slash if required $command = $this->slashify($zippath) . 'zip'; // Let's inform what we are excluding/including if (count($excludes) > 0) { pb_backupbuddy::status('details', __('Calculating directories/files to exclude from backup (relative to site root).', 'it-l10n-backupbuddy')); foreach ($excludes as $exclude) { if (!strstr($exclude, 'backupbuddy_backups')) { // Set variable to show we are excluding additional directories besides backup dir. $excluding_additional = true; } pb_backupbuddy::status('details', __('Excluding', 'it-l10n-backupbuddy') . ': ' . $exclude); $exclude_count++; } } if (true === $excluding_additional) { pb_backupbuddy::status('message', __('Excluding archives directory and additional directories defined in settings.', 'it-l10n-backupbuddy') . ' ' . $exclude_count . ' ' . __('total', 'it-l10n-backupbuddy') . '.'); } else { pb_backupbuddy::status('message', __('Only excluding archives directory based on settings.', 'it-l10n-backupbuddy') . ' ' . $exclude_count . ' ' . __('total', 'it-l10n-backupbuddy') . '.'); } pb_backupbuddy::status('message', __('Zip process reported: Determining list of file + directories to be added to the zip archive', 'it-l10n-backupbuddy')); // Now let's create the list of files and empty (vacant) directories to include in the backup. // Note: we can only include vacant directories (those that had no content in the first place). // An empty directory may have had content that was excluded but if we give this directory to // pclzip it automatically recurses down into it (we have no control over that) which would then // mess up the exclusions. Make sure the visitor only retains a subset of the fields that we need // here so as to keep memory usage down. $visitor = new pluginbuddy_zbdir_visitor_details(array('filename', 'directory', 'vacant', 'relative_path', 'size')); $options = array('exclusions' => $excludes, 'pattern_exclusions' => array(), 'inclusions' => array(), 'pattern_inclusions' => array(), 'keep_tree' => false, 'ignore_symlinks' => $this->get_ignore_symlinks(), 'visitor' => $visitor); try { $lister = new pluginbuddy_zbdir($basedir, $options); // As we are not keeping the tree we haev already done the visitor pass // as the tree was built so our visitor contains all the information we // need so we can destroy the lister object unset($lister); $result = true; pb_backupbuddy::status('message', __('Zip process reported: Determined list of file + directories to be added to the zip archive', 'it-l10n-backupbuddy')); } catch (Exception $e) { // We couldn't build the list as required so need to bail $error_string = $e->getMessage(); pb_backupbuddy::status('details', sprintf(__('Zip process reported: Unable to determine list of files + directories for backup - error reported: %1$s', 'it-l10n-backupbuddy'), $error_string)); // TODO: Should do some cleanup of any temporary directory, visitor, etc. but not for now $result = false; } // In case that took a while use the helper to try and keep the process alive // Calling burst_end() here as a kludge for now $zh->burst_end(); if (true === $result) { // Now we have our flat file/directory list from the visitor - remember we didn't // keep the tree as we shouldn't need it for anything else as we can get all we need // from the visitor. We'll get a list of the subset of things we need from the visitor // so we can get rid of the visitor later. We'll use this list later to create our // partial inclusion list files to feed to zip for each burst. $the_list = $visitor->get_as_array(array('filename', 'directory', 'vacant', 'relative_path', 'size')); // Need to remove empty values now so that we don't get misleading values // Here "empty value" means there is no actual path that zip would be able to // add and so this would have had to have been ignored later and if we counted // it as "vaid" now then numebrs would be awry. In this case it is probably // _only_ the entry that would be for the / directory which actually has a // empty relative path and filename. foreach ($the_list as $key => $value) { if (empty($value['relative_path']) && empty($value['filename'])) { unset($the_list[$key]); } } pb_backupbuddy::status('details', sprintf(__('Zip process reported: %1$s (directories + files) will be requested to be added to backup zip archive', 'it-l10n-backupbuddy'), count($the_list))); //$zh->set_options( array( 'directory_count' => ( $visitor->count( 'directory' => true ), 'file_count' => $visitor->count( array( 'directory' => false ) ) ) ); // Find the sum total size of all non-directory (i.e., file) items $total_size = 0; foreach ($the_list as $the_item) { if (false === $the_item['directory']) { $total_size += (int) $the_item['size']; } } pb_backupbuddy::status('details', sprintf(__('Zip process reported: %1$s bytes will be requested to be added to backup zip archive', 'it-l10n-backupbuddy'), $total_size)); //$zh->set_options( array( 'content_size' => $total_size ) ); // Retain this for reference for now // file_put_contents( ( dirname( $tempdir ) . DIRECTORY_SEPARATOR . self::ZIP_CONTENT_FILE_NAME ), print_r( $the_list, true ) ); // Presently we don't need the visitor any longer so we can free up some // memory by deleting. We have all we need in $the_list and we will use this // to create our burst content lists unset($visitor); } // Only continue if we have a valid list // This isn't ideal at present but will suffice if (true === $result) { // Check if the version of zip in use supports log file (which will help with memory usage for large sites) if (true === $this->get_zip_supports_log_file()) { // Choose to use log file so quieten stdout - we'll set up the log file later $command .= ' -q'; $zip_using_log_file = true; } // Check if we need to turn off compression by settings (faster but larger backup) if (true !== $this->get_compression()) { $command .= ' -0'; pb_backupbuddy::status('details', __('Zip archive creation compression disabled based on settings.', 'it-l10n-backupbuddy')); } else { pb_backupbuddy::status('details', __('Zip archive creation compression enabled based on settings.', 'it-l10n-backupbuddy')); } // Check if ignoring (not following) symlinks if (true === $this->get_ignore_symlinks()) { // Not all OS support this for command line zip but best to handle it late and just // indicate here it is requested but not supported by OS switch ($this->get_os_type()) { case self::OS_TYPE_NIX: // Want to not follow symlinks so set command option and set flag for later use $command .= ' -y'; $zip_ignoring_symlinks = true; pb_backupbuddy::status('details', __('Zip archive creation symbolic links will not be followed based on settings.', 'it-l10n-backupbuddy')); break; case self::OS_TYPE_WIN: pb_backupbuddy::status('details', __('Zip archive creation symbolic links requested to not be followed based on settings but this option is not supported on this operating system.', 'it-l10n-backupbuddy')); break; default: pb_backupbuddy::status('details', __('Zip archive creation symbolic links requested to not be followed based on settings but this option is not supported on this operating system.', 'it-l10n-backupbuddy')); } } else { pb_backupbuddy::status('details', __('Zip archive creation symbolic links will be followed based on settings.', 'it-l10n-backupbuddy')); } // Check if we are ignoring warnings - meaning can still get a backup even // if, e.g., some files cannot be read if (true === $this->get_ignore_warnings()) { // Note: warnings are being ignored but will still be gathered and logged pb_backupbuddy::status('details', __('Zip archive creation actionable warnings will be ignored based on settings.', 'it-l10n-backupbuddy')); } else { pb_backupbuddy::status('details', __('Zip archive creation actionable warnings will not be ignored based on settings.', 'it-l10n-backupbuddy')); } // We want to "grow" a file with each successive "burst" after the first. If the zip // file doesn't exist when -g is given it will be created - but the problem is that // zip also throws a warning and if we are not ignoring warnings we get caught on this. // We could filter out this warning but that would be fiddly - so instead let's we'll // need to be able to switch the option off/on somehow. We could have two copies of // teh command string, one with and one without, but that is a bit messy. Or we could // have the command in two parts that we splice together (also messy). We could append // it when we need it but that might not be compatible with the specific form of the // command in all cases. We could put it in and then on the first call use a filtered // command string without it. We could have a separate command "object" that we use // to build the command each time we want it and we can turn the option on/off. // All of these are possible but which is simplest? $command .= ' -g'; // Now we'll set up the logging to file if required - use full logging // If using log file we want to append to any existing on each burst. // In the case where not using a log file we get the output in any array // and we'll simply accumulate the arrays for each burst. When complete // we process the whole log file or the aggrgate array - this is simpler // than trying to process results as we go. if (true === $zip_using_log_file) { $logfile_name = $tempdir . self::ZIP_LOG_FILE_NAME; $command .= " -lf '{$logfile_name}' -li -la"; } // Set temporary directory to store ZIP while it's being generated. $command .= " -b '{$tempdir}'"; // Specify where to place the finalized zip archive file // If warnings are being ignored we can tell zip to create the zip archive in the final // location - otherwise we must put it in a temporary location and move it later only // if there are no warnings. This copes with the case where (this) controlling script // gets timed out by the server and if the file were created in the final location with // warnings that should not be ignored we cannot prevent it being created. The -MM option // could be used but this prevents us catching such warnings and being able to report // them to the user in the case where the script hasn't been terminated. Additionally the // -MM option would bail out on the first encountered problem and so if there were a few // problems they would each not be found until the current one is fixed and try again. // TODO: This will have to change when we start to use burst modes properly because we // have to keep the zip file in the temporary directory after each burst has grown it // and we can only move it to teh final location when complete. This is much like pclzip // works anyway and this mode for exec was never actually a design feature but just a // convenienec in some cases that should not be needed now anyway. // if ( true === $this->get_ignore_warnings() ) { // // $temp_zip = $zip; // // } else { // // $temp_zip = $tempdir . basename( $zip ); // // } // Temporary zip file is _always_ located in the temp dir now $temp_zip = $tempdir . basename($zip); $command .= " '{$temp_zip}' ."; // Now create the inclusions file in the tempdir $ifile = $tempdir . self::ZIP_INCLUSIONS_FILE_NAME; // Now the tricky bit - we have to determine how we are going to give the lisy of files // to zip to use. Preferred way would be as a parameter that tells it to include the // files listed in the file. Unfortunately there is no such option for zip - a list of // files to include in a zip can only be given as discrete file names on the command line // or read from stdin. Giving a long list of names on the command line is not // feasible so we have to use a stdin based method which is either to cat the file and // pipe it in to zip or we can use an stdin file descriptor redirection to fetch the // contents of the file. We can only use these methods safely on *nix systems and when // exec_dir is not in use. // When we cannot use the stdin approach we have to resort to using the -i@file // parameter along with the -r recursion option so that zip will match the "patterns" // we give it in the file as it recurses the directory tree. This is not an ideal solution // because the recursion can be slow on some servers where there is a big directory tree // and much of it is irrelevant and does not belong to the site - but we have no other choice. // We shouldn't have to use this method very much and it should be ok in many cases // where there isn't much that is superfluous in the directory tree. // So let's make up the final command to execute based on the operational environment // and then we can simply "reuse" the command on each burst, with the addition of the // -g option on bursts after the first. if (true === $this->get_exec_dir_flag() || self::OS_TYPE_WIN === $this->get_os_type()) { // We are running on Windows or using exec_dir so have to use -r and -i@file $command .= " -r -i@" . "'{$ifile}'"; } else { // We aer running under a nice *nix environment so we can use a stdin redirection // approach. Let's just use redirection for now as that avoids having to use cat and // piping. // $command .= " -@"; // $command = "cat '{$ifile}' | " . $command; $command .= " -@"; $command .= " <'{$ifile}'"; } // If we can't use a log file but exec_dir isn't in use we can redirect stderr to stdout // If exec_dir is in use we cannot redirect because of command line escaping so cannot log errors/warnings if (false === $zip_using_log_file) { if (false === $this->get_exec_dir_flag()) { $command .= ' 2>&1'; } else { pb_backupbuddy::status('details', sprintf(__('Zip Errors/Warnings cannot not be logged with this version of zip and exec_dir active', 'it-l10n-backupbuddy'), true)); } } else { // Using log file but need to redirect stderr to null because zip // still seems to send some stuff to stderr as well as to log file. // Note: if exec_dir is in use then we cannot redirect so theer may // be some stuff gets sent to stderr and logged but it's not worth // telling that - if we really need that then we can change the below // into an if/then/else and log the condition. $command .= $this->get_exec_dir_flag() ? "" : " 2>" . $this->get_null_device(); } // Remember our "master" command $master_command = $command; // Remember the "master" inclusions list filename $master_ifile = $ifile; // Use this to memorise the worst exit code we had (where we didn't immediately // bail out because it signalled a bad failure) $max_exitcode = 0; // Do this as close to when we actually want to start monitoring usage // Maybe this is redundant as we have already called this in the constructor. // If we want to do this then we have to call with true to reset monitoring to // start now. $zh->initialize_monitoring_usage(); // Now we have our command prototype we can start bursting // Simply build a burst list based on content size. Currently no // look-ahead so the size will always exceed the current size threshold // by some amount. May consider using a look-ahead to see if the next // item would exceed the threshold in which case don't add it (unless it // would be the only content in which case have to add it but also log // a warning). while (!empty($the_list)) { // Populate the content file for zip $ilist = array(); // Tell helper that we are preparing a new burst $zh->burst_begin(); pb_backupbuddy::status('details', sprintf(__('Zip process reported: Starting burst number: %1$s', 'it-l10n-backupbuddy'), $zh->get_burst_count())); pb_backupbuddy::status('details', sprintf(__('Zip process reported: Current burst size threshold: %1$s bytes', 'it-l10n-backupbuddy'), $zh->get_burst_current_size_threshold())); // Helper keeps track of what is being added to the burst content and will // tell us when the content is sufficient for this burst based on it's // criteria - this can adapt to how each successive burst goes. // The array shifting isn't really very efficient but is functional for now. $item = array_shift($the_list); while (NULL !== $item && false === $zh->burst_content_complete()) { $file = $item['relative_path'] . $item['filename']; // We shouldn't have any empty items here as we should have removed them // earlier, but just in case... if (!empty($file)) { $ilist[] = $file; // Call the helper event handler as we add each file to the list $zh->burst_content_added($item); } // Not reached size threshold yet so get next item $item = array_shift($the_list); } // Since we would have taken a new element off the array specilatively // we need to put it back if we had already reached burst content completion NULL !== $item ? array_unshift($the_list, $item) : false; // Retain this for reference for now //file_put_contents( ( dirname( $tempdir ) . DIRECTORY_SEPARATOR . self::ZIP_CONTENT_FILE_NAME ), print_r( $ilist, true ) ); // Make sure we expunge any previous version of the inclusions file if (file_exists($ifile)) { @unlink($ifile); } // Slight kludge for now to make sure each burst content file is uniquely named $ifile = str_replace(".txt", "_" . $zh->get_burst_count() . ".txt", $master_ifile); $file_ok = @file_put_contents($ifile, implode(PHP_EOL, $ilist) . PHP_EOL); if (false === $file_ok || 0 === $file_ok) { // The file write failed for some reason, e.g., no disk space? We need to // bail and set exit code so that problem is apparent pb_backupbuddy::status('details', sprintf(__('Zip process reported: Unable to write burst content file: `%1$s`', 'it-l10n-backupbuddy'), $ifile)); $exitcode = 255; break; } unset($ilist); // Remember the current directory and change to the directory being added so that "." is valid in command $working_dir = getcwd(); chdir($dir); // For first invocation we must not set the grow option so remove it from the command // Bit of a hack for now. if (1 === $zh->get_burst_count()) { $command = str_replace("-g", "", $master_command); } else { $command = $master_command; } // Make sure we put the correct burst content file name in the command // Slight kludge for now until we build the command line dynamically each burst $command = str_replace($master_ifile, $ifile, $command); $command = self::OS_TYPE_WIN === $this->get_os_type() ? str_replace('\'', '"', $command) : $command; pb_backupbuddy::status('details', sprintf(__('Zip process reported: Burst requests %1$s (directories + files) items with %2$s bytes of content to be added to backup zip archive', 'it-l10n-backupbuddy'), $zh->get_burst_content_count(), $zh->get_burst_content_size())); pb_backupbuddy::status('details', sprintf(__('Zip process reported: Using burst content file: `%1$s`', 'it-l10n-backupbuddy'), $ifile)); pb_backupbuddy::status('details', __('Zip process reported: ') . $this->get_method_tag() . __(' command', 'it-l10n-backupbuddy') . ': ' . $command); // Allow helper to check how the burst goes $zh->burst_start(); // Successive invocations will append to $output array so we don't have to do anything special // If we are using a log file then we have set that to append as well - so in either case when // we finally exit we will have the sum total of the log output from all invocations. @exec($command, $output, $exitcode); // And now we can analyse what happened and plan for next burst if any $zh->burst_stop(); // Wrap up the individual burst handling // Note: because we called exec we basically went into a wait condition and so (on Linux) // we didn't consume any max_execution_time so we never really have to bother about // resetting it. However, it is true that time will have elapsed so if this burst _does_ // take longer than our current burst threshold period then max_execution_time would be // reset - but what this doesn't cover is a _cumulative_ effect of bursts and so we might // consider reworking the mechanism to monitor this separately from the individual burst // period (the confusion relates to this having originally applied to the time based // burst handling fro pclzip rather than teh size based for exec). It could also be more // relevant for Windows that doesn't stop the clock when exec is called. $zh->burst_end(); // Keep a running total of the backup file size (this is temporary code) $temp_zip_stats = pluginbuddy_stat::stat($temp_zip); // Only log anything if we got some valid file stats if (false !== $temp_zip_stats) { pb_backupbuddy::status('details', sprintf(__('Zip process reported: Accumulated zip archive file size: %1$s bytes', 'it-l10n-backupbuddy'), $temp_zip_stats['dsize'])); } pb_backupbuddy::status('details', sprintf(__('Zip process reported: Ending burst number: %1$s', 'it-l10n-backupbuddy'), $zh->get_burst_count())); // Set current working directory back to where we were chdir($working_dir); // We have to check the exit code to decide whether to keep going ot bail out (break). // If we get a 0 exit code ot 18 exit code then keep going and remember we got the 18 // so that we can emit that as the final exit code if applicable. If we get any other // exit code then we must break out immediately. if (0 !== $exitcode && 18 !== $exitcode) { // Zip failure of some sort - must bail out with current exit code break; } else { // Make sure exit code is always the worst we've had so that when // we've done our last burst we drop out with the correct exit code set // This is really to make sure we drop out with exit code 18 if we had // this in _any_ burst as we would keep going and subsequent burst(s) may // return 0. If we had any other non-zero exit code it would be a "fatal" // error and we would have dropped out immediately anyway. $exitcode = $max_exitcode > $exitcode ? $max_exitcode : ($max_exitcode = $exitcode); } } // Convenience for handling different scanarios $result = false; // We can report how many dirs/files added pb_backupbuddy::status('details', sprintf(__('Zip process reported: Accumulated burst requested %1$s (directories + files) items requested to be added to backup zip archive (final)', 'it-l10n-backupbuddy'), $zh->get_added_dir_count() + $zh->get_added_file_count())); // If we used a log file then process the log file - else process output // Always scan the output/logfile for warnings, etc. and show warnings even if user has chosen to ignore them if (true === $zip_using_log_file) { try { $logfile = new SplFileObject($logfile_name, "rb"); while (!$logfile->eof()) { $line = $logfile->current(); $id = $logfile->key(); // Use the line number as unique key for later sorting $logfile->next(); if (preg_match('/^\\s*(zip warning:)/i', $line)) { // Looking for specific types of warning - in particular want the warning that // indicates a file couldn't be read as we want to treat that as a "skipped" // warning that indicates that zip flagged this as a potential problem but // created the zip file anyway - but it would have generated the non-zero exit // code of 18 and we key off that later. All other warnings are not considered // reasons to return a non-zero exit code whilst still creating a zip file so // we'll follow the lead on that and not have other warning types halt the backup. // So we'll try and look for a warning output that looks like it is file related... if (preg_match('/^\\s*(zip warning:)\\s*([^:]*:)\\s*(.*)/i', $line, $matches)) { // Matched to what looks like a file related warning so check particular cases switch (strtolower($matches[2])) { case "could not open for reading:": $zip_warnings[self::ZIP_WARNING_SKIPPED][$id] = trim($line); $zip_warnings_count++; break; case "name not matched:": $zip_other[self::ZIP_OTHER_GENERIC][$id] = trim($line); $zip_other_count++; break; default: $zip_warnings[self::ZIP_WARNING_GENERIC][$id] = trim($line); $zip_warnings_count++; } } else { // Didn't match to what would look like a file related warning so count it regardless $zip_warnings[self::ZIP_WARNING_GENERIC][$id] = trim($line); $zip_warnings_count++; } } elseif (preg_match('/^\\s*(zip error:)/i', $line)) { $zip_errors[$id] = trim($line); $zip_errors_count++; } elseif (preg_match('/^\\s*(adding:)/i', $line)) { // Currently not processing additions entried //$zip_additions[] = trim( $line ); //$zip_additions_count++; } elseif (preg_match('/^\\s*(sd:)/i', $line)) { $zip_debug[$id] = trim($line); $zip_debug_count++; } elseif (preg_match('/^.*(skipped:)\\s*(?P<skipped>\\d+)/i', $line, $matches)) { if (isset($matches['skipped'])) { $zip_skipped_count = $matches['skipped']; } } else { // Currently not processing other entries //$zip_other[] = trim( $line ); //$zip_other_count++; } } unset($logfile); @unlink($logfile_name); } catch (Exception $e) { // Something fishy - we should have been able to open the log file... $error_string = $e->getMessage(); pb_backupbuddy::status('details', sprintf(__('Log file could not be opened - error reported: %1$s', 'it-l10n-backupbuddy'), $error_string)); } } else { // TODO: $output could be large so if we parse it all into separate arrays then may want to shift // out each line and then discard it after copied to another array $id = 0; // Create a unique key (like a line number) for later sorting foreach ($output as $line) { if (preg_match('/^\\s*(zip warning:)/i', $line)) { // Looking for specific types of warning - in particular want the warning that // indicates a file couldn't be read as we want to treat that as a "skipped" // warning that indicates that zip flagged this as a potential problem but // created the zip file anyway - but it would have generated the non-zero exit // code of 18 and we key off that later. All other warnings are not considered // reasons to return a non-zero exit code whilst still creating a zip file so // we'll follow the lead on that and not have other warning types halt the backup. // So we'll try and look for a warning output that looks like it is file related... if (preg_match('/^\\s*(zip warning:)\\s*([^:]*:)\\s*(.*)/i', $line, $matches)) { // Matched to what looks like a file related warning so check particular cases switch (strtolower($matches[2])) { case "could not open for reading:": $zip_warnings[self::ZIP_WARNING_SKIPPED][$id++] = trim($line); $zip_warnings_count++; break; case "name not matched:": $zip_other[self::ZIP_OTHER_GENERIC][$id++] = trim($line); $zip_other_count++; break; default: $zip_warnings[self::ZIP_WARNING_GENERIC][$id++] = trim($line); $zip_warnings_count++; } } else { // Didn't match to what would look like a file related warning so count it regardless $zip_warnings[self::ZIP_WARNING_GENERIC][$id++] = trim($line); $zip_warnings_count++; } } elseif (preg_match('/^\\s*(zip error:)/i', $line)) { $zip_errors[$id++] = trim($line); $zip_errors_count++; } elseif (preg_match('/^\\s*(adding:)/i', $line)) { // Currently not processing additions entried //$zip_additions[] = trim( $line ); //$zip_additions_count++; $id++; } elseif (preg_match('/^\\s*(sd:)/i', $line)) { $zip_debug[$id++] = trim($line); $zip_debug_count++; } elseif (preg_match('/^.*(skipped:)\\s*(?P<skipped>\\d+)/i', $line, $matches)) { if (isset($matches['skipped'])) { $zip_skipped_count = $matches['skipped']; } } else { // Currently not processing other entries //$zip_other[] = trim( $line ); //$zip_other_count++; $id++; } } // Now free up the memory... unset($output); } // Set convenience flags $have_zip_warnings = 0 < $zip_warnings_count; $have_zip_errors = 0 < $zip_errors_count; $have_zip_additions = 0 < $zip_additions_count; $have_zip_debug = 0 < $zip_debug_count; $have_zip_other = 0 < $zip_other_count; // Always report the exit code regardless of whether we might ignore it or not pb_backupbuddy::status('details', __('Zip process exit code: ', 'it-l10n-backupbuddy') . $exitcode); // Always report the number of warnings - even just to confirm that we didn't have any pb_backupbuddy::status('details', sprintf(__('Zip process reported: %1$s warning%2$s', 'it-l10n-backupbuddy'), $zip_warnings_count, 1 == $zip_warnings_count ? '' : 's')); // Always report warnings regardless of whether user has selected to ignore them if (true === $have_zip_warnings) { $this->log_zip_reports($zip_warnings, self::$_warning_desc, "WARNING", self::MAX_WARNING_LINES_TO_SHOW, dirname(dirname($tempdir)) . DIRECTORY_SEPARATOR . 'pb_backupbuddy' . DIRECTORY_SEPARATOR . self::ZIP_WARNINGS_FILE_NAME); } // Always report other reports regardless if (true === $have_zip_other) { // Only report number of informationals if we have any as they are not that important pb_backupbuddy::status('details', sprintf(__('Zip process reported: %1$s information%2$s', 'it-l10n-backupbuddy'), $zip_other_count, 1 == $zip_other_count ? 'al' : 'als')); $this->log_zip_reports($zip_other, self::$_other_desc, "INFORMATION", self::MAX_OTHER_LINES_TO_SHOW, dirname(dirname($tempdir)) . DIRECTORY_SEPARATOR . 'pb_backupbuddy' . DIRECTORY_SEPARATOR . self::ZIP_OTHERS_FILE_NAME); } // See if we can figure out what happened - note that $exitcode could be non-zero for actionable warning(s) or error // if ( (no zip file) or (fatal exit code) or (not ignoring warnable exit code) ) // TODO: Handle condition testing with function calls based on mapping exit codes to exit type (fatal vs non-fatal) if (!@file_exists($temp_zip) || 0 != $exitcode && 18 != $exitcode || 18 == $exitcode && !$this->get_ignore_warnings()) { // If we have any zip errors reported show them regardless if (true === $have_zip_errors) { pb_backupbuddy::status('details', sprintf(__('Zip process reported: %1$s error%2$s', 'it-l10n-backupbuddy'), $zip_errors_count, 1 == $zip_errors_count ? '' : 's')); foreach ($zip_errors as $line) { pb_backupbuddy::status('details', __('Zip process reported: ', 'it-l10n-backupbuddy') . $line); } } // Report whether or not the zip file was created (whether that be in the final or temporary location) if (!@file_exists($temp_zip)) { pb_backupbuddy::status('details', __('Zip Archive file not created - check process exit code.', 'it-l10n-backupbuddy')); } else { pb_backupbuddy::status('details', __('Zip Archive file created but with errors/actionable-warnings so will be deleted - check process exit code and warnings.', 'it-l10n-backupbuddy')); } // The operation has failed one way or another. Note that as the user didn't choose to ignore errors the zip file // is always created in a temporary location and then only moved to final location on success without error or warnings. // Therefore if there is a zip file (produced but with warnings) it will not be visible and will be deleted when the // temporary directory is deleted below. $result = false; } else { // NOTE: Probably the two paths below can be reduced to one because even if we are // ignoring warnings we are still building the zip in temporary location and finally // moving it because we are growing it. // Got file with no error or warnings _or_ with warnings that the user has chosen to ignore if (false === $this->get_ignore_warnings()) { // Because not ignoring warnings the zip archive was built in temporary location so we need to move it pb_backupbuddy::status('details', __('Moving Zip Archive file to local archive directory.', 'it-l10n-backupbuddy')); // Make sure no stale file information clearstatcache(); @rename($temp_zip, $zip); if (@file_exists($zip)) { pb_backupbuddy::status('details', __('Zip Archive file moved to local archive directory.', 'it-l10n-backupbuddy')); pb_backupbuddy::status('message', __('Zip Archive file successfully created with no errors or actionable warnings.', 'it-l10n-backupbuddy')); $this->log_archive_file_stats($zip, array('content_size' => $total_size)); // Temporary for now - try and incorporate into stats logging (makes the stats logging function part of the zip helper class?) pb_backupbuddy::status('details', sprintf(__('Zip Archive file size: %1$s (directories + files) actually added', 'it-l10n-backupbuddy'), $zh->get_added_dir_count() + $zh->get_added_file_count() - $zip_skipped_count)); $result = true; } else { pb_backupbuddy::status('details', __('Zip Archive file could not be moved to local archive directory.', 'it-l10n-backupbuddy')); $result = false; } } else { // With multi-burst we haev to always build the zip in temp location so always have to move it pb_backupbuddy::status('details', __('Moving Zip Archive file to local archive directory.', 'it-l10n-backupbuddy')); // Make sure no stale file information clearstatcache(); @rename($temp_zip, $zip); if (@file_exists($zip)) { pb_backupbuddy::status('details', __('Zip Archive file moved to local archive directory.', 'it-l10n-backupbuddy')); pb_backupbuddy::status('message', __('Zip Archive file successfully created with no errors (any actionable warnings ignored by user settings).', 'it-l10n-backupbuddy')); $this->log_archive_file_stats($zip, array('content_size' => $total_size)); // Temporary for now - try and incorporate into stats logging (makes the stats logging function part of the zip helper class?) pb_backupbuddy::status('details', sprintf(__('Zip Archive file size: %1$s (directories + files) actually added', 'it-l10n-backupbuddy'), $zh->get_added_dir_count() + $zh->get_added_file_count() - $zip_skipped_count)); $result = true; } else { pb_backupbuddy::status('details', __('Zip Archive file could not be moved to local archive directory.', 'it-l10n-backupbuddy')); $result = false; } } } } // Cleanup the temporary directory that will have all detritus and maybe incomplete zip file pb_backupbuddy::status('details', __('Removing temporary directory.', 'it-l10n-backupbuddy')); if (!$this->delete_directory_recursive($tempdir)) { pb_backupbuddy::status('details', __('Temporary directory could not be deleted: ', 'it-l10n-backupbuddy') . $tempdir); } return $result; }
/** * create() * * A function that creates an archive file * * The $excludes will be a list or relative path excludes * * @param string $zip Full path & filename of ZIP Archive file to create * @param string $dir Full path of directory to add to ZIP Archive file * @parame array $excludes List of either absolute path exclusions or relative exclusions * @param string $tempdir Full path of directory for temporary usage * @return bool True if the creation was successful, false otherwise * */ public function create($zip, $dir, $excludes, $tempdir, $listmaker = NULL) { $za = NULL; $result = false; $exitcode = 255; $zip_output = array(); $temp_zip = ''; $excluding_additional = false; $exclude_count = 0; $exclusions = array(); $temp_file_compression_threshold = 5; $pre_add_func = ''; $have_zip_errors = false; $zip_errors_count = 0; $zip_errors = array(); $have_zip_warnings = false; $zip_warnings_count = 0; $zip_warnings = array(); $have_zip_additions = false; $zip_additions_count = 0; $zip_additions = array(); $have_zip_debug = false; $zip_debug_count = 0; $zip_debug = array(); $have_zip_other = false; $zip_other_count = 0; $zip_other = array(); $zip_ignoring_symlinks = false; $symlinks_found = array(); $zh = NULL; $lister = NULL; $visitor = NULL; $total_size = 0; $the_list = array(); $saved_ignored_symdirs = array(); // The basedir must have a trailing normalized directory separator $basedir = rtrim(trim($dir), self::DIRECTORY_SEPARATORS) . self::NORM_DIRECTORY_SEPARATOR; // Normalize platform specific directory separators in path $basedir = str_replace(DIRECTORY_SEPARATOR, self::NORM_DIRECTORY_SEPARATOR, $basedir); // Ensure no stale file information clearstatcache(); // Create the helper function here so we can use it outside of the post-add // function. Using all defaults so includes multi-burst and server tickling // for now but with options we can modify this. $zh = new pb_backupbuddy_pclzip_helper(); // Note: could enforce trailing directory separator for robustness if (empty($tempdir) || !file_exists($tempdir)) { // This breaks the rule of single point of exit (at end) but it's early enough to not be a problem pb_backupbuddy::status('details', __('Temporary working directory must be available.', 'it-l10n-backupbuddy')); return false; } pb_backupbuddy::status('message', __('Using Compatibility Mode.', 'it-l10n-backupbuddy')); pb_backupbuddy::status('message', __('If your backup times out in Compatibility Mode try disabling zip compression in Settings.', 'it-l10n-backupbuddy')); // Check if pclzip temporary directory is already defined - if it is and // PclZip has already been loaded then this may caue a problem with where // temporary files are created if (defined('PCLZIP_TEMPORARY_DIR')) { pb_backupbuddy::status('details', __('PCLZIP_TEMPORARY_DIR already defined - may cause problems if PclZip library already loaded by another plugin', 'it-l10n-backupbuddy') . ': ' . PCLZIP_TEMPORARY_DIR); } // Define in any case so that it is used if possible define('PCLZIP_TEMPORARY_DIR', $tempdir); // Let's inform what we are excluding/including if (count($excludes) > 0) { pb_backupbuddy::status('details', __('Calculating directories/files to exclude from backup (relative to site root).', 'it-l10n-backupbuddy')); foreach ($excludes as $exclude) { if (!strstr($exclude, 'backupbuddy_backups')) { // Set variable to show we are excluding additional directories besides backup dir. $excluding_additional = true; } pb_backupbuddy::status('details', __('Excluding', 'it-l10n-backupbuddy') . ': ' . $exclude); $exclude_count++; } } if (true === $excluding_additional) { pb_backupbuddy::status('message', __('Excluding archives directory and additional directories defined in settings.', 'it-l10n-backupbuddy') . ' ' . $exclude_count . ' ' . __('total', 'it-l10n-backupbuddy') . '.'); } else { pb_backupbuddy::status('message', __('Only excluding archives directory based on settings.', 'it-l10n-backupbuddy') . ' ' . $exclude_count . ' ' . __('total', 'it-l10n-backupbuddy') . '.'); } pb_backupbuddy::status('message', __('Zip process reported: Determining list of file + directories to be added to the zip archive', 'it-l10n-backupbuddy')); // Now let's create the list of files and empty (vacant) directories to include in the backup. // Note: we can only include vacant directories (those that had no content in the first place). // An empty directory may have had content that was excluded but if we give this directory to // pclzip it automatically recurses down into it (we have no control over that) which would then // mess up the exclusions. $visitor = new pluginbuddy_zbdir_visitor_details(); $options = array('exclusions' => $excludes, 'pattern_exclusions' => array(), 'inclusions' => array(), 'pattern_inclusions' => array(), 'keep_tree' => false, 'ignore_symlinks' => $this->get_ignore_symlinks(), 'visitor' => $visitor); try { $lister = new pluginbuddy_zbdir($basedir, $options); $result = true; pb_backupbuddy::status('message', __('Zip process reported: Determined list of file + directories to be added to the zip archive', 'it-l10n-backupbuddy')); } catch (Exception $e) { // We couldn't build the list as required so need to bail $error_string = $e->getMessage(); pb_backupbuddy::status('details', sprintf(__('Zip process reported: Unable to determine list of files + directories for backup - error reported: %1$s', 'it-l10n-backupbuddy'), $error_string)); // TODO: Should do some cleanup of any temporary directory, visitor, etc. but not for now $result = false; } // In case that took a while use the helper to try and keep the process alive // Calling monitor_activity() here $zh->monitor_activity(); if (true === $result) { // Now we have our flat file/directory list from the visitor - remember we didn't // keep the tree as we shouldn't need it for anything else as we can get all we need // from the visitor. First create our list. We have to do this first because we need to // know if we are bypassing ignored symdirs (not including them in the list) so we can // add the number of these to the total number of items from our simple (vacant) directory // and file count total so that the final stats of what was actually added and the details // of what we didn't add will all add up - sounds convoluted, well that's because it is... $backup_list = $visitor->get_as_array(array('filename', 'directory', 'vacant', 'absolute_path')); foreach ($backup_list as $backup_item) { if (false === $backup_item['directory']) { // Not a directory so must be a file (whether symlink or not) so always ass $the_list[] = $backup_item['absolute_path'] . $backup_item['filename']; } elseif (true === $backup_item['directory'] && (isset($backup_item['vacant']) && true === $backup_item['vacant'])) { // It's a directory and has the vacant attribute and it is vacant so we can // safely add it. // We cannot add non-vacant directories because pclzip will recurse into them. // If the directory does not have the vacant attribute that is because it is // a symlink dir that wasn't followed so we neither know whether it is vacant // not empty and so we cannot risk adding it in case it is not empty $the_list[] = $backup_item['absolute_path'] . $backup_item['filename']; } elseif (true === $backup_item['directory'] && !isset($backup_item['vacant'])) { // It's s directory but vacant attribute isn't set then must be an ignored // symlink directory so we'll remember it so we can add it as an informational // at the end of the process $saved_ignored_symdirs[] = $backup_item['absolute_path'] . $backup_item['filename']; } } // We don't need the backup list array any more unset($backup_list); pb_backupbuddy::status('details', sprintf(__('Zip process reported: %1$s (directories + files) will be requested to be added to backup zip archive', 'it-l10n-backupbuddy'), count($the_list) + count($saved_ignored_symdirs))); //$zh->set_options( array( 'directory_count' => ( $visitor->count( 'directory' => true, 'vacant' => true ) + count( $saved_ignored_symdirs ), 'file_count' => $visitor->count( array( 'directory' => false ) ) ) ); $size_list = $visitor->get_as_array(array('directory', 'size')); $total_size = 0; foreach ($size_list as $size_item) { if (false === $size_item['directory']) { $total_size += (int) $size_item['size']; } } unset($size_list); pb_backupbuddy::status('details', sprintf(__('Zip process reported: %1$s bytes will be requested to be added to backup zip archive', 'it-l10n-backupbuddy'), $total_size)); //$zh->set_options( array( 'content_size' => $total_size ) ); // Retain this for reference for now file_put_contents(dirname($tempdir) . DIRECTORY_SEPARATOR . self::ZIP_CONTENT_FILE_NAME, print_r($the_list, true)); // Presently we don't need the visitor any longer so we can free up some // memory by deleting unset($visitor); // Get started with out zip object // Put our final zip file in the temporary directory - it will be moved later $temp_zip = $tempdir . basename($zip); // This should give us a new archive object, of not catch it and bail out try { $za = new pluginbuddy_PclZip($temp_zip); $result = true; } catch (Exception $e) { // Something fishy - the methods indicated pclzip but we couldn't find the class $error_string = $e->getMessage(); pb_backupbuddy::status('details', sprintf(__('pclzip indicated as available method but error reported: %1$s', 'it-l10n-backupbuddy'), $error_string)); $result = false; } } // Only continue if we have a valid list and archive object // This isn't ideal at present but will suffice if (true === $result) { // Basic argument list $arguments = array(); array_push($arguments, $the_list); array_push($arguments, PCLZIP_OPT_REMOVE_PATH, $dir); if (true !== $this->get_compression()) { // Note: don't need to force use of temporary files for compression pb_backupbuddy::status('details', __('Zip archive creation compression disabled based on settings.', 'it-l10n-backupbuddy')); array_push($arguments, PCLZIP_OPT_NO_COMPRESSION); } else { // Note: force the use of temporary files for compression when file size exceeds given value. // This over-rides the "auto-sense" which is based on memory_limit and this _may_ indicate a // memory availability that is higher than reality leading to memory allocation failure if // trying to compress large files. Set the threshold low enough (specify in MB) so that except in // The tightest memory situations we should be ok. Could have option to force use of temporary // files regardless. pb_backupbuddy::status('details', __('Zip archive creation compression enabled based on settings.', 'it-l10n-backupbuddy')); array_push($arguments, PCLZIP_OPT_TEMP_FILE_THRESHOLD, $temp_file_compression_threshold); } // Check if ignoring (not following) symlinks if (true === $this->get_ignore_symlinks()) { // Want to not follow symlinks so set flag for later use $zip_ignoring_symlinks = true; pb_backupbuddy::status('details', __('Zip archive creation symbolic links will be ignored based on settings.', 'it-l10n-backupbuddy')); } else { pb_backupbuddy::status('details', __('Zip archive creation symbolic links will not be ignored based on settings.', 'it-l10n-backupbuddy')); } // Check if we are ignoring warnings - meaning can still get a backup even // if, e.g., some files cannot be read if (true === $this->get_ignore_warnings()) { // Note: warnings are being ignored but will still be gathered and logged pb_backupbuddy::status('details', __('Zip archive creation actionable warnings will be ignored based on settings.', 'it-l10n-backupbuddy')); } else { pb_backupbuddy::status('details', __('Zip archive creation actionable warnings will not be ignored based on settings.', 'it-l10n-backupbuddy')); } // Use anonymous function to weed out the unreadable and non-existent files (common reason for failure) // and possibly symlinks based on user settings. // PclZip will record these files as 'skipped' in the file status and we can post-process to determine // if we had any of these and hence either stop the backup or continue dependent on whether the user // has chosen to ignore warnings or not and/or ignore symlinks or not. // Unfortunately we cannot directly tag the file with the reason why it has been skipped so when we // have to process the skipped items we have to try and work out why it was skipped - but shouldn't // be too hard. // TODO: Consider moving this into the PclZip wrapper and have a method to set the various pre/post // functions or select predefined functions (such as this). if (true) { // Note: This could be simplified - it's written to be extensible but may not need to be $args = '$event, &$header'; $code = ''; $code .= 'static $symlinks = array(); '; $code .= '$result = true; '; // Handle symlinks - keep the two cases of ignoring/not-ignoring separate for now to make logic more // apparent - but could be merged with different conditional handling // For a valid symlink: is_link() -> true; is_file()/is_dir() -> true; file_exists() -> true // For a broken symlink: is_link() -> true; is_file()/is_dir() -> false; file_exists() -> false // Note: pclzip first tests every file using file_exists() before ever trying to add the file so // for a broken symlink it will _always_ error out immediately it discovers a broken symlink so // we never have a chance to filter these out at this stage. if (true === $zip_ignoring_symlinks) { // If it's a symlink or it's neither a file nor a directory then ignore it. A broken symlink // will never get this far because pclzip will have choked on it $code .= 'if ( ( true === $result ) && !( @is_link( $header[\'filename\'] ) ) ) { '; $code .= ' if ( @is_file( $header[\'filename\'] ) || @is_dir( $header[\'filename\'] ) ) { '; $code .= ' $result = true; '; $code .= ' foreach ( $symlinks as $prefix ) { '; $code .= ' if ( !( false === strpos( $header[\'filename\'], $prefix ) ) ) { '; $code .= ' $result = false; '; $code .= ' break; '; $code .= ' } '; $code .= ' } '; $code .= ' } else { '; // $code .= ' error_log( "Neither a file nor a directory (ignoring): \'" . $header[\'filename\'] . "\'" ); '; $code .= ' $result = false; '; $code .= ' } '; $code .= '} else { '; // $code .= ' error_log( "File is a symlink (ignoring): \'" . $header[\'filename\'] . "\'" ); '; $code .= ' $symlinks[] = $header[\'filename\']; '; // $code .= ' error_log( "Symlinks Array: \'" . print_r( $symlinks, true ) . "\'" ); '; $code .= ' $result = false; '; $code .= '} '; } else { // If it's neither a file nor directory then ignore it - a valid symlink will register as a file // or directory dependent on what it is pointing at. A broken symlink will never get this far. $code .= 'if ( ( true === $result ) && ( @is_file( $header[\'filename\'] ) || @is_dir( $header[\'filename\'] ) ) ) { '; $code .= ' $result = true; '; $code .= '} else { '; // $code .= ' error_log( "Neither a file nor a directory (ignoring): \'" . $header[\'filename\'] . "\'" ); '; $code .= ' $result = false; '; $code .= '} '; } // Add the code block for ignoring unreadable files if (true) { $code .= 'if ( ( true === $result ) && ( @is_readable( $header[\'filename\'] ) ) ) { '; $code .= ' $result = true; '; $code .= '} else { '; // $code .= ' error_log( "File not readable: \'" . $header[\'filename\'] . "\'" ); '; $code .= ' $result = false; '; $code .= '} '; } // Return true (to include file) if file passes conditions otherwise false (to skip file) if not $code .= 'return ( ( true === $result ) ? 1 : 0 ); '; $pre_add_func = create_function($args, $code); } // If we had cause to create a pre add function then add it to the argument list here if (!empty($pre_add_func)) { array_push($arguments, PCLZIP_CB_PRE_ADD, $pre_add_func); } // Add a post-add function for progress monitoring, usage data monitoring, // burst handling and server tickling - using the zip helper object // we created earlier $post_add_func = ''; if (true) { $args = '$event, &$header'; $code = ''; $code .= '$result = true; '; $code .= '$zh = pb_backupbuddy_pclzip_helper::get_instance();'; $code .= '$result = $zh->event_handler( $event, $header );'; $code .= 'return $result;'; $post_add_func = create_function($args, $code); } // If we had cause to create a pre add function then add it to the argument list here if (!empty($post_add_func)) { array_push($arguments, PCLZIP_CB_POST_ADD, $post_add_func); } if (@file_exists($zip)) { pb_backupbuddy::status('details', __('Existing ZIP Archive file will be replaced.', 'it-l10n-backupbuddy')); @unlink($zip); } // Now actually create the zip archive file // First implode any embedded array in the argument list and truncate the result if too long // Assume no arrays embedded in arrays - currently no reason for that // TODO: Make the summary length configurable so that can see more if required // TODO: Consider mapping pclzip argument identifiers to string representations for clarity $args = '$item'; $code = 'if ( is_array( $item ) ) { $string_item = implode( ",", $item); return ( ( strlen( $string_item ) <= 50 ) ? $string_item : "List: " . substr( $string_item, 0, 50 ) . "..." ); } else { return $item; }; '; $imploder_func = create_function($args, $code); $imploded_arguments = array_map($imploder_func, $arguments); pb_backupbuddy::status('details', $this->get_method_tag() . __(' command arguments', 'it-l10n-backupbuddy') . ': ' . implode(';', $imploded_arguments)); // Do this as close to when we actually want to start monitoring usage $zh->initialize_monitoring_usage(); $output = call_user_func_array(array(&$za, 'create'), $arguments); // Work out whether we have a problem or not if (is_array($output)) { // It's an array so at least we produced a zip archive $exitcode = 0; // We can report how many dirs/files added according to pclzip pb_backupbuddy::status('details', sprintf(__('Zip process reported: %1$s (directories + files) added to backup zip archive (final)', 'it-l10n-backupbuddy'), $zh->get_added_dir_count() + $zh->get_added_file_count())); // Process the array for any "warnings" or other reportable conditions $id = 0; // Create a unique key (like a line number) for later sorting foreach ($output as $file) { switch ($file['status']) { case "skipped": // First need to filter out any files skipped because under a symlink dir foreach ($symlinks_found as $prefix) { if (!(false === strpos($file['filename'], $prefix))) { $id++; // break out of the foreach and the switch break 2; } } // For skipped files need to determine why it was skipped if (true === $zip_ignoring_symlinks && @is_link($file['filename'])) { // Remember this for filtering other files skipped because in symlink directory $symlinks_found[] = $file['filename']; // Skipped because we are ignoring symlinks and this is a symlink $zip_other[self::ZIP_OTHER_IGNORED_SYMLINK][$id++] = $file['filename']; $zip_other_count++; } else { //Skipped because probably unreadable or non-existent (catch-all for now) $zip_warnings[self::ZIP_WARNING_SKIPPED][$id++] = $file['filename']; $zip_warnings_count++; } break; case "filtered": $zip_warnings[self::ZIP_OTHER_FILTERED][$id++] = $file['filename']; $zip_warnings_count++; break; case "filename_too_long": $zip_warnings[self::ZIP_OTHER_LONGPATH][$id++] = $file['filename']; $zip_warnings_count++; break; default: // Currently not processing "ok" entries $id++; } } // Now also add in INFORMATIONALs for any ignored symdirs because these would not have // been included in the build list. They were not included because pclzip would have attempted // to follow them and then we would have had to "filter" them and all entries that pclzip // would have created under them which is just a wster of time - best to not include at all // at tell the user now that we didnt include them foreach ($saved_ignored_symdirs as $ignored_symdir) { $zip_other[self::ZIP_OTHER_IGNORED_SYMLINK][$id++] = $ignored_symdir; $zip_other_count++; } // Now free up the memory... unset($output); // Set convenience flags $have_zip_warnings = 0 < $zip_warnings_count; $have_zip_other = 0 < $zip_other_count; } else { // Not an array so a bad error code, something we didn't or couldn't catch $exitcode = $za->errorCode(); // Put the error information into an array for consistency $zip_errors[] = $za->errorInfo(true); $zip_errors_count = sizeof($zip_errors); $have_zip_errors = 0 < $zip_errors_count; } // Convenience for handling different scanarios $result = false; // Always report the exit code regardless of whether we might ignore it or not pb_backupbuddy::status('details', __('Zip process exit code: ', 'it-l10n-backupbuddy') . $exitcode); // Always report the number of warnings - even just to confirm that we didn't have any pb_backupbuddy::status('details', sprintf(__('Zip process reported: %1$s warning%2$s', 'it-l10n-backupbuddy'), $zip_warnings_count, 1 == $zip_warnings_count ? '' : 's')); // Always report warnings regardless of whether user has selected to ignore them if (true === $have_zip_warnings) { $this->log_zip_reports($zip_warnings, self::$_warning_desc, "WARNING", self::MAX_WARNING_LINES_TO_SHOW, dirname(dirname($tempdir)) . DIRECTORY_SEPARATOR . 'pb_backupbuddy' . DIRECTORY_SEPARATOR . self::ZIP_WARNINGS_FILE_NAME); } // Always report other reports regardless if (true === $have_zip_other) { // Only report number of informationals if we have any as they are not that important pb_backupbuddy::status('details', sprintf(__('Zip process reported: %1$s information%2$s', 'it-l10n-backupbuddy'), $zip_other_count, 1 == $zip_other_count ? 'al' : 'als')); $this->log_zip_reports($zip_other, self::$_other_desc, "INFORMATION", self::MAX_OTHER_LINES_TO_SHOW, dirname(dirname($tempdir)) . DIRECTORY_SEPARATOR . 'pb_backupbuddy' . DIRECTORY_SEPARATOR . self::ZIP_OTHERS_FILE_NAME); } // See if we can figure out what happened // Note: only expect exitcode to be non-zero for an error we couldn't pre-empt // Note: warnings will cause the operation to be stopped if user hasn't chosen to ignore regardless // of whether we got a zip file (which we most likely did). // Note: a non-zero exitcode and presence of warnings are mutually exclusive if (!@file_exists($temp_zip) || 0 != $exitcode || true == $have_zip_warnings && !$this->get_ignore_warnings()) { // If we have any zip errors reported show them regardless if (true == $have_zip_errors) { pb_backupbuddy::status('details', sprintf(__('Zip process reported: %1$s error%2$s', 'it-l10n-backupbuddy'), $zip_errors_count, 1 == $zip_errors_count ? '' : 's')); foreach ($zip_errors as $line) { pb_backupbuddy::status('details', __('Zip process reported: ', 'it-l10n-backupbuddy') . $line); } } // Report whether or not the zip file was created (this will always be in the temporary location) if (!@file_exists($temp_zip)) { pb_backupbuddy::status('details', __('Zip Archive file not created - check process exit code.', 'it-l10n-backupbuddy')); } else { pb_backupbuddy::status('details', __('Zip Archive file created but with errors/actionable-warnings so will be deleted - check process exit code and warnings.', 'it-l10n-backupbuddy')); } // The operation has failed one way or another. Note that for pclzip the zip file is always created in the temporary // location regardless of whether the user selected to ignore errors or not (we can never guarantee to create a valid // zip file because the script might be terminated by the server so we must wait to produce a valid file and then // move it to the final location if it is valid). // Therefore if there is a zip file (produced but with warnings) it will not be visible and will be deleted when the // temporary directory is deleted below. $result = false; } else { // Got file with no error or warnings _or_ with warnings that the user has chosen to ignore // File always built in temporary location so always need to move it pb_backupbuddy::status('details', __('Moving Zip Archive file to local archive directory.', 'it-l10n-backupbuddy')); // Make sure no stale file information clearstatcache(); // Relocate the temporary zip file to final location @rename($temp_zip, $zip); // Check that we moved the file ok if (@file_exists($zip)) { pb_backupbuddy::status('details', __('Zip Archive file moved to local archive directory.', 'it-l10n-backupbuddy')); pb_backupbuddy::status('message', __('Zip Archive file successfully created with no errors (any actionable warnings ignored by user settings).', 'it-l10n-backupbuddy')); $this->log_archive_file_stats($zip, array('content_size' => $total_size)); // Temporary for now - try and incorporate into stats logging (makes the stats logging function part of the zip helper class?) pb_backupbuddy::status('details', sprintf(__('Zip Archive file size: %1$s (directories + files) actually added', 'it-l10n-backupbuddy'), $zh->get_added_dir_count() + $zh->get_added_file_count())); $result = true; } else { pb_backupbuddy::status('details', __('Zip Archive file could not be moved to local archive directory.', 'it-l10n-backupbuddy')); $result = false; } } } // Cleanup the temporary directory that will have all detritus and maybe incomplete zip file pb_backupbuddy::status('details', __('Removing temporary directory.', 'it-l10n-backupbuddy')); if (!$this->delete_directory_recursive($tempdir)) { pb_backupbuddy::status('details', __('Temporary directory could not be deleted: ', 'it-l10n-backupbuddy') . $tempdir); } if (NULL != $za) { unset($za); } return $result; }
/** * create_generic() * * A function that creates an archive file * * The $excludes will be a list or relative path excludes if the $listmaker object is null otehrwise * will be absolute path excludes and relative path excludes can be had from the $listmaker object * * @param string $zip Full path & filename of ZIP Archive file to create * @param string $dir Full path of directory to add to ZIP Archive file * @parame array $excludes List of either absolute path exclusions or relative exclusions * @param string $tempdir [Optional] Full path of directory for temporary usage * @return bool True if the creation was successful, false otherwise * */ protected function create_generic($zip, $dir, $excludes, $tempdir) { $result = false; $exitcode = 255; $output = array(); $zippath = ''; $command = ''; $temp_zip = ''; $excluding_additional = false; $exclude_count = 0; $exclusions = array(); $have_zip_errors = false; $zip_errors_count = 0; $zip_errors = array(); $have_zip_warnings = false; $zip_warnings_count = 0; $zip_warnings = array(); $have_zip_additions = false; $zip_additions_count = 0; $zip_additions = array(); $have_zip_debug = false; $zip_debug_count = 0; $zip_debug = array(); $have_zip_other = false; $zip_other_count = 0; $zip_other = array(); $zip_skipped_count = 0; $zip_using_log_file = false; $logfile_name = ''; $contentfile_name = ''; $contentfile_fp = 0; $have_more_content = true; $zip_ignoring_symlinks = false; $zm = null; $lister = null; $visitor = null; $logger = null; $total_size = 0; $total_count = 0; $the_list = array(); $zip_error_encountered = false; $zip_period_expired = false; // The basedir must have a trailing normalized directory separator $basedir = rtrim(trim($dir), self::DIRECTORY_SEPARATORS) . self::NORM_DIRECTORY_SEPARATOR; // Normalize platform specific directory separators in path $basedir = str_replace(DIRECTORY_SEPARATOR, self::NORM_DIRECTORY_SEPARATOR, $basedir); // Ensure no stale file information clearstatcache(); // Create the zip monitor function here // Zip monitor will inherit the logger from this object $zm = new pb_backupbuddy_zip_monitor($this); // $zm->set_burst_max_period( self::ZIP_EXEC_DEFAULT_BURST_MAX_PERIOD )->set_burst_threshold_period( 'auto' )->log_parameters(); $zm->set_burst_size_min($this->get_min_burst_content())->set_burst_size_max($this->get_max_burst_content())->set_burst_current_size_threshold($zm->get_burst_size_min())->log_parameters(); // Note: could enforce trailing directory separator for robustness if (empty($tempdir) || !file_exists($tempdir)) { // This breaks the rule of single point of exit (at end) but it's early enough to not be a problem $this->log('details', __('Zip process reported: Temporary working directory must be available.', 'it-l10n-backupbuddy')); return false; } // Log the temporary working directory so we might be able to spot problems $this->log('details', __('Temporary working directory available: ', 'it-l10n-backupbuddy') . '`' . $tempdir . '`'); $this->log('message', __('Zip process reported: Using Exec Mode.', 'it-l10n-backupbuddy')); // Tell which zip version is being used $version = $this->get_zip_version(); if (true === is_array($version)) { 2 == $version['major'] && 0 == $version['minor'] ? $version['minor'] = 'X' : true; $this->log('details', sprintf(__('Zip process reported: Using zip version: %1$s.%2$s', 'it-l10n-backupbuddy'), $version['major'], $version['minor'])); } else { $version = array("major" => "X", "minor" => "Y"); $this->log('details', sprintf(__('Zip process reported: Using zip version: %1$s.%2$s', 'it-l10n-backupbuddy'), $version['major'], $version['minor'])); } // Get the command path for the zip command - should return a trimmed string $zippath = $this->get_command_path(self::COMMAND_ZIP_PATH); // Determine if we are using an absolute path if (!empty($zippath)) { $this->log('details', __('Zip process reported: Using absolute zip path: ', 'it-l10n-backupbuddy') . $zippath); } // Add the trailing slash if required $command = $this->slashify($zippath) . 'zip'; // Notify the start of the step $this->log('details', sprintf(__('Zip process reported: Zip archive initial step started with step period threshold: %1$ss', 'it-l10n-backupbuddy'), $this->get_step_period())); // Let's inform what we are excluding/including if (count($excludes) > 0) { $this->log('details', __('Zip process reported: Calculating directories/files to exclude from backup (relative to site root).', 'it-l10n-backupbuddy')); foreach ($excludes as $exclude) { if (!strstr($exclude, 'backupbuddy_backups')) { // Set variable to show we are excluding additional directories besides backup dir. $excluding_additional = true; } $this->log('details', __('Zip process reported: Excluding', 'it-l10n-backupbuddy') . ': ' . $exclude); $exclude_count++; } } if (true === $excluding_additional) { $this->log('message', __('Zip process reported: Excluding archives directory and additional directories defined in settings.', 'it-l10n-backupbuddy') . ' ' . $exclude_count . ' ' . __('total', 'it-l10n-backupbuddy') . '.'); } else { $this->log('message', __('Zip process reported: Only excluding archives directory based on settings.', 'it-l10n-backupbuddy') . ' ' . $exclude_count . ' ' . __('total', 'it-l10n-backupbuddy') . '.'); } $this->log('message', __('Zip process reported: Determining list of candidate files + directories to be added to the zip archive', 'it-l10n-backupbuddy')); // Now let's create the list of files and empty (vacant) directories to include in the backup. // Note: we can only include vacant directories (those that had no content in the first place). // An empty directory may have had content that was excluded but if we give this directory to // pclzip it automatically recurses down into it (we have no control over that) which would then // mess up the exclusions. Make sure the visitor only retains a subset of the fields that we need // here so as to keep memory usage down. $visitor = new pluginbuddy_zbdir_visitor_details(array('filename', 'directory', 'vacant', 'relative_path', 'size')); // Give the visitor a logger (maybe we should pass ours) and // also a process monitor (give it ours - the "global" one). As // the visitor is called regularly by zbdir as the site is scanned // we can hook the process monitoring into that. // $logger = new pluginbuddy_zipbuddy_logger( 'Zip process reported: ' ); $visitor->set_logger($this->get_logger()); $visitor->set_process_monitor($this->get_process_monitor()); $options = array('exclusions' => $excludes, 'pattern_exclusions' => array(), 'inclusions' => array(), 'pattern_inclusions' => array(), 'keep_tree' => false, 'ignore_symlinks' => $this->get_ignore_symlinks(), 'visitor' => $visitor); try { $lister = new pluginbuddy_zbdir($basedir, $options); // As we are not keeping the tree we haev already done the visitor pass // as the tree was built so our visitor contains all the information we // need so we can destroy the lister object unset($lister); $result = true; $this->log('message', __('Zip process reported: Determined list of candidate files + directories to be added to the zip archive', 'it-l10n-backupbuddy')); } catch (Exception $e) { // We couldn't build the list as required so need to bail $error_string = $e->getMessage(); $this->log('details', sprintf(__('Zip process reported: Unable to determine list of candidate files + directories for backup - error reported: %1$s', 'it-l10n-backupbuddy'), $error_string)); // TODO: Should do some cleanup of any temporary directory, visitor, etc. but not for now $result = false; } // In case that took a while use the monitor to try and keep the process alive $zm->burst_end(); $this->get_process_monitor()->checkpoint(); if (true === $result) { // Now we have our flat file/directory list from the visitor - remember we didn't // keep the tree as we shouldn't need it for anything else as we can get all we need // from the visitor. We'll get a list of the subset of things we need from the visitor // so we can get rid of the visitor later. We'll use this list later to create our // partial inclusion list files to feed to zip for each burst. $the_list = $visitor->get_as_array(array('filename', 'directory', 'vacant', 'relative_path', 'size')); // Need to remove empty values now so that we don't get misleading values // Here "empty value" means there is no actual path that zip would be able to // add and so this would have had to have been ignored later and if we counted // it as "vaid" now then numebrs would be awry. In this case it is probably // _only_ the entry that would be for the / directory which actually has a // empty relative path and filename. foreach ($the_list as $key => $value) { if (empty($value['relative_path']) && empty($value['filename'])) { unset($the_list[$key]); } } // Save the total count of items to be added $total_count = count($the_list); $this->log('details', sprintf(__('Zip process reported: %1$s (directories + files) will be requested to be added to backup zip archive', 'it-l10n-backupbuddy'), $total_count)); //$zm->set_options( array( 'directory_count' => ( $visitor->count( 'directory' => true ), 'file_count' => $visitor->count( array( 'directory' => false ) ) ) ); // Find the sum total size of all non-directory (i.e., file) items // Make sure we can handle >2GB on a 32 bit PHP by using double // Note: Currently assuming no single item >2GB size as using the // basic size as returned by stat(). We'll likely need to change to // use our stat() to allow for up to 4GB item size on 32 bit PHP $total_size = (double) 0; foreach ($the_list as $the_item) { if (false === $the_item['directory']) { $total_size += (int) $the_item['size']; } } $this->log('details', sprintf(__('Zip process reported: %1$s bytes will be requested to be added to backup zip archive', 'it-l10n-backupbuddy'), number_format($total_size, 0, ".", ""))); //$zm->set_options( array( 'content_size' => $total_size ) ); // This is where we want to save the contents list $contentfile_name = $tempdir . self::ZIP_CONTENT_FILE_NAME; // Now push the list to a file $this->log('details', sprintf(__('Zip process reported: Writing zip content list to file: %1$s', 'it-l10n-backupbuddy'), $contentfile_name)); try { $contentfile = new SplFileObject($contentfile_name, "wb"); // Simple way to ensure we don't get a final empty line in file that messes up // the read and json_decode. We could later use different ways such as using // marker arrays at start/end so we can include other stuff maybe but this is // all we need for now. $prefix = ''; foreach ($the_list as $the_item) { $encoded_item = serialize($the_item); // Need to bail out if it looks like we failed to encode the data if (0 === strlen($encoded_item)) { throw new Exception('Serialization of content list data failed'); } $bytes_written = $contentfile->fwrite($prefix . $encoded_item); // Be very careful to make sure we had a valid write - in paticular // make sure we didn't write 0 bytes since even an empty line from the // array should have the PHP_EOL bytes written if (null === $bytes_written || 0 === $bytes_written || strlen($prefix) >= $bytes_written) { throw new Exception('Failed to append to content file during creation'); } $prefix = PHP_EOL; } } catch (Exception $e) { // Something fishy - we should have been able to open and // write to the content file... $error_string = $e->getMessage(); $this->log('details', sprintf(__('Zip process reported: Zip content file could not be created or data not encoded - error reported: %1$s', 'it-l10n-backupbuddy'), $error_string)); // Temporary measure for bailing out on problems creting/appending content file $result = false; } // We are done with populating the content file unset($contentfile); // Retain this for reference for now //file_put_contents( ( dirname( dirname( $tempdir ) ) . DIRECTORY_SEPARATOR . self::ZIP_CONTENT_FILE_NAME ), print_r( $the_list, true ) ); // Presently we don't need the visitor any longer so we can free up some // memory by deleting. We have all we need in $the_list and we will use this // to create our burst content lists unset($visitor); } // Only continue if we have a valid list // This isn't ideal at present but will suffice if (true === $result) { // Check if the version of zip in use supports log file (which will help with memory usage for large sites) if (true === $this->get_zip_supports_log_file()) { // Choose to use log file so quieten stdout - we'll set up the log file later $command .= ' -q'; $zip_using_log_file = true; } // Check if we need to turn off compression by settings (faster but larger backup) if (true !== $this->get_compression()) { $command .= ' -0'; $this->log('details', __('Zip process reported: Zip archive creation compression disabled based on settings.', 'it-l10n-backupbuddy')); } else { $this->log('details', __('Zip process reported: Zip archive creation compression enabled based on settings.', 'it-l10n-backupbuddy')); } // Check if ignoring (not following) symlinks if (true === $this->get_ignore_symlinks()) { // Not all OS support this for command line zip but best to handle it late and just // indicate here it is requested but not supported by OS switch ($this->get_os_type()) { case self::OS_TYPE_NIX: // Want to not follow symlinks so set command option and set flag for later use $command .= ' -y'; $zip_ignoring_symlinks = true; $this->log('details', __('Zip process reported: Zip archive creation symbolic links will not be followed based on settings.', 'it-l10n-backupbuddy')); break; case self::OS_TYPE_WIN: $this->log('details', __('Zip process reported: Zip archive creation symbolic links requested to not be followed based on settings but this option is not supported on this operating system.', 'it-l10n-backupbuddy')); break; default: $this->log('details', __('Zip process reported: Zip archive creation symbolic links requested to not be followed based on settings but this option is not supported on this operating system.', 'it-l10n-backupbuddy')); } } else { $this->log('details', __('Zip process reported: Zip archive creation symbolic links will be followed based on settings.', 'it-l10n-backupbuddy')); } // Check if we are ignoring warnings - meaning can still get a backup even // if, e.g., some files cannot be read if (true === $this->get_ignore_warnings()) { // Note: warnings are being ignored but will still be gathered and logged $this->log('details', __('Zip process reported: Zip archive creation actionable warnings will be ignored based on settings.', 'it-l10n-backupbuddy')); } else { $this->log('details', __('Zip process reported: Zip archive creation actionable warnings will not be ignored based on settings.', 'it-l10n-backupbuddy')); } // We want to "grow" a file with each successive "burst". Because we have // already created the empty zip archive we can always grow. If we hadn't // already created the empty archive the use of grow on the first burst // would throw a warning and if we are not ignoring warnings this would // halt the backup - option would be to ignore that particular warning // but with the already created file that shouldn't be necessary. // Note: still will not "grow" an empty zip but rather changes to "add" // and throws a warning so really the only way to overcome is to actualy // have zip content (a dummy file) $command .= ' -g'; // Set up the log file - if $zip_using_log_file is true it means we can log // directly to the log file from the zip utility so we'll set that up. If it // is false it means the version of zip utility in use cnnot log directly to // file so we'll be accumulating the output of each burst into an array and // at burst completion we'll append the log details to the log file. So in // either case we'll end up with a log file that we process from warnings, etc. // This approach gives us a unified process and also makes it easy to handle // the log over multiple steps if required. $logfile_name = $tempdir . self::ZIP_LOG_FILE_NAME; if (true === $zip_using_log_file) { $command .= " -lf '{$logfile_name}' -li -la"; } // Set temporary directory to store ZIP while it's being generated. $command .= " -b '{$tempdir}'"; // Temporary zip file is _always_ located in the temp dir now and we move it // to the final location after completion if it is a good completion $temp_zip = $tempdir . basename($zip); $command .= " '{$temp_zip}' ."; // Now create the inclusions file in the tempdir $ifile = $tempdir . self::ZIP_INCLUSIONS_FILE_NAME; // Now the tricky bit - we have to determine how we are going to give the lisy of files // to zip to use. Preferred way would be as a parameter that tells it to include the // files listed in the file. Unfortunately there is no such option for zip - a list of // files to include in a zip can only be given as discrete file names on the command line // or read from stdin. Giving a long list of names on the command line is not // feasible so we have to use a stdin based method which is either to cat the file and // pipe it in to zip or we can use an stdin file descriptor redirection to fetch the // contents of the file. We can only use these methods safely on *nix systems and when // exec_dir is not in use. // When we cannot use the stdin approach we have to resort to using the -i@file // parameter along with the -r recursion option so that zip will match the "patterns" // we give it in the file as it recurses the directory tree. This is not an ideal solution // because the recursion can be slow on some servers where there is a big directory tree // and much of it is irrelevant and does not belong to the site - but we have no other choice. // We shouldn't have to use this method very much and it should be ok in many cases // where there isn't much that is superfluous in the directory tree. // So let's make up the final command to execute based on the operational environment // and then we can simply "reuse" the command on each burst, with the addition of the // -g option on bursts after the first. if (true === $this->get_exec_dir_flag() || self::OS_TYPE_WIN === $this->get_os_type()) { // We are running on Windows or using exec_dir so have to use -r and -i@file $command .= " -r -i@" . "'{$ifile}'"; } else { // We aer running under a nice *nix environment so we can use a stdin redirection // approach. Let's just use redirection for now as that avoids having to use cat and // piping. // $command .= " -@"; // $command = "cat '{$ifile}' | " . $command; $command .= " -@"; $command .= " <'{$ifile}'"; } // If we can't use a log file but exec_dir isn't in use we can redirect stderr to stdout // If exec_dir is in use we cannot redirect because of command line escaping so cannot log errors/warnings if (false === $zip_using_log_file) { if (false === $this->get_exec_dir_flag()) { $command .= ' 2>&1'; } else { $this->log('details', sprintf(__('Zip process reported: Zip Errors/Warnings cannot not be logged with this version of zip and exec_dir active', 'it-l10n-backupbuddy'), true)); } } else { // Using log file but need to redirect stderr to null because zip // still seems to send some stuff to stderr as well as to log file. // Note: if exec_dir is in use then we cannot redirect so theer may // be some stuff gets sent to stderr and logged but it's not worth // telling that - if we really need that then we can change the below // into an if/then/else and log the condition. $command .= $this->get_exec_dir_flag() ? "" : " 2>" . $this->get_null_device(); } // Remember our "master" command $master_command = $command; // Remember the "master" inclusions list filename $master_ifile = $ifile; // Use this to memorise the worst exit code we had (where we didn't immediately // bail out because it signalled a bad failure) $max_exitcode = 0; // Do this as close to when we actually want to start monitoring usage // Maybe this is redundant as we have already called this in the constructor. // If we want to do this then we have to call with true to reset monitoring to // start now. $this->get_process_monitor()->initialize_monitoring_usage(); // Now we have our command prototype we can start bursting // Simply build a burst list based on content size. Currently no // look-ahead so the size will always exceed the current size threshold // by some amount. May consider using a look-ahead to see if the next // item would exceed the threshold in which case don't add it (unless it // would be the only content in which case have to add it but also log // a warning). // We'll stop either when noting more to add or we have exceeded our step // period or we have encountered an error. // Note: we might bail out immediately if previous processing has already // caused us to exceed the step period. while ($have_more_content && !($zip_period_expired = $this->exceeded_step_period($this->get_process_monitor()->get_elapsed_time())) && !$zip_error_encountered) { // Populate the content file for zip $ilist = array(); // Tell helper that we are preparing a new burst $zm->burst_begin(); $this->log('details', sprintf(__('Zip process reported: Starting burst number: %1$s', 'it-l10n-backupbuddy'), $zm->get_burst_count())); $this->log('details', sprintf(__('Zip process reported: Current burst size threshold: %1$s bytes', 'it-l10n-backupbuddy'), number_format($zm->get_burst_current_size_threshold(), 0, ".", ""))); // Open the content list file and seek to the "current" position. This // will be initially zero and then updated after each burst. For multi-step // it will be zero on the first step and then would be passed back in // as a parameter on subsequent steps based on where in the file the previous // step reached. // TODO: Maybe a sanity check to make sure position seems tenable try { $contentfile = new SplFileObject($contentfile_name, "rb"); $contentfile->fseek($contentfile_fp); // Helper keeps track of what is being added to the burst content and will // tell us when the content is sufficient for this burst based on it's // criteria - this can adapt to how each successive burst goes. while (!$contentfile->eof() && false === $zm->burst_content_complete()) { // Should be at least one item to grab from the list and then move to next // and remember it for if we drop out because burst content complete, in // that case we'll return to that point in the file at the next burst start. // Check for unserialize failure and bail $item = @unserialize($contentfile->current()); if (false === $item) { throw new Exception('Unserialization of content list data failed: `' . $contentfile->current() . '`'); } $contentfile->next(); $file = $item['relative_path'] . $item['filename']; // We shouldn't have any empty items here as we should have removed them // earlier, but just in case... if (!empty($file)) { $ilist[] = $file; // Call the helper event handler as we add each file to the list $zm->burst_content_added($item); } } // Burst list is completed by way of end of content list file or size threshold if (!$contentfile->eof()) { // We haven't exhausted the content list yet so remember where we // are at for next burst $contentfile_fp = $contentfile->ftell(); } else { // Exhausted the content list so make sure we drop out after this burst // if we don't break out of the loop due to a zip error or reached step // duration limit $have_more_content = false; } // Finished one way or another so close content list file for this burst unset($contentfile); } catch (Exception $e) { // Something fishy - we should have been able to open the content file... // TODO: We need to bail out totally here I think $error_string = $e->getMessage(); $this->log('details', sprintf(__('Zip process reported: Zip content list file could not be opened/read - error reported: %1$s', 'it-l10n-backupbuddy'), $error_string)); $exitcode = 255; $zip_error_encountered = true; break; } // Retain this for reference for now //file_put_contents( ( dirname( $tempdir ) . DIRECTORY_SEPARATOR . self::ZIP_CONTENT_FILE_NAME ), print_r( $ilist, true ) ); // Make sure we expunge any previous version of the inclusions file if (file_exists($ifile)) { @unlink($ifile); } // Slight kludge for now to make sure each burst content file is uniquely named $ifile = str_replace(".txt", "_" . $zm->get_burst_count() . ".txt", $master_ifile); $file_ok = @file_put_contents($ifile, implode(PHP_EOL, $ilist) . PHP_EOL); if (false === $file_ok || 0 === $file_ok) { // The file write failed for some reason, e.g., no disk space? We need to // bail and set exit code so that problem is apparent $this->log('details', sprintf(__('Zip process reported: Unable to write burst content file: `%1$s`', 'it-l10n-backupbuddy'), $ifile)); $exitcode = 255; $zip_error_encountered = true; break; } unset($ilist); // Remember the current directory and change to the directory being added so that "." is valid in command $working_dir = getcwd(); chdir($dir); // We don't need to remove the -g option from the command on our first burst any // longer because we are creating an initial empty zip that can be grown // Note: Sadly we still need to do add on the first burst to avoid empty zip // warning (until we can have dumy content in the zip to prevent that warning). if (1 === $zm->get_burst_count()) { $command = str_replace("-g", "", $master_command); } else { $command = $master_command; } // $command = $master_command; // Make sure we put the correct burst content file name in the command // Slight kludge for now until we build the command line dynamically each burst $command = str_replace($master_ifile, $ifile, $command); $command = self::OS_TYPE_WIN === $this->get_os_type() ? str_replace('\'', '"', $command) : $command; $this->log('details', sprintf(__('Zip process reported: Burst requests %1$s (directories + files) items with %2$s bytes of content to be added to backup zip archive', 'it-l10n-backupbuddy'), $zm->get_burst_content_count(), $zm->get_burst_content_size())); $this->log('details', sprintf(__('Zip process reported: Using burst content file: `%1$s`', 'it-l10n-backupbuddy'), $ifile)); $this->log('details', __('Zip process reported: ') . $this->get_method_tag() . __(' command', 'it-l10n-backupbuddy') . ': ' . $command); // Allow helper to check how the burst goes $zm->burst_start(); // We need the $output array to contain only output for this burst so // always reset it before invoking exec. $output = array(); @exec($command, $output, $exitcode); // And now we can analyse what happened and plan for next burst if any $zm->burst_stop(); // Wrap up the individual burst handling // Note: because we called exec we basically went into a wait condition and so (on Linux) // we didn't consume any max_execution_time so we never really have to bother about // resetting it. However, it is true that time will have elapsed so if this burst _does_ // take longer than our current burst threshold period then max_execution_time would be // reset - but what this doesn't cover is a _cumulative_ effect of bursts and so we might // consider reworking the mechanism to monitor this separately from the individual burst // period (the confusion relates to this having originally applied to the time based // burst handling fro pclzip rather than teh size based for exec). It could also be more // relevant for Windows that doesn't stop the clock when exec is called. $zm->burst_end(); $this->get_process_monitor()->checkpoint(); // Now if we are not loggign directly to file we need to append the $output array // to the log file - first invocation will create the file. if (false === $zip_using_log_file) { $this->log('details', sprintf(__('Zip process reported: Appending zip burst log detail to zip log file: %1$s', 'it-l10n-backupbuddy'), $logfile_name)); try { $logfile = new SplFileObject($logfile_name, "ab"); foreach ($output as $line) { $bytes_written = $logfile->fwrite($line . PHP_EOL); // Be very careful to make sure we had a valid write - in paticular // make sure we didn't write 0 bytes since even an empty line from the // array should have the PHP_EOL bytes written if (null === $bytes_written || 0 === $bytes_written) { unset($logfile); unset($output); throw new Exception('Failed to append to zip log file during zip creation - zip log details will be incomplete but zip exit code will still be valid'); } } unset($logfile); unset($output); } catch (Exception $e) { // Something fishy - we should have been able to open and // write to the log file... $error_string = $e->getMessage(); $this->log('details', sprintf(__('Zip process reported: Zip log file could not be opened/appended-to - error reported: %1$s', 'it-l10n-backupbuddy'), $error_string)); } } // Report progress at end of burst $this->log('details', sprintf(__('Zip process reported: Accumulated bursts requested %1$s (directories + files) items to be added to backup zip archive (end of burst)', 'it-l10n-backupbuddy'), $zm->get_added_dir_count() + $zm->get_added_file_count())); // Work out percentage progress on items if (0 < $total_count) { $percentage_complete = (int) (($zm->get_added_dir_count() + $zm->get_added_file_count()) / $total_count * 100); $this->log('details', sprintf(__('Zip process reported: Accumulated bursts requested %1$s%% of %2$s (directories + files) total items to be added to backup zip archive (end of burst)', 'it-l10n-backupbuddy'), $percentage_complete, $total_count)); } // Keep a running total of the backup file size (this is temporary code) // Using our stat() function in case file size exceeds 2GB on a 32 bit PHP system $temp_zip_stats = pluginbuddy_stat::stat($temp_zip); // Only log anything if we got some valid file stats if (false !== $temp_zip_stats) { $this->log('details', sprintf(__('Zip process reported: Accumulated zip archive file size: %1$s bytes', 'it-l10n-backupbuddy'), number_format($temp_zip_stats['dsize'], 0, ".", ""))); } $this->log('details', sprintf(__('Zip process reported: Ending burst number: %1$s', 'it-l10n-backupbuddy'), $zm->get_burst_count())); // Set current working directory back to where we were chdir($working_dir); // We have to check the exit code to decide whether to keep going ot bail out (break). // If we get a 0 exit code ot 18 exit code then keep going and remember we got the 18 // so that we can emit that as the final exit code if applicable. If we get any other // exit code then we must break out immediately. if (0 !== $exitcode && 18 !== $exitcode) { // Zip failure of some sort - must bail out with current exit code $zip_error_encountered = true; } else { // Make sure exit code is always the worst we've had so that when // we've done our last burst we drop out with the correct exit code set // This is really to make sure we drop out with exit code 18 if we had // this in _any_ burst as we would keep going and subsequent burst(s) may // return 0. If we had any other non-zero exit code it would be a "fatal" // error and we would have dropped out immediately anyway. $exitcode = $max_exitcode > $exitcode ? $max_exitcode : ($max_exitcode = $exitcode); } // Now inject a little delay until the next burst. This may be required to give the // server time to catch up with finalizing file creation and/or it may be required to // reduce the average load a little so there isn't a sustained "peak" // Theoretically a sleep could be interrupted by a signal and it would return some // non-zero value or false - but if that is the case it probably signals something // more troubling so there is little point in tryng to "handle" such a condition here. if (0 < ($burst_gap = $this->get_burst_gap())) { $this->log('details', sprintf(__('Zip process reported: Starting burst gap delay of: %1$ss', 'it-l10n-backupbuddy'), $burst_gap)); sleep($burst_gap); } } // Exited the loop for some reason so decide what to do now. // If we didn't exit because of exceeding the step period then it's a // normal exit and we'll process accordingly and end up returning true // or false. If we exited because of exceeding step period then we need // to return the current state array to enable next iteration to pick up // where we left off. // Note: we might consider having the zip helper give us a state to // restore on it when we create one again - but for now we'll not do that if ($zip_period_expired) { // Report progress at end of step $this->log('details', sprintf(__('Zip process reported: Accumulated bursts requested %1$s (directories + files) items to be added to backup zip archive (end of step)', 'it-l10n-backupbuddy'), $zm->get_added_dir_count() + $zm->get_added_file_count())); // Work out percentage progress on items if (0 < $total_count) { $percentage_complete = (int) (($zm->get_added_dir_count() + $zm->get_added_file_count()) / $total_count * 100); $this->log('details', sprintf(__('Zip process reported: Accumulated bursts requested %1$s%% of %2$s (directories + files) total items to be added to backup zip archive (end of step)', 'it-l10n-backupbuddy'), $percentage_complete, $total_count)); } $this->log('details', sprintf(__('Zip process reported: Zip archive build step terminated after %1$ss, continuation step will be scheduled', 'it-l10n-backupbuddy'), $this->get_process_monitor()->get_elapsed_time())); // Need to set up the state information we'll need to tell the next // loop how to set things up to continue. Next time around if another // step is required then some of these may be changed and others may // stay the same. // Note: the method tag 'mt' is used to tell zipbuddy exactly which // zipper to use, the one that was picked first time through. $state = array('name' => pluginbuddy_zipbuddy::STATE_NAME_IN_PROGRESS, 'id' => pluginbuddy_zipbuddy::STATE_ID_IN_PROGRESS, 'zipbuddy' => array('mt' => $this->get_method_tag()), 'zipper' => array('fp' => $contentfile_fp, 'mec' => $max_exitcode, 'sp' => $this->get_step_period(), 'root' => $dir, 'ts' => $total_size, 'tc' => $total_count), 'helper' => array('dc' => $zm->get_added_dir_count(), 'fc' => $zm->get_added_file_count())); // Now we can return directly as we haev nothing to clear up return $state; } // Convenience for handling different scanarios $result = false; // We can report how many dirs/files finally added $this->log('details', sprintf(__('Zip process reported: Accumulated bursts requested %1$s (directories + files) items to be added to backup zip archive (final)', 'it-l10n-backupbuddy'), $zm->get_added_dir_count() + $zm->get_added_file_count())); // Work out percentage progress on items if (0 < $total_count) { $percentage_complete = (int) (($zm->get_added_dir_count() + $zm->get_added_file_count()) / $total_count * 100); $this->log('details', sprintf(__('Zip process reported: Accumulated bursts requested %1$s%% of %2$s (directories + files) total items to be added to backup zip archive (final)', 'it-l10n-backupbuddy'), $percentage_complete, $total_count)); } // Always logging to file one way or another // Always scan the output/logfile for warnings, etc. and show warnings even if user has chosen to ignore them try { $logfile = new SplFileObject($logfile_name, "rb"); while (!$logfile->eof()) { $line = $logfile->current(); $id = $logfile->key(); // Use the line number as unique key for later sorting $logfile->next(); if (preg_match('/^\\s*(zip warning:)/i', $line)) { // Looking for specific types of warning - in particular want the warning that // indicates a file couldn't be read as we want to treat that as a "skipped" // warning that indicates that zip flagged this as a potential problem but // created the zip file anyway - but it would have generated the non-zero exit // code of 18 and we key off that later. All other warnings are not considered // reasons to return a non-zero exit code whilst still creating a zip file so // we'll follow the lead on that and not have other warning types halt the backup. // So we'll try and look for a warning output that looks like it is file related... if (preg_match('/^\\s*(zip warning:)\\s*([^:]*:)\\s*(.*)/i', $line, $matches)) { // Matched to what looks like a file related warning so check particular cases switch (strtolower($matches[2])) { case "could not open for reading:": $zip_warnings[self::ZIP_WARNING_SKIPPED][$id] = trim($line); $zip_warnings_count++; break; case "filtered:": $zip_warnings[self::ZIP_WARNING_FILTERED][$id] = trim($line); $zip_warnings_count++; break; case "filename too long:": $zip_warnings[self::ZIP_WARNING_LONGPATH][$id] = trim($line); $zip_warnings_count++; break; case "unknown add status:": $zip_warnings[self::ZIP_WARNING_GENERIC][$id] = trim($line); $zip_warnings_count++; break; case "name not matched:": $zip_other[self::ZIP_OTHER_GENERIC][$id] = trim($line); $zip_other_count++; break; default: $zip_warnings[self::ZIP_WARNING_GENERIC][$id] = trim($line); $zip_warnings_count++; } } else { // Didn't match to what would look like a file related warning so count it regardless $zip_warnings[self::ZIP_WARNING_GENERIC][$id] = trim($line); $zip_warnings_count++; } } elseif (preg_match('/^\\s*(zip info:)/i', $line)) { // An informational may have associated reason and filename so // check for that if (preg_match('/^\\s*(zip info:)\\s*([^:]*:)\\s*(.*)/i', $line, $matches)) { // Matched to what looks like a file related info so check particular cases switch (strtolower($matches[2])) { case "ignored symlink:": $zip_other[self::ZIP_OTHER_IGNORED_SYMLINK][$id] = trim($line); $zip_other_count++; break; default: $zip_other[self::ZIP_OTHER_GENERIC][$id] = trim($line); $zip_other_count++; } } else { // Didn't match to what would look like a file related info so count it regardless $zip_other[self::ZIP_OTHER_GENERIC][$id] = trim($line); $zip_other_count++; } } elseif (preg_match('/^\\s*(zip error:)/i', $line)) { $zip_errors[$id] = trim($line); $zip_errors_count++; } elseif (preg_match('/^\\s*(adding:)/i', $line)) { // Currently not processing additions entried //$zip_additions[] = trim( $line ); //$zip_additions_count++; } elseif (preg_match('/^\\s*(sd:)/i', $line)) { $zip_debug[$id] = trim($line); $zip_debug_count++; } elseif (preg_match('/^.*(skipped:)\\s*(?P<skipped>\\d+)/i', $line, $matches)) { // Each burst may have some skipped files and each will report separately if (isset($matches['skipped'])) { $zip_skipped_count += $matches['skipped']; } } else { // Currently not processing other entries //$zip_other[] = trim( $line ); //$zip_other_count++; } } unset($logfile); @unlink($logfile_name); } catch (Exception $e) { // Something fishy - we should have been able to open the log file... $error_string = $e->getMessage(); $this->log('details', sprintf(__('Zip process reported: Zip log file could not be opened - error reported: %1$s', 'it-l10n-backupbuddy'), $error_string)); } // Set convenience flags $have_zip_warnings = 0 < $zip_warnings_count; $have_zip_errors = 0 < $zip_errors_count; $have_zip_additions = 0 < $zip_additions_count; $have_zip_debug = 0 < $zip_debug_count; $have_zip_other = 0 < $zip_other_count; // Always report the exit code regardless of whether we might ignore it or not $this->log('details', __('Zip process reported: Zip process exit code: ', 'it-l10n-backupbuddy') . $exitcode); // Always report the number of warnings - even just to confirm that we didn't have any $this->log('details', sprintf(__('Zip process reported: %1$s warning%2$s', 'it-l10n-backupbuddy'), $zip_warnings_count, 1 == $zip_warnings_count ? '' : 's')); // Always report warnings regardless of whether user has selected to ignore them if (true === $have_zip_warnings) { $this->log_zip_reports($zip_warnings, self::$_warning_desc, "WARNING", self::MAX_WARNING_LINES_TO_SHOW, dirname(dirname($tempdir)) . DIRECTORY_SEPARATOR . 'pb_backupbuddy' . DIRECTORY_SEPARATOR . self::ZIP_WARNINGS_FILE_NAME); } // Always report other reports regardless if (true === $have_zip_other) { // Only report number of informationals if we have any as they are not that important $this->log('details', sprintf(__('Zip process reported: %1$s information%2$s', 'it-l10n-backupbuddy'), $zip_other_count, 1 == $zip_other_count ? 'al' : 'als')); $this->log_zip_reports($zip_other, self::$_other_desc, "INFORMATION", self::MAX_OTHER_LINES_TO_SHOW, dirname(dirname($tempdir)) . DIRECTORY_SEPARATOR . 'pb_backupbuddy' . DIRECTORY_SEPARATOR . self::ZIP_OTHERS_FILE_NAME); } // See if we can figure out what happened - note that $exitcode could be non-zero for actionable warning(s) or error // if ( (no zip file) or (fatal exit code) or (not ignoring warnable exit code) ) // TODO: Handle condition testing with function calls based on mapping exit codes to exit type (fatal vs non-fatal) if (!@file_exists($temp_zip) || 0 != $exitcode && 18 != $exitcode || 18 == $exitcode && !$this->get_ignore_warnings()) { // If we have any zip errors reported show them regardless if (true === $have_zip_errors) { $this->log('details', sprintf(__('Zip process reported: %1$s error%2$s', 'it-l10n-backupbuddy'), $zip_errors_count, 1 == $zip_errors_count ? '' : 's')); foreach ($zip_errors as $line) { $this->log('details', __('Zip process reported: ', 'it-l10n-backupbuddy') . $line); } } // Report whether or not the zip file was created (whether that be in the final or temporary location) if (!@file_exists($temp_zip)) { $this->log('details', __('Zip process reported: Zip Archive file not created - check process exit code.', 'it-l10n-backupbuddy')); } else { $this->log('details', __('Zip process reported: Zip Archive file created but with errors/actionable-warnings so will be deleted - check process exit code and warnings.', 'it-l10n-backupbuddy')); } // The operation has failed one way or another. Note that as the user didn't choose to ignore errors the zip file // is always created in a temporary location and then only moved to final location on success without error or warnings. // Therefore if there is a zip file (produced but with warnings) it will not be visible and will be deleted when the // temporary directory is deleted below. $result = false; } else { // NOTE: Probably the two paths below can be reduced to one because even if we are // ignoring warnings we are still building the zip in temporary location and finally // moving it because we are growing it. // Got file with no error or warnings _or_ with warnings that the user has chosen to ignore if (false === $this->get_ignore_warnings()) { // Because not ignoring warnings the zip archive was built in temporary location so we need to move it $this->log('details', __('Zip process reported: Moving Zip Archive file to local archive directory.', 'it-l10n-backupbuddy')); // Make sure no stale file information clearstatcache(); @rename($temp_zip, $zip); if (@file_exists($zip)) { $this->log('details', __('Zip process reported: Zip Archive file moved to local archive directory.', 'it-l10n-backupbuddy')); $this->log('message', __('Zip process reported: Zip Archive file successfully created with no errors or actionable warnings.', 'it-l10n-backupbuddy')); $this->log_archive_file_stats($zip, array('content_size' => $total_size)); // Temporary for now - try and incorporate into stats logging (makes the stats logging function part of the zip helper class?) $this->log('details', sprintf(__('Zip process reported: Zip Archive file size: %1$s of %2$s (directories + files) actually added', 'it-l10n-backupbuddy'), $zm->get_added_dir_count() + $zm->get_added_file_count() - $zip_skipped_count, $total_count)); // Work out percentage on items if (0 < $total_count) { $percentage_complete = (int) (($zm->get_added_dir_count() + $zm->get_added_file_count() - $zip_skipped_count) / $total_count * 100); $this->log('details', sprintf(__('Zip process reported: Zip archive file size: %1$s%% of %2$s (directories + files) actually added', 'it-l10n-backupbuddy'), $percentage_complete, $total_count)); } $result = true; } else { $this->log('details', __('Zip process reported: Zip Archive file could not be moved to local archive directory.', 'it-l10n-backupbuddy')); $result = false; } } else { // With multi-burst we haev to always build the zip in temp location so always have to move it $this->log('details', __('Zip process reported: Moving Zip Archive file to local archive directory.', 'it-l10n-backupbuddy')); // Make sure no stale file information clearstatcache(); @rename($temp_zip, $zip); if (@file_exists($zip)) { $this->log('details', __('Zip process reported: Zip Archive file moved to local archive directory.', 'it-l10n-backupbuddy')); $this->log('message', __('Zip process reported: Zip Archive file successfully created with no errors (any actionable warnings ignored by user settings).', 'it-l10n-backupbuddy')); $this->log_archive_file_stats($zip, array('content_size' => $total_size)); // Temporary for now - try and incorporate into stats logging (makes the stats logging function part of the zip helper class?) $this->log('details', sprintf(__('Zip process reported: Zip Archive file size: %1$s of %2$s (directories + files) actually added', 'it-l10n-backupbuddy'), $zm->get_added_dir_count() + $zm->get_added_file_count() - $zip_skipped_count, $total_count)); // Work out percentage on items if (0 < $total_count) { $percentage_complete = (int) (($zm->get_added_dir_count() + $zm->get_added_file_count() - $zip_skipped_count) / $total_count * 100); $this->log('details', sprintf(__('Zip process reported: Zip archive file size: %1$s%% of %2$s (directories + files) actually added', 'it-l10n-backupbuddy'), $percentage_complete, $total_count)); } $result = true; } else { $this->log('details', __('Zip process reported: Zip Archive file could not be moved to local archive directory.', 'it-l10n-backupbuddy')); $result = false; } } } } // Cleanup the temporary directory that will have all detritus and maybe incomplete zip file $this->log('details', __('Zip process reported: Removing temporary directory.', 'it-l10n-backupbuddy')); if (!$this->delete_directory_recursive($tempdir)) { $this->log('details', __('Zip process reported: Temporary directory could not be deleted: ', 'it-l10n-backupbuddy') . $tempdir); } return $result; }