Compare commits

..

1 commit

27 changed files with 1036 additions and 1267 deletions

View file

@ -1,2 +0,0 @@
js/
vendor/

View file

@ -5,7 +5,7 @@
<name>Groupfolder Filesystem Snapshots</name> <name>Groupfolder Filesystem Snapshots</name>
<summary>Allows restoring a groupfolder to a previous snapshot in the filesystem</summary> <summary>Allows restoring a groupfolder to a previous snapshot in the filesystem</summary>
<description>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</description> <description>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</description>
<version>1.4.1</version> <version>1.3.1</version>
<licence>agpl</licence> <licence>agpl</licence>
<author homepage="https://verdigado.com/">verdigado eG</author> <author homepage="https://verdigado.com/">verdigado eG</author>
<author mail="jonathan.treffler@verdigado.com">Jonathan Treffler</author> <author mail="jonathan.treffler@verdigado.com">Jonathan Treffler</author>
@ -13,7 +13,7 @@
<category>files</category> <category>files</category>
<bugs>https://git.verdigado.com/verdigado-public/nextcloud_groupfolder_filesystem_snapshots/issues</bugs> <bugs>https://git.verdigado.com/verdigado-public/nextcloud_groupfolder_filesystem_snapshots/issues</bugs>
<dependencies> <dependencies>
<nextcloud min-version="29" max-version="30"/> <nextcloud min-version="28" max-version="29"/>
<database>pgsql</database> <database>pgsql</database>
<database>sqlite</database> <database>sqlite</database>
<database>mysql</database> <database>mysql</database>

View file

@ -1,6 +1,6 @@
[package] [package]
before_cmds = [ before_cmds = [
"composer install", "composer install",
"npm ci", "npm install",
"npm run build", "npm run build",
] ]

View file

View file

@ -1,9 +0,0 @@
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);");

View file

@ -1,7 +0,0 @@
{ "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);"
}

View file

@ -1,9 +0,0 @@
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);");

View file

@ -1,7 +0,0 @@
{ "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);"
}

View file

@ -19,29 +19,29 @@ class AdminSettingsController extends Controller {
); );
} }
/** /**
* @return JSONResponse * @return JSONResponse
*/ */
public function index(): JSONResponse { public function index(): JSONResponse {
return new JSONResponse($this->settingsService->getAppValues()); return new JSONResponse($this->settingsService->getAppValues());
} }
/** /**
* @param $key * @param $key
* *
* @return JSONResponse * @return JSONResponse
*/ */
public function show($key): JSONResponse { public function show($key): JSONResponse {
return new JSONResponse($this->settingsService->getAppValue($key)); return new JSONResponse($this->settingsService->getAppValue($key));
} }
/** /**
* @param $key * @param $key
* @param $value * @param $value
* *
* @return JSONResponse * @return JSONResponse
*/ */
public function update($key, $value): JSONResponse { public function update($key, $value): JSONResponse {
return new JSONResponse($this->settingsService->setAppValue($key, $value)); return new JSONResponse($this->settingsService->setAppValue($key, $value));
} }
} }

View file

@ -7,25 +7,25 @@ use OCP\AppFramework\Db\Entity;
class DiffTask extends Entity implements JsonSerializable { class DiffTask extends Entity implements JsonSerializable {
protected $userId; protected $userId;
protected $groupfolderId; protected $groupfolderId;
protected $relativePath; protected $relativePath;
protected $snapshotId; protected $snapshotId;
protected $timestamp; protected $timestamp;
public function __construct() { public function __construct() {
$this->addType('id','integer'); $this->addType('id','integer');
$this->addType('groupfolderId','integer'); $this->addType('groupfolderId','integer');
} }
public function jsonSerialize() { public function jsonSerialize() {
return [ return [
'id' => $this->id, 'id' => $this->id,
'userId' => $this->userId, 'userId' => $this->userId,
'groupfolderId' => $this->groupfolderId, 'groupfolderId' => $this->groupfolderId,
'relativePath' => $this->relativePath, 'relativePath' => $this->relativePath,
'snapshotId' => $this->snapshotId, 'snapshotId' => $this->snapshotId,
'timestamp' => $this->timestamp, 'timestamp' => $this->timestamp,
]; ];
} }
} }

View file

@ -9,29 +9,29 @@ use OCP\AppFramework\Db\QBMapper;
*/ */
class DiffTaskMapper extends QBMapper { class DiffTaskMapper extends QBMapper {
public function __construct(IDBConnection $db) { public function __construct(IDBConnection $db) {
parent::__construct($db, 'groupfolder_snapshots_tasks', DiffTask::class); parent::__construct($db, 'groupfolder_snapshots_tasks', DiffTask::class);
} }
public function find(int $id, string $userId) { public function find(int $id, string $userId) {
$qb = $this->db->getQueryBuilder(); $qb = $this->db->getQueryBuilder();
$qb->select('*') $qb->select('*')
->from($this->getTableName()) ->from($this->getTableName())
->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) ->where($qb->expr()->eq('id', $qb->createNamedParameter($id)))
->andWhere($qb->expr()->eq('user_id', $qb->createNamedParameter($userId))); ->andWhere($qb->expr()->eq('user_id', $qb->createNamedParameter($userId)));
return $this->findEntity($qb); return $this->findEntity($qb);
} }
public function findAll(string $userId) { public function findAll(string $userId) {
$qb = $this->db->getQueryBuilder(); $qb = $this->db->getQueryBuilder();
$qb->select('*') $qb->select('*')
->from($this->getTableName()) ->from($this->getTableName())
->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId))); ->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId)));
return $this->findEntities($qb); return $this->findEntities($qb);
} }
} }

View file

@ -7,49 +7,45 @@ use OCP\AppFramework\Db\Entity;
class DiffTaskResult extends Entity implements JsonSerializable { class DiffTaskResult extends Entity implements JsonSerializable {
protected $taskId; protected $taskId;
protected $timestamp; protected $timestamp;
protected $type; protected $type;
protected $beforeFileExists; protected $beforeFileExists;
protected $beforePath; protected $beforePath;
protected $beforeSize; protected $beforeSize;
protected $currentFileExists; protected $currentFileExists;
protected $currentFileId; protected $currentPath;
protected $currentSize;
protected $currentPath;
protected $currentSize;
protected $reverted; protected $reverted;
public function __construct() { public function __construct() {
$this->addType('id','integer'); $this->addType('id','integer');
$this->addType('taskId','integer'); $this->addType('taskId','integer');
$this->addType('beforeFileExists','boolean'); $this->addType('beforeFileExists','boolean');
$this->addType('beforeSize','integer'); $this->addType('beforeSize','integer');
$this->addType('currentFileExists','boolean'); $this->addType('currentFileExists','boolean');
$this->addType('currentFileId','integer'); $this->addType('currentSize','integer');
$this->addType('currentSize','integer');
$this->addType('reverted','boolean'); $this->addType('reverted','boolean');
} }
public function jsonSerialize() { public function jsonSerialize() {
return [ return [
'id' => $this->id, 'id' => $this->id,
'taskId' => $this->taskId, 'taskId' => $this->taskId,
'type' => $this->type, 'type' => $this->type,
'before' => [ 'before' => [
'fileExists' => $this->beforeFileExists, 'fileExists' => $this->beforeFileExists,
'path' => $this->beforePath, 'path' => $this->beforePath,
'size' => $this->beforeSize, 'size' => $this->beforeSize,
], ],
'current' => [ 'current' => [
'fileExists' => $this->currentFileExists, 'fileExists' => $this->currentFileExists,
'fileId' => $this->currentFileId, 'path' => $this->currentPath,
'path' => $this->currentPath, 'size' => $this->currentSize,
'size' => $this->currentSize,
], ],
'reverted' => $this->reverted, 'reverted' => $this->reverted,
]; ];
} }
} }

View file

@ -9,29 +9,29 @@ use OCP\AppFramework\Db\QBMapper;
*/ */
class DiffTaskResultMapper extends QBMapper { class DiffTaskResultMapper extends QBMapper {
public function __construct(IDBConnection $db) { public function __construct(IDBConnection $db) {
parent::__construct($db, 'groupfolder_snapshots_task_results', DiffTaskResult::class); parent::__construct($db, 'groupfolder_snapshots_task_results', DiffTaskResult::class);
} }
public function find(int $id) { public function find(int $id) {
$qb = $this->db->getQueryBuilder(); $qb = $this->db->getQueryBuilder();
$qb->select('*') $qb->select('*')
->from($this->getTableName()) ->from($this->getTableName())
->where($qb->expr()->eq('id', $qb->createNamedParameter($id))); ->where($qb->expr()->eq('id', $qb->createNamedParameter($id)));
return $this->findEntity($qb); return $this->findEntity($qb);
} }
public function findAll(int $taskId) { public function findAll(int $taskId) {
$qb = $this->db->getQueryBuilder(); $qb = $this->db->getQueryBuilder();
$qb->select('*') $qb->select('*')
->from($this->getTableName()) ->from($this->getTableName())
->where($qb->expr()->eq('task_id', $qb->createNamedParameter($taskId))); ->where($qb->expr()->eq('task_id', $qb->createNamedParameter($taskId)));
return $this->findEntities($qb); return $this->findEntities($qb);
} }
public function markReverted(int $id) { public function markReverted(int $id) {
$taskResult = $this->find($id); $taskResult = $this->find($id);

View file

@ -5,30 +5,16 @@ namespace OCA\GroupfolderFilesystemSnapshots\Entity;
use JsonSerializable; use JsonSerializable;
class Snapshot implements JsonSerializable { class Snapshot implements JsonSerializable {
public function __construct( /** @var string */
private string $id, private $id;
private string $name,
private ?\DateTimeImmutable $createdTimestamp = null,
) {
}
public function getId(): string { public function __construct(string $id) {
return $this->id; $this->id = $id;
} }
public function getName(): string { public function jsonSerialize(): mixed {
return $this->name;
}
public function getCreatedTimestamp(): ?\DateTimeImmutable {
return $this->createdTimestamp;
}
public function jsonSerialize(): mixed {
return [ return [
'id' => $this->id, 'id' => $this->id
'name' => $this->name,
'createdTimestamp' => $this->createdTimestamp?->getTimestamp(),
]; ];
} }
} }

View file

@ -3,34 +3,34 @@
namespace OCA\GroupfolderFilesystemSnapshots\Helpers; namespace OCA\GroupfolderFilesystemSnapshots\Helpers;
class FileHelper { class FileHelper {
private static function seperateFilesFromFolders($parentDir, $items) { private static function seperateFilesFromFolders($parentDir, $items) {
$files = []; $files = [];
$folders = []; $folders = [];
foreach($items as $item) { foreach($items as $item) {
if(is_dir($parentDir . DIRECTORY_SEPARATOR . $item)) { if(is_dir($parentDir . DIRECTORY_SEPARATOR . $item)) {
$folders[] = $item; $folders[] = $item;
} else { } else {
$files[] = $item; $files[] = $item;
} }
} }
return array($files, $folders); return array($files, $folders);
} }
public static function getFilesAndFolders($dir) { public static function getFilesAndFolders($dir) {
$scan = array_diff(scandir($dir), array('..', '.')); $scan = array_diff(scandir($dir), array('..', '.'));
return self::seperateFilesFromFolders($dir, $scan); return self::seperateFilesFromFolders($dir, $scan);
} }
public static function getFilesizesOfFiles($prefix, array $files) { public static function getFilesizesOfFiles($prefix, array $files) {
$result = array(); $result = array();
foreach($files as $index=>$file) { foreach($files as $index=>$file) {
$result[$index] = filesize($prefix . DIRECTORY_SEPARATOR . $file); $result[$index] = filesize($prefix . DIRECTORY_SEPARATOR . $file);
} }
return $result; return $result;
} }
} }

View file

@ -70,12 +70,12 @@ class PathManager {
private function checkIfGroupfolderExists(int $groupfolderId): bool { private function checkIfGroupfolderExists(int $groupfolderId): bool {
$storageId = $this->getRootFolderStorageId(); $storageId = $this->getRootFolderStorageId();
if ($storageId === null) { if ($storageId === null) {
return false; return "storage Id null";
} }
$folder = $this->groupfolderFolderManager->getFolder($groupfolderId, $storageId); $folder = $this->groupfolderFolderManager->getFolder($groupfolderId, $storageId);
if ($folder === false) { if ($folder === false) {
return false; return "Folder does not exist";
} }
return true; return true;

View file

@ -2,21 +2,16 @@
namespace OCA\GroupfolderFilesystemSnapshots\Manager; namespace OCA\GroupfolderFilesystemSnapshots\Manager;
use OCP\IL10N;
use OCA\GroupfolderFilesystemSnapshots\Manager\PathManager; use OCA\GroupfolderFilesystemSnapshots\Manager\PathManager;
use OCA\GroupfolderFilesystemSnapshots\Service\SettingsService;
use OCA\GroupfolderFilesystemSnapshots\Entity\Snapshot; use OCA\GroupfolderFilesystemSnapshots\Entity\Snapshot;
class SnapshotManager { class SnapshotManager {
private string $snapshotNamingScheme = ""; private PathManager $pathManager;
public function __construct(
protected readonly IL10N $l10n, public function __construct(PathManager $pathManager){
private readonly PathManager $pathManager, $this->pathManager = $pathManager;
private readonly SettingsService $settingsService,
){
$this->snapshotNamingScheme = $this->settingsService->getAppValue("snapshot_naming_scheme");
} }
private function validSnapshotId(string $snapshotId) { private function validSnapshotId(string $snapshotId) {
@ -32,94 +27,20 @@ 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) { function get(string $snapshotId) {
if(self::snapshotExists($snapshotId)) { if(self::snapshotExists($snapshotId)) {
return $this->createSnapshotEntity($snapshotId); return new Snapshot($snapshotId);
} else { } else {
return false; return false;
} }
} }
function getAll(): array { function getAll() {
$snapshots = [];
$iterator = new \FilesystemIterator($this->pathManager->getFilesystemSnapshotsPath()); $iterator = new \FilesystemIterator($this->pathManager->getFilesystemSnapshotsPath());
foreach ($iterator as $fileinfo) {
foreach ($iterator as $fileinfo) { if(!$fileinfo->isDir()) continue;
if(!$fileinfo->isDir()) continue; yield new Snapshot($fileinfo->getFilename());
$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);
}
}
} }

View file

@ -1,33 +0,0 @@
<?php
namespace OCA\GroupfolderFilesystemSnapshots\Migration;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\SimpleMigrationStep;
use OCP\Migration\IOutput;
class Version140Date20250701164500 extends SimpleMigrationStep {
const RESULTS_TABLE = "groupfolder_snapshots_task_results";
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
$table = $schema->getTable(self::RESULTS_TABLE);
if(!$table->hasColumn('current_file_id')) {
$table->addColumn('current_file_id', Types::BIGINT, [
'notnull' => false,
]);
}
return $schema;
}
}

View file

@ -5,223 +5,227 @@ namespace OCA\GroupfolderFilesystemSnapshots;
use OCA\GroupfolderFilesystemSnapshots\Helpers\FileHelper; use OCA\GroupfolderFilesystemSnapshots\Helpers\FileHelper;
class RecursiveDiff { class RecursiveDiff {
private $scan1files = [];
private $scan1folders = [];
private $scan2files = []; public string $dir1;
private $scan2folders = []; public string $dir2;
private $subJobs = []; private $prefix;
private $subJobProgress = []; private $newResultCallback;
private $progress = 0; private $progressCallback;
public function __construct( private $scan1files = [];
public readonly string $dir1, private $scan1folders = [];
public readonly string $dir2,
private readonly string $prefix = "",
private readonly array $folderBlocklist = [],
private $newResultCallback,
private $progressCallback,
){}
public function scan() { private $scan2files = [];
$scan_num_files = 0; private $scan2folders = [];
if(file_exists($this->dir1) && is_dir($this->dir1)) { private $subJobs = [];
[$this->scan1files, $this->scan1folders] = FileHelper::getFilesAndFolders($this->dir1);
}
if(file_exists($this->dir2) && is_dir($this->dir2)) { private $subJobProgress = [];
[$this->scan2files, $this->scan2folders] = FileHelper::getFilesAndFolders($this->dir2); private $progress = 0;
}
$scan_num_files += sizeof($this->scan1files); public function __construct($dir1, $dir2, $prefix = "", $newResultCallback, $progressCallback){
$scan_num_files += sizeof($this->scan2files); $this->dir1 = $dir1;
$this->dir2 = $dir2;
$this->prefix = $prefix;
$allSubfolders = array_unique(array_merge($this->scan1folders, $this->scan2folders)); $this->newResultCallback = $newResultCallback;
$this->progressCallback = $progressCallback;
}
foreach($allSubfolders as $key=>$folder) { public function scan() {
$subdir1 = $this->dir1 . DIRECTORY_SEPARATOR . $folder; $scan_num_files = 0;
$subdir2 = $this->dir2 . DIRECTORY_SEPARATOR . $folder;
$subprefix = $this->prefix . DIRECTORY_SEPARATOR . $folder;
$subFolderBlocklist = $this->folderBlocklist[$folder] ?? [];
if($subFolderBlocklist === true) { if(file_exists($this->dir1) && is_dir($this->dir1)) {
continue; [$this->scan1files, $this->scan1folders] = FileHelper::getFilesAndFolders($this->dir1);
} }
$newJob = new RecursiveDiff($subdir1, $subdir2, $subprefix, $subFolderBlocklist, $this->newResultCallback, function($numDoneFiles) use ($key) { if(file_exists($this->dir2) && is_dir($this->dir2)) {
$this->subJobProgress[$key] = $numDoneFiles; [$this->scan2files, $this->scan2folders] = FileHelper::getFilesAndFolders($this->dir2);
}
$this->updateProgress(); $scan_num_files += sizeof($this->scan1files);
}); $scan_num_files += sizeof($this->scan2files);
$this->subJobs[] = $newJob; $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;
return $scan_num_files; $newJob = new RecursiveDiff($subdir1, $subdir2, $subprefix, $this->newResultCallback, function($numDoneFiles) use ($key) {
} $this->subJobProgress[$key] = $numDoneFiles;
private function updateProgress() { $this->updateProgress();
($this->progressCallback)(array_sum($this->subJobProgress) + $this->progress); });
}
function diff() { $this->subJobs[] = $newJob;
$diff = [];
foreach($this->subJobs as $job) { $scan_num_files += $newJob->scan();
$result = $job->diff(); }
array_push($diff, ...$result);
}
$fileCreations = array_diff($this->scan2files, $this->scan1files); return $scan_num_files;
$fileCreationsFilesizes = FileHelper::getFilesizesOfFiles($this->dir2, $fileCreations); }
$fileDeletions = array_diff($this->scan1files, $this->scan2files); private function updateProgress() {
$fileDeletionsFilesizes = FileHelper::getFilesizesOfFiles($this->dir1, $fileDeletions); ($this->progressCallback)(array_sum($this->subJobProgress) + $this->progress);
}
$filePossibleEdits = array_intersect($this->scan1files, $this->scan2files); function diff() {
$diff = [];
/*$diff[] = [ foreach($this->subJobs as $job) {
"type" => "DEBUG", $result = $job->diff();
"prefix" => $this->prefix, array_push($diff, ...$result);
"fileCreations" => $fileCreations, }
"fileCreationsFilesizes" => $fileCreationsFilesizes,
"fileDeletions" => $fileDeletions,
"fileDeletionsFilesizes" => $fileDeletionsFilesizes,
//"folderCreations" => $folderCreations,
//"folderDeletions" => $folderDeletions,
"allSubfolders" => $allSubfolders,
];*/
// search for creations and deletions, that are actually renames $fileCreations = array_diff($this->scan2files, $this->scan1files);
foreach($fileCreations as $creationIndex=>$creation) { $fileCreationsFilesizes = FileHelper::getFilesizesOfFiles($this->dir2, $fileCreations);
$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) { $filePossibleEdits = array_intersect($this->scan1files, $this->scan2files);
/*$diff[] = [
"type" => "DEBUG",
"comparing" => [
"creation" => $creationIndex,
"deletions" => $renameContenders,
],
];*/
$creationSHA = sha1_file($creationPath); /*$diff[] = [
foreach($renameContenders as $contender) { "type" => "DEBUG",
$deletion = $fileDeletions[$contender]; "prefix" => $this->prefix,
$deletionPath = $this->dir1 . DIRECTORY_SEPARATOR . $deletion; "fileCreations" => $fileCreations,
$deletionSHA = sha1_file($deletionPath); "fileCreationsFilesizes" => $fileCreationsFilesizes,
"fileDeletions" => $fileDeletions,
"fileDeletionsFilesizes" => $fileDeletionsFilesizes,
//"folderCreations" => $folderCreations,
//"folderDeletions" => $folderDeletions,
"allSubfolders" => $allSubfolders,
];*/
if($deletionSHA == $creationSHA) { // search for creations and deletions, that are actually renames
($this->newResultCallback)( foreach($fileCreations as $creationIndex=>$creation) {
type: "RENAME", $creationPath = $this->dir2 . DIRECTORY_SEPARATOR . $creation;
beforeFileExists: True, $creationSize = $fileCreationsFilesizes[$creationIndex];
beforePath: $this->prefix . DIRECTORY_SEPARATOR . $deletion,
beforeSize: $creationSize,
currentFileExists: True,
currentPath: $this->prefix . DIRECTORY_SEPARATOR . $creation,
currentSize: $creationSize,
);
unset($fileCreations[$creationIndex]); $renameContenders = array_keys($fileDeletionsFilesizes, $creationSize);
unset($fileDeletions[$contender]);
$this->progress += 2; if(sizeof($renameContenders) != 0) {
$this->updateProgress(); /*$diff[] = [
"type" => "DEBUG",
"comparing" => [
"creation" => $creationIndex,
"deletions" => $renameContenders,
],
];*/
break; $creationSHA = sha1_file($creationPath);
} foreach($renameContenders as $contender) {
} $deletion = $fileDeletions[$contender];
} $deletionPath = $this->dir1 . DIRECTORY_SEPARATOR . $deletion;
} $deletionSHA = sha1_file($deletionPath);
foreach($fileCreations as $index=>$creation) { if($deletionSHA == $creationSHA) {
($this->newResultCallback)( ($this->newResultCallback)(
type: "CREATION", type: "RENAME",
beforeFileExists: False, beforeFileExists: True,
beforePath: NULL, beforePath: $this->prefix . DIRECTORY_SEPARATOR . $deletion,
beforeSize: NULL, beforeSize: $creationSize,
currentFileExists: True, currentFileExists: True,
currentPath: $this->prefix . DIRECTORY_SEPARATOR . $creation, currentPath: $this->prefix . DIRECTORY_SEPARATOR . $creation,
currentSize: $fileCreationsFilesizes[$index], currentSize: $creationSize,
); );
$this->progress++; unset($fileCreations[$creationIndex]);
$this->updateProgress(); unset($fileDeletions[$contender]);
}
foreach($fileDeletions as $index=>$deletion) { $this->progress += 2;
($this->newResultCallback)( $this->updateProgress();
type: "DELETION",
beforeFileExists: True,
beforePath: $this->prefix . DIRECTORY_SEPARATOR . $deletion,
beforeSize: $fileDeletionsFilesizes[$index],
currentFileExists: False,
currentPath: NULL,
currentSize: NULL,
);
$this->progress++; break;
$this->updateProgress(); }
} }
}
}
foreach($filePossibleEdits as $possibleEdit) { foreach($fileCreations as $index=>$creation) {
$file1 = $this->dir1 . DIRECTORY_SEPARATOR . $possibleEdit; ($this->newResultCallback)(
$file2 = $this->dir2 . DIRECTORY_SEPARATOR . $possibleEdit; type: "CREATION",
$file1Size = filesize($file1); beforeFileExists: False,
$file2Size = filesize($file2); beforePath: NULL,
beforeSize: NULL,
currentFileExists: True,
currentPath: $this->prefix . DIRECTORY_SEPARATOR . $creation,
currentSize: $fileCreationsFilesizes[$index],
);
$this->progress += 2; $this->progress++;
$this->updateProgress(); $this->updateProgress();
}
if(filemtime($file1) == filemtime($file2)) { foreach($fileDeletions as $index=>$deletion) {
//not different because same mtime ($this->newResultCallback)(
continue; type: "DELETION",
} else { beforeFileExists: True,
// mtime different, but could just have gotten touched without modifications beforePath: $this->prefix . DIRECTORY_SEPARATOR . $deletion,
if($file1Size == $file2Size) { beforeSize: $fileDeletionsFilesizes[$index],
// if filesize is the same check for binary differences currentFileExists: False,
$handle1 = fopen($file1, 'rb'); currentPath: NULL,
$handle2 = fopen($file2, 'rb'); currentSize: NULL,
);
$filesdifferent = false; $this->progress++;
$this->updateProgress();
}
while(!feof($handle1)) { foreach($filePossibleEdits as $possibleEdit) {
if(fread($handle1, 8192) != fread($handle2, 8192)) { $file1 = $this->dir1 . DIRECTORY_SEPARATOR . $possibleEdit;
// files are different $file2 = $this->dir2 . DIRECTORY_SEPARATOR . $possibleEdit;
$filesdifferent = true; $file1Size = filesize($file1);
break; $file2Size = filesize($file2);
}
}
fclose($handle1); $this->progress += 2;
fclose($handle2); $this->updateProgress();
if(!$filesdifferent) { if(filemtime($file1) == filemtime($file2)) {
continue; //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;
}
}
}
($this->newResultCallback)( ($this->newResultCallback)(
type: "EDIT", type: "EDIT",
beforeFileExists: True, beforeFileExists: True,
beforePath: $this->prefix . DIRECTORY_SEPARATOR . $possibleEdit, beforePath: $this->prefix . DIRECTORY_SEPARATOR . $possibleEdit,
beforeSize: $file1Size, beforeSize: $file1Size,
currentFileExists: True, currentFileExists: True,
currentPath: $this->prefix . DIRECTORY_SEPARATOR . $possibleEdit, currentPath: $this->prefix . DIRECTORY_SEPARATOR . $possibleEdit,
currentSize: $file2Size, currentSize: $file2Size,
); );
} }
return $diff; return $diff;
} }
} }

View file

@ -6,27 +6,27 @@ use OCP\IURLGenerator;
use OCP\Settings\IIconSection; use OCP\Settings\IIconSection;
class SnapshotsSection implements IIconSection { class SnapshotsSection implements IIconSection {
private IL10N $l; private IL10N $l;
private IURLGenerator $urlGenerator; private IURLGenerator $urlGenerator;
public function __construct(IL10N $l, IURLGenerator $urlGenerator) { public function __construct(IL10N $l, IURLGenerator $urlGenerator) {
$this->l = $l; $this->l = $l;
$this->urlGenerator = $urlGenerator; $this->urlGenerator = $urlGenerator;
} }
public function getIcon(): string { public function getIcon(): string {
return $this->urlGenerator->imagePath('core', 'actions/settings-dark.svg'); return $this->urlGenerator->imagePath('core', 'actions/settings-dark.svg');
} }
public function getID(): string { public function getID(): string {
return 'groupfolder_filesystem_snapshots'; return 'groupfolder_filesystem_snapshots';
} }
public function getName(): string { public function getName(): string {
return $this->l->t('Groupfolder Filesystem Snapshots'); return $this->l->t('Groupfolder Filesystem Snapshots');
} }
public function getPriority(): int { public function getPriority(): int {
return 98; return 98;
} }
} }

View file

@ -58,7 +58,7 @@ class DiffTaskResultService {
$diffTaskResult = $this->find($id); $diffTaskResult = $this->find($id);
if($diffTaskResult->getReverted()) { if($diffTaskResult->getReverted()) {
throw new AlreadyRevertedException(); throw new AlreadyRevertedException;
} }
$taskId = $diffTaskResult->getTaskId(); $taskId = $diffTaskResult->getTaskId();
@ -66,35 +66,35 @@ class DiffTaskResultService {
$snapshotPath = $this->pathManager->getGroupFolderSnapshotDirectory($diffTask->getGroupfolderId(), $diffTask->getRelativePath(), $diffTask->getSnapshotId()); $snapshotPath = $this->pathManager->getGroupFolderSnapshotDirectory($diffTask->getGroupfolderId(), $diffTask->getRelativePath(), $diffTask->getSnapshotId());
$parentFolder = $this->pathManager->getGroupfolderMountById($diffTask->getGroupfolderId())->get($diffTask->getRelativePath()); $gruenerFolder = $this->pathManager->getGroupfolderMountById($diffTask->getGroupfolderId())->get($diffTask->getRelativePath());
switch($diffTaskResult->getType()) { switch($diffTaskResult->getType()) {
case "CREATION": case "CREATION":
$currentFile = $this->getSubfolderMustExist($parentFolder, $diffTaskResult->getCurrentPath()); $currentFile = $this->getSubfolderMustExist($gruenerFolder, $diffTaskResult->getCurrentPath());
$currentFile->delete(); $currentFile->delete();
break; break;
case "RENAME": case "RENAME":
$currentFile = $this->getSubfolderMustExist($parentFolder, $diffTaskResult->getCurrentPath()); $currentFile = $this->getSubfolderMustExist($gruenerFolder, $diffTaskResult->getCurrentPath());
$beforeDirectory = $this->getOrCreateSubdirectoryByFilepath($parentFolder, $diffTaskResult->getBeforePath()); $beforeDirectory = $this->getOrCreateSubdirectoryByFilepath($gruenerFolder, $diffTaskResult->getBeforePath());
$currentFile->move($beforeDirectory->getPath() . DIRECTORY_SEPARATOR . basename($diffTaskResult->getBeforePath())); $currentFile->move($beforeDirectory->getPath() . DIRECTORY_SEPARATOR . basename($diffTaskResult->getBeforePath()));
break; break;
case "DELETION": case "DELETION":
$beforeFileFilesystemPath = $snapshotPath . DIRECTORY_SEPARATOR . $diffTaskResult->getBeforePath(); $beforeFileFilesystemPath = $snapshotPath . DIRECTORY_SEPARATOR . $diffTaskResult->getBeforePath();
$beforeDirectory = $this->getOrCreateSubdirectoryByFilepath($parentFolder, $diffTaskResult->getBeforePath()); $beforeDirectory = $this->getOrCreateSubdirectoryByFilepath($gruenerFolder, $diffTaskResult->getBeforePath());
$restoredFile = $beforeDirectory->newFile(basename($diffTaskResult->getBeforePath())); $restoredFile = $beforeDirectory->newFile(basename($diffTaskResult->getBeforePath()));
$this->copyFilesystemFileToNextcloudFile($beforeFileFilesystemPath, $restoredFile); $this->copyFilesystemFileToNextcloudFile($beforeFileFilesystemPath, $restoredFile);
break; break;
case "EDIT": case "EDIT":
$currentFile = $this->getSubfolderMustExist($parentFolder, $diffTaskResult->getCurrentPath()); $currentFile = $this->getSubfolderMustExist($gruenerFolder, $diffTaskResult->getCurrentPath());
$beforeFileFilesystemPath = $snapshotPath . DIRECTORY_SEPARATOR . $diffTaskResult->getBeforePath(); $beforeFileFilesystemPath = $snapshotPath . DIRECTORY_SEPARATOR . $diffTaskResult->getBeforePath();
$this->copyFilesystemFileToNextcloudFile($beforeFileFilesystemPath, $currentFile); $this->copyFilesystemFileToNextcloudFile($beforeFileFilesystemPath, $currentFile);
break; break;
default: default:
throw new Exception(); throw new \Exception;
} }
return $this->mapper->markReverted($id); return $this->mapper->markReverted($id);
@ -104,7 +104,7 @@ class DiffTaskResultService {
if($parent->nodeExists($path)) { if($parent->nodeExists($path)) {
return $parent->get($path); return $parent->get($path);
} else { } else {
throw new ChangesMadeSinceDiffException(); throw new ChangesMadeSinceDiffException;
} }
} }
@ -119,7 +119,7 @@ class DiffTaskResultService {
if($temp instanceof \OCP\Files\Folder) { if($temp instanceof \OCP\Files\Folder) {
$beforeDirectory = $temp; $beforeDirectory = $temp;
} else { } else {
throw new ChangesMadeSinceDiffException(); throw new ChangesMadeSinceDiffException;
} }
} else { } else {
$beforeDirectory = $beforeDirectory->newFolder($subdir); $beforeDirectory = $beforeDirectory->newFolder($subdir);

View file

@ -58,8 +58,7 @@ class DiffTaskService {
} }
} }
function create(string $relativePathInGroupfolder, int $groupfolderId, string $snapshotId, string $userId, array $folderBlocklist, Callable $progressCallback = null): ?DiffTask { function create(string $relativePathInGroupfolder, int $groupfolderId, string $snapshotId, string $userId, Callable $progressCallback = null): ?DiffTask {
$parentNode = $this->pathManager->getGroupfolderMountById($groupfolderId)->get($relativePathInGroupfolder);
$snapshotPath = $this->pathManager->getGroupFolderSnapshotDirectory($groupfolderId, $relativePathInGroupfolder, $snapshotId); $snapshotPath = $this->pathManager->getGroupFolderSnapshotDirectory($groupfolderId, $relativePathInGroupfolder, $snapshotId);
$groupfolderPath = $this->pathManager->getGroupFolderDirectory($groupfolderId, $relativePathInGroupfolder); $groupfolderPath = $this->pathManager->getGroupFolderDirectory($groupfolderId, $relativePathInGroupfolder);
@ -81,25 +80,16 @@ class DiffTaskService {
$snapshotPath, $snapshotPath,
$groupfolderPath, $groupfolderPath,
"", "",
$folderBlocklist, function(string $type, bool $beforeFileExists, ?string $beforePath, ?int $beforeSize, bool $currentFileExists, ?string $currentPath, ?int $currentSize) use ($task) {
function(string $type, bool $beforeFileExists, ?string $beforePath, ?int $beforeSize, bool $currentFileExists, ?string $currentPath, ?int $currentSize) use ($task, $parentNode) {
$newResult = new DiffTaskResult(); $newResult = new DiffTaskResult();
$newResult->setTaskId($task->getId()); $newResult->setTaskId($task->getId());
$newResult->setType($type); $newResult->setType($type);
$newResult->setBeforeFileExists($beforeFileExists); $newResult->setBeforeFileExists($beforeFileExists);
if($beforeFileExists) { $newResult->setBeforePath($beforePath);
$newResult->setBeforePath($beforePath); $newResult->setBeforeSize($beforeSize);
$newResult->setBeforeSize($beforeSize);
}
$newResult->setCurrentFileExists($currentFileExists); $newResult->setCurrentFileExists($currentFileExists);
if($currentFileExists) { $newResult->setCurrentPath($currentPath);
$newResult->setCurrentFileId($parentNode->get($currentPath)?->getId()); $newResult->setCurrentSize($currentSize);
$newResult->setCurrentPath($currentPath);
$newResult->setCurrentSize($currentSize);
}
$newResult = $this->diffTaskResultMapper->insert($newResult); $newResult = $this->diffTaskResultMapper->insert($newResult);
}, },
function($numDoneFiles) use ($progressCallback, &$numFiles) { function($numDoneFiles) use ($progressCallback, &$numFiles) {
@ -107,7 +97,8 @@ class DiffTaskService {
($progressCallback)([ ($progressCallback)([
"overallFiles" => $numFiles, "overallFiles" => $numFiles,
"doneFiles" => $numDoneFiles, "doneFiles" => $numDoneFiles,
"progress" => floor(($numDoneFiles / $numFiles) * 100) / 100, "progress" => number_format(($numDoneFiles / $numFiles),2),
"progressPercent" => (number_format(($numDoneFiles / $numFiles),2) * 100) . "%",
]); ]);
} }
}, },
@ -122,6 +113,7 @@ class DiffTaskService {
"overallFiles" => $numFiles, "overallFiles" => $numFiles,
"doneFiles" => $numFiles, "doneFiles" => $numFiles,
"progress" => 1.0, "progress" => 1.0,
"progressPercent" => "100.00%",
"result" => $task, "result" => $task,
]); ]);
} }

View file

@ -9,7 +9,7 @@ use OCP\IConfig;
class SettingsService { class SettingsService {
private static array $VALID_APP_SETTINGS = ["filesystem_mountpoint_path", "filesystem_snapshots_path", "snapshot_naming_scheme"]; private static array $VALID_APP_SETTINGS = ["filesystem_mountpoint_path", "filesystem_snapshots_path"];
public function __construct(private IConfig $config) { 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(in_array($key, self::$VALID_APP_SETTINGS)) {
if($value !== '') { if($value !== '') {
$this->config->setAppValue(Application::APP_ID, $key, $value); $this->config->setAppValue(Application::APP_ID, $key, $value);
@ -38,5 +38,5 @@ class SettingsService {
return $value; return $value;
} }
} }
} }

View file

@ -6,21 +6,21 @@ use OCP\Settings\ISettings;
class SnapshotsAdmin implements ISettings { class SnapshotsAdmin implements ISettings {
public function __construct() { public function __construct() {
} }
/** /**
* @return TemplateResponse * @return TemplateResponse
*/ */
public function getForm() { public function getForm() {
return new TemplateResponse('groupfolder_filesystem_snapshots', 'settings/admin'); return new TemplateResponse('groupfolder_filesystem_snapshots', 'settings/admin');
} }
public function getSection() { public function getSection() {
return 'groupfolder_filesystem_snapshots'; return 'groupfolder_filesystem_snapshots';
} }
public function getPriority() { public function getPriority() {
return 10; return 10;
} }
} }

1299
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -36,6 +36,6 @@
"devDependencies": { "devDependencies": {
"@nextcloud/babel-config": "^1.2.0", "@nextcloud/babel-config": "^1.2.0",
"@nextcloud/browserslist-config": "^3.0.1", "@nextcloud/browserslist-config": "^3.0.1",
"@nextcloud/webpack-vue-config": "^5.4.0" "@nextcloud/webpack-vue-config": "^6.0.0"
} }
} }

View file

@ -4,13 +4,12 @@
name="Groupfolder Filesystem Snapshots" name="Groupfolder Filesystem Snapshots"
:limit-width="false"> :limit-width="false">
<div v-if="!loading"> <div v-if="!loading">
<Field :is="getAppSettingComponent(setting)" <Field :is="setting.sensitive ? NcPasswordField : NcTextField"
v-for="setting in app_settings" v-for="setting in app_settings"
v-bind="getAppSettingProps(setting)"
:key="setting.id" :key="setting.id"
class="settings_field" class="settings_field"
:value="settings?.[setting.id]" :value="settings?.[setting.id]"
@update:modelValue="(newValue) => updateSetting(setting.id, newValue)" :label="setting.name"
@update:value="(newValue) => updateSetting(setting.id, newValue)" /> @update:value="(newValue) => updateSetting(setting.id, newValue)" />
</div> </div>
</NcSettingsSection> </NcSettingsSection>
@ -19,7 +18,7 @@
<script setup> <script setup>
import { ref } from "vue"; import { ref } from "vue";
import debounceFunction from 'debounce-fn'; import debounceFunction from 'debounce-fn';
import { NcSettingsSection, NcTextField, NcPasswordField, NcSelect } from "@nextcloud/vue" import { NcSettingsSection, NcTextField, NcPasswordField } from "@nextcloud/vue"
import { adminSettingsApi } from "./adminSettingsApi.js"; import { adminSettingsApi } from "./adminSettingsApi.js";
@ -27,53 +26,10 @@ let loading = ref(true);
let settings = ref({}); let settings = ref({});
const app_settings = [ const app_settings = [
{id: "filesystem_mountpoint_path", name: "Filesystem Mountpoint Path", type: "text" }, {id: "filesystem_mountpoint_path", name: "Filesystem Mountpoint Path"},
{id: "filesystem_snapshots_path", name: "Filesystem Snapshots Path", type: "text"}, {id: "filesystem_snapshots_path", name: "Filesystem Snapshots Path"},
{
id: "snapshot_naming_scheme",
name: "Snapshot Naming Scheme",
type: "select",
options: [
{
id: '',
label: 'None',
},
{
id: 'zfs-auto-snapshot',
label: 'zfs-auto-snapshot',
},
]
},
]; ];
const getAppSettingComponent = function(appSetting) {
if(appSetting.type === "select") {
return NcSelect;
} else {
if(appSetting?.sensitive) {
return NcPasswordField;
} else {
return NcTextField;
}
}
}
const getAppSettingProps = function(appSetting) {
if(appSetting.type === "select") {
return {
options: appSetting.options,
label: "label",
inputLabel: appSetting.name,
reduce: (option) => ( option.id )
};
} else {
return {
label: appSetting.name,
clearable: false,
};
}
}
adminSettingsApi.getAllSettings().then((result) => { adminSettingsApi.getAllSettings().then((result) => {
settings.value = result; settings.value = result;
loading.value = false; loading.value = false;