<?php

defined('ABSPATH') || defined('DUPXABSPATH') || exit;

use VendorDuplicator\Amk\JsonSerialize\JsonSerialize;
use VendorDuplicator\Amk\JsonSerialize\JsonUnserializeMap;
use Duplicator\Addons\ProBase\License\License;
use Duplicator\Core\Controllers\ControllersManager;
use Duplicator\Core\MigrationMng;
use Duplicator\Installer\Package\ArchiveDescriptor;
use Duplicator\Installer\Package\DescriptorDBTableInfo;
use Duplicator\Libs\DupArchive\Headers\DupArchiveFileHeader;
use Duplicator\Libs\DupArchive\Headers\DupArchiveHeader;
use Duplicator\Libs\DupArchive\Processors\DupArchiveProcessingFailure;
use Duplicator\Libs\Snap\SnapIO;
use Duplicator\Libs\Snap\SnapOrigFileManager;
use Duplicator\Libs\Snap\SnapString;
use Duplicator\Libs\Snap\SnapUtil;
use Duplicator\Libs\Snap\SnapWP;
use Duplicator\Libs\WpConfig\WPConfigTransformer;
use Duplicator\Models\BrandEntity;
use Duplicator\Models\Storages\AbstractStorageEntity;
use Duplicator\Models\Storages\Local\LocalStorage;
use Duplicator\Models\Storages\StoragesUtil;
use Duplicator\Models\Storages\UnknownStorage;
use Duplicator\Models\SystemGlobalEntity;
use Duplicator\Package\Create\BuildComponents;
use Duplicator\Package\Create\BuildProgress;
use Duplicator\Package\Create\DatabaseInfo;
use Duplicator\Package\Create\DbBuildProgress;
use Duplicator\Package\Create\DupArchive\PackageDupArchiveCreateState;
use Duplicator\Package\Create\DupArchive\PackageDupArchiveExpandState;
use Duplicator\Package\NameFormat;
use Duplicator\Package\Recovery\RecoveryPackage;
use Duplicator\Package\Recovery\RecoveryStatus;
use Duplicator\Package\SettingsUtils;
use Duplicator\Utils\Crypt\CryptBlowfish;
use Duplicator\Utils\ExpireOptions;

require_once(DUPLICATOR____PATH . '/classes/package/class.pack.upload.info.php');
require_once(DUPLICATOR____PATH . '/classes/package/class.pack.multisite.php');
require_once(DUPLICATOR____PATH . '/classes/package/class.pack.archive.file.list.php');
require_once(DUPLICATOR____PATH . '/classes/class.exceptions.php');

final class DUP_PRO_PackageStatus
{
    const REQUIREMENTS_FAILED = -6;
    const STORAGE_FAILED      = -5;
    const STORAGE_CANCELLED   = -4;
    const PENDING_CANCEL      = -3;
    const BUILD_CANCELLED     = -2;
    const ERROR               = -1;
    const PRE_PROCESS         = 0;
    const SCANNING            = 3;
    const AFTER_SCAN          = 5;
    const START               = 10;
    const DBSTART             = 20;
    const DBDONE              = 39;
    const ARCSTART            = 40;
    const ARCVALIDATION       = 60;
    const ARCDONE             = 65;
    const COPIEDPACKAGE       = 70;
    const STORAGE_PROCESSING  = 75;
    const COMPLETE            = 100;
}

final class DUP_PRO_PackageType
{
    const MANUAL    = 0;
    const SCHEDULED = 1;
    const RUN_NOW   = 2;
}

abstract class DUP_PRO_Package_File_Type
{
    const Installer = 0;
    const Archive   = 1;
    const SQL       = 2;
    const Log       = 3;
    const Dump      = 4;
    const Scan      = 5;
}

/**
 * Class used to store and process all Backup logic
 *
 * @package Dupicator\classes
 */
class DUP_PRO_Package
{
    const OPT_ACTIVE = 'duplicator_pro_package_active';

    const FLAG_MANUAL                = 'MANUAL';
    const FLAG_SCHEDULE              = 'SCHEDULE';
    const FLAG_SCHEDULE_RUN_NOW      = 'SCHEDULE_RUN_NOW';
    const FLAG_DB_ONLY               = 'DB_ONLY';
    const FLAG_MEDIA_ONLY            = 'MEDIA_ONLY';
    const FLAG_HAVE_LOCAL            = 'HAVE_LOCAL';
    const FLAG_HAVE_REMOTE           = 'HAVE_REMOTE';
    const FLAG_DISASTER_AVAIABLE     = 'DISASTER_AVAIABLE';
    const FLAG_DISASTER_SET          = 'DISASTER_SET';
    const FLAG_CREATED_AFTER_RESTORE = 'CREATED_AFTER_RESTORE';
    const FLAG_ZIP_ARCHIVE           = 'ZIP_ARCHIVE';
    const FLAG_DUP_ARCHIVE           = 'DUP_ARCHIVE';
    const FLAG_ACTIVE                = 'ACTIVE'; // For future use
    const FLAG_TEMPLATE              = 'TEMPLATE'; // For future use
    const PACKAGE_HASH_DATE_FORMAT   = 'YmdHis';

    /** @var int<-1,max> */
    public $ID = -1;
    /** @var string */
    public $VersionWP = '';
    /** @var string */
    public $VersionDB = '';
    /** @var string */
    public $VersionPHP = '';
    /** @var string */
    public $VersionOS = '';
    /** @var string */
    protected $Name = '';
    /** @var string */
    protected $Hash = '';
    /** @var int Enum DUP_PRO_PackageType */
    public $Type = DUP_PRO_PackageType::MANUAL;
    /** @var string */
    public $notes = '';
    /** @var string */
    public $StorePath = DUPLICATOR_PRO_SSDIR_PATH_TMP;
    /** @var string */
    public $StoreURL = DUPLICATOR_PRO_SSDIR_URL . '/';
    /** @var string */
    public $ScanFile = '';
    /** @var float */
    public $timer_start = -1;
    /** @var string */
    public $Runtime = '';
    /** @var string */
    public $ExeSize = '0';
    /** @var string */
    public $ZipSize = '0';
    /** @var string */
    public $Brand = '';
    /** @var int<-2,max> */
    public $Brand_ID = -2;
    /** @var int Enum DUP_PRO_ZipArchive_Mode */
    public $ziparchive_mode = DUP_PRO_ZipArchive_Mode::Multithreaded;
    /** @var DUP_PRO_Archive */
    public $Archive = null;
    /** @var DUP_PRO_Multisite */
    public $Multisite = null;
    /** @var DUP_PRO_Installer */
    public $Installer = null;
    /** @var DUP_PRO_Database */
    public $Database = null;
    /** @var string[] */
    public $components = array();

    /** @var float */
    public $Status = DUP_PRO_PackageStatus::PRE_PROCESS;
    /** @var int<-1,max> */
    public $schedule_id = -1;
    // Schedule ID that created this
    // Chunking progress through build and storage uploads

    /** @var BuildProgress */
    public $build_progress = null;
    /** @var DbBuildProgress */
    public $db_build_progress = null;
    /** @var DUP_PRO_Package_Upload_Info[] */
    public $upload_infos = [];
    /** @var int<-1,max> */
    public $active_storage_id = -1;
    /** @var int<-1,max> */
    public $template_id = -1;
    /** @var bool */
    protected $buildEmailSent = false;

    /** @var string */
    protected $version = DUPLICATOR_PRO_VERSION;
    /** @var string */
    protected $created = '';
    /** @var string */
    protected $updated = '';
    /** @var string[] list ENUM self::FLAG_* */
    protected $flags = [];
    /** @var bool */
    protected $flagUpdatedAfterLoad = true;

    /**
     * Class contructor
     *
     * @param int                             $type       DUP_PRO_PackageType ENUM
     * @param int[]                           $storageIds Storages id
     * @param DUP_PRO_Package_Template_Entity $template   Template for Backup or null
     * @param DUP_PRO_Schedule_Entity         $schedule   Schedule for Backup or null
     */
    public function __construct(
        $type = DUP_PRO_PackageType::MANUAL,
        $storageIds = [],
        DUP_PRO_Package_Template_Entity $template = null,
        DUP_PRO_Schedule_Entity $schedule = null
    ) {
        global $wp_version;

        switch ($type) {
            case DUP_PRO_PackageType::MANUAL:
                $this->Type = DUP_PRO_PackageType::MANUAL;
                break;
            case DUP_PRO_PackageType::SCHEDULED:
                $this->Type = DUP_PRO_PackageType::SCHEDULED;
                break;
            case DUP_PRO_PackageType::RUN_NOW:
                $this->Type = DUP_PRO_PackageType::RUN_NOW;
                break;
            default:
                throw new Exception("Package type $type not supported");
        }

        $this->VersionOS  = defined('PHP_OS') ? PHP_OS : 'unknown';
        $this->VersionWP  = $wp_version;
        $this->VersionPHP = phpversion();
        $dbversion        = DUP_PRO_DB::getVersion();
        $this->VersionDB  = (empty($dbversion) ? '- unknown -' : $dbversion);

        if ($schedule !== null) {
            $this->schedule_id = $schedule->getId();
        }

        $timestamp     = time();
        $this->created = gmdate("Y-m-d H:i:s", $timestamp);
        $this->Name    = $this->getNameFromFormat($template, $timestamp);
        $this->Hash    = $this->make_hash();

        $this->components = BuildComponents::COMPONENTS_DEFAULT;

        $this->Database          = new DUP_PRO_Database($this);
        $this->Archive           = new DUP_PRO_Archive($this);
        $this->Multisite         = new DUP_PRO_Multisite();
        $this->Installer         = new DUP_PRO_Installer($this);
        $this->build_progress    = new BuildProgress();
        $this->db_build_progress = new DbBuildProgress();

        $this->build_progress->setBuildMode();

        $this->setByTemplate($template);
        if (empty($storageIds)) {
            $storageIds = [StoragesUtil::getDefaultStorageId()];
        }
        $this->add_upload_infos($storageIds);
        $this->updatePackageFlags();
    }

    /**
     * Set properties by template
     *
     * @param DUP_PRO_Package_Template_Entity $template template
     *
     * @return void
     */
    protected function setByTemplate(DUP_PRO_Package_Template_Entity $template = null)
    {
        if ($template === null) {
            return;
        }

        //BRAND
        $brand_data = BrandEntity::getByIdOrDefault((int) $template->installer_opts_brand);
        $brand_data->prepareAttachmentsInstaller();
        $this->Brand    = $brand_data->name;
        $this->Brand_ID = $brand_data->getId();

        //MULTISITE
        $this->Multisite->FilterSites = $template->filter_sites;

        //ARCHIVE
        $this->components           = $template->components;
        $this->Archive->FilterOn    = $template->archive_filter_on;
        $this->Archive->FilterDirs  = $template->archive_filter_dirs;
        $this->Archive->FilterExts  = $template->archive_filter_exts;
        $this->Archive->FilterFiles = $template->archive_filter_files;
        $this->Archive->FilterNames = $template->archive_filter_names;

        //INSTALLER
        $this->Installer->OptsDBHost   = $template->installer_opts_db_host;
        $this->Installer->OptsDBName   = $template->installer_opts_db_name;
        $this->Installer->OptsDBUser   = $template->installer_opts_db_user;
        $this->Installer->OptsSecureOn = $template->installer_opts_secure_on;
        $this->Installer->passowrd     = $template->installerPassowrd;
        $this->Installer->OptsSkipScan = $template->installer_opts_skip_scan;

        // CPANEL
        $this->Installer->OptsCPNLEnable   = $template->installer_opts_cpnl_enable;
        $this->Installer->OptsCPNLHost     = $template->installer_opts_cpnl_host;
        $this->Installer->OptsCPNLUser     = $template->installer_opts_cpnl_user;
        $this->Installer->OptsCPNLDBAction = $template->installer_opts_cpnl_db_action;
        $this->Installer->OptsCPNLDBHost   = $template->installer_opts_cpnl_db_host;
        $this->Installer->OptsCPNLDBName   = $template->installer_opts_cpnl_db_name;
        $this->Installer->OptsCPNLDBUser   = $template->installer_opts_cpnl_db_user;

        //DATABASE
        $this->Database->FilterOn        = $template->database_filter_on;
        $this->Database->prefixFilter    = $template->databasePrefixFilter;
        $this->Database->prefixSubFilter = $template->databasePrefixSubFilter;
        $this->Database->FilterTables    = $template->database_filter_tables;
        $this->Database->Compatible      = $template->database_compatibility_modes;
    }

    /**
     * Destructor
     *
     * @return void
     */
    public function __destruct()
    {
        unset($this->Database);
        unset($this->Archive);
        unset($this->Multisite);
        unset($this->Installer);
        unset($this->build_progress);
        unset($this->db_build_progress);
        foreach ($this->upload_infos as $obj) {
            unset($obj);
        }
        unset($this->upload_infos);
    }

    /**
     * Clone
     *
     * @return void
     */
    public function __clone()
    {
        $this->Database          = clone $this->Database;
        $this->Archive           = clone $this->Archive;
        $this->Multisite         = clone $this->Multisite;
        $this->Installer         = clone $this->Installer;
        $this->build_progress    = clone $this->build_progress;
        $this->db_build_progress = clone $this->db_build_progress;
        $cloneInfo               = array();
        foreach ($this->upload_infos as $key => $obj) {
            $cloneInfo[$key] = clone $obj;
        }
        $this->upload_infos = $cloneInfo;
    }

    /**
     * Get name
     *
     * @return string
     */
    public function getName()
    {
        return $this->Name;
    }

    /**
     * Get hash
     *
     * @return string
     */
    public function getHash()
    {
        return $this->Hash;
    }

    /**
     * Get name hash
     *
     * @return string
     */
    public function getNameHash()
    {
        return $this->Name . '_' . $this->Hash;
    }

    /**
     * Returns true if this is a DB only Backup
     *
     * @return bool
     */
    public function isDBOnly()
    {
        return BuildComponents::isDBOnly($this->components) || $this->Archive->ExportOnlyDB;
    }

    /**
     * Returns true if this is a File only Backup
     *
     * @return bool
     */
    public function isDBExcluded()
    {
        return BuildComponents::isDBExcluded($this->components);
    }

    /**
     * Cancel all uploads
     *
     * @return void
     */
    public function cancel_all_uploads()
    {
        DUP_PRO_Log::trace("Cancelling all uploads");
        foreach ($this->upload_infos as $upload_info) {
            if ($upload_info->has_completed() == false) {
                $upload_info->cancelTransfer();
            }
        }
    }

    /**
     * Get upload infos
     *
     * @return array<int,DUP_PRO_Package_Upload_Info>
     */
    public function get_latest_upload_infos()
    {
        $upload_infos = array();
        // Just save off the latest per the storage id
        foreach ($this->upload_infos as $upload_info) {
            $upload_infos[$upload_info->getStorageId()] = $upload_info;
        }

        return $upload_infos;
    }

    /**
     * What % along we are in the given status level
     *
     * @return float
     */
    public function get_status_progress()
    {
        if ($this->Status != DUP_PRO_PackageStatus::STORAGE_PROCESSING) {
            return 0.0;
        }

        if ($this->isDownloadInProgress()) {
            $activeInfo = null;
            foreach ($this->upload_infos as $uInfo) {
                if ($uInfo->isDownloadFromRemote() && $uInfo->has_completed() === false) {
                    $activeInfo = $uInfo;
                }
            }

            if (is_null($activeInfo)) {
                return 0.0;
            }

            return round($activeInfo->progress, 1);
        }

        $completed_infos  = 0;
        $total_infos      = count($this->upload_infos);
        $partial_progress = 0;
        foreach ($this->upload_infos as $upload_info) {
            if ($upload_info->isDownloadFromRemote()) {
                continue;
            }

            if ($upload_info->has_completed()) {
                $completed_infos++;
            } else {
                $partial_progress += $upload_info->progress;
            }
        }

        DUP_PRO_Log::trace("partial progress $partial_progress");
        DUP_PRO_Log::trace("completed infos before $completed_infos");
        $bcd = ($partial_progress / (float) 100);
        DUP_PRO_Log::trace("partial progress info contributor=$bcd");
        $completed_infos += $bcd;
        DUP_PRO_Log::trace("completed infos after $completed_infos");
        // Add on the particulars where the latest guy is at
        // return 100 * (bcdiv($completed_infos, $total_infos, 2));
        return DUP_PRO_U::percentage($completed_infos, $total_infos, 0);
    }

    /**
     * Add upload info
     *
     * @param int[] $storage_ids storage ids
     *
     * @return void
     */
    public function add_upload_infos($storage_ids)
    {
        DUP_PRO_Log::trace('adding upload infos');
        $this->upload_infos = [];
        foreach ($storage_ids as $storage_id) {
            $storage_id_is_exist = AbstractStorageEntity::exists($storage_id);
            if ($storage_id_is_exist) {
                $this->upload_infos[] = new DUP_PRO_Package_Upload_Info($storage_id);
            }
        }
        DUP_PRO_Log::trace("Number update infos added:" . count($this->upload_infos));
    }

    /**
     * Get display size
     *
     * @return string
     */
    public function get_display_size()
    {
        $global = DUP_PRO_Global_Entity::getInstance();
        if ($this->Status == 100 || $this->transferWasInterrupted()) {
            return DUP_PRO_U::byteSize($this->Archive->Size);
        } elseif (
            ($this->build_progress->current_build_mode == DUP_PRO_Archive_Build_Mode::DupArchive) &&
            ($this->Status >= DUP_PRO_PackageStatus::ARCVALIDATION) &&
            ($this->Status <= DUP_PRO_PackageStatus::ARCDONE)
        ) {
            return __('Validating', 'duplicator-pro');
        } elseif (
            (($this->build_progress->current_build_mode == DUP_PRO_Archive_Build_Mode::Shell_Exec) ||
            (($this->build_progress->current_build_mode == DUP_PRO_Archive_Build_Mode::ZipArchive) &&
            ($global->ziparchive_mode == DUP_PRO_ZipArchive_Mode::SingleThread))) &&
            ($this->Status <= DUP_PRO_PackageStatus::ARCDONE) &&
            ($this->Status >= DUP_PRO_PackageStatus::PRE_PROCESS)
        ) {
            return __('Building', 'duplicator-pro');
        } else {
            $size              = 0;
            $temp_archive_path = DUPLICATOR_PRO_SSDIR_PATH_TMP . '/' . $this->get_archive_filename();
            $archive_path      = DUPLICATOR_PRO_SSDIR_PATH . '/' . $this->get_archive_filename();
            if (file_exists($archive_path)) {
                $size = @filesize($archive_path);
            } elseif (file_exists($temp_archive_path)) {
                $size = @filesize($temp_archive_path);
            } else {
                //  DUP_PRO_Log::trace("Couldn't find archive for file size");
            }
            return DUP_PRO_U::byteSize($size);
        }
    }

    /**
     * Get scan filename
     *
     * @return string
     */
    public function get_scan_filename()
    {
        return $this->getNameHash() . '_scan.json';
    }

    /**
     * Get scan URL
     *
     * @return string
     */
    public function get_scan_url()
    {
        return $this->StoreURL . $this->get_scan_filename();
    }

    /**
     * Get safe scan filepath
     *
     * @return string
     */
    public function get_safe_scan_filepath()
    {
        $filename = $this->get_scan_filename();
        return SnapIO::safePath(DUPLICATOR_PRO_SSDIR_PATH . "/$filename");
    }

    /**
     * Get log filename
     *
     * @return string
     */
    public function get_log_filename()
    {
        return $this->getNameHash() . '_log.txt';
    }

    /**
     * @return string Url to the Backup's log file
     */
    public function get_log_url()
    {
        $link_log = $this->StoreURL . $this->get_log_filename();
        if (!file_exists($this->get_safe_log_filepath())) {
            // backward compatibility
            $link_log = "{$this->StoreURL}{$this->getNameHash()}.log";
        }

        return $link_log;
    }

    /**
     * Get dump filename
     *
     * @return string
     */
    public function get_dump_filename()
    {
        return $this->getNameHash() . '_dump.txt';
    }

    /**
     * Get safe log filepath
     *
     * @return string
     */
    public function get_safe_log_filepath()
    {
        $filename = $this->get_log_filename();
        return SnapIO::safePath(DUPLICATOR_PRO_SSDIR_PATH . "/$filename");
    }

    /**
     * Dump file exists
     *
     * @return bool
     */
    public function dump_file_exists()
    {
        $filename = $this->get_dump_filename();
        $filepath = SnapIO::safePath(DUPLICATOR_PRO_DUMP_PATH . "/$filename");
        return file_exists($filepath);
    }

    /**
     * Get upload info for storage id
     *
     * @param int $storage_id storage id
     *
     * @return ?DUP_PRO_Package_Upload_Info
     */
    public function get_upload_info_for_storage_id($storage_id)
    {
        $selected_upload_info = null;
        foreach ($this->upload_infos as $upload_info) {
            if ($upload_info->getStorageId() == $storage_id) {
                $selected_upload_info = &$upload_info;
                break;
            }
        }

        return $selected_upload_info;
    }

    /**
     * Get local Backup file
     *
     * @param int $file_type DUP_PRO_Package_File_Type
     *
     * @return bool|string file path or false if don't exists
     */
    public function getLocalPackageFilePath($file_type)
    {
        switch ($file_type) {
            case DUP_PRO_Package_File_Type::Installer:
                $fileName = $this->Installer->getInstallerLocalName();
                break;
            case DUP_PRO_Package_File_Type::Archive:
                $fileName = $this->get_archive_filename();
                break;
            case DUP_PRO_Package_File_Type::Log:
                $fileName = $this->get_log_filename();
                break;
            default:
                throw new Exception("File type $file_type not supported");
        }

        //First check if default file exists
        if (file_exists($filePath = SnapIO::trailingslashit(DUPLICATOR_PRO_SSDIR_PATH) . $fileName)) {
            return SnapIO::safePath($filePath);
        }

        foreach ($this->getLocalStorages() as $localStorage) {
            $filePath = SnapIO::trailingslashit($localStorage->getLocationString()) . $fileName;
            if (file_exists($filePath)) {
                return SnapIO::safePath($filePath);
            }
        }

        return false;
    }

    /**
     * @param int $fileType Type of File to be Downloaded (DUP_PRO_Package_File_Type)
     *
     * @return string URL at which the file can be downloaded
     */
    public function getLocalPackageFileURL($fileType)
    {
        if ($fileType == DUP_PRO_Package_File_Type::Log) {
            return $this->get_log_url();
        }

        if (!$this->getLocalPackageFilePath($fileType)) {
            return "";
        }

        switch ($fileType) {
            case DUP_PRO_Package_File_Type::Installer:
                return $this->getLocalPackageAjaxDownloadURL(DUP_PRO_Package_File_Type::Installer);
            case DUP_PRO_Package_File_Type::Archive:
                return file_exists(SnapIO::trailingslashit(DUPLICATOR_PRO_SSDIR_PATH) . $this->get_archive_filename())
                    ? $this->Archive->getURL()
                    : $this->getLocalPackageAjaxDownloadURL(DUP_PRO_Package_File_Type::Archive);
            default:
                throw new Exception("File type $fileType not supported");
        }
    }

    /**
     * Get download security token
     *
     * @param string $hash hash
     *
     * @return string
     */
    public static function getLocalPackageAjaxDownloadToken($hash)
    {
        return md5($hash . CryptBlowfish::getDefaultKey());
    }

    /**
     * Get local Backup ajax download url
     *
     * @param int $fileType Type of File to be Downloaded (DUP_PRO_Package_File_Type)
     *
     * @return string URL at which the file can be downloaded
     */
    public function getLocalPackageAjaxDownloadURL($fileType)
    {
        return admin_url('admin-ajax.php') . "?" . http_build_query(array(
            'action'   => 'duplicator_pro_download_package_file',
            'hash'     =>  $this->Hash,
            'token'    =>  self::getLocalPackageAjaxDownloadToken($this->Hash),
            'fileType' => $fileType,
        ));
    }

    /**
     * Return list of local storages
     *
     * @return AbstractStorageEntity[]
     */
    public function getLocalStorages()
    {
        $storages = [];
        foreach ($this->upload_infos as $upload_info) {
            if (!$upload_info->isLocal()) {
                continue;
            }
            $storages[] = $upload_info->getStorage();
        }
        return $storages;
    }

    /**
     * Validates the inputs from the UI for correct data input
     *
     * @return DUP_PRO_Validator
     */
    public function validateInputs()
    {
        $validator = new DUP_PRO_Validator();

        if ($this->Archive->FilterOn) {
            $validator->explode_filter_custom(
                $this->Archive->FilterDirs,
                ';',
                DUP_PRO_Validator::FILTER_VALIDATE_FOLDER_WITH_COMMENT,
                array(
                    'valkey' => 'FilterDirs',
                    'errmsg' => __('Directory: <b>%1$s</b> is an invalid path. Please remove the value from the Archive > Files Tab > Folders input box and apply only valid paths.', 'duplicator-pro'),
                )
            );

            $validator->explode_filter_custom(
                $this->Archive->FilterExts,
                ';',
                DUP_PRO_Validator::FILTER_VALIDATE_FILE_EXT,
                array(
                    'valkey' => 'FilterExts',
                    'errmsg' => __('File extension: <b>%1$s</b> is an invalid extension name. Please remove the value from the Archive > Files Tab > File Extensions input box and apply only valid extensions. For example \'jpg\'', 'duplicator-pro'),
                )
            );

            $validator->explode_filter_custom(
                $this->Archive->FilterFiles,
                ';',
                DUP_PRO_Validator::FILTER_VALIDATE_FILE_WITH_COMMENT,
                array(
                    'valkey' => 'FilterFiles',
                    'errmsg' => __('File: <b>%1$s</b> is an invalid file name. Please remove the value from the Archive > Files Tab > Files input box and apply only valid file names.', 'duplicator-pro'),
                )
            );
        }

        //FILTER_VALIDATE_DOMAIN throws notice message on PHP 5.6
        if (defined('FILTER_VALIDATE_DOMAIN')) {
            // phpcs:ignore PHPCompatibility.Constants.NewConstants.filter_validate_domainFound
            $validator->filter_var($this->Installer->OptsDBHost, FILTER_VALIDATE_DOMAIN, array(
                'valkey'   => 'OptsDBHost',
                'errmsg'   => __('MySQL Server Host: <b>%1$s</b> isn\'t a valid host', 'duplicator-pro'),
                'acc_vals' => array(
                    '',
                    'localhost',
                ),
            ));
        }

        /*
         * no exist in PRO version
          $validator->filter_var($this->Installer->OptsDBPort, FILTER_VALIDATE_INT , array(
          'valkey' => 'OptsDBPort' ,
          'errmsg' => __('MySQL Server Port: <b>%1$s</b> isn\'t a valid port', 'duplicator-pro'),
          'acc_vals' => array(
          ''
          ),
          'options' => array(
          'min_range' => 0
          )
          )
          );
         */
        return $validator;
    }

    /**
     * Process storage upload
     *
     * @return bool
     */
    public function process_storages()
    {
        //START LOGGING
        DUP_PRO_Log::open($this->getNameHash());
        DUP_PRO_Log::info("-----------------------------------------");
        DUP_PRO_Log::info("STORAGE PROCESSING THREAD INITIATED");
        $complete = (count($this->upload_infos) == 0);
        // Indicates if all storages have finished (succeeded or failed all-together)

        $error_present         = false;
        $local_default_present = false;
        if (!$complete) {
            $complete            = true;
            $latest_upload_infos = $this->get_latest_upload_infos();

            foreach ($latest_upload_infos as $upload_info) {
                if ($upload_info->isDefaultStorage()) {
                    $local_default_present = true;
                }

                if ($upload_info->isFailed()) {
                    DUP_PRO_Log::trace("The following Upload Info is marked as failed");
                    DUP_PRO_Log::traceObject('upload_info var:', $upload_info);
                    $error_present = true;
                } elseif ($upload_info->has_completed() == false) {
                    DUP_PRO_Log::trace("The following Upload Info hasn't completed yet");
                    DUP_PRO_Log::traceObject('upload_info var:', $upload_info);
                    $complete = false;
                    DUP_PRO_Log::trace("Telling storage id {$upload_info->getStorageId()} to process");
                    if (($storage = AbstractStorageEntity::getById($upload_info->getStorageId())) === false) {
                        DUP_PRO_Log::trace('Storage Object is null. Maybe storage is deleted.');
                    } else {
                        if ($upload_info->has_started() === false) {
                            DUP_PRO_Log::trace("Upload Info hasn't started yet, starting it");
                            $upload_info->start();
                        }

                        // Process a bit of work then let the next cron take care of if it's completed or not.
                        StoragesUtil::processPackage($this, $upload_info);
                    }
                    break;
                } else {
                    if (($storage = AbstractStorageEntity::getById($upload_info->getStorageId())) !== false) {
                        $storage_type_string = strtoupper($storage->getStypeName());
                        DUP_PRO_Log::trace(
                            "Upload Info already completed for storage id: " . $upload_info->getStorageId() .
                            ", type: " . $storage_type_string . ", name: " . $storage->getName()
                        );
                    }
                }
            }
        } else {
            DUP_PRO_Log::trace("No storage ids defined for Backup $this->ID!");
            $error_present = true;
        }

        if ($complete) {
            DUP_PRO_Log::info("STORAGE PROCESSING COMPLETED");
            if ($error_present) {
                DUP_PRO_Log::trace("Storage error is present");
                $this->set_status(DUP_PRO_PackageStatus::COMPLETE);
                $this->post_scheduled_build_processing(1, false);
                if ($local_default_present == false) {
                    DUP_PRO_Log::trace("Deleting Backup files from default location.");
                    self::delete_default_local_files($this->getNameHash(), true, false);
                }
            } else {
                if ($local_default_present == false) {
                    DUP_PRO_Log::trace("Deleting Backup files from default location.");
                    self::delete_default_local_files($this->getNameHash(), true, false);
                } else {
                    $default_local_storage = StoragesUtil::getDefaultStorage();
                    DUP_PRO_Log::trace('Purge old default local storage Backups');
                    $default_local_storage->purgeOldPackages();
                }
                $this->set_status(DUP_PRO_PackageStatus::COMPLETE);
                $this->post_scheduled_build_processing(1, true);
            }
            do_action('duplicator_pro_package_transfer_completed', $this);
        }

        return $complete;
    }

    /**
     * Conditions Example
     * [
     *   relation = 'AND',
     *   [ 'op' => '>=' , 'status' =>  DUP_PRO_PackageStatus::START ]
     *   [ 'op' => '<' , 'status' =>  DUP_PRO_PackageStatus::COMPLETED ]
     * ]
     *
     * @param array<string|int,string|array{op:string,status:int}> $conditions Conditions
     *
     * @return string
     */
    protected static function statusContitionsToWhere($conditions = array())
    {
        $accepted_op = array(
            '<',
            '>',
            '=',
            '<>',
            '>=',
            '<=',
        );
        $relation    = (isset($conditions['relation']) && strtoupper($conditions['relation']) == 'OR') ? ' OR ' : ' AND ';
        unset($conditions['relation']);
        $where = '';
        if (!empty($conditions)) {
            $str_conds = array();
            foreach ($conditions as $cond) {
                $op          = (isset($cond['op']) && in_array($cond['op'], $accepted_op)) ? $cond['op'] : '=';
                $status      = isset($cond['status']) ? (int) $cond['status'] : 0;
                $str_conds[] = 'status ' . $op . ' ' . $status;
            }

            $where = implode($relation, $str_conds) . ' ';
        } else {
            $where = '1 ';
        }

        return $where;
    }

    /**
     * Select Backups from database
     *
     * @param string $where      where conditions
     * @param int    $limit      max row numbers if 0 the limit is PHP_INT_MAX
     * @param int    $offset     offset 0 is at begin
     * @param string $orderBy    default `id` ASC if empty no order
     * @param string $resultType ids => int[], row => row without Backup blob, fullRow => row with Backup blob, objs => DUP_Package objects[]
     *
     * @return DUP_PRO_Package[]|object[]|int[]
     */
    public static function dbSelect(
        $where,
        $limit = 0,
        $offset = 0,
        $orderBy = '`id` ASC',
        $resultType = 'objs'
    ) {
        global $wpdb;
        $table      = self::getTableName();
        $where      = ' WHERE ' . (strlen($where) > 0 ? $where : '1');
        $packages   = array();
        $offsetStr  = ' OFFSET ' . (int) $offset;
        $limitStr   = ' LIMIT ' . ($limit > 0 ? $limit : PHP_INT_MAX);
        $orderByStr = empty($orderBy) ? '' : ' ORDER BY ' . $orderBy . ' ';
        switch ($resultType) {
            case 'ids':
                $cols = '`id`';
                break;
            case 'row':
                $cols = '`id`,`name`,`hash`,`archive_name`,`status`,`progress`,`flags`,`owner`,`version`,`created`,`updated_at`';
                break;
            case 'fullRow':
            case 'objs':
            default:
                $cols = '*';
                break;
        }

        $rows = $wpdb->get_results('SELECT ' . $cols . ' FROM `' . $table . '` ' . $where . $orderByStr . $limitStr . $offsetStr);
        if ($rows != null) {
            switch ($resultType) {
                case 'ids':
                    foreach ($rows as $row) {
                        $packages[] = $row->id;
                    }
                    break;
                case 'row':
                case 'fullRow':
                    $packages = $rows;
                    break;
                case 'objs':
                default:
                    foreach ($rows as $row) {
                        $package = self::package_from_row($row);
                        if ($package != null) {
                            $packages[] = $package;
                        }
                    }
            }
        }
        return $packages;
    }

    /**
     * Execute $callback function foreach Backup result
     *
     * @param callable $callback function callback(DUP_PRO_Package $package)
     * @param string   $where    where conditions
     * @param int      $limit    max row numbers if 0 the limit is PHP_INT_MAX
     * @param int      $offset   offset 0 is at begin
     * @param string   $orderBy  default `id` ASC if empty no order
     *
     * @return void
     */
    public static function dbSelectCallback(
        $callback,
        $where,
        $limit = 0,
        $offset = 0,
        $orderBy = '`id` ASC'
    ) {
        if (!is_callable($callback)) {
            throw new Exception('No callback function passed');
        }

        $ids = self::dbSelect($where, $limit, $offset, $orderBy, 'ids');

        foreach ($ids as $id) {
            if (($package = self::get_by_id($id)) == false) {
                continue;
            }

            call_user_func($callback, $package);
            unset($package);
        }
    }

    /**
     * Get Backups with status conditions and/or pagination
     * Conditions Example
     * [
     *   relation = 'AND',
     *   [ 'op' => '>=' , 'status' =>  DUP_PRO_PackageStatus::START ]
     *   [ 'op' => '<' , 'status' =>  DUP_PRO_PackageStatus::COMPLETED ]
     * ]
     *
     * @param array<string|int,string|array{op:string,status:int}> $conditions Conditions if empty get all Backups
     * @param int                                                  $limit      max row numbers if 0 the limit is PHP_INT_MAX
     * @param int                                                  $offset     offset 0 is at begin
     * @param string                                               $orderBy    default `id` ASC if empty no order
     * @param string                                               $resultType ids => int[], row => row without Backup blob, fullRow => row with Backup blob, objs => DUP_Package objects[]
     *
     * @return DUP_PRO_Package[]|object[]|int[]
     */
    public static function get_packages_by_status(
        $conditions = array(),
        $limit = 0,
        $offset = 0,
        $orderBy = '`id` ASC',
        $resultType = 'objs'
    ) {
        return self::dbSelect(self::statusContitionsToWhere($conditions), $limit, $offset, $orderBy, $resultType);
    }

    /**
     * Get Backups row db with status conditions and/or pagination
     *
     * Conditions Example
     * [
     *   relation = 'AND',
     *   [ 'op' => '>=' , 'status' =>  DUP_PRO_PackageStatus::START ]
     *   [ 'op' => '<' , 'status' =>  DUP_PRO_PackageStatus::COMPLETED ]
     * ]
     *
     * @param array<string|int,string|array{op:string,status:int}> $conditions Conditions if empty get all Backups
     * @param int                                                  $limit      max row numbers if 0 the limit is PHP_INT_MAX
     * @param int                                                  $offset     offset 0 is at begin
     * @param string                                               $orderBy    default `id` ASC if empty no order
     *
     * @return object[]      // return row database without Backup blob
     */
    public static function get_row_by_status(
        $conditions = array(),
        $limit = 0,
        $offset = 0,
        $orderBy = '`id` ASC'
    ) {
        return self::dbSelect(self::statusContitionsToWhere($conditions), $limit, $offset, $orderBy, 'row');
    }

    /**
     * Get Backups ids with status conditions and/or pagination
     * Conditions Example
     * [
     *   relation = 'AND',
     *   [ 'op' => '>=' , 'status' =>  DUP_PRO_PackageStatus::START ]
     *   [ 'op' => '<' , 'status' =>  DUP_PRO_PackageStatus::COMPLETED ]
     * ]
     *
     * @param array<string|int,string|array{op:string,status:int}> $conditions Conditions if empty get all Backups
     * @param int                                                  $limit      max row numbers if 0 the limit is PHP_INT_MAX
     * @param int                                                  $offset     offset 0 is at begin
     * @param string                                               $orderBy    default `id` ASC if empty no order
     *
     * @return int[] return row database without Backup blob
     */
    public static function get_ids_by_status(
        $conditions = array(),
        $limit = 0,
        $offset = 0,
        $orderBy = '`id` ASC'
    ) {
        return self::dbSelect(self::statusContitionsToWhere($conditions), $limit, $offset, $orderBy, 'ids');
    }

    /**
     * count Backup with status condition
     * Conditions Example
     * [
     *   relation = 'AND',
     *   [ 'op' => '>=' , 'status' =>  DUP_PRO_PackageStatus::START ]
     *   [ 'op' => '<' , 'status' =>  DUP_PRO_PackageStatus::COMPLETED ]
     * ]
     *
     * @param array<string|int,string|array{op:string,status:int}> $conditions Conditions if empty get all Backups
     *
     * @return int
     */
    public static function count_by_status($conditions = array())
    {
        global $wpdb;
        $table = self::getTableName();
        $where = self::statusContitionsToWhere($conditions);
        $count = $wpdb->get_var("SELECT count(id) FROM `{$table}` WHERE " . $where);
        return $count;
    }

    /**
     * Execute $callback function foreach Backup result
     * For each iteration the memory is released
     * Conditions Example
     * [
     *   relation = 'AND',
     *   [ 'op' => '>=' , 'status' =>  DUP_PRO_PackageStatus::START ]
     *   [ 'op' => '<' , 'status' =>  DUP_PRO_PackageStatus::COMPLETED ]
     * ]
     *
     * @param callable                                             $callback   function callback(DUP_PRO_Package $package)
     * @param array<string|int,string|array{op:string,status:int}> $conditions Conditions if empty get all Backups
     * @param int                                                  $limit      max row numbers if 0 the limit is PHP_INT_MAX
     * @param int                                                  $offset     offset 0 is at begin
     * @param string                                               $orderBy    default `id` ASC if empty no order
     *
     * @return void
     */
    public static function by_status_callback(
        $callback,
        $conditions = array(),
        $limit = 0,
        $offset = 0,
        $orderBy = '`id` ASC'
    ) {
        self::dbSelectCallback($callback, self::statusContitionsToWhere($conditions), $limit, $offset, $orderBy);
    }

    /**
     * Set Backup for cancellation
     *
     * @return void
     */
    public function set_for_cancel()
    {
        $pending_cancellations = self::get_pending_cancellations();
        if (!in_array($this->ID, $pending_cancellations)) {
            array_push($pending_cancellations, $this->ID);
            ExpireOptions::set(DUPLICATOR_PRO_PENDING_CANCELLATION_TRANSIENT, $pending_cancellations, DUPLICATOR_PRO_PENDING_CANCELLATION_TIMEOUT);
        }
    }

    /**
     * Get all Backups marked for cancellation
     *
     * @return int[] array of Backup ids
     */
    public static function get_pending_cancellations()
    {
        $pending_cancellations = ExpireOptions::get(DUPLICATOR_PRO_PENDING_CANCELLATION_TRANSIENT);
        if ($pending_cancellations === false) {
            $pending_cancellations = array();
        }
        return $pending_cancellations;
    }

    /**
     * Check if the Backup is marked for cancellation
     *
     * @return bool
     */
    public function is_cancel_pending()
    {
        $pending_cancellations = self::get_pending_cancellations();
        return in_array($this->ID, $pending_cancellations);
    }

    /**
     * Clear all pending cancellations
     *
     * @return void
     */
    public static function clear_pending_cancellations()
    {
        if (ExpireOptions::delete(DUPLICATOR_PRO_PENDING_CANCELLATION_TRANSIENT) == false) {
            DUP_PRO_Log::traceError("Couldn't remove pending cancel transient");
        }
    }

    /**
     *
     * @param int $id Backup ID
     *
     * @return false|DUP_PRO_Package false if fail
     */
    public static function get_by_id($id)
    {
        if ($id < 0) {
            return false;
        }

        global $wpdb;
        $table = self::getTableName();
        $sql   = $wpdb->prepare("SELECT * FROM `{$table}` where ID = %d", $id);
        $row   = $wpdb->get_row($sql);
        //DUP_PRO_Log::traceObject('Object row', $row);
        if ($row) {
            return self::package_from_row($row);
        } else {
            return false;
        }
    }

    /**
     *
     * @param string $hash Hash
     *
     * @return DUP_PRO_Package|bool false if fail
     */
    public static function get_by_hash($hash)
    {
        global $wpdb;
        $table = self::getTableName();
        $sql   = $wpdb->prepare("SELECT * FROM `{$table}` where hash = %s", $hash);
        $row   = $wpdb->get_row($sql);
        if ($row) {
            return self::package_from_row($row);
        } else {
            return false;
        }
    }

    /**
     *
     * @param object $row Database row
     *
     * @return null|DUP_PRO_Package
     */
    private static function package_from_row($row)
    {
        $package = null;

        if (strlen($row->hash) == 0) {
            DUP_PRO_Log::trace("Hash is 0 for the Backup $row->id...");
            return null;
        }

        if (property_exists($row, 'id')) {
            $row->id = (int) $row->id;
        }
        if (property_exists($row, 'status')) {
            $row->status = (int) $row->status;
        }
        if (property_exists($row, 'progress')) {
            $row->progress = (float) $row->progress;
            if (property_exists($row, 'status') && $row->progress == 0 && $row->status > 0) {
                $row->progress = (float) $row->status;
            }
        }
        if (property_exists($row, 'flags')) {
            if (strlen($row->flags) == 0) {
                $row->flags = [];
            } else {
                $row->flags = explode(',', $row->flags);
            }
        }

        try {
            $package = self::get_from_json($row->package, $row);
        } catch (Exception $ex) {
            DUP_PRO_Log::traceError("Problem getting Backup from json. msg: " . $ex->getMessage());
            return null;
        }

        return $package;
    }

    /**
     *
     * @param boolean $delete_temp Deprecated, always true
     *
     * @return boolean
     */
    public function delete($delete_temp = false)
    {
        $ret_val = false;
        global $wpdb;
        $tblName   = self::getTableName();
        $getResult = $wpdb->get_results($wpdb->prepare("SELECT name, hash FROM `{$tblName}` WHERE id = %d", $this->ID), ARRAY_A);
        if ($getResult) {
            $row       = $getResult[0];
            $name_hash = "{$row['name']}_{$row['hash']}";
            $delResult = $wpdb->query($wpdb->prepare("DELETE FROM `{$tblName}` WHERE id = %d", $this->ID));
            if ($delResult != 0) {
                $ret_val = true;
                self::delete_default_local_files($name_hash, $delete_temp);
                $this->delete_local_storage_files();
            }
        }

        return $ret_val;
    }

    /**
     * Use only in extreme cases to get rid of a runaway Backup
     *
     * @param int $id Backup ID
     *
     * @return boolean
     */
    public static function force_delete($id)
    {
        $ret_val = false;
        global $wpdb;
        $tblName   = self::getTableName();
        $getResult = $wpdb->get_results($wpdb->prepare("SELECT name, hash FROM `{$tblName}` WHERE id = %d", $id), ARRAY_A);
        if ($getResult) {
            $row       = $getResult[0];
            $name_hash = "{$row['name']}_{$row['hash']}";
            $delResult = $wpdb->query($wpdb->prepare("DELETE FROM `{$tblName}` WHERE id = %d", $id));
            if ($delResult != 0) {
                $ret_val = true;
                self::delete_default_local_files($name_hash, true);
            }
        }

        return $ret_val;
    }

    /**
     * Delete local storage files
     *
     * @return void
     */
    private function delete_local_storage_files()
    {
        $storages            = $this->get_storages();
        $archive_filename    = $this->get_archive_filename();
        $installer_filename  = $this->Installer->getInstallerLocalName();
        $log_filename        = $this->get_log_filename();
        $scan_filename       = $this->get_scan_filename();
        $files_list_filename = $this->get_files_list_filename();
        $dirs_list_filename  = $this->get_dirs_list_filename();

        foreach ($storages as $storage) {
            if ($storage->getSType() !== LocalStorage::getSType()) {
                continue;
            }
            $path                = $storage->getLocationString();
            $archive_filepath    = $path . "/" . $archive_filename;
            $installer_filepath  = $path . "/" . $installer_filename;
            $log_filepath        = $path . "/" . $log_filename;
            $scan_filepath       = $path . "/" . $scan_filename;
            $files_list_filepath = $path . "/" . $files_list_filename;
            $dirs_list_filepath  = $path . "/" . $dirs_list_filename;
            @unlink($archive_filepath);
            @unlink($installer_filepath);
            @unlink($log_filepath);
            @unlink($scan_filepath);
            @unlink($files_list_filepath);
            @unlink($dirs_list_filepath);
        }
    }

    /**
     * Delete default local files
     *
     * @param string $name_hash        Package namehash
     * @param bool   $delete_temp      if set to true will delete temp files too
     * @param bool   $delete_log_files if set to true will delete log files too
     *
     * @return void
     */
    public static function delete_default_local_files($name_hash, $delete_temp, $delete_log_files = true)
    {
        if ($delete_temp) {
            self::deletePackageFilesInDir($name_hash, DUPLICATOR_PRO_SSDIR_PATH_TMP, true);
        }
        self::deletePackageFilesInDir($name_hash, DUPLICATOR_PRO_SSDIR_PATH, $delete_log_files);
    }

    /**
     * Removes all files related to the namehash from the directory
     *
     * @param string $nameHash       Package namehash
     * @param string $dir            path to dir
     * @param bool   $deleteLogFiles if set to true will delete log files too
     *
     * @return void
     */
    public static function deletePackageFilesInDir($nameHash, $dir, $deleteLogFiles = false)
    {
        $globFiles = glob(SnapIO::safePath(SnapIO::untrailingslashit($dir) . "/" . $nameHash . "_*"));
        foreach ($globFiles as $globFile) {
            if (!$deleteLogFiles && SnapString::endsWith($globFile, '_log.txt')) {
                DUP_PRO_Log::trace("Skipping purge of $globFile because deleteLogFiles is false.");
                continue;
            }

            if (SnapIO::unlink($globFile)) {
                DUP_PRO_Log::trace("Successful purge of $globFile.");
            } else {
                DUP_PRO_Log::trace("Failed purge of $globFile.");
            }
        }
    }

    /**
     * Return Backup from json
     *
     * @param string  $json    json string
     * @param ?object $rowData Database row data
     *
     * @return self
     */
    protected static function get_from_json($json, $rowData = null)
    {
        $map = new JsonUnserializeMap(
            [
                ''                                           => 'cl:' . self::class,
                'Archive'                                    => 'cl:' . DUP_PRO_Archive::class,
                'Archive/Package'                            => 'rf:',
                'Archive/FilterInfo'                         => 'cl:' . DUP_PRO_Archive_Filter_Info::class,
                'Archive/FilterInfo/Dirs'                    => '?cl:' . DUP_PRO_Archive_Filter_Scope_Directory::class,
                'Archive/FilterInfo/Files'                   => '?cl:' . DUP_PRO_Archive_Filter_Scope_File::class,
                'Archive/FilterInfo/Exts'                    => '?cl:' . DUP_PRO_Archive_Filter_Scope_Base::class,
                'Archive/FilterInfo/TreeSize'                => '?cl:' . DUP_PRO_Tree_files::class,
                'Archive/listDirObj'                         => '?cl:' . DUP_PRO_Archive_File_List::class,
                'Archive/listFileObj'                        => '?cl:' . DUP_PRO_Archive_File_List::class,
                'Multisite'                                  => 'cl:' . DUP_PRO_Multisite::class,
                'Installer'                                  => 'cl:' . DUP_PRO_Installer::class,
                'Installer/Package'                          => 'rf:',
                'Installer/origFileManger'                   => '?cl:' . SnapOrigFileManager::class,
                'Installer/configTransformer'                => '?cl:' . WPConfigTransformer::class,
                'Installer/archiveDescriptor'                => '?cl:' . ArchiveDescriptor::class,
                'Database'                                   => 'cl:' . DUP_PRO_Database::class,
                'Database/Package'                           => 'rf:',
                'Database/info'                              => 'cl:' . DatabaseInfo::class,
                'Database/info/tablesList/*'                 => 'cl:' . DescriptorDBTableInfo::class,
                'build_progress'                             => 'cl:' . BuildProgress::class,
                'build_progress/dupCreate'                   => '?cl:' . PackageDupArchiveCreateState::class,
                'build_progress/dupCreate/package'           => 'rf:',
                'build_progress/dupCreate/archiveHeader'     => 'cl:' . DupArchiveHeader::class,
                'build_progress/dupCreate/failures/*'        => 'cl:' . DupArchiveProcessingFailure::class,
                'build_progress/dupExpand'                   => '?cl:' . PackageDupArchiveExpandState::class,
                'build_progress/dupExpand/package'           => 'rf:',
                'build_progress/dupExpand/archiveHeader'     => 'cl:' . DupArchiveHeader::class,
                'build_progress/dupExpand/currentFileHeader' => '?cl:' . DupArchiveFileHeader::class,
                'build_progress/dupExpand/failures/*'        => 'cl:' . DupArchiveProcessingFailure::class,
                'db_build_progress'                          => 'cl:' . DbBuildProgress::class,
                'upload_infos/*'                             => 'cl:' . DUP_PRO_Package_Upload_Info::class,
            ]
        );

        $package = JsonSerialize::unserializeWithMap($json, $map);
        if (!$package instanceof self) {
            throw new Exception('Can\'t read json object ');
        }

        if (is_object($rowData)) {
            $reflect = new ReflectionObject($package);

            $dbValuesToProps = [
                'id'         => 'ID',
                'name'       => 'Name',
                'hash'       => 'Hash',
                'status'     => 'Status',
                'flags'      => 'flags',
                'version'    => 'version',
                'created'    => 'created',
                'updated_at' => 'updated',
            ];

            foreach ($dbValuesToProps as $dbKey => $propName) {
                if (
                    !isset($rowData->{$dbKey}) ||
                    !property_exists($package, $propName)
                ) {
                    continue;
                }

                $prop = $reflect->getProperty($propName);
                $prop->setAccessible(true);
                $prop->setValue($package, $rowData->{$dbKey});
            }
        }

        if (strlen($package->getVersion()) == 0) {
            $tmp              = JsonSerialize::unserialize($json);
            $package->version = $tmp['Version'];
        }
        $package->flagUpdatedAfterLoad = false; //Don't load Backup flags here to avoid infine loop, package object require deep refactoring

        return $package;
    }

    /**
     * Return true if contains non default storage
     *
     * @return bool
     */
    public function contains_non_default_storage()
    {
        $defStorageId = StoragesUtil::getDefaultStorageId();
        foreach ($this->upload_infos as $upload_info) {
            if ($upload_info->getStorageId() === $defStorageId) {
                continue;
            }

            if (($storage = AbstractStorageEntity::getById($upload_info->getStorageId())) === false) {
                DUP_PRO_Log::traceError("Package refers to a storage provider that no longer exists - " . $upload_info->getStorageId());
                continue;
            }

            return true;
        }
        return false;
    }

    /**
     * Return true if package have storage type
     *
     * @param int $storage_type storage type
     *
     * @return bool
     */
    public function contains_storage_type($storage_type)
    {
        foreach ($this->get_storages() as $storage) {
            if ($storage->getSType() == $storage_type) {
                return true;
            }
        }
        return false;
    }

    /**
     * Check if package have local storage
     *
     * @return bool
     */
    public function haveLocalStorage()
    {
        foreach ($this->upload_infos as $upload_info) {
            if ($upload_info->isLocal()) {
                $filePath = SnapIO::trailingslashit($upload_info->getStorage()->getLocationString()) . $this->get_archive_filename();
                if (file_exists($filePath)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Check if package have remote storage
     *
     * @return bool
     */
    public function haveRemoteStorage()
    {
        foreach ($this->upload_infos as $upload_info) {
            if (
                $upload_info->isRemote() &&
                $upload_info->packageExists() &&
                $upload_info->has_completed(true) &&
                !$upload_info->isDownloadFromRemote()
            ) {
                return true;
            }
        }
        return false;
    }

    /**
     * Get remote Storages list
     *
     * @return AbstractStorageEntity[]
     */
    public function getRemoteStorages()
    {
        $packageUpdate = false;
        $storages      = [];
        $storagesIds   = [];
        foreach ($this->upload_infos as $upload_info) {
            if (
                !$upload_info->isRemote() ||
                !$upload_info->packageExists() ||
                !$upload_info->has_completed(true)
            ) {
                continue;
            }
            if (($storage = AbstractStorageEntity::getById($upload_info->getStorageId())) === false) {
                continue;
            }
            if (in_array($storage->getId(), $storagesIds)) {
                continue;
            }
            if (!$storage->hasPackage($this)) {
                $upload_info->setPackageExists(false);
                $packageUpdate = true;
                continue;
            }
            $storages[]    = $storage;
            $storagesIds[] = $storage->getId();
        }
        if ($packageUpdate) {
            $this->update();
        }
        return $storages;
    }

    /**
     * Return archive file name
     *
     * @return string
     */
    public function get_archive_filename()
    {
        $extension = strtolower($this->Archive->Format);
        return "{$this->getNameHash()}_archive.{$extension}";
    }

    /**
     * Get the name of the file that contains the database
     *
     * @return string
     */
    public function get_database_filename()
    {
        return $this->getNameHash() . '_database.sql';
    }

    /**
     * Get the name of the file that contains the list of files
     *
     * @return string
     */
    public function get_files_list_filename()
    {
        return $this->getNameHash() . DUP_PRO_Archive::FILES_LIST_FILE_NAME_SUFFIX;
    }

    /**
     * Get the name of the file that contains the list of directories
     *
     * @return string
     */
    public function get_dirs_list_filename()
    {
        return $this->getNameHash() . DUP_PRO_Archive::DIRS_LIST_FILE_NAME_SUFFIX;
    }

    /**
     * Get the next active Backup
     *
     * @return null|DUP_PRO_Package
     */
    public static function get_next_active_package()
    {
        $result = self::get_packages_by_status(array(
            'relation' => 'AND',
            array(
                'op'     => '>=',
                'status' => DUP_PRO_PackageStatus::PRE_PROCESS,
            ),
            array(
                'op'     => '<',
                'status' => DUP_PRO_PackageStatus::COMPLETE,
            )
        ), 1, 0, '`id` ASC');
        if (count($result) > 0) {
            return $result[0];
        } else {
            return null;
        }
    }

    /**
     * Get the number of Backups
     *
     * @return int
     */
    public static function getNumPackages()
    {
        $ids = DUP_PRO_Package::get_ids_by_status();
        return count($ids);
    }

    /**
     * Get the number of complete Backups
     *
     * @return int
     */
    public static function getNumCompletePackages()
    {
        $ids = DUP_PRO_Package::get_ids_by_status(
            [
                [
                    'op'     => '>=',
                    'status' => DUP_PRO_PackageStatus::COMPLETE,
                ],
            ]
        );
        return count($ids);
    }

    /**
     * Quickly determine without going through the overhead of creating Backup objects
     *
     * @return bool
     */
    public static function isPackageRunning()
    {
        $ids = DUP_PRO_Package::get_ids_by_status(
            [
                [
                    'op'     => '>=',
                    'status' => DUP_PRO_PackageStatus::PRE_PROCESS,
                ],
                [
                    'op'     => '<',
                    'status' => DUP_PRO_PackageStatus::COMPLETE,
                ],
            ]
        );
        return count($ids) > 0;
    }

    /**
     * Check is Brand is properly prepered
     *
     * @return array<string,mixed>
     */
    public static function is_active_brand_prepared()
    {
        $manual_template = DUP_PRO_Package_Template_Entity::get_manual_template();
        if ($manual_template != null) {
            $brand = BrandEntity::getByIdOrDefault((int) $manual_template->installer_opts_brand);
            if (is_array($brand->attachments)) {
                $attachments = count($brand->attachments);
                $exists      = array();
                if ($attachments > 0) {
                    $installer = DUPLICATOR____PATH . '/installer/dup-installer/assets/images/brand';
                    if (file_exists($installer) && is_dir($installer)) {
                        foreach ($brand->attachments as $attachment) {
                            if (file_exists("{$installer}{$attachment}")) {
                                $exists[] = "{$installer}{$attachment}";
                            }
                        }
                    }
                }
                //return ($attachments == count($exists));

                return array(
                    'LogoAttachmentExists' => ($attachments > 0),
                    'LogoCount'            => $attachments,
                    'LogoFinded'           => count($exists),
                    'LogoImageExists'      => ($attachments == count($exists)),
                    'LogoImages'           => $exists,
                    'Name'                 => $brand->name,
                    'Notes'                => $brand->notes,
                );
            }
        }

        return array(
            'LogoAttachmentExists' => false,
            'LogoCount'            => 0,
            'LogoFinded'           => 0,
            'LogoImageExists'      => true,
            'LogoImages'           => array(),
            'Name'                 => __('Default', 'duplicator-pro'),
            'Notes'                => __('The default content used when a brand is not defined.', 'duplicator-pro'),
        );
    }

    /**
     * Generates a scan report
     *
     * @return array<string,mixed> of scan results
     */
    public function create_scan_report()
    {
        DUP_PRO_Log::trace('Scanning');
        try {
            $global = DUP_PRO_Global_Entity::getInstance();
            if (is_numeric($this->ID) && $this->ID > 0) {
                $this->set_status(DUP_PRO_PackageStatus::SCANNING);
            }


            self::safe_tmp_cleanup();
            do_action('duplicator_before_scan_report', $this);
            $timerStart                = DUP_PRO_U::getMicrotime();
            $report                    = array();
            $this->ScanFile            = "{$this->getNameHash()}_scan.json";
            $report['RPT']['ScanTime'] = "0";
            $report['RPT']['ScanFile'] = $this->ScanFile;
            //FILES
            $this->Archive->buildScanStats();
            $scanPath  = DUPLICATOR_PRO_SSDIR_PATH_TMP . "/{$this->ScanFile}";
            $dirCount  = $this->Archive->DirCount;
            $fileCount = $this->Archive->FileCount;
            $fullCount = $dirCount + $fileCount;
            //Formated
            $report['ARC']['Size']      = DUP_PRO_U::byteSize($this->Archive->Size) or "unknown";
            $report['ARC']['DirCount']  = number_format($dirCount);
            $report['ARC']['FileCount'] = number_format($fileCount);
            $report['ARC']['FullCount'] = number_format($fullCount);
            //Int Type
            $report['ARC']['USize']      = $this->Archive->Size;
            $report['ARC']['UDirCount']  = $dirCount;
            $report['ARC']['UFileCount'] = $fileCount;
            $report['ARC']['UFullCount'] = $fullCount;

            $report['ARC']['UnreadableDirCount']  = $this->Archive->FilterInfo->Dirs->getUnreadableCount();
            $report['ARC']['UnreadableFileCount'] = $this->Archive->FilterInfo->Files->getUnreadableCount();
            $report['ARC']['FilterDirsAll']       = $this->Archive->FilterDirsAll;
            $report['ARC']['FilterFilesAll']      = $this->Archive->FilterFilesAll;
            $report['ARC']['FilterExtsAll']       = $this->Archive->FilterExtsAll;
            $report['ARC']['FilteredCoreDirs']    = $this->Archive->filterWpCoreFoldersList();
            $report['ARC']['FilteredSites']       = array();
            if (is_multisite() && License::can(License::CAPABILITY_MULTISITE_PLUS)) {
                foreach ($this->Multisite->FilterSites as $subsiteId) {
                    $report['ARC']['FilteredSites'][] = get_blog_details(array('blog_id' => $subsiteId));
                }
            }

            if ($global->archive_build_mode == DUP_PRO_Archive_Build_Mode::ZipArchive) {
                $site_warning_size = DUPLICATOR_PRO_SCAN_SITE_ZIP_ARCHIVE_WARNING_SIZE;
            } else {
                $site_warning_size = DUPLICATOR_PRO_SCAN_SITE_WARNING_SIZE;
            }
            // In Windows 32-bit, > 2GB number are negative
            $report['ARC']['Status']['Size']            = !($this->Archive->Size > $site_warning_size || $this->Archive->Size < 0);
            $report['ARC']['RecursiveLinks']            = $this->Archive->RecursiveLinks;
            $report['ARC']['UnreadableItems']           = array_merge($this->Archive->FilterInfo->Files->Unreadable, $this->Archive->FilterInfo->Dirs->Unreadable);
            $report['ARC']['Status']['Big']             = !(count($this->Archive->FilterInfo->Files->Size) > 0);
            $report['ARC']['Status']['AddonSites']      = !(count($this->Archive->FilterInfo->Dirs->AddonSites) > 0);
            $report['ARC']['Status']['UnreadableItems'] = empty($this->Archive->RecursiveLinks) && empty($report['ARC']['UnreadableItems']);
            $privileges_to_show_create_func             = true;
            $procedures                                 = $GLOBALS['wpdb']->get_col("SHOW PROCEDURE STATUS WHERE `Db` = '" . $GLOBALS['wpdb']->dbname . "'", 1);
            if (count($procedures)) {
                $create                         = $GLOBALS['wpdb']->get_row("SHOW CREATE PROCEDURE `" . $procedures[0] . "`", ARRAY_N);
                $privileges_to_show_create_func = isset($create[2]);
            }

            $functions = $GLOBALS['wpdb']->get_col("SHOW FUNCTION STATUS WHERE `Db` = '" . $GLOBALS['wpdb']->dbname . "'", 1);
            if (count($functions)) {
                $create                         = $GLOBALS['wpdb']->get_row("SHOW CREATE FUNCTION `" . $functions[0] . "`", ARRAY_N);
                $privileges_to_show_create_func = $privileges_to_show_create_func && isset($create[2]);
            }

            $privileges_to_show_create_func                  = apply_filters('duplicator_privileges_to_show_create_func', $privileges_to_show_create_func);
            $report['ARC']['Status']['showCreateFuncStatus'] = $privileges_to_show_create_func;
            $report['ARC']['Status']['showCreateFunc']       = $privileges_to_show_create_func;
            //SERVER
            $srv           = DUP_PRO_Server::getChecks($this);
            $report['SRV'] = $srv['SRV'];
            //$report['SRV']['Brand']=self::is_active_brand_prepared();
            //DATABASE
            $db                             = $this->Database->getScanData();
            $report['DB']['Status']         = $db['Status'];
            $report['DB']['SizeInBytes']    = $db['Size'];
            $report['DB']['Size']           = DUP_PRO_U::byteSize($db['Size']) or "unknown";
            $report['DB']['Rows']           = number_format($db['Rows']) or "unknown";
            $report['DB']['TableCount']     = $db['TableCount'] or "unknown";
            $report['DB']['TableList']      = $db['TableList'] or "unknown";
            $report['DB']['FilteredTables'] = ($this->Database->FilterOn ? explode(',', $this->Database->FilterTables) : array());
            $report['DB']['DBExcluded']     = BuildComponents::isDBExcluded($this->components);
            $report['RPT']['ScanCreated']   = @date("Y-m-d H:i:s");
            $report['RPT']['ScanTime']      = DUP_PRO_U::elapsedTime(DUP_PRO_U::getMicrotime(), $timerStart);
            $report['RPT']['ScanPath']      = $scanPath;
            $report['RPT']['ScanFile']      = $this->ScanFile;
            //Drag and drop/migration compatibility check
            $report['ARC']['Subsites']                         = DUP_PRO_MU::getSubsites($this->Multisite->FilterSites, $report['DB']['FilteredTables'], $this->Archive->FilterDirsAll);
            $report['ARC']['Status']['HasImportableSites']     = SnapUtil::inArrayExtended($report['ARC']['Subsites'], function ($subsite) {

                    return count($subsite->filteredTables) === 0;
            });
            $report['ARC']['Status']['HasNotImportableSites']  = SnapUtil::inArrayExtended($report['ARC']['Subsites'], function ($subsite) {

                    return count($subsite->filteredTables) > 0;
            });
            $report['ARC']['Status']['HasFilteredCoreFolders'] = $this->Archive->hasWpCoreFolderFiltered();
            ;
            $report['ARC']['Status']['HasFilteredSiteTables'] = $this->Database->info->tablesBaseCount !== $this->Database->info->tablesFinalCount;
            ;
            $report['ARC']['Status']['HasFilteredSites']       = !empty($report['ARC']['FilteredSites']);
            $report['ARC']['Status']['Network']                = !$report['ARC']['Status']['HasNotImportableSites'] &&
                !$report['ARC']['Status']['HasFilteredSites'];
            $report['ARC']['Status']['IsDBOnly']               = $this->isDBOnly();
            $report['ARC']['Status']['PackageIsNotImportable'] = !(
                (
                    !$report['ARC']['Status']['HasFilteredSiteTables'] ||
                    $report['ARC']['Status']['HasImportableSites']
                )
                &&
                (
                    !$report['ARC']['Status']['HasNotImportableSites'] ||
                    License::can(License::CAPABILITY_MULTISITE_PLUS)
                )
            );
            //Pass = 1;  Warn = 2; Fail = 3;
            $report['Status'] = 1;
            DUP_PRO_Log::trace("Open scan file: " . $report['RPT']['ScanPath']);
            $fp = fopen($report['RPT']['ScanPath'], 'w');
            if (!$fp) {
                throw new Exception('File open failed: "' . $report['RPT']['ScanPath'] . '"');
            }

            if (($json = JsonSerialize::serialize($report, JSON_PRETTY_PRINT | JsonSerialize::JSON_SKIP_CLASS_NAME)) === false) {
                throw new Exception('Problem encoding json');
            }

            SnapIO::fwrite($fp, $json);

            if (fclose($fp) == false) {
                DUP_PRO_Log::trace('File close failed: "' . $report['RPT']['ScanPath'] . '"');
            } else {
                DUP_PRO_Log::trace("CLose scan file: " . $report['RPT']['ScanPath']);
            }

            //Safe to clear at this point only JSON
            //report stores the full directory and file lists
            $this->Archive->Dirs  = [];
            $this->Archive->Files = [];
            /**
             * don't save filter info in report scan json.
             */
            $report['ARC']['FilterInfo'] = $this->Archive->FilterInfo;
            DUP_PRO_Log::trace("TOTAL SCAN TIME = " . DUP_PRO_U::elapsedTime(DUP_PRO_U::getMicrotime(), $timerStart));
        } catch (Exception $ex) {
            DUP_PRO_Log::trace("SCAN ERROR: " . $ex->getMessage());
            DUP_PRO_Log::trace("SCAN ERROR: " . $ex->getTraceAsString());
            DUP_PRO_Log::error("An error has occurred scanning the file system.", $ex->getMessage());
        }

        do_action('duplicator_after_scan_report', $this, $report);
        return $report;
    }

    /**
     * Adds file and dirs lists to scan report.
     *
     * @param string $json_path string The path to the json file
     *
     * @return mixed The scan report
     */
    public function getScanReportFromJson($json_path)
    {
        $base_path    = str_replace("_scan.json", "", $json_path);
        $fileListPath = $base_path . DUP_PRO_Archive::FILES_LIST_FILE_NAME_SUFFIX;
        $dirListPath  = $base_path . DUP_PRO_Archive::DIRS_LIST_FILE_NAME_SUFFIX;
        if (!file_exists($json_path)) {
            $message = sprintf(__("ERROR: Can't find Scanfile %s. Please ensure there no non-English characters in the Backup or schedule name.", 'duplicator-pro'), $json_path);
            throw new DUP_PRO_NoScanFileException($message);
        }

        if (!file_exists($fileListPath)) {
            $message = sprintf(__("ERROR: Can't find list of files %s. Please ensure there no non-English characters in the Backup or schedule name.", 'duplicator-pro'), $fileListPath);
            throw new DUP_PRO_NoFileListException($message);
        }

        if (!file_exists($dirListPath)) {
            $message = sprintf(__("ERROR: Can't find list of directories %s. Please ensure there no non-English characters in the Backup or schedule name.", 'duplicator-pro'), $dirListPath);
            throw new DUP_PRO_NoDirListException($message);
        }

        $json_contents = file_get_contents($json_path);

        $report = json_decode($json_contents);
        if ($report === null) {
            throw new DUP_PRO_JsonDecodeException("Couldn't decode scan file.");
        }

        $targetRootPath     = DUP_PRO_Archive::getTargetRootPath();
        $fileListObj        = new DUP_PRO_Archive_File_List($fileListPath);
        $report->ARC->Files = $fileListObj->getArrayPaths($targetRootPath);
        $dirListObj         = new DUP_PRO_Archive_File_List($dirListPath);
        $report->ARC->Dirs  = $dirListObj->getArrayPaths($targetRootPath);
        return $report;
    }

    /**
     * Return Backup flags
     *
     * @return string[] ENUM of DUP_PRO_Package::FLAG_* constants
     */
    protected function getFlags()
    {
        if ($this->flagUpdatedAfterLoad == false) {
            $this->updatePackageFlags();
            $this->flagUpdatedAfterLoad = true;
        }
        return $this->flags;
    }

    /**
     * Check if package have flag
     *
     * @param string $flag flag to check, ENUM of DUP_PRO_Package::FLAG_* constants
     *
     * @return bool
     */
    public function hasFlag($flag)
    {
        return in_array($flag, $this->getFlags());
    }

    /**
     * Retur the Backup flags
     *
     * @return void
     */
    protected function updatePackageFlags()
    {
        if (empty($this->flags)) {
            switch ($this->Type) {
                case DUP_PRO_PackageType::MANUAL:
                    $this->flags[] = self::FLAG_MANUAL;
                    break;
                case DUP_PRO_PackageType::SCHEDULED:
                    $this->flags[] = self::FLAG_SCHEDULE;
                    break;
                case DUP_PRO_PackageType::RUN_NOW:
                    $this->flags[] = self::FLAG_SCHEDULE_RUN_NOW;
                    break;
            }

            if ($this->Archive->Format == 'ZIP') {
                $this->flags[] = self::FLAG_ZIP_ARCHIVE;
            } else {
                $this->flags[] = self::FLAG_DUP_ARCHIVE;
            }

            if ($this->isDBOnly()) {
                $this->flags[] = self::FLAG_DB_ONLY;
            }

            if (BuildComponents::isMediaOnly($this->components)) {
                $this->flags[] = self::FLAG_MEDIA_ONLY;
            }
        }

        $this->flags = array_diff(
            $this->flags,
            [
                self::FLAG_HAVE_LOCAL,
                self::FLAG_HAVE_REMOTE,
                self::FLAG_DISASTER_SET,
                self::FLAG_DISASTER_AVAIABLE,
            ]
        );

        if ($this->Status == DUP_PRO_PackageStatus::COMPLETE) {
            // ONLY for complete Backups
            if ($this->haveLocalStorage()) {
                $this->flags[] = self::FLAG_HAVE_LOCAL;
            }

            if ($this->haveRemoteStorage()) {
                $this->flags[] = self::FLAG_HAVE_REMOTE;
            }

            if (RecoveryPackage::getRecoverPackageId() === $this->ID) {
                $this->flags[] = self::FLAG_DISASTER_SET;
            } else {
                $status = new RecoveryStatus($this);
                if ($status->isRecoveable()) {
                    $this->flags[] = self::FLAG_DISASTER_AVAIABLE;
                }
            }
        }
    }

    /**
     * Update the Backup migration flag
     *
     * @return void
     */
    public function updateMigrateAfterInstallFlag()
    {
        $this->updatePackageFlags();
        $this->flags = array_diff(
            $this->flags,
            [self::FLAG_CREATED_AFTER_RESTORE]
        );
        $data        = MigrationMng::getMigrationData();
        // check if package id is set for old versions before 4.5.14
        if ($data->restoreBackupMode && $data->packageId > 0) {
            $installTime = strtotime($data->installTime);
            $created     = strtotime($this->created);
            if (
                $this->ID > $data->packageId && // If Backup is create after installer Backup
                $created < $installTime // But berore the installer time
            ) {
                $this->flags[] = self::FLAG_CREATED_AFTER_RESTORE;
            }
        }
        $this->flags = array_values($this->flags);
    }

    /**
     * Clean object before save
     *
     * @return void
     */
    protected function cleanObjectBeforeSave()
    {
        if ($this->Status == DUP_PRO_PackageStatus::COMPLETE || $this->Status < 0) {
            // If complete clean build progress, to clean temp data
            $this->build_progress->reset();
            $this->db_build_progress->reset();
        }
        $this->Archive->FilterInfo->reset();
    }

    /**
     *
     * @param bool $die if true die on error otherwise return true on success and false on error
     *
     * @return bool
     */
    public function save($die = true)
    {
        if ($this->ID < 1) {
            /** @var wpdb $wpdb */
            global $wpdb;

            $this->version = DUPLICATOR_PRO_VERSION;
            // Created is set in the constructor
            $this->updated = gmdate("Y-m-d H:i:s");

            $results = $wpdb->insert(
                self::getTableName(),
                array(
                    'name'         => $this->Name,
                    'hash'         => $this->Hash,
                    'archive_name' => $this->get_archive_filename(),
                    'status'       => 0,
                    'progress'     => 0,
                    'flags'        => '',
                    'package'      => '',
                    'owner'        => '',
                    'version'      => $this->version,
                    'created'      => $this->created,
                    'updated_at'   => $this->updated,
                ),
                array(
                    '%s',
                    '%s',
                    '%s',
                    '%d',
                    '%f',
                    '%s',
                    '%s',
                    '%s',
                    '%s',
                    '%s',
                    '%s',
                )
            );
            if ($results === false) {
                DUP_PRO_Log::trace("Problem inserting Backup: {$wpdb->last_error}");
                if ($die) {
                    DUP_PRO_Log::error("Duplicator is unable to insert a Backup record into the database table.", "'{$wpdb->last_error}'");
                }
                return false;
            }
            $this->ID = $wpdb->insert_id;
        }
        // I run the update in each case even after the insert because the saved object does not have the id
        return $this->update($die);
    }

    /**
     * Starts the Backup build process
     *
     * @param bool $closeOnEnd if true the function will close the log and die on error
     *
     * @return DUP_PRO_Package
     */
    public function run_build($closeOnEnd = true)
    {
        try {
            DUP_PRO_Log::trace('Main build step');
            global $wp_version;
            // START LOGGING
            DUP_PRO_Log::open($this->getNameHash());
            $global = DUP_PRO_Global_Entity::getInstance();
            $this->build_progress->startTimer();
            if ($this->build_progress->initialized == false) {
                DUP_PRO_Log::trace("**** START OF BUILD: " . $this->getNameHash());

                do_action('duplicator_pro_build_before_start', $this);
                $this->Installer->setFileName($this->getNameHash());
                $this->timer_start      = DUP_PRO_U::getMicrotime();
                $extension              = strtolower($this->Archive->Format);
                $this->Archive->File    = "{$this->getNameHash()}_archive.{$extension}";
                $this->Database->File   = "{$this->getNameHash()}_database.sql";
                $this->Database->DBMode = DUP_PRO_DB::getBuildMode();
                $this->ziparchive_mode  = $global->ziparchive_mode;
                if (!License::can(License::CAPABILITY_MULTISITE_PLUS)) {
                    $this->Multisite->FilterSites = array();
                }
                $php_max_time       = @ini_get("max_execution_time");
                $php_max_memory     = @ini_get('memory_limit');
                $php_max_time       = ($php_max_time == 0) ? "(0) no time limit imposed" : "[{$php_max_time}] not allowed";
                $php_max_memory     = ($php_max_memory === false) ? "Unable to set php memory_limit" : WP_MAX_MEMORY_LIMIT . " ({$php_max_memory} default)";
                $architecture       = SnapUtil::getArchitectureString();
                $clientkickoffstate = $global->clientside_kickoff ? 'on' : 'off';
                $archive_engine     = $global->get_archive_engine();
                $serverSoftware     = SnapUtil::sanitizeTextInput(INPUT_SERVER, 'SERVER_SOFTWARE', 'unknown');
                $info               = "********************************************************************************\n";
                $info              .= "********************************************************************************\n";
                $info              .= "DUPLICATOR PRO PACKAGE-LOG: " . @date("Y-m-d H:i:s") . "\n";
                $info              .= "NOTICE: Do NOT post to public sites or forums \n";
                $info              .= "PACKAGE CREATION START\n";
                $info              .= "********************************************************************************\n";
                $info              .= "********************************************************************************\n";
                $info              .= "VERSION:\t" . DUPLICATOR_PRO_VERSION . "\n";
                $info              .= "WORDPRESS:\t{$wp_version}\n";
                $info              .= "PHP INFO:\t" . phpversion() . ' | ' . 'SAPI: ' . php_sapi_name() . "\n";
                $info              .= "SERVER:\t\t{$serverSoftware} \n";
                $info              .= "ARCHITECTURE:\t{$architecture} \n";
                $info              .= "CLIENT KICKOFF: {$clientkickoffstate} \n";
                $info              .= "PHP TIME LIMIT: {$php_max_time} \n";
                $info              .= "PHP MAX MEMORY: {$php_max_memory} \n";
                $info              .= "RUN TYPE:\t" . $this->get_type_string() . "\n";
                $info              .= "MEMORY STACK:\t" . DUP_PRO_Server::getPHPMemory() . "\n";
                $info              .= "ARCHIVE ENGINE: {$archive_engine}\n";
                $info              .= "PACKAGE COMPONENTS:\n\t" . BuildComponents::displayComponentsList($this->components, ",\n\t");
                DUP_PRO_Log::infoTrace($info);
                // CREATE DB RECORD
                $this->build_progress->setBuildMode();

                if ($this->Archive->isArchiveEncrypt() && !SettingsUtils::isArchiveEncryptionAvailable()) {
                    throw new Exception("Archive encryption isn't available.");
                }

                $this->ID                          = $this->find_hash_key($this->Hash);
                $this->build_progress->initialized = true;
                if ($this->ID > 0) {
                    DUP_PRO_Log::trace("ID non zero so setting to start");
                    $this->set_status(DUP_PRO_PackageStatus::START);
                } else {
                    $this->Status = DUP_PRO_PackageStatus::START;
                    $this->save();
                }
                do_action('duplicator_pro_build_start', $this);
            }

            // At one point having this as an else as not part of the main logic prevented failure emails from getting sent.
            // Note2: Think that by putting has_completed() at top of check will prevent archive from continuing to build after a failure has hit.
            if ($this->build_progress->hasCompleted()) {
                $schedule = DUP_PRO_Schedule_Entity::getById($this->schedule_id);
                DUP_PRO_Log::info("\n********************************************************************************");
                DUP_PRO_Log::info("STORAGE:");
                DUP_PRO_Log::info("********************************************************************************");
                foreach ($this->upload_infos as $upload_info) {
                    if (($storage = AbstractStorageEntity::getById($upload_info->getStorageId())) === false) {
                        continue;
                    }
                    // Protection against deleted storage
                    $storage_type_string = strtoupper($storage->getStypeName());
                    $storage_path        = $storage->getLocationString();
                    DUP_PRO_Log::info($storage_type_string . ": " . $storage->getName() . ', ' . $storage_path);
                }

                if (!$this->build_progress->failed) {
                    // Only makees sense to perform build integrity check on completed archives
                    $this->build_integrity_check();
                }

                $timerEnd      = DUP_PRO_U::getMicrotime();
                $timerSum      = DUP_PRO_U::elapsedTime($timerEnd, $this->timer_start);
                $this->Runtime = $timerSum;
                // FINAL REPORT
                $info  = "\n********************************************************************************\n";
                $info .= "RECORD ID:[{$this->ID}]\n";
                $info .= "TOTAL PROCESS RUNTIME: {$timerSum}\n";
                $info .= "PEAK PHP MEMORY USED: " . DUP_PRO_Server::getPHPMemory(true) . "\n";
                $info .= "DONE PROCESSING => {$this->Name} " . @date("Y-m-d H:i:s") . "\n";
                DUP_PRO_Log::info($info);
                DUP_PRO_Log::trace("Done Backup building");

                if ($this->build_progress->failed) {
                    throw new Exception("Backup creation failed.");
                } else {
                    if ($schedule != null) {
                        //    $schedule->times_run++;
                        //                  $schedule->last_run_time     = time();
                        //                   $schedule->last_run_status   = DUP_PRO_Schedule_Entity::RUN_STATUS_SUCCESS;
                        //$schedule->save();
                        // don't send build email for success - rely on storage phase to handle that
                    }

                    //File Cleanup
                    $this->build_cleanup();
                    do_action('duplicator_pro_build_completed', $this);
                }
            } elseif (!$this->build_progress->database_script_built) {
                //START BUILD
                //PHPs serialze method will return the object, but the ID above is not passed
                //for one reason or another so passing the object back in seems to do the trick

                try {
                    if ((!$global->package_mysqldump) && ($global->package_phpdump_mode == DUP_PRO_DB::PHPDUMP_MODE_MULTI)) {
                        $this->Database->buildInChunks();
                    } else {
                        $this->Database->build();
                        $this->build_progress->database_script_built = true;
                        $this->update();
                    }
                } catch (Exception $e) {
                    do_action('duplicator_pro_build_database_fail', $this);
                    DUP_PRO_Log::infoTrace("Runtime error in database dump Message: " . $e->getMessage());
                    throw $e;
                }

                DUP_PRO_Log::trace("Done building database");
                if ($this->build_progress->database_script_built) {
                    DUP_PRO_Log::trace("Set db built for Backup $this->ID");
                }
            } elseif (!$this->build_progress->archive_built) {
                $this->Archive->buildFile($this, $this->build_progress);
                $this->update();
            } elseif (!$this->build_progress->installer_built) {
                // Note: Duparchive builds installer within the main build flow not here
                $this->Installer->build($this->build_progress);
                $this->update();
                if ($this->build_progress->failed) {
                    throw new Exception('ERROR: Problem adding installer to archive.');
                }
            }

            if ($this->build_progress->failed) {
                throw new Exception('Build progress fail');
            }
        } catch (Exception $e) {
            $message  = "Backup creation failed.\n"
                . " EXCEPTION message: " . $e->getMessage() . "\n";
            $message .= $e->getFile() . ' LINE: ' . $e->getLine() . "\n";
            $message .= $e->getTraceAsString();
            $this->buildFail($message, $closeOnEnd);
        }

        if ($closeOnEnd) {
            DUP_PRO_Log::close();
        }
        return $this;
    }

    /**
     * Backup build fail, this method die the process and set the Backup status to error
     *
     * @param string $message Error message
     * @param bool   $die     If true, the process will die
     *
     * @return void
     */
    public function buildFail($message, $die = true)
    {
        $this->build_progress->failed = true;
        $this->set_status(DUP_PRO_PackageStatus::ERROR);
        $this->post_scheduled_build_processing(0, false);
        do_action('duplicator_pro_build_fail', $this);
        DUP_PRO_Log::error($message, '', $die);
    }

    /**
     * Integriry check for the build process
     *
     * @return void
     */
    protected function build_integrity_check()
    {
        //INTEGRITY CHECKS
        //We should not rely on data set in the serlized object, we need to manually check each value
        //indepentantly to have a true integrity check.
        DUP_PRO_Log::info("\n********************************************************************************");
        DUP_PRO_Log::info("INTEGRITY CHECKS:");
        DUP_PRO_Log::info("********************************************************************************");
        //------------------------
        //SQL CHECK:  File should be at minimum 5K.  A base WP install with only Create tables is about 9K
        $sql_temp_path = SnapIO::safePath(DUPLICATOR_PRO_SSDIR_PATH_TMP . '/' . $this->Database->File);
        $sql_temp_size = @filesize($sql_temp_path);
        $sql_easy_size = DUP_PRO_U::byteSize($sql_temp_size);
        $sql_done_txt  = DUP_PRO_U::tailFile($sql_temp_path, 3);

        // Note: Had to add extra size check of 800 since observed bad sql when filter was on
        if (
            in_array(BuildComponents::COMP_DB, $this->components) &&
            (!strstr($sql_done_txt, DUPLICATOR_PRO_DB_EOF_MARKER) ||
            (!$this->Database->FilterOn && $sql_temp_size < DUPLICATOR_PRO_MIN_SIZE_DBFILE_WITHOUT_FILTERS) ||
            ($this->Database->FilterOn && $this->Database->info->tablesFinalCount > 0 && $sql_temp_size < DUPLICATOR_PRO_MIN_SIZE_DBFILE_WITH_FILTERS))
        ) {
            $this->build_progress->failed = true;
            $error_text                   = "ERROR: SQL file not complete.  The file looks too small ($sql_temp_size bytes) or the end of file marker was not found.";
            $system_global                = SystemGlobalEntity::getInstance();
            if ($this->Database->DBMode == 'MYSQLDUMP') {
                $fix_text = __('Click button to switch database engine to PHP', 'duplicator-pro');
                $system_global->addQuickFix(
                    $error_text,
                    $fix_text,
                    array(
                        'global' => array(
                            'package_mysqldump'          => 0,
                            'package_mysqldump_qrylimit' => 32768,
                        ),
                    )
                );
            } else {
                $fix_text = __('Click button to switch database engine to MySQLDump', 'duplicator-pro');
                $system_global->addQuickFix($error_text, $fix_text, array(
                    'global' => array(
                        'package_mysqldump'      => 1,
                        'package_mysqldump_path' => '',
                    ),
                ));
            }
            DUP_PRO_Log::error("$error_text  **RECOMMENDATION: $fix_text", '', false);
            throw new Exception($error_text);
        }

        DUP_PRO_Log::info("SQL FILE: {$sql_easy_size}");
        //------------------------
        //INSTALLER CHECK:
        $exe_temp_path = SnapIO::safePath(DUPLICATOR_PRO_SSDIR_PATH_TMP . '/' . $this->Installer->getInstallerLocalName());
        $exe_temp_size = @filesize($exe_temp_path);
        $exe_easy_size = DUP_PRO_U::byteSize($exe_temp_size);
        $exe_done_txt  = DUP_PRO_U::tailFile($exe_temp_path, 10);
        if (!strstr($exe_done_txt, 'DUPLICATOR_PRO_INSTALLER_EOF') && !$this->build_progress->failed) {
            throw new Exception("ERROR: Installer file not complete.  The end of file marker was not found.  Please try to re-create the Backup.");
        }
        DUP_PRO_Log::info("INSTALLER FILE: {$exe_easy_size}");
        //------------------------
        //ARCHIVE CHECK:
        // Only performs check if we were able to obtain the count
        DUP_PRO_Log::trace("Archive file count is " . $this->Archive->file_count);
        if ($this->Archive->file_count != -1) {
            $zip_easy_size = DUP_PRO_U::byteSize($this->Archive->Size);
            if (!($this->Archive->Size)) {
                throw new Exception("ERROR: The archive file contains no size. Archive Size: {$zip_easy_size}");
            }

            $scan_filepath = DUPLICATOR_PRO_SSDIR_PATH_TMP . "/{$this->getNameHash()}_scan.json";
            $json          = '';
            DUP_PRO_Log::trace("***********Does $scan_filepath exist?");
            if (file_exists($scan_filepath)) {
                $json = file_get_contents($scan_filepath);
            } else {
                $error_message = sprintf(__("Can't find Scanfile %s. Please ensure there no non-English characters in the Backup or schedule name.", 'duplicator-pro'), $scan_filepath);
                throw new Exception($error_message);
            }

            $scanReport         = json_decode($json);
            $expected_filecount = (int) ($scanReport->ARC->UDirCount + $scanReport->ARC->UFileCount);
            DUP_PRO_Log::info("ARCHIVE FILE: {$zip_easy_size} ");
            DUP_PRO_Log::info(sprintf(__('EXPECTED FILE/DIRECTORY COUNT: %1$s', 'duplicator-pro'), number_format($expected_filecount)));
            DUP_PRO_Log::info(sprintf(__('ACTUAL FILE/DIRECTORY COUNT: %1$s', 'duplicator-pro'), number_format($this->Archive->file_count)));
            $this->ExeSize = $exe_easy_size;
            $this->ZipSize = $zip_easy_size;
            /* ------- ZIP Filecount Check -------- */
            // Any zip of over 500 files should be within 2% - this is probably too loose but it will catch gross errors
            DUP_PRO_Log::trace("Expected filecount = $expected_filecount and archive filecount=" . $this->Archive->file_count);
            if ($expected_filecount > 500) {
                $straight_ratio = (float) $expected_filecount / (float) $this->Archive->file_count;
                // RSR NEW
                $warning_count = $scanReport->ARC->UnreadableFileCount + $scanReport->ARC->UnreadableDirCount;
                DUP_PRO_Log::trace("Unread counts) unreadfile:{$scanReport->ARC->UnreadableFileCount} unreaddir:{$scanReport->ARC->UnreadableDirCount}");
                $warning_ratio = ((float) ($expected_filecount + $warning_count)) / (float) $this->Archive->file_count;
                DUP_PRO_Log::trace("Straight ratio is $straight_ratio and warning ratio is $warning_ratio. # Expected=$expected_filecount # Warning=$warning_count and #Archive File {$this->Archive->file_count}");
                // Allow the real file count to exceed the expected by 10% but only allow 1% the other way
                if (($straight_ratio < 0.90) || ($straight_ratio > 1.01)) {
                    // Has to exceed both the straight as well as the warning ratios
                    if (($warning_ratio < 0.90) || ($warning_ratio > 1.01)) {
                        $zip_file_count = $this->Archive->file_count;
                        $error_message  = sprintf('ERROR: File count in archive vs expected suggests a bad archive (%1$d vs %2$d).', $zip_file_count, $expected_filecount);
                        if ($this->build_progress->current_build_mode == DUP_PRO_Archive_Build_Mode::Shell_Exec) {
                            // $fix_text = "Go to: Settings > Packages Tab > Archive Engine to ZipArchive.";
                            $fix_text      = __("Click on button to set archive engine to DupArchive.", 'duplicator-pro');
                            $system_global = SystemGlobalEntity::getInstance();
                            $system_global->addQuickFix(
                                $error_message,
                                $fix_text,
                                array(
                                    'global' => array('archive_build_mode' => 3),
                                )
                            );
                            $error_message .= ' **' . sprintf(__("RECOMMENDATION: %s", 'duplicator-pro'), $fix_text);
                        }

                        DUP_PRO_Log::trace($error_message);
                        throw new Exception($error_message);
                    }
                }
            }
        }
    }

    /**
     * Post scheduled build failure
     *
     * @param array<string, mixed> $tests Tests results
     *
     * @return void
     */
    public function post_scheduled_build_failure($tests = null)
    {
        $this->post_scheduled_build_processing(0, false, $tests);
    }

    /**
     * Post scheduled storage failure
     *
     * @return void
     */
    public function post_scheduled_storage_failure()
    {
        $this->post_scheduled_build_processing(1, false);
    }

    /**
     * Processes the Backup after the build
     *
     * @param int                  $stage   0 for failure at build, 1 for failure during storage phase
     * @param bool                 $success true if build was successful
     * @param array<string, mixed> $tests   Tests results
     *
     * @return void
     */
    private function post_scheduled_build_processing($stage, $success, $tests = [])
    {
        try {
            if ($this->schedule_id == -1) {
                return;
            }
            if (($schedule = DUP_PRO_Schedule_Entity::getById($this->schedule_id)) === false) {
                throw new Exception("Couldn't get schedule by ID {$this->schedule_id} to start post scheduled build processing.");
            }

            $system_global                  = SystemGlobalEntity::getInstance();
            $system_global->schedule_failed = !$success;
            $system_global->save();
            $schedule->times_run++;
            $schedule->last_run_time   = time();
            $schedule->last_run_status = ($success ? DUP_PRO_Schedule_Entity::RUN_STATUS_SUCCESS : DUP_PRO_Schedule_Entity::RUN_STATUS_FAILURE);
            $schedule->save();

            if (!empty($tests) && $tests['RES']['INSTALL'] == 'Fail') {
                $system_global->addQuickFix(
                    __('Backup was cancelled because installer files from a previous migration were found.', 'duplicator-pro'),
                    __(
                        'Click the button to remove all installer files.',
                        'duplicator-pro'
                    ),
                    array(
                        'special' => array('remove_installer_files' => 1),
                    )
                );
            }

            $this->sendBuildEmail($stage, $success);
        } catch (Exception $ex) {
            DUP_PRO_Log::trace($ex->getMessage());
        }
    }

    /**
     * Processes the Backup after the build
     *
     * @param int  $stage   0 for failure at build, 1 for failure during storage phase
     * @param bool $success true if build was successful
     *
     * @return void
     */
    protected function sendBuildEmail($stage, $success)
    {
        try {
            if ($this->buildEmailSent) {
                return;
            }

            $global = DUP_PRO_Global_Entity::getInstance();
            switch ($global->send_email_on_build_mode) {
                case DUP_PRO_Email_Build_Mode::No_Emails:
                    return;
                case DUP_PRO_Email_Build_Mode::Email_On_All_Builds:
                    break;
                case DUP_PRO_Email_Build_Mode::Email_On_Failure:
                    if ($success) {
                        return;
                    }
                    break;
                default:
                    return;
            }

            $to = !empty($global->notification_email_address) ? $global->notification_email_address : get_option('admin_email');
            if (empty($to) !== false) {
                throw new Exception("Would normally send a build notification but admin email is empty.");
            }

            if (($schedule = DUP_PRO_Schedule_Entity::getById($this->schedule_id)) === false) {
                throw new Exception("Couldn't get schedule by ID {$this->schedule_id} to start post scheduled build processing.");
            }

            DUP_PRO_Log::trace("Attempting to send build notification to $to");
            $data = array(
                'success'      => $success,
                'messageTitle' => __('BACKUP SUCCEEDED', 'duplicator-pro'),
                'packageID'    => $this->ID,
                'packageName'  => $this->Name,
                'scheduleName' => $schedule->name,
                'storageNames' => array_map(function (AbstractStorageEntity $s) {
                    return $s->getName();
                }, $this->get_storages()),
                'packagesLink' => ControllersManager::getMenuLink(ControllersManager::PACKAGES_SUBMENU_SLUG, null, null, array(), false),
                'logExists'    => file_exists($this->get_safe_log_filepath()),
            );
            if ($success) {
                $data    = array_merge($data, array(
                    'fileCount'   => $this->Archive->FileCount,
                    'packageSize' => DUP_PRO_U::byteSize($this->Archive->Size),
                    'tableCount'  => $this->Database->info->tablesFinalCount,
                    'sqlSize'     => DUP_PRO_U::byteSize($this->Database->Size),
                ));
                $subject = sprintf(__('Backup of %1$s (%2$s) Succeeded', 'duplicator-pro'), home_url(), $schedule->name);
            } else {
                $data['messageTitle']  = __('BACKUP FAILED', 'duplicator-pro') . ' ';
                $data['messageTitle'] .= $stage !== 0
                ? __('DURING BUILD PHASE', 'duplicator-pro')
                : __('DURING STORAGE PHASE. CHECK SITE FOR DETAILS.', 'duplicator-pro');
                $subject               = sprintf(__('Backup of %1$s (%2$s) Failed', 'duplicator-pro'), home_url(), $schedule->name);
            }

            $message     = \Duplicator\Core\Views\TplMng::getInstance()->render("mail/scheduled-build", $data, false);
            $attachments = $data['logExists'] ? $this->get_safe_log_filepath() : '';

            if (!wp_mail($to, $subject, $message, array('Content-Type: text/html; charset=UTF-8'), $attachments)) {
                throw new Exception("Problem sending build notification to {$to} regarding Backup {$this->ID}");
            }

            $this->buildEmailSent = true;
            $this->save();
            DUP_PRO_Log::trace('wp_mail reporting send success');
        } catch (Exception $ex) {
            DUP_PRO_Log::traceException($ex, "Problem sending build notification email");
        } catch (Error $ex) {
            DUP_PRO_Log::traceException($ex, "Problem sending build notification email");
        }
    }

    /**
     * Get type string
     *
     * @return string
     */
    public function get_type_string()
    {
        switch ($this->Type) {
            case DUP_PRO_PackageType::MANUAL:
                if ($this->template_id != -1) {
                    $template = DUP_PRO_Package_Template_Entity::getById($this->template_id);
                    if (isset($template->is_manual) && !$template->is_manual) {
                        return __('Template', 'duplicator-pro') . ' ' . $template->name;
                    }
                }

                return __('Manual', 'duplicator-pro');
            case DUP_PRO_PackageType::SCHEDULED:
                return __('Schedule', 'duplicator-pro');
            case DUP_PRO_PackageType::RUN_NOW:
                return __('Schedule (Run Now)', 'duplicator-pro');
            default:
                return __('Unknown', 'duplicator-pro');
        }
    }

    /**
     * Get active storage, false if none
     *
     * @return false|AbstractStorageEntity
     */
    public function get_active_storage()
    {
        if ($this->active_storage_id != -1) {
            if (($storage = AbstractStorageEntity::getById($this->active_storage_id)) === false) {
                DUP_PRO_Log::traceError("Active storage for Backup {$this->ID} is {$this->active_storage_id} but it's coming back false so resetting.");
                $this->active_storage_id = -1;
                $this->save();
            }
            return $storage;
        } else {
            return false;
        }
    }

    /**
     * Returns true if a download is in progress
     *
     * @return bool
     */
    public function isDownloadInProgress()
    {
        foreach ($this->upload_infos as $upload_info) {
            if ($upload_info->isDownloadFromRemote() && $upload_info->has_completed() === false) {
                return true;
            }
        }

        return false;
    }

    /**
     * Return Backup life
     *
     * @param string $type can be hours,human,timestamp
     *
     * @return int|string Backup life in hours, timestamp or human readable format
     */
    public function getPackageLife($type = 'timestamp')
    {
        $created = strtotime($this->created);
        $current = strtotime(gmdate("Y-m-d H:i:s"));
        $delta   = $current - $created;

        switch ($type) {
            case 'hours':
                return max(0, floor($delta / 60 / 60));
            case 'human':
                return human_time_diff($created, $current);
            case 'timestamp':
            default:
                return $delta;
        }
    }

    /**
     * Get Backup storages
     *
     * @return AbstractStorageEntity[]
     */
    public function get_storages()
    {
        $storages = [];
        foreach ($this->upload_infos as $upload_info) {
            $storage = $upload_info->getStorage();
            if ($storage instanceof UnknownStorage) {
                continue;
            }
            $storages[] = $storage;
        }
        return $storages;
    }

    /**
     * Used when we already have a Backup object that we need to make active
     *
     * @return void
     */
    public function set_temporary_package()
    {
        self::save_temporary_package($this);
    }

    /**
     *  Saves the active options associated with the active(latest) package.
     *
     *  @param mixed $post The _POST server object
     *
     *  @return void
     *
     *  @see DUP_PRO_Package::GetActive
     **/
    public static function set_manual_template_from_post($post = null)
    {
        if (isset($post)) {
            $post                  = stripslashes_deep($post);
            $mtemplate             = DUP_PRO_Package_Template_Entity::get_manual_template();
            $mtemplate->components = BuildComponents::getFromInput($post);

            if (isset($post['package_name_format'])) {
                $mtemplate->package_name_format = SnapUtil::sanitize($post['package_name_format']);
            }

            if (isset($post['filter-paths'])) {
                $post_filter_paths               = SnapUtil::sanitizeNSChars($post['filter-paths']);
                $mtemplate->archive_filter_dirs  = DUP_PRO_Archive::parseDirectoryFilter($post_filter_paths);
                $mtemplate->archive_filter_files = DUP_PRO_Archive::parseFileFilter($post_filter_paths);
            } else {
                $mtemplate->archive_filter_dirs  = '';
                $mtemplate->archive_filter_files = '';
            }

            $filter_sites = !empty($post['mu-exclude']) ? $post['mu-exclude'] : '';
            if (isset($post['filter-exts'])) {
                $post_filter_exts               = sanitize_text_field($post['filter-exts']);
                $mtemplate->archive_filter_exts = DUP_PRO_Archive::parseExtensionFilter($post_filter_exts);
            } else {
                $mtemplate->archive_filter_exts = '';
            }

            $tablelist  = isset($post['dbtables-list']) ? SnapUtil::sanitizeNSCharsNewlineTrim($post['dbtables-list']) : '';
            $compatlist = isset($post['dbcompat']) ? implode(',', $post['dbcompat']) : '';
            // PACKAGE
            // Replaces any \n \r or \n\r from the Backup notes
            if (isset($post['package-notes'])) {
                $mtemplate->notes = SnapUtil::sanitizeNSCharsNewlineTrim($post['package-notes']);
            } else {
                $mtemplate->notes = '';
            }

            //MULTISITE
            $mtemplate->filter_sites = $filter_sites;
            //ARCHIVE
            $mtemplate->archive_filter_on    = isset($post['filter-on']) ? 1 : 0;
            $mtemplate->archive_filter_names = isset($post['filter-names']) ? true : false;
            //INSTALLER
            $secureOn = (isset($post['secure-on']) ? (int) $post['secure-on'] : ArchiveDescriptor::SECURE_MODE_NONE);
            switch ($secureOn) {
                case ArchiveDescriptor::SECURE_MODE_NONE:
                case ArchiveDescriptor::SECURE_MODE_INST_PWD:
                case ArchiveDescriptor::SECURE_MODE_ARC_ENCRYPT:
                    $mtemplate->installer_opts_secure_on = $secureOn;
                    break;
                default:
                    throw new Exception(__('Select valid secure mode', 'duplicator-pro'));
            }

            $mtemplate->installerPassowrd = isset($post['secure-pass']) ? SnapUtil::sanitizeNSCharsNewlineTrim($post['secure-pass']) : '';
            //BRAND
            $mtemplate->installer_opts_brand     = ((isset($post['installer_opts_brand']) && (int) $post['installer_opts_brand'] > 0) ? (int) $post['installer_opts_brand'] : -1);
            $mtemplate->installer_opts_skip_scan = (isset($post['skipscan']) && 1 == $post['skipscan']) ? 1 : 0;
            //cPanel
            $mtemplate->installer_opts_cpnl_enable    = (isset($post['installer_opts_cpnl_enable']) && 1 == $post['installer_opts_cpnl_enable']) ? 1 : 0;
            $mtemplate->installer_opts_cpnl_host      = isset($post['installer_opts_cpnl_host']) ? sanitize_text_field($post['installer_opts_cpnl_host']) : '';
            $mtemplate->installer_opts_cpnl_user      = isset($post['installer_opts_cpnl_user']) ? sanitize_text_field($post['installer_opts_cpnl_user']) : '';
            $mtemplate->installer_opts_cpnl_db_action = isset($post['installer_opts_cpnl_db_action']) ? sanitize_text_field($post['installer_opts_cpnl_db_action']) : '';
            $mtemplate->installer_opts_cpnl_db_host   = isset($post['installer_opts_cpnl_db_host']) ? sanitize_text_field($post['installer_opts_cpnl_db_host']) : '';
            $mtemplate->installer_opts_cpnl_db_name   = isset($post['installer_opts_cpnl_db_name']) ? sanitize_text_field($post['installer_opts_cpnl_db_name']) : '';
            $mtemplate->installer_opts_cpnl_db_user   = isset($post['installer_opts_cpnl_db_user']) ? sanitize_text_field($post['installer_opts_cpnl_db_user']) : '';
            //Basic
            $mtemplate->installer_opts_db_host = isset($post['installer_opts_db_host']) ? sanitize_text_field($post['installer_opts_db_host']) : '';
            $mtemplate->installer_opts_db_name = isset($post['installer_opts_db_name']) ? sanitize_text_field($post['installer_opts_db_name']) : '';
            $mtemplate->installer_opts_db_user = isset($post['installer_opts_db_user']) ? sanitize_text_field($post['installer_opts_db_user']) : '';
            // DATABASE
            $mtemplate->database_filter_on      = isset($post['dbfilter-on']) ? 1 : 0;
            $mtemplate->databasePrefixFilter    = isset($post['db-prefix-filter']) ? true : false;
            $mtemplate->databasePrefixSubFilter = isset($post['db-prefix-sub-filter']) ? true : false;
            $mtemplate->database_filter_tables  = sanitize_text_field($tablelist);

            $mtemplate->database_compatibility_modes = $compatlist;
            $mtemplate->save();
        }
    }

    /**
     *
     * @param int   $template_id Template ID
     * @param int[] $storage_ids Array of storage IDs
     *
     * @return self|null Retunr Backup or null if manual template don't exists
     */
    public static function set_temporary_package_from_template_and_storages($template_id, $storage_ids)
    {
        // Use the manual template for the data while the $template_id is used just to record where it originally came from
        $manual_template = DUP_PRO_Package_Template_Entity::get_manual_template();
        if ($manual_template != null) {
            $package = new self(
                DUP_PRO_PackageType::MANUAL,
                $storage_ids,
                $manual_template
            );

            //PACKAGE
            $package->notes = $manual_template->notes;
            // this is the template set in manual mode
            $package->template_id = $template_id;

            $system_global = SystemGlobalEntity::getInstance();
            $system_global->clearFixes();
            $system_global->package_check_ts = 0;
            $system_global->save();
            self::save_temporary_package($package);
            return $package;
        } else {
            DUP_PRO_Log::trace("Manual template doesn't exist!");
            return null;
        }
    }

    /**
     * Save temporary Backup
     *
     * @return void
     */
    protected function saveTemporary()
    {
        $this->cleanObjectBeforeSave();
        $this->version = DUPLICATOR_PRO_VERSION;
        $this->updated = gmdate("Y-m-d H:i:s");
        update_option(
            self::OPT_ACTIVE,
            JsonSerialize::serialize(
                $this,
                JSON_PRETTY_PRINT | JsonSerialize::JSON_SKIP_CLASS_NAME
            )
        );
    }

    /**
     * save Backup on OPT_ACTIVE after clean
     *
     * @param self $package Backup to save
     * @param bool $clone   if true clone obkect before clean to prevent original obj modificantion
     *
     * @return void
     */
    protected static function save_temporary_package($package, $clone = true)
    {
        if ($clone) {
            $cleanPack = clone $package;
        } else {
            $cleanPack = $package;
        }
        $cleanPack->saveTemporary();
    }

    /**
     * Delete temporary Backup
     *
     * @return void
     */
    public static function delete_temporary_package()
    {
        delete_option(self::OPT_ACTIVE);
    }

    /**
     * Save any property of this class through reflection
     *
     * @param string $property A valid public property in this class
     * @param mixed  $value    The value for the new dynamic property
     *
     * @return void
     */
    public static function set_temporary_package_member($property, $value)
    {

        $package = self::get_temporary_package();
        if ($property == 'Status') {
            do_action('duplicator_pro_package_before_set_status', $package, $value);
        }
        $reflectionClass = new ReflectionClass($package);
        $reflectionClass->getProperty($property)->setValue($package, $value);
        self::save_temporary_package($package, false);
        if ($property == 'Status') {
            do_action('duplicator_pro_package_after_set_status', $package, $value);
        }
    }

    /**
     *  Sets the status to log the state of the build
     *
     *  @param int $status The status level for where the Backup is
     *
     *  @return void
     */
    public function set_status($status)
    {
        if (!is_numeric($status) || $status < -6 || $status > 100) {
            DUP_PRO_Log::error("Package SetStatus did not receive a proper code.");
        }

        // execute hooks only if status has changed
        $doHook = ($this->Status !== $status);
        if ($doHook) {
            do_action('duplicator_pro_package_before_set_status', $this, $status);
        }
        $this->Status = $status;
        $this->update(); // alwais update Backup
        if ($doHook) {
            do_action('duplicator_pro_package_after_set_status', $this, $status);
        }
    }

    /**
     * update Backup in database
     *
     * @param bool $die if true die on error otherwise return true on success and false on error
     *
     * @return bool
     */
    public function update($die = true)
    {
        /** @var wpdb $wpdb */
        global $wpdb;
        global $current_user;

        $this->cleanObjectBeforeSave();
        $this->updatePackageFlags();
        $this->version = DUPLICATOR_PRO_VERSION;
        $this->updated = gmdate("Y-m-d H:i:s");

        $packageObj = JsonSerialize::serialize($this, JSON_PRETTY_PRINT | JsonSerialize::JSON_SKIP_CLASS_NAME);
        if (!$packageObj) {
            if ($die) {
                DUP_PRO_Log::error("Package SetStatus was unable to serialize Backup object while updating record.");
            }
            return false;
        }
        $wpdb->flush();
        if (
            $wpdb->update(
                self::getTableName(),
                array(
                    'name'         => $this->Name,
                    'hash'         => $this->Hash,
                    'archive_name' => $this->get_archive_filename(),
                    'status'       => (int) $this->Status,
                    'progress'     => $this->Status,
                    'flags'        => implode(',', $this->flags),
                    'package'      => $packageObj,
                    'owner'        => isset($current_user->user_login) ? $current_user->user_login : 'unknown',
                    'version'      => $this->version,
                    'created'      => $this->created,
                    'updated_at'   => $this->updated,
                ),
                array('ID' => $this->ID),
                array(
                    '%s',
                    '%s',
                    '%s',
                    '%d',
                    '%f',
                    '%s',
                    '%s',
                    '%s',
                    '%s',
                    '%s',
                    '%s',
                ),
                array('%d')
            ) === false
        ) {
            if ($die) {
                DUP_PRO_Log::error("Database update error: " . $wpdb->last_error);
            }
            return false;
        }

        return true;
    }

    /**
     * Does a hash already exists
     *
     * @param string $hash The hash to check
     *
     * @return int Returns 0 if no has is found, if found returns the table ID
     */
    public function find_hash_key($hash)
    {
        global $wpdb;
        $table = self::getTableName();
        $qry   = $wpdb->get_row("SELECT ID, hash FROM `{$table}` WHERE hash = '{$hash}'");
        if (strlen($qry->hash) == 0) {
            return 0;
        } else {
            return $qry->ID;
        }
    }

    /**
     *  Makes the hashkey for the Backup files
     *
     *  @return string A unique hashkey
     */
    protected function make_hash()
    {
        // IMPORTANT!  Be VERY careful in changing this format - the FTP delete logic requires 3 segments with the last segment to be the date in YmdHis format.
        try {
            if (function_exists('random_bytes')) {
                // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.random_bytesFound
                $rand = (string) random_bytes(8);
                return bin2hex($rand) . mt_rand(1000, 9999) . '_' . date(self::PACKAGE_HASH_DATE_FORMAT);
            } else {
                return strtolower(md5(uniqid((string) rand(), true))) . '_' . date(self::PACKAGE_HASH_DATE_FORMAT);
            }
        } catch (Exception $exc) {
            return strtolower(md5(uniqid((string) rand(), true))) . '_' . date(self::PACKAGE_HASH_DATE_FORMAT);
        }
    }

    /**
     * Gets the active Backup.  The active Backup is defined as the Backup that was lasted saved.
     * Do to cache issues with the built in WP function get_option moved call to a direct DB call.
     *
     * @see DUP_PRO_Package::SaveActive
     *
     * @param bool $create_if_not_exists if true create a new Backup if not exists
     *
     * @return ?DUP_PRO_Package Retur Backup or null on failure
     */
    public static function get_temporary_package($create_if_not_exists = true)
    {

        global $wpdb;
        $row = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT option_value FROM `{$wpdb->options}` WHERE option_name = %s LIMIT 1",
                self::OPT_ACTIVE
            )
        );
        if (is_object($row)) {
            return self::get_from_json($row->option_value);
        } elseif ($create_if_not_exists) {
            return new DUP_PRO_Package();
        } else {
            return null;
        }
    }

    /**
     * Creates a default name
     *
     * @param bool $preDate if true prepend date to name
     *
     * @return string Default Backup name
     */
    public static function get_default_name($preDate = true)
    {
        //Remove specail_chars from final result
        $special_chars = array(
            ".",
            "-",
        );
        $name          = ($preDate) ? date('Ymd') . '_' . sanitize_title(get_bloginfo('name', 'display')) : sanitize_title(get_bloginfo('name', 'display')) . '_' . date('Ymd');
        $name          = substr(sanitize_file_name($name), 0, 40);
        $name          = str_replace($special_chars, '', $name);
        return $name;
    }

    /**
     * Generate a Backup name from a template
     *
     * @param ?DUP_PRO_Package_Template_Entity $template  Template to use
     * @param int                              $timestamp Timestamp
     *
     * @return string
     */
    protected function getNameFromFormat(DUP_PRO_Package_Template_Entity $template = null, $timestamp = 0)
    {
        $nameFormat = new NameFormat();
        $nameFormat->setTimestamp($timestamp);
        $nameFormat->setScheduleId($this->schedule_id);
        if ($template instanceof DUP_PRO_Package_Template_Entity) {
            $nameFormat->setFormat($template->package_name_format);
            $nameFormat->setTemplateId($template->getId());
        }
        return $nameFormat->getName();
    }

    /**
     * Safe tmp cleanup
     *
     * @param bool $purge_temp_archives if true purge temp archives
     *
     * @return void
     */
    public static function safe_tmp_cleanup($purge_temp_archives = false)
    {
        if ($purge_temp_archives) {
            $dir = DUPLICATOR_PRO_SSDIR_PATH_TMP . "/*_archive.zip.*";
            foreach (glob($dir) as $file_path) {
                unlink($file_path);
            }
            $dir = DUPLICATOR_PRO_SSDIR_PATH_TMP . "/*_archive.daf.*";
            foreach (glob($dir) as $file_path) {
                unlink($file_path);
            }
        } else {
            //Remove all temp files that are 24 hours old
            $dir   = DUPLICATOR_PRO_SSDIR_PATH_TMP . "/*";
            $files = glob($dir);
            if ($files !== false) {
                foreach ($files as $file_path) {
                    if (basename($file_path) === 'index.php') {
                        continue;
                    }
                    // Cut back to keeping things around for just 15 min
                    if (filemtime($file_path) <= time() - DUP_PRO_Constants::TEMP_CLEANUP_SECONDS) {
                        SnapIO::rrmdir($file_path);
                    }
                }
            }
        }
    }

    /**
     *  Cleanup all tmp files
     *
     *  @param bool $all empty all contents
     *
     *  @return bool true on success fail on failure
     */
    public static function tmp_cleanup($all = false)
    {
        //Delete all files now
        if ($all) {
            $dir = DUPLICATOR_PRO_SSDIR_PATH_TMP . "/*";
            foreach (glob($dir) as $file) {
                if (basename($file) === 'index.php') {
                    continue;
                }
                SnapIO::rrmdir($file);
            }
        } else {
            // Remove scan files that are 24 hours old
            $dir = DUPLICATOR_PRO_SSDIR_PATH_TMP . "/*_scan.json";
            foreach (glob($dir) as $file) {
                if (filemtime($file) <= time() - DUP_PRO_Constants::TEMP_CLEANUP_SECONDS) {
                    SnapIO::rrmdir($file);
                }
            }
        }

        // Clean up extras directory if it is still hanging around
        $extras_directory = SnapIO::safePath(DUPLICATOR_PRO_SSDIR_PATH_TMP) . '/extras';
        if (file_exists($extras_directory)) {
            try {
                if (!SnapIO::rrmdir($extras_directory)) {
                    throw new Exception('Failed to delete: ' . $extras_directory);
                }
            } catch (Exception $ex) {
                DUP_PRO_Log::trace("Couldn't recursively delete {$extras_directory}");
            }
        }

        return true;
    }

    /**
     * Build cleanup
     *
     * @return void
     */
    private function build_cleanup()
    {
        if (($files = SnapIO::regexGlob(DUPLICATOR_PRO_SSDIR_PATH_TMP, [])) !== false) {
            $filesToStore = array(
                $this->Installer->getInstallerLocalName(),
                $this->Archive->File,
            );
            $newPath      = DUPLICATOR_PRO_SSDIR_PATH;

            foreach ($files as $file) {
                $fileName = basename($file);

                if (!strstr($fileName, $this->getNameHash())) {
                    continue;
                }

                if (in_array($fileName, $filesToStore)) {
                    if (function_exists('rename')) {
                        rename($file, "{$newPath}/{$fileName}");
                    } elseif (function_exists('copy')) {
                        copy($file, "{$newPath}/{$fileName}");
                    } else {
                        throw new Exception('copy and rename function don\'t found');
                    }
                }

                if (file_exists($file)) {
                    unlink($file);
                }
            }
        }
        $this->set_status(DUP_PRO_PackageStatus::COPIEDPACKAGE);
    }

    /**
     *  Provides various date formats
     *
     *  @param string $utcDate created date in the GMT timezone
     *  @param int    $format  Various date formats to apply
     *
     *  @return string formatted date
     */
    public static function format_and_get_local_date_time($utcDate, $format = 1)
    {
        $date = get_date_from_gmt($utcDate);
        $date = new DateTime($date);
        switch ($format) {
            //YEAR
            case 1:
                return $date->format('Y-m-d H:i');
            case 2:
                return $date->format('Y-m-d H:i:s');
            case 3:
                return $date->format('y-m-d H:i');
            case 4:
                return $date->format('y-m-d H:i:s');
            //MONTH
            case 5:
                return $date->format('m-d-Y H:i');
            case 6:
                return $date->format('m-d-Y H:i:s');
            case 7:
                return $date->format('m-d-y H:i');
            case 8:
                return $date->format('m-d-y H:i:s');
            //DAY
            case 9:
                return $date->format('d-m-Y H:i');
            case 10:
                return $date->format('d-m-Y H:i:s');
            case 11:
                return $date->format('d-m-y H:i');
            case 12:
                return $date->format('d-m-y H:i:s');
            default:
                return $date->format('Y-m-d H:i');
        }
    }

    /**
     * Get Backup hash
     *
     * @return string Backup hash
     */
    public function get_package_hash()
    {
        if (strpos($this->Hash, '_') === false) {
            return '';
        }

        $hashParts    = explode('_', $this->Hash);
        $firstPart    = substr($hashParts[0], 0, 7);
        $secondPart   = substr($hashParts[1], -8);
        $package_hash = $firstPart . '-' . $secondPart;
        return $package_hash;
    }

    /**
     * Get secondary Backup hash
     *
     * @return string Backup hash
     */
    public function getSecondaryPackageHash()
    {
        $newHash      = $this->make_hash();
        $hashParts    = explode('_', $newHash);
        $firstPart    = substr($hashParts[0], 0, 7);
        $hashParts    = explode('_', $this->Hash);
        $secondPart   = substr($hashParts[1], -8);
        $package_hash = $firstPart . '-' . $secondPart;
        return $package_hash;
    }

    /**
     *  Provides the full sql file path in archive
     *
     *  @return string the full sql file path in archive
     */
    public function get_sql_ark_file_path()
    {
        $package_hash      = $this->get_package_hash();
        $sql_ark_file_Path = 'dup-installer/dup-database__' . $package_hash . '.sql';
        return $sql_ark_file_Path;
    }

    /**
     * @return bool returns true if Backup transfer was canceled or failed
     */
    public function transferWasInterrupted()
    {
        $recentUploadInfos = self::getRecentUploadInfos();
        foreach ($recentUploadInfos as $recentUploadInfo) {
            if ($recentUploadInfo->isFailed() || $recentUploadInfo->isCancelled()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Get recent unique $uploadInfos with giving highest priority to the latest one uploadInfo
     * if two or more uploadInfo of the same storage type exists
     *
     * @return DUP_PRO_Package_Upload_Info[]
     */
    private function getRecentUploadInfos()
    {
        $uploadInfos    = array();
        $tempStorageIds = array();
        foreach (array_reverse($this->upload_infos) as $upload_info) {
            if (!in_array($upload_info->getStorageId(), $tempStorageIds)) {
                $tempStorageIds[] = $upload_info->getStorageId();
                $uploadInfos[]    = $upload_info;
            }
        }
        return $uploadInfos;
    }

    /**
     * Get the timestamp from the Backup filename
     *
     * @param string $filename The filename to parse
     *
     * @return float|false The timestamp or false if not found
     */
    public static function get_timestamp_from_filename($filename)
    {
        $retval = false;
        $global = DUP_PRO_Global_Entity::getInstance();
        if ((DUP_PRO_STR::endsWith($filename, "_{$global->installer_base_name}")) || (DUP_PRO_STR::endsWith($filename, '_archive.zip')) || (DUP_PRO_STR::endsWith($filename, '_archive.daf'))) {
            $pieces      = explode('_', $filename);
            $piece_count = count($pieces);
            if ($piece_count >= 4) {
                $numeric_index = count($pieces) - 2;
                // Right before the _installer or _archive
                if (is_numeric($pieces[$numeric_index])) {
                    $retval = (float)$pieces[$numeric_index];
                } else {
                    DUP_PRO_Log::trace("Problem parsing file $filename when doing a comparison for ftp purge. Non-numeric timestamp");
                    $retval = false;
                }
            } else {
                DUP_PRO_Log::trace("Problem parsing file $filename when doing a comparison for ftp purge");
                $retval = false;
            }
        } else {
            $retval = false;
        }

        return $retval;
    }

    /**
     * Compares two backup filenames and returns -1, 0, 1 if a is less than, equal to, or greater than b
     *
     * @param string $filename_a The filename to parse
     * @param string $filename_b The filename to parse
     *
     * @return int
     */
    public static function compare_package_filenames_by_date($filename_a, $filename_b)
    {
        $ret_val = 0;
        // Should be in the format uniqueid_2digityear
        $a_timestamp = self::get_timestamp_from_filename($filename_a);
        $b_timestamp = self::get_timestamp_from_filename($filename_b);
        DUP_PRO_Log::trace("comparing a:$a_timestamp to b:$b_timestamp");
        if ($a_timestamp === false || $b_timestamp === false) {
            throw new Exception('Invalid timestamp for sorting');
        }
        if ($a_timestamp > $b_timestamp) {
            $ret_val = 1;
        } elseif ($a_timestamp < $b_timestamp) {
            $ret_val = -1;
        } else {
            $ret_val = 0;
        }
        return $ret_val;
    }

    /**
     * Get Backup table name
     *
     * @return string
     */
    public static function getTableName()
    {
        global $wpdb;
        return $wpdb->base_prefix . "duplicator_backups";
    }

    /**
     * Get version of Backups stored in DB
     *
     * @return string
     */
    public function getVersion()
    {
        return $this->version;
    }

    /**
     * Get created date
     *
     * @return string
     */
    public function getCreated()
    {
        return $this->created;
    }

    /**
     * Init entity table
     *
     * @return string[] Strings containing the results of the various update queries.
     */
    final public static function initTable()
    {
        /** @var wpdb $wpdb */
        global $wpdb;
        $charset_collate = $wpdb->get_charset_collate();
        $tableName       = self::getTableName();

        $flags = [
            self::FLAG_MANUAL,
            self::FLAG_SCHEDULE,
            self::FLAG_SCHEDULE_RUN_NOW,
            self::FLAG_DB_ONLY,
            self::FLAG_MEDIA_ONLY,
            self::FLAG_HAVE_LOCAL,
            self::FLAG_HAVE_REMOTE,
            self::FLAG_DISASTER_AVAIABLE,
            self::FLAG_DISASTER_SET,
            self::FLAG_CREATED_AFTER_RESTORE,
            self::FLAG_ACTIVE,
            self::FLAG_TEMPLATE,
            self::FLAG_ZIP_ARCHIVE,
            self::FLAG_DUP_ARCHIVE,
        ];

        $flagsStr = array_map(function ($flag) {
            return "'{$flag}'";
        }, $flags);
        $flagsStr = implode(',', $flagsStr);

        // PRIMARY KEY must have 2 spaces before for dbDelta to work
        // Mysql 5.5 can't have more than 1 DEFAULT CURRENT_TIMESTAMP
        $sql = <<<SQL
CREATE TABLE `{$tableName}` (
    `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
    `name` varchar(250) NOT NULL,
    `hash` varchar(50) NOT NULL,
    `archive_name` varchar(350) NOT NULL DEFAULT '',
    `status` int(11) NOT NULL,
    `progress` float NOT NULL DEFAULT 0,
    `flags` set({$flagsStr}) NOT NULL DEFAULT '',
    `package` longtext NOT NULL,
    `owner` varchar(60) NOT NULL DEFAULT '',
    `version` varchar(30) NOT NULL DEFAULT '',
    `created` TIMESTAMP NOT NULL DEFAULT '0000-00-00 00:00:00',
    `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY  (`id`),
    KEY `hash` (`hash`),
    KEY `flags` (`flags`),
    KEY `version` (`version`),
    KEY `created` (`created`),
    KEY `updated_at` (`updated_at`),
    KEY `status` (`status`),
    KEY `name` (`name`(191)),
    KEY `archive_name` (`archive_name`(191))
) {$charset_collate};
SQL;

        return SnapWP::dbDelta($sql);
    }
}
