diff --git a/appinfo/info.xml b/appinfo/info.xml index 054b554..63d18f4 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -14,4 +14,8 @@ + + OCA\GroupfolderFilesystemSnapshots\Settings\SnapshotsAdmin + OCA\GroupfolderFilesystemSnapshots\Sections\SnapshotsSection + diff --git a/lib/Entity/Snapshot.php b/lib/Entity/Snapshot.php new file mode 100644 index 0000000..e571a7d --- /dev/null +++ b/lib/Entity/Snapshot.php @@ -0,0 +1,20 @@ +id = $id; + } + + public function jsonSerialize(): mixed { + return [ + 'id' => $this->id + ]; + } +} diff --git a/lib/Manager/DiffManager.php b/lib/Manager/DiffManager.php new file mode 100644 index 0000000..f0f0852 --- /dev/null +++ b/lib/Manager/DiffManager.php @@ -0,0 +1,208 @@ +seperateFilesFromFolders($dir, $scan); + } + + private function getFilesizesOfFiles($prefix, array $files) { + $result = array(); + foreach($files as $index=>$file) { + $result[$index] = filesize($prefix . DIRECTORY_SEPARATOR . $file); + } + + return $result; + } + + function diffDirectories($dir1, $dir2, $prefix = "") { + $diff = []; + + $scan1files = []; + $scan1folders = []; + if(file_exists($dir1) && is_dir($dir1)) { + list($scan1files, $scan1folders) = $this->getFilesAndFolders($dir1); + } + + $scan2files = []; + $scan2folders = []; + if(file_exists($dir2) && is_dir($dir2)) { + list($scan2files, $scan2folders) = $this->getFilesAndFolders($dir2); + } + + $fileCreations = array_diff($scan2files, $scan1files); + $fileCreationsFilesizes = $this->getFilesizesOfFiles($dir2, $fileCreations); + + $fileDeletions = array_diff($scan1files, $scan2files); + $fileDeletionsFilesizes = $this->getFilesizesOfFiles($dir1, $fileDeletions); + + $filePossibleEdits = array_intersect($scan1files, $scan2files); + + $allSubfolders = array_unique(array_merge($scan1folders, $scan2folders)); + + /*$diff[] = [ + "type" => "DEBUG", + "prefix" => $prefix, + "fileCreations" => $fileCreations, + "fileCreationsFilesizes" => $fileCreationsFilesizes, + "fileDeletions" => $fileDeletions, + "fileDeletionsFilesizes" => $fileDeletionsFilesizes, + //"folderCreations" => $folderCreations, + //"folderDeletions" => $folderDeletions, + "allSubfolders" => $allSubfolders, + ];*/ + + // search for creations and deletions, that are actually renames + foreach($fileCreations as $creationIndex=>$creation) { + $creationPath = $dir2 . DIRECTORY_SEPARATOR . $creation; + $creationSize = $fileCreationsFilesizes[$creationIndex]; + + $renameContenders = array_keys($fileDeletionsFilesizes, $creationSize); + + if(sizeof($renameContenders) != 0) { + /*$diff[] = [ + "type" => "DEBUG", + "comparing" => [ + "creation" => $creationIndex, + "deletions" => $renameContenders, + ], + ];*/ + + $creationSHA = sha1_file($creationPath); + foreach($renameContenders as $contender) { + $deletion = $fileDeletions[$contender]; + $deletionPath = $dir1 . DIRECTORY_SEPARATOR . $deletion; + $deletionSHA = sha1_file($deletionPath); + + if($deletionSHA == $creationSHA) { + $diff[] = [ + "type" => "RENAME", + "before" => [ + "exists" => True, + "path" => $prefix . DIRECTORY_SEPARATOR . $deletion, + "size" => $creationSize, + ], + "afterwards" => [ + "exists" => True, + "path" => $prefix . DIRECTORY_SEPARATOR . $creation, + "size" => $creationSize, + ] + + ]; + + unset($fileCreations[$creationIndex]); + unset($fileDeletions[$contender]); + + break; + } + } + } + } + + foreach($fileCreations as $index=>$creation) { + $diff[] = [ + "type" => "CREATION", + "before" => [ + "exists" => False, + ], + "afterwards" => [ + "exists" => True, + "path" => $prefix . DIRECTORY_SEPARATOR . $creation, + "size" => $fileCreationsFilesizes[$index], + ] + ]; + } + + foreach($fileDeletions as $index=>$deletion) { + $diff[] = [ + "type" => "DELETION", + "before" => [ + "exists" => True, + "path" => $prefix . DIRECTORY_SEPARATOR . $deletion, + "size" => $fileDeletionsFilesizes[$index], + ], + "afterwards" => [ + "exists" => False, + ], + ]; + } + + foreach($filePossibleEdits as $possibleEdit) { + $file1 = $dir1 . DIRECTORY_SEPARATOR . $possibleEdit; + $file2 = $dir2 . DIRECTORY_SEPARATOR . $possibleEdit; + $file1Size = filesize($file1); + $file2Size = filesize($file2); + + 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'); + + $filesdifferent = false; + + while(!feof($handle1)) { + if(fread($handle1, 8192) != fread($handle2, 8192)) { + // files are different + $filesdifferent = true; + break; + } + } + + fclose($handle1); + fclose($handle2); + + if(!$filesdifferent) { + continue; + } + } + } + + + $diff[] = [ + "type" => "EDIT", + "before" => [ + "path" => $prefix . DIRECTORY_SEPARATOR . $possibleEdit, + "size" => $file1Size, + ], + "afterwards" => [ + "path" => $prefix . DIRECTORY_SEPARATOR . $possibleEdit, + "size" => $file2Size, + ] + + ]; + } + + foreach($allSubfolders as $folder) { + array_push($diff, ...($this->diffDirectories($dir1 . DIRECTORY_SEPARATOR . $folder, $dir2 . DIRECTORY_SEPARATOR . $folder, $prefix . DIRECTORY_SEPARATOR . $folder))); + } + + return $diff; + } +} \ No newline at end of file diff --git a/lib/Manager/PathManager.php b/lib/Manager/PathManager.php new file mode 100644 index 0000000..662bbca --- /dev/null +++ b/lib/Manager/PathManager.php @@ -0,0 +1,78 @@ +config = $config; + $this->groupfolderFolderManager = $manager; + $this->rootFolder = $rootFolder; + } + + public function getFilesystemSnapshotPath() { + return self::FILESYSTEM_SNAPSHOT_PATH; + } + + // Nextcloud general + public function getDataDirectory() { + return $this->config->getSystemValue("datadirectory"); + } + + private function getRootFolderStorageId(): ?int { + return $this->rootFolder->getMountPoint()->getNumericStorageId(); + } + + // Snapshots + public function getSnapshotPath(string $snapshotId) { + return realpath(self::FILESYSTEM_SNAPSHOT_PATH . DIRECTORY_SEPARATOR . $snapshotId . DIRECTORY_SEPARATOR); + } + + public function convertToSnapshotPath(string $filesystemPath, string $snapshotId) { + $filesystemPath = realpath($filesystemPath); + + if(!str_starts_with($filesystemPath, self::FILESYSTEM_ROOT_PATH)) { + return false; + } + + return str_replace(self::FILESYSTEM_ROOT_PATH, $this->getSnapshotPath($snapshotId) . DIRECTORY_SEPARATOR, $filesystemPath); + } + + // Groupfolders + private function checkIfGroupfolderExists(int $groupfolderId): bool { + $storageId = $this->getRootFolderStorageId(); + if ($storageId === null) { + return "storage Id null"; + } + + $folder = $this->groupfolderFolderManager->getFolder($groupfolderId, $storageId); + if ($folder === false) { + return "Folder does not exist"; + } + + return true; + } + + public function getGroupFolderDirectory(int $groupfolderId) { + $folderExistsCheck = $this->checkIfGroupfolderExists($groupfolderId); + if(!$folderExistsCheck) { + return $folderExistsCheck; + } + + return realpath($this->getDataDirectory() . DIRECTORY_SEPARATOR . "__groupfolders" . DIRECTORY_SEPARATOR . $groupfolderId . DIRECTORY_SEPARATOR); + } + + public function getGroupFolderSnapshotDirectory(int $groupfolderId, string $snapshotId) { + return $this->convertToSnapshotPath($this->getGroupFolderDirectory($groupfolderId), $snapshotId); + } +} \ No newline at end of file diff --git a/lib/Manager/SnapshotManager.php b/lib/Manager/SnapshotManager.php index ef88bf3..b71fe5a 100644 --- a/lib/Manager/SnapshotManager.php +++ b/lib/Manager/SnapshotManager.php @@ -2,15 +2,55 @@ namespace OCA\GroupfolderFilesystemSnapshots\Manager; -class SnapshotManager { - function get() { - $filesystem_path = "/srv/nextcloud/files/"; - $filesystem_snapshot_path = "/srv/nextcloud/files/.zfs/snapshot/"; +use OCA\GroupfolderFilesystemSnapshots\Manager\PathManager; +use OCA\GroupfolderFilesystemSnapshots\Manager\DiffManager; - $iterator = new \FilesystemIterator($filesystem_snapshot_path); - foreach ($iterator as $fileinfo) { - if(!$fileinfo->isDir()) continue; - yield $fileinfo->getFilename(); +use OCA\GroupfolderFilesystemSnapshots\Entity\Snapshot; + +class SnapshotManager { + private PathManager $pathManager; + private DiffManager $diffManager; + + + public function __construct(PathManager $pathManager, DiffManager $diffManager){ + $this->pathManager = $pathManager; + $this->diffManager = $diffManager; + } + + private function validSnapshotId(string $snapshotId) { + return (preg_match("/^[a-zA-Z0-9-_]+$/", $snapshotId) == 1); + } + + function snapshotExists(string $snapshotId): bool { + if($this->validSnapshotId($snapshotId)) { + $path = $this->pathManager->getSnapshotPath($snapshotId); + return (file_exists($path) && is_dir($path)); + } else { + return false; } } + + function get(string $snapshotId) { + if(self::snapshotExists($snapshotId)) { + return new Snapshot($snapshotId); + } else { + return false; + } + + } + + function getAll() { + $iterator = new \FilesystemIterator($this->pathManager->getFilesystemSnapshotPath()); + foreach ($iterator as $fileinfo) { + if(!$fileinfo->isDir()) continue; + yield new Snapshot($fileinfo->getFilename()); + } + } + + function getDiff(int $groupfolderId, string $snapshotId) { + $groupfolderPath = $this->pathManager->getGroupFolderDirectory($groupfolderId); + $snapshotPath = $this->pathManager->getGroupFolderSnapshotDirectory($groupfolderId, $snapshotId); + + return $this->diffManager->diffDirectories($snapshotPath, $groupfolderPath); + } } \ No newline at end of file diff --git a/lib/Sections/SnapshotsSection.php b/lib/Sections/SnapshotsSection.php new file mode 100644 index 0000000..ca5800e --- /dev/null +++ b/lib/Sections/SnapshotsSection.php @@ -0,0 +1,32 @@ +l = $l; + $this->urlGenerator = $urlGenerator; + } + + public function getIcon(): string { + return $this->urlGenerator->imagePath('core', 'actions/settings-dark.svg'); + } + + public function getID(): string { + return 'groupfolder_filesystem_snapshots'; + } + + public function getName(): string { + return $this->l->t('Groupfolder Filesystem Snapshots'); + } + + public function getPriority(): int { + return 98; + } +} \ No newline at end of file diff --git a/lib/Settings/SnapshotsAdmin.php b/lib/Settings/SnapshotsAdmin.php new file mode 100644 index 0000000..eeeedd2 --- /dev/null +++ b/lib/Settings/SnapshotsAdmin.php @@ -0,0 +1,33 @@ +config = $config; + } + + /** + * @return TemplateResponse + */ + public function getForm() { + $parameters = [ + 'Filesystem Snapshots Path' => $this->config->getSystemValue('snapshots_path', true), + ]; + + return new TemplateResponse('settings', 'settings/admin', $parameters, ''); + } + + public function getSection() { + return 'groupfolder_filesystem_snapshots'; + } + + public function getPriority() { + return 10; + } +} \ No newline at end of file