diff --git a/.l10nignore b/.l10nignore new file mode 100644 index 0000000..e505894 --- /dev/null +++ b/.l10nignore @@ -0,0 +1,2 @@ +js/ +vendor/ \ No newline at end of file diff --git a/appinfo/info.xml b/appinfo/info.xml index 7b0d2c6..6edf2a5 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -5,7 +5,7 @@ Groupfolder Filesystem Snapshots Allows restoring a groupfolder to a previous snapshot in the filesystem App proving a PHP API for other apps, that allows (partially) restoring a groupfolder to a previous snapshot in the filesystem. Requires a filesystem with snapshot support (tested with and made for ZFS). It is made for other apps to integrate with, IT DOES NOT WORK STANDALONE - 1.3.1 + 1.4.1 agpl verdigado eG Jonathan Treffler @@ -13,7 +13,7 @@ files https://git.verdigado.com/verdigado-public/nextcloud_groupfolder_filesystem_snapshots/issues - + pgsql sqlite mysql diff --git a/krankerl.toml b/krankerl.toml index eee1014..ecf3843 100644 --- a/krankerl.toml +++ b/krankerl.toml @@ -1,6 +1,6 @@ [package] before_cmds = [ "composer install", - "npm install", + "npm ci", "npm run build", ] diff --git a/l10n/.gitkeep b/l10n/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/l10n/de.js b/l10n/de.js new file mode 100644 index 0000000..a94a64b --- /dev/null +++ b/l10n/de.js @@ -0,0 +1,9 @@ +OC.L10N.register( + "groupfolder_filesystem_snapshots", + { + "Automated hourly backup" : "Stündliches automatisches Backup", + "Automated daily backup" : "Tägliches automatisches Backup", + "Automated weekly backup" : "Wöchentliches automatisches Backup", + "Automated monthly backup" : "Monatliches automatisches Backup" +}, +"nplurals=2; plural=(n != 1);"); diff --git a/l10n/de.json b/l10n/de.json new file mode 100644 index 0000000..8ae274f --- /dev/null +++ b/l10n/de.json @@ -0,0 +1,7 @@ +{ "translations": { + "Automated hourly backup" : "Stündliches automatisches Backup", + "Automated daily backup" : "Tägliches automatisches Backup", + "Automated weekly backup" : "Wöchentliches automatisches Backup", + "Automated monthly backup" : "Monatliches automatisches Backup" +},"pluralForm" :"nplurals=2; plural=(n != 1);" +} \ No newline at end of file diff --git a/l10n/de_DE.js b/l10n/de_DE.js new file mode 100644 index 0000000..a94a64b --- /dev/null +++ b/l10n/de_DE.js @@ -0,0 +1,9 @@ +OC.L10N.register( + "groupfolder_filesystem_snapshots", + { + "Automated hourly backup" : "Stündliches automatisches Backup", + "Automated daily backup" : "Tägliches automatisches Backup", + "Automated weekly backup" : "Wöchentliches automatisches Backup", + "Automated monthly backup" : "Monatliches automatisches Backup" +}, +"nplurals=2; plural=(n != 1);"); diff --git a/l10n/de_DE.json b/l10n/de_DE.json new file mode 100644 index 0000000..8ae274f --- /dev/null +++ b/l10n/de_DE.json @@ -0,0 +1,7 @@ +{ "translations": { + "Automated hourly backup" : "Stündliches automatisches Backup", + "Automated daily backup" : "Tägliches automatisches Backup", + "Automated weekly backup" : "Wöchentliches automatisches Backup", + "Automated monthly backup" : "Monatliches automatisches Backup" +},"pluralForm" :"nplurals=2; plural=(n != 1);" +} \ No newline at end of file diff --git a/lib/Controller/AdminSettingsController.php b/lib/Controller/AdminSettingsController.php index 97302d0..399e08e 100644 --- a/lib/Controller/AdminSettingsController.php +++ b/lib/Controller/AdminSettingsController.php @@ -19,29 +19,29 @@ class AdminSettingsController extends Controller { ); } - /** - * @return JSONResponse - */ - public function index(): JSONResponse { - return new JSONResponse($this->settingsService->getAppValues()); - } + /** + * @return JSONResponse + */ + public function index(): JSONResponse { + return new JSONResponse($this->settingsService->getAppValues()); + } - /** - * @param $key - * - * @return JSONResponse - */ - public function show($key): JSONResponse { - return new JSONResponse($this->settingsService->getAppValue($key)); - } + /** + * @param $key + * + * @return JSONResponse + */ + public function show($key): JSONResponse { + return new JSONResponse($this->settingsService->getAppValue($key)); + } - /** - * @param $key - * @param $value - * - * @return JSONResponse - */ - public function update($key, $value): JSONResponse { + /** + * @param $key + * @param $value + * + * @return JSONResponse + */ + public function update($key, $value): JSONResponse { return new JSONResponse($this->settingsService->setAppValue($key, $value)); - } + } } \ No newline at end of file diff --git a/lib/Db/DiffTask.php b/lib/Db/DiffTask.php index 0398287..7a5a8c4 100644 --- a/lib/Db/DiffTask.php +++ b/lib/Db/DiffTask.php @@ -7,25 +7,25 @@ use OCP\AppFramework\Db\Entity; class DiffTask extends Entity implements JsonSerializable { - protected $userId; - protected $groupfolderId; + protected $userId; + protected $groupfolderId; protected $relativePath; - protected $snapshotId; - protected $timestamp; + protected $snapshotId; + protected $timestamp; - public function __construct() { - $this->addType('id','integer'); - $this->addType('groupfolderId','integer'); - } + public function __construct() { + $this->addType('id','integer'); + $this->addType('groupfolderId','integer'); + } - public function jsonSerialize() { - return [ - 'id' => $this->id, + public function jsonSerialize() { + return [ + 'id' => $this->id, 'userId' => $this->userId, - 'groupfolderId' => $this->groupfolderId, + 'groupfolderId' => $this->groupfolderId, 'relativePath' => $this->relativePath, - 'snapshotId' => $this->snapshotId, - 'timestamp' => $this->timestamp, - ]; - } + 'snapshotId' => $this->snapshotId, + 'timestamp' => $this->timestamp, + ]; + } } \ No newline at end of file diff --git a/lib/Db/DiffTaskMapper.php b/lib/Db/DiffTaskMapper.php index b1811c6..86f0353 100644 --- a/lib/Db/DiffTaskMapper.php +++ b/lib/Db/DiffTaskMapper.php @@ -9,29 +9,29 @@ use OCP\AppFramework\Db\QBMapper; */ class DiffTaskMapper extends QBMapper { - public function __construct(IDBConnection $db) { - parent::__construct($db, 'groupfolder_snapshots_tasks', DiffTask::class); - } + public function __construct(IDBConnection $db) { + parent::__construct($db, 'groupfolder_snapshots_tasks', DiffTask::class); + } - public function find(int $id, string $userId) { - $qb = $this->db->getQueryBuilder(); + public function find(int $id, string $userId) { + $qb = $this->db->getQueryBuilder(); - $qb->select('*') - ->from($this->getTableName()) - ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) - ->andWhere($qb->expr()->eq('user_id', $qb->createNamedParameter($userId))); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) + ->andWhere($qb->expr()->eq('user_id', $qb->createNamedParameter($userId))); - return $this->findEntity($qb); - } + return $this->findEntity($qb); + } - public function findAll(string $userId) { - $qb = $this->db->getQueryBuilder(); + public function findAll(string $userId) { + $qb = $this->db->getQueryBuilder(); - $qb->select('*') - ->from($this->getTableName()) - ->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId))); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId))); - return $this->findEntities($qb); - } + return $this->findEntities($qb); + } } \ No newline at end of file diff --git a/lib/Db/DiffTaskResult.php b/lib/Db/DiffTaskResult.php index b6865a9..e16d63e 100644 --- a/lib/Db/DiffTaskResult.php +++ b/lib/Db/DiffTaskResult.php @@ -7,45 +7,49 @@ use OCP\AppFramework\Db\Entity; class DiffTaskResult extends Entity implements JsonSerializable { - protected $taskId; - protected $timestamp; - protected $type; + protected $taskId; + protected $timestamp; + protected $type; - protected $beforeFileExists; - protected $beforePath; - protected $beforeSize; + protected $beforeFileExists; + protected $beforePath; + protected $beforeSize; - protected $currentFileExists; - protected $currentPath; - protected $currentSize; + protected $currentFileExists; + protected $currentFileId; + + protected $currentPath; + protected $currentSize; protected $reverted; - public function __construct() { - $this->addType('id','integer'); - $this->addType('taskId','integer'); - $this->addType('beforeFileExists','boolean'); - $this->addType('beforeSize','integer'); - $this->addType('currentFileExists','boolean'); - $this->addType('currentSize','integer'); + public function __construct() { + $this->addType('id','integer'); + $this->addType('taskId','integer'); + $this->addType('beforeFileExists','boolean'); + $this->addType('beforeSize','integer'); + $this->addType('currentFileExists','boolean'); + $this->addType('currentFileId','integer'); + $this->addType('currentSize','integer'); $this->addType('reverted','boolean'); - } + } - public function jsonSerialize() { - return [ - 'id' => $this->id, - 'taskId' => $this->taskId, - 'type' => $this->type, - 'before' => [ - 'fileExists' => $this->beforeFileExists, - 'path' => $this->beforePath, - 'size' => $this->beforeSize, - ], - 'current' => [ - 'fileExists' => $this->currentFileExists, - 'path' => $this->currentPath, - 'size' => $this->currentSize, + public function jsonSerialize() { + return [ + 'id' => $this->id, + 'taskId' => $this->taskId, + 'type' => $this->type, + 'before' => [ + 'fileExists' => $this->beforeFileExists, + 'path' => $this->beforePath, + 'size' => $this->beforeSize, + ], + 'current' => [ + 'fileExists' => $this->currentFileExists, + 'fileId' => $this->currentFileId, + 'path' => $this->currentPath, + 'size' => $this->currentSize, ], 'reverted' => $this->reverted, - ]; - } + ]; + } } \ No newline at end of file diff --git a/lib/Db/DiffTaskResultMapper.php b/lib/Db/DiffTaskResultMapper.php index 882a0e1..7ecbaf2 100644 --- a/lib/Db/DiffTaskResultMapper.php +++ b/lib/Db/DiffTaskResultMapper.php @@ -9,29 +9,29 @@ use OCP\AppFramework\Db\QBMapper; */ class DiffTaskResultMapper extends QBMapper { - public function __construct(IDBConnection $db) { - parent::__construct($db, 'groupfolder_snapshots_task_results', DiffTaskResult::class); - } + public function __construct(IDBConnection $db) { + parent::__construct($db, 'groupfolder_snapshots_task_results', DiffTaskResult::class); + } - public function find(int $id) { - $qb = $this->db->getQueryBuilder(); + public function find(int $id) { + $qb = $this->db->getQueryBuilder(); - $qb->select('*') - ->from($this->getTableName()) - ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))); - return $this->findEntity($qb); - } + return $this->findEntity($qb); + } - public function findAll(int $taskId) { - $qb = $this->db->getQueryBuilder(); + public function findAll(int $taskId) { + $qb = $this->db->getQueryBuilder(); - $qb->select('*') - ->from($this->getTableName()) - ->where($qb->expr()->eq('task_id', $qb->createNamedParameter($taskId))); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('task_id', $qb->createNamedParameter($taskId))); - return $this->findEntities($qb); - } + return $this->findEntities($qb); + } public function markReverted(int $id) { $taskResult = $this->find($id); diff --git a/lib/Entity/Snapshot.php b/lib/Entity/Snapshot.php index e571a7d..90a99fe 100644 --- a/lib/Entity/Snapshot.php +++ b/lib/Entity/Snapshot.php @@ -5,16 +5,30 @@ namespace OCA\GroupfolderFilesystemSnapshots\Entity; use JsonSerializable; class Snapshot implements JsonSerializable { - /** @var string */ - private $id; + public function __construct( + private string $id, + private string $name, + private ?\DateTimeImmutable $createdTimestamp = null, + ) { + } - public function __construct(string $id) { - $this->id = $id; - } + public function getId(): string { + return $this->id; + } - public function jsonSerialize(): mixed { + public function getName(): string { + return $this->name; + } + + public function getCreatedTimestamp(): ?\DateTimeImmutable { + return $this->createdTimestamp; + } + + public function jsonSerialize(): mixed { return [ - 'id' => $this->id + 'id' => $this->id, + 'name' => $this->name, + 'createdTimestamp' => $this->createdTimestamp?->getTimestamp(), ]; } } diff --git a/lib/Helpers/FileHelper.php b/lib/Helpers/FileHelper.php index c987549..27b6992 100644 --- a/lib/Helpers/FileHelper.php +++ b/lib/Helpers/FileHelper.php @@ -3,34 +3,34 @@ namespace OCA\GroupfolderFilesystemSnapshots\Helpers; class FileHelper { - private static function seperateFilesFromFolders($parentDir, $items) { - $files = []; - $folders = []; + private static function seperateFilesFromFolders($parentDir, $items) { + $files = []; + $folders = []; - foreach($items as $item) { - if(is_dir($parentDir . DIRECTORY_SEPARATOR . $item)) { - $folders[] = $item; - } else { - $files[] = $item; - } - } + foreach($items as $item) { + if(is_dir($parentDir . DIRECTORY_SEPARATOR . $item)) { + $folders[] = $item; + } else { + $files[] = $item; + } + } - return array($files, $folders); - } + return array($files, $folders); + } - public static function getFilesAndFolders($dir) { - $scan = array_diff(scandir($dir), array('..', '.')); + public static function getFilesAndFolders($dir) { + $scan = array_diff(scandir($dir), array('..', '.')); - return self::seperateFilesFromFolders($dir, $scan); - } + return self::seperateFilesFromFolders($dir, $scan); + } - public static function getFilesizesOfFiles($prefix, array $files) { - $result = array(); + public static function getFilesizesOfFiles($prefix, array $files) { + $result = array(); - foreach($files as $index=>$file) { - $result[$index] = filesize($prefix . DIRECTORY_SEPARATOR . $file); - } + foreach($files as $index=>$file) { + $result[$index] = filesize($prefix . DIRECTORY_SEPARATOR . $file); + } - return $result; - } + return $result; + } } \ No newline at end of file diff --git a/lib/Manager/PathManager.php b/lib/Manager/PathManager.php index 7b8a6e9..d7f0234 100644 --- a/lib/Manager/PathManager.php +++ b/lib/Manager/PathManager.php @@ -70,12 +70,12 @@ class PathManager { private function checkIfGroupfolderExists(int $groupfolderId): bool { $storageId = $this->getRootFolderStorageId(); if ($storageId === null) { - return "storage Id null"; + return false; } $folder = $this->groupfolderFolderManager->getFolder($groupfolderId, $storageId); if ($folder === false) { - return "Folder does not exist"; + return false; } return true; diff --git a/lib/Manager/SnapshotManager.php b/lib/Manager/SnapshotManager.php index a66def3..ac9594b 100644 --- a/lib/Manager/SnapshotManager.php +++ b/lib/Manager/SnapshotManager.php @@ -2,16 +2,21 @@ namespace OCA\GroupfolderFilesystemSnapshots\Manager; -use OCA\GroupfolderFilesystemSnapshots\Manager\PathManager; +use OCP\IL10N; +use OCA\GroupfolderFilesystemSnapshots\Manager\PathManager; +use OCA\GroupfolderFilesystemSnapshots\Service\SettingsService; use OCA\GroupfolderFilesystemSnapshots\Entity\Snapshot; class SnapshotManager { - private PathManager $pathManager; + private string $snapshotNamingScheme = ""; - - public function __construct(PathManager $pathManager){ - $this->pathManager = $pathManager; + public function __construct( + protected readonly IL10N $l10n, + private readonly PathManager $pathManager, + private readonly SettingsService $settingsService, + ){ + $this->snapshotNamingScheme = $this->settingsService->getAppValue("snapshot_naming_scheme"); } private function validSnapshotId(string $snapshotId) { @@ -27,20 +32,94 @@ class SnapshotManager { } } + private function createSnapshotEntity(string $id): Snapshot { + if ($this->snapshotNamingScheme === "zfs-auto-snapshot" && str_starts_with($id, "zfs-auto-snap")) { + if (str_starts_with($id, "zfs-auto-snap_hourly-")) { + $name = $this->l10n->t("Automated hourly backup"); + $datetimestring = str_replace("zfs-auto-snap_hourly-", "", $id); + } elseif (str_starts_with($id, "zfs-auto-snap_daily-")) { + $name = $this->l10n->t("Automated daily backup"); + $datetimestring = str_replace("zfs-auto-snap_daily-", "", $id); + } elseif (str_starts_with($id, "zfs-auto-snap_weekly-")) { + $name = $this->l10n->t("Automated weekly backup"); + $datetimestring = str_replace("zfs-auto-snap_weekly-", "", $id); + } elseif (str_starts_with($id, "zfs-auto-snap_monthly-")) { + $name = $this->l10n->t("Automated monthly backup"); + $datetimestring = str_replace("zfs-auto-snap_monthly-", "", $id); + } + + if(isset($datetimestring)) { + $datetimearray = explode("-", $datetimestring); + $timestring = array_pop($datetimearray); + + $year = (int)$datetimearray[0]; + $month = (int)$datetimearray[1]; + $day = (int)$datetimearray[2]; + $hour = (int)substr($timestring, 0, 2); + $minute = (int)substr($timestring, 2, 2); + + $createdTimestamp = (new \DateTimeImmutable()) + ->setDate($year, $month, $day) + ->setTime($hour, $minute); + } + + return new Snapshot( + id: $id, + name: $name ?: $id, + createdTimestamp: $createdTimestamp, + ); + } else { + return new Snapshot( + id: $id, + name: $id, + ); + } + } + function get(string $snapshotId) { if(self::snapshotExists($snapshotId)) { - return new Snapshot($snapshotId); + return $this->createSnapshotEntity($snapshotId); } else { return false; } - } - function getAll() { + function getAll(): array { + $snapshots = []; + $iterator = new \FilesystemIterator($this->pathManager->getFilesystemSnapshotsPath()); - foreach ($iterator as $fileinfo) { - if(!$fileinfo->isDir()) continue; - yield new Snapshot($fileinfo->getFilename()); - } + + foreach ($iterator as $fileinfo) { + if(!$fileinfo->isDir()) continue; + $snapshots[] = $this->createSnapshotEntity($fileinfo->getFilename()); + } + + return $snapshots; } + + function getAllGenerator(){ + $iterator = new \FilesystemIterator($this->pathManager->getFilesystemSnapshotsPath()); + + foreach ($iterator as $fileinfo) { + if(!$fileinfo->isDir()) continue; + yield $this->createSnapshotEntity($fileinfo->getFilename()); + } + } + + /** + * @var $subPathFilter Only return snapshots that have this subfolder in the specified groupfolder + */ + function getFilteredGenerator(int $groupfolderId, string $subDirectoryFilter) { + $iterator = new \FilesystemIterator($this->pathManager->getFilesystemSnapshotsPath()); + + $groupfolderSubdirectoryPath = $this->pathManager->getGroupFolderDirectory($groupfolderId, $subDirectoryFilter); + + foreach ($iterator as $fileinfo) { + if(!$fileinfo->isDir()) continue; + $snapshotId = $fileinfo->getFilename(); + $filterFullPath = $this->pathManager->convertToSnapshotPath($groupfolderSubdirectoryPath, $snapshotId); + if(!(is_dir($filterFullPath))) continue; + yield $this->createSnapshotEntity($snapshotId); + } + } } \ No newline at end of file diff --git a/lib/Migration/Version140Date20250701164500.php b/lib/Migration/Version140Date20250701164500.php new file mode 100644 index 0000000..bebdd12 --- /dev/null +++ b/lib/Migration/Version140Date20250701164500.php @@ -0,0 +1,33 @@ +getTable(self::RESULTS_TABLE); + + if(!$table->hasColumn('current_file_id')) { + $table->addColumn('current_file_id', Types::BIGINT, [ + 'notnull' => false, + ]); + } + + return $schema; + } +} diff --git a/lib/RecursiveDiff.php b/lib/RecursiveDiff.php index 98ed344..9196eee 100644 --- a/lib/RecursiveDiff.php +++ b/lib/RecursiveDiff.php @@ -5,227 +5,223 @@ namespace OCA\GroupfolderFilesystemSnapshots; use OCA\GroupfolderFilesystemSnapshots\Helpers\FileHelper; class RecursiveDiff { + private $scan1files = []; + private $scan1folders = []; - public string $dir1; - public string $dir2; + private $scan2files = []; + private $scan2folders = []; - private $prefix; + private $subJobs = []; - private $newResultCallback; - private $progressCallback; + private $subJobProgress = []; + private $progress = 0; - private $scan1files = []; - private $scan1folders = []; + public function __construct( + public readonly string $dir1, + public readonly string $dir2, + private readonly string $prefix = "", + private readonly array $folderBlocklist = [], + private $newResultCallback, + private $progressCallback, + ){} - private $scan2files = []; - private $scan2folders = []; + public function scan() { + $scan_num_files = 0; - private $subJobs = []; + if(file_exists($this->dir1) && is_dir($this->dir1)) { + [$this->scan1files, $this->scan1folders] = FileHelper::getFilesAndFolders($this->dir1); + } + + if(file_exists($this->dir2) && is_dir($this->dir2)) { + [$this->scan2files, $this->scan2folders] = FileHelper::getFilesAndFolders($this->dir2); + } - private $subJobProgress = []; - private $progress = 0; + $scan_num_files += sizeof($this->scan1files); + $scan_num_files += sizeof($this->scan2files); + + $allSubfolders = array_unique(array_merge($this->scan1folders, $this->scan2folders)); - public function __construct($dir1, $dir2, $prefix = "", $newResultCallback, $progressCallback){ - $this->dir1 = $dir1; - $this->dir2 = $dir2; - $this->prefix = $prefix; + foreach($allSubfolders as $key=>$folder) { + $subdir1 = $this->dir1 . DIRECTORY_SEPARATOR . $folder; + $subdir2 = $this->dir2 . DIRECTORY_SEPARATOR . $folder; + $subprefix = $this->prefix . DIRECTORY_SEPARATOR . $folder; + $subFolderBlocklist = $this->folderBlocklist[$folder] ?? []; - $this->newResultCallback = $newResultCallback; - $this->progressCallback = $progressCallback; - } + if($subFolderBlocklist === true) { + continue; + } + + $newJob = new RecursiveDiff($subdir1, $subdir2, $subprefix, $subFolderBlocklist, $this->newResultCallback, function($numDoneFiles) use ($key) { + $this->subJobProgress[$key] = $numDoneFiles; - public function scan() { - $scan_num_files = 0; + $this->updateProgress(); + }); - if(file_exists($this->dir1) && is_dir($this->dir1)) { - [$this->scan1files, $this->scan1folders] = FileHelper::getFilesAndFolders($this->dir1); - } - - if(file_exists($this->dir2) && is_dir($this->dir2)) { - [$this->scan2files, $this->scan2folders] = FileHelper::getFilesAndFolders($this->dir2); - } + $this->subJobs[] = $newJob; - $scan_num_files += sizeof($this->scan1files); - $scan_num_files += sizeof($this->scan2files); - - $allSubfolders = array_unique(array_merge($this->scan1folders, $this->scan2folders)); + $scan_num_files += $newJob->scan(); + } - foreach($allSubfolders as $key=>$folder) { - $subdir1 = $this->dir1 . DIRECTORY_SEPARATOR . $folder; - $subdir2 = $this->dir2 . DIRECTORY_SEPARATOR . $folder; - $subprefix = $this->prefix . DIRECTORY_SEPARATOR . $folder; - - $newJob = new RecursiveDiff($subdir1, $subdir2, $subprefix, $this->newResultCallback, function($numDoneFiles) use ($key) { - $this->subJobProgress[$key] = $numDoneFiles; + return $scan_num_files; + } - $this->updateProgress(); - }); + private function updateProgress() { + ($this->progressCallback)(array_sum($this->subJobProgress) + $this->progress); + } - $this->subJobs[] = $newJob; + function diff() { + $diff = []; - $scan_num_files += $newJob->scan(); - } + foreach($this->subJobs as $job) { + $result = $job->diff(); + array_push($diff, ...$result); + } - return $scan_num_files; - } + $fileCreations = array_diff($this->scan2files, $this->scan1files); + $fileCreationsFilesizes = FileHelper::getFilesizesOfFiles($this->dir2, $fileCreations); - private function updateProgress() { - ($this->progressCallback)(array_sum($this->subJobProgress) + $this->progress); - } + $fileDeletions = array_diff($this->scan1files, $this->scan2files); + $fileDeletionsFilesizes = FileHelper::getFilesizesOfFiles($this->dir1, $fileDeletions); - function diff() { - $diff = []; + $filePossibleEdits = array_intersect($this->scan1files, $this->scan2files); - foreach($this->subJobs as $job) { - $result = $job->diff(); - array_push($diff, ...$result); - } + /*$diff[] = [ + "type" => "DEBUG", + "prefix" => $this->prefix, + "fileCreations" => $fileCreations, + "fileCreationsFilesizes" => $fileCreationsFilesizes, + "fileDeletions" => $fileDeletions, + "fileDeletionsFilesizes" => $fileDeletionsFilesizes, + //"folderCreations" => $folderCreations, + //"folderDeletions" => $folderDeletions, + "allSubfolders" => $allSubfolders, + ];*/ - $fileCreations = array_diff($this->scan2files, $this->scan1files); - $fileCreationsFilesizes = FileHelper::getFilesizesOfFiles($this->dir2, $fileCreations); + // search for creations and deletions, that are actually renames + foreach($fileCreations as $creationIndex=>$creation) { + $creationPath = $this->dir2 . DIRECTORY_SEPARATOR . $creation; + $creationSize = $fileCreationsFilesizes[$creationIndex]; + + $renameContenders = array_keys($fileDeletionsFilesizes, $creationSize); - $fileDeletions = array_diff($this->scan1files, $this->scan2files); - $fileDeletionsFilesizes = FileHelper::getFilesizesOfFiles($this->dir1, $fileDeletions); + if(sizeof($renameContenders) != 0) { + /*$diff[] = [ + "type" => "DEBUG", + "comparing" => [ + "creation" => $creationIndex, + "deletions" => $renameContenders, + ], + ];*/ - $filePossibleEdits = array_intersect($this->scan1files, $this->scan2files); + $creationSHA = sha1_file($creationPath); + foreach($renameContenders as $contender) { + $deletion = $fileDeletions[$contender]; + $deletionPath = $this->dir1 . DIRECTORY_SEPARATOR . $deletion; + $deletionSHA = sha1_file($deletionPath); - /*$diff[] = [ - "type" => "DEBUG", - "prefix" => $this->prefix, - "fileCreations" => $fileCreations, - "fileCreationsFilesizes" => $fileCreationsFilesizes, - "fileDeletions" => $fileDeletions, - "fileDeletionsFilesizes" => $fileDeletionsFilesizes, - //"folderCreations" => $folderCreations, - //"folderDeletions" => $folderDeletions, - "allSubfolders" => $allSubfolders, - ];*/ + if($deletionSHA == $creationSHA) { + ($this->newResultCallback)( + type: "RENAME", + beforeFileExists: True, + beforePath: $this->prefix . DIRECTORY_SEPARATOR . $deletion, + beforeSize: $creationSize, + currentFileExists: True, + currentPath: $this->prefix . DIRECTORY_SEPARATOR . $creation, + currentSize: $creationSize, + ); - // search for creations and deletions, that are actually renames - foreach($fileCreations as $creationIndex=>$creation) { - $creationPath = $this->dir2 . DIRECTORY_SEPARATOR . $creation; - $creationSize = $fileCreationsFilesizes[$creationIndex]; - - $renameContenders = array_keys($fileDeletionsFilesizes, $creationSize); + unset($fileCreations[$creationIndex]); + unset($fileDeletions[$contender]); - if(sizeof($renameContenders) != 0) { - /*$diff[] = [ - "type" => "DEBUG", - "comparing" => [ - "creation" => $creationIndex, - "deletions" => $renameContenders, - ], - ];*/ + $this->progress += 2; + $this->updateProgress(); - $creationSHA = sha1_file($creationPath); - foreach($renameContenders as $contender) { - $deletion = $fileDeletions[$contender]; - $deletionPath = $this->dir1 . DIRECTORY_SEPARATOR . $deletion; - $deletionSHA = sha1_file($deletionPath); + break; + } + } + } + } - if($deletionSHA == $creationSHA) { - ($this->newResultCallback)( - type: "RENAME", - beforeFileExists: True, - beforePath: $this->prefix . DIRECTORY_SEPARATOR . $deletion, - beforeSize: $creationSize, - currentFileExists: True, - currentPath: $this->prefix . DIRECTORY_SEPARATOR . $creation, - currentSize: $creationSize, - ); + foreach($fileCreations as $index=>$creation) { + ($this->newResultCallback)( + type: "CREATION", + beforeFileExists: False, + beforePath: NULL, + beforeSize: NULL, + currentFileExists: True, + currentPath: $this->prefix . DIRECTORY_SEPARATOR . $creation, + currentSize: $fileCreationsFilesizes[$index], + ); - unset($fileCreations[$creationIndex]); - unset($fileDeletions[$contender]); + $this->progress++; + $this->updateProgress(); + } - $this->progress += 2; - $this->updateProgress(); + foreach($fileDeletions as $index=>$deletion) { + ($this->newResultCallback)( + type: "DELETION", + beforeFileExists: True, + beforePath: $this->prefix . DIRECTORY_SEPARATOR . $deletion, + beforeSize: $fileDeletionsFilesizes[$index], + currentFileExists: False, + currentPath: NULL, + currentSize: NULL, + ); - break; - } - } - } - } + $this->progress++; + $this->updateProgress(); + } - foreach($fileCreations as $index=>$creation) { - ($this->newResultCallback)( - type: "CREATION", - beforeFileExists: False, - beforePath: NULL, - beforeSize: NULL, - currentFileExists: True, - currentPath: $this->prefix . DIRECTORY_SEPARATOR . $creation, - currentSize: $fileCreationsFilesizes[$index], - ); + foreach($filePossibleEdits as $possibleEdit) { + $file1 = $this->dir1 . DIRECTORY_SEPARATOR . $possibleEdit; + $file2 = $this->dir2 . DIRECTORY_SEPARATOR . $possibleEdit; + $file1Size = filesize($file1); + $file2Size = filesize($file2); - $this->progress++; - $this->updateProgress(); - } + $this->progress += 2; + $this->updateProgress(); - foreach($fileDeletions as $index=>$deletion) { - ($this->newResultCallback)( - type: "DELETION", - beforeFileExists: True, - beforePath: $this->prefix . DIRECTORY_SEPARATOR . $deletion, - beforeSize: $fileDeletionsFilesizes[$index], - currentFileExists: False, - currentPath: NULL, - currentSize: NULL, - ); + if(filemtime($file1) == filemtime($file2)) { + //not different because same mtime + continue; + } else { + // mtime different, but could just have gotten touched without modifications + if($file1Size == $file2Size) { + // if filesize is the same check for binary differences + $handle1 = fopen($file1, 'rb'); + $handle2 = fopen($file2, 'rb'); - $this->progress++; - $this->updateProgress(); - } + $filesdifferent = false; - foreach($filePossibleEdits as $possibleEdit) { - $file1 = $this->dir1 . DIRECTORY_SEPARATOR . $possibleEdit; - $file2 = $this->dir2 . DIRECTORY_SEPARATOR . $possibleEdit; - $file1Size = filesize($file1); - $file2Size = filesize($file2); + while(!feof($handle1)) { + if(fread($handle1, 8192) != fread($handle2, 8192)) { + // files are different + $filesdifferent = true; + break; + } + } - $this->progress += 2; - $this->updateProgress(); + fclose($handle1); + fclose($handle2); - if(filemtime($file1) == filemtime($file2)) { - //not different because same mtime - continue; - } else { - // mtime different, but could just have gotten touched without modifications - if($file1Size == $file2Size) { - // if filesize is the same check for binary differences - $handle1 = fopen($file1, 'rb'); - $handle2 = fopen($file2, 'rb'); + if(!$filesdifferent) { + continue; + } + } + } - $filesdifferent = false; + + ($this->newResultCallback)( + type: "EDIT", + beforeFileExists: True, + beforePath: $this->prefix . DIRECTORY_SEPARATOR . $possibleEdit, + beforeSize: $file1Size, + currentFileExists: True, + currentPath: $this->prefix . DIRECTORY_SEPARATOR . $possibleEdit, + currentSize: $file2Size, + ); + } - while(!feof($handle1)) { - if(fread($handle1, 8192) != fread($handle2, 8192)) { - // files are different - $filesdifferent = true; - break; - } - } - - fclose($handle1); - fclose($handle2); - - if(!$filesdifferent) { - continue; - } - } - } - - - ($this->newResultCallback)( - type: "EDIT", - beforeFileExists: True, - beforePath: $this->prefix . DIRECTORY_SEPARATOR . $possibleEdit, - beforeSize: $file1Size, - currentFileExists: True, - currentPath: $this->prefix . DIRECTORY_SEPARATOR . $possibleEdit, - currentSize: $file2Size, - ); - } - - return $diff; - } + return $diff; + } } \ No newline at end of file diff --git a/lib/Sections/SnapshotsSection.php b/lib/Sections/SnapshotsSection.php index ca5800e..75f6ac5 100644 --- a/lib/Sections/SnapshotsSection.php +++ b/lib/Sections/SnapshotsSection.php @@ -6,27 +6,27 @@ use OCP\IURLGenerator; use OCP\Settings\IIconSection; class SnapshotsSection implements IIconSection { - private IL10N $l; - private IURLGenerator $urlGenerator; + private IL10N $l; + private IURLGenerator $urlGenerator; - public function __construct(IL10N $l, IURLGenerator $urlGenerator) { - $this->l = $l; - $this->urlGenerator = $urlGenerator; - } + public function __construct(IL10N $l, IURLGenerator $urlGenerator) { + $this->l = $l; + $this->urlGenerator = $urlGenerator; + } - public function getIcon(): string { - return $this->urlGenerator->imagePath('core', 'actions/settings-dark.svg'); - } + public function getIcon(): string { + return $this->urlGenerator->imagePath('core', 'actions/settings-dark.svg'); + } - public function getID(): string { - return 'groupfolder_filesystem_snapshots'; - } + public function getID(): string { + return 'groupfolder_filesystem_snapshots'; + } - public function getName(): string { - return $this->l->t('Groupfolder Filesystem Snapshots'); - } + public function getName(): string { + return $this->l->t('Groupfolder Filesystem Snapshots'); + } - public function getPriority(): int { - return 98; - } + public function getPriority(): int { + return 98; + } } \ No newline at end of file diff --git a/lib/Service/DiffTaskResultService.php b/lib/Service/DiffTaskResultService.php index 399185b..ed2e750 100644 --- a/lib/Service/DiffTaskResultService.php +++ b/lib/Service/DiffTaskResultService.php @@ -58,7 +58,7 @@ class DiffTaskResultService { $diffTaskResult = $this->find($id); if($diffTaskResult->getReverted()) { - throw new AlreadyRevertedException; + throw new AlreadyRevertedException(); } $taskId = $diffTaskResult->getTaskId(); @@ -66,35 +66,35 @@ class DiffTaskResultService { $snapshotPath = $this->pathManager->getGroupFolderSnapshotDirectory($diffTask->getGroupfolderId(), $diffTask->getRelativePath(), $diffTask->getSnapshotId()); - $gruenerFolder = $this->pathManager->getGroupfolderMountById($diffTask->getGroupfolderId())->get($diffTask->getRelativePath()); + $parentFolder = $this->pathManager->getGroupfolderMountById($diffTask->getGroupfolderId())->get($diffTask->getRelativePath()); switch($diffTaskResult->getType()) { case "CREATION": - $currentFile = $this->getSubfolderMustExist($gruenerFolder, $diffTaskResult->getCurrentPath()); + $currentFile = $this->getSubfolderMustExist($parentFolder, $diffTaskResult->getCurrentPath()); $currentFile->delete(); break; case "RENAME": - $currentFile = $this->getSubfolderMustExist($gruenerFolder, $diffTaskResult->getCurrentPath()); - $beforeDirectory = $this->getOrCreateSubdirectoryByFilepath($gruenerFolder, $diffTaskResult->getBeforePath()); + $currentFile = $this->getSubfolderMustExist($parentFolder, $diffTaskResult->getCurrentPath()); + $beforeDirectory = $this->getOrCreateSubdirectoryByFilepath($parentFolder, $diffTaskResult->getBeforePath()); $currentFile->move($beforeDirectory->getPath() . DIRECTORY_SEPARATOR . basename($diffTaskResult->getBeforePath())); break; case "DELETION": $beforeFileFilesystemPath = $snapshotPath . DIRECTORY_SEPARATOR . $diffTaskResult->getBeforePath(); - $beforeDirectory = $this->getOrCreateSubdirectoryByFilepath($gruenerFolder, $diffTaskResult->getBeforePath()); + $beforeDirectory = $this->getOrCreateSubdirectoryByFilepath($parentFolder, $diffTaskResult->getBeforePath()); $restoredFile = $beforeDirectory->newFile(basename($diffTaskResult->getBeforePath())); $this->copyFilesystemFileToNextcloudFile($beforeFileFilesystemPath, $restoredFile); break; case "EDIT": - $currentFile = $this->getSubfolderMustExist($gruenerFolder, $diffTaskResult->getCurrentPath()); + $currentFile = $this->getSubfolderMustExist($parentFolder, $diffTaskResult->getCurrentPath()); $beforeFileFilesystemPath = $snapshotPath . DIRECTORY_SEPARATOR . $diffTaskResult->getBeforePath(); $this->copyFilesystemFileToNextcloudFile($beforeFileFilesystemPath, $currentFile); break; default: - throw new \Exception; + throw new Exception(); } return $this->mapper->markReverted($id); @@ -104,7 +104,7 @@ class DiffTaskResultService { if($parent->nodeExists($path)) { return $parent->get($path); } else { - throw new ChangesMadeSinceDiffException; + throw new ChangesMadeSinceDiffException(); } } @@ -119,7 +119,7 @@ class DiffTaskResultService { if($temp instanceof \OCP\Files\Folder) { $beforeDirectory = $temp; } else { - throw new ChangesMadeSinceDiffException; + throw new ChangesMadeSinceDiffException(); } } else { $beforeDirectory = $beforeDirectory->newFolder($subdir); diff --git a/lib/Service/DiffTaskService.php b/lib/Service/DiffTaskService.php index 16ebb46..0f789ec 100644 --- a/lib/Service/DiffTaskService.php +++ b/lib/Service/DiffTaskService.php @@ -58,7 +58,8 @@ class DiffTaskService { } } - function create(string $relativePathInGroupfolder, int $groupfolderId, string $snapshotId, string $userId, Callable $progressCallback = null): ?DiffTask { + function create(string $relativePathInGroupfolder, int $groupfolderId, string $snapshotId, string $userId, array $folderBlocklist, Callable $progressCallback = null): ?DiffTask { + $parentNode = $this->pathManager->getGroupfolderMountById($groupfolderId)->get($relativePathInGroupfolder); $snapshotPath = $this->pathManager->getGroupFolderSnapshotDirectory($groupfolderId, $relativePathInGroupfolder, $snapshotId); $groupfolderPath = $this->pathManager->getGroupFolderDirectory($groupfolderId, $relativePathInGroupfolder); @@ -80,16 +81,25 @@ class DiffTaskService { $snapshotPath, $groupfolderPath, "", - function(string $type, bool $beforeFileExists, ?string $beforePath, ?int $beforeSize, bool $currentFileExists, ?string $currentPath, ?int $currentSize) use ($task) { + $folderBlocklist, + function(string $type, bool $beforeFileExists, ?string $beforePath, ?int $beforeSize, bool $currentFileExists, ?string $currentPath, ?int $currentSize) use ($task, $parentNode) { $newResult = new DiffTaskResult(); $newResult->setTaskId($task->getId()); $newResult->setType($type); + $newResult->setBeforeFileExists($beforeFileExists); - $newResult->setBeforePath($beforePath); - $newResult->setBeforeSize($beforeSize); + if($beforeFileExists) { + $newResult->setBeforePath($beforePath); + $newResult->setBeforeSize($beforeSize); + } + $newResult->setCurrentFileExists($currentFileExists); - $newResult->setCurrentPath($currentPath); - $newResult->setCurrentSize($currentSize); + if($currentFileExists) { + $newResult->setCurrentFileId($parentNode->get($currentPath)?->getId()); + $newResult->setCurrentPath($currentPath); + $newResult->setCurrentSize($currentSize); + } + $newResult = $this->diffTaskResultMapper->insert($newResult); }, function($numDoneFiles) use ($progressCallback, &$numFiles) { @@ -97,8 +107,7 @@ class DiffTaskService { ($progressCallback)([ "overallFiles" => $numFiles, "doneFiles" => $numDoneFiles, - "progress" => number_format(($numDoneFiles / $numFiles),2), - "progressPercent" => (number_format(($numDoneFiles / $numFiles),2) * 100) . "%", + "progress" => floor(($numDoneFiles / $numFiles) * 100) / 100, ]); } }, @@ -113,7 +122,6 @@ class DiffTaskService { "overallFiles" => $numFiles, "doneFiles" => $numFiles, "progress" => 1.0, - "progressPercent" => "100.00%", "result" => $task, ]); } diff --git a/lib/Service/SettingsService.php b/lib/Service/SettingsService.php index 9e9a56b..9846e84 100644 --- a/lib/Service/SettingsService.php +++ b/lib/Service/SettingsService.php @@ -9,7 +9,7 @@ use OCP\IConfig; class SettingsService { - private static array $VALID_APP_SETTINGS = ["filesystem_mountpoint_path", "filesystem_snapshots_path"]; + private static array $VALID_APP_SETTINGS = ["filesystem_mountpoint_path", "filesystem_snapshots_path", "snapshot_naming_scheme"]; public function __construct(private IConfig $config) { } @@ -28,7 +28,7 @@ class SettingsService { } } - public function setAppValue(string $key, string $value): string { + public function setAppValue(string $key, string $value): string { if(in_array($key, self::$VALID_APP_SETTINGS)) { if($value !== '') { $this->config->setAppValue(Application::APP_ID, $key, $value); @@ -38,5 +38,5 @@ class SettingsService { return $value; } - } + } } \ No newline at end of file diff --git a/lib/Settings/SnapshotsAdmin.php b/lib/Settings/SnapshotsAdmin.php index 24bed92..848cb58 100644 --- a/lib/Settings/SnapshotsAdmin.php +++ b/lib/Settings/SnapshotsAdmin.php @@ -6,21 +6,21 @@ use OCP\Settings\ISettings; class SnapshotsAdmin implements ISettings { - public function __construct() { - } + public function __construct() { + } - /** - * @return TemplateResponse - */ - public function getForm() { - return new TemplateResponse('groupfolder_filesystem_snapshots', 'settings/admin'); - } + /** + * @return TemplateResponse + */ + public function getForm() { + return new TemplateResponse('groupfolder_filesystem_snapshots', 'settings/admin'); + } - public function getSection() { - return 'groupfolder_filesystem_snapshots'; - } + public function getSection() { + return 'groupfolder_filesystem_snapshots'; + } - public function getPriority() { - return 10; - } + public function getPriority() { + return 10; + } } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 0933a90..fcbb78f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ }, "engines": { "node": "^16.0.0", - "npm": "^11.0.0" + "npm": "^7.0.0 || ^8.0.0" } }, "node_modules/@ampproject/remapping": { diff --git a/package.json b/package.json index 6a214a6..f4c091a 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ ], "engines": { "node": "^16.0.0", - "npm": "^11.0.0" + "npm": "^7.0.0 || ^8.0.0" }, "devDependencies": { "@nextcloud/babel-config": "^1.2.0", diff --git a/src/AdminSettings.vue b/src/AdminSettings.vue index fd5a7a3..e605975 100644 --- a/src/AdminSettings.vue +++ b/src/AdminSettings.vue @@ -4,12 +4,13 @@ name="Groupfolder Filesystem Snapshots" :limit-width="false">
-
@@ -18,7 +19,7 @@