implemented snapshot diff; started work on admin settings
This commit is contained in:
parent
4c67bf71c4
commit
8b22d56e32
7 changed files with 423 additions and 8 deletions
|
@ -14,4 +14,8 @@
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<nextcloud min-version="25" max-version="26"/>
|
<nextcloud min-version="25" max-version="26"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
<settings>
|
||||||
|
<admin>OCA\GroupfolderFilesystemSnapshots\Settings\SnapshotsAdmin</admin>
|
||||||
|
<admin-section>OCA\GroupfolderFilesystemSnapshots\Sections\SnapshotsSection</admin-section>
|
||||||
|
</settings>
|
||||||
</info>
|
</info>
|
||||||
|
|
20
lib/Entity/Snapshot.php
Normal file
20
lib/Entity/Snapshot.php
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace OCA\GroupfolderFilesystemSnapshots\Entity;
|
||||||
|
|
||||||
|
use JsonSerializable;
|
||||||
|
|
||||||
|
class Snapshot implements JsonSerializable {
|
||||||
|
/** @var string */
|
||||||
|
private $id;
|
||||||
|
|
||||||
|
public function __construct(string $id) {
|
||||||
|
$this->id = $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function jsonSerialize(): mixed {
|
||||||
|
return [
|
||||||
|
'id' => $this->id
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
208
lib/Manager/DiffManager.php
Normal file
208
lib/Manager/DiffManager.php
Normal file
|
@ -0,0 +1,208 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace OCA\GroupfolderFilesystemSnapshots\Manager;
|
||||||
|
|
||||||
|
class DiffManager {
|
||||||
|
|
||||||
|
public function __construct(){
|
||||||
|
}
|
||||||
|
|
||||||
|
private function seperateFilesFromFolders($parentDir, $items) {
|
||||||
|
$files = [];
|
||||||
|
$folders = [];
|
||||||
|
|
||||||
|
foreach($items as $item) {
|
||||||
|
if(is_dir($parentDir . DIRECTORY_SEPARATOR . $item)) {
|
||||||
|
$folders[] = $item;
|
||||||
|
} else {
|
||||||
|
$files[] = $item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array($files, $folders);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getFilesAndFolders($dir) {
|
||||||
|
$scan = array_diff(scandir($dir), array('..', '.'));
|
||||||
|
|
||||||
|
return $this->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;
|
||||||
|
}
|
||||||
|
}
|
78
lib/Manager/PathManager.php
Normal file
78
lib/Manager/PathManager.php
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace OCA\GroupfolderFilesystemSnapshots\Manager;
|
||||||
|
|
||||||
|
use OCP\IConfig;
|
||||||
|
use OCP\Files\IRootFolder;
|
||||||
|
use OCA\GroupFolders\Folder\FolderManager;
|
||||||
|
|
||||||
|
class PathManager {
|
||||||
|
private IConfig $config;
|
||||||
|
private IRootFolder $rootFolder;
|
||||||
|
private FolderManager $groupfolderFolderManager;
|
||||||
|
|
||||||
|
const FILESYSTEM_ROOT_PATH = "/srv/nextcloud/files/";
|
||||||
|
const FILESYSTEM_SNAPSHOT_PATH = "/srv/nextcloud/files/.zfs/snapshot/";
|
||||||
|
|
||||||
|
public function __construct(IConfig $config, IRootFolder $rootFolder, FolderManager $manager){
|
||||||
|
$this->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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,15 +2,55 @@
|
||||||
|
|
||||||
namespace OCA\GroupfolderFilesystemSnapshots\Manager;
|
namespace OCA\GroupfolderFilesystemSnapshots\Manager;
|
||||||
|
|
||||||
class SnapshotManager {
|
use OCA\GroupfolderFilesystemSnapshots\Manager\PathManager;
|
||||||
function get() {
|
use OCA\GroupfolderFilesystemSnapshots\Manager\DiffManager;
|
||||||
$filesystem_path = "/srv/nextcloud/files/";
|
|
||||||
$filesystem_snapshot_path = "/srv/nextcloud/files/.zfs/snapshot/";
|
|
||||||
|
|
||||||
$iterator = new \FilesystemIterator($filesystem_snapshot_path);
|
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) {
|
foreach ($iterator as $fileinfo) {
|
||||||
if(!$fileinfo->isDir()) continue;
|
if(!$fileinfo->isDir()) continue;
|
||||||
yield $fileinfo->getFilename();
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
32
lib/Sections/SnapshotsSection.php
Normal file
32
lib/Sections/SnapshotsSection.php
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
namespace OCA\GroupfolderFilesystemSnapshots\Sections;
|
||||||
|
|
||||||
|
use OCP\IL10N;
|
||||||
|
use OCP\IURLGenerator;
|
||||||
|
use OCP\Settings\IIconSection;
|
||||||
|
|
||||||
|
class SnapshotsSection implements IIconSection {
|
||||||
|
private IL10N $l;
|
||||||
|
private IURLGenerator $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 getID(): string {
|
||||||
|
return 'groupfolder_filesystem_snapshots';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(): string {
|
||||||
|
return $this->l->t('Groupfolder Filesystem Snapshots');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPriority(): int {
|
||||||
|
return 98;
|
||||||
|
}
|
||||||
|
}
|
33
lib/Settings/SnapshotsAdmin.php
Normal file
33
lib/Settings/SnapshotsAdmin.php
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
namespace OCA\GroupfolderFilesystemSnapshots\Settings;
|
||||||
|
|
||||||
|
use OCP\AppFramework\Http\TemplateResponse;
|
||||||
|
use OCP\IConfig;
|
||||||
|
use OCP\Settings\ISettings;
|
||||||
|
|
||||||
|
class SnapshotsAdmin implements ISettings {
|
||||||
|
private IConfig $config;
|
||||||
|
|
||||||
|
public function __construct(IConfig $config) {
|
||||||
|
$this->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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue