diff --git a/composer.json b/composer.json
index ecaf1ca9e..6f5d4788e 100755
--- a/composer.json
+++ b/composer.json
@@ -11,7 +11,8 @@
"doctrine/annotations": "^1.10",
"lavary/crunz": "^2.2",
"symfony/routing": "^5.1",
- "robmorgan/phinx": "^0.12.4"
+ "robmorgan/phinx": "^0.12.4",
+ "symfony/yaml": "^5.3"
},
"autoload": {
"psr-4": {
diff --git a/composer.lock b/composer.lock
index d246f254e..3e8e4ef38 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "0794a5a9aef9cea2de69788c63a07b37",
+ "content-hash": "b527b2c61f83b92047d2263dcce6d018",
"packages": [
{
"name": "cakephp/core",
@@ -3518,16 +3518,16 @@
},
{
"name": "symfony/yaml",
- "version": "v5.1.2",
+ "version": "v5.3.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
- "reference": "ea342353a3ef4f453809acc4ebc55382231d4d23"
+ "reference": "4500fe63dc9c6ffc32d3b1cb0448c329f9c814b7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/yaml/zipball/ea342353a3ef4f453809acc4ebc55382231d4d23",
- "reference": "ea342353a3ef4f453809acc4ebc55382231d4d23",
+ "url": "https://api.github.com/repos/symfony/yaml/zipball/4500fe63dc9c6ffc32d3b1cb0448c329f9c814b7",
+ "reference": "4500fe63dc9c6ffc32d3b1cb0448c329f9c814b7",
"shasum": ""
},
"require": {
@@ -3548,11 +3548,6 @@
"Resources/bin/yaml-lint"
],
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "5.1-dev"
- }
- },
"autoload": {
"psr-4": {
"Symfony\\Component\\Yaml\\": ""
@@ -3575,9 +3570,23 @@
"homepage": "https://symfony.com/contributors"
}
],
- "description": "Symfony Yaml Component",
+ "description": "Loads and dumps YAML files",
"homepage": "https://symfony.com",
- "time": "2020-05-20T17:43:50+00:00"
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-07-29T06:20:01+00:00"
},
{
"name": "vlucas/phpdotenv",
diff --git a/db/migrations/20210819182053_profile_backup.php b/db/migrations/20210819182053_profile_backup.php
new file mode 100644
index 000000000..3f4c6c6f0
--- /dev/null
+++ b/db/migrations/20210819182053_profile_backup.php
@@ -0,0 +1,44 @@
+table('Profile_Backup_Strategies', ['id' => "PBS_ID", 'primary_key' => ["PBS_ID"]]);
+ $table->addColumn('PBS_Date_Created', 'datetime', ['default' => 'CURRENT_TIMESTAMP'])
+ ->addColumn('PBS_Name', 'string')
+ ->create();
+
+ $this->execute("INSERT INTO `Profile_Backup_Strategies`(`PBS_Name`) VALUES ('Download & Archive')");
+
+ $table = $this->table('Profile_Backup_Schedule', ['id' => "PBS_ID", 'primary_key' => ["PBS_ID"]]);
+ $table->addColumn('PBS_Date_Created', 'datetime', ['default' => 'CURRENT_TIMESTAMP'])
+ ->addColumn('PBS_User_ID', 'integer')
+ ->addColumn('PBS_Schedule_String', 'string')
+ ->addColumn('PBS_Retention', 'integer')
+ ->addColumn('PBS_Host_ID', 'integer')
+ ->addColumn('PBS_Project', 'string')
+ ->addColumn('PBS_Stratergy_ID', 'integer')
+ ->addColumn('PBS_Disabled', 'boolean', ['null'=>false, "default"=>0])
+ ->addColumn('PBS_Disabled_Date', 'datetime', ['null'=>true])
+ ->addColumn('PBS_Disabled_By', 'integer', ['null'=>true])
+ ->addForeignKey('PBS_User_ID', 'Users', 'User_ID', ['delete'=> 'RESTRICT', 'update'=> 'RESTRICT'])
+ ->addForeignKey('PBS_Stratergy_ID', 'Profile_Backup_Strategies', 'PBS_ID', ['delete'=> 'RESTRICT', 'update'=> 'RESTRICT'])
+ ->addForeignKey('PBS_Disabled_By', 'Users', 'User_ID', ['delete'=> 'RESTRICT', 'update'=> 'RESTRICT'])
+ ->addForeignKey('PBS_Host_ID', 'Hosts', 'Host_ID', ['delete'=> 'CASCADE', 'update'=> 'RESTRICT'])
+ ->create();
+
+ $table = $this->table('Profile_Backups', ['id' => "PB_ID", 'primary_key' => ["PB_ID"]]);
+ $table->addColumn('PB_Date_Created', 'datetime', ['default' => 'CURRENT_TIMESTAMP'])
+ ->addColumn('PB_Host_ID', 'integer')
+ ->addColumn('PB_Local_Path', 'string')
+ ->addColumn('PB_Project', 'string')
+ ->addColumn('PB_Filesize', 'biginteger')
+ ->addColumn('PB_Deleted', 'datetime', ['null'=>true])
+ ->addForeignKey('PB_Host_ID', 'Hosts', 'Host_ID', ['delete'=> 'RESTRICT', 'update'=> 'RESTRICT'])
+ ->create();
+ }
+}
diff --git a/src/classes/Constants/ProfileBackupStrategies.php b/src/classes/Constants/ProfileBackupStrategies.php
new file mode 100644
index 000000000..217f7dc4e
--- /dev/null
+++ b/src/classes/Constants/ProfileBackupStrategies.php
@@ -0,0 +1,7 @@
+getProfilesBackupsOverview = $getProfilesBackupsOverview;
+ }
+
+ public function get($userId)
+ {
+ return $this->getProfilesBackupsOverview->get($userId);
+ }
+}
diff --git a/src/classes/Model/Backups/Profiles/FetchProfileBackups.php b/src/classes/Model/Backups/Profiles/FetchProfileBackups.php
new file mode 100644
index 000000000..c196f2426
--- /dev/null
+++ b/src/classes/Model/Backups/Profiles/FetchProfileBackups.php
@@ -0,0 +1,67 @@
+database = $database->dbObject;
+ }
+
+ public function fetchAll()
+ {
+ $sql = "SELECT
+ `PB_ID` as `id`,
+ `PB_Date_Created` as `storedDateCreated`,
+ `PB_Host_ID` as `hostId`,
+ `PB_Local_Path` as `localPath`,
+ `PB_Project` as `project`,
+ `PB_Deleted` as `deletedDate`,
+ `PB_Filesize` as `filesize`
+ FROM
+ `Profile_Backups`
+ ORDER BY
+ `storedDateCreated` DESC
+ ";
+ $do = $this->database->query($sql);
+ return $do->fetchAll(\PDO::FETCH_ASSOC);
+ }
+
+ public function fetchBackupsUserCanAccess(int $userId)
+ {
+ $sql = "SELECT
+ `PB_ID` as `id`,
+ `PB_Date_Created` as `storedDateCreated`,
+ `PB_Host_ID` as `hostId`,
+ `PB_Local_Path` as `localPath`,
+ `PB_Project` as `project`,
+ `PB_Deleted` as `deletedDate`,
+ `PB_Filesize` as `filesize`
+ FROM
+ `Profile_Backups`
+ WHERE
+ `PB_Project` IN(
+ SELECT
+ `UAP_Project`
+ FROM
+ `User_Allowed_Projects`
+ WHERE
+ `UAP_User_ID` = :userId
+ ) AND `PB_Host_ID` IN(
+ SELECT
+ `UAP_Host_ID`
+ FROM
+ `User_Allowed_Projects`
+ WHERE
+ `UAP_User_ID` = :userId
+ )";
+ $do = $this->database->prepare($sql);
+ $do->execute([
+ ":userId"=>$userId
+ ]);
+ return $do->fetchAll(\PDO::FETCH_ASSOC);
+ }
+}
diff --git a/src/classes/Model/Hosts/Backups/Profiles/InsertProfilesBackup.php b/src/classes/Model/Hosts/Backups/Profiles/InsertProfilesBackup.php
new file mode 100644
index 000000000..6f2f4204c
--- /dev/null
+++ b/src/classes/Model/Hosts/Backups/Profiles/InsertProfilesBackup.php
@@ -0,0 +1,40 @@
+database = $database->dbObject;
+ }
+
+ public function insert(
+ int $hostId,
+ string $project,
+ string $localPath,
+ int $filesize
+ ) {
+ $sql = "INSERT INTO `Profile_Backups`(
+ `PB_Host_ID`,
+ `PB_Local_Path`,
+ `PB_Project`,
+ `PB_Filesize`
+ ) VALUES(
+ :hostId,
+ :localPath,
+ :project,
+ :filesize
+ )";
+ $do = $this->database->prepare($sql);
+ $do->execute([
+ ":hostId"=>$hostId,
+ ":localPath"=>$localPath,
+ ":project"=>$project,
+ ":filesize"=>$filesize
+ ]);
+ return $this->database->lastInsertId();
+ }
+}
diff --git a/src/classes/Model/Hosts/Backups/Profiles/Schedules/FetchProfilesBackupSchedules.php b/src/classes/Model/Hosts/Backups/Profiles/Schedules/FetchProfilesBackupSchedules.php
new file mode 100644
index 000000000..0f4016dad
--- /dev/null
+++ b/src/classes/Model/Hosts/Backups/Profiles/Schedules/FetchProfilesBackupSchedules.php
@@ -0,0 +1,63 @@
+database = $database->dbObject;
+ }
+
+ public function fetchActiveSchedsGroupedByHostId()
+ {
+ $sql = "SELECT
+ `PBS_Host_ID` as `hostId`,
+ `PBS_Project` as `project`,
+ `PBS_Schedule_String` as `scheduleString`,
+ `PBS_Stratergy_ID` as `strategyId`,
+ `PBS_Retention` as `scheduleRetention`,
+ `Profile_Backup_Strategies`.`PBS_Name` as `strategyName`
+ FROM
+ `Profile_Backup_Schedule`
+ LEFT JOIN `Profile_Backup_Strategies` ON
+ `PBS_Stratergy_ID` = `Profile_Backup_Strategies`.`PBS_ID`
+ WHERE
+ `PBS_Disabled` = 0
+ ORDER BY
+ `Profile_Backup_Schedule`.`PBS_ID` ASC
+ ";
+ $do = $this->database->query($sql);
+ return $do->fetchAll(\PDO::FETCH_GROUP|\PDO::FETCH_ASSOC);
+ }
+
+ public function fetchActive(int $hostId)
+ {
+ $sql = "SELECT
+ `IBS_Host_ID` as `hostId`,
+ `IBS_Instance` as `instance`,
+ `IBS_Project` as `project`,
+ `IBS_Schedule_String` as `scheduleString`,
+ `IBS_BS_ID` as `strategyId`,
+ `IBS_Retention` as `scheduleRetention`,
+ `Backup_Strategies`.`BS_Name` as `strategyName`
+ FROM
+ `Instance_Backup_Schedule`
+ LEFT JOIN `Backup_Strategies` ON
+ `Instance_Backup_Schedule`.`IBS_BS_ID` = `Backup_Strategies`.`BS_ID`
+ WHERE
+ `IBS_Host_ID` = :hostId
+ AND
+ `IBS_Disabled` = 0
+ ORDER BY
+ `IBS_ID` ASC
+ ";
+ $do = $this->database->prepare($sql);
+ $do->execute([
+ ":hostId"=>$hostId
+ ]);
+ return $do->fetchAll(\PDO::FETCH_ASSOC);
+ }
+}
diff --git a/src/classes/Model/Hosts/Backups/Profiles/Schedules/InsertProfileBackupSchedule.php b/src/classes/Model/Hosts/Backups/Profiles/Schedules/InsertProfileBackupSchedule.php
new file mode 100644
index 000000000..4e0eebbe8
--- /dev/null
+++ b/src/classes/Model/Hosts/Backups/Profiles/Schedules/InsertProfileBackupSchedule.php
@@ -0,0 +1,49 @@
+database = $database->dbObject;
+ }
+
+ public function insert(
+ int $userId,
+ int $hostId,
+ string $project,
+ string $schedule,
+ int $strategyId,
+ int $retention
+ ) {
+ $sql = "INSERT INTO `Profile_Backup_Schedule`
+ (
+ `PBS_User_ID`,
+ `PBS_Host_ID`,
+ `PBS_Project`,
+ `PBS_Schedule_String`,
+ `PBS_Stratergy_ID`,
+ `PBS_Retention`
+ ) VALUES (
+ :userId,
+ :hostId,
+ :project,
+ :schedule,
+ :strategyId,
+ :retention
+ );";
+ $do = $this->database->prepare($sql);
+ $do->execute([
+ ":userId"=>$userId,
+ ":hostId"=>$hostId,
+ ":project"=>$project,
+ ":schedule"=>$schedule,
+ ":strategyId"=>$strategyId,
+ ":retention"=>$retention
+ ]);
+ return $do->rowCount() ? true : false;
+ }
+}
diff --git a/src/classes/Model/Hosts/Backups/Profiles/Schedules/UpdateProfileBackupSchedules.php b/src/classes/Model/Hosts/Backups/Profiles/Schedules/UpdateProfileBackupSchedules.php
new file mode 100644
index 000000000..8f726ad6f
--- /dev/null
+++ b/src/classes/Model/Hosts/Backups/Profiles/Schedules/UpdateProfileBackupSchedules.php
@@ -0,0 +1,38 @@
+database = $database->dbObject;
+ }
+
+ public function disableActiveScheds(
+ int $userId,
+ int $hostId,
+ string $project
+ ) {
+ $sql = "UPDATE `Profile_Backup_Schedule`
+ SET
+ `PBS_Disabled` = 1,
+ `PBS_Disabled_By` = :userId,
+ `PBS_Disabled_Date` = CURRENT_TIMESTAMP
+ WHERE
+ `PBS_Host_ID` = :hostId
+ AND
+ `PBS_Project` = :project
+ AND
+ `PBS_Disabled` = 0;";
+ $do = $this->database->prepare($sql);
+ $do->execute([
+ ":userId"=>$userId,
+ ":hostId"=>$hostId,
+ ":project"=>$project
+ ]);
+ return $do->rowCount() ? true : false;
+ }
+}
diff --git a/src/classes/Tools/Backups/Profiles/GetHostProfileStatusForBackupSet.php b/src/classes/Tools/Backups/Profiles/GetHostProfileStatusForBackupSet.php
new file mode 100644
index 000000000..710fa9a55
--- /dev/null
+++ b/src/classes/Tools/Backups/Profiles/GetHostProfileStatusForBackupSet.php
@@ -0,0 +1,200 @@
+getHostsInstances = $getHostsInstances;
+ $this->lxdClient = $lxdClient;
+ $this->fetchProfilesBackupSchedules = $fetchProfilesBackupSchedules;
+ $this->universe = $universe;
+ $this->hasExtension = $hasExtension;
+ }
+
+ public function get(int $userId, array $backups)
+ {
+ $clustersAndStandalone = $this->universe->getEntitiesUserHasAccesTo($userId, "projects");
+
+ $backupsByHostId = $this->createBackupsByHostIdStruct($backups);
+
+ $groupedSchedule = $this->groupSchedules($this->fetchProfilesBackupSchedules->fetchActiveSchedsGroupedByHostId());
+
+ $missingBackups = [];
+
+ foreach ($clustersAndStandalone["clusters"] as $cluster) {
+ foreach ($cluster["members"] as $host) {
+ if ($host->hostOnline()) {
+ $missingBackups[$host->getAlias()] = $this->addDetailsToHost($host, $backupsByHostId, $groupedSchedule);
+ }
+ }
+ }
+
+ foreach ($clustersAndStandalone["standalone"]["members"] as $host) {
+ if ($host->hostOnline()) {
+ $missingBackups[$host->getAlias()] = $this->addDetailsToHost($host, $backupsByHostId, $groupedSchedule);
+ }
+ }
+
+ return $missingBackups;
+ }
+
+ private function addDetailsToHost($host, $backupsByHostId, $groupedSchedule)
+ {
+ $backupsToSearch = [];
+
+ if (isset($backupsByHostId[$host->getHostId()])) {
+ $backupsToSearch = $backupsByHostId[$host->getHostId()];
+ }
+
+ $projects = [];
+ $seenProjectNames = [];
+
+ foreach ($host->getCustomProp("projects") as $name) {
+ $lastBackup = $this->findLastBackup($name, $backupsToSearch);
+ $project = ["name"=>$name];
+ $allBackups = $this->findAllBackupsandTotalSize($name, $backupsToSearch);
+
+ $scheduleString = "";
+ $scheduleRetention = "";
+ $strategyName = "";
+
+ if (isset($groupedSchedule[$host->getHostId()][$name])) {
+ $scheduleString = $groupedSchedule[$host->getHostId()][$name]["scheduleString"];
+ $scheduleRetention = $groupedSchedule[$host->getHostId()][$name]["scheduleRetention"];
+ $strategyName = $groupedSchedule[$host->getHostId()][$name]["strategyName"];
+ }
+
+ $project["projectExists"] = true;
+ $project["lastBackup"] = $lastBackup;
+ $project["totalSize"] = $allBackups["size"];
+ $project["allBackups"] = $allBackups["backups"];
+ $project["scheduleString"] = $scheduleString;
+ $project["scheduleRetention"] = $scheduleRetention;
+ $project["strategyName"] = $strategyName;
+
+ $projects[] = $project;
+
+ if (!isset($seenProjectNames[$name])) {
+ $seenProjectNames[$name] = [];
+ }
+
+ $seenProjectNames[$name][] = $name;
+ }
+
+ foreach ($backupsToSearch as $backup) {
+ if (!isset($seenProjectNames[$backup["project"]])) {
+ continue;
+ }
+
+ if (!isset($seenProjectNames[$backup["project"]])) {
+ $seenProjectNames[$backup["project"]] = [];
+ }
+
+ if (!in_array($backup["project"], $seenProjectNames[$backup["project"]])) {
+ $allBackups = $this->findAllBackupsandTotalSize($backup["project"], $backupsToSearch);
+
+ $projects[] = [
+ "name"=>$backup["project"],
+ "projectExists"=>false,
+ "scheduleString"=>"N/A",
+ "lastBackup"=>$this->findLastBackup($backup["project"], $backupsToSearch),
+ "allBackups"=>$allBackups["backups"],
+ "totalSize"=>$allBackups["size"],
+ "scheduleRetention"=>"",
+ "strategyName"=>""
+ ];
+ $seenProjectNames[$backup["project"]][] = $backup["project"];
+ }
+ }
+
+
+ $supportsBackups = $this->hasExtension->checkWithHost($host, LxdApiExtensions::CONTAINER_BACKUP);
+ $host->setCustomProp("supportsBackups", $supportsBackups);
+ $host->setCustomProp("projects", $projects);
+ $host->removeCustomProp("hostInfo");
+ $host->removeCustomProp("instances");
+
+ return $host;
+ }
+
+ private function createBackupsByHostIdStruct(array $backups)
+ {
+ $backupsByHostId = [];
+ foreach ($backups as $backup) {
+ if (!isset($backupsByHostId[$backup["hostId"]])) {
+ $backupsByHostId[$backup["hostId"]] = [];
+ }
+ $backupsByHostId[$backup["hostId"]][] = $backup;
+ }
+ return $backupsByHostId;
+ }
+
+ private function findAllBackupsandTotalSize(string $project, array $hostBackups)
+ {
+ $output = [];
+ $totalSize = 0;
+ foreach ($hostBackups as $backup) {
+ if ($backup["project"] == $project) {
+ $totalSize += $backup["filesize"];
+ $output[] = $backup;
+ }
+ }
+ return [
+ "backups"=>$output,
+ "size"=>$totalSize
+ ];
+ }
+
+ private function findLastBackup(string $project, ?array $hostBackups)
+ {
+ $x = [
+ "neverBackedUp"=>true
+ ];
+
+ if (empty($hostBackups)) {
+ return $x;
+ }
+
+ $latestDate = null;
+
+ foreach ($hostBackups as $backup) {
+ if ($project == $backup["project"]) {
+ $x["neverBackedUp"] = false;
+ if ((new \DateTime($backup["storedDateCreated"])) > $latestDate) {
+ $latestDate = (new \DateTime($backup["storedDateCreated"]));
+ $x = array_merge($x, $backup);
+ }
+ }
+ }
+ return $x;
+ }
+
+ private function groupSchedules(array $schedules)
+ {
+ foreach ($schedules as $hostId => $instances) {
+ foreach ($instances as $index => $instance) {
+ $schedules[$hostId][$instance["project"]] = $instance;
+ unset($schedules[$hostId][$index]);
+ }
+ }
+ return $schedules;
+ }
+}
diff --git a/src/classes/Tools/Backups/Profiles/GetProfilesBackupsOverview.php b/src/classes/Tools/Backups/Profiles/GetProfilesBackupsOverview.php
new file mode 100644
index 000000000..cdc7b7bb9
--- /dev/null
+++ b/src/classes/Tools/Backups/Profiles/GetProfilesBackupsOverview.php
@@ -0,0 +1,86 @@
+fetchProfileBackups = $fetchProfileBackups;
+ $this->getHostProfileStatusForBackupSet = $getHostProfileStatusForBackupSet;
+ $this->fetchUserDetails = $fetchUserDetails;
+ }
+
+ public function get($userId)
+ {
+ if ($this->fetchUserDetails->isAdmin($userId)) {
+ $allBackups = $this->fetchProfileBackups->fetchAll();
+ } else {
+ $allBackups = $this->fetchProfileBackups->fetchProfileBackupsUserCanAccess($userId);
+ }
+
+ $properties = $this->getProperties($allBackups);
+
+ $allBackups = $this->getHostProfileStatusForBackupSet->get($userId, $properties["localBackups"]);
+
+ return [
+ "sizeByMonthYear"=>$properties["sizeByMonthYear"],
+ "filesByMonthYear"=>$properties["filesByMonthYear"],
+ "allBackups"=>$allBackups
+ ];
+ }
+
+ private function getProperties(array $backups)
+ {
+ $sizeByMonthYear = [];
+ $filesByMonthYear = [];
+
+ foreach ($backups as $index => $backup) {
+ $date = new \DateTime($backup["storedDateCreated"]);
+ $month = $date->format("n");
+ $year = $date->format("Y");
+
+ if (!isset($sizeByMonthYear[$year])) {
+ $this->createYearArray($sizeByMonthYear, $year);
+ $this->createYearArray($filesByMonthYear, $year);
+ }
+
+
+ $filesize = $backup["filesize"];
+ // After 6 months or so this can be removed, most backup should have
+ // thier size stored in the database then.
+ if ($filesize == 0 && file_exists($backup["localPath"])) {
+ $filesize = filesize($backup["localPath"]);
+ }
+
+ $backup["filesize"] = $filesize;
+
+ if ($backup["deletedDate"] != null) {
+ unset($backups[$index]);
+ }
+
+ $sizeByMonthYear[$year][$month] += $filesize;
+ $filesByMonthYear[$year][$month] ++;
+ }
+
+ return [
+ "sizeByMonthYear"=>$sizeByMonthYear,
+ "filesByMonthYear"=>$filesByMonthYear,
+ "localBackups"=>$backups
+ ];
+ }
+
+ private function createYearArray(&$array, $year)
+ {
+ $array[$year] = array_fill(1, 12, null);
+ }
+}
diff --git a/src/classes/Tools/Hosts/Backups/Profiles/AddBackupSchedule.php b/src/classes/Tools/Hosts/Backups/Profiles/AddBackupSchedule.php
new file mode 100644
index 000000000..e50afadc5
--- /dev/null
+++ b/src/classes/Tools/Hosts/Backups/Profiles/AddBackupSchedule.php
@@ -0,0 +1,59 @@
+insertProfileBackupSchedule = $insertProfileBackupSchedule;
+ $this->updateProfileBackupSchedules = $updateProfileBackupSchedules;
+ }
+
+ public function add(
+ string $userId,
+ Host $host,
+ string $project,
+ string $frequency,
+ string $time,
+ int $strategyId,
+ int $retention,
+ array $daysOfWeek = [],
+ int $dayOfMonth = 0
+ ) {
+ $this->validateBakupSchedule($frequency, $time);
+
+ $this->updateProfileBackupSchedules->disableActiveScheds(
+ $userId,
+ $host->getHostId(),
+ $host->callClientMethod("getProject")
+ );
+
+ $this->insertProfileBackupSchedule->insert(
+ $userId,
+ $host->getHostId(),
+ $host->callClientMethod("getProject"),
+ $frequency . "~ " . $time . "~ [" . implode(",", $daysOfWeek) . "]~ $dayOfMonth~",
+ $strategyId,
+ $retention
+ );
+ }
+
+ private function validateBakupSchedule(string $frequency, string $time)
+ {
+ $allowedSchedules = ["daily", "weekly", "monthly"];
+ if (!in_array($frequency, $allowedSchedules)) {
+ throw new \Exception("Only supporting daily & weekly backups for the time being", 1);
+ }
+ // https://stackoverflow.com/a/3964994/4008082
+ if (preg_match("/^(?:2[0-3]|[01][0-9]):[0-5][0-9]$/", $time) == false) {
+ throw new \Exception("Time not correct format HH:MM 24 hour format", 1);
+ }
+ }
+}
diff --git a/src/classes/Tools/Hosts/Backups/Profiles/BackupProfiles.php b/src/classes/Tools/Hosts/Backups/Profiles/BackupProfiles.php
new file mode 100644
index 000000000..2c4ddbf3b
--- /dev/null
+++ b/src/classes/Tools/Hosts/Backups/Profiles/BackupProfiles.php
@@ -0,0 +1,58 @@
+getSetting = $getSetting;
+ $this->insertProfilesBackup = $insertProfilesBackup;
+ }
+
+ public function create(
+ Host $host,
+ string $project
+ ) {
+ $profiles = $host->profiles->all(LxdRecursionLevels::INSTANCE_FULL_RECURSION);
+ $zip = new \ZipArchive;
+
+ $backupPath = $this->getSetting->getSettingLatestValue(InstanceSettingsKeys::BACKUP_DIRECTORY);
+ $timezone = $this->getSetting->getSettingLatestValue(InstanceSettingsKeys::TIMEZONE);
+
+ $name = (new \DateTime())->setTimezone(new \DateTimeZone($timezone))->format("Y-m-d_H:i:s");
+
+ $backupPath .= "/{$host->getHostId()}/$project/profiles/";
+
+ if (!is_dir($backupPath)) {
+ mkdir($backupPath, 0777, true);
+ }
+
+ $backupPath .= "$name.zip";
+
+ $res = $zip->open($backupPath, \ZipArchive::CREATE);
+
+ foreach ($profiles as $name => $profile) {
+ $yaml = Yaml::dump($profile, 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK);
+ $zip->addFromString("{$profile["name"]}.yaml", (string) $yaml);
+ }
+
+ $zip->close();
+
+ $this->insertProfilesBackup->insert(
+ $host->getHostId(),
+ $project,
+ $backupPath,
+ filesize($backupPath)
+ );
+ }
+}
diff --git a/src/cronJobs/backupProfilesTask.php b/src/cronJobs/backupProfilesTask.php
new file mode 100644
index 000000000..81f8452b6
--- /dev/null
+++ b/src/cronJobs/backupProfilesTask.php
@@ -0,0 +1,66 @@
+build();
+
+$env = new Dotenv\Dotenv(__DIR__ . "/../../");
+$env->load();
+
+$fetchBackupSchedules = $container->make("dhope0000\LXDClient\Model\Hosts\Backups\Profiles\Schedules\FetchProfilesBackupSchedules");
+$schedules = $fetchBackupSchedules->fetchActiveSchedsGroupedByHostId();
+var_dump($schedules);
+
+$executeString = PHP_BINARY . ' ' . __DIR__ . '/scripts/backupProfiles.php';
+
+$dayOfWeekList = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
+
+$schedule = new Schedule();
+
+$getInstanceSetting = $container->make("dhope0000\LXDClient\Model\InstanceSettings\GetSetting");
+$timezone = $getInstanceSetting->getSettingLatestValue(dhope0000\LXDClient\Constants\InstanceSettingsKeys::TIMEZONE);
+
+foreach ($schedules as $hostId => $hostSchedules) {
+ foreach ($hostSchedules as $item) {
+ $argString = "{$hostId} {$item["project"]} {$item["strategyId"]}";
+ $name = "Backing profiles for $argString";
+
+ // Support the original backup format that was "daily hour:minute"
+ if (preg_match('/daily [0-9]{2}:[0-9]{2}/m', $item["scheduleString"])) {
+ $tSchedule = new BackupSchedule("daily", [explode(" ", $item["scheduleString"])[1]]);
+ } else {
+ $tSchedule = $createBackupSchedule->parseString($item["scheduleString"]);
+ }
+
+ if ($tSchedule->getRange() == "daily") {
+ foreach ($tSchedule->getTimes() as $time) {
+ $task = $schedule->run("$executeString $argString")->description($name . " daily@" . $time);
+ $task->timezone($timezone);
+ $task->daily()->at($time);
+ }
+ } elseif ($tSchedule->getRange() == "weekly") {
+ foreach ($tSchedule->getDaysOfWeek() as $dayOfWeek) {
+ foreach ($tSchedule->getTimes() as $time) {
+ $task = $schedule->run("$executeString $argString")->description($name . " " . $dayOfWeekList[$dayOfWeek] . "@" . $time);
+ $task->timezone($timezone);
+ $task->weeklyOn($dayOfWeek, $time);
+ }
+ }
+ } elseif ($tSchedule->getRange() == "monthly") {
+ foreach ($tSchedule->getTimes() as $time) {
+ $tParts = explode(":", $time);
+ $task = $schedule->run("$executeString $argString")->description($name . " monthly@" . $time);
+ $task->timezone($timezone);
+ $task->cron("{$tParts[1]} {$tParts[0]} {$tSchedule->getDayOfMonth()} * *");
+ }
+ } else {
+ throw new \Exception("Unsupported backup schedule", 1);
+ }
+ }
+}
+
+return $schedule;
diff --git a/src/cronJobs/scripts/backupProfiles.php b/src/cronJobs/scripts/backupProfiles.php
new file mode 100644
index 000000000..8c1e06d42
--- /dev/null
+++ b/src/cronJobs/scripts/backupProfiles.php
@@ -0,0 +1,37 @@
+build();
+
+if (count($argv) !== 4) {
+ throw new \Exception("script should be called backupProfiles.php hostId project strategyId", 1);
+}
+
+$hostId = $argv[1];
+
+if (!is_numeric($hostId)) {
+ throw new \Exception("host must be numeric id", 1);
+}
+
+$project = $argv[2];
+$strategy = $argv[3];
+
+if (!is_numeric($strategy)) {
+ throw new \Exception("Please provide strategy as numeric id", 1);
+}
+
+$getHost = $container->make("dhope0000\LXDClient\Model\Hosts\GetDetails");
+$backupProfiles = $container->make("dhope0000\LXDClient\Tools\Hosts\Backups\Profiles\BackupProfiles");
+
+$host = $getHost->fetchHost($hostId);
+
+$backupProfiles->create(
+ $host,
+ $project
+);
diff --git a/src/views/boxes/backups.php b/src/views/boxes/backups.php
index 67f1f0839..259613874 100644
--- a/src/views/boxes/backups.php
+++ b/src/views/boxes/backups.php
@@ -1,441 +1,21 @@
-
-
-
-
-
-
-
-
-
- | Instance |
- Schedule |
- Last Backed Up |
- # Backups |
- Size On Disk |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- This instance has been deleted, this backup is all that remains!
-
-
-
-
- | Name |
- Date Created |
- Size On Disk |
- Restore |
- Delete |
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/views/boxes/backups/instaces.php b/src/views/boxes/backups/instaces.php
new file mode 100644
index 000000000..93d078be2
--- /dev/null
+++ b/src/views/boxes/backups/instaces.php
@@ -0,0 +1,546 @@
+
+
+
+
+
+
+
+
+
+ | Instance |
+ Schedule |
+ Last Backed Up |
+ # Backups |
+ Size On Disk |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This instance has been deleted, this backup is all that remains!
+
+
+
+
+ | Name |
+ Date Created |
+ Size On Disk |
+ Restore |
+ Delete |
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/boxes/backups/profiles.php b/src/views/boxes/backups/profiles.php
new file mode 100644
index 000000000..c74a0321f
--- /dev/null
+++ b/src/views/boxes/backups/profiles.php
@@ -0,0 +1,245 @@
+
+
+
+
+
+
+
+
+
+ | Project |
+ Schedule |
+ Last Backed Up |
+ # Backups |
+ Size On Disk |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This instance has been deleted, this backup is all that remains!
+
+
+
+
+ | Name |
+ Date Created |
+ Size On Disk |
+ Restore |
+ Delete |
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/index.php b/src/views/index.php
index dc5dac612..2b23fe880 100755
--- a/src/views/index.php
+++ b/src/views/index.php
@@ -165,6 +165,10 @@
getAllData: '/api/AnalyticData/DownloadHistoryController/download'
},
backups: {
+ profiles: {
+ getOverview: '/api/Backups/Profiles/GetProfilesBackupsOverviewController/get',
+ },
+ //TODO These should all be under an "instance" key
strategies: {
getAll: "/api/Backups/Strategies/GetStrategiesController/get",
},