697 lines
17 KiB
PHP
697 lines
17 KiB
PHP
<?php
|
|
/**
|
|
* @package akeebabackupwp
|
|
* @copyright Copyright (c)2014-2019 Nicholas K. Dionysopoulos / Akeeba Ltd
|
|
* @license GNU GPL version 3 or later
|
|
*/
|
|
|
|
namespace Akeeba\WPCLI\Command;
|
|
|
|
use Akeeba\Engine\Factory;
|
|
use Akeeba\Engine\Platform;
|
|
use Awf\Mvc\Model;
|
|
use Solo\Application;
|
|
use Solo\Model\Manage;
|
|
use Solo\Model\Remotefiles;
|
|
use Solo\Model\Upload;
|
|
use WP_CLI;
|
|
use WP_CLI\Utils as CliUtils;
|
|
|
|
/**
|
|
* Take and manage backups.
|
|
*
|
|
* @package Akeeba\WPCLI\Command
|
|
*
|
|
* @since version
|
|
*/
|
|
class Backup
|
|
{
|
|
/**
|
|
* Takes a backup with Akeeba Backup. WARNING! Do NOT use with the --http=<http> option of WP-CLI, it will NOT work on most sites.
|
|
*
|
|
* ## OPTIONS
|
|
*
|
|
* [--profile=<profile>]
|
|
* : Take a backup using the given profile ID, uses profile #1 if not specified
|
|
*
|
|
* [--description=<description>]
|
|
* : Apply this backup description, accepts the standard Akeeba Backup archive naming variables
|
|
*
|
|
* [--comment=<comment>]
|
|
* : Use this backup comment, provide it in HTML
|
|
*
|
|
* [--overrides=<overrides>]
|
|
* : Set up configuration overrides in the format "key1=value1,key2=value2"
|
|
*
|
|
* ## EXAMPLES
|
|
*
|
|
* wp akeeba backup take --profile=2
|
|
*
|
|
* wp akeeba backup take --description="Before changing menu on [DATE] [TIME]"
|
|
*
|
|
* @when after_wp_load
|
|
* @alias run
|
|
* @alias backup
|
|
*
|
|
* @param array $args Positional arguments (literal arguments)
|
|
* @param array $assoc_args Associative arguments (--flag, --no-flag, --key=value)
|
|
*
|
|
* @return void
|
|
*
|
|
* @throws WP_CLI\ExitException
|
|
*/
|
|
public function take($args, $assoc_args)
|
|
{
|
|
if (!defined('AKEEBABACKUP_PRO') || !AKEEBABACKUP_PRO)
|
|
{
|
|
WP_CLI::error("This command is only available in Akeeba Backup Professional");
|
|
}
|
|
|
|
$proCommands = new ProFeatures();
|
|
$proCommands->takeBackup($args, $assoc_args);
|
|
}
|
|
|
|
/**
|
|
* Lists the backup records known to Akeeba Backup
|
|
*
|
|
* ## OPTIONS
|
|
*
|
|
* [--from=<from>]
|
|
* : How many backup records to skip before starting the output. Default: 0.
|
|
*
|
|
* [--limit=<limit>]
|
|
* : Maximum number of backup records to display. Default: 50.
|
|
*
|
|
* [--description=<description>]
|
|
* : Optional. Listed backup records must match this (partial) description.
|
|
*
|
|
* [--after=<after>]
|
|
* : Optional. List backup records taken after this date.
|
|
*
|
|
* [--before=<before>]
|
|
* : Optional. List backup records taken before this date.
|
|
*
|
|
* [--origin=<origin>]
|
|
* : Optional. List backups from this origin only e.g. backend, frontend, json and so on.
|
|
*
|
|
* [--profile=<profile>]
|
|
* : Optional. List backups taken with this profile. Expect the numeric profile ID.
|
|
*
|
|
* [--sort-by=<column>]
|
|
* : Sort the output by the given column.
|
|
* ---
|
|
* default: id
|
|
* options:
|
|
* - id
|
|
* - description
|
|
* - profile_id
|
|
* - backupstart
|
|
* ---
|
|
*
|
|
* [--sort-order=<sortOrder>]
|
|
* : Sort order
|
|
* ---
|
|
* default: asc
|
|
* options:
|
|
* - asc
|
|
* - desc
|
|
* ---
|
|
*
|
|
* [--format=<format>]
|
|
* : The format for the returned list
|
|
* ---
|
|
* default: table
|
|
* options:
|
|
* - table
|
|
* - json
|
|
* - csv
|
|
* - yaml
|
|
* - count
|
|
* ---
|
|
*
|
|
* ## EXAMPLES
|
|
*
|
|
* wp akeeba backup list
|
|
*
|
|
* wp akeeba backup list --profile=2 --format=json
|
|
*
|
|
* wp akeeba backup list --profile=2 --filter="core." --sort-by=key --sort-order=desc
|
|
*
|
|
* @when after_wp_load
|
|
* @subcommand list
|
|
*
|
|
* @param array $args Positional arguments (literal arguments)
|
|
* @param array $assoc_args Associative arguments (--flag, --no-flag, --key=value)
|
|
*
|
|
* @return void
|
|
*
|
|
* @throws WP_CLI\ExitException
|
|
*/
|
|
public function _list($args, $assoc_args)
|
|
{
|
|
/** @var Application $akeebaBackupApplication */
|
|
global $akeebaBackupApplication;
|
|
|
|
$from = isset($assoc_args['from']) ? (int) $assoc_args['from'] : 0;
|
|
$limit = isset($assoc_args['limit']) ? (int) $assoc_args['limit'] : 50;
|
|
$format = isset($assoc_args['format']) ? $assoc_args['format'] : 'table';
|
|
|
|
$filters = $this->getFilters($assoc_args);
|
|
$order = $this->getOrdering($assoc_args);
|
|
|
|
$model = new Manage();
|
|
$model->setState('limitstart', $from);
|
|
$model->setState('limit', $limit);
|
|
|
|
$output = $model->getStatisticsListWithMeta(false, $filters, $order);
|
|
|
|
if (empty($output))
|
|
{
|
|
return;
|
|
}
|
|
|
|
$keys = array_keys($output);
|
|
$firstKey = array_shift($keys);
|
|
CliUtils\format_items($format, $output, array_keys($output[$firstKey]));
|
|
}
|
|
|
|
/**
|
|
* Display detailed information about a backup attempt known to Akeeba Backup.
|
|
*
|
|
* ## OPTIONS
|
|
*
|
|
* <id>
|
|
* : The numeric id of the backup attempt you want to display
|
|
*
|
|
* [--format=<format>]
|
|
* : The format for the returned list
|
|
* ---
|
|
* default: table
|
|
* options:
|
|
* - table
|
|
* - json
|
|
* - csv
|
|
* - yaml
|
|
* - count
|
|
* ---
|
|
*
|
|
* ## EXAMPLES
|
|
*
|
|
* wp akeeba backup info 123
|
|
*
|
|
* wp akeeba backup info 123 --format=json
|
|
*
|
|
* @when after_wp_load
|
|
*
|
|
* @param array $args Positional arguments (literal arguments)
|
|
* @param array $assoc_args Associative arguments (--flag, --no-flag, --key=value)
|
|
*
|
|
* @return void
|
|
*
|
|
* @throws WP_CLI\ExitException
|
|
*/
|
|
public function info($args, $assoc_args)
|
|
{
|
|
$id = isset($args[0]) ? (int) $args[0] : 0;
|
|
$format = isset($assoc_args['format']) ? $assoc_args['format'] : 'table';
|
|
|
|
if ($id <= 0)
|
|
{
|
|
throw new WP_CLI\ExitException("The backup ID must be a positive integer.");
|
|
}
|
|
|
|
$record = Platform::getInstance()->get_statistics($id);
|
|
|
|
CliUtils\format_items($format, [$record], array_keys($record));
|
|
}
|
|
|
|
/**
|
|
* Change the description and/or comment of a backup attempt known to Akeeba Backup.
|
|
*
|
|
* ## OPTIONS
|
|
*
|
|
* <id>
|
|
* : The numeric id of the backup attempt you want to modify
|
|
*
|
|
* [--description=<description>]
|
|
* : The new description to save into the backup attempt record.
|
|
*
|
|
* [--comment=<comment>]
|
|
* : The new comment to save into the backup attempt record.
|
|
*
|
|
* ## EXAMPLES
|
|
*
|
|
* wp akeeba backup modify 123 --description="Something"
|
|
*
|
|
* wp akeeba backup modify 123 --comment="More info about this backup"
|
|
*
|
|
* wp akeeba backup modify 123 --description="Something" --comment="More info about this backup"
|
|
*
|
|
* @when after_wp_load
|
|
*
|
|
* @param array $args Positional arguments (literal arguments)
|
|
* @param array $assoc_args Associative arguments (--flag, --no-flag, --key=value)
|
|
*
|
|
* @return void
|
|
*
|
|
* @throws WP_CLI\ExitException
|
|
*/
|
|
public function modify($args, $assoc_args)
|
|
{
|
|
$id = isset($args[0]) ? (int) $args[0] : 0;
|
|
$description = isset($assoc_args['description']) ? $assoc_args['description'] : null;
|
|
$comment = isset($assoc_args['comment']) ? $assoc_args['comment'] : null;
|
|
|
|
if ($id <= 0)
|
|
{
|
|
throw new WP_CLI\ExitException("The backup ID must be a positive integer.");
|
|
}
|
|
|
|
if (is_null($description) && is_null($comment))
|
|
{
|
|
throw new WP_CLI\ExitException("You must specify either --comment or --description.");
|
|
}
|
|
|
|
$record = Platform::getInstance()->get_statistics($id);
|
|
|
|
if (!is_null($description))
|
|
{
|
|
$record['description'] = $description;
|
|
}
|
|
|
|
if (!is_null($comment))
|
|
{
|
|
$record['comment'] = $comment;
|
|
}
|
|
|
|
$dummy = null;
|
|
$result = Platform::getInstance()->set_or_update_statistics($id, $record, $dummy);
|
|
|
|
if ($result === false)
|
|
{
|
|
WP_CLI::error("Backup record '$id' could not be modified.");
|
|
|
|
return;
|
|
}
|
|
|
|
WP_CLI::success("Backup record '$id' modified successfully.");
|
|
}
|
|
|
|
/**
|
|
* Delete the backup record and / or backup archives associated with it.
|
|
*
|
|
* ## OPTIONS
|
|
*
|
|
* <id>
|
|
* : The numeric id of the backup attempt you want to delete
|
|
*
|
|
* [--only-files]
|
|
* : Only delete the backup archive files, if they are stored on the site's server. Otherwise deletes the entire backup record.
|
|
*
|
|
* ## EXAMPLES
|
|
*
|
|
* wp akeeba backup delete 123
|
|
*
|
|
* wp akeeba backup delete 123 --only-files
|
|
*
|
|
* @when after_wp_load
|
|
*
|
|
* @param array $args Positional arguments (literal arguments)
|
|
* @param array $assoc_args Associative arguments (--flag, --no-flag, --key=value)
|
|
*
|
|
* @return void
|
|
*
|
|
* @throws WP_CLI\ExitException
|
|
*/
|
|
public function delete($args, $assoc_args)
|
|
{
|
|
$id = isset($args[0]) ? (int) $args[0] : 0;
|
|
$onlyFiles = isset($assoc_args['only-files']) ? (bool) $assoc_args['only-files'] : false;
|
|
|
|
if ($id <= 0)
|
|
{
|
|
throw new WP_CLI\ExitException("The backup ID must be a positive integer.");
|
|
}
|
|
|
|
$model = new Manage();
|
|
$model->setState('id', $id);
|
|
|
|
try
|
|
{
|
|
if ($onlyFiles)
|
|
{
|
|
$model->deleteFile();
|
|
|
|
WP_CLI::success("The files of backup record '$id' have been deleted successfully.");
|
|
|
|
return;
|
|
}
|
|
|
|
$model->delete();
|
|
|
|
WP_CLI::success("The backup record '$id' has been deleted successfully.");
|
|
|
|
}
|
|
catch (\RuntimeException $e)
|
|
{
|
|
WP_CLI::error("Cannot delete backup record '$id': {$e->getMessage()}");
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Retry uploading the archive of a backup attempt known to Akeeba Backup to the remote storage configured in the respective backup profile.
|
|
*
|
|
* ## OPTIONS
|
|
*
|
|
* <id>
|
|
* : The numeric id of the backup attempt you want to re-upload
|
|
*
|
|
* ## EXAMPLES
|
|
*
|
|
* wp akeeba backup upload 123
|
|
*
|
|
* @when after_wp_load
|
|
*
|
|
* @param array $args Positional arguments (literal arguments)
|
|
* @param array $assoc_args Associative arguments (--flag, --no-flag, --key=value)
|
|
*
|
|
* @return void
|
|
*
|
|
* @throws WP_CLI\ExitException
|
|
*/
|
|
public function upload($args, $assoc_args)
|
|
{
|
|
$id = isset($args[0]) ? (int) $args[0] : 0;
|
|
|
|
if ($id <= 0)
|
|
{
|
|
throw new WP_CLI\ExitException("The backup ID must be a positive integer.");
|
|
}
|
|
|
|
$model = new Upload();
|
|
$part = 0;
|
|
$frag = 0;
|
|
|
|
while (true)
|
|
{
|
|
$model->setState('id', $id);
|
|
$model->setState('part', $part);
|
|
$model->setState('frag', $frag);
|
|
WP_CLI::log("Trying to re-upload backup record '$id', part file #{$part}, fragment #{$frag}. This may take a while.");
|
|
|
|
// Try uploading
|
|
$result = $model->upload();
|
|
|
|
// Get the modified model state
|
|
$id = $model->getState('id');
|
|
$part = $model->getState('part');
|
|
$frag = $model->getState('frag');
|
|
|
|
if (($part >= 0) && ($result === true))
|
|
{
|
|
WP_CLI::success("Re-upload of backup record '$id' is complete.");
|
|
|
|
return;
|
|
}
|
|
|
|
if ($result === false)
|
|
{
|
|
$errorMessage = $model->getState('errorMessage', '');
|
|
WP_CLI::error("Re-upload of backup record '$id' failed: $errorMessage");
|
|
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Download the archive of a backup attempt known to Akeeba Backup from the remote storage configured in the respective backup profile back to the site's server.
|
|
*
|
|
* ## OPTIONS
|
|
*
|
|
* <id>
|
|
* : The numeric id of the backup attempt you want to fetch
|
|
*
|
|
* ## EXAMPLES
|
|
*
|
|
* wp akeeba backup fetch 123
|
|
*
|
|
* @when after_wp_load
|
|
*
|
|
* @param array $args Positional arguments (literal arguments)
|
|
* @param array $assoc_args Associative arguments (--flag, --no-flag, --key=value)
|
|
*
|
|
* @return void
|
|
*
|
|
* @throws WP_CLI\ExitException
|
|
*/
|
|
public function fetch($args, $assoc_args)
|
|
{
|
|
$id = isset($args[0]) ? (int) $args[0] : 0;
|
|
|
|
if ($id <= 0)
|
|
{
|
|
throw new WP_CLI\ExitException("The backup ID must be a positive integer.");
|
|
}
|
|
|
|
$model = new Remotefiles();
|
|
$part = 0;
|
|
$frag = 0;
|
|
|
|
while (true)
|
|
{
|
|
$model->setState('id', $id);
|
|
$model->setState('part', $part);
|
|
$model->setState('frag', $frag);
|
|
WP_CLI::log("Trying to re-upload backup record '$id', part file #{$part}, fragment #{$frag}. This may take a while.");
|
|
|
|
// Try uploading
|
|
$ret = $model->downloadToServer();
|
|
|
|
// Get the modified model state
|
|
$id = $model->getState('id');
|
|
$part = $model->getState('part');
|
|
$frag = $model->getState('frag');
|
|
|
|
if ($ret['finished'])
|
|
{
|
|
WP_CLI::success("Fetching back backup record '$id' is complete.");
|
|
|
|
return;
|
|
}
|
|
|
|
if ($ret['error'])
|
|
{
|
|
WP_CLI::error("Fetching back of backup record '$id' failed: {$ret['error']}");
|
|
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Output the archive of a backup attempt known to Akeeba Backup, as long as it's stored on the site's server. WARNING! You SHOULD NOT use this with the --http=<http> option of WP-CLI, it will most likely result in a corrupt or truncated backup archive.
|
|
*
|
|
* ## OPTIONS
|
|
*
|
|
* <id>
|
|
* : The numeric id of the backup attempt you want to download
|
|
*
|
|
* [<part>]
|
|
* : The part number to download: 0 (.jpa), 1 (.j01) etc. If a part does not exist you get an error.
|
|
*
|
|
* [--file=<file>]
|
|
* : The file you want to write to. Otherwise the raw binary data is output to STDOUT. The extension will be changed automatically.
|
|
*
|
|
* ## EXAMPLES
|
|
*
|
|
* wp akeeba backup download 123 > foobar.jpa
|
|
*
|
|
* wp akeeba backup download 123 --file=foobar.jpa
|
|
*
|
|
* @when after_wp_load
|
|
*
|
|
* @param array $args Positional arguments (literal arguments)
|
|
* @param array $assoc_args Associative arguments (--flag, --no-flag, --key=value)
|
|
*
|
|
* @return void
|
|
*
|
|
* @throws WP_CLI\ExitException
|
|
*/
|
|
public function download($args, $assoc_args)
|
|
{
|
|
// TODO Complain if we're under the --http connection method
|
|
|
|
$id = isset($args[0]) ? (int) $args[0] : 0;
|
|
$part = isset($args[1]) ? (int) $args[1] : 0;
|
|
$outFile = isset($assoc_args['file']) ? $assoc_args['file'] : null;
|
|
|
|
if ($id <= 0)
|
|
{
|
|
throw new WP_CLI\ExitException("The backup ID must be a positive integer.");
|
|
}
|
|
|
|
$model = new \Solo\Model\Manage();
|
|
$model->setState('id', $id);
|
|
|
|
$stat = Platform::getInstance()->get_statistics($id);
|
|
$allFileNames = Factory::getStatistics()->get_all_filenames($stat);
|
|
|
|
if (empty($allFileNames))
|
|
{
|
|
WP_CLI::error("Backup record '$id' does not have any files available for download. Have you already deleted them?");
|
|
}
|
|
|
|
if (is_null($allFileNames))
|
|
{
|
|
WP_CLI::error("Backup record '$id' does not have any files available for download on the server. If they are stored remotely you may need to use the fetch command first.");
|
|
}
|
|
|
|
if (($part >= count($allFileNames)) || !isset($allFileNames[$part]))
|
|
{
|
|
WP_CLI::error("There is no part '$part' of backup record '$id'.");
|
|
}
|
|
|
|
$fileName = $allFileNames[$part];
|
|
|
|
if (!@file_exists($fileName))
|
|
{
|
|
WP_CLI::error("Can not find part '$part' of backup record '$id' on the server.");
|
|
}
|
|
|
|
$basename = @basename($fileName);
|
|
$fileSize = @filesize($fileName);
|
|
$extension = strtolower(str_replace(".", "", strrchr($fileName, ".")));
|
|
|
|
if (empty($outFile))
|
|
{
|
|
readfile($fileName);
|
|
|
|
return;
|
|
}
|
|
|
|
if (is_dir($outFile))
|
|
{
|
|
$outFile = rtrim($outFile, '//\\') . DIRECTORY_SEPARATOR . $basename;
|
|
}
|
|
else
|
|
{
|
|
$dotPos = strrpos($outFile, '.');
|
|
$outFile = ($dotPos === false) ? $outFile : (substr($outFile, 0, $dotPos) . '.' . $extension);
|
|
}
|
|
|
|
// Read in 1M chunks
|
|
$blocksize = 1048576;
|
|
$handle = @fopen($fileName, "r");
|
|
|
|
if ($handle === false)
|
|
{
|
|
WP_CLI::error("Cannot open '$fileName' for reading. Check the permissions / ACLs of the file.");
|
|
}
|
|
|
|
$fp = @fopen($outFile, 'wb');
|
|
|
|
if ($fp === false)
|
|
{
|
|
fclose($handle);
|
|
WP_CLI::error("Cannot open '$outFile' for writing. Check whether the folder exists and the permissions / ACLs of both the enclosing folder and the file.");
|
|
}
|
|
|
|
while (!@feof($handle))
|
|
{
|
|
fwrite($fp, @fread($handle, $blocksize));
|
|
}
|
|
|
|
@fclose($handle);
|
|
@fclose($fp);
|
|
}
|
|
|
|
private function getArg($argument, array $argList, $default = null)
|
|
{
|
|
return isset($argList[$argument]) ? $argList[$argument] : $default;
|
|
}
|
|
|
|
private function getFilters($args)
|
|
{
|
|
$filters = array();
|
|
|
|
if ($this->getArg('description', $args))
|
|
{
|
|
$filters[] = array(
|
|
'field' => 'description',
|
|
'operand' => 'LIKE',
|
|
'value' => $this->getArg('description', $args)
|
|
);
|
|
}
|
|
|
|
if ($this->getArg('after', $args) && $this->getArg('before', $args))
|
|
{
|
|
$filters[] = array(
|
|
'field' => 'backupstart',
|
|
'operand' => 'BETWEEN',
|
|
'value' => $this->getArg('after', $args),
|
|
'value2' => $this->getArg('before', $args)
|
|
);
|
|
}
|
|
elseif ($this->getArg('after', $args))
|
|
{
|
|
$filters[] = array(
|
|
'field' => 'backupstart',
|
|
'operand' => '>=',
|
|
'value' => $this->getArg('after', $args),
|
|
);
|
|
}
|
|
elseif ($this->getArg('before', $args))
|
|
{
|
|
$filters[] = array(
|
|
'field' => 'backupstart',
|
|
'operand' => '<=',
|
|
'value' => $this->getArg('before', $args),
|
|
);
|
|
}
|
|
|
|
if ($this->getArg('origin', $args))
|
|
{
|
|
$filters[] = array(
|
|
'field' => 'origin',
|
|
'operand' => '=',
|
|
'value' => $this->getArg('origin', $args)
|
|
);
|
|
}
|
|
|
|
if ($this->getArg('profile', $args))
|
|
{
|
|
$filters[] = array(
|
|
'field' => 'profile_id',
|
|
'operand' => '=',
|
|
'value' => (int) $this->getArg('profile', $args)
|
|
);
|
|
}
|
|
|
|
$filters[] = array(
|
|
'field' => 'tag',
|
|
'operand' => '<>',
|
|
'value' => 'restorepoint'
|
|
);
|
|
|
|
|
|
if (empty($filters))
|
|
{
|
|
$filters = null;
|
|
}
|
|
|
|
return $filters;
|
|
}
|
|
|
|
private function getOrdering($args)
|
|
{
|
|
$order = array(
|
|
'by' => $this->getArg('sort-by', $args),
|
|
'order' => $this->getArg('sort-order', $args)
|
|
);
|
|
|
|
return $order;
|
|
}
|
|
}
|