diff --git a/apps/dav/lib/Meta/MetaFile.php b/apps/dav/lib/Meta/MetaFile.php index be48f5189e88..845777c702c2 100644 --- a/apps/dav/lib/Meta/MetaFile.php +++ b/apps/dav/lib/Meta/MetaFile.php @@ -2,7 +2,7 @@ /** * @author Thomas Müller * - * @copyright Copyright (c) 2017, ownCloud GmbH + * @copyright Copyright (c) 2022, ownCloud GmbH * @license AGPL-3.0 * * This code is free software: you can redistribute it and/or modify @@ -26,6 +26,7 @@ use OCA\DAV\Files\IProvidesAdditionalHeaders; use OCA\DAV\Files\IFileNode; use OCP\Files\IProvidesVersionAuthor; +use OCP\Files\IProvidesVersionTag; use OCP\Files\Node; use Sabre\DAV\File; @@ -128,9 +129,9 @@ public function getNode() { } /** - * @return string + * @inheritdoc */ - public function getVersionAuthor() : string { + public function getVersionEditedBy() : string { if ($this->file instanceof IProvidesVersionAuthor) { return $this->file->getEditedBy(); } @@ -138,14 +139,11 @@ public function getVersionAuthor() : string { } /** - * @return string + * @inheritdoc */ - public function getVersionAuthorName() : string { - if ($this->file instanceof IProvidesVersionAuthor) { - $uid = $this->file->getEditedBy(); - $manager = \OC::$server->getUserManager(); - $user = $manager->get($uid); - return $user !== null ? $user->getDisplayName() : ''; + public function getVersionTag() : string { + if ($this->file instanceof IProvidesVersionTag) { + return $this->file->getVersionTag(); } return ''; } diff --git a/apps/dav/lib/Meta/MetaFolder.php b/apps/dav/lib/Meta/MetaFolder.php index 02f3f7eaea4c..9a129943e61e 100644 --- a/apps/dav/lib/Meta/MetaFolder.php +++ b/apps/dav/lib/Meta/MetaFolder.php @@ -24,6 +24,8 @@ use OCP\Files\File; use OCP\Files\Folder; use OCP\Files\Node; +use OCP\Files\IProvidesVersionAuthor; +use OCP\Files\IProvidesVersionTag; use Sabre\DAV\Collection; /** @@ -71,4 +73,24 @@ private function nodeFactory(Node $node) { } throw new \InvalidArgumentException(); } + + /** + * @inheritdoc + */ + public function getVersionEditedBy() : string { + if ($this->folder instanceof IProvidesVersionAuthor) { + return $this->folder->getEditedBy(); + } + return ''; + } + + /** + * @inheritdoc + */ + public function getVersionTag() : string { + if ($this->folder instanceof IProvidesVersionTag) { + return $this->folder->getVersionTag(); + } + return ''; + } } diff --git a/apps/dav/lib/Meta/MetaPlugin.php b/apps/dav/lib/Meta/MetaPlugin.php index d6608faaa75a..760492b2b747 100644 --- a/apps/dav/lib/Meta/MetaPlugin.php +++ b/apps/dav/lib/Meta/MetaPlugin.php @@ -1,6 +1,8 @@ + * @author Jannik Stehle + * @author Piotr Mrowczynski * * @copyright Copyright (c) 2019, ownCloud GmbH * @license AGPL-3.0 @@ -33,7 +35,9 @@ class MetaPlugin extends ServerPlugin { public const PATH_FOR_FILEID_PROPERTYNAME = '{http://owncloud.org/ns}meta-path-for-user'; public const VERSION_EDITED_BY_PROPERTYNAME = '{http://owncloud.org/ns}meta-version-edited-by'; - public const VERSION_EDITED_BY_PROPERTYNAME_NAME = '{http://owncloud.org/ns}meta-version-edited-by-name'; + public const VERSION_EDITED_BY_NAME_PROPERTYNAME = '{http://owncloud.org/ns}meta-version-edited-by-name'; + public const VERSION_TAG_PROPERTYNAME = '{http://owncloud.org/ns}meta-version-tag'; + /** * Reference to main server object * @@ -98,12 +102,36 @@ public function handleGetProperties(PropFind $propFind, INode $node) { $file = \current($files); return $baseFolder->getRelativePath($file->getPath()); }); + $propFind->handle(self::VERSION_EDITED_BY_PROPERTYNAME, function () use ($node) { + return $node->getVersionEditedBy(); + }); + $propFind->handle(self::VERSION_EDITED_BY_NAME_PROPERTYNAME, function () use ($node) { + $versionEditedBy = $node->getVersionEditedBy(); + if (!$versionEditedBy) { + return ''; + } + $manager = \OC::$server->getUserManager(); // FIXME: not so good + $user = $manager->get($versionEditedBy); + return $user !== null ? $user->getDisplayName() : ''; + }); + $propFind->handle(self::VERSION_TAG_PROPERTYNAME, function () use ($node) { + return $node->getVersionTag(); + }); } elseif ($node instanceof MetaFile) { $propFind->handle(self::VERSION_EDITED_BY_PROPERTYNAME, function () use ($node) { - return $node->getVersionAuthor(); + return $node->getVersionEditedBy(); + }); + $propFind->handle(self::VERSION_EDITED_BY_NAME_PROPERTYNAME, function () use ($node) { + $versionEditedBy = $node->getVersionEditedBy(); + if (!$versionEditedBy) { + return ''; + } + $manager = \OC::$server->getUserManager(); // FIXME: not so good + $user = $manager->get($versionEditedBy); + return $user !== null ? $user->getDisplayName() : ''; }); - $propFind->handle(self::VERSION_EDITED_BY_PROPERTYNAME_NAME, function () use ($node) { - return $node->getVersionAuthorName(); + $propFind->handle(self::VERSION_TAG_PROPERTYNAME, function () use ($node) { + return $node->getVersionTag(); }); } } diff --git a/apps/files_trashbin/lib/Trashbin.php b/apps/files_trashbin/lib/Trashbin.php index 6651e87a4169..d0f388287de5 100644 --- a/apps/files_trashbin/lib/Trashbin.php +++ b/apps/files_trashbin/lib/Trashbin.php @@ -389,7 +389,7 @@ private static function retainVersions($filename, $owner, $ownerPath, $timestamp // Temporary $config = \OC::$server->getConfig(); - $metaEnabled = $config->getSystemValue('file_storage.save_version_author', false) === true; + $metaEnabled = ($config->getSystemValue('file_storage.save_version_metadata', false) === true); /** @var MetaStorage|null $metaStorage */ $metaStorage = null; @@ -413,35 +413,50 @@ private static function retainVersions($filename, $owner, $ownerPath, $timestamp $src = $owner . '/files_versions/' . $ownerPath; $dst = $owner . '/files_trashbin/versions/' . \basename($ownerPath) . '.d' . $timestamp; self::copy_recursive($src, $dst, $rootView); - if ($metaEnabled) { - $metaStorage->copyRecursiveMetaDataFiles('/files_versions/' . $ownerPath, $owner, '/files_trashbin/versions/' . \basename($ownerPath) . '.d' . $timestamp, $owner); - } } if (!$forceCopy) { $src = '/files_versions/' . $ownerPath; $dst ='/files_trashbin/versions/' . $filename . '.d' . $timestamp; self::move($rootView, "$owner$src", "$user$dst"); - if ($metaEnabled) { - $metaStorage->renameOrCopy('rename', $src . MetaStorage::VERSION_FILE_EXT, $owner, $dst . MetaStorage::VERSION_FILE_EXT, $user); - } } } elseif ($versions = \OCA\Files_Versions\Storage::getVersions($owner, $ownerPath)) { + // NOTE: move logic for versions metadata to versions storage (including current version logic for parent file) + + // copy version root metadata + if ($metaEnabled) { + if ($owner !== $user || $forceCopy) { + $src = '/files_versions/' . $ownerPath . MetaStorage::CURRENT_FILE_PREFIX . MetaStorage::VERSION_FILE_EXT; + $dst = '/files_trashbin/versions/' . \basename($ownerPath) . MetaStorage::CURRENT_FILE_PREFIX . '.d' . $timestamp . MetaStorage::VERSION_FILE_EXT ; + $metaStorage->renameOrCopy('copy', $src, $owner, $dst, $owner); + } + if (!$forceCopy) { + $src = '/files_versions/' . $ownerPath . MetaStorage::CURRENT_FILE_PREFIX . MetaStorage::VERSION_FILE_EXT; + $dst = '/files_trashbin/versions/' . $filename . MetaStorage::CURRENT_FILE_PREFIX . '.d' . $timestamp . MetaStorage::VERSION_FILE_EXT; + $metaStorage->renameOrCopy('rename', $src, $owner, $dst, $user); + } + } + foreach ($versions as $v) { if ($owner !== $user || $forceCopy) { + // copy version data $src = '/files_versions' . $v['path'] . '.v' . $v['version']; $dst = '/files_trashbin/versions/' . $v['name'] . '.v' . $v['version'] . '.d' . $timestamp; self::copy($rootView, "$owner$src", "$owner$dst"); + + // copy version metadata if ($metaEnabled) { $metaStorage->renameOrCopy('copy', $src . MetaStorage::VERSION_FILE_EXT, $owner, $dst . MetaStorage::VERSION_FILE_EXT, $owner); } } if (!$forceCopy) { + // copy version data $src = '/files_versions' . $v['path'] . '.v' . $v['version']; $dst = '/files_trashbin/versions/' . $filename . '.v' . $v['version'] . '.d' . $timestamp; self::move($rootView, "$owner$src", "$user$dst"); + + // copy version metadata if ($metaEnabled) { $metaStorage->renameOrCopy('rename', $src . MetaStorage::VERSION_FILE_EXT, $owner, $dst . MetaStorage::VERSION_FILE_EXT, $user); - ; } } } @@ -580,7 +595,7 @@ public static function restore($filename, $targetLocation = null) { \OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_restore', ['filePath' => Filesystem::normalizePath('/' . $targetLocation), 'trashPath' => Filesystem::normalizePath($filename)]); - self::restoreVersions($view, $filename, $targetLocation); + self::restoreVersionsFromTrashbin($view, $filename, $targetLocation); if ($timestamp) { $query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=? AND `id`=? AND `timestamp`=?'); @@ -602,7 +617,7 @@ public static function restore($filename, $targetLocation = null) { * @param string $location location where the file will be restored * @return false|null */ - private static function restoreVersions(View $view, $filename, $targetLocation) { + private static function restoreVersionsFromTrashbin(View $view, $filename, $targetLocation) { if (\OCP\App::isEnabled('files_versions')) { $user = User::getUser(); $rootView = new View('/'); @@ -618,7 +633,7 @@ private static function restoreVersions(View $view, $filename, $targetLocation) // Temporary $config = \OC::$server->getConfig(); - $metaEnabled = $config->getSystemValue('file_storage.save_version_author', false) === true; + $metaEnabled = ($config->getSystemValue('file_storage.save_version_metadata', false) === true); /** @var MetaStorage|null $metaStorage */ $metaStorage = null; @@ -642,6 +657,13 @@ private static function restoreVersions(View $view, $filename, $targetLocation) $filenameOnlyWithoutTimestamp = $filenameOnly; $dirAndFilename = "{$dir}/{$filenameOnly}"; } + + if ($metaEnabled && $timestamp) { + $src = '/files_trashbin/versions/' . $dirAndFilename . MetaStorage::CURRENT_FILE_PREFIX . '.d' . $timestamp; + $dst = '/files_versions/' . $ownerPath . MetaStorage::CURRENT_FILE_PREFIX; + $metaStorage->renameOrCopy('rename', $src . MetaStorage::VERSION_FILE_EXT, $user, $dst . MetaStorage::VERSION_FILE_EXT, $owner); + } + $versions = self::getVersionsFromTrash($filenameOnlyWithoutTimestamp, $timestamp, $user); foreach ($versions as $v) { if ($timestamp) { diff --git a/apps/files_versions/appinfo/routes.php b/apps/files_versions/appinfo/routes.php new file mode 100644 index 000000000000..4b8fb3329e20 --- /dev/null +++ b/apps/files_versions/appinfo/routes.php @@ -0,0 +1,36 @@ + + * + * @copyright Copyright (c) 2022, ownCloud GmbH + * @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 + * + */ + +$application = new \OCA\Files_Versions\AppInfo\Application(); + +$application->registerRoutes( + // @phan-suppress-next-line PhanUndeclaredThis + $this, + [ + 'routes' => [ + [ + 'name' => 'Version#publishVersion', + 'url' => '/publish-version', + 'verb' => 'POST' + ] + ] + ] +); diff --git a/apps/files_versions/css/versions.css b/apps/files_versions/css/versions.css index 7afd46f4c30e..e47b8094bef4 100644 --- a/apps/files_versions/css/versions.css +++ b/apps/files_versions/css/versions.css @@ -5,7 +5,7 @@ .versionsTabView li { width: 100%; cursor: default; - height: 56px; + height: 60px; float: left; border-bottom: 1px solid rgba(100,100,100,.1); } @@ -34,9 +34,13 @@ .versionsTabView .preview-container { display: inline-block; - vertical-align: top; + vertical-align: top; + text-align: center; } +.versionsTabView .preview-container { + padding: 5px; +} .versionsTabView img { cursor: pointer; padding-right: 4px; @@ -47,10 +51,13 @@ background-size: 32px; width: 32px; height: 32px; + display: block; + margin: 0 auto; } .versionsTabView .version-container { display: inline-block; + margin-top: 5px; } .versionsTabView .versiondate { @@ -58,6 +65,17 @@ vertical-align: super; } +.versionsTabView .versionstatus { + vertical-align: super; +} + +/* FIXME: we need to find better solution for this ref. https://github.com/owncloud/core/pull/40531#issuecomment-1420926550 */ +@media(max-width: 1200px) { + .versionsTabView .versionstatus { + display: none; + } +} + .versionsTabView .version-details { text-align: left; } @@ -66,8 +84,24 @@ padding: 0 10px; } +.versionsTabView .action-container { + display: inline; +} + .versionsTabView .revertVersion { cursor: pointer; float: right; margin-right: -10px; } + +.versionsTabView .publishVersion { + cursor: pointer; + float: right; + margin-right: -10px; +} +.versionsTabView .current-version .version-headline { + font-weight: bold; +} +.versionsTabView .current-version { + background: #f8f8f8; +} diff --git a/apps/files_versions/js/versioncollection.js b/apps/files_versions/js/versioncollection.js index 906750afac21..d0f1e70809fe 100644 --- a/apps/files_versions/js/versioncollection.js +++ b/apps/files_versions/js/versioncollection.js @@ -15,19 +15,23 @@ _.extend(OC.Files.Client, { PROPERTY_FILEID: '{' + OC.Files.Client.NS_OWNCLOUD + '}id', PROPERTY_VERSION_EDITED_BY: '{' + OC.Files.Client.NS_OWNCLOUD + '}meta-version-edited-by', - PROPERTY_VERSION_EDITED_BY_NAME: '{' + OC.Files.Client.NS_OWNCLOUD + '}meta-version-edited-by-name', + PROPERTY_VERSION_EDITED_BY_NAME: '{' + OC.Files.Client.NS_OWNCLOUD + '}meta-version-edited-by-name', + PROPERTY_VERSION_TAG: '{' + OC.Files.Client.NS_OWNCLOUD + '}meta-version-tag', }); /** + * Collection of noncurrent versions of a file + * * @memberof OCA.Versions */ var VersionCollection = OC.Backbone.Collection.extend({ sync: OC.Backbone.davSync, davProperties: { - 'meta-version-edited-by': OC.Files.Client.PROPERTY_VERSION_EDITED_BY, - 'meta-version-edited-by-name': OC.Files.Client.PROPERTY_VERSION_EDITED_BY_NAME, - 'id': OC.Files.Client.PROPERTY_FILEID, + 'meta-version-edited-by': OC.Files.Client.PROPERTY_VERSION_EDITED_BY, + 'meta-version-edited-by-name': OC.Files.Client.PROPERTY_VERSION_EDITED_BY_NAME, + 'meta-version-tag': OC.Files.Client.PROPERTY_VERSION_TAG, + 'id': OC.Files.Client.PROPERTY_FILEID, 'getlastmodified': OC.Files.Client.PROPERTY_GETLASTMODIFIED, 'getcontentlength': OC.Files.Client.PROPERTY_GETCONTENTLENGTH, 'resourcetype': OC.Files.Client.PROPERTY_RESOURCETYPE, @@ -68,8 +72,9 @@ size: version.getcontentlength, mimetype: version.getcontenttype, editedBy: version['meta-version-edited-by'], - editedByName: version['meta-version-edited-by-name'], - fileId: fileId + editedByName: version['meta-version-edited-by-name'], + fileId: fileId, + versionTag: version['meta-version-tag'] }; }); } diff --git a/apps/files_versions/js/versionmodel.js b/apps/files_versions/js/versionmodel.js index 02055b5f5c6b..2026b9c84968 100644 --- a/apps/files_versions/js/versionmodel.js +++ b/apps/files_versions/js/versionmodel.js @@ -9,7 +9,10 @@ */ (function() { + /** + * Noncurrent version in a collection + * * @memberof OCA.Versions */ var VersionModel = OC.Backbone.Model.extend({ diff --git a/apps/files_versions/js/versionsrootmodel.js b/apps/files_versions/js/versionsrootmodel.js new file mode 100644 index 000000000000..31d096b25a41 --- /dev/null +++ b/apps/files_versions/js/versionsrootmodel.js @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2022 + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +/* global moment */ + +(function() { + + _.extend(OC.Files.Client, { + PROPERTY_FILEID: '{' + OC.Files.Client.NS_OWNCLOUD + '}id', + PROPERTY_VERSION_EDITED_BY: '{' + OC.Files.Client.NS_OWNCLOUD + '}meta-version-edited-by', + PROPERTY_VERSION_EDITED_BY_NAME: '{' + OC.Files.Client.NS_OWNCLOUD + '}meta-version-edited-by-name', + PROPERTY_VERSION_TAG: '{' + OC.Files.Client.NS_OWNCLOUD + '}meta-version-tag', + }); + + /** + * Versions collection root - current file version + * + * @memberof OCA.Versions + */ + var VersionsRootModel = OC.Backbone.Model.extend({ + sync: OC.Backbone.davSync, + + davProperties: { + 'meta-version-edited-by': OC.Files.Client.PROPERTY_VERSION_EDITED_BY, + 'meta-version-edited-by-name': OC.Files.Client.PROPERTY_VERSION_EDITED_BY_NAME, + 'meta-version-tag': OC.Files.Client.PROPERTY_VERSION_TAG, + 'id': OC.Files.Client.PROPERTY_FILEID, + 'getlastmodified': OC.Files.Client.PROPERTY_GETLASTMODIFIED, + 'getcontentlength': OC.Files.Client.PROPERTY_GETCONTENTLENGTH, + 'resourcetype': OC.Files.Client.PROPERTY_RESOURCETYPE, + 'getcontenttype': OC.Files.Client.PROPERTY_GETCONTENTTYPE, + }, + + /** + * @var OCA.Files.FileInfoModel + */ + _fileInfo: null, + + setFileInfo: function(fileInfo) { + this._fileInfo = fileInfo; + }, + + getFileInfo: function() { + return this._fileInfo; + }, + + parse: function(version) { + var revision = version.id; + return { + id: revision, + name: revision, + fullPath: this._fileInfo.getFullPath(), + timestamp: moment(new Date(this._fileInfo.get('mtime'))).format('X'), + relativeTimestamp: moment(new Date(this._fileInfo.get('mtime'))).fromNow(), + size: this._fileInfo.get('size'), + mimetype: this._fileInfo.get('mimetype'), + editedBy: version['meta-version-edited-by'], + editedByName: version['meta-version-edited-by-name'], + fileId: this._fileInfo.get('id'), + versionTag: version['meta-version-tag'] + }; + }, + + url: function() { + // NOTE: same as OCA.Versions.VersionCollection but with depth=0 + return OC.linkToRemoteBase('dav') + '/meta/' + + encodeURIComponent(this._fileInfo.get('id')) + '/v'; + }, + + getFullPath: function() { + return this._fileInfo.getFullPath(); + }, + + getPreviewUrl: function() { + return OC.linkToRemoteBase('dav') + '/files/' + OC.getCurrentUser().uid + this._fileInfo.getFullPath() + '?preview=1'; + }, + + getDownloadUrl: function() { + return OC.linkToRemoteBase('dav') + '/files/' + OC.getCurrentUser().uid + this._fileInfo.getFullPath(); + }, + + /** + * Publish a tag for current version + */ + publish: function(options) { + var model = this; + $.post(OC.generateUrl('/apps/files_versions/publish-version'), { path: this.getFullPath() }) + .done(function() { + if (options.success) { + options.success.call(options.context, model, {}, options); + } + }) + .fail(function () { + if (options.error) { + options.error.call(options.context, model, {}, options); + } + }); + }, + }); + + OCA.Versions = OCA.Versions || {}; + + OCA.Versions.VersionsRootModel = VersionsRootModel; +})(); diff --git a/apps/files_versions/js/versionstabview.js b/apps/files_versions/js/versionstabview.js index 55788f88d81a..fd1a552a833a 100644 --- a/apps/files_versions/js/versionstabview.js +++ b/apps/files_versions/js/versionstabview.js @@ -24,29 +24,62 @@ return OC.MimeType.getIconUrl(mime); } - var TEMPLATE_ITEM = - '
  • ' + + var TEMPLATE_CURRENT = + '
  • ' + '
    ' + '
    ' + '' + + '{{versionTag}}' + '
    ' + '
    ' + + '' + + '{{#hasDetails}}' + + '
    ' + + '{{humanReadableSize}}' + + '{{editedByName}}' + + '
    ' + + '{{/hasDetails}}' + + '
    ' + + '
    ' + + '{{#canPublish}}' + + '' + + '{{/canPublish}}' + + '
    ' + + '
    ' + + '
  • '; + + var TEMPLATE_VERSION = + '
  • ' + '
    ' + + '
    ' + + '' + + '{{versionTag}}' + + '
    ' + + '
    ' + + '' + '{{#hasDetails}}' + '
    ' + '{{humanReadableSize}}' + - '{{editedByName}}' + + '{{editedByName}}' + '
    ' + '{{/hasDetails}}' + '
    ' + + '
    ' + '{{#canRevert}}' + '' + '{{/canRevert}}' + '
    ' + + '
    ' + '
  • '; var TEMPLATE = @@ -68,17 +101,24 @@ $versionsContainer: null, events: { - 'click .revertVersion': '_onClickRevertVersion' + 'click .revertVersion': '_onClickRevertVersion', + 'click .publishVersion': '_onClickPublishVersion' }, initialize: function() { OCA.Files.DetailTabView.prototype.initialize.apply(this, arguments); + + // versions collection root - current version + this.versionsRoot = new OCA.Versions.VersionsRootModel(); + this.versionsRoot.on('sync', this._onAddVersionsRootModel, this); + + // versions collection - noncurrent versions this.collection = new OCA.Versions.VersionCollection(); - this.collection.on('request', this._onRequest, this); - this.collection.on('sync', this._onEndRequest, this); + this.collection.on('request', this._onCollectionRequest, this); + this.collection.on('sync', this._onCollectionEndRequest, this); this.collection.on('update', this._onUpdate, this); this.collection.on('error', this._onError, this); - this.collection.on('add', this._onAddModel, this); + this.collection.on('add', this._onAddVersionModel, this); }, getLabel: function() { @@ -99,7 +139,7 @@ _onClickRevertVersion: function(ev) { var self = this; var $target = $(ev.target); - var fileInfoModel = this.collection.getFileInfo(); + var fileInfoModel = this.versionsRoot.getFileInfo(); var revision; if (!$target.is('li')) { $target = $target.closest('li'); @@ -113,6 +153,10 @@ success: function() { // reset and re-fetch the updated collection self.$versionsContainer.empty(); + + self.versionsRoot.setFileInfo(fileInfoModel); + self.versionsRoot.fetch(); + self.collection.setFileInfo(fileInfoModel); self.collection.reset([], {silent: true}); self.collection.fetch(); @@ -127,17 +171,29 @@ // temp dummy, until we can do a PROPFIND etag: versionModel.get('id') + versionModel.get('timestamp') }); + OC.Notification.show( + t('files_versions', + 'File {file} has been reverted and marked as new current version', + { + file: versionModel.getFullPath() + } + ), + { timeout: 7 } + ); }, error: function() { fileInfoModel.trigger('busy', fileInfoModel, false); self.$el.find('.versions').removeClass('hidden'); self._toggleLoading(false); - OC.Notification.show(t('files_versions', 'Failed to revert {file} to revision {timestamp}.', - { - file: versionModel.getFullPath(), - timestamp: OC.Util.formatDate(versionModel.get('timestamp') * 1000) - }), + OC.Notification.show( + t('files_versions', + 'Failed to revert {file} to revision {timestamp}.', + { + file: versionModel.getFullPath(), + timestamp: OC.Util.formatDate(versionModel.get('timestamp') * 1000) + } + ), { type: 'error' } @@ -150,26 +206,63 @@ fileInfoModel.trigger('busy', fileInfoModel, true); }, + _onClickPublishVersion: function(ev) { + var self = this; + var fileInfoModel = this.versionsRoot.getFileInfo(); + ev.preventDefault(); + + this.versionsRoot.publish({ + success: function() { + // reset and re-fetch the updated root for publishing metadata, + // and collection just in case new version got generated in the meanwhile by other process + self.$versionsContainer.empty(); + + self.versionsRoot.setFileInfo(fileInfoModel); + self.versionsRoot.fetch(); + + self.collection.setFileInfo(fileInfoModel); + self.collection.reset([], {silent: true}); + self.collection.fetch(); + + self.$el.find('.versions').removeClass('hidden'); + }, + error: function() { + self.$el.find('.versions').removeClass('hidden'); + self._toggleLoading(false); + OC.Notification.show(t('files_versions', 'Failed to publish version'),{type: 'error'}); + } + }); + }, + _toggleLoading: function(state) { this._loading = state; this.$el.find('.loading').toggleClass('hidden', !state); }, - _onRequest: function() { + _onCollectionRequest: function() { this._toggleLoading(true); }, - _onEndRequest: function() { + _onCollectionEndRequest: function() { this._toggleLoading(false); + this.$el.find('.empty').toggleClass('hidden', !!this.collection.length); }, - _onAddModel: function(model) { - var $el = $(this.itemTemplate(this._formatItem(model))); + _onAddVersionModel: function(model) { + // add version to the list (collection child) + var $el = $(this.versionTemplate(this._formatVersion(model))); this.$versionsContainer.append($el); $el.find('.has-tooltip').tooltip(); }, + _onAddVersionsRootModel: function(model) { + // add current version (versions root) as first item in the list + var $el = $(this.currentTemplate(this._formatCurrent(model))); + this.$versionsContainer.prepend($el); + $el.find('.has-tooltip').tooltip(); + }, + template: function(data) { if (!this._template) { this._template = Handlebars.compile(TEMPLATE); @@ -178,17 +271,29 @@ return this._template(data); }, - itemTemplate: function(data) { - if (!this._itemTemplate) { - this._itemTemplate = Handlebars.compile(TEMPLATE_ITEM); + versionTemplate: function(data) { + if (!this._versionTemplate) { + this._versionTemplate = Handlebars.compile(TEMPLATE_VERSION); + } + + return this._versionTemplate(data); + }, + + currentTemplate: function(data) { + if (!this._currentTemplate) { + this._currentTemplate = Handlebars.compile(TEMPLATE_CURRENT); } - return this._itemTemplate(data); + return this._currentTemplate(data); }, setFileInfo: function(fileInfo) { if (fileInfo) { this.render(); + + this.versionsRoot.setFileInfo(fileInfo); + this.versionsRoot.fetch(); + this.collection.setFileInfo(fileInfo); this.collection.reset([], {silent: true}); this.nextPage(); @@ -198,9 +303,10 @@ } }, - _formatItem: function(version) { + _formatVersion: function(version) { var timestamp = version.get('timestamp') * 1000; var size = version.has('size') ? version.get('size') : 0; + var isMajorVersion = version.has('versionTag') && version.get('versionTag').indexOf('.0', version.get('versionTag').length - '.0'.length) !== -1; return _.extend({ versionId: version.get('id'), @@ -216,16 +322,42 @@ revertLabel: t('files_versions', 'Restore'), canRevert: (this.collection.getFileInfo().get('permissions') & OC.PERMISSION_UPDATE) !== 0, editedBy: version.has('editedBy'), - editedByName: version.has('editedByName') + editedByName: version.has('editedByName'), + versionTag: version.has('versionTag'), + isMajorVersion: isMajorVersion, + majorVersionlabel: isMajorVersion ? t('files_versions', 'persistent') : '' }, version.attributes); }, + _formatCurrent: function(current) { + var size = current.has('size') ? current.get('size') : 0; + + return _.extend({ + versionId: current.get('id'), + formattedTimestamp: OC.Util.formatDate(current.get('mtime')), + relativeTimestamp: OC.Util.relativeModifiedDate(current.get('mtime')), + humanReadableSize: OC.Util.humanFileSize(size, true), + altSize: n('files', '%n byte', '%n bytes', size), + hasDetails: current.has('size'), + downloadUrl: current.getDownloadUrl(), + downloadIconUrl: OC.imagePath('core', 'actions/download'), + publishIconUrl: OC.imagePath('core', 'actions/checkmark'), + previewUrl: getPreviewUrl(current), + publishLabel: t('files_versions', 'Publish version'), + canPublish: current.has('versionTag') && (current.get('versionTag') !== ''), + editedBy: current.has('editedBy'), + editedByName: current.has('editedByName'), + versionTag: current.has('versionTag'), + currentVersionLabel: t('files_versions', 'current') + }, current.attributes); + }, + /** * Renders this details view */ render: function() { this.$el.html(this.template({ - emptyResultLabel: t('files_versions', 'No other versions available'), + emptyResultLabel: '', // not needed anymore as we always display current file })); this.$el.find('.has-tooltip').tooltip(); this.$versionsContainer = this.$el.find('ul.versions'); diff --git a/apps/files_versions/lib/AppInfo/Application.php b/apps/files_versions/lib/AppInfo/Application.php index dd2e6fc043c6..dbbd75f699bf 100644 --- a/apps/files_versions/lib/AppInfo/Application.php +++ b/apps/files_versions/lib/AppInfo/Application.php @@ -60,8 +60,7 @@ public function __construct(array $urlParams = []) { /** @var AllConfig $config */ $config = $container->query('ServerContainer')->getConfig(); - $metaEnabled = $config->getSystemValue('file_storage.save_version_author', false) === true; - + $metaEnabled = ($config->getSystemValue('file_storage.save_version_metadata', false) === true); if ($metaEnabled) { $container->registerService( MetaStorage::class, diff --git a/apps/files_versions/lib/Controller/VersionController.php b/apps/files_versions/lib/Controller/VersionController.php new file mode 100644 index 000000000000..e547f889acf9 --- /dev/null +++ b/apps/files_versions/lib/Controller/VersionController.php @@ -0,0 +1,73 @@ + + * @author Piotr Mrowczynski + * + * @copyright Copyright (c) 2022, ownCloud GmbH + * @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 + * + */ + +namespace OCA\Files_Versions\Controller; + +use OCA\Files_Versions\Storage; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\ILogger; +use OCP\IRequest; +use OCP\AppFramework\Http\JSONResponse; + +/** + * Class VersionController + * + * @package OCA\Files\Controller + */ +class VersionController extends Controller { + /** @var ILogger */ + private $logger; + + /** + * @param string $appName + * @param IRequest $request + * @param ILogger $logger + */ + public function __construct( + $appName, + IRequest $request, + ILogger $logger + ) { + parent::__construct($appName, $request); + $this->logger = $logger; + } + + /** + * Publish current version + * + * @NoAdminRequired + * + * @param string $path + * @return JSONResponse + */ + public function publishVersion($path) { + try { + Storage::publishCurrentVersion($path); + } catch (\Exception $e) { + $this->logger->logException($e, ['app' => 'files_versions']); + return new JSONResponse([], Http::STATUS_INTERNAL_SERVER_ERROR); + } + + return new JSONResponse(); + } +} diff --git a/apps/files_versions/lib/Hooks.php b/apps/files_versions/lib/Hooks.php index 607067b290a2..40a3128eb0c6 100644 --- a/apps/files_versions/lib/Hooks.php +++ b/apps/files_versions/lib/Hooks.php @@ -37,7 +37,10 @@ public static function connectHooks() { // Listen to write signals \OCP\Util::connectHook('OC_Filesystem', 'write', 'OCA\Files_Versions\Hooks', 'write_hook'); - if (\OC::$server->getConfig()->getSystemValue('file_storage.save_version_author', false) === true) { + $config = \OC::$server->getConfig(); + $metaEnabled = ($config->getSystemValue('file_storage.save_version_metadata', false) === true); + + if ($metaEnabled) { \OCP\Util::connectHook('OC_Filesystem', 'post_write', 'OCA\Files_Versions\Hooks', 'post_write_hook'); } @@ -72,7 +75,7 @@ public static function post_write_hook($params) { if (\OCP\App::isEnabled('files_versions')) { $path = $params[\OC\Files\Filesystem::signal_param_path]; if ($path<>'') { - Storage::storeMetaForCurrentFile($path); + Storage::postStore($path); } } } @@ -172,6 +175,7 @@ public static function pre_renameOrCopy_hook($params) { */ public static function onLoadFilesAppScripts() { \OCP\Util::addScript('files_versions', 'versionmodel'); + \OCP\Util::addScript('files_versions', 'versionsrootmodel'); \OCP\Util::addScript('files_versions', 'versioncollection'); \OCP\Util::addScript('files_versions', 'versionstabview'); \OCP\Util::addScript('files_versions', 'filesplugin'); diff --git a/apps/files_versions/lib/MetaStorage.php b/apps/files_versions/lib/MetaStorage.php index 9cffef240f47..e0d4644f67af 100644 --- a/apps/files_versions/lib/MetaStorage.php +++ b/apps/files_versions/lib/MetaStorage.php @@ -23,7 +23,6 @@ use OC\Files\Filesystem; use OC\Files\ObjectStore\ObjectStoreStorage; use OC\Files\View; -use OCA\DAV\Meta\MetaPlugin; use OCP\Files\FileInfo; /** @@ -53,8 +52,8 @@ * └── Hello.txt.v1638547177 # Content of the prev. version */ class MetaStorage { - /** @var string File-extension of the metadata of the current file */ - public const CURRENT_FILE_EXT = ".current.json"; + /** @var string File-prefix of the metadata of the current file */ + public const CURRENT_FILE_PREFIX = ".current"; /** @var string File-extension of the metadata file for a specific version */ public const VERSION_FILE_EXT = ".json"; @@ -83,13 +82,15 @@ public function __construct(string $dataDir, FileHelper $fileHelper) { /** * Creates or overwrites a metadata file for a current file. This is called - * after every modification of a file to store the last author. + * after every modification of a file to store some metadata. * * @param string $currentFileName Path relative to the current user's home + * @param string $uid + * @param bool $minor if true increases minor version, otherwise major * @return bool * @throws \Exception */ - public function createCurrent(string $currentFileName) : bool { + public function createForCurrent(string $currentFileName, string $uid, bool $minor) : bool { // if the file gets streamed we need to remove the .part extension // to get the right target $ext = \pathinfo($currentFileName, PATHINFO_EXTENSION); @@ -97,101 +98,73 @@ public function createCurrent(string $currentFileName) : bool { $currentFileName = \substr($currentFileName, 0, \strlen($currentFileName) - 5); } - // we don't support versioned directories - if (Filesystem::is_dir($currentFileName) || !Filesystem::file_exists($currentFileName)) { - return false; - } + $absPathOnDisk = $this->pathToAbsDiskPath($uid, "/files_versions/$currentFileName" . MetaStorage::CURRENT_FILE_PREFIX . MetaStorage::VERSION_FILE_EXT); + $userView = $this->fileHelper->getUserView($uid); + $this->fileHelper->createMissingDirectories($userView, $currentFileName); - list($owner, $fileName) = Storage::getUidAndFilename($currentFileName); + // get metadata for old current file (if exists) + $oldMetadata = $this->readMetaFile($absPathOnDisk); - $userPath = "/files_versions$fileName" . MetaStorage::CURRENT_FILE_EXT; + // generate Version Edited By property + $newVersionEditedBy = \OC_User::getUser(); - $author = \OC_User::getUser(); - $absPathOnDisk = $this->pathToAbsDiskPath($owner, $userPath); - $userView = $this->fileHelper->getUserView($owner); - $this->fileHelper->createMissingDirectories($userView, $fileName); + // generate Version Tag property + $oldVersionTag = $oldMetadata['version_tag'] ?? ''; + $newVersionTag = $this->incrementVersionTag($oldVersionTag, $minor); - return $this->writeMetaFile($author, $absPathOnDisk) !== false; + $metadata = [ + 'edited_by' => $newVersionEditedBy, + 'version_tag' => $newVersionTag + ]; + return $this->writeMetaFile($metadata, $absPathOnDisk) !== false; } /** - * Associates the current metadata of a file with a given version. - * - * After a version was created by making a copy before modification the - * current metadata becomes the metadata of the new version. + * Get a metadata file for a non-conncurent version of file. * - * @param string $currentFileName * @param FileInfo $versionFile - * @param string $uid + * @return array metadata + * @throws \Exception */ - public function moveCurrentToVersion(string $currentFileName, FileInfo $versionFile, string $uid) { - $currentMetaFile = $this->pathToAbsDiskPath($uid, "/files_versions/$currentFileName" . self::CURRENT_FILE_EXT); - $targetMetaFile = $this->dataDir . $versionFile->getPath() . self::VERSION_FILE_EXT; - - if (\file_exists($currentMetaFile)) { - @\rename($currentMetaFile, $targetMetaFile); + public function getVersionMetadata(FileInfo $versionFile) : array { + $absPathOnDisk = $this->dataDir . '/' . $versionFile->getPath() . MetaStorage::VERSION_FILE_EXT; + if (\file_exists($absPathOnDisk)) { + return $this->readMetaFile($absPathOnDisk); } + return []; } /** - * Creates a metadata-file for an existing version-file. Overwrites existing - * metadata. - * - * @param string $authorUid - * @param string $fileOwner - * @param FileInfo $version - */ - public function createForVersion(string $authorUid, string $fileOwner, FileInfo $version) { - $path = self::pathToAbsDiskPath($fileOwner, $version->getInternalPath()) . self::VERSION_FILE_EXT; - self::writeMetaFile($authorUid, $path); - } - - /** - * Retrieve the uid of the user that has authored a given version. + * Get a metadata file for a current file. * - * @param FileInfo $versionFile - * @return string|null null if no metadata is available + * @param string $currentFileName Path relative to the current user's home + * @param string $uid + * @return array metadata + * @throws \Exception */ - public function getAuthorUid(FileInfo $versionFile) : ?string { - $metaDataFilePath = $this->dataDir . '/' . $versionFile->getPath() . MetaStorage::VERSION_FILE_EXT; - if (\file_exists($metaDataFilePath)) { - return $this->getVersionAuthorByPath($metaDataFilePath); - } + public function getCurrentMetadata(string $currentFileName, string $uid) : array { + $absPathOnDisk = $this->pathToAbsDiskPath($uid, "/files_versions/$currentFileName" . MetaStorage::CURRENT_FILE_PREFIX . MetaStorage::VERSION_FILE_EXT); - return null; + return $this->readMetaFile($absPathOnDisk); } /** - * Retrieve the uid of the user that has authored the current version. + * Copies the current metadata of a file with a given version. * + * After a version was created by making a copy before modification the + * current metadata becomes the metadata of the new version. + * + * @param string $currentFileName + * @param FileInfo $versionFile * @param string $uid - * @param string $filename - * @return string|null null if no metadata is available */ - public function getCurrentVersionAuthorUid(string $uid, string $filename) : ?string { - $metaDataFilePath = $this->pathToAbsDiskPath($uid, "files_versions$filename" . self::CURRENT_FILE_EXT); - if (\file_exists($metaDataFilePath)) { - return $this->getVersionAuthorByPath($metaDataFilePath); - } - - return null; - } + public function copyCurrentToVersion(string $currentFileName, FileInfo $versionFile, string $uid) { + $currentMetaFile = $this->pathToAbsDiskPath($uid, "/files_versions/$currentFileName" . MetaStorage::CURRENT_FILE_PREFIX . MetaStorage::VERSION_FILE_EXT); + $targetMetaFile = $this->dataDir . $versionFile->getPath() . self::VERSION_FILE_EXT; - /** - * Retrieve the uid of a given version file path. Presumes that the file exists. - * - * @param string $path - * @return string|null - */ - protected function getVersionAuthorByPath(string $path) : ?string { - $json = \file_get_contents($path); - if ($decoded = \json_decode($json, true)) { - if (isset($decoded[MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME])) { - return $decoded[MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME]; - } + if (\file_exists($currentMetaFile)) { + @\copy($currentMetaFile, $targetMetaFile); } - - return null; } /** @@ -210,9 +183,9 @@ public function deleteForVersion(View $versionsView, string $versionPath) { * * @param string $filename Path to the file relative to files/ for which the current revision should be deleted */ - public function deleteCurrent(View $versionsView, string $filename) { + public function deleteForCurrent(View $versionsView, string $filename) { $uid = $versionsView->getOwner("/"); - $toDelete = $this->pathToAbsDiskPath($uid, "files_versions$filename" . self::CURRENT_FILE_EXT); + $toDelete = $this->pathToAbsDiskPath($uid, "files_versions$filename" . MetaStorage::CURRENT_FILE_PREFIX . MetaStorage::VERSION_FILE_EXT); if (\file_exists($toDelete)) { \unlink($toDelete); @@ -223,23 +196,32 @@ public function deleteCurrent(View $versionsView, string $filename) { * After a version is restored, the version's metadata is also restored * and becomes the current metadata of the file. * + * @param string $currentFileName + * @param FileInfo $restoreVersionFile * @param string $uid - * @param string $fileToRestore - * @param string $target */ - public function restore(string $uid, string $fileToRestore, string $target) { - $restoreDirName = \dirname($fileToRestore); - $restoreName = \basename($target); - - $src = self::pathToAbsDiskPath($uid, $fileToRestore) . self::VERSION_FILE_EXT; - $dst = self::pathToAbsDiskPath($uid, "$restoreDirName/$restoreName") . self::CURRENT_FILE_EXT; - - if (\file_exists($src)) { - \rename($src, $dst); - } elseif (\file_exists($dst)) { - // Remove current author file in case there is no author to restore - \unlink($dst); - } + public function restore(string $currentFileName, FileInfo $restoreVersionFile, string $uid) { + // NOTE: restoring a version just copies the contents to new current version, it does not affect past version + + // get current metafile + $currentMetaFile = $this->pathToAbsDiskPath($uid, "/files_versions/$currentFileName" . MetaStorage::CURRENT_FILE_PREFIX . MetaStorage::VERSION_FILE_EXT); + + // fetch metadata + $currentMetaData = $this->readMetaFile($currentMetaFile); + + // restored version is technically a new version generated by a user + $newVersionEditedBy = \OC_User::getUser(); + + // increment version tag for current meta + $oldCurrentVersionTag = $currentMetaData['version_tag'] ?? ''; + $newCurrentVersionTag = $this->incrementVersionTag($oldCurrentVersionTag, true); + + // create new currentMetaFile + $metadata = [ + 'edited_by' => $newVersionEditedBy, + 'version_tag' => $newCurrentVersionTag, + ]; + $this->writeMetaFile($metadata, $currentMetaFile); } /** @@ -303,6 +285,36 @@ public function isObjectStoreEnabled(): bool { return $this->objectStoreEnabled; } + /** + * Get the incremented version tag for a new version, which is + * latest version +0.1 or new major version. + * + * @param string $oldVersionTag + * @param bool $minor if true increases minor version, otherwise major + * @return string + */ + private function incrementVersionTag(string $oldVersionTag, bool $minor) : string { + if ($oldVersionTag === '') { + // initialize + return '0.1'; + } + + $versionParts = explode(".", $oldVersionTag); + $majorVersionPart = $versionParts[0]; + $minorVersionPart = $versionParts[1]; + if ($minor) { + // by default increase minor version + $newVersionTag = $majorVersionPart . '.' . \strval(((int)$minorVersionPart) + 1); + } elseif ($minorVersionPart !== '0') { + // increase major only when not already increased + $newVersionTag = \strval(((int)$majorVersionPart) + 1) . '.0'; + } else { + // just keep old tag + $newVersionTag = $oldVersionTag; + } + return $newVersionTag; + } + /** * Helper to convert relative vfs paths to absolute on disk paths * @@ -315,12 +327,46 @@ private function pathToAbsDiskPath(string $uid, string $path) : string { } /** - * @param string $authorUid + * @param array $metadata * @param string $diskPath - * @return false|int + * @return int|false */ - private function writeMetaFile(string $authorUid, string $diskPath) { - $metaJson = \json_encode([MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME => $authorUid]); + private function writeMetaFile(array $metadata, string $diskPath) { + $metaJson = \json_encode($metadata); return \file_put_contents($diskPath, $metaJson); } + + /** + * @param string $diskPath + * @return array metadata + */ + private function readMetaFile(string $diskPath) { + if (!\file_exists($diskPath)) { + return []; + } + + $json = \file_get_contents($diskPath); + if ($decoded = \json_decode($json, true)) { + $metadata = []; + + // handling for edited_by + if (isset($decoded['edited_by'])) { + $metadata['edited_by'] = $decoded['edited_by']; + } + + // LEGACY: property wrongly taken from DAV interface + // backwards compatibilitiy handling for edited_by which in the past had DAV property name + if (isset($decoded['{http://owncloud.org/ns}meta-version-edited-by'])) { + $metadata['edited_by'] = $decoded['{http://owncloud.org/ns}meta-version-edited-by']; + } + + // handling for version tags + if (isset($decoded['version_tag'])) { + $metadata['version_tag'] = $decoded['version_tag']; + } + return $metadata; + } + + return []; + } } diff --git a/apps/files_versions/lib/Storage.php b/apps/files_versions/lib/Storage.php index e8818c87c300..acf648f19a07 100644 --- a/apps/files_versions/lib/Storage.php +++ b/apps/files_versions/lib/Storage.php @@ -181,7 +181,7 @@ public static function store($filename) { return false; } - // Write metadata of the current file in case we have a new file + // no versions yet if (!Filesystem::file_exists($filename)) { return false; } @@ -228,7 +228,11 @@ public static function store($filename) { ]); if (self::metaEnabled()) { - self::$metaData->moveCurrentToVersion($filename, $fileInfo, $uid); + // version last current file metadata into noncurrent version + self::$metaData->copyCurrentToVersion($filename, $fileInfo, $uid); + + // create new current file metadata + self::$metaData->createForCurrent($filename, $uid, true); } } } @@ -239,9 +243,19 @@ public static function store($filename) { * @param string $filename * @throws \Exception */ - public static function storeMetaForCurrentFile(string $filename) { + public static function postStore(string $filename) { if (self::metaEnabled()) { - self::$metaData->createCurrent($filename); + // we don't support versioned directories + if (Filesystem::is_dir($filename) || !Filesystem::file_exists($filename)) { + return false; + } + + list($uid, $currentFileName) = self::getUidAndFilename($filename); + $versionMetadata = self::$metaData->getCurrentMetadata($currentFileName, $uid); + if (!$versionMetadata) { + // make sure metadata for current exists + self::$metaData->createForCurrent($currentFileName, $uid, true); + } } } @@ -256,6 +270,29 @@ public static function markDeletedFile($path) { 'filename' => $filename]; } + /** + * check whether verion can be expired + * + * @param View $view + * @param string $path + * @return bool + */ + protected static function isPublishedVersion($view, $path) { + if (self::metaEnabled()) { + $versionFileInfo = $view->getFileInfo($path); + if ($versionFileInfo) { + $versionMetadata = self::$metaData->getVersionMetadata($versionFileInfo); + + // we should not expire major versions (published workflow) + $versionTag = $versionMetadata['version_tag'] ?? ''; + if (\substr($versionTag, -\strlen('.0')) === '.0') { + return true; + } + } + } + return false; + } + /** * delete the version from the storage and cache * @@ -304,7 +341,7 @@ public static function delete($path) { } } if (self::metaEnabled()) { - self::$metaData->deleteCurrent($view, $filename); + self::$metaData->deleteForCurrent($view, $filename); } } unset(self::$deletedFiles[$path]); @@ -353,6 +390,15 @@ public static function renameOrCopy($sourcePath, $targetPath, $operation) { // create missing dirs if necessary self::getFileHelper()->createMissingDirectories(new View("/$targetOwner"), $targetPath); + if (self::metaEnabled()) { + // NOTE: we need to move current file first as in case of interuption lack of this file could cause issues + + // Also move/copy the current version + $src = '/files_versions/' . $sourcePath . MetaStorage::CURRENT_FILE_PREFIX . MetaStorage::VERSION_FILE_EXT; + $dst = '/files_versions/' . $targetPath . MetaStorage::CURRENT_FILE_PREFIX . MetaStorage::VERSION_FILE_EXT; + self::$metaData->renameOrCopy($operation, $src, $sourceOwner, $dst, $targetOwner); + } + foreach ($versions as $v) { // move each version one by one to the target directory $rootView->$operation( @@ -369,13 +415,6 @@ public static function renameOrCopy($sourcePath, $targetPath, $operation) { } } - if (self::metaEnabled()) { - // Also move/copy the current version - $src = '/files_versions/' . $sourcePath . MetaStorage::CURRENT_FILE_EXT; - $dst = '/files_versions/' . $targetPath . MetaStorage::CURRENT_FILE_EXT; - self::$metaData->renameOrCopy($operation, $src, $sourceOwner, $dst, $targetOwner); - } - // if we moved versions directly for a file, schedule expiration check for that file if (!$rootView->is_dir('/' . $targetOwner . '/files/' . $targetPath)) { self::scheduleExpire($targetOwner, $targetPath); @@ -393,7 +432,7 @@ public static function restoreVersion($uid, $filename, $fileToRestore, $revision $versionCreated = false; - //first create a new version + // first create a new version $version = 'files_versions'.$filename.'.v'.$users_view->filemtime('files'.$filename); if (!$users_view->file_exists($version)) { $users_view->copy('files'.$filename, $version); @@ -402,12 +441,7 @@ public static function restoreVersion($uid, $filename, $fileToRestore, $revision // create metadata for version if enabled if (self::metaEnabled()) { $versionFileInfo = $users_view->getFileInfo($version); - if ($versionFileInfo && !$versionFileInfo->getStorage()->instanceOfStorage(ObjectStoreStorage::class)) { - $versionAuthor = self::$metaData->getCurrentVersionAuthorUid($uid, $filename); - if ($versionAuthor) { - self::$metaData->createForVersion($versionAuthor, $uid, $versionFileInfo); - } - } + self::$metaData->copyCurrentToVersion($filename, $versionFileInfo, $uid); } } @@ -428,13 +462,16 @@ public static function restoreVersion($uid, $filename, $fileToRestore, $revision // rollback if (self::copyFileContents($users_view, $fileToRestore, 'files' . $filename)) { - $users_view->touch("/files$filename", $revision); - Storage::scheduleExpire($uid, $filename); + // restore/revert of versions is technically creating new file, thus increment mtime + $users_view->touch("/files$filename"); if (self::metaEnabled()) { - self::$metaData->restore($uid, $fileToRestore, 'files' . $filename); + $versionFileInfo = $users_view->getFileInfo('files_versions'.$filename.'.v'.$revision); + self::$metaData->restore($filename, $versionFileInfo, $uid); } + Storage::scheduleExpire($uid, $filename); + \OC_Hook::emit('\OCP\Versions', 'rollback', [ 'path' => $filename, 'user' => $uid, @@ -466,19 +503,14 @@ private static function copyFileContents($view, $path1, $path2) { $view->lockFile($path1, ILockingProvider::LOCK_EXCLUSIVE); $view->lockFile($path2, ILockingProvider::LOCK_EXCLUSIVE); - // TODO add a proper way of overwriting a file while maintaining file ids if ($storage1->instanceOfStorage('\OC\Files\ObjectStore\ObjectStoreStorage') || $storage2->instanceOfStorage('\OC\Files\ObjectStore\ObjectStoreStorage')) { $source = $storage1->fopen($internalPath1, 'r'); $target = $storage2->fopen($internalPath2, 'w'); list(, $result) = \OC_Helper::streamCopy($source, $target); \fclose($source); \fclose($target); - - if ($result !== false) { - $storage1->unlink($internalPath1); - } } else { - $result = $storage2->moveFromStorage($storage1, $internalPath1, $internalPath2); + $result = $storage2->copyFromStorage($storage1, $internalPath1, $internalPath2); } $view->unlockFile($path1, ILockingProvider::LOCK_EXCLUSIVE); @@ -488,7 +520,48 @@ private static function copyFileContents($view, $path1, $path2) { } /** - * get a list of all available versions of a file in descending chronological order + * get current version of the file + * @param string $uid user id from the owner of the file + * @param string $filename file to get versioning data for, relative to the user files dir + */ + public static function getCurrentVersion($uid, $filename) { + $version = []; + if ($filename === null || $filename === '') { + return $version; + } + + // add author information if the feature is enabled + if (self::metaEnabled()) { + // handle only allowed metadata values + $versionMetadata = self::$metaData->getCurrentMetadata($filename, $uid); + + $version['edited_by'] = $versionMetadata['edited_by'] ?? ''; + $version['version_tag'] = $versionMetadata['version_tag'] ?? ''; + } + + return $version; + } + + /** + * Publish the current version into major version + * that would persist the version long-term + */ + public static function publishCurrentVersion($filename) { + if (self::metaEnabled()) { + // we don't support versioned directories + if (Filesystem::is_dir($filename) || !Filesystem::file_exists($filename)) { + return false; + } + + list($uid, $currentFileName) = self::getUidAndFilename($filename); + + // overwrite current file metadata with minor=false to create new major version + self::$metaData->createForCurrent($currentFileName, $uid, false); + } + } + + /** + * get a list of all available noncurrent versions of a file in descending chronological order * @param string $uid user id from the owner of the file * @param string $filename file to find versions of, relative to the user files dir * @@ -515,7 +588,8 @@ public static function getVersions($uid, $filename) { if ($dirContent === false) { return $versions; } - + + // add historical versions if (\is_resource($dirContent)) { while (($entryName = \readdir($dirContent)) !== false) { if (!Filesystem::isIgnoredDir($entryName)) { @@ -525,7 +599,11 @@ public static function getVersions($uid, $filename) { $pathparts = \pathinfo($entryName); $timestamp = \substr($pathparts['extension'], 1); $filename = $pathparts['filename']; + + // ordering key $key = $timestamp . '#' . $filename; + + // add version info $versions[$key]['version'] = $timestamp; $versions[$key]['humanReadableTimestamp'] = self::getHumanReadableTimestamp($timestamp); $versions[$key]['preview'] = ''; @@ -537,14 +615,14 @@ public static function getVersions($uid, $filename) { $versions[$key]['storage_location'] = "$dir/$entryName"; $versions[$key]['owner'] = $uid; - // add author information if the feature is enabled + // add version meta info if (self::metaEnabled()) { $versionFileInfo = $view->getFileInfo("$dir/$entryName"); if ($versionFileInfo) { - $authorUid = self::$metaData->getAuthorUid($versionFileInfo); - if ($authorUid !== null) { - $versions[$key]['edited_by'] = $authorUid; - } + $versionMetadata = self::$metaData->getVersionMetadata($versionFileInfo); + + $versions[$key]['edited_by'] = $versionMetadata['edited_by'] ?? ''; + $versions[$key]['version_tag'] = $versionMetadata['version_tag'] ?? ''; } } } @@ -584,6 +662,9 @@ public static function expireOlderThanMaxForUser($uid) { $view = new View('/' . $uid . '/files_versions'); if (!empty($toDelete)) { foreach ($toDelete as $version) { + if (self::isPublishedVersion($view, $version['path'] . '.v' . $version['version'])) { + continue; + } $hookData = [ 'user' => $uid, 'path' => $version['path'] . '.v' . $version['version'], @@ -811,6 +892,9 @@ public static function expire($filename, $uid) { } foreach ($toDelete as $key => $path) { + if (self::isPublishedVersion($versionsFileview, $path)) { + continue; + } $versionInfo = self::getFileHelper()->getPathAndRevision($path); $hookData = [ 'user' => $uid, @@ -836,6 +920,10 @@ public static function expire($filename, $uid) { \reset($allVersions); while ($availableSpace < 0 && $i < $numOfVersions) { $version = \current($allVersions); + + if (self::isPublishedVersion($versionsFileview, $version['path'] . '.v' . $version['version'])) { + continue; + } $hookData = [ 'user' => $uid, 'path' => $version['path'].'.v'.$version['version'], diff --git a/apps/files_versions/tests/VersioningTest.php b/apps/files_versions/tests/VersioningTest.php index e1e35a911415..715a97e60e35 100644 --- a/apps/files_versions/tests/VersioningTest.php +++ b/apps/files_versions/tests/VersioningTest.php @@ -37,11 +37,12 @@ use OC\Files\ObjectStore\ObjectStoreStorage; use OC\Files\Storage\Temporary; -use OCA\DAV\Meta\MetaPlugin; use OCA\Files_Versions\FileHelper; use OCA\Files_Versions\MetaStorage; +use OCA\Files_Versions\Expiration; use OCP\Files\Storage; use OCP\IConfig; +use OCP\AppFramework\Utility\ITimeFactory; use PHPUnit\Framework\MockObject\MockObject; use Test\TestCase; @@ -136,23 +137,26 @@ protected function tearDown(): void { } /** - * Enables versioning metadata for unit-testing. Each test in this suite - * is executed once with and without versioning metadata enabled. + * Enables customizing system config for unit-testing. */ - private function overwriteConfig($saveVersionAuthor) { + private function overwriteConfig($saveVersionMetadata=false, $versionsRetentionObligation='auto') { $config = \OC::$server->getConfig(); $this->mockConfig = $this->createMock('\OCP\IConfig'); $this->mockConfig->expects($this->any()) ->method('getSystemValue') - ->will($this->returnCallback(function ($key, $default) use ($config, $saveVersionAuthor) { - if ($key === 'filesystem_check_changes') { - return \OC\Files\Cache\Watcher::CHECK_ONCE; - } elseif ($key === 'file_storage.save_version_author') { - return $saveVersionAuthor; - } else { - return $config->getSystemValue($key, $default); + ->will($this->returnCallback( + function ($key, $default) use ($config, $saveVersionMetadata, $versionsRetentionObligation) { + if ($key === 'filesystem_check_changes') { + return \OC\Files\Cache\Watcher::CHECK_ONCE; + } elseif ($key === 'file_storage.save_version_metadata') { + return $saveVersionMetadata; + } elseif ($key === 'versions_retention_obligation') { + return $versionsRetentionObligation; + } else { + return $config->getSystemValue($key, $default); + } } - })); + )); $this->overwriteService('AllConfig', $this->mockConfig); @@ -161,7 +165,7 @@ private function overwriteConfig($saveVersionAuthor) { \OC::registerShareHooks(); \OCA\Files_Versions\Storage::enableMetaData(null); - if ($saveVersionAuthor) { + if ($saveVersionMetadata) { \OCA\Files_Versions\Storage::enableMetaData(new MetaStorage($this->dataDir, new FileHelper())); } @@ -211,11 +215,13 @@ public function testMoveFileIntoSharedFolderAsRecipient(bool $metaDataEnabled) { if ($metaDataEnabled && !$this->objectStoreEnabled) { // one metadata file for each version - $m1 = $versionsFolder2 . '/test.txt.v' . $t1 . '.json'; - $m2 = $versionsFolder2 . '/test.txt.v' . $t2 . '.json'; + $m0 = $versionsFolder2 . '/test.txt' . MetaStorage::CURRENT_FILE_PREFIX . MetaStorage::VERSION_FILE_EXT; + $m1 = $versionsFolder2 . '/test.txt.v' . $t1 . MetaStorage::VERSION_FILE_EXT; + $m2 = $versionsFolder2 . '/test.txt.v' . $t2 . MetaStorage::VERSION_FILE_EXT; - \file_put_contents("$this->dataDir/$m1", \json_encode([MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME => $this->user2])); - \file_put_contents("$this->dataDir/$m2", \json_encode([MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME => $this->user2])); + \file_put_contents("$this->dataDir/$m0", \json_encode(['edited_by' => $this->user2])); + \file_put_contents("$this->dataDir/$m1", \json_encode(['edited_by' => $this->user2])); + \file_put_contents("$this->dataDir/$m2", \json_encode(['edited_by' => $this->user2])); } // move file into the shared folder as recipient @@ -225,6 +231,7 @@ public function testMoveFileIntoSharedFolderAsRecipient(bool $metaDataEnabled) { $this->assertFalse($this->rootView->file_exists($v2)); if ($metaDataEnabled && !$this->objectStoreEnabled) { + $this->assertFalse(\file_exists("$this->dataDir/$m0")); $this->assertFalse(\file_exists("$this->dataDir/$m1")); $this->assertFalse(\file_exists("$this->dataDir/$m2")); } @@ -240,9 +247,11 @@ public function testMoveFileIntoSharedFolderAsRecipient(bool $metaDataEnabled) { $this->assertTrue($this->rootView->file_exists($v2Renamed)); if ($metaDataEnabled && !$this->objectStoreEnabled) { - $m1Renamed = $versionsFolder1 . '/folder1/test.txt.v' . $t1 . '.json'; - $m2Renamed = $versionsFolder1 . '/folder1/test.txt.v' . $t2 . '.json'; + $m0Renamed = $versionsFolder1 . '/folder1/test.txt' . MetaStorage::CURRENT_FILE_PREFIX . MetaStorage::VERSION_FILE_EXT; + $m1Renamed = $versionsFolder1 . '/folder1/test.txt.v' . $t1 . MetaStorage::VERSION_FILE_EXT; + $m2Renamed = $versionsFolder1 . '/folder1/test.txt.v' . $t2 . MetaStorage::VERSION_FILE_EXT; + $this->assertTrue($this->rootView->file_exists($m0Renamed)); $this->assertTrue($this->rootView->file_exists($m1Renamed)); $this->assertTrue($this->rootView->file_exists($m2Renamed)); } @@ -250,17 +259,172 @@ public function testMoveFileIntoSharedFolderAsRecipient(bool $metaDataEnabled) { \OC::$server->getShareManager()->deleteShare($share); } + /** + * @dataProvider metaDataEnabledProvider + */ + public function testPublishCurrentVersion(bool $metaDataEnabled) { + $this->overwriteConfig($metaDataEnabled); + + \OC\Files\Filesystem::mkdir('folder1'); + \OC\Files\Filesystem::mkdir('folder2'); + \OC\Files\Filesystem::file_put_contents('folder1/test.txt', 'test file'); + + // create some versions + $this->rootView->mkdir($this->versionsRootOfUser1 . '/folder1'); + + if ($metaDataEnabled && !$this->objectStoreEnabled) { + $m0 = $this->versionsRootOfUser1 . '/folder1/test.txt' . MetaStorage::CURRENT_FILE_PREFIX . MetaStorage::VERSION_FILE_EXT; + \file_put_contents("$this->dataDir/$m0", \json_encode(['edited_by' => $this->user1, 'version_tag' => '1.1'])); + } + + $current0 = \OCA\Files_Versions\Storage::getCurrentVersion($this->user1, '/folder1/test.txt'); + + if ($metaDataEnabled && !$this->objectStoreEnabled) { + $this->assertEquals('1.1', $current0['version_tag']); + } else { + $this->assertEmpty($current0); + } + + \OCA\Files_Versions\Storage::publishCurrentVersion('/folder1/test.txt'); + + $current1 = \OCA\Files_Versions\Storage::getCurrentVersion($this->user1, '/folder1/test.txt'); + + if ($metaDataEnabled && !$this->objectStoreEnabled) { + $this->assertEquals('2.0', $current1['version_tag']); + } else { + $this->assertEmpty($current1); + } + } + + /** + * @dataProvider metaDataEnabledProvider + */ + public function testExpire(bool $metaDataEnabled) { + $this->overwriteConfig($metaDataEnabled); + + \OC\Files\Filesystem::file_put_contents('test.txt', 'test file'); + + // create some versions + $this->rootView->mkdir($this->versionsRootOfUser1); + + // create some versions + $t1 = \time(); + $t2 = $t1 - 1; // could be retained due to 1s rule + $t3 = $t1 - 2; + $t4 = $t1 - 4; + $t5 = $t1 - 5; // could be retained due to 1s rule + + $v1 = $this->versionsRootOfUser1 . '/test.txt.v' . $t1; + $v2 = $this->versionsRootOfUser1 . '/test.txt.v' . $t2; + $v3 = $this->versionsRootOfUser1 . '/test.txt.v' . $t3; + $v4 = $this->versionsRootOfUser1 . '/test.txt.v' . $t4; + $v5 = $this->versionsRootOfUser1 . '/test.txt.v' . $t5; + $this->rootView->file_put_contents($v1, 'version1'); + $this->rootView->file_put_contents($v2, 'version2'); + $this->rootView->file_put_contents($v3, 'version3'); + $this->rootView->file_put_contents($v4, 'version4'); + $this->rootView->file_put_contents($v5, 'version5'); + + if ($metaDataEnabled && !$this->objectStoreEnabled) { + // one metadata file for each version + $m0 = $this->versionsRootOfUser1 . '/test.txt' . MetaStorage::CURRENT_FILE_PREFIX . MetaStorage::VERSION_FILE_EXT; + $m1 = $this->versionsRootOfUser1 . '/test.txt.v' . $t1 . MetaStorage::VERSION_FILE_EXT; + $m2 = $this->versionsRootOfUser1 . '/test.txt.v' . $t2 . MetaStorage::VERSION_FILE_EXT; + $m3 = $this->versionsRootOfUser1 . '/test.txt.v' . $t3 . MetaStorage::VERSION_FILE_EXT; + $m4 = $this->versionsRootOfUser1 . '/test.txt.v' . $t4 . MetaStorage::VERSION_FILE_EXT; + $m5 = $this->versionsRootOfUser1 . '/test.txt.v' . $t5 . MetaStorage::VERSION_FILE_EXT; + \file_put_contents("$this->dataDir/$m0", \json_encode(['edited_by' => $this->user1, 'version_tag' => '1.2'])); + \file_put_contents("$this->dataDir/$m1", \json_encode(['edited_by' => $this->user1, 'version_tag' => '1.1'])); + \file_put_contents("$this->dataDir/$m2", \json_encode(['edited_by' => $this->user1, 'version_tag' => '1.0'])); // published + \file_put_contents("$this->dataDir/$m3", \json_encode(['edited_by' => $this->user1, 'version_tag' => '0.3'])); + \file_put_contents("$this->dataDir/$m4", \json_encode(['edited_by' => $this->user1, 'version_tag' => '0.2'])); + \file_put_contents("$this->dataDir/$m5", \json_encode(['edited_by' => $this->user1, 'version_tag' => '0.1'])); + } + + $versions = \OCA\Files_Versions\Storage::getVersions($this->user1, '/test.txt'); + $this->assertEquals(5, \count($versions)); + + \OCA\Files_Versions\Storage::expire('/test.txt', $this->user1); + + $versions = \OCA\Files_Versions\Storage::getVersions($this->user1, '/test.txt'); + + if ($metaDataEnabled && !$this->objectStoreEnabled) { + // one expires due to retention and being minor, + // and another one does not expire due to retention due to publishing + $this->assertEquals(4, \count($versions)); + } else { + $this->assertEquals(3, \count($versions)); + } + + $this->assertTrue($this->rootView->file_exists($v1)); + if ($metaDataEnabled && !$this->objectStoreEnabled) { + // due to being published + $this->assertTrue($this->rootView->file_exists($v2)); + } else { + $this->assertFalse($this->rootView->file_exists($v2)); + } + $this->assertTrue($this->rootView->file_exists($v3)); + $this->assertTrue($this->rootView->file_exists($v4)); + $this->assertFalse($this->rootView->file_exists($v5)); + } + + /** + * @dataProvider metaDataEnabledProvider + */ + public function testIsPublishedVersion(bool $metaDataEnabled) { + $this->overwriteConfig($metaDataEnabled); + + \OC\Files\Filesystem::mkdir('folder1'); + \OC\Files\Filesystem::mkdir('folder2'); + \OC\Files\Filesystem::file_put_contents('folder1/test.txt', 'test file'); + + $t1 = \time(); + // second version is 2 weeks old + $t2 = $t1 - 60 * 60 * 24 * 14; + // second version is 3 weeks old + $t2 = $t1 - 60 * 60 * 24 * 21; + + // create some versions + $this->rootView->mkdir($this->versionsRootOfUser1 . '/folder1'); + $v1 = $this->versionsRootOfUser1 . '/folder1/test.txt.v' . $t1; + $v2 = $this->versionsRootOfUser1 . '/folder1/test.txt.v' . $t2; + + $m0 = $this->versionsRootOfUser1 . '/folder1/test.txt' . MetaStorage::CURRENT_FILE_PREFIX . MetaStorage::VERSION_FILE_EXT; + $m1 = $v1 . MetaStorage::VERSION_FILE_EXT; + $m2 = $v2 . MetaStorage::VERSION_FILE_EXT; + + $this->rootView->file_put_contents($v1, 'version1'); + $this->rootView->file_put_contents($v2, 'version2'); + + if ($metaDataEnabled && !$this->objectStoreEnabled) { + \file_put_contents("$this->dataDir/$m0", \json_encode(['edited_by' => $this->user1, 'version_tag' => '1.1'])); + \file_put_contents("$this->dataDir/$m1", \json_encode(['edited_by' => $this->user1, 'version_tag' => '1.0'])); + \file_put_contents("$this->dataDir/$m2", \json_encode(['edited_by' => $this->user1, 'version_tag' => '0.1'])); + } + + $this->loginAsUser($this->user1); + + if ($metaDataEnabled && !$this->objectStoreEnabled) { + $this->assertTrue(VersionStorageToTest::callProtectedIsPublishedVersion($this->rootView, $this->versionsRootOfUser1 . '/folder1/test.txt.v' . $t1)); + $this->assertFalse(VersionStorageToTest::callProtectedIsPublishedVersion($this->rootView, $this->versionsRootOfUser1 . '/folder1/test.txt.v' . $t2)); + } else { + $this->assertFalse(VersionStorageToTest::callProtectedIsPublishedVersion($this->rootView, $this->versionsRootOfUser1 . '/folder1/test.txt.v' . $t1)); + $this->assertFalse(VersionStorageToTest::callProtectedIsPublishedVersion($this->rootView, $this->versionsRootOfUser1 . '/folder1/test.txt.v' . $t2)); + } + } + /** * @medium * test expire logic * @dataProvider versionsProvider */ public function testGetExpireList($versions, $sizeOfAllDeletedFiles) { + $this->overwriteConfig(); + // last interval end at 2592000 $startTime = 5000000; - $testClass = new VersionStorageToTest(); - list($deleted, $size) = $testClass->callProtectedGetExpireList($startTime, $versions); + list($deleted, $size) = VersionStorageToTest::callProtectedGetExpireList($startTime, $versions); // we should have deleted 16 files each of the size 1 $this->assertEquals($sizeOfAllDeletedFiles, $size); @@ -410,20 +574,23 @@ public function testRename(bool $metaDataEnabled) { // create some versions $v1 = $this->versionsRootOfUser1 . '/test.txt.v' . $t1; $v2 = $this->versionsRootOfUser1 . '/test.txt.v' . $t2; - $m1 = $this->versionsRootOfUser1 . '/test.txt.v' . $t1 . '.json'; - $m2 = $this->versionsRootOfUser1 . '/test.txt.v' . $t1 . '.json'; + $m0 = $this->versionsRootOfUser1 . '/test.txt' . MetaStorage::CURRENT_FILE_PREFIX . MetaStorage::VERSION_FILE_EXT; + $m1 = $this->versionsRootOfUser1 . '/test.txt.v' . $t1 . MetaStorage::VERSION_FILE_EXT; + $m2 = $this->versionsRootOfUser1 . '/test.txt.v' . $t1 . MetaStorage::VERSION_FILE_EXT; $v1Renamed = $this->versionsRootOfUser1 . '/test2.txt.v' . $t1; $v2Renamed = $this->versionsRootOfUser1 . '/test2.txt.v' . $t2; - $m1Renamed = $this->versionsRootOfUser1 . '/test2.txt.v' . $t1 . '.json'; - $m2Renamed = $this->versionsRootOfUser1 . '/test2.txt.v' . $t1 . '.json'; + $m0Renamed = $this->versionsRootOfUser1 . '/test2.txt' . MetaStorage::CURRENT_FILE_PREFIX . MetaStorage::VERSION_FILE_EXT; + $m1Renamed = $this->versionsRootOfUser1 . '/test2.txt.v' . $t1 . MetaStorage::VERSION_FILE_EXT; + $m2Renamed = $this->versionsRootOfUser1 . '/test2.txt.v' . $t1 . MetaStorage::VERSION_FILE_EXT; $this->rootView->file_put_contents($v1, 'version1'); $this->rootView->file_put_contents($v2, 'version2'); if ($metaDataEnabled && !$this->objectStoreEnabled) { - \file_put_contents("$this->dataDir/$m1", \json_encode([MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME => $this->user2])); - \file_put_contents("$this->dataDir/$m2", \json_encode([MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME => $this->user2])); + \file_put_contents("$this->dataDir/$m0", \json_encode(['edited_by' => $this->user2])); + \file_put_contents("$this->dataDir/$m1", \json_encode(['edited_by' => $this->user2])); + \file_put_contents("$this->dataDir/$m2", \json_encode(['edited_by' => $this->user2])); } // execute rename hook of versions app @@ -438,9 +605,11 @@ public function testRename(bool $metaDataEnabled) { $this->assertTrue($this->rootView->file_exists($v2Renamed)); if ($metaDataEnabled && !$this->objectStoreEnabled) { + $this->assertFalse(\file_exists("$this->dataDir/$m0")); $this->assertFalse(\file_exists("$this->dataDir/$m1")); $this->assertFalse(\file_exists("$this->dataDir/$m2")); + $this->assertTrue($this->rootView->file_exists($m0Renamed)); $this->assertTrue($this->rootView->file_exists($m1Renamed)); $this->assertTrue($this->rootView->file_exists($m2Renamed)); } @@ -465,20 +634,23 @@ public function testRenameInSharedFolder(bool $metaDataEnabled) { // create some versions $v1 = $this->versionsRootOfUser1 . '/folder1/test.txt.v' . $t1; $v2 = $this->versionsRootOfUser1 . '/folder1/test.txt.v' . $t2; - $m1 = $v1 . '.json'; - $m2 = $v2 . '.json'; + $m0 = $this->versionsRootOfUser1 . '/folder1/test.txt' . MetaStorage::CURRENT_FILE_PREFIX . MetaStorage::VERSION_FILE_EXT; + $m1 = $v1 . MetaStorage::VERSION_FILE_EXT; + $m2 = $v2 . MetaStorage::VERSION_FILE_EXT; $v1Renamed = $this->versionsRootOfUser1 . '/folder1/folder2/test.txt.v' . $t1; $v2Renamed = $this->versionsRootOfUser1 . '/folder1/folder2/test.txt.v' . $t2; - $m1Renamed = $v1Renamed . '.json'; - $m2Renamed = $v2Renamed . '.json'; + $m0Renamed = $this->versionsRootOfUser1 . '/folder1/folder2/test.txt' . MetaStorage::CURRENT_FILE_PREFIX . MetaStorage::VERSION_FILE_EXT; + $m1Renamed = $v1Renamed . MetaStorage::VERSION_FILE_EXT; + $m2Renamed = $v2Renamed . MetaStorage::VERSION_FILE_EXT; $this->rootView->file_put_contents($v1, 'version1'); $this->rootView->file_put_contents($v2, 'version2'); if ($metaDataEnabled && !$this->objectStoreEnabled) { - \file_put_contents("$this->dataDir/$m1", \json_encode([MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME => $this->user1])); - \file_put_contents("$this->dataDir/$m2", \json_encode([MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME => $this->user1])); + \file_put_contents("$this->dataDir/$m0", \json_encode(['edited_by' => $this->user1])); + \file_put_contents("$this->dataDir/$m1", \json_encode(['edited_by' => $this->user1])); + \file_put_contents("$this->dataDir/$m2", \json_encode(['edited_by' => $this->user1])); } $node = \OC::$server->getUserFolder($this->user1)->get('folder1'); @@ -508,9 +680,11 @@ public function testRenameInSharedFolder(bool $metaDataEnabled) { $this->assertTrue($this->rootView->file_exists($v2Renamed)); if ($metaDataEnabled && !$this->objectStoreEnabled) { + $this->assertFalse(\file_exists("$this->dataDir/$m0")); $this->assertFalse(\file_exists("$this->dataDir/$m1")); $this->assertFalse(\file_exists("$this->dataDir/$m2")); + $this->assertTrue($this->rootView->file_exists($m0Renamed)); $this->assertTrue($this->rootView->file_exists($m1Renamed)); $this->assertTrue($this->rootView->file_exists($m2Renamed)); } @@ -540,17 +714,20 @@ public function testMoveFolder(bool $metaDataEnabled) { $v1Renamed = $this->versionsRootOfUser1 . '/folder2/folder1/test.txt.v' . $t1; $v2Renamed = $this->versionsRootOfUser1 . '/folder2/folder1/test.txt.v' . $t2; - $m1 = $v1 . '.json'; - $m2 = $v2 . '.json'; - $m1Renamed = $v1Renamed . '.json'; - $m2Renamed = $v2Renamed . '.json'; + $m0 = $this->versionsRootOfUser1 . '/folder1/test.txt' . MetaStorage::CURRENT_FILE_PREFIX . MetaStorage::VERSION_FILE_EXT; + $m1 = $v1 . MetaStorage::VERSION_FILE_EXT; + $m2 = $v2 . MetaStorage::VERSION_FILE_EXT; + $m0Renamed = $this->versionsRootOfUser1 . '/folder2/folder1/test.txt' . MetaStorage::CURRENT_FILE_PREFIX . MetaStorage::VERSION_FILE_EXT; + $m1Renamed = $v1Renamed . MetaStorage::VERSION_FILE_EXT; + $m2Renamed = $v2Renamed . MetaStorage::VERSION_FILE_EXT; $this->rootView->file_put_contents($v1, 'version1'); $this->rootView->file_put_contents($v2, 'version2'); if ($metaDataEnabled && !$this->objectStoreEnabled) { - \file_put_contents("$this->dataDir/$m1", \json_encode([MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME => $this->user1])); - \file_put_contents("$this->dataDir/$m2", \json_encode([MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME => $this->user1])); + \file_put_contents("$this->dataDir/$m0", \json_encode(['edited_by' => $this->user1])); + \file_put_contents("$this->dataDir/$m1", \json_encode(['edited_by' => $this->user1])); + \file_put_contents("$this->dataDir/$m2", \json_encode(['edited_by' => $this->user1])); } // execute rename hook of versions app @@ -565,9 +742,11 @@ public function testMoveFolder(bool $metaDataEnabled) { $this->assertTrue($this->rootView->file_exists($v2Renamed)); if ($metaDataEnabled && !$this->objectStoreEnabled) { + $this->assertFalse(\file_exists("$this->dataDir/$m0")); $this->assertFalse(\file_exists("$this->dataDir/$m1")); $this->assertFalse(\file_exists("$this->dataDir/$m2")); + $this->assertTrue($this->rootView->file_exists($m0Renamed)); $this->assertTrue($this->rootView->file_exists($m1Renamed)); $this->assertTrue($this->rootView->file_exists($m2Renamed)); } @@ -608,10 +787,12 @@ public function testMoveFolderIntoSharedFolderAsRecipient(bool $metaDataEnabled) $v2 = $versionsFolder2 . '/folder2/test.txt.v' . $t2; if ($metaDataEnabled && !$this->objectStoreEnabled) { - $m1 = $v1 . '.json'; - $m2 = $v2 . '.json'; - \file_put_contents("$this->dataDir/$m1", \json_encode([MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME => $this->user2])); - \file_put_contents("$this->dataDir/$m2", \json_encode([MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME => $this->user2])); + $m0 = $versionsFolder2 . '/folder2/test.txt' . MetaStorage::CURRENT_FILE_PREFIX . MetaStorage::VERSION_FILE_EXT; + $m1 = $v1 . MetaStorage::VERSION_FILE_EXT; + $m2 = $v2 . MetaStorage::VERSION_FILE_EXT; + \file_put_contents("$this->dataDir/$m0", \json_encode(['edited_by' => $this->user2])); + \file_put_contents("$this->dataDir/$m1", \json_encode(['edited_by' => $this->user2])); + \file_put_contents("$this->dataDir/$m2", \json_encode(['edited_by' => $this->user2])); } $this->rootView->file_put_contents($v1, 'version1'); @@ -629,13 +810,15 @@ public function testMoveFolderIntoSharedFolderAsRecipient(bool $metaDataEnabled) $v1Renamed = $versionsFolder1 . '/folder1/folder2/test.txt.v' . $t1; $v2Renamed = $versionsFolder1 . '/folder1/folder2/test.txt.v' . $t2; - $m1Renamed = $v1Renamed . '.json'; - $m2Renamed = $v2Renamed . '.json'; + $m0Renamed = $versionsFolder1 . '/folder1/folder2/test.txt' . MetaStorage::CURRENT_FILE_PREFIX . MetaStorage::VERSION_FILE_EXT; + $m1Renamed = $v1Renamed . MetaStorage::VERSION_FILE_EXT; + $m2Renamed = $v2Renamed . MetaStorage::VERSION_FILE_EXT; $this->assertTrue($this->rootView->file_exists($v1Renamed)); $this->assertTrue($this->rootView->file_exists($v2Renamed)); if ($metaDataEnabled && !$this->objectStoreEnabled) { + $this->assertTrue($this->rootView->file_exists($m0Renamed)); $this->assertTrue($this->rootView->file_exists($m1Renamed)); $this->assertTrue($this->rootView->file_exists($m2Renamed)); } @@ -660,20 +843,24 @@ public function testRenameSharedFile(bool $metaDataEnabled) { // create some versions $v1 = $this->versionsRootOfUser1 . '/test.txt.v' . $t1; $v2 = $this->versionsRootOfUser1 . '/test.txt.v' . $t2; - $m1 = $v1 . '.json'; - $m2 = $v2 . '.json'; + + $m0 = $this->versionsRootOfUser1 . '/test.txt' . MetaStorage::CURRENT_FILE_PREFIX . MetaStorage::VERSION_FILE_EXT; + $m1 = $v1 . MetaStorage::VERSION_FILE_EXT; + $m2 = $v2 . MetaStorage::VERSION_FILE_EXT; // the renamed versions should not exist! Because we only moved the mount point! $v1Renamed = $this->versionsRootOfUser1 . '/test2.txt.v' . $t1; $v2Renamed = $this->versionsRootOfUser1 . '/test2.txt.v' . $t2; - $m1Renamed = $v1Renamed . '.json'; - $m2Renamed = $v2Renamed . '.json'; + $m0Renamed = $this->versionsRootOfUser1 . '/test2.txt' . MetaStorage::CURRENT_FILE_PREFIX . MetaStorage::VERSION_FILE_EXT; + $m1Renamed = $v1Renamed . MetaStorage::VERSION_FILE_EXT; + $m2Renamed = $v2Renamed . MetaStorage::VERSION_FILE_EXT; $this->rootView->file_put_contents($v1, 'version1'); $this->rootView->file_put_contents($v2, 'version2'); if ($metaDataEnabled && !$this->objectStoreEnabled) { - \file_put_contents("$this->dataDir/$m1", \json_encode([MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME => $this->user1])); - \file_put_contents("$this->dataDir/$m2", \json_encode([MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME => $this->user1])); + \file_put_contents("$this->dataDir/$m0", \json_encode(['edited_by' => $this->user1])); + \file_put_contents("$this->dataDir/$m1", \json_encode(['edited_by' => $this->user1])); + \file_put_contents("$this->dataDir/$m2", \json_encode(['edited_by' => $this->user1])); } $node = \OC::$server->getUserFolder($this->user1)->get('test.txt'); @@ -703,9 +890,11 @@ public function testRenameSharedFile(bool $metaDataEnabled) { $this->assertFalse($this->rootView->file_exists($v2Renamed)); if ($metaDataEnabled && !$this->objectStoreEnabled) { + $this->assertTrue(\file_exists("$this->dataDir/$m0")); $this->assertTrue(\file_exists("$this->dataDir/$m1")); $this->assertTrue(\file_exists("$this->dataDir/$m2")); + $this->assertFalse($this->rootView->file_exists($m0Renamed)); $this->assertFalse($this->rootView->file_exists($m1Renamed)); $this->assertFalse($this->rootView->file_exists($m2Renamed)); } @@ -731,17 +920,20 @@ public function testCopy(bool $metaDataEnabled) { $v1Copied = $this->versionsRootOfUser1 . '/test2.txt.v' . $t1; $v2Copied = $this->versionsRootOfUser1 . '/test2.txt.v' . $t2; - $m1 = $v1 . '.json'; - $m2 = $v2 . '.json'; - $m1Copied = $v1Copied . '.json'; - $m2Copied = $v2Copied . '.json'; + $m0 = $this->versionsRootOfUser1 . '/test.txt' . MetaStorage::CURRENT_FILE_PREFIX . MetaStorage::VERSION_FILE_EXT; + $m1 = $v1 . MetaStorage::VERSION_FILE_EXT; + $m2 = $v2 . MetaStorage::VERSION_FILE_EXT; + $m0Copied = $this->versionsRootOfUser1 . '/test2.txt' . MetaStorage::CURRENT_FILE_PREFIX . MetaStorage::VERSION_FILE_EXT; + $m1Copied = $v1Copied . MetaStorage::VERSION_FILE_EXT; + $m2Copied = $v2Copied . MetaStorage::VERSION_FILE_EXT; $this->rootView->file_put_contents($v1, 'version1'); $this->rootView->file_put_contents($v2, 'version2'); if ($metaDataEnabled && !$this->objectStoreEnabled) { - \file_put_contents("$this->dataDir/$m1", \json_encode([MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME => $this->user1])); - \file_put_contents("$this->dataDir/$m2", \json_encode([MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME => $this->user1])); + \file_put_contents("$this->dataDir/$m0", \json_encode(['edited_by' => $this->user1])); + \file_put_contents("$this->dataDir/$m1", \json_encode(['edited_by' => $this->user1])); + \file_put_contents("$this->dataDir/$m2", \json_encode(['edited_by' => $this->user1])); } // execute copy hook of versions app @@ -756,9 +948,11 @@ public function testCopy(bool $metaDataEnabled) { $this->assertTrue($this->rootView->file_exists($v2Copied)); if ($metaDataEnabled && !$this->objectStoreEnabled) { + $this->assertTrue(\file_exists("$this->dataDir/$m0")); $this->assertTrue(\file_exists("$this->dataDir/$m1")); $this->assertTrue(\file_exists("$this->dataDir/$m2")); + $this->assertTrue($this->rootView->file_exists($m0Copied)); $this->assertTrue($this->rootView->file_exists($m1Copied)); $this->assertTrue($this->rootView->file_exists($m2Copied)); } @@ -799,8 +993,9 @@ public function testGetVersions(string $filepath, bool $enableMetadata) { // create some versions $v1 = $this->versionsRootOfUser1 . $filepath . '.v' . $t1; $v2 = $this->versionsRootOfUser1 . $filepath . '.v' . $t2; - $m1 = $v1 . '.json'; - $m2 = $v2 . '.json'; + + $m1 = $v1 . MetaStorage::VERSION_FILE_EXT; + $m2 = $v2 . MetaStorage::VERSION_FILE_EXT; $this->rootView->mkdir($this->versionsRootOfUser1 . $parent); @@ -808,8 +1003,8 @@ public function testGetVersions(string $filepath, bool $enableMetadata) { $this->rootView->file_put_contents($v2, 'version2'); if ($enableMetadata && !$this->objectStoreEnabled) { - \file_put_contents("$this->dataDir/$m1", \json_encode([MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME => $this->user1])); - \file_put_contents("$this->dataDir/$m2", \json_encode([MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME => $this->user1])); + \file_put_contents("$this->dataDir/$m1", \json_encode(['edited_by' => $this->user1])); + \file_put_contents("$this->dataDir/$m2", \json_encode(['edited_by' => $this->user1])); } // execute copy hook of versions app @@ -930,7 +1125,7 @@ function ($p) use (&$params) { private function doTestRestore(bool $metaDataEnabled) { $filePath = $this->user1 . '/files/sub/test.txt'; $this->rootView->file_put_contents($filePath, 'test file'); - + $t0 = $this->rootView->filemtime($filePath); // not exactly the same timestamp as the file @@ -942,16 +1137,27 @@ private function doTestRestore(bool $metaDataEnabled) { $v1 = $this->versionsRootOfUser1 . '/sub/test.txt.v' . $t1; $v2 = $this->versionsRootOfUser1 . '/sub/test.txt.v' . $t2; - $m1 = $v1 . '.json'; - $m2 = $v2 . '.json'; + $m0 = $this->versionsRootOfUser1 . '/sub/test.txt' . MetaStorage::CURRENT_FILE_PREFIX . MetaStorage::VERSION_FILE_EXT; + $m1 = $v1 . MetaStorage::VERSION_FILE_EXT; + $m2 = $v2 . MetaStorage::VERSION_FILE_EXT; $this->rootView->mkdir($this->versionsRootOfUser1 . '/sub'); $this->rootView->file_put_contents($v1, 'version1'); $this->rootView->file_put_contents($v2, 'version2'); if ($metaDataEnabled && !$this->objectStoreEnabled) { - \file_put_contents("$this->dataDir/$m1", \json_encode([MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME => $this->user1])); - \file_put_contents("$this->dataDir/$m2", \json_encode([MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME => $this->user2])); + \file_put_contents("$this->dataDir/$m0", \json_encode([ + 'edited_by' => $this->user1, + 'version_tag' => '0.3' + ])); + \file_put_contents("$this->dataDir/$m1", \json_encode([ + 'edited_by' => $this->user1, + 'version_tag' => '0.2' + ])); + \file_put_contents("$this->dataDir/$m2", \json_encode([ + 'edited_by' => $this->user2, + 'version_tag' => '0.1' + ])); } $oldVersions = \OCA\Files_Versions\Storage::getVersions( @@ -968,6 +1174,10 @@ private function doTestRestore(bool $metaDataEnabled) { $this->connectMockHooks('rollback', $params); $v = $oldVersions["$t2#test.txt"]; + + // make sure mtime could increase before attempt to restore from past version to new file as we test for it + \sleep(1); + $this->assertTrue(\OCA\Files_Versions\Storage::restoreVersion($this->user1, $v['path'], $v['storage_location'], $t2)); $expectedParams = [ 'path' => '/sub/test.txt', @@ -981,8 +1191,8 @@ private function doTestRestore(bool $metaDataEnabled) { $info2 = $this->rootView->getFileInfo($filePath); $this->assertNotEquals( - $info2['etag'], $info1['etag'], + $info2['etag'], 'Etag must change after rolling back version' ); $this->assertEquals( @@ -990,10 +1200,10 @@ private function doTestRestore(bool $metaDataEnabled) { $info1['fileid'], 'File id must not change after rolling back version' ); - $this->assertEquals( + $this->assertNotEquals( $info2['mtime'], $t2, - 'Restored file must have mtime from version' + 'New version restored from past version must receive new mtime' ); $newVersions = \OCA\Files_Versions\Storage::getVersions( @@ -1003,22 +1213,26 @@ private function doTestRestore(bool $metaDataEnabled) { $this->assertTrue( $this->rootView->file_exists($this->versionsRootOfUser1 . '/sub/test.txt.v' . $t0), - 'A version file must be created for the file before restoration' + 'A version file must be created for the current file before restoration' ); $this->assertTrue( $this->rootView->file_exists($v1), - 'Untouched version file is still there' + 'Untouched version file must be present in files_version folder' ); - $this->assertFalse( + $this->assertTrue( $this->rootView->file_exists($v2), - 'Restored version file gone from files_version folder' + 'Version file from which restore has been done must be present in files_version folder' ); if ($metaDataEnabled && !$this->objectStoreEnabled) { + $this->assertTrue( + \file_exists("$this->dataDir/$this->versionsRootOfUser1/sub/test.txt" . MetaStorage::CURRENT_FILE_PREFIX . MetaStorage::VERSION_FILE_EXT), + 'A current version metadata-file must be present for the file' + ); $this->assertTrue( \file_exists("$this->dataDir/$this->versionsRootOfUser1/sub/test.txt.v$t0" . MetaStorage::VERSION_FILE_EXT), - 'A version metadata-file must be created for the file before restoration' + 'A noncurrent version metadata-file must be created for the file before restoration' ); $this->assertTrue( @@ -1026,13 +1240,13 @@ private function doTestRestore(bool $metaDataEnabled) { 'Untouched metadata-file is still there' ); - $this->assertFalse( + $this->assertTrue( \file_exists("$this->dataDir/$m2"), - 'Restored metadata file must be gone from files_version folder' + 'Version metadata file from which restore has been done must be present in files_version folder' ); } - $this->assertCount(2, $newVersions, 'Additional version created'); + $this->assertCount(3, $newVersions, 'Additional new version created for restoration from point in time'); $this->assertArrayHasKey( $t0 . '#' . 'test.txt', @@ -1042,13 +1256,34 @@ private function doTestRestore(bool $metaDataEnabled) { $this->assertArrayHasKey( $t1 . '#' . 'test.txt', $newVersions, - 'Untouched version is still there' + 'Untouched version metadata file must be present in files_version folder' ); - $this->assertArrayNotHasKey( + $this->assertArrayHasKey( $t2 . '#' . 'test.txt', $newVersions, - 'Restored version is not in the list any more' + 'Version metadata file from which restore has been done must be present in files_version folder' ); + + $currentVersion = \OCA\Files_Versions\Storage::getCurrentVersion( + $this->user1, + '/sub/test.txt' + ); + + if ($metaDataEnabled && !$this->objectStoreEnabled) { + // make sure versions tags are incremented correctly + // make sure restored version tags is preserved + $this->assertEquals($this->user1, $currentVersion['edited_by']); + $this->assertEquals('0.4', $currentVersion['version_tag']); + + $this->assertEquals($this->user1, $newVersions[$t0 . '#' . 'test.txt']['edited_by']); + $this->assertEquals('0.3', $newVersions[$t0 . '#' . 'test.txt']['version_tag']); + + $this->assertEquals($this->user1, $newVersions[$t1 . '#' . 'test.txt']['edited_by']); + $this->assertEquals('0.2', $newVersions[$t1 . '#' . 'test.txt']['version_tag']); + + $this->assertEquals($this->user2, $newVersions[$t2 . '#' . 'test.txt']['edited_by']); + $this->assertEquals('0.1', $newVersions[$t2 . '#' . 'test.txt']['version_tag']); + } } /** @@ -1185,10 +1420,19 @@ private function markTestSkippedIfStorageHasOwnVersioning() { // extend the original class to make it possible to test protected methods class VersionStorageToTest extends \OCA\Files_Versions\Storage { - /** - * @param integer $time + /* + * FIXME: this is workaround as it is imposible without refactor to mock config for Storage::getExpiration + * and test expiry methods as these are static */ - public function callProtectedGetExpireList($time, $versions) { + public static function callProtectedGetExpireList($time, $versions) { return self::getExpireList($time, $versions); } + + /* + * FIXME: this is workaround as it is imposible without refactor to mock config for Storage::getExpiration + * and test expiry methods as these are static + */ + public static function callProtectedIsPublishedVersion($view, $path) { + return self::isPublishedVersion($view, $path); + } } diff --git a/apps/files_versions/tests/js/versionstabviewSpec.js b/apps/files_versions/tests/js/versionstabviewSpec.js index a7301dbe473e..9de7fdb91bde 100644 --- a/apps/files_versions/tests/js/versionstabviewSpec.js +++ b/apps/files_versions/tests/js/versionstabviewSpec.js @@ -8,11 +8,12 @@ * */ describe('OCA.Versions.VersionsTabView', function() { + var VersionsRootModel = OCA.Versions.VersionsRootModel; var VersionCollection = OCA.Versions.VersionCollection; var VersionModel = OCA.Versions.VersionModel; var VersionsTabView = OCA.Versions.VersionsTabView; - var fetchStub, fileInfoModel, tabView, testVersions, clock; + var fetchRootModelStub, fetchCollectionStub, fileInfoModel, tabView, testVersions, clock; beforeEach(function() { clock = sinon.useFakeTimers(Date.UTC(2015, 6, 17, 1, 2, 0, 3)); @@ -38,7 +39,8 @@ describe('OCA.Versions.VersionsTabView', function() { testVersions = [version1, version2]; - fetchStub = sinon.stub(VersionCollection.prototype, 'fetch'); + fetchRootModelStub = sinon.stub(VersionsRootModel.prototype, 'fetch'); + fetchCollectionStub = sinon.stub(VersionCollection.prototype, 'fetch'); fileInfoModel = new OCA.Files.FileInfoModel({ id: 123, name: 'test.txt', @@ -49,7 +51,8 @@ describe('OCA.Versions.VersionsTabView', function() { }); afterEach(function() { - fetchStub.restore(); + fetchRootModelStub.restore(); + fetchCollectionStub.restore(); tabView.remove(); clock.restore(); }); @@ -57,7 +60,8 @@ describe('OCA.Versions.VersionsTabView', function() { describe('rendering', function() { it('reloads matching versions when setting file info model', function() { tabView.setFileInfo(fileInfoModel); - expect(fetchStub.calledOnce).toEqual(true); + expect(fetchRootModelStub.calledOnce).toEqual(true); + expect(fetchCollectionStub.calledOnce).toEqual(true); }); it('renders loading icon while fetching versions', function() { diff --git a/changelog/unreleased/40531 b/changelog/unreleased/40531 new file mode 100644 index 000000000000..eae5006df8b8 --- /dev/null +++ b/changelog/unreleased/40531 @@ -0,0 +1,11 @@ +Enhancement: Persistent major file version workflow + +- Restore operation logic changed. Now restore is creating new current version of the file from one of past noncurrent versions of the file. Current version also receives incremented mtime for the file, and author of the files is the user that restored the file. The old noncurrent version is no longer removed upon restore and current version no longer receives mtime of the version. +- The current version of the file is now shown in the Versions Tab, highlighted with "gray" background +- Versions now persist additional extended metadata on versioning tags, that allow easier identification of the versions. Each update increases minor version for the file. +Current version of the file now can be published, which increases major version tag. +- Each new edit of the file would create noncurrent versions. The ones tagged with major version due to publishing, will be persisted long term and wont be subject to any retention policies. +- Migrate from deprecated save_version_author to save_version_metadata + +https://github.com/owncloud/core/pull/40531 +https://github.com/owncloud/enterprise/issues/5286 diff --git a/config/config.sample.php b/config/config.sample.php index 87050edf937b..c5e6a60f5edd 100644 --- a/config/config.sample.php +++ b/config/config.sample.php @@ -715,11 +715,12 @@ 'versions_retention_obligation' => 'auto', /** - * Save and display the author of each version of uploaded and edited files. + * Save additional metadata (author, version tag, etc.) of each version of uploaded and edited files. * + * WARNING: This feature CANNOT be temporarily disabled once enabled. Consequitive enabling would require repair job that erases all extended versions metadata. * WARNING: This does not work for S3 storage backends. */ -'file_storage.save_version_author' => false, +'file_storage.save_version_metadata' => false, /** * ownCloud Verifications diff --git a/core/Migrations/Version20230210103154.php b/core/Migrations/Version20230210103154.php new file mode 100644 index 000000000000..02784c8196d9 --- /dev/null +++ b/core/Migrations/Version20230210103154.php @@ -0,0 +1,21 @@ +getSystemConfig()->getValue('file_storage.save_version_author', null); + if ($oldValue !== null) { + \OC::$server->getSystemConfig()->setValue('file_storage.save_version_metadata', $oldValue); + } + \OC::$server->getSystemConfig()->deleteValue('file_storage.save_version_author'); + } +} diff --git a/lib/private/Files/Meta/MetaFileIdNode.php b/lib/private/Files/Meta/MetaFileIdNode.php index 9746f81f4a79..5aff55a986a9 100644 --- a/lib/private/Files/Meta/MetaFileIdNode.php +++ b/lib/private/Files/Meta/MetaFileIdNode.php @@ -30,6 +30,8 @@ /** * Class MetaFileIdNode - this class represents the file id part of the meta endpoint * + * /meta/fileid + * * @package OC\Files\Meta */ class MetaFileIdNode extends AbstractFolder { @@ -53,6 +55,20 @@ public function __construct(MetaRootNode $parentNode, IRootFolder $root, \OCP\Fi $this->node = $node; } + /** + * @inheritdoc + */ + public function getName() { + return "{$this->node->getId()}"; + } + + /** + * @inheritdoc + */ + public function getPath() { + return "/meta/{$this->node->getId()}"; + } + /** * @inheritdoc */ @@ -106,18 +122,11 @@ public function getFreeSpace() { return FileInfo::SPACE_UNKNOWN; } - /** - * @inheritdoc - */ - public function getPath() { - return $this->getInternalPath(); - } - /** * @inheritdoc */ public function getInternalPath() { - return "/meta/{$this->node->getId()}"; + return $this->getPath(); } /** @@ -161,11 +170,4 @@ public function isShareable() { public function getParent() { return $this->parentNode; } - - /** - * @inheritdoc - */ - public function getName() { - return "{$this->node->getId()}"; - } } diff --git a/lib/private/Files/Meta/MetaFileVersionNode.php b/lib/private/Files/Meta/MetaFileVersionNode.php index aa1ed4d0eec9..c2eeb8821295 100644 --- a/lib/private/Files/Meta/MetaFileVersionNode.php +++ b/lib/private/Files/Meta/MetaFileVersionNode.php @@ -2,7 +2,7 @@ /** * @author Thomas Müller * - * @copyright Copyright (c) 2018, ownCloud GmbH + * @copyright Copyright (c) 2022, ownCloud GmbH * @license AGPL-3.0 * * This code is free software: you can redistribute it and/or modify @@ -29,6 +29,7 @@ use OC\Preview; use OCA\Files_Sharing\SharedStorage; use OCP\Files\IProvidesVersionAuthor; +use OCP\Files\IProvidesVersionTag; use OCP\Files\IRootFolder; use OCP\Files\IPreviewNode; use OCP\Files\Storage\IVersionedStorage; @@ -36,12 +37,11 @@ use OCP\IImage; /** - * Class MetaFileVersionNode - this class represents a version of a file in the - * meta endpoint + * Noncurrent version of the file node. This class represents a version of a file in the meta endpoint * * @package OC\Files\Meta */ -class MetaFileVersionNode extends AbstractFile implements IPreviewNode, IProvidesAdditionalHeaders, IProvidesVersionAuthor { +class MetaFileVersionNode extends AbstractFile implements IPreviewNode, IProvidesAdditionalHeaders, IProvidesVersionAuthor, IProvidesVersionTag { /** @var string */ private $versionId; /** @var MetaVersionCollection */ @@ -80,7 +80,18 @@ public function __construct( } /** - * @return string + * @inheritdoc + */ + public function getName() { + return $this->versionId; + } + + public function getPath() { + return $this->parent->getPath() . '/' . $this->getName(); + } + + /** + * @inheritdoc */ public function getEditedBy() : string { return $this->versionInfo['edited_by'] ?? ''; @@ -89,8 +100,8 @@ public function getEditedBy() : string { /** * @inheritdoc */ - public function getName() { - return $this->versionId; + public function getVersionTag() : string { + return $this->versionInfo['version_tag'] ?? ''; } /** @@ -167,10 +178,6 @@ public function getId() { return $this->parent->getId(); } - public function getPath() { - return $this->parent->getPath() . '/' . $this->getName(); - } - public function getMountPoint() { return Filesystem::getMountManager()->find($this->versionInfo['path']); } diff --git a/lib/private/Files/Meta/MetaRootNode.php b/lib/private/Files/Meta/MetaRootNode.php index b28a2db8fd7b..3ad09814464b 100644 --- a/lib/private/Files/Meta/MetaRootNode.php +++ b/lib/private/Files/Meta/MetaRootNode.php @@ -47,6 +47,21 @@ public function __construct(IRootFolder $rootFolder, IUserSession $userSession) $this->rootFolder = $rootFolder; $this->userSession = $userSession; } + + /** + * @inheritdoc + */ + public function getName() { + return 'meta'; + } + + /** + * @inheritdoc + */ + public function getPath() { + return '/meta'; + } + /** * @inheritdoc */ @@ -120,13 +135,6 @@ public function isCreatable() { return false; } - /** - * @inheritdoc - */ - public function getPath() { - return '/meta'; - } - /** * @inheritdoc */ @@ -162,11 +170,4 @@ public function isDeletable() { public function isShareable() { return false; } - - /** - * @inheritdoc - */ - public function getName() { - return 'meta'; - } } diff --git a/lib/private/Files/Meta/MetaVersionCollection.php b/lib/private/Files/Meta/MetaVersionCollection.php index 6ee666c44cd5..c4d48f319ad0 100644 --- a/lib/private/Files/Meta/MetaVersionCollection.php +++ b/lib/private/Files/Meta/MetaVersionCollection.php @@ -24,21 +24,25 @@ use OC\Files\Node\AbstractFolder; use OCP\Files\IRootFolder; +use OCP\Files\IProvidesVersionAuthor; +use OCP\Files\IProvidesVersionTag; use OCP\Files\Storage\IVersionedStorage; use OCP\Files\NotFoundException; use OCP\Files\Storage; /** - * Class MetaVersionCollection - this class represents the versions sub folder - * of a file + * Collection root (current file node) of noncurrent versions (directory children nodes). This + * class represents the versions sub folder of a file * * @package OC\Files\Meta */ -class MetaVersionCollection extends AbstractFolder { +class MetaVersionCollection extends AbstractFolder implements IProvidesVersionAuthor, IProvidesVersionTag { /** @var IRootFolder */ private $root; /** @var \OCP\Files\Node */ private $node; + /** @var array */ + private $versionInfo; /** * MetaVersionCollection constructor. @@ -51,6 +55,56 @@ public function __construct(IRootFolder $root, \OCP\Files\Node $node) { $this->node = $node; } + public function getName() { + return "v"; + } + + public function getPath() { + return "/meta/{$this->getId()}/v"; + } + + /** + * @inheritdoc + */ + public function getEditedBy() : string { + if (!$this->versionInfo) { + $storage = $this->node->getStorage(); + $internalPath = $this->node->getInternalPath(); + + if (!$storage->instanceOfStorage(IVersionedStorage::class)) { + return ''; + } + + /** @var IVersionedStorage | Storage $storage */ + '@phan-var IVersionedStorage | Storage $storage'; + $version = $storage->getCurrentVersion($internalPath); + $this->versionInfo = $version; + } + + return $this->versionInfo['edited_by'] ?? ''; + } + + /** + * @inheritdoc + */ + public function getVersionTag() : string { + if (!$this->versionInfo) { + $storage = $this->node->getStorage(); + $internalPath = $this->node->getInternalPath(); + + if (!$storage->instanceOfStorage(IVersionedStorage::class)) { + return ''; + } + + /** @var IVersionedStorage | Storage $storage */ + '@phan-var IVersionedStorage | Storage $storage'; + $version = $storage->getCurrentVersion($internalPath); + $this->versionInfo = $version; + } + + return $this->versionInfo['version_tag'] ?? ''; + } + /** * @inheritdoc */ @@ -119,12 +173,4 @@ public function get($path) { public function getId() { return $this->node->getId(); } - - public function getName() { - return "v"; - } - - public function getPath() { - return "/meta/{$this->getId()}/v"; - } } diff --git a/lib/private/Files/Storage/Common.php b/lib/private/Files/Storage/Common.php index b88c091bfdc6..82248d7149cd 100644 --- a/lib/private/Files/Storage/Common.php +++ b/lib/private/Files/Storage/Common.php @@ -700,6 +700,7 @@ public function getAvailability() { public function setAvailability($isAvailable) { $this->getStorageCache()->setAvailability($isAvailable); } + public function getVersions($internalPath) { // KISS implementation if (!\OC_App::isEnabled('files_versions')) { @@ -715,6 +716,16 @@ public function getVersions($internalPath) { )); } + public function getCurrentVersion($internalPath) { + // KISS implementation + if (!\OC_App::isEnabled('files_versions')) { + return []; + } + list($uid, $filename) = $this->convertInternalPathToGlobalPath($internalPath); + + return \OCA\Files_Versions\Storage::getCurrentVersion($uid, $filename); + } + /** * @param $internalPath * @return array diff --git a/lib/private/Files/Storage/Wrapper/Jail.php b/lib/private/Files/Storage/Wrapper/Jail.php index c25a716ae950..f9a455f23e87 100644 --- a/lib/private/Files/Storage/Wrapper/Jail.php +++ b/lib/private/Files/Storage/Wrapper/Jail.php @@ -541,7 +541,7 @@ public function saveVersion($internalPath) { } /** - * List all versions for the given file + * List versioning metadata of all noncurrent versions for the given file * * @param string $internalPath * @return array @@ -554,7 +554,20 @@ public function getVersions($internalPath) { } /** - * Get one explicit version for the given file + * Get versioning metadata for current version of the file (versions root) + * + * @param string $internalPath + * @return array + * @since 10.0.9 + */ + public function getCurrentVersion($internalPath) { + $wrapperStorage = $this->getWrapperStorage(); + '@phan-var \OC\Files\Storage\Common $wrapperStorage'; + return $wrapperStorage->getCurrentVersion($this->getSourcePath($internalPath)); + } + + /** + * Get versioning metadata for one explicit noncurrent version of the given file * * @param string $internalPath * @param string $versionId diff --git a/lib/public/Files/IProvidesVersionTag.php b/lib/public/Files/IProvidesVersionTag.php new file mode 100644 index 000000000000..ed8e7c41917c --- /dev/null +++ b/lib/public/Files/IProvidesVersionTag.php @@ -0,0 +1,40 @@ + + * @author Piotr Mrowczynski + * + * @copyright Copyright (c) 2022, ownCloud GmbH + * @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 + * + */ + +namespace OCP\Files; + +/** + * Interface IProvidesVersionTag + * This interface provides version author retrieval for file version + * + * @package OCP\Files + * @since 10.12.0 + */ +interface IProvidesVersionTag { + /** + * Returns the versioning tag of the file. + * + * @return string tag or empty string if this is the initial version + * @since 10.12.0 + */ + public function getVersionTag() : string; +} diff --git a/lib/public/Files/Storage/IVersionedStorage.php b/lib/public/Files/Storage/IVersionedStorage.php index fdd068eb4eb2..fea4893bc172 100644 --- a/lib/public/Files/Storage/IVersionedStorage.php +++ b/lib/public/Files/Storage/IVersionedStorage.php @@ -29,26 +29,35 @@ */ interface IVersionedStorage { /** - * List all versions for the given file + * Get metadata for current version of the file (versions root) * * @param string $internalPath - * @return array + * @return array metadata or null if not supported + * @since 10.12.0 + */ + public function getCurrentVersion($internalPath); + + /** + * List metadata of all noncurrent versions for the given file + * + * @param string $internalPath + * @return array list of versions metadata or empty array if not supported * @since 10.0.9 */ public function getVersions($internalPath); /** - * Get one explicit version for the given file + * Get metadata of one explicit noncurrent version for the given file. * * @param string $internalPath * @param string $versionId - * @return array + * @return array metadata or null if not supported * @since 10.0.9 */ public function getVersion($internalPath, $versionId); /** - * Get the content of a given version of a given file as stream resource + * Get the content of a given noncurrent version of a given file as stream resource * * @param string $internalPath * @param string $versionId @@ -58,7 +67,7 @@ public function getVersion($internalPath, $versionId); public function getContentOfVersion($internalPath, $versionId); /** - * Restore the given version of a given file + * Restore the given noncurrent version of a given file to current version * * @param string $internalPath * @param string $versionId @@ -68,7 +77,7 @@ public function getContentOfVersion($internalPath, $versionId); public function restoreVersion($internalPath, $versionId); /** - * Tells the storage to explicitly create a version of a given file + * Tells the storage to explicitly create a new noncurrent version of a file * * @param string $internalPath * @return bool diff --git a/tests/acceptance/features/apiVersions/fileVersionAuthor.feature b/tests/acceptance/features/apiVersions/fileVersionAuthor.feature index 2b6efbd5ce6d..966a415c792f 100644 --- a/tests/acceptance/features/apiVersions/fileVersionAuthor.feature +++ b/tests/acceptance/features/apiVersions/fileVersionAuthor.feature @@ -23,11 +23,11 @@ Feature: file versions remember the author of each version And user "David" has uploaded file with content "uploaded content david" to "/test/textfile0.txt" When user "Alice" gets the number of versions of file "/test/textfile0.txt" Then the HTTP status code should be "207" - And the number of versions should be "3" - And the content of version index "1" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content carol" - And the content of version index "2" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content brian" - And the content of version index "3" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content alice" - And as users "Alice,Brian,Carol,David" the authors of the versions of file "/test/textfile0.txt" should be: + And the number of noncurrent versions should be "3" + And the content of noncurrent version index "1" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content carol" + And the content of noncurrent version index "2" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content brian" + And the content of noncurrent version index "3" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content alice" + And as users "Alice,Brian,Carol,David" the authors of the noncurrent versions of file "/test/textfile0.txt" should be: | index | author | | 1 | Carol | | 2 | Brian | @@ -46,10 +46,10 @@ Feature: file versions remember the author of each version And user "Carol" has uploaded file with content "uploaded content carol" to "/test/textfile0.txt" When user "Alice" gets the number of versions of file "/test/textfile0.txt" Then the HTTP status code should be "207" - And the number of versions should be "2" - And the content of version index "1" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content brian" - And the content of version index "2" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content alice" - And as users "Alice,Brian,Carol" the authors of the versions of file "/test/textfile0.txt" should be: + And the number of noncurrent versions should be "2" + And the content of noncurrent version index "1" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content brian" + And the content of noncurrent version index "2" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content alice" + And as users "Alice,Brian,Carol" the authors of the noncurrent versions of file "/test/textfile0.txt" should be: | index | author | | 1 | Brian | | 2 | Alice | @@ -66,10 +66,10 @@ Feature: file versions remember the author of each version And user "Carol" has uploaded file with content "uploaded content carol" to "/textfile0.txt" When user "Alice" gets the number of versions of file "textfile0.txt" Then the HTTP status code should be "207" - And the number of versions should be "2" - And the content of version index "1" of file "/textfile0.txt" for user "Alice" should be "uploaded content brian" - And the content of version index "2" of file "/textfile0.txt" for user "Alice" should be "uploaded content alice" - And as users "Alice,Brian,Carol" the authors of the versions of file "/textfile0.txt" should be: + And the number of noncurrent versions should be "2" + And the content of noncurrent version index "1" of file "/textfile0.txt" for user "Alice" should be "uploaded content brian" + And the content of noncurrent version index "2" of file "/textfile0.txt" for user "Alice" should be "uploaded content alice" + And as users "Alice,Brian,Carol" the authors of the noncurrent versions of file "/textfile0.txt" should be: | index | author | | 1 | Brian | | 2 | Alice | @@ -90,11 +90,11 @@ Feature: file versions remember the author of each version And user "Brian" has moved file "/textfile0.txt" to "/test/textfile0.txt" When user "Alice" gets the number of versions of file "/test/textfile0.txt" Then the HTTP status code should be "207" - And the number of versions should be "3" - And the content of version index "1" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content carol" - And the content of version index "2" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content brian" - And the content of version index "3" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content alice" - And as users "Alice,Brian,Carol" the authors of the versions of file "/test/textfile0.txt" should be: + And the number of noncurrent versions should be "3" + And the content of noncurrent version index "1" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content carol" + And the content of noncurrent version index "2" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content brian" + And the content of noncurrent version index "3" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content alice" + And as users "Alice,Brian,Carol" the authors of the noncurrent versions of file "/test/textfile0.txt" should be: | index | author | | 1 | Carol | | 2 | Brian | @@ -113,14 +113,14 @@ Feature: file versions remember the author of each version And user "Alice" has moved file "/exist.txt" to "/textfile0.txt" When user "Alice" gets the number of versions of file "textfile0.txt" Then the HTTP status code should be "207" - And the number of versions should be "2" - And the content of version index "1" of file "/textfile0.txt" for user "Alice" should be "uploaded content brian" - And the content of version index "2" of file "/textfile0.txt" for user "Alice" should be "uploaded content alice" - And as user "Alice" the authors of the versions of file "/textfile0.txt" should be: + And the number of noncurrent versions should be "2" + And the content of noncurrent version index "1" of file "/textfile0.txt" for user "Alice" should be "uploaded content brian" + And the content of noncurrent version index "2" of file "/textfile0.txt" for user "Alice" should be "uploaded content alice" + And as user "Alice" the authors of the noncurrent versions of file "/textfile0.txt" should be: | index | author | | 1 | Brian | | 2 | Alice | - And as users "Brian,Carol" the authors of the versions of file "/exist.txt" should be: + And as users "Brian,Carol" the authors of the noncurrent versions of file "/exist.txt" should be: | index | author | | 1 | Brian | | 2 | Alice | @@ -138,14 +138,14 @@ Feature: file versions remember the author of each version And user "Brian" has moved file "/exist.txt" to "/textfile0.txt" When user "Alice" gets the number of versions of file "exist.txt" Then the HTTP status code should be "207" - And the number of versions should be "2" - And the content of version index "1" of file "/exist.txt" for user "Alice" should be "uploaded content brian" - And the content of version index "2" of file "/exist.txt" for user "Alice" should be "uploaded content alice" - And as users "Alice,Carol" the authors of the versions of file "/exist.txt" should be: + And the number of noncurrent versions should be "2" + And the content of noncurrent version index "1" of file "/exist.txt" for user "Alice" should be "uploaded content brian" + And the content of noncurrent version index "2" of file "/exist.txt" for user "Alice" should be "uploaded content alice" + And as users "Alice,Carol" the authors of the noncurrent versions of file "/exist.txt" should be: | index | author | | 1 | Brian | | 2 | Alice | - And as user "Brian" the authors of the versions of file "/textfile0.txt" should be: + And as user "Brian" the authors of the noncurrent versions of file "/textfile0.txt" should be: | index | author | | 1 | Brian | | 2 | Alice | @@ -166,11 +166,11 @@ Feature: file versions remember the author of each version And user "Alice" has shared folder "/test" with group "grp1" When user "Alice" gets the number of versions of file "/test/textfile0.txt" Then the HTTP status code should be "207" - And the number of versions should be "3" - And the content of version index "1" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content carol" - And the content of version index "2" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content brian" - And the content of version index "3" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content alice" - And as users "Alice,Brian,Carol" the authors of the versions of file "/test/textfile0.txt" should be: + And the number of noncurrent versions should be "3" + And the content of noncurrent version index "1" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content carol" + And the content of noncurrent version index "2" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content brian" + And the content of noncurrent version index "3" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content alice" + And as users "Alice,Brian,Carol" the authors of the noncurrent versions of file "/test/textfile0.txt" should be: | index | author | | 1 | Carol | | 2 | Brian | @@ -194,30 +194,30 @@ Feature: file versions remember the author of each version And user "David" has uploaded file with content "uploaded content david" to "/test/textfile0.txt" When user "Alice" gets the number of versions of file "/test/textfile0.txt" Then the HTTP status code should be "207" - And the number of versions should be "3" - And the content of version index "1" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content carol" - And the content of version index "2" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content brian" - And the content of version index "3" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content alice" - And as users "Alice,David" the authors of the versions of file "/test/textfile0.txt" should be: + And the number of noncurrent versions should be "3" + And the content of noncurrent version index "1" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content carol" + And the content of noncurrent version index "2" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content brian" + And the content of noncurrent version index "3" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content alice" + And as users "Alice,David" the authors of the noncurrent versions of file "/test/textfile0.txt" should be: | index | author | | 1 | Carol | | 2 | Brian | | 3 | Alice | - And as users "Brian,Carol" the authors of the versions of file "/test (2)/textfile0.txt" should be: + And as users "Brian,Carol" the authors of the noncurrent versions of file "/test (2)/textfile0.txt" should be: | index | author | | 1 | Carol | | 2 | Brian | | 3 | Alice | When user "Brian" gets the number of versions of file "/test/textfile0.txt" Then the HTTP status code should be "207" - And the number of versions should be "1" - And the content of version index "1" of file "/test/textfile0.txt" for user "Brian" should be "duplicate brian" - And as user "Brian" the authors of the versions of file "/test/textfile0.txt" should be: + And the number of noncurrent versions should be "1" + And the content of noncurrent version index "1" of file "/test/textfile0.txt" for user "Brian" should be "duplicate brian" + And as user "Brian" the authors of the noncurrent versions of file "/test/textfile0.txt" should be: | index | author | | 1 | Brian | When user "Carol" gets the number of versions of file "/test/textfile0.txt" Then the HTTP status code should be "207" - And the number of versions should be "0" + And the number of noncurrent versions should be "0" @skip_on_objectstore Scenario: enable file versioning and check the history of changes from multiple users who have a matching file @@ -234,30 +234,30 @@ Feature: file versions remember the author of each version And user "David" has uploaded file with content "uploaded content david" to "/textfile0.txt" When user "Alice" gets the number of versions of file "/textfile0.txt" Then the HTTP status code should be "207" - And the number of versions should be "3" - And the content of version index "1" of file "/textfile0.txt" for user "Alice" should be "uploaded content carol" - And the content of version index "2" of file "/textfile0.txt" for user "Alice" should be "uploaded content brian" - And the content of version index "3" of file "/textfile0.txt" for user "Alice" should be "uploaded content alice" - And as users "Alice,David" the authors of the versions of file "/textfile0.txt" should be: + And the number of noncurrent versions should be "3" + And the content of noncurrent version index "1" of file "/textfile0.txt" for user "Alice" should be "uploaded content carol" + And the content of noncurrent version index "2" of file "/textfile0.txt" for user "Alice" should be "uploaded content brian" + And the content of noncurrent version index "3" of file "/textfile0.txt" for user "Alice" should be "uploaded content alice" + And as users "Alice,David" the authors of the noncurrent versions of file "/textfile0.txt" should be: | index | author | | 1 | Carol | | 2 | Brian | | 3 | Alice | - And as users "Brian,Carol" the authors of the versions of file "/textfile0 (2).txt" should be: + And as users "Brian,Carol" the authors of the noncurrent versions of file "/textfile0 (2).txt" should be: | index | author | | 1 | Carol | | 2 | Brian | | 3 | Alice | When user "Brian" gets the number of versions of file "/textfile0.txt" Then the HTTP status code should be "207" - And the number of versions should be "1" - And the content of version index "1" of file "/textfile0.txt" for user "Brian" should be "duplicate brian" - And as user "Brian" the authors of the versions of file "/textfile0.txt" should be: + And the number of noncurrent versions should be "1" + And the content of noncurrent version index "1" of file "/textfile0.txt" for user "Brian" should be "duplicate brian" + And as user "Brian" the authors of the noncurrent versions of file "/textfile0.txt" should be: | index | author | | 1 | Brian | When user "Carol" gets the number of versions of file "/textfile0.txt" Then the HTTP status code should be "207" - And the number of versions should be "0" + And the number of noncurrent versions should be "0" @skip_on_objectstore Scenario: enable file versioning and check the version author after restoring a version of a file inside a folder @@ -269,10 +269,11 @@ Feature: file versions remember the author of each version And user "Carol" has uploaded file with content "uploaded content carol" to "/test/textfile0.txt" When user "Brian" restores version index "1" of file "/test/textfile0.txt" using the WebDAV API Then the HTTP status code should be "204" - And as user "Alice,Brian,Carol" the authors of the versions of file "/test/textfile0.txt" should be: + And as user "Alice,Brian,Carol" the authors of the noncurrent versions of file "/test/textfile0.txt" should be: | index | author | | 1 | Carol | - | 2 | Alice | + | 2 | Brian | + | 3 | Alice | @skip_on_objectstore Scenario: enable file versioning and check the version author after restoring a version of a file @@ -283,10 +284,11 @@ Feature: file versions remember the author of each version And user "Carol" has uploaded file with content "uploaded content carol" to "/textfile0.txt" When user "Brian" restores version index "1" of file "/textfile0.txt" using the WebDAV API Then the HTTP status code should be "204" - And as user "Alice,Brian,Carol" the authors of the versions of file "/textfile0.txt" should be: + And as user "Alice,Brian,Carol" the authors of the noncurrent versions of file "/textfile0.txt" should be: | index | author | | 1 | Carol | - | 2 | Alice | + | 2 | Brian | + | 3 | Alice | @skip_on_objectstore Scenario: check the author of the file version which was created before enabling the version storage @@ -297,14 +299,17 @@ Feature: file versions remember the author of each version And user "Brian" has uploaded file with content "uploaded content brian" to "/textfile0.txt" When user "Brian" restores version index "1" of file "/textfile0.txt" using the WebDAV API Then the HTTP status code should be "204" - And as user "Alice,Brian" the authors of the versions of file "/textfile0.txt" should be: + And as user "Alice,Brian" the authors of the noncurrent versions of file "/textfile0.txt" should be: | index | author | | 1 | Brian | + | 2 | | When user "Brian" restores version index "1" of file "/textfile0.txt" using the WebDAV API Then the HTTP status code should be "204" - And as user "Alice,Brian" the authors of the versions of file "/textfile0.txt" should be: + And as user "Alice,Brian" the authors of the noncurrent versions of file "/textfile0.txt" should be: | index | author | - | 1 | | + | 1 | Brian | + | 2 | Brian | + | 3 | | @skip_on_objectstore Scenario: check the author of the file version (inside a folder) which was created before enabling the version storage @@ -316,11 +321,14 @@ Feature: file versions remember the author of each version And user "Brian" has uploaded file with content "uploaded content brian" to "/test/textfile0.txt" When user "Brian" restores version index "1" of file "/test/textfile0.txt" using the WebDAV API Then the HTTP status code should be "204" - And as user "Alice,Brian" the authors of the versions of file "/test/textfile0.txt" should be: + And as user "Alice,Brian" the authors of the noncurrent versions of file "/test/textfile0.txt" should be: | index | author | | 1 | Brian | + | 2 | | When user "Brian" restores version index "1" of file "/test/textfile0.txt" using the WebDAV API Then the HTTP status code should be "204" - And as user "Alice,Brian" the authors of the versions of file "/test/textfile0.txt" should be: + And as user "Alice,Brian" the authors of the noncurrent versions of file "/test/textfile0.txt" should be: | index | author | - | 1 | | \ No newline at end of file + | 1 | Brian | + | 2 | Brian | + | 3 | | diff --git a/tests/acceptance/features/apiVersions/fileVersionAuthorSharingToShares.feature b/tests/acceptance/features/apiVersions/fileVersionAuthorSharingToShares.feature index c16cee37025b..b5e8e601e8ab 100644 --- a/tests/acceptance/features/apiVersions/fileVersionAuthorSharingToShares.feature +++ b/tests/acceptance/features/apiVersions/fileVersionAuthorSharingToShares.feature @@ -28,16 +28,16 @@ Feature: file versions remember the author of each version And user "David" has uploaded file with content "uploaded content david" to "/Shares/test/textfile0.txt" When user "Alice" gets the number of versions of file "/test/textfile0.txt" Then the HTTP status code should be "207" - And the number of versions should be "3" - And the content of version index "1" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content carol" - And the content of version index "2" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content brian" - And the content of version index "3" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content alice" - And as users "Alice" the authors of the versions of file "/test/textfile0.txt" should be: + And the number of noncurrent versions should be "3" + And the content of noncurrent version index "1" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content carol" + And the content of noncurrent version index "2" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content brian" + And the content of noncurrent version index "3" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content alice" + And as users "Alice" the authors of the noncurrent versions of file "/test/textfile0.txt" should be: | index | author | | 1 | Carol | | 2 | Brian | | 3 | Alice | - And as users "Brian,Carol,David" the authors of the versions of file "/Shares/test/textfile0.txt" should be: + And as users "Brian,Carol,David" the authors of the noncurrent versions of file "/Shares/test/textfile0.txt" should be: | index | author | | 1 | Carol | | 2 | Brian | @@ -58,14 +58,14 @@ Feature: file versions remember the author of each version And user "Carol" has uploaded file with content "uploaded content carol" to "/Shares/test/textfile0.txt" When user "Alice" gets the number of versions of file "/test/textfile0.txt" Then the HTTP status code should be "207" - And the number of versions should be "2" - And the content of version index "1" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content brian" - And the content of version index "2" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content alice" - And as users "Alice" the authors of the versions of file "/test/textfile0.txt" should be: + And the number of noncurrent versions should be "2" + And the content of noncurrent version index "1" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content brian" + And the content of noncurrent version index "2" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content alice" + And as users "Alice" the authors of the noncurrent versions of file "/test/textfile0.txt" should be: | index | author | | 1 | Brian | | 2 | Alice | - And as users "Brian,Carol" the authors of the versions of file "/Shares/test/textfile0.txt" should be: + And as users "Brian,Carol" the authors of the noncurrent versions of file "/Shares/test/textfile0.txt" should be: | index | author | | 1 | Brian | | 2 | Alice | @@ -84,14 +84,14 @@ Feature: file versions remember the author of each version And user "Carol" has uploaded file with content "uploaded content carol" to "/Shares/textfile0.txt" When user "Alice" gets the number of versions of file "textfile0.txt" Then the HTTP status code should be "207" - And the number of versions should be "2" - And the content of version index "1" of file "/textfile0.txt" for user "Alice" should be "uploaded content brian" - And the content of version index "2" of file "/textfile0.txt" for user "Alice" should be "uploaded content alice" - And as users "Alice" the authors of the versions of file "/textfile0.txt" should be: + And the number of noncurrent versions should be "2" + And the content of noncurrent version index "1" of file "/textfile0.txt" for user "Alice" should be "uploaded content brian" + And the content of noncurrent version index "2" of file "/textfile0.txt" for user "Alice" should be "uploaded content alice" + And as users "Alice" the authors of the noncurrent versions of file "/textfile0.txt" should be: | index | author | | 1 | Brian | | 2 | Alice | - And as users "Brian,Carol" the authors of the versions of file "/Shares/textfile0.txt" should be: + And as users "Brian,Carol" the authors of the noncurrent versions of file "/Shares/textfile0.txt" should be: | index | author | | 1 | Brian | | 2 | Alice | @@ -114,16 +114,16 @@ Feature: file versions remember the author of each version And user "Brian" has moved file "/textfile0.txt" to "/Shares/test/textfile0.txt" When user "Alice" gets the number of versions of file "/test/textfile0.txt" Then the HTTP status code should be "207" - And the number of versions should be "3" - And the content of version index "1" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content carol" - And the content of version index "2" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content brian" - And the content of version index "3" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content alice" - And as users "Alice" the authors of the versions of file "/test/textfile0.txt" should be: + And the number of noncurrent versions should be "3" + And the content of noncurrent version index "1" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content carol" + And the content of noncurrent version index "2" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content brian" + And the content of noncurrent version index "3" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content alice" + And as users "Alice" the authors of the noncurrent versions of file "/test/textfile0.txt" should be: | index | author | | 1 | Carol | | 2 | Brian | | 3 | Alice | - And as users "Brian,Carol" the authors of the versions of file "/Shares/test/textfile0.txt" should be: + And as users "Brian,Carol" the authors of the noncurrent versions of file "/Shares/test/textfile0.txt" should be: | index | author | | 1 | Carol | | 2 | Brian | @@ -144,14 +144,14 @@ Feature: file versions remember the author of each version And user "Alice" has moved file "/exist.txt" to "/textfile0.txt" When user "Alice" gets the number of versions of file "textfile0.txt" Then the HTTP status code should be "207" - And the number of versions should be "2" - And the content of version index "1" of file "/textfile0.txt" for user "Alice" should be "uploaded content brian" - And the content of version index "2" of file "/textfile0.txt" for user "Alice" should be "uploaded content alice" - And as user "Alice" the authors of the versions of file "/textfile0.txt" should be: + And the number of noncurrent versions should be "2" + And the content of noncurrent version index "1" of file "/textfile0.txt" for user "Alice" should be "uploaded content brian" + And the content of noncurrent version index "2" of file "/textfile0.txt" for user "Alice" should be "uploaded content alice" + And as user "Alice" the authors of the noncurrent versions of file "/textfile0.txt" should be: | index | author | | 1 | Brian | | 2 | Alice | - And as users "Brian,Carol" the authors of the versions of file "/Shares/exist.txt" should be: + And as users "Brian,Carol" the authors of the noncurrent versions of file "/Shares/exist.txt" should be: | index | author | | 1 | Brian | | 2 | Alice | @@ -171,18 +171,18 @@ Feature: file versions remember the author of each version And user "Brian" has moved file "/Shares/exist.txt" to "/Shares/textfile0.txt" When user "Alice" gets the number of versions of file "exist.txt" Then the HTTP status code should be "207" - And the number of versions should be "2" - And the content of version index "1" of file "/exist.txt" for user "Alice" should be "uploaded content brian" - And the content of version index "2" of file "/exist.txt" for user "Alice" should be "uploaded content alice" - And as users "Alice" the authors of the versions of file "/exist.txt" should be: + And the number of noncurrent versions should be "2" + And the content of noncurrent version index "1" of file "/exist.txt" for user "Alice" should be "uploaded content brian" + And the content of noncurrent version index "2" of file "/exist.txt" for user "Alice" should be "uploaded content alice" + And as users "Alice" the authors of the noncurrent versions of file "/exist.txt" should be: | index | author | | 1 | Brian | | 2 | Alice | - And as users "Carol" the authors of the versions of file "/Shares/exist.txt" should be: + And as users "Carol" the authors of the noncurrent versions of file "/Shares/exist.txt" should be: | index | author | | 1 | Brian | | 2 | Alice | - And as user "Brian" the authors of the versions of file "/Shares/textfile0.txt" should be: + And as user "Brian" the authors of the noncurrent versions of file "/Shares/textfile0.txt" should be: | index | author | | 1 | Brian | | 2 | Alice | @@ -207,16 +207,16 @@ Feature: file versions remember the author of each version And user "Carol" has accepted share "/test" offered by user "Alice" When user "Alice" gets the number of versions of file "/test/textfile0.txt" Then the HTTP status code should be "207" - And the number of versions should be "3" - And the content of version index "1" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content carol" - And the content of version index "2" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content brian" - And the content of version index "3" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content alice" - And as users "Alice" the authors of the versions of file "/test/textfile0.txt" should be: + And the number of noncurrent versions should be "3" + And the content of noncurrent version index "1" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content carol" + And the content of noncurrent version index "2" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content brian" + And the content of noncurrent version index "3" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content alice" + And as users "Alice" the authors of the noncurrent versions of file "/test/textfile0.txt" should be: | index | author | | 1 | Carol | | 2 | Brian | | 3 | Alice | - And as users "Brian,Carol" the authors of the versions of file "/Shares/test/textfile0.txt" should be: + And as users "Brian,Carol" the authors of the noncurrent versions of file "/Shares/test/textfile0.txt" should be: | index | author | | 1 | Carol | | 2 | Brian | @@ -249,30 +249,30 @@ Feature: file versions remember the author of each version And user "David" has uploaded file with content "uploaded content david" to "/Shares/test (2)/textfile0.txt" When user "Alice" gets the number of versions of file "/test/textfile0.txt" Then the HTTP status code should be "207" - And the number of versions should be "3" - And the content of version index "1" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content carol" - And the content of version index "2" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content brian" - And the content of version index "3" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content alice" - And as users "Alice" the authors of the versions of file "/test/textfile0.txt" should be: + And the number of noncurrent versions should be "3" + And the content of noncurrent version index "1" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content carol" + And the content of noncurrent version index "2" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content brian" + And the content of noncurrent version index "3" of file "/test/textfile0.txt" for user "Alice" should be "uploaded content alice" + And as users "Alice" the authors of the noncurrent versions of file "/test/textfile0.txt" should be: | index | author | | 1 | Carol | | 2 | Brian | | 3 | Alice | - And as users "Brian,Carol,David" the authors of the versions of file "/Shares/test (2)/textfile0.txt" should be: + And as users "Brian,Carol,David" the authors of the noncurrent versions of file "/Shares/test (2)/textfile0.txt" should be: | index | author | | 1 | Carol | | 2 | Brian | | 3 | Alice | When user "Brian" gets the number of versions of file "/test/textfile0.txt" Then the HTTP status code should be "207" - And the number of versions should be "1" - And the content of version index "1" of file "/test/textfile0.txt" for user "Brian" should be "duplicate brian" - And as user "Brian" the authors of the versions of file "/test/textfile0.txt" should be: + And the number of noncurrent versions should be "1" + And the content of noncurrent version index "1" of file "/test/textfile0.txt" for user "Brian" should be "duplicate brian" + And as user "Brian" the authors of the noncurrent versions of file "/test/textfile0.txt" should be: | index | author | | 1 | Brian | When user "Carol" gets the number of versions of file "/test/textfile0.txt" Then the HTTP status code should be "207" - And the number of versions should be "0" + And the number of noncurrent versions should be "0" @skip_on_objectstore Scenario: enable file versioning and check the history of changes from multiple users who have a matching file @@ -298,30 +298,30 @@ Feature: file versions remember the author of each version And user "David" has uploaded file with content "uploaded content david" to "/Shares/textfile0 (2).txt" When user "Alice" gets the number of versions of file "/textfile0.txt" Then the HTTP status code should be "207" - And the number of versions should be "3" - And the content of version index "1" of file "/textfile0.txt" for user "Alice" should be "uploaded content carol" - And the content of version index "2" of file "/textfile0.txt" for user "Alice" should be "uploaded content brian" - And the content of version index "3" of file "/textfile0.txt" for user "Alice" should be "uploaded content alice" - And as users "Alice" the authors of the versions of file "/textfile0.txt" should be: + And the number of noncurrent versions should be "3" + And the content of noncurrent version index "1" of file "/textfile0.txt" for user "Alice" should be "uploaded content carol" + And the content of noncurrent version index "2" of file "/textfile0.txt" for user "Alice" should be "uploaded content brian" + And the content of noncurrent version index "3" of file "/textfile0.txt" for user "Alice" should be "uploaded content alice" + And as users "Alice" the authors of the noncurrent versions of file "/textfile0.txt" should be: | index | author | | 1 | Carol | | 2 | Brian | | 3 | Alice | - And as users "Brian,Carol,David" the authors of the versions of file "/Shares/textfile0 (2).txt" should be: + And as users "Brian,Carol,David" the authors of the noncurrent versions of file "/Shares/textfile0 (2).txt" should be: | index | author | | 1 | Carol | | 2 | Brian | | 3 | Alice | When user "Brian" gets the number of versions of file "/textfile0.txt" Then the HTTP status code should be "207" - And the number of versions should be "1" - And the content of version index "1" of file "/textfile0.txt" for user "Brian" should be "duplicate brian" - And as user "Brian" the authors of the versions of file "/textfile0.txt" should be: + And the number of noncurrent versions should be "1" + And the content of noncurrent version index "1" of file "/textfile0.txt" for user "Brian" should be "duplicate brian" + And as user "Brian" the authors of the noncurrent versions of file "/textfile0.txt" should be: | index | author | | 1 | Brian | When user "Carol" gets the number of versions of file "/textfile0.txt" Then the HTTP status code should be "207" - And the number of versions should be "0" + And the number of noncurrent versions should be "0" @skip_on_objectstore @files_sharing-app-required Scenario: enable file versioning and check the version author after restoring a version of a file inside a folder @@ -335,14 +335,16 @@ Feature: file versions remember the author of each version And user "Carol" has uploaded file with content "uploaded content carol" to "/Shares/test/textfile0.txt" When user "Brian" restores version index "1" of file "/Shares/test/textfile0.txt" using the WebDAV API Then the HTTP status code should be "204" - And as user "Alice" the authors of the versions of file "/test/textfile0.txt" should be: + And as user "Alice" the authors of the noncurrent versions of file "/test/textfile0.txt" should be: | index | author | | 1 | Carol | - | 2 | Alice | - And as user "Brian,Carol" the authors of the versions of file "/Shares/test/textfile0.txt" should be: + | 2 | Brian | + | 3 | Alice | + And as user "Brian,Carol" the authors of the noncurrent versions of file "/Shares/test/textfile0.txt" should be: | index | author | | 1 | Carol | - | 2 | Alice | + | 2 | Brian | + | 3 | Alice | @skip_on_objectstore @files_sharing-app-required Scenario: enable file versioning and check the version author after restoring a version of a file @@ -355,14 +357,16 @@ Feature: file versions remember the author of each version And user "Carol" has uploaded file with content "uploaded content carol" to "/Shares/textfile0.txt" When user "Brian" restores version index "1" of file "/Shares/textfile0.txt" using the WebDAV API Then the HTTP status code should be "204" - And as user "Alice" the authors of the versions of file "/textfile0.txt" should be: + And as user "Alice" the authors of the noncurrent versions of file "/textfile0.txt" should be: | index | author | | 1 | Carol | - | 2 | Alice | - And as user "Brian,Carol" the authors of the versions of file "/Shares/textfile0.txt" should be: + | 2 | Brian | + | 3 | Alice | + And as user "Brian,Carol" the authors of the noncurrent versions of file "/Shares/textfile0.txt" should be: | index | author | | 1 | Carol | - | 2 | Alice | + | 2 | Brian | + | 3 | Alice | @skip_on_objectstore @files_sharing-app-required Scenario: check the author of the file version which was created before enabling the version storage @@ -374,20 +378,26 @@ Feature: file versions remember the author of each version And user "Brian" has uploaded file with content "uploaded content brian" to "/Shares/textfile0.txt" When user "Brian" restores version index "1" of file "/Shares/textfile0.txt" using the WebDAV API Then the HTTP status code should be "204" - And as user "Alice" the authors of the versions of file "/textfile0.txt" should be: + And as user "Alice" the authors of the noncurrent versions of file "/textfile0.txt" should be: | index | author | | 1 | Brian | - And as user "Brian" the authors of the versions of file "/Shares/textfile0.txt" should be: + | 2 | | + And as user "Brian" the authors of the noncurrent versions of file "/Shares/textfile0.txt" should be: | index | author | | 1 | Brian | + | 2 | | When user "Brian" restores version index "1" of file "/Shares/textfile0.txt" using the WebDAV API Then the HTTP status code should be "204" - And as user "Alice" the authors of the versions of file "/textfile0.txt" should be: + And as user "Alice" the authors of the noncurrent versions of file "/textfile0.txt" should be: | index | author | - | 1 | | - And as user "Brian" the authors of the versions of file "/Shares/textfile0.txt" should be: + | 1 | Brian | + | 2 | Brian | + | 3 | | + And as user "Brian" the authors of the noncurrent versions of file "/Shares/textfile0.txt" should be: | index | author | - | 1 | | + | 1 | Brian | + | 2 | Brian | + | 3 | | @skip_on_objectstore @files_sharing-app-required Scenario: check the author of the file version (inside a folder) which was created before enabling the version storage @@ -400,17 +410,23 @@ Feature: file versions remember the author of each version And user "Brian" has uploaded file with content "uploaded content brian" to "/Shares/test/textfile0.txt" When user "Brian" restores version index "1" of file "/Shares/test/textfile0.txt" using the WebDAV API Then the HTTP status code should be "204" - And as user "Alice" the authors of the versions of file "/test/textfile0.txt" should be: + And as user "Alice" the authors of the noncurrent versions of file "/test/textfile0.txt" should be: | index | author | | 1 | Brian | - And as user "Brian" the authors of the versions of file "/Shares/test/textfile0.txt" should be: + | 2 | | + And as user "Brian" the authors of the noncurrent versions of file "/Shares/test/textfile0.txt" should be: | index | author | | 1 | Brian | + | 2 | | When user "Brian" restores version index "1" of file "/Shares/test/textfile0.txt" using the WebDAV API Then the HTTP status code should be "204" - And as user "Alice" the authors of the versions of file "/test/textfile0.txt" should be: + And as user "Alice" the authors of the noncurrent versions of file "/test/textfile0.txt" should be: | index | author | - | 1 | | - And as user "Brian" the authors of the versions of file "/Shares/test/textfile0.txt" should be: + | 1 | Brian | + | 2 | Brian | + | 3 | | + And as user "Brian" the authors of the noncurrent versions of file "/Shares/test/textfile0.txt" should be: | index | author | - | 1 | | + | 1 | Brian | + | 2 | Brian | + | 3 | | diff --git a/tests/acceptance/features/apiVersions/fileVersions.feature b/tests/acceptance/features/apiVersions/fileVersions.feature index 4e221a47a365..6f9bba858a84 100644 --- a/tests/acceptance/features/apiVersions/fileVersions.feature +++ b/tests/acceptance/features/apiVersions/fileVersions.feature @@ -95,7 +95,7 @@ Feature: dav-versions And the content of file "/davtest.txt" for user "Alice" should be "Back To The Future." @smokeTest @skipOnStorage:ceph @files_primary_s3-issue-161 - Scenario Outline: Uploading a chunked file does create the correct version that can be restored + Scenario Outline: Uploading a chunked file does create the correct version that can be restored from past version Given using DAV path And user "Alice" has uploaded file with content "textfile0" to "textfile0.txt" When user "Alice" uploads file "filesForUpload/davtest.txt" to "/textfile0.txt" in 2 chunks using the WebDAV API @@ -115,14 +115,14 @@ Feature: dav-versions | new | 204 | @skipOnStorage:ceph @files_primary_s3-issue-161 @newChunking @skipOnStorage:scality - Scenario: Uploading a file asynchronously does create the correct version that can be restored + Scenario: Uploading a file asynchronously does create the correct version that can be restored from past version Given the administrator has enabled async operations And user "Alice" has uploaded file with content "textfile0" to "textfile0.txt" When user "Alice" uploads file "filesForUpload/davtest.txt" asynchronously to "textfile0.txt" in 2 chunks using the WebDAV API And user "Alice" uploads file "filesForUpload/lorem.txt" asynchronously to "textfile0.txt" in 2 chunks using the WebDAV API And user "Alice" restores version index "1" of file "/textfile0.txt" using the WebDAV API Then the HTTP status code of responses on all endpoints should be "202" - And the version folder of file "/textfile0.txt" for user "Alice" should contain "2" elements + And the version folder of file "/textfile0.txt" for user "Alice" should contain "3" elements And the content of file "/textfile0.txt" for user "Alice" should be "Dav-Test" @skipOnStorage:ceph @skipOnStorage:scality @files_primary_s3-issue-156 @@ -140,7 +140,7 @@ Feature: dav-versions Given user "Brian" has been created with default attributes and without skeleton files And user "Alice" has uploaded file with content "123" to "/davtest.txt" And we save it into "FILEID" - When user "Brian" sends HTTP method "PROPFIND" to URL "/remote.php/dav/meta/<>" + When user "Brian" sends HTTP method "PROPFIND" to URL "/remote.php/dav/meta/<featureContext->getBaseUrlWithoutPath() . $xmlPart[$versionIndex]; + + \usleep(VERSION_MTIME_WAIT_TIMEOUT_MICROSEC); // make sure new version gets generated + $response = HttpRequestHelper::sendRequest( $fullUrl, $this->featureContext->getStepLineRef(), @@ -246,7 +249,7 @@ public function theContentLengthOfFileForUserInVersionsFolderIs( } /** - * @Then /^as (?:users|user) "([^"]*)" the authors of the versions of file "([^"]*)" should be:$/ + * @Then /^as (?:users|user) "([^"]*)" the authors of the noncurrent versions of file "([^"]*)" should be:$/ * * @param string $users comma-separated list of usernames * @param string $filename @@ -255,7 +258,7 @@ public function theContentLengthOfFileForUserInVersionsFolderIs( * @return void * @throws Exception */ - public function asUsersAuthorsOfVersionsOfFileShouldBe( + public function asUsersAuthorsOfNoncurrentVersionsOfFileShouldBe( string $users, string $filename, TableNode $table @@ -270,7 +273,7 @@ public function asUsersAuthorsOfVersionsOfFileShouldBe( $actualUsername = $this->featureContext->getActualUsername($username); $this->userGetsVersionMetadataOfFile($actualUsername, $filename); foreach ($requiredVersionMetadata as $versionMetadata) { - $this->featureContext->theAuthorOfEditedVersionFile( + $this->featureContext->theAuthorOfNoncurrentVersionFile( $versionMetadata['index'], $versionMetadata['author'] ); @@ -314,7 +317,7 @@ public function downloadVersion(string $user, string $path, string $index):void } /** - * @Then /^the content of version index "([^"]*)" of file "([^"]*)" for user "([^"]*)" should be "([^"]*)"$/ + * @Then /^the content of noncurrent version index "([^"]*)" of file "([^"]*)" for user "([^"]*)" should be "([^"]*)"$/ * * @param string $index * @param string $path @@ -324,7 +327,7 @@ public function downloadVersion(string $user, string $path, string $index):void * @return void * @throws Exception */ - public function theContentOfVersionIndexOfFileForUserShouldBe( + public function theContentOfNoncurrentVersionIndexOfFileForUserShouldBe( string $index, string $path, string $user, diff --git a/tests/acceptance/features/bootstrap/WebDav.php b/tests/acceptance/features/bootstrap/WebDav.php index c33149929b8a..3b37ea4b8267 100644 --- a/tests/acceptance/features/bootstrap/WebDav.php +++ b/tests/acceptance/features/bootstrap/WebDav.php @@ -511,14 +511,14 @@ public function downloadPreviews(string $user, ?string $path, ?string $doDavRequ } /** - * @Then the number of versions should be :arg1 + * @Then the number of noncurrent versions should be :arg1 * * @param int $number * * @return void * @throws Exception */ - public function theNumberOfVersionsShouldBe(int $number):void { + public function theNumberOfNoncurrentVersionsShouldBe(int $number):void { $resXml = $this->getResponseXmlObject(); if ($resXml === null) { $resXml = HttpRequestHelper::getResponseXml( @@ -5450,7 +5450,7 @@ public function theAdministratorHasEnabledTheFileVersionStorage(string $enabledO $this->runOcc( [ 'config:system:set', - 'file_storage.save_version_author', + 'file_storage.save_version_metadata', '--type', 'boolean', '--value', @@ -5459,7 +5459,7 @@ public function theAdministratorHasEnabledTheFileVersionStorage(string $enabledO } /** - * @Then the author of the created version with index :index should be :expectedUsername + * @Then the author of the noncurrent version with index :index should be :expectedUsername * * @param string $index * @param string $expectedUsername @@ -5467,7 +5467,7 @@ public function theAdministratorHasEnabledTheFileVersionStorage(string $enabledO * @return void * @throws Exception */ - public function theAuthorOfEditedVersionFile(string $index, string $expectedUsername): void { + public function theAuthorOfNoncurrentVersionFile(string $index, string $expectedUsername): void { $expectedUserDisplayName = $this->getUserDisplayName($expectedUsername); $resXml = $this->getResponseXmlObject(); if ($resXml === null) { @@ -5482,7 +5482,7 @@ public function theAuthorOfEditedVersionFile(string $index, string $expectedUser $xmlPart = $resXml->xpath("//oc:meta-version-edited-by"); $authors = []; foreach ($xmlPart as $idx => $author) { - // The first element is the root path element which is not a version + // The first element is the root path element (current version) which is not a noncurrent version // So skipping it if ($idx !== 0) { $authors[] = $author->__toString(); @@ -5506,7 +5506,7 @@ public function theAuthorOfEditedVersionFile(string $index, string $expectedUser $xmlPart = $resXml->xpath("//oc:meta-version-edited-by-name"); $displaynames = []; foreach ($xmlPart as $idx => $displayname) { - // The first element is the root path element which is not a version + // The first element is the root path element (current version) which is not a noncurrent version // So skipping it if ($idx !== 0) { $displaynames[] = $displayname->__toString(); diff --git a/tests/acceptance/features/bootstrap/WebUIFilesContext.php b/tests/acceptance/features/bootstrap/WebUIFilesContext.php index 661663881749..22d550642730 100644 --- a/tests/acceptance/features/bootstrap/WebUIFilesContext.php +++ b/tests/acceptance/features/bootstrap/WebUIFilesContext.php @@ -2649,7 +2649,7 @@ public function theVersionsListShouldContainEntries(int $num):void { } /** - * @Then the author(s) of the version(s) of file/folder :resource should be: + * @Then the author(s) of the current and noncurrent version(s) of file/folder :resource should be: * * @param string $resource * @param TableNode $versionTable @@ -2657,7 +2657,7 @@ public function theVersionsListShouldContainEntries(int $num):void { * @return void * @throws Exception */ - public function theAuthorsOfVersionsOfFileShouldBe(string $resource, TableNode $versionTable):void { + public function theAuthorsOfCurrentAndNoncurrentVersionsOfFileShouldBe(string $resource, TableNode $versionTable):void { $this->featureContext->verifyTableNodeColumns( $versionTable, ['index', 'author'] @@ -2693,6 +2693,8 @@ public function theUserRestoresTheFileToLastVersionUsingTheWebui():void { $this->filesPage->getDetailsDialog()->restoreCurrentFileToLastVersion( $this->getSession() ); + $session = $this->getSession(); + $this->filesPage->waitTillPageIsLoaded($session); } /** diff --git a/tests/acceptance/features/bootstrap/bootstrap.php b/tests/acceptance/features/bootstrap/bootstrap.php index fd23729363eb..eca54480dfc4 100644 --- a/tests/acceptance/features/bootstrap/bootstrap.php +++ b/tests/acceptance/features/bootstrap/bootstrap.php @@ -47,6 +47,9 @@ const MINIMUM_UI_WAIT_TIMEOUT_MILLISEC = 500; const MINIMUM_UI_WAIT_TIMEOUT_MICROSEC = MINIMUM_UI_WAIT_TIMEOUT_MILLISEC * 1000; +// Minimum mtime difference for new version to generate +const VERSION_MTIME_WAIT_TIMEOUT_MICROSEC = 1000000; + // Minimum timeout for emails const EMAIL_WAIT_TIMEOUT_SEC = 10; const EMAIL_WAIT_TIMEOUT_MILLISEC = EMAIL_WAIT_TIMEOUT_SEC * 1000; diff --git a/tests/acceptance/features/lib/FilesPageElement/DetailsDialog.php b/tests/acceptance/features/lib/FilesPageElement/DetailsDialog.php index 01a16e61b86d..e6b3cedb91e1 100644 --- a/tests/acceptance/features/lib/FilesPageElement/DetailsDialog.php +++ b/tests/acceptance/features/lib/FilesPageElement/DetailsDialog.php @@ -67,7 +67,7 @@ class DetailsDialog extends OwncloudPage { private $tagsDropDownResultXpath = "//div[contains(@class, 'systemtags-select2-dropdown')]" . "//ul[@class='select2-results']" . "//span[@class='label']"; - private $tagEditInputXpath = "//input[@id='view12-rename-input']"; + private $tagEditInputXpath = "//input[@id='view13-rename-input']"; protected $tagDeleteConfirmButtonXpath = ".//div[contains(@class, 'oc-dialog-buttonrow twobuttons') and not(ancestor::div[contains(@style, 'display: none')])]//button[text()='Yes']"; @@ -81,7 +81,7 @@ class DetailsDialog extends OwncloudPage { private $versionsListXpath = "//div[@id='versionsTabView']//ul[@class='versions']"; private $versionDetailsXpath = "//div[@id='versionsTabView']//ul[@class='versions']/li//div[@class='version-details']"; - private $lastVersionRevertButton = "//div[@id='versionsTabView']//ul[@class='versions']//li[1]/div/a"; + private $lastVersionRevertButton = "//div[@id='versionsTabView']//ul[@class='versions']//li[2]/div/div[@class='action-container']/a"; /** * @@ -661,6 +661,7 @@ public function getTagsInputFieldItemsXpath(): string { * @return void */ public function restoreCurrentFileToLastVersion(Session $session): void { + \usleep(VERSION_MTIME_WAIT_TIMEOUT_MICROSEC); // make sure new version gets generated $revertBtn = $this->getLastVersionRevertButton(); $revertBtn->click(); $this->waitForAjaxCallsToStartAndFinish($session); diff --git a/tests/acceptance/features/webUIFiles/versions.feature b/tests/acceptance/features/webUIFiles/versions.feature index a61226a7358b..743029cbc2a9 100644 --- a/tests/acceptance/features/webUIFiles/versions.feature +++ b/tests/acceptance/features/webUIFiles/versions.feature @@ -12,7 +12,7 @@ Feature: Versions of a file | Alice | @skipOnStorage:ceph @files_primary_s3-issue-67 - Scenario: upload new file with same name to see if different versions are shown + Scenario: upload new file with same name to see if current version and noncurrent versions are shown Given user "Alice" has uploaded file with content "some content" to "/randomfile.txt" And user "Alice" has logged in using the webUI And the user has browsed to the files page @@ -20,7 +20,7 @@ Feature: Versions of a file And user "Alice" has uploaded file with content "new lorem content" to "/randomfile.txt" When the user browses directly to display the "versions" details of file "randomfile.txt" in folder "/" Then the content of file "randomfile.txt" for user "Alice" should be "new lorem content" - And the versions list should contain 2 entries + And the versions list should contain 3 entries Scenario: restoring file to old version changes the content of the file @@ -33,7 +33,7 @@ Feature: Versions of a file Then the content of file "randomfile.txt" for user "Alice" should be "lorem content" @files_sharing-app-required - Scenario: sharee can see the versions of a file + Scenario: sharee can see the current version and noncurrent versions of a file Given user "Brian" has been created with default attributes and without skeleton files And user "Alice" has uploaded file with content "lorem content" to "/randomfile.txt" And user "Alice" has uploaded file with content "lorem" to "/randomfile.txt" @@ -43,10 +43,10 @@ Feature: Versions of a file And the user has browsed to the files page When the user browses directly to display the "versions" details of file "randomfile.txt" in folder "/" Then the content of file "randomfile.txt" for user "Brian" should be "new lorem content" - And the versions list should contain 2 entries + And the versions list should contain 3 entries @skipOnStorage:ceph @files_primary_s3-issue-155 - Scenario: file versions cannot be seen on the webUI after deleting versions + Scenario: file versions should show only current version on the webUI after deleting versions Given user "Alice" has uploaded file with content "lorem content" to "/randomfile.txt" And user "Alice" has uploaded file with content "lorem" to "/randomfile.txt" And user "Alice" has uploaded file with content "new lorem content" to "/randomfile.txt" @@ -54,7 +54,7 @@ Feature: Versions of a file And the user has browsed to the files page And the administrator has cleared the versions for user "Alice" When the user browses directly to display the "versions" details of file "randomfile.txt" in folder "/" - And the versions list should contain 0 entries + And the versions list should contain 1 entries @skipOnStorage:ceph @files_primary_s3-issue-155 Scenario: file versions cannot be seen on the webUI only for user whose versions is deleted @@ -67,15 +67,15 @@ Feature: Versions of a file And the user has browsed to the files page And the administrator has cleared the versions for user "Alice" When the user browses directly to display the "versions" details of file "randomfile.txt" in folder "/" - Then the versions list should contain 0 entries + Then the versions list should contain 1 entries When the user logs out of the webUI And user "Brian" logs in using the webUI And the user has browsed to the files page And the user browses directly to display the "versions" details of file "randomfile.txt" in folder "/" - Then the versions list should contain 1 entries + Then the versions list should contain 2 entries @skipOnStorage:ceph @files_primary_s3-issue-155 - Scenario: file versions cannot be seen on the webUI for all users after deleting versions for all users + Scenario: file versions show only current version on the webUI for all users after deleting versions for all users Given user "Brian" has been created with default attributes and without skeleton files And user "Alice" has uploaded file with content "lorem content" to "/randomfile.txt" And user "Alice" has uploaded file with content "lorem" to "/randomfile.txt" @@ -85,12 +85,12 @@ Feature: Versions of a file And the user has browsed to the files page And the administrator has cleared the versions for all users When the user browses directly to display the "versions" details of file "randomfile.txt" in folder "/" - Then the versions list should contain 0 entries + Then the versions list should contain 1 entries When the user logs out of the webUI And user "Brian" logs in using the webUI And the user has browsed to the files page And the user browses directly to display the "versions" details of file "randomfile.txt" in folder "/" - Then the versions list should contain 0 entries + Then the versions list should contain 1 entries @skipOnStorage:ceph @files_primary_s3-issue-67 Scenario: versions author is displayed @@ -101,7 +101,7 @@ Feature: Versions of a file And user "Alice" has logged in using the webUI And the user has browsed to the files page When the user browses directly to display the "versions" details of file "randomfile.txt" in folder "/" - Then the authors of the versions of file "randomfile.txt" should be: + Then the authors of the current and noncurrent versions of file "randomfile.txt" should be: | index | author | | 1 | Alice | | 2 | Alice | @@ -120,11 +120,12 @@ Feature: Versions of a file And user "Carol" has logged in using the webUI And the user has browsed to the files page When the user browses directly to display the "versions" details of file "randomfile.txt" in folder "/" - Then the authors of the versions of file "randomfile.txt" should be: + Then the authors of the current and noncurrent versions of file "randomfile.txt" should be: | index | author | | 1 | Brian | - | 2 | Alice | + | 2 | Brian | | 3 | Alice | + | 4 | Alice | @skipOnStorage:ceph @files_primary_s3-issue-67 Scenario: sharee can see the versions' respective author after version restore @@ -140,14 +141,17 @@ Feature: Versions of a file And user "Carol" has logged in using the webUI And the user has browsed to the files page When the user browses directly to display the "versions" details of file "randomfile.txt" in folder "/" - Then the authors of the versions of file "randomfile.txt" should be: + Then the authors of the current and noncurrent versions of file "randomfile.txt" should be: | index | author | - | 1 | Alice | - | 2 | Brian | - | 3 | Alice | + | 1 | Carol | + | 2 | Alice | + | 3 | Brian | + | 4 | Alice | When the user restores the file to last version using the webUI - Then the authors of the versions of file "randomfile.txt" should be: + Then the authors of the current and noncurrent versions of file "randomfile.txt" should be: | index | author | | 1 | Carol | - | 2 | Brian | + | 2 | Carol | | 3 | Alice | + | 4 | Brian | + | 5 | Alice | diff --git a/tests/karma.config.js b/tests/karma.config.js index 42b43ff3be4f..3c04f9a66818 100644 --- a/tests/karma.config.js +++ b/tests/karma.config.js @@ -87,6 +87,7 @@ module.exports = function(config) { srcFiles: [ // need to enforce loading order... 'apps/files_versions/js/versionmodel.js', + 'apps/files_versions/js/versionsrootmodel.js', 'apps/files_versions/js/versioncollection.js', 'apps/files_versions/js/versionstabview.js' ],