diff --git a/appinfo/info.xml b/appinfo/info.xml index 7b0d2c6..bf96d33 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.0 agpl verdigado eG Jonathan Treffler diff --git a/lib/Db/DiffTaskResult.php b/lib/Db/DiffTaskResult.php index b6865a9..7ea190a 100644 --- a/lib/Db/DiffTaskResult.php +++ b/lib/Db/DiffTaskResult.php @@ -16,7 +16,9 @@ class DiffTaskResult extends Entity implements JsonSerializable { protected $beforeSize; protected $currentFileExists; - protected $currentPath; + protected $currentFileId; + + protected $currentPath; protected $currentSize; protected $reverted; @@ -26,6 +28,7 @@ class DiffTaskResult extends Entity implements JsonSerializable { $this->addType('beforeFileExists','boolean'); $this->addType('beforeSize','integer'); $this->addType('currentFileExists','boolean'); + $this->addType('currentFileId','integer'); $this->addType('currentSize','integer'); $this->addType('reverted','boolean'); } @@ -42,6 +45,7 @@ class DiffTaskResult extends Entity implements JsonSerializable { ], 'current' => [ 'fileExists' => $this->currentFileExists, + 'fileId' => $this->currentFileId, 'path' => $this->currentPath, 'size' => $this->currentSize, ], 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/DiffTaskService.php b/lib/Service/DiffTaskService.php index 16ebb46..c4d7032 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" => round(($numDoneFiles / $numFiles),2), ]); } }, @@ -113,7 +122,6 @@ class DiffTaskService { "overallFiles" => $numFiles, "doneFiles" => $numFiles, "progress" => 1.0, - "progressPercent" => "100.00%", "result" => $task, ]); }