mirror of https://github.com/nextcloud/server.git
Merge pull request #35160 from nextcloud/artonge/feat/version_naming_backend
Allow to name a versionpull/36332/head
commit
2f3007205d
@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright Copyright (c) 2022 Louis Chmn <louis@chmn.me>
|
||||
*
|
||||
* @author Louis Chmn <louis@chmn.me>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Files_Versions\Db;
|
||||
|
||||
use JsonSerializable;
|
||||
|
||||
use OCP\AppFramework\Db\Entity;
|
||||
use OCP\DB\Types;
|
||||
|
||||
/**
|
||||
* @method int getFileId()
|
||||
* @method void setFileId(int $fileId)
|
||||
* @method int getTimestamp()
|
||||
* @method void setTimestamp(int $timestamp)
|
||||
* @method int getSize()
|
||||
* @method void setSize(int $size)
|
||||
* @method int getMimetype()
|
||||
* @method void setMimetype(int $mimetype)
|
||||
* @method array|null getMetadata()
|
||||
* @method void setMetadata(array $metadata)
|
||||
*/
|
||||
class VersionEntity extends Entity implements JsonSerializable {
|
||||
protected ?int $fileId = null;
|
||||
protected ?int $timestamp = null;
|
||||
protected ?int $size = null;
|
||||
protected ?int $mimetype = null;
|
||||
protected ?array $metadata = null;
|
||||
|
||||
public function __construct() {
|
||||
$this->addType('id', Types::INTEGER);
|
||||
$this->addType('file_id', Types::INTEGER);
|
||||
$this->addType('timestamp', Types::INTEGER);
|
||||
$this->addType('size', Types::INTEGER);
|
||||
$this->addType('mimetype', Types::INTEGER);
|
||||
$this->addType('metadata', Types::JSON);
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array {
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'file_id' => $this->fileId,
|
||||
'timestamp' => $this->timestamp,
|
||||
'size' => $this->size,
|
||||
'mimetype' => $this->mimetype,
|
||||
'metadata' => $this->metadata,
|
||||
];
|
||||
}
|
||||
|
||||
public function getLabel(): string {
|
||||
return $this->metadata['label'] ?? '';
|
||||
}
|
||||
|
||||
public function setLabel(string $label): void {
|
||||
$this->metadata['label'] = $label;
|
||||
$this->markFieldUpdated('metadata');
|
||||
}
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright Copyright (c) 2022 Louis Chmn <louis@chmn.me>
|
||||
*
|
||||
* @author Louis Chmn <louis@chmn.me>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Files_Versions\Db;
|
||||
|
||||
use OCA\Files_Versions\Db\VersionEntity;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\AppFramework\Db\QBMapper;
|
||||
use OCP\DB\IResult;
|
||||
|
||||
/**
|
||||
* @extends QBMapper<VersionEntity>
|
||||
*/
|
||||
class VersionsMapper extends QBMapper {
|
||||
public function __construct(IDBConnection $db) {
|
||||
parent::__construct($db, 'files_versions', VersionEntity::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return VersionEntity[]
|
||||
*/
|
||||
public function findAllVersionsForFileId(int $fileId): array {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
|
||||
$qb->select('*')
|
||||
->from($this->getTableName())
|
||||
->where($qb->expr()->eq('file_id', $qb->createNamedParameter($fileId)));
|
||||
|
||||
return $this->findEntities($qb);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return VersionEntity
|
||||
*/
|
||||
public function findCurrentVersionForFileId(int $fileId): VersionEntity {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
|
||||
$qb->select('*')
|
||||
->from($this->getTableName())
|
||||
->where($qb->expr()->eq('file_id', $qb->createNamedParameter($fileId)))
|
||||
->orderBy('timestamp', 'DESC')
|
||||
->setMaxResults(1);
|
||||
|
||||
return $this->findEntity($qb);
|
||||
}
|
||||
|
||||
public function findVersionForFileId(int $fileId, int $timestamp): VersionEntity {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
|
||||
$qb->select('*')
|
||||
->from($this->getTableName())
|
||||
->where($qb->expr()->eq('file_id', $qb->createNamedParameter($fileId)))
|
||||
->andWhere($qb->expr()->eq('timestamp', $qb->createNamedParameter($timestamp)));
|
||||
|
||||
return $this->findEntity($qb);
|
||||
}
|
||||
|
||||
public function deleteAllVersionsForFileId(int $fileId): int {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
|
||||
return $qb->delete($this->getTableName())
|
||||
->where($qb->expr()->eq('file_id', $qb->createNamedParameter($fileId)))
|
||||
->executeStatement();
|
||||
}
|
||||
}
|
@ -1,142 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @copyright Copyright (c) 2016, ownCloud, Inc.
|
||||
*
|
||||
* @author Bart Visscher <bartv@thisnet.nl>
|
||||
* @author Björn Schießle <bjoern@schiessle.org>
|
||||
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
|
||||
* @author Morris Jobke <hey@morrisjobke.de>
|
||||
* @author Robin Appelman <robin@icewind.nl>
|
||||
* @author Robin McCorkell <robin@mccorkell.me.uk>
|
||||
* @author Sam Tuke <mail@samtuke.com>
|
||||
*
|
||||
* @license AGPL-3.0
|
||||
*
|
||||
* This code is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License, version 3,
|
||||
* as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License, version 3,
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
*/
|
||||
namespace OCA\Files_Versions;
|
||||
|
||||
use OC\Files\Filesystem;
|
||||
use OC\Files\Mount\MoveableMount;
|
||||
use OC\Files\View;
|
||||
use OCP\Util;
|
||||
|
||||
class Hooks {
|
||||
public static function connectHooks() {
|
||||
// Listen to write signals
|
||||
Util::connectHook('OC_Filesystem', 'write', Hooks::class, 'write_hook');
|
||||
// Listen to delete and rename signals
|
||||
Util::connectHook('OC_Filesystem', 'post_delete', Hooks::class, 'remove_hook');
|
||||
Util::connectHook('OC_Filesystem', 'delete', Hooks::class, 'pre_remove_hook');
|
||||
Util::connectHook('OC_Filesystem', 'post_rename', Hooks::class, 'rename_hook');
|
||||
Util::connectHook('OC_Filesystem', 'post_copy', Hooks::class, 'copy_hook');
|
||||
Util::connectHook('OC_Filesystem', 'rename', Hooks::class, 'pre_renameOrCopy_hook');
|
||||
Util::connectHook('OC_Filesystem', 'copy', Hooks::class, 'pre_renameOrCopy_hook');
|
||||
}
|
||||
|
||||
/**
|
||||
* listen to write event.
|
||||
*/
|
||||
public static function write_hook(array $params): void {
|
||||
$path = $params[Filesystem::signal_param_path];
|
||||
if ($path !== '') {
|
||||
Storage::store($path);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Erase versions of deleted file
|
||||
* @param array $params
|
||||
*
|
||||
* This function is connected to the delete signal of OC_Filesystem
|
||||
* cleanup the versions directory if the actual file gets deleted
|
||||
*/
|
||||
public static function remove_hook(array $params): void {
|
||||
$path = $params[Filesystem::signal_param_path];
|
||||
if ($path !== '') {
|
||||
Storage::delete($path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* mark file as "deleted" so that we can clean up the versions if the file is gone
|
||||
* @param array $params
|
||||
*/
|
||||
public static function pre_remove_hook(array $params): void {
|
||||
$path = $params[Filesystem::signal_param_path];
|
||||
if ($path !== '') {
|
||||
Storage::markDeletedFile($path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* rename/move versions of renamed/moved files
|
||||
* @param array $params array with oldpath and newpath
|
||||
*
|
||||
* This function is connected to the rename signal of OC_Filesystem and adjust the name and location
|
||||
* of the stored versions along the actual file
|
||||
*/
|
||||
public static function rename_hook(array $params): void {
|
||||
$oldpath = $params['oldpath'];
|
||||
$newpath = $params['newpath'];
|
||||
if ($oldpath !== '' && $newpath !== '') {
|
||||
Storage::renameOrCopy($oldpath, $newpath, 'rename');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* copy versions of copied files
|
||||
* @param array $params array with oldpath and newpath
|
||||
*
|
||||
* This function is connected to the copy signal of OC_Filesystem and copies the
|
||||
* the stored versions to the new location
|
||||
*/
|
||||
public static function copy_hook(array $params): void {
|
||||
$oldpath = $params['oldpath'];
|
||||
$newpath = $params['newpath'];
|
||||
if ($oldpath !== '' && $newpath !== '') {
|
||||
Storage::renameOrCopy($oldpath, $newpath, 'copy');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember owner and the owner path of the source file.
|
||||
* If the file already exists, then it was a upload of a existing file
|
||||
* over the web interface and we call Storage::store() directly
|
||||
*
|
||||
* @param array $params array with oldpath and newpath
|
||||
*
|
||||
*/
|
||||
public static function pre_renameOrCopy_hook(array $params): void {
|
||||
// if we rename a movable mount point, then the versions don't have
|
||||
// to be renamed
|
||||
$absOldPath = Filesystem::normalizePath('/' . \OC_User::getUser() . '/files' . $params['oldpath']);
|
||||
$manager = Filesystem::getMountManager();
|
||||
$mount = $manager->find($absOldPath);
|
||||
$internalPath = $mount->getInternalPath($absOldPath);
|
||||
if ($internalPath === '' and $mount instanceof MoveableMount) {
|
||||
return;
|
||||
}
|
||||
|
||||
$view = new View(\OC_User::getUser() . '/files');
|
||||
if ($view->file_exists($params['newpath'])) {
|
||||
Storage::store($params['newpath']);
|
||||
} else {
|
||||
Storage::setSourcePathAndUser($params['oldpath']);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,347 @@
|
||||
<?php
|
||||
/**
|
||||
* @copyright Copyright (c) 2016, ownCloud, Inc.
|
||||
*
|
||||
* @author Bart Visscher <bartv@thisnet.nl>
|
||||
* @author Björn Schießle <bjoern@schiessle.org>
|
||||
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
|
||||
* @author Morris Jobke <hey@morrisjobke.de>
|
||||
* @author Robin Appelman <robin@icewind.nl>
|
||||
* @author Robin McCorkell <robin@mccorkell.me.uk>
|
||||
* @author Sam Tuke <mail@samtuke.com>
|
||||
* @author Louis Chmn <louis@chmn.me>
|
||||
*
|
||||
* @license AGPL-3.0
|
||||
*
|
||||
* This code is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License, version 3,
|
||||
* as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License, version 3,
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
*/
|
||||
namespace OCA\Files_Versions\Listener;
|
||||
|
||||
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
|
||||
use OC\DB\Exceptions\DbalException;
|
||||
use OC\Files\Filesystem;
|
||||
use OC\Files\Mount\MoveableMount;
|
||||
use OC\Files\Node\NonExistingFile;
|
||||
use OC\Files\View;
|
||||
use OCA\Files_Versions\Db\VersionEntity;
|
||||
use OCA\Files_Versions\Db\VersionsMapper;
|
||||
use OCA\Files_Versions\Storage;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\EventDispatcher\Event;
|
||||
use OCP\EventDispatcher\IEventListener;
|
||||
use OCP\Files\Events\Node\BeforeNodeCopiedEvent;
|
||||
use OCP\Files\Events\Node\BeforeNodeDeletedEvent;
|
||||
use OCP\Files\Events\Node\BeforeNodeRenamedEvent;
|
||||
use OCP\Files\Events\Node\BeforeNodeTouchedEvent;
|
||||
use OCP\Files\Events\Node\BeforeNodeWrittenEvent;
|
||||
use OCP\Files\Events\Node\NodeCopiedEvent;
|
||||
use OCP\Files\Events\Node\NodeCreatedEvent;
|
||||
use OCP\Files\Events\Node\NodeDeletedEvent;
|
||||
use OCP\Files\Events\Node\NodeRenamedEvent;
|
||||
use OCP\Files\Events\Node\NodeTouchedEvent;
|
||||
use OCP\Files\Events\Node\NodeWrittenEvent;
|
||||
use OCP\Files\Folder;
|
||||
use OCP\Files\IMimeTypeLoader;
|
||||
use OCP\Files\IRootFolder;
|
||||
use OCP\Files\Node;
|
||||
|
||||
class FileEventsListener implements IEventListener {
|
||||
private IRootFolder $rootFolder;
|
||||
private VersionsMapper $versionsMapper;
|
||||
/**
|
||||
* @var array<int, array>
|
||||
*/
|
||||
private array $writeHookInfo = [];
|
||||
/**
|
||||
* @var array<int, Node>
|
||||
*/
|
||||
private array $nodesTouched = [];
|
||||
/**
|
||||
* @var array<string, Node>
|
||||
*/
|
||||
private array $versionsDeleted = [];
|
||||
private IMimeTypeLoader $mimeTypeLoader;
|
||||
|
||||
public function __construct(
|
||||
IRootFolder $rootFolder,
|
||||
VersionsMapper $versionsMapper,
|
||||
IMimeTypeLoader $mimeTypeLoader
|
||||
) {
|
||||
$this->rootFolder = $rootFolder;
|
||||
$this->versionsMapper = $versionsMapper;
|
||||
$this->mimeTypeLoader = $mimeTypeLoader;
|
||||
}
|
||||
|
||||
public function handle(Event $event): void {
|
||||
if ($event instanceof NodeCreatedEvent) {
|
||||
$this->created($event->getNode());
|
||||
}
|
||||
|
||||
if ($event instanceof BeforeNodeTouchedEvent) {
|
||||
$this->pre_touch_hook($event->getNode());
|
||||
}
|
||||
|
||||
if ($event instanceof NodeTouchedEvent) {
|
||||
$this->touch_hook($event->getNode());
|
||||
}
|
||||
|
||||
if ($event instanceof BeforeNodeWrittenEvent) {
|
||||
$this->write_hook($event->getNode());
|
||||
}
|
||||
|
||||
if ($event instanceof NodeWrittenEvent) {
|
||||
$this->post_write_hook($event->getNode());
|
||||
}
|
||||
|
||||
if ($event instanceof BeforeNodeDeletedEvent) {
|
||||
$this->pre_remove_hook($event->getNode());
|
||||
}
|
||||
|
||||
if ($event instanceof NodeDeletedEvent) {
|
||||
$this->remove_hook($event->getNode());
|
||||
}
|
||||
|
||||
if ($event instanceof NodeRenamedEvent) {
|
||||
$this->rename_hook($event->getSource(), $event->getTarget());
|
||||
}
|
||||
|
||||
if ($event instanceof NodeCopiedEvent) {
|
||||
$this->copy_hook($event->getSource(), $event->getTarget());
|
||||
}
|
||||
|
||||
if ($event instanceof BeforeNodeRenamedEvent) {
|
||||
$this->pre_renameOrCopy_hook($event->getSource(), $event->getTarget());
|
||||
}
|
||||
|
||||
if ($event instanceof BeforeNodeCopiedEvent) {
|
||||
$this->pre_renameOrCopy_hook($event->getSource(), $event->getTarget());
|
||||
}
|
||||
}
|
||||
|
||||
public function pre_touch_hook(Node $node): void {
|
||||
// Do not handle folders.
|
||||
if ($node instanceof Folder) {
|
||||
return;
|
||||
}
|
||||
|
||||
// $node is a non-existing on file creation.
|
||||
if ($node instanceof NonExistingFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->nodesTouched[$node->getId()] = $node;
|
||||
}
|
||||
|
||||
public function touch_hook(Node $node): void {
|
||||
$previousNode = $this->nodesTouched[$node->getId()] ?? null;
|
||||
|
||||
if ($previousNode === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
unset($this->nodesTouched[$node->getId()]);
|
||||
|
||||
try {
|
||||
// We update the timestamp of the version entity associated with the previousNode.
|
||||
$versionEntity = $this->versionsMapper->findVersionForFileId($previousNode->getId(), $previousNode->getMTime());
|
||||
// Create a version in the DB for the current content.
|
||||
$versionEntity->setTimestamp($node->getMTime());
|
||||
$this->versionsMapper->update($versionEntity);
|
||||
} catch (DbalException $ex) {
|
||||
// Ignore UniqueConstraintViolationException, as we are probably in the middle of a rollback
|
||||
// Where the previous node would temporary have the mtime of the old version, so the rollback touches it to fix it.
|
||||
if (!($ex->getPrevious() instanceof UniqueConstraintViolationException)) {
|
||||
throw $ex;
|
||||
}
|
||||
} catch (DoesNotExistException $ex) {
|
||||
// Ignore DoesNotExistException, as we are probably in the middle of a rollback
|
||||
// Where the previous node would temporary have a wrong mtime, so the rollback touches it to fix it.
|
||||
}
|
||||
}
|
||||
|
||||
public function created(Node $node): void {
|
||||
// Do not handle folders.
|
||||
if ($node instanceof Folder) {
|
||||
return;
|
||||
}
|
||||
|
||||
$versionEntity = new VersionEntity();
|
||||
$versionEntity->setFileId($node->getId());
|
||||
$versionEntity->setTimestamp($node->getMTime());
|
||||
$versionEntity->setSize($node->getSize());
|
||||
$versionEntity->setMimetype($this->mimeTypeLoader->getId($node->getMimetype()));
|
||||
$versionEntity->setMetadata([]);
|
||||
$this->versionsMapper->insert($versionEntity);
|
||||
}
|
||||
|
||||
/**
|
||||
* listen to write event.
|
||||
*/
|
||||
public function write_hook(Node $node): void {
|
||||
// Do not handle folders.
|
||||
if ($node instanceof Folder) {
|
||||
return;
|
||||
}
|
||||
|
||||
// $node is a non-existing on file creation.
|
||||
if ($node instanceof NonExistingFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
$path = $this->getPathForNode($node);
|
||||
$result = Storage::store($path);
|
||||
|
||||
// Store the result of the version creation so it can be used in post_write_hook.
|
||||
$this->writeHookInfo[$node->getId()] = [
|
||||
'previousNode' => $node,
|
||||
'versionCreated' => $result !== false
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* listen to post_write event.
|
||||
*/
|
||||
public function post_write_hook(Node $node): void {
|
||||
// Do not handle folders.
|
||||
if ($node instanceof Folder) {
|
||||
return;
|
||||
}
|
||||
|
||||
$writeHookInfo = $this->writeHookInfo[$node->getId()] ?? null;
|
||||
|
||||
if ($writeHookInfo === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($writeHookInfo['versionCreated'] && $node->getMTime() !== $writeHookInfo['previousNode']->getMTime()) {
|
||||
// If a new version was created, insert a version in the DB for the current content.
|
||||
// Unless both versions have the same mtime.
|
||||
$versionEntity = new VersionEntity();
|
||||
$versionEntity->setFileId($node->getId());
|
||||
$versionEntity->setTimestamp($node->getMTime());
|
||||
$versionEntity->setSize($node->getSize());
|
||||
$versionEntity->setMimetype($this->mimeTypeLoader->getId($node->getMimetype()));
|
||||
$versionEntity->setMetadata([]);
|
||||
$this->versionsMapper->insert($versionEntity);
|
||||
} else {
|
||||
// If no new version was stored in the FS, no new version should be added in the DB.
|
||||
// So we simply update the associated version.
|
||||
$currentVersionEntity = $this->versionsMapper->findVersionForFileId($node->getId(), $writeHookInfo['previousNode']->getMtime());
|
||||
$currentVersionEntity->setTimestamp($node->getMTime());
|
||||
$currentVersionEntity->setSize($node->getSize());
|
||||
$currentVersionEntity->setMimetype($this->mimeTypeLoader->getId($node->getMimetype()));
|
||||
$this->versionsMapper->update($currentVersionEntity);
|
||||
}
|
||||
|
||||
unset($this->writeHookInfo[$node->getId()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erase versions of deleted file
|
||||
*
|
||||
* This function is connected to the delete signal of OC_Filesystem
|
||||
* cleanup the versions directory if the actual file gets deleted
|
||||
*/
|
||||
public function remove_hook(Node $node): void {
|
||||
// Need to normalize the path as there is an issue with path concatenation in View.php::getAbsolutePath.
|
||||
$path = Filesystem::normalizePath($node->getPath());
|
||||
if (!array_key_exists($path, $this->versionsDeleted)) {
|
||||
return;
|
||||
}
|
||||
$node = $this->versionsDeleted[$path];
|
||||
$relativePath = $this->getPathForNode($node);
|
||||
unset($this->versionsDeleted[$path]);
|
||||
Storage::delete($relativePath);
|
||||
$this->versionsMapper->deleteAllVersionsForFileId($node->getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* mark file as "deleted" so that we can clean up the versions if the file is gone
|
||||
*/
|
||||
public function pre_remove_hook(Node $node): void {
|
||||
$path = $this->getPathForNode($node);
|
||||
Storage::markDeletedFile($path);
|
||||
$this->versionsDeleted[$node->getPath()] = $node;
|
||||
}
|
||||
|
||||
/**
|
||||
* rename/move versions of renamed/moved files
|
||||
*
|
||||
* This function is connected to the rename signal of OC_Filesystem and adjust the name and location
|
||||
* of the stored versions along the actual file
|
||||
*/
|
||||
public function rename_hook(Node $source, Node $target): void {
|
||||
$oldPath = $this->getPathForNode($source);
|
||||
$newPath = $this->getPathForNode($target);
|
||||
Storage::renameOrCopy($oldPath, $newPath, 'rename');
|
||||
}
|
||||
|
||||
/**
|
||||
* copy versions of copied files
|
||||
*
|
||||
* This function is connected to the copy signal of OC_Filesystem and copies the
|
||||
* the stored versions to the new location
|
||||
*/
|
||||
public function copy_hook(Node $source, Node $target): void {
|
||||
$oldPath = $this->getPathForNode($source);
|
||||
$newPath = $this->getPathForNode($target);
|
||||
Storage::renameOrCopy($oldPath, $newPath, 'copy');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember owner and the owner path of the source file.
|
||||
* If the file already exists, then it was a upload of a existing file
|
||||
* over the web interface and we call Storage::store() directly
|
||||
*
|
||||
*
|
||||
*/
|
||||
public function pre_renameOrCopy_hook(Node $source, Node $target): void {
|
||||
// if we rename a movable mount point, then the versions don't have
|
||||
// to be renamed
|
||||
$oldPath = $this->getPathForNode($source);
|
||||
$newPath = $this->getPathForNode($target);
|
||||
$absOldPath = Filesystem::normalizePath('/' . \OC_User::getUser() . '/files' . $oldPath);
|
||||
$manager = Filesystem::getMountManager();
|
||||
$mount = $manager->find($absOldPath);
|
||||
$internalPath = $mount->getInternalPath($absOldPath);
|
||||
if ($internalPath === '' and $mount instanceof MoveableMount) {
|
||||
return;
|
||||
}
|
||||
|
||||
$view = new View(\OC_User::getUser() . '/files');
|
||||
if ($view->file_exists($newPath)) {
|
||||
Storage::store($newPath);
|
||||
} else {
|
||||
Storage::setSourcePathAndUser($oldPath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the path relative to the current user root folder.
|
||||
* If no user is connected, use the node's owner.
|
||||
*/
|
||||
private function getPathForNode(Node $node): ?string {
|
||||
try {
|
||||
return $this->rootFolder
|
||||
->getUserFolder(\OC_User::getUser())
|
||||
->getRelativePath($node->getPath());
|
||||
} catch (\Throwable $ex) {
|
||||
return $this->rootFolder
|
||||
->getUserFolder($node->getOwner()->getUid())
|
||||
->getRelativePath($node->getPath());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright Copyright (c) 2022 Louis Chmn <louis@chmn.me>
|
||||
*
|
||||
* @author Louis Chmn <louis@chmn.me>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Files_Versions\Migration;
|
||||
|
||||
use Closure;
|
||||
use OCP\DB\ISchemaWrapper;
|
||||
use OCP\DB\Types;
|
||||
use OCP\Migration\IOutput;
|
||||
use OCP\Migration\SimpleMigrationStep;
|
||||
|
||||
/**
|
||||
* Auto-generated migration step: Please modify to your needs!
|
||||
*/
|
||||
class Version1020Date20221114144058 extends SimpleMigrationStep {
|
||||
/**
|
||||
* @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): ?ISchemaWrapper {
|
||||
/** @var ISchemaWrapper $schema */
|
||||
$schema = $schemaClosure();
|
||||
|
||||
if ($schema->hasTable("files_versions")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$table = $schema->createTable("files_versions");
|
||||
$table->addColumn('id', Types::BIGINT, [
|
||||
'autoincrement' => true,
|
||||
'notnull' => true,
|
||||
'length' => 20,
|
||||
]);
|
||||
$table->addColumn('file_id', Types::BIGINT, [
|
||||
'notnull' => true,
|
||||
'length' => 20,
|
||||
]);
|
||||
$table->addColumn('timestamp', Types::BIGINT, [
|
||||
'notnull' => true,
|
||||
'length' => 20,
|
||||
]);
|
||||
$table->addColumn('size', Types::BIGINT, [
|
||||
'notnull' => true,
|
||||
'length' => 20,
|
||||
]);
|
||||
$table->addColumn('mimetype', Types::BIGINT, [
|
||||
'notnull' => true,
|
||||
'length' => 20,
|
||||
]);
|
||||
$table->addColumn('metadata', Types::JSON, [
|
||||
'notnull' => true,
|
||||
]);
|
||||
|
||||
$table->setPrimaryKey(['id']);
|
||||
$table->addUniqueIndex(['file_id', 'timestamp'], 'files_versions_uniq_index');
|
||||
|
||||
return $schema;
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright Copyright (c) 2022 Louis Chmn <louis@chmn.me>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
namespace OCA\Files_Versions\Versions;
|
||||
|
||||
/**
|
||||
* @since 26.0.0
|
||||
*/
|
||||
interface IDeletableVersionBackend {
|
||||
/**
|
||||
* Delete a version.
|
||||
*
|
||||
* @since 26.0.0
|
||||
*/
|
||||
public function deleteVersion(IVersion $version): void;
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright Copyright (c) 2022 Louis Chmn <louis@chmn.me>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
namespace OCA\Files_Versions\Versions;
|
||||
|
||||
/**
|
||||
* @since 26.0.0
|
||||
*/
|
||||
interface INameableVersion {
|
||||
/**
|
||||
* Get the user created label
|
||||
*
|
||||
* @return string
|
||||
* @since 26.0.0
|
||||
*/
|
||||
public function getLabel(): string;
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright Copyright (c) 2022 Louis Chmn <louis@chmn.me>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
namespace OCA\Files_Versions\Versions;
|
||||
|
||||
/**
|
||||
* @since 26.0.0
|
||||
*/
|
||||
interface INameableVersionBackend {
|
||||
/**
|
||||
* Set the label for a version.
|
||||
*
|
||||
* @since 26.0.0
|
||||
*/
|
||||
public function setVersionLabel(IVersion $version, string $label): void;
|
||||
}
|
@ -0,0 +1,303 @@
|
||||
<!--
|
||||
- @copyright 2022 Carl Schwan <carl@carlschwan.eu>
|
||||
- @license AGPL-3.0-or-later
|
||||
-
|
||||
- This program is free software: you can redistribute it and/or modify
|
||||
- it under the terms of the GNU Affero General Public License as
|
||||
- published by the Free Software Foundation, either version 3 of the
|
||||
- License, or (at your option) any later version.
|
||||
-
|
||||
- This program is distributed in the hope that it will be useful,
|
||||
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
- GNU Affero General Public License for more details.
|
||||
-
|
||||
- You should have received a copy of the GNU Affero General Public License
|
||||
- along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<template>
|
||||
<div>
|
||||
<NcListItem class="version"
|
||||
:title="versionLabel"
|
||||
:href="downloadURL"
|
||||
:force-display-actions="true"
|
||||
data-files-versions-version>
|
||||
<template #icon>
|
||||
<img lazy="true"
|
||||
:src="previewURL"
|
||||
alt=""
|
||||
height="256"
|
||||
width="256"
|
||||
class="version__image">
|
||||
</template>
|
||||
<template #subtitle>
|
||||
<div class="version__info">
|
||||
<span v-tooltip="formattedDate">{{ version.mtime | humanDateFromNow }}</span>
|
||||
<!-- Separate dot to improve alignement -->
|
||||
<span class="version__info__size">•</span>
|
||||
<span class="version__info__size">{{ version.size | humanReadableSize }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #actions>
|
||||
<NcActionButton v-if="capabilities.files.version_labeling === true"
|
||||
:close-after-click="true"
|
||||
@click="openVersionLabelModal">
|
||||
<template #icon>
|
||||
<Pencil :size="22" />
|
||||
</template>
|
||||
{{ version.label === '' ? t('files_versions', 'Name this version') : t('files_versions', 'Edit version name') }}
|
||||
</NcActionButton>
|
||||
<NcActionButton v-if="!isCurrent"
|
||||
:close-after-click="true"
|
||||
@click="restoreVersion">
|
||||
<template #icon>
|
||||
<BackupRestore :size="22" />
|
||||
</template>
|
||||
{{ t('files_versions', 'Restore version') }}
|
||||
</NcActionButton>
|
||||
<NcActionLink :href="downloadURL"
|
||||
:close-after-click="true"
|
||||
:download="downloadURL">
|
||||
<template #icon>
|
||||
<Download :size="22" />
|
||||
</template>
|
||||
{{ t('files_versions', 'Download version') }}
|
||||
</NcActionLink>
|
||||
<NcActionButton v-if="!isCurrent && capabilities.files.version_deletion === true"
|
||||
:close-after-click="true"
|
||||
@click="deleteVersion">
|
||||
<template #icon>
|
||||
<Delete :size="22" />
|
||||
</template>
|
||||
{{ t('files_versions', 'Delete version') }}
|
||||
</NcActionButton>
|
||||
</template>
|
||||
</NcListItem>
|
||||
<NcModal v-if="showVersionLabelForm"
|
||||
:title="t('files_versions', 'Name this version')"
|
||||
@close="showVersionLabelForm = false">
|
||||
<form class="version-label-modal"
|
||||
@submit.prevent="setVersionLabel(formVersionLabelValue)">
|
||||
<label>
|
||||
<div class="version-label-modal__title">{{ t('photos', 'Version name') }}</div>
|
||||
<NcTextField ref="labelInput"
|
||||
:value.sync="formVersionLabelValue"
|
||||
:placeholder="t('photos', 'Version name')"
|
||||
:label-outside="true" />
|
||||
</label>
|
||||
|
||||
<div class="version-label-modal__info">
|
||||
{{ t('photos', 'Named versions are persisted, and excluded from automatic cleanups when your storage quota is full.') }}
|
||||
</div>
|
||||
|
||||
<div class="version-label-modal__actions">
|
||||
<NcButton :disabled="formVersionLabelValue.trim().length === 0" @click="setVersionLabel('')">
|
||||
{{ t('files_versions', 'Remove version name') }}
|
||||
</NcButton>
|
||||
<NcButton type="primary" native-type="submit">
|
||||
<template #icon>
|
||||
<Check />
|
||||
</template>
|
||||
{{ t('files_versions', 'Save version name') }}
|
||||
</NcButton>
|
||||
</div>
|
||||
</form>
|
||||
</NcModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BackupRestore from 'vue-material-design-icons/BackupRestore.vue'
|
||||
import Download from 'vue-material-design-icons/Download.vue'
|
||||
import Pencil from 'vue-material-design-icons/Pencil.vue'
|
||||
import Check from 'vue-material-design-icons/Check.vue'
|
||||
import Delete from 'vue-material-design-icons/Delete'
|
||||
import { NcActionButton, NcActionLink, NcListItem, NcModal, NcButton, NcTextField, Tooltip } from '@nextcloud/vue'
|
||||
import moment from '@nextcloud/moment'
|
||||
import { translate } from '@nextcloud/l10n'
|
||||
import { joinPaths } from '@nextcloud/paths'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
|
||||
export default {
|
||||
name: 'Version',
|
||||
components: {
|
||||
NcActionLink,
|
||||
NcActionButton,
|
||||
NcListItem,
|
||||
NcModal,
|
||||
NcButton,
|
||||
NcTextField,
|
||||
BackupRestore,
|
||||
Download,
|
||||
Pencil,
|
||||
Check,
|
||||
Delete,
|
||||
},
|
||||
directives: {
|
||||
tooltip: Tooltip,
|
||||
},
|
||||
filters: {
|
||||
/**
|
||||
* @param {number} bytes
|
||||
* @return {string}
|
||||
*/
|
||||
humanReadableSize(bytes) {
|
||||
return OC.Util.humanFileSize(bytes)
|
||||
},
|
||||
/**
|
||||
* @param {number} timestamp
|
||||
* @return {string}
|
||||
*/
|
||||
humanDateFromNow(timestamp) {
|
||||
return moment(timestamp).fromNow()
|
||||
},
|
||||
},
|
||||
props: {
|
||||
/** @type {Vue.PropOptions<import('../utils/versions.js').Version>} */
|
||||
version: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
fileInfo: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isCurrent: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isFirstVersion: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showVersionLabelForm: false,
|
||||
formVersionLabelValue: this.version.label,
|
||||
capabilities: loadState('core', 'capabilities', { files: { version_labeling: false, version_deletion: false } }),
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
versionLabel() {
|
||||
if (this.isCurrent) {
|
||||
if (this.version.label === undefined || this.version.label === '') {
|
||||
return translate('files_versions', 'Current version')
|
||||
} else {
|
||||
return `${this.version.label} (${translate('files_versions', 'Current version')})`
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isFirstVersion && this.version.label === '') {
|
||||
return translate('files_versions', 'Initial version')
|
||||
}
|
||||
|
||||
return this.version.label
|
||||
},
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
downloadURL() {
|
||||
if (this.isCurrent) {
|
||||
return joinPaths('/remote.php/webdav', this.fileInfo.path, this.fileInfo.name)
|
||||
} else {
|
||||
return this.version.url
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
previewURL() {
|
||||
if (this.isCurrent) {
|
||||
return generateUrl('/core/preview?fileId={fileId}&c={fileEtag}&x=250&y=250&forceIcon=0&a=0', {
|
||||
fileId: this.fileInfo.id,
|
||||
fileEtag: this.fileInfo.etag,
|
||||
})
|
||||
} else {
|
||||
return this.version.preview
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openVersionLabelModal() {
|
||||
this.showVersionLabelForm = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.labelInput.$el.getElementsByTagName('input')[0].focus()
|
||||
})
|
||||
},
|
||||
|
||||
restoreVersion() {
|
||||
this.$emit('restore', this.version)
|
||||
},
|
||||
|
||||
setVersionLabel(label) {
|
||||
this.formVersionLabelValue = label
|
||||
this.showVersionLabelForm = false
|
||||
this.$emit('label-update', this.version, label)
|
||||
},
|
||||
|
||||
deleteVersion() {
|
||||
this.$emit('delete', this.version)
|
||||
},
|
||||
|
||||
formattedDate() {
|
||||
return moment(this.version.mtime)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.version {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
&__info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
&__size {
|
||||
color: var(--color-text-lighter);
|
||||
}
|
||||
}
|
||||
|
||||
&__image {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-large);
|
||||
}
|
||||
}
|
||||
|
||||
.version-label-modal {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
height: 250px;
|
||||
padding: 16px;
|
||||
|
||||
&__title {
|
||||
margin-bottom: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__info {
|
||||
margin-top: 12px;
|
||||
color: var(--color-text-maxcontrast);
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 64px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,81 @@
|
||||
/**
|
||||
* @copyright Copyright (c) 2022 Louis Chemineau <louis@chmn.me>
|
||||
*
|
||||
* @author Louis Chemineau <louis@chmn.me>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
import path from "path"
|
||||
|
||||
export function uploadThreeVersions(user) {
|
||||
cy.uploadContent(user, new Blob(['v1'], { type: 'text/plain' }), 'text/plain', '/test.txt')
|
||||
cy.wait(1000)
|
||||
cy.uploadContent(user, new Blob(['v2'], { type: 'text/plain' }), 'text/plain', '/test.txt')
|
||||
cy.wait(1000)
|
||||
cy.uploadContent(user, new Blob(['v3'], { type: 'text/plain' }), 'text/plain', '/test.txt')
|
||||
cy.login(user)
|
||||
}
|
||||
|
||||
export function openVersionsPanel(fileName: string) {
|
||||
cy.get(`[data-file="${fileName}"]`).within(() => {
|
||||
cy.get('[data-action="menu"]')
|
||||
.click()
|
||||
|
||||
cy.get('.fileActionsMenu')
|
||||
.get('.action-details')
|
||||
.click()
|
||||
})
|
||||
|
||||
cy.get('#app-sidebar-vue')
|
||||
.get('[aria-controls="tab-version_vue"]')
|
||||
.click()
|
||||
|
||||
}
|
||||
|
||||
export function openVersionMenu(index: number) {
|
||||
cy.get('#tab-version_vue').within(() => {
|
||||
cy.get('[data-files-versions-version]')
|
||||
.eq(index).within(() => {
|
||||
cy.get('.action-item__menutoggle').filter(':visible')
|
||||
.click()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function clickPopperAction(actionName: string) {
|
||||
cy.get('.v-popper__popper').filter(':visible')
|
||||
.contains(actionName)
|
||||
.click()
|
||||
}
|
||||
|
||||
export function nameVersion(index: number, name: string) {
|
||||
openVersionMenu(index)
|
||||
clickPopperAction("Name this version")
|
||||
cy.get(':focused').type(`${name}{enter}`)
|
||||
}
|
||||
|
||||
export function assertVersionContent(index: number, expectedContent: string) {
|
||||
const downloadsFolder = Cypress.config('downloadsFolder')
|
||||
|
||||
openVersionMenu(index)
|
||||
clickPopperAction("Download version")
|
||||
|
||||
return cy.readFile(path.join(downloadsFolder, 'test.txt'))
|
||||
.then((versionContent) => expect(versionContent).to.equal(expectedContent))
|
||||
.then(() => cy.exec(`rm ${downloadsFolder}/test.txt`))
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
/**
|
||||
* @copyright Copyright (c) 2022 Louis Chmn <louis@chmn.me>
|
||||
*
|
||||
* @author Louis Chmn <louis@chmn.me>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
import { openVersionsPanel, uploadThreeVersions } from './filesVersionsUtils'
|
||||
|
||||
describe('Versions creation', () => {
|
||||
before(() => {
|
||||
cy.createRandomUser()
|
||||
.then((user) => {
|
||||
uploadThreeVersions(user)
|
||||
cy.login(user)
|
||||
cy.visit('/apps/files')
|
||||
openVersionsPanel('test.txt')
|
||||
})
|
||||
})
|
||||
|
||||
it('Opens the versions panel and sees the versions', () => {
|
||||
openVersionsPanel('test.txt')
|
||||
|
||||
cy.get('#tab-version_vue').within(() => {
|
||||
cy.get('[data-files-versions-version]').should('have.length', 3)
|
||||
cy.get('[data-files-versions-version]').eq(0).contains('Current version')
|
||||
cy.get('[data-files-versions-version]').eq(2).contains('Initial version')
|
||||
})
|
||||
})
|
||||
})
|
@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @copyright Copyright (c) 2022 Louis Chmn <louis@chmn.me>
|
||||
*
|
||||
* @author Louis Chmn <louis@chmn.me>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
import { assertVersionContent, openVersionsPanel, uploadThreeVersions } from './filesVersionsUtils'
|
||||
|
||||
describe('Versions download', () => {
|
||||
before(() => {
|
||||
cy.createRandomUser()
|
||||
.then((user) => {
|
||||
uploadThreeVersions(user)
|
||||
cy.login(user)
|
||||
cy.visit('/apps/files')
|
||||
openVersionsPanel('test.txt')
|
||||
})
|
||||
})
|
||||
|
||||
it('Download versions and assert there content', () => {
|
||||
assertVersionContent(0, 'v3')
|
||||
assertVersionContent(1, 'v2')
|
||||
assertVersionContent(2, 'v1')
|
||||
})
|
||||
})
|
@ -0,0 +1,65 @@
|
||||
/**
|
||||
* @copyright Copyright (c) 2022 Louis Chmn <louis@chmn.me>
|
||||
*
|
||||
* @author Louis Chmn <louis@chmn.me>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
import { assertVersionContent, nameVersion, openVersionsPanel, uploadThreeVersions } from './filesVersionsUtils'
|
||||
|
||||
describe('Versions expiration', () => {
|
||||
beforeEach(() => {
|
||||
cy.createRandomUser()
|
||||
.then((user) => {
|
||||
uploadThreeVersions(user)
|
||||
cy.login(user)
|
||||
cy.visit('/apps/files')
|
||||
openVersionsPanel('test.txt')
|
||||
})
|
||||
})
|
||||
|
||||
it('Expire all versions', () => {
|
||||
cy.runOccCommand('versions:expire')
|
||||
cy.visit('/apps/files')
|
||||
openVersionsPanel('test.txt')
|
||||
|
||||
cy.get('#tab-version_vue').within(() => {
|
||||
cy.get('[data-files-versions-version]').should('have.length', 1)
|
||||
cy.get('[data-files-versions-version]').eq(0).contains('Current version')
|
||||
})
|
||||
|
||||
assertVersionContent(0, 'v3')
|
||||
})
|
||||
|
||||
it('Expire versions v2', () => {
|
||||
nameVersion(2, 'v1')
|
||||
|
||||
cy.runOccCommand('versions:expire')
|
||||
cy.visit('/apps/files')
|
||||
openVersionsPanel('test.txt')
|
||||
|
||||
cy.get('#tab-version_vue').within(() => {
|
||||
cy.get('[data-files-versions-version]').should('have.length', 2)
|
||||
cy.get('[data-files-versions-version]').eq(0).contains('Current version')
|
||||
cy.get('[data-files-versions-version]').eq(1).contains('v1')
|
||||
})
|
||||
|
||||
assertVersionContent(0, 'v3')
|
||||
assertVersionContent(1, 'v1')
|
||||
})
|
||||
})
|
@ -0,0 +1,57 @@
|
||||
/**
|
||||
* @copyright Copyright (c) 2022 Louis Chmn <louis@chmn.me>
|
||||
*
|
||||
* @author Louis Chmn <louis@chmn.me>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
import { nameVersion, openVersionsPanel, uploadThreeVersions } from './filesVersionsUtils'
|
||||
|
||||
describe('Versions naming', () => {
|
||||
before(() => {
|
||||
cy.createRandomUser()
|
||||
.then((user) => {
|
||||
uploadThreeVersions(user)
|
||||
cy.login(user)
|
||||
cy.visit('/apps/files')
|
||||
openVersionsPanel('test.txt')
|
||||
})
|
||||
})
|
||||
|
||||
it('Names the initial version as v1', () => {
|
||||
nameVersion(2, 'v1')
|
||||
cy.get('#tab-version_vue').within(() => {
|
||||
cy.get('[data-files-versions-version]').eq(2).contains('v1')
|
||||
cy.get('[data-files-versions-version]').eq(2).contains('Initial version').should('not.exist')
|
||||
})
|
||||
})
|
||||
|
||||
it('Names the second version as v2', () => {
|
||||
nameVersion(1, 'v2')
|
||||
cy.get('#tab-version_vue').within(() => {
|
||||
cy.get('[data-files-versions-version]').eq(1).contains('v2')
|
||||
})
|
||||
})
|
||||
|
||||
it('Names the current version as v3', () => {
|
||||
nameVersion(0, 'v3')
|
||||
cy.get('#tab-version_vue').within(() => {
|
||||
cy.get('[data-files-versions-version]').eq(0).contains('v3 (Current version)')
|
||||
})
|
||||
})
|
||||
})
|
@ -0,0 +1,55 @@
|
||||
/**
|
||||
* @copyright Copyright (c) 2022 Louis Chmn <louis@chmn.me>
|
||||
*
|
||||
* @author Louis Chmn <louis@chmn.me>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
import { assertVersionContent, clickPopperAction, openVersionMenu, openVersionsPanel, uploadThreeVersions } from './filesVersionsUtils'
|
||||
|
||||
function restoreVersion(index: number) {
|
||||
openVersionMenu(index)
|
||||
clickPopperAction("Restore version")
|
||||
}
|
||||
|
||||
describe('Versions restoration', () => {
|
||||
before(() => {
|
||||
cy.createRandomUser()
|
||||
.then((user) => {
|
||||
uploadThreeVersions(user)
|
||||
cy.login(user)
|
||||
cy.visit('/apps/files')
|
||||
openVersionsPanel('test.txt')
|
||||
})
|
||||
})
|
||||
|
||||
it('Restores initial version', () => {
|
||||
restoreVersion(2)
|
||||
cy.get('#tab-version_vue').within(() => {
|
||||
cy.get('[data-files-versions-version]').should('have.length', 3)
|
||||
cy.get('[data-files-versions-version]').eq(0).contains('Current version')
|
||||
cy.get('[data-files-versions-version]').eq(2).contains('Initial version').should('not.exist')
|
||||
})
|
||||
})
|
||||
|
||||
it('Downloads versions and assert there content', () => {
|
||||
assertVersionContent(0, 'v1')
|
||||
assertVersionContent(1, 'v3')
|
||||
assertVersionContent(2, 'v2')
|
||||
})
|
||||
})
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue