File: //proc/self/cwd/wp-content/plugins/backup/src/JetBackup/Snapshot/Snapshot.php
<?php
namespace JetBackup\Snapshot;
use Exception;
use JetBackup\Archive\Archive;
use JetBackup\BackupJob\BackupJob;
use JetBackup\CLI\CLI;
use JetBackup\Cron\Task\Task;
use JetBackup\Data\Engine;
use JetBackup\Data\SleekStore;
use JetBackup\Destination\Destination;
use JetBackup\Destination\Vendors\Imported\Imported;
use JetBackup\Entities\Util;
use JetBackup\Exception\AjaxException;
use JetBackup\Exception\ArchiveException;
use JetBackup\Exception\DBException;
use JetBackup\Exception\GzipException;
use JetBackup\Exception\IOException;
use JetBackup\Exception\JBException;
use JetBackup\Exception\QueueException;
use JetBackup\Exception\SnapshotMetaException;
use JetBackup\Export\Vendor\Vendor;
use JetBackup\Factory;
use JetBackup\JetBackup;
use JetBackup\Log\LogController;
use JetBackup\Queue\Queue;
use JetBackup\Queue\QueueItem;
use JetBackup\Queue\QueueItemDownload;
use JetBackup\Queue\QueueItemExport;
use JetBackup\Queue\QueueItemExtract;
use JetBackup\Queue\QueueItemRestore;
use JetBackup\ResumableTask\ResumableTask;
use JetBackup\Schedule\Schedule;
use JetBackup\UserInput\UserInput;
use JetBackup\Wordpress\Wordpress;
use SleekDB\Exceptions\InvalidArgumentException;
use SleekDB\QueryBuilder;
if (!defined( '__JETBACKUP__')) die('Direct access is not allowed');
class Snapshot extends Engine {
const COLLECTION = 'snapshots';
const UNIQUE_ID = 'unique_id';
const NAME = 'name';
const VERSION = 'version';
const BACKUP_TYPE = 'backup_type';
const NOTES = 'notes';
const CREATED = 'created';
const CONTAINS = 'contains';
const STRUCTURE = 'structure';
const SIZE = 'size';
const REINDEX = 'reindex';
const LOCKED = 'locked';
const DELETED = 'deleted';
const SCHEDULES = 'schedules';
const SCHEDULES_NAMES = 'schedules_names';
const DESTINATION_ID = 'destination_id';
const DESTINATION_NAME = 'destination_name';
const JOB_IDENTIFIER = 'job_identifier';
const ITEMS = 'items';
const PARAMS = 'params';
const SKELETON_DATABASE_DIRNAME = 'database';
const SKELETON_LOG_DIRNAME = 'log';
const SKELETON_LOG_FILENAME = 'task.log.gz';
const SKELETON_TEMP_DIRNAME = 'tmp';
const SKELETON_META_DIRNAME = 'meta';
const SKELETON_CONFIG_DIRNAME = 'config';
const SKELETON_FILES_DIRNAME = 'files';
const SKELETON_FILES_ARCHIVE_NAME = 'files' . Archive::ARCHIVE_EXT;
const SKELETON_META_FILENAME = 'meta.json';
const SKELETON_FILES_LIST_FILENAME = 'files.list';
const META_MIN_VERSION = '1.0.3';
const META_VERSION = '1.0.3';
const META_FILEPATH = '%s' . JetBackup::SEP . self::SKELETON_META_DIRNAME . JetBackup::SEP . self::SKELETON_META_FILENAME;
const LOG_FILEPATH = '%s' . JetBackup::SEP . self::SKELETON_LOG_DIRNAME . JetBackup::SEP . self::SKELETON_LOG_FILENAME;
const SNAPSHOT_NAME_PATTERN = 'snap_%s_%s';
const SNAPSHOT_NAME_REGEX = "/^snap_([\d]{4}-[\d]{2}-[\d]{2})_([\d]{6})_(.*)$/";
const PARAM_MULTISITE = 'multisite';
const PARAM_SITE_URL = 'site_url';
const PARAM_DB_PREFIX = 'db_prefix';
const PARAM_DB_EXCLUDED = 'db_excluded';
const PARAM_WP_CONTENT_ONLY_BACKUP = 'wp_content_only_backup';
/**
* @var SnapshotItem[]
*/
private array $_items=[];
public function __construct($_id=null) {
parent::__construct(self::COLLECTION);
if($_id) $this->_loadById((int) $_id);
}
public function setUniqueId($id) { $this->set(self::UNIQUE_ID, $id); }
public function getUniqueId():string { return $this->get(self::UNIQUE_ID); }
public function setCreated(int $created) { $this->set(self::CREATED, $created); }
public function getCreated():int { return $this->get(self::CREATED, 0); }
public function setName(string $name) { $this->set(self::NAME, $name); }
public function getName():string { return $this->get(self::NAME); }
public function setBackupType(int $type) { $this->set(self::BACKUP_TYPE, $type); }
public function getBackupType() : int { return $this->get(self::BACKUP_TYPE, 0); }
public function setDestinationId(int $_id) { $this->set(self::DESTINATION_ID, $_id); }
public function getDestinationId():int { return $this->get(self::DESTINATION_ID, 0); }
public function getDestinationName():string {return (new Destination($this->getDestinationId()))->getName();}
public function setJobIdentifier(string $identifier) { $this->set(self::JOB_IDENTIFIER, $identifier); }
public function getJobIdentifier():string { return $this->get(self::JOB_IDENTIFIER); }
public function setSize(int $size) { $this->set(self::SIZE, $size); }
public function getSize():int { return (int) $this->get(self::SIZE, 0); }
public function setStructure(int $structure) { $this->set(self::STRUCTURE, $structure); }
public function getStructure():int { return (int) $this->get(self::STRUCTURE, 0); }
public function setContains(int $contains) { $this->set(self::CONTAINS, $contains); }
public function getContains():int { return (int) $this->get(self::CONTAINS, 0); }
public function setNotes(string $notes) { $this->set(self::NOTES, $notes); }
public function getNotes():string { return $this->get(self::NOTES); }
public function setSchedules(array $schedules) { $this->set(self::SCHEDULES, $schedules); }
public function getSchedules(): array {return $this->get(self::SCHEDULES, []);}
/**
* @return array
*/
public function getSchedulesNamesByType(): array {
$schedules = $this->getSchedules();
$output = [];
if (!empty($schedules)) {
foreach ($schedules as $schedule_type) {
$output[] = [
'schedule_id' => $schedule_type,
'schedule_name' => Schedule::TYPE_NAMES[$schedule_type] ?? 'Unknown',
];
}
}
return $output;
}
public function setReindex(bool $reindex) { $this->set(self::REINDEX, $reindex); }
public function isReindex():bool { return $this->get(self::REINDEX, false); }
public function setLocked(bool $locked) { $this->set(self::LOCKED, $locked); }
public function isLocked():bool { return $this->get(self::LOCKED, false); }
public function setDeleted(int $deleted) { $this->set(self::DELETED, $deleted); }
public function getDeleted():int { return $this->get(self::DELETED, 0); }
public function isDeleted():bool { return $this->getDeleted() > 0 && $this->getDeleted() <= time(); }
public function setParams(array $params) { $this->set(self::PARAMS, $params); }
public function getParams():array { return $this->get(self::PARAMS, []); }
public function getParam(string $key, $default=null) {
$params = $this->getParams();
return $params[$key] ?? $default;
}
public function addParam(string $key, $value):void {
$params = $this->getParams();
$params[$key] = $value;
$this->setParams($params);
}
public function setItems(array $items) { $this->_items = $items; }
public function addItem(SnapshotItem $item) { $this->_items[] = $item; }
/**
* @return SnapshotItem[]
* @throws \SleekDB\Exceptions\IOException
* @throws InvalidArgumentException|DBException
*/
public function getItems():array {
if($this->_items) return $this->_items;
$output = [];
$list = SnapshotItem::query()
->where([SnapshotItem::PARENT_ID, '=', $this->getId()])
->orderBy([SnapshotItem::NAME =>'asc'])
->getQuery()
->fetch();
foreach($list as $item_details) $output[] = new SnapshotItem($item_details[JetBackup::ID_FIELD]);
return $output;
}
/**
* @throws InvalidArgumentException
* @throws \SleekDB\Exceptions\IOException
*/
public function removeItems() {
SnapshotItem::query()
->where([SnapshotItem::PARENT_ID, '=', $this->getId()])
->getQuery()
->delete();
}
/**
* @param $source
* @param LogController|null $logController
* @param callable|null $callback
* @param array $excludes
* @param array $includes
*
* @return void
* @throws ArchiveException
* @throws DBException
* @throws GzipException
* @throws InvalidArgumentException
* @throws \SleekDB\Exceptions\IOException
*/
public function extract($source, ?LogController $logController=null, ?callable $callback=null, array $excludes=[], array $includes=[]) {
if(!($items = $this->getItems())) return;
if(!$logController) $logController = new LogController();
foreach($items as $item) $item->extract($source, $logController, $callback, $excludes, $includes);
}
/**
* @throws InvalidArgumentException
* @throws \SleekDB\Exceptions\IOException
*/
public function delete():void {
parent::delete();
SnapshotItem::query()
->where([SnapshotItem::PARENT_ID, '=', $this->getId()])
->getQuery()
->delete();
}
public function isCompressed():bool { return $this->getStructure() == BackupJob::STRUCTURE_COMPRESSED; }
public static function generateName(): string {
return sprintf(self::SNAPSHOT_NAME_PATTERN, Util::date('Y-m-d_His'), Util::generateUniqueId());
}
public function save():void {
if(!$this->getUniqueId()) $this->setUniqueId(Util::generateUniqueId());
parent::save();
foreach($this->_items as $item) {
$item->setParentId($this->getId());
$item->save();
}
}
public static function db():SleekStore {
return new SleekStore(self::COLLECTION);
}
public static function query():QueryBuilder {
return self::db()->createQueryBuilder();
}
/**
* @throws \SleekDB\Exceptions\IOException
* @throws InvalidArgumentException
*/
public static function getTotalSnapshots():int {
return count(self::query()
->select([JetBackup::ID_FIELD])
->getQuery()
->fetch());
}
/**
* @param int $type
*
* @return void
*/
public function addScheduleByType(int $type):void {
$types = $this->getSchedules();
if(in_array($type, $types)) return;
$types[] = $type;
$this->setSchedules($types);
}
/**
* @throws \SleekDB\Exceptions\IOException
* @throws QueueException
* @throws InvalidArgumentException
*/
public function addToDownloadQueue() {
$download = new QueueItemDownload();
$download->setSnapshotId($this->getId());
$queue_item = QueueItem::prepare();
$queue_item->setType(Queue::QUEUE_TYPE_DOWNLOAD);
$queue_item->setItemId($this->getId());
$queue_item->setItemData($download);
Queue::addToQueue($queue_item);
}
/**
* @throws \SleekDB\Exceptions\IOException
* @throws QueueException
* @throws InvalidArgumentException
*/
public function addToDownloadLogQueue() {
$download = new QueueItemDownload();
$download->setSnapshotId($this->getId());
$queue_item = QueueItem::prepare();
$queue_item->setType(Queue::QUEUE_TYPE_DOWNLOAD_BACKUP_LOG);
$queue_item->setItemId($this->getId());
$queue_item->setItemData($download);
Queue::addToQueue($queue_item);
}
/**
* @throws \SleekDB\Exceptions\IOException
* @throws QueueException
* @throws InvalidArgumentException
*/
private static function _addToRestoreQueue(int $id=0, string $path='', int $options=0, array $exclude=[], array $include=[], array $exclude_db=[], array $include_db=[], array $filemanager=[]) {
$restore = new QueueItemRestore();
// these file cannot be restored, we can cut our own branch
$exclude[] = 'wp-config.php';
$exclude[] = '.htaccess';
if($id) $restore->setSnapshotId($id);
elseif($path) {
if(!file_exists($path) || !is_readable($path)) throw new QueueException("The provided snapshot path not exists");
if( !Archive::isGzCompressed($path) && !Archive::isTar($path)) throw new QueueException("The provided snapshot path is not a tar.gz file");
$restore->setSnapshotPath($path);
} else throw new QueueException("You must provide snapshot id or path");
// Check if backup was created with wp-content-only restriction
$isWpContentOnlyBackup = false;
if ($id) {
$snapshot = new Snapshot($id);
$isWpContentOnlyBackup = $snapshot->getParam(self::PARAM_WP_CONTENT_ONLY_BACKUP, false);
}
// Apply wp-content-only restriction if:
// 1. Current setting is enabled, OR
// 2. Backup was originally created with this restriction (WP Cloud or setting was enabled)
// Note: in multisite restore, when restoring subsites $includes will already have values
if ((Factory::getSettingsRestore()->isRestoreWpContentOnlyEnabled() || $isWpContentOnlyBackup) && empty($include)) {
$exclude = [];
$include[] = Wordpress::WP_CONTENT . JetBackup::SEP . '*';
}
$restore->setOptions($options);
$restore->setExcludes($exclude);
$restore->setIncludes($include);
$restore->setExcludedDatabases($exclude_db);
$restore->setIncludedDatabases($include_db);
$restore->setFileManager($filemanager);
$queue_item = QueueItem::prepare();
$queue_item->setType(Queue::QUEUE_TYPE_RESTORE);
$queue_item->setItemData($restore);
Queue::addToQueue($queue_item);
}
/**
* @param $path
* @param int $options
* @param array $exclude
* @param array $exclude_db
* @param array $include_db
*
* @return void
* @throws InvalidArgumentException
* @throws QueueException
* @throws \SleekDB\Exceptions\IOException
*/
public static function addToRestoreQueueByPath($path, int $options= QueueItemRestore::OPTION_RESTORE_FILES_ENTIRE | QueueItemRestore::OPTION_RESTORE_DATABASE_ENTIRE, array $exclude=[], array $exclude_db=[], array $include_db=[]) {
$include = []; // placeholder for future implementation
self::_addToRestoreQueue(0, $path, $options, $exclude, $include, $exclude_db, $include_db);
}
/**
* Import a backup file from a given path and save it to the database.
* This allows the backup to be indexed and restored later without re-uploading.
*
* @param string $path Path to the backup file (tar or tar.gz)
* @param bool $crossDomain Allow cross-domain restores
*
* @return Snapshot The imported and saved snapshot
* @throws QueueException
* @throws InvalidArgumentException
* @throws \SleekDB\Exceptions\IOException
* @throws DBException
* @throws \JetBackup\Exception\DestinationException
*/
public static function importFromPath(string $path, bool $crossDomain = true): Snapshot {
// 1. Validate backup file
if (!file_exists($path) || !is_readable($path)) {
throw new QueueException("The provided backup file doesn't exist or is not readable");
}
$isCompressed = Archive::isGzCompressed($path);
if (!$isCompressed && !Archive::isTar($path)) {
throw new QueueException("The provided file is not a valid backup file (tar or tar.gz)");
}
// 2. Extract only meta.json to a temp location
$tempDir = Factory::getLocations()->getTempDir() . JetBackup::SEP . 'import_' . Util::generateUniqueId();
if (!file_exists($tempDir)) {
mkdir($tempDir, 0700, true);
}
$extractPath = $path;
// If compressed, decompress to temp location first
if ($isCompressed) {
$decompressedPath = $tempDir . JetBackup::SEP . basename($path, '.gz');
\JetBackup\Archive\Gzip::decompress($path, $decompressedPath);
$extractPath = $decompressedPath;
}
// Extract only the meta directory from the tar
try {
$archive = new Archive($extractPath);
$archive->setExcludeCallback(function($path, $isDir) {
// Exclude everything EXCEPT meta directory files
$metaDir = self::SKELETON_META_DIRNAME;
return !str_starts_with($path, $metaDir . JetBackup::SEP) && $path !== $metaDir;
});
$archive->extract($tempDir);
} catch (ArchiveException $e) {
Util::rm($tempDir);
throw new QueueException("Failed to extract backup meta: " . $e->getMessage());
}
// Remove decompressed temp file if we created one
if ($isCompressed && file_exists($extractPath)) {
unlink($extractPath);
}
// 3. Parse meta.json
$metaFile = sprintf(self::META_FILEPATH, $tempDir);
if (!file_exists($metaFile)) {
Util::rm($tempDir);
throw new QueueException("The provided backup doesn't contain a valid meta file");
}
$snapshot = new Snapshot();
try {
$snapshot->importMeta($metaFile, $crossDomain);
} catch (SnapshotMetaException $e) {
Util::rm($tempDir);
throw new QueueException("Can't import backup: " . $e->getMessage());
}
// Clean up temp meta extraction
Util::rm($tempDir);
// 4. Get or create Imported destination
$destination = Destination::createImportedDestination();
// 5. Generate job identifier for imported backup
$jobIdentifier = 'imported_' . Util::generateUniqueId();
// 6. Check for existing snapshot with same name to avoid duplicates
$existing = self::query()
->where([self::NAME, '=', $snapshot->getName()])
->where([self::DESTINATION_ID, '=', $destination->getId()])
->getQuery()
->first();
if ($existing) {
// Return existing snapshot, no need to re-import
return new Snapshot($existing[JetBackup::ID_FIELD]);
}
// 7. Move backup file to permanent location
$destination->connect();
$permanentDir = JetBackup::SEP . $jobIdentifier;
if (!$destination->dirExists($permanentDir)) {
$destination->createDir($permanentDir);
}
$backupFilename = $snapshot->getName() . ($isCompressed ? '.tar.gz' : '.tar');
$permanentPath = $permanentDir . JetBackup::SEP . $backupFilename;
// Copy file to permanent location (use copy to preserve original until confirmed)
$destination->getInstance()->copyFileToRemote($path, $permanentPath);
$destination->disconnect();
$dataDir = Factory::getLocations()->getDataDir();
$archiveFilename = basename($permanentPath);
$extractFolderName = $snapshot->getName();
$archivePath = $dataDir . JetBackup::SEP. Imported::IMPORTS_DIR . JetBackup::SEP.$jobIdentifier. JetBackup::SEP. $archiveFilename;
$extractDir = $dataDir . JetBackup::SEP. Imported::IMPORTS_DIR . JetBackup::SEP .$jobIdentifier. JetBackup::SEP. $extractFolderName;
if (!is_dir($extractDir)) {
mkdir($extractDir, 0755, true);
}
try {
$archive = new Archive($archivePath);
$archive->extract($extractDir);
} catch (\Exception $e) {
throw new QueueException("Failed to extract imported backup: " . $e->getMessage());
}
unlink($archivePath);
// Remove original uploaded file
unlink($path);
// If the file was decompressed from a .gz file, the original .gz parent dir may now be empty
$parentDir = dirname($path);
if (is_dir($parentDir) && count(scandir($parentDir)) == 2) {
rmdir($parentDir);
}
// 8. Set snapshot fields
$snapshot->setDestinationId($destination->getId());
$snapshot->setJobIdentifier($jobIdentifier);
$snapshot->addParam('imported', true);
$snapshot->addParam('import_time', time());
$snapshot->addParam('original_filename', basename($path));
// Add import schedule type for retention tracking
$snapshot->addScheduleByType(Schedule::TYPE_IMPORTED);
// 9. Save snapshot to database
$snapshot->save();
return $snapshot;
}
/**
* @return void
* @throws InvalidArgumentException
* @throws QueueException
* @throws \SleekDB\Exceptions\IOException
*/
public function addToRestoreQueue(int $options=QueueItemRestore::OPTION_RESTORE_FILES_ENTIRE | QueueItemRestore::OPTION_RESTORE_DATABASE_ENTIRE, array $exclude=[], array $include=[], array $exclude_db=[], array $include_db=[], array $filemanager=[]) {
self::_addToRestoreQueue($this->getId(), '', $options, $exclude, $include, $exclude_db, $include_db, $filemanager);
}
/**
* @param int $type
*
* @return void
* @throws InvalidArgumentException
* @throws QueueException
* @throws \SleekDB\Exceptions\IOException
*/
public function addToExportQueue(int $type) {
if (!$type || !in_array($type, Vendor::ALL_VENDORS)) throw new QueueException("Invalid Panel type");
$export = new QueueItemExport();
$export->setType($type);
$export->setSnapshotId($this->getId());
$queue_item = QueueItem::prepare();
$queue_item->setType(Queue::QUEUE_TYPE_EXPORT);
$queue_item->setItemId($this->getId());
$queue_item->setItemData($export);
Queue::addToQueue($queue_item);
}
/**
* @throws InvalidArgumentException
* @throws \SleekDB\Exceptions\IOException
* @throws QueueException
*/
public function addToExtractQueue($extract_path=null): void {
$extract = new QueueItemExtract();
$extract->setSnapshotId($this->getId());
if($extract_path) $extract->setExtractPath($extract_path);
$queue_item = QueueItem::prepare();
$queue_item->setType(Queue::QUEUE_TYPE_EXTRACT);
$queue_item->setItemId($this->getId());
$queue_item->setItemData($extract);
Queue::addToQueue($queue_item);
}
/**
* @param int $type
*
* @return void
*/
public function removeSchedule(int $type):void {
if($type < 1) return;
$types = $this->getSchedules();
$offset = array_search($type, $types);
if($offset === false) return;
array_splice($types, $offset, 1);
$this->setSchedules($types);
}
/**
* @throws InvalidArgumentException
* @throws \SleekDB\Exceptions\IOException
*/
public static function getTotalSnapshotsSize():int {
$list = self::query()
->where([Engine::ENGINE, '=', Engine::ENGINE_WP])
->select([self::SIZE])
->getQuery()
->fetch();
$size = 0;
foreach ($list as $item) $size += $item[self::SIZE];
return $size;
}
/**
* @param int $destinationID
*
* @return void
* @throws AjaxException
* @throws DBException
* @throws InvalidArgumentException
* @throws \SleekDB\Exceptions\IOException
*/
public static function deleteByDestinationID(int $destinationID):void {
$snapshots = Snapshot::query()
->select([JetBackup::ID_FIELD])
->where([Snapshot::DESTINATION_ID, '=', $destinationID])
->getQuery()
->fetch();
if(empty($snapshots)) return;
foreach ($snapshots as $snapshotID) {
$snapshot = new Snapshot($snapshotID[JetBackup::ID_FIELD]);
if(!$snapshot->getId()) throw new AjaxException("Invalid snapshot id provided");
$snapshot->delete();
}
}
/**
* @return array
*/
public function getDisplay(): array {
return [
JetBackup::ID_FIELD => $this->getId(),
self::UNIQUE_ID => $this->getUniqueId(),
self::DESTINATION_ID => $this->getDestinationId(),
self::DESTINATION_NAME => $this->getDestinationName(),
self::NAME => $this->getName(),
self::BACKUP_TYPE => $this->getBackupType(),
Engine::ENGINE => $this->getEngine(),
self::CONTAINS => $this->getContains(),
self::STRUCTURE => $this->getStructure(),
self::SCHEDULES => $this->getSchedules(),
self::SCHEDULES_NAMES => $this->getSchedulesNamesByType(),
self::NOTES => $this->getNotes(),
self::PARAMS => $this->getParams(),
self::CREATED => $this->getCreated(),
self::SIZE => Util::bytesToHumanReadable($this->getSize()),
self::LOCKED => $this->isLocked() ? 1 : 0,
self::DELETED => $this->getDeleted()
];
}
/**
* @return array
*/
public function getDisplayCLI(): array {
return [
'ID' => $this->getId(),
'Destination ID' => $this->getDestinationId(),
'Name' => $this->getName(),
'Backup Type' => $this->getBackupType(),
'Engine' => $this->getEngineName(),
'Contains' => $this->getContains(),
'Structure' => $this->getStructure(),
'Schedules' => $this->getSchedules(),
'Notes' => $this->getNotes(),
'Created' => CLI::date($this->getCreated()),
'Size' => $this->getSize(),
'Locked' => $this->isLocked() ? 'Yes' : 'No',
'Deleted' => $this->getDeleted(),
];
}
public function exportMeta($path):void {
if($this->getEngine() != Engine::ENGINE_WP)
throw new SnapshotMetaException("Only snapshot of type WordPress is allowed to be exported");
$items = [];
foreach($this->_items as $item) $items[] = $item->exportMeta();
$umask = umask(077);
file_put_contents(sprintf(self::META_FILEPATH, $path), json_encode([
self::VERSION => self::META_VERSION,
self::NAME => $this->getName(),
self::BACKUP_TYPE => $this->getBackupType(),
Engine::ENGINE => $this->getEngine(),
self::CONTAINS => $this->getContains(),
self::STRUCTURE => $this->getStructure(),
self::SCHEDULES => $this->getSchedules(),
self::JOB_IDENTIFIER => $this->getJobIdentifier(),
self::CREATED => $this->getCreated(),
self::SIZE => $this->getSize(),
self::PARAMS => $this->getParams(),
self::ITEMS => $items,
]));
umask($umask);
}
/**
* @param string $file
* @param bool $cross_domain
*
* @return void
* @throws SnapshotMetaException
*/
public function importMeta(string $file, bool $cross_domain=false):void {
$meta = json_decode(file_get_contents($file));
$current_site_url = Wordpress::getSiteDomain();
if(isset($meta->version) && version_compare($meta->version, self::META_MIN_VERSION, '>=')) {
$this->setEngine($meta->{Engine::ENGINE});
$this->setBackupType($meta->{self::BACKUP_TYPE});
$this->setCreated($meta->{self::CREATED});
$this->setName($meta->{self::NAME});
$this->setContains($meta->{self::CONTAINS});
$this->setParams((array) $meta->{self::PARAMS});
$this->setStructure($meta->{self::STRUCTURE});
$this->setSchedules($meta->{self::SCHEDULES});
$this->setSize($meta->{self::SIZE});
$backup_site_url = preg_replace("#^http(s?)://#", "", $this->getParam(self::PARAM_SITE_URL));
if( !$cross_domain && $backup_site_url != $current_site_url)
throw new SnapshotMetaException("This snapshot isn't owned by this site, you need to allow cross-domain restores");
foreach($meta->{self::ITEMS} as $item_meta) {
$item = new SnapshotItem();
$item->importMeta($item_meta);
$this->addItem($item);
}
} elseif(isset($meta->meta_version)) {
// Support meta file with version 1.0.2 and below
$backup_site_url = preg_replace("#^http(s?)://#", "", $meta->domain_backup);
if( !$cross_domain && $backup_site_url != $current_site_url)
throw new SnapshotMetaException("This snapshot isn't owned by this site, you need to allow cross-domain restores");
$contains = 0;
if(isset($meta->job_settings->backup_contains)) {
$backup_contains = (array) $meta->job_settings->backup_contains;
if(in_array('homedir', $backup_contains)) $contains |= BackupJob::BACKUP_ACCOUNT_CONTAINS_HOMEDIR;
if(in_array('database', $backup_contains)) $contains |= BackupJob::BACKUP_ACCOUNT_CONTAINS_DATABASE;
}
$this->setEngine(isset($meta->queue_item->engine) ? (Wordpress::strContains($meta->queue_item->engine, '5.303,13.236,5.301,20.758 C91.384,64.08,82.732,78.016,69.871,85.516z') ? Engine::ENGINE_WP : Engine::ENGINE_JB) : Engine::ENGINE_WP);
$this->setBackupType(BackupJob::TYPE_ACCOUNT);
$this->setCreated($meta->queue_item->created ?? 0);
$this->setName(isset($meta->queue_item->snap) && $meta->queue_item->snap ? 'snap_' . $meta->queue_item->snap : '');
$this->setContains($contains);
if(isset($meta->backup_structure_name)) $this->setStructure($meta->backup_structure_name == 'Compressed' ? BackupJob::STRUCTURE_COMPRESSED : BackupJob::STRUCTURE_ARCHIVED);
$this->setSize($meta->size ?? 0);
$this->addParam(Snapshot::PARAM_MULTISITE, $meta->multisite_details);
$this->addParam(Snapshot::PARAM_SITE_URL, $meta->domain_backup);
$schedules = [];
if(isset($meta->queue_item->parent_schedules)) foreach($meta->queue_item->parent_schedules as $schedule_id) $schedules[] = (int) $schedule_id;
$this->setSchedules($schedules);
if($this->getContains() & BackupJob::BACKUP_ACCOUNT_CONTAINS_HOMEDIR) {
$item = new SnapshotItem();
$item->setBackupType(BackupJob::TYPE_ACCOUNT);
$item->setBackupContains(BackupJob::BACKUP_ACCOUNT_CONTAINS_HOMEDIR);
$item->setCreated($meta->queue_item->created);
$item->setName('');
$item->setPath('files/files.tar' . ($this->isCompressed() ? '.gz' : ''));
$item->setSize($meta->size - $meta->db_size);
$this->addItem($item);
}
if(isset($meta->db_items) && is_array($meta->db_items)) {
foreach ($meta->db_items as $db_item) {
$item = new SnapshotItem();
$item->setName($db_item->name ?? '');
$item->setPath('db_dumps/' . $db_item->name . '.sql' . ($db_item->compressed ? '.gz' : ''));
$item->setSize($db_item->size ?? 0);
$item->setCreated($db_item->mtime);
$item->setBackupType(BackupJob::TYPE_ACCOUNT);
$item->setBackupContains(BackupJob::BACKUP_ACCOUNT_CONTAINS_DATABASE);
$item->addParam(Snapshot::PARAM_DB_PREFIX, $meta->db_prefix);
$item->addParam(Snapshot::PARAM_DB_EXCLUDED, in_array($db_item->name, $meta->job_settings->exDatabase));
$this->addItem($item);
}
}
$item = new SnapshotItem();
$item->setBackupType(BackupJob::TYPE_ACCOUNT);
$item->setBackupContains(BackupJob::BACKUP_ACCOUNT_CONTAINS_FULL);
$item->setCreated($meta->queue_item->created);
$item->setName('');
$item->setPath('');
$item->setSize(0);
$item->addParam(Snapshot::PARAM_MULTISITE, $meta->multisite_details);
$item->addParam(Snapshot::PARAM_SITE_URL, $meta->domain_backup);
$this->addItem($item);
} else throw new SnapshotMetaException("Can't find meta file version");
$this->_validateImportMeta();
}
/**
* @return void
* @throws SnapshotMetaException
*/
private function _validateImportMeta():void {
if(!$this->getName()) throw new SnapshotMetaException("Can't find name in meta file");
if(!$this->getEngine()) throw new SnapshotMetaException("Can't find engine in meta file");
if(!$this->getBackupType()) throw new SnapshotMetaException("Can't find backup type in meta file");
if(!$this->getCreated()) throw new SnapshotMetaException("Can't find creation date in meta file");
if(!$this->getContains()) throw new SnapshotMetaException("Can't find backup contains in meta file");
if(!$this->getStructure()) throw new SnapshotMetaException("Can't find backup structure in meta file");
// Schedules are optional - manual/on-demand backups may not have schedules
if(!$this->_items) throw new SnapshotMetaException("Can't find backup items in meta file");
}
}