aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files_versions
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files_versions')
-rw-r--r--apps/files_versions/.l10nignore5
-rw-r--r--apps/files_versions/ajax/getVersions.php55
-rw-r--r--apps/files_versions/ajax/preview.php65
-rw-r--r--apps/files_versions/ajax/rollbackVersion.php40
-rw-r--r--apps/files_versions/appinfo/app.php26
-rw-r--r--apps/files_versions/appinfo/application.php49
-rw-r--r--apps/files_versions/appinfo/info.xml54
-rw-r--r--apps/files_versions/appinfo/install.php23
-rw-r--r--apps/files_versions/appinfo/register_command.php28
-rw-r--r--apps/files_versions/appinfo/routes.php52
-rw-r--r--apps/files_versions/appinfo/update.php23
-rw-r--r--apps/files_versions/command/cleanup.php114
-rw-r--r--apps/files_versions/command/expire.php64
-rw-r--r--apps/files_versions/composer/autoload.php25
-rw-r--r--apps/files_versions/composer/composer.json13
-rw-r--r--apps/files_versions/composer/composer.lock18
-rw-r--r--apps/files_versions/composer/composer/ClassLoader.php579
-rw-r--r--apps/files_versions/composer/composer/InstalledVersions.php359
-rw-r--r--apps/files_versions/composer/composer/LICENSE21
-rw-r--r--apps/files_versions/composer/composer/autoload_classmap.php52
-rw-r--r--apps/files_versions/composer/composer/autoload_namespaces.php9
-rw-r--r--apps/files_versions/composer/composer/autoload_psr4.php10
-rw-r--r--apps/files_versions/composer/composer/autoload_real.php37
-rw-r--r--apps/files_versions/composer/composer/autoload_static.php78
-rw-r--r--apps/files_versions/composer/composer/installed.json5
-rw-r--r--apps/files_versions/composer/composer/installed.php23
-rw-r--r--apps/files_versions/css/versions.css53
-rw-r--r--apps/files_versions/download.php48
-rw-r--r--apps/files_versions/img/app-dark.svg1
-rw-r--r--apps/files_versions/img/app.svg104
-rw-r--r--apps/files_versions/js/filesplugin.js34
-rw-r--r--apps/files_versions/js/versioncollection.js97
-rw-r--r--apps/files_versions/js/versionmodel.js77
-rw-r--r--apps/files_versions/js/versionstabview.js218
-rw-r--r--apps/files_versions/l10n/ar.js30
-rw-r--r--apps/files_versions/l10n/ar.json30
-rw-r--r--apps/files_versions/l10n/ast.js26
-rw-r--r--apps/files_versions/l10n/ast.json26
-rw-r--r--apps/files_versions/l10n/az.js11
-rw-r--r--apps/files_versions/l10n/az.json9
-rw-r--r--apps/files_versions/l10n/be.js17
-rw-r--r--apps/files_versions/l10n/be.json15
-rw-r--r--apps/files_versions/l10n/bg.js22
-rw-r--r--apps/files_versions/l10n/bg.json20
-rw-r--r--apps/files_versions/l10n/bg_BG.js11
-rw-r--r--apps/files_versions/l10n/bg_BG.json9
-rw-r--r--apps/files_versions/l10n/bn_BD.js11
-rw-r--r--apps/files_versions/l10n/bn_BD.json9
-rw-r--r--apps/files_versions/l10n/bn_IN.js11
-rw-r--r--apps/files_versions/l10n/bn_IN.json9
-rw-r--r--apps/files_versions/l10n/bs.js11
-rw-r--r--apps/files_versions/l10n/bs.json9
-rw-r--r--apps/files_versions/l10n/ca.js27
-rw-r--r--apps/files_versions/l10n/ca.json27
-rw-r--r--apps/files_versions/l10n/cs.js29
-rw-r--r--apps/files_versions/l10n/cs.json27
-rw-r--r--apps/files_versions/l10n/cs_CZ.js11
-rw-r--r--apps/files_versions/l10n/cs_CZ.json9
-rw-r--r--apps/files_versions/l10n/cy_GB.js6
-rw-r--r--apps/files_versions/l10n/cy_GB.json4
-rw-r--r--apps/files_versions/l10n/da.js28
-rw-r--r--apps/files_versions/l10n/da.json28
-rw-r--r--apps/files_versions/l10n/de.js28
-rw-r--r--apps/files_versions/l10n/de.json28
-rw-r--r--apps/files_versions/l10n/de_DE.js28
-rw-r--r--apps/files_versions/l10n/de_DE.json28
-rw-r--r--apps/files_versions/l10n/el.js23
-rw-r--r--apps/files_versions/l10n/el.json23
-rw-r--r--apps/files_versions/l10n/en_GB.js28
-rw-r--r--apps/files_versions/l10n/en_GB.json28
-rw-r--r--apps/files_versions/l10n/eo.js11
-rw-r--r--apps/files_versions/l10n/eo.json9
-rw-r--r--apps/files_versions/l10n/es.js32
-rw-r--r--apps/files_versions/l10n/es.json32
-rw-r--r--apps/files_versions/l10n/es_AR.js11
-rw-r--r--apps/files_versions/l10n/es_AR.json9
-rw-r--r--apps/files_versions/l10n/es_EC.js22
-rw-r--r--apps/files_versions/l10n/es_EC.json20
-rw-r--r--apps/files_versions/l10n/es_MX.js29
-rw-r--r--apps/files_versions/l10n/es_MX.json29
-rw-r--r--apps/files_versions/l10n/et_EE.js28
-rw-r--r--apps/files_versions/l10n/et_EE.json28
-rw-r--r--apps/files_versions/l10n/eu.js27
-rw-r--r--apps/files_versions/l10n/eu.json27
-rw-r--r--apps/files_versions/l10n/fa.js25
-rw-r--r--apps/files_versions/l10n/fa.json25
-rw-r--r--apps/files_versions/l10n/fi.js25
-rw-r--r--apps/files_versions/l10n/fi.json23
-rw-r--r--apps/files_versions/l10n/fi_FI.js11
-rw-r--r--apps/files_versions/l10n/fi_FI.json9
-rw-r--r--apps/files_versions/l10n/fr.js30
-rw-r--r--apps/files_versions/l10n/fr.json30
-rw-r--r--apps/files_versions/l10n/ga.js29
-rw-r--r--apps/files_versions/l10n/ga.json27
-rw-r--r--apps/files_versions/l10n/gl.js27
-rw-r--r--apps/files_versions/l10n/gl.json27
-rw-r--r--apps/files_versions/l10n/he.js11
-rw-r--r--apps/files_versions/l10n/he.json9
-rw-r--r--apps/files_versions/l10n/hr.js11
-rw-r--r--apps/files_versions/l10n/hr.json9
-rw-r--r--apps/files_versions/l10n/hu.js28
-rw-r--r--apps/files_versions/l10n/hu.json26
-rw-r--r--apps/files_versions/l10n/hu_HU.js11
-rw-r--r--apps/files_versions/l10n/hu_HU.json9
-rw-r--r--apps/files_versions/l10n/hy.js8
-rw-r--r--apps/files_versions/l10n/hy.json6
-rw-r--r--apps/files_versions/l10n/id.js11
-rw-r--r--apps/files_versions/l10n/id.json9
-rw-r--r--apps/files_versions/l10n/is.js26
-rw-r--r--apps/files_versions/l10n/is.json26
-rw-r--r--apps/files_versions/l10n/it.js30
-rw-r--r--apps/files_versions/l10n/it.json30
-rw-r--r--apps/files_versions/l10n/ja.js28
-rw-r--r--apps/files_versions/l10n/ja.json28
-rw-r--r--apps/files_versions/l10n/ka.js25
-rw-r--r--apps/files_versions/l10n/ka.json23
-rw-r--r--apps/files_versions/l10n/ka_GE.js8
-rw-r--r--apps/files_versions/l10n/ka_GE.json6
-rw-r--r--apps/files_versions/l10n/km.js11
-rw-r--r--apps/files_versions/l10n/km.json9
-rw-r--r--apps/files_versions/l10n/kn.js11
-rw-r--r--apps/files_versions/l10n/kn.json9
-rw-r--r--apps/files_versions/l10n/ko.js24
-rw-r--r--apps/files_versions/l10n/ko.json24
-rw-r--r--apps/files_versions/l10n/ku_IQ.js6
-rw-r--r--apps/files_versions/l10n/ku_IQ.json4
-rw-r--r--apps/files_versions/l10n/lb.js11
-rw-r--r--apps/files_versions/l10n/lb.json9
-rw-r--r--apps/files_versions/l10n/lt_LT.js23
-rw-r--r--apps/files_versions/l10n/lt_LT.json23
-rw-r--r--apps/files_versions/l10n/lv.js11
-rw-r--r--apps/files_versions/l10n/lv.json9
-rw-r--r--apps/files_versions/l10n/mk.js28
-rw-r--r--apps/files_versions/l10n/mk.json28
-rw-r--r--apps/files_versions/l10n/ms_MY.js11
-rw-r--r--apps/files_versions/l10n/ms_MY.json9
-rw-r--r--apps/files_versions/l10n/nb.js28
-rw-r--r--apps/files_versions/l10n/nb.json26
-rw-r--r--apps/files_versions/l10n/nb_NO.js11
-rw-r--r--apps/files_versions/l10n/nb_NO.json9
-rw-r--r--apps/files_versions/l10n/nl.js28
-rw-r--r--apps/files_versions/l10n/nl.json28
-rw-r--r--apps/files_versions/l10n/nn_NO.js11
-rw-r--r--apps/files_versions/l10n/nn_NO.json9
-rw-r--r--apps/files_versions/l10n/oc.js11
-rw-r--r--apps/files_versions/l10n/oc.json9
-rw-r--r--apps/files_versions/l10n/pl.js30
-rw-r--r--apps/files_versions/l10n/pl.json30
-rw-r--r--apps/files_versions/l10n/pt_BR.js30
-rw-r--r--apps/files_versions/l10n/pt_BR.json30
-rw-r--r--apps/files_versions/l10n/pt_PT.js11
-rw-r--r--apps/files_versions/l10n/pt_PT.json9
-rw-r--r--apps/files_versions/l10n/ro.js10
-rw-r--r--apps/files_versions/l10n/ro.json8
-rw-r--r--apps/files_versions/l10n/ru.js28
-rw-r--r--apps/files_versions/l10n/ru.json28
-rw-r--r--apps/files_versions/l10n/si_LK.js6
-rw-r--r--apps/files_versions/l10n/si_LK.json4
-rw-r--r--apps/files_versions/l10n/sk.js29
-rw-r--r--apps/files_versions/l10n/sk.json27
-rw-r--r--apps/files_versions/l10n/sk_SK.js11
-rw-r--r--apps/files_versions/l10n/sk_SK.json9
-rw-r--r--apps/files_versions/l10n/sl.js22
-rw-r--r--apps/files_versions/l10n/sl.json22
-rw-r--r--apps/files_versions/l10n/sq.js11
-rw-r--r--apps/files_versions/l10n/sq.json9
-rw-r--r--apps/files_versions/l10n/sr.js28
-rw-r--r--apps/files_versions/l10n/sr.json28
-rw-r--r--apps/files_versions/l10n/sr@latin.js11
-rw-r--r--apps/files_versions/l10n/sr@latin.json9
-rw-r--r--apps/files_versions/l10n/sv.js28
-rw-r--r--apps/files_versions/l10n/sv.json28
-rw-r--r--apps/files_versions/l10n/sw.js29
-rw-r--r--apps/files_versions/l10n/sw.json27
-rw-r--r--apps/files_versions/l10n/ta_LK.js6
-rw-r--r--apps/files_versions/l10n/ta_LK.json4
-rw-r--r--apps/files_versions/l10n/th_TH.js11
-rw-r--r--apps/files_versions/l10n/th_TH.json9
-rw-r--r--apps/files_versions/l10n/tr.js28
-rw-r--r--apps/files_versions/l10n/tr.json28
-rw-r--r--apps/files_versions/l10n/ug.js27
-rw-r--r--apps/files_versions/l10n/ug.json27
-rw-r--r--apps/files_versions/l10n/uk.js30
-rw-r--r--apps/files_versions/l10n/uk.json30
-rw-r--r--apps/files_versions/l10n/ur_PK.js6
-rw-r--r--apps/files_versions/l10n/ur_PK.json4
-rw-r--r--apps/files_versions/l10n/vi.js24
-rw-r--r--apps/files_versions/l10n/vi.json24
-rw-r--r--apps/files_versions/l10n/zh_CN.js28
-rw-r--r--apps/files_versions/l10n/zh_CN.json28
-rw-r--r--apps/files_versions/l10n/zh_HK.js25
-rw-r--r--apps/files_versions/l10n/zh_HK.json25
-rw-r--r--apps/files_versions/l10n/zh_TW.js28
-rw-r--r--apps/files_versions/l10n/zh_TW.json28
-rw-r--r--apps/files_versions/lib/AppInfo/Application.php151
-rw-r--r--apps/files_versions/lib/BackgroundJob/ExpireVersions.php68
-rw-r--r--apps/files_versions/lib/Capabilities.php35
-rw-r--r--apps/files_versions/lib/Command/CleanUp.php115
-rw-r--r--apps/files_versions/lib/Command/Expire.php49
-rw-r--r--apps/files_versions/lib/Command/ExpireVersions.php96
-rw-r--r--apps/files_versions/lib/Controller/PreviewController.php92
-rw-r--r--apps/files_versions/lib/Db/VersionEntity.php74
-rw-r--r--apps/files_versions/lib/Db/VersionsMapper.php104
-rw-r--r--apps/files_versions/lib/Events/CreateVersionEvent.php60
-rw-r--r--apps/files_versions/lib/Events/VersionCreatedEvent.php39
-rw-r--r--apps/files_versions/lib/Events/VersionRestoredEvent.php33
-rw-r--r--apps/files_versions/lib/Expiration.php (renamed from apps/files_versions/lib/expiration.php)96
-rw-r--r--apps/files_versions/lib/Listener/FileEventsListener.php472
-rw-r--r--apps/files_versions/lib/Listener/LegacyRollbackListener.php35
-rw-r--r--apps/files_versions/lib/Listener/LoadAdditionalListener.php28
-rw-r--r--apps/files_versions/lib/Listener/LoadSidebarListener.php28
-rw-r--r--apps/files_versions/lib/Listener/VersionAuthorListener.php56
-rw-r--r--apps/files_versions/lib/Listener/VersionStorageMoveListener.php140
-rw-r--r--apps/files_versions/lib/Migration/Version1020Date20221114144058.php67
-rw-r--r--apps/files_versions/lib/Sabre/Plugin.php99
-rw-r--r--apps/files_versions/lib/Sabre/RestoreFolder.php61
-rw-r--r--apps/files_versions/lib/Sabre/RootCollection.php55
-rw-r--r--apps/files_versions/lib/Sabre/VersionCollection.php81
-rw-r--r--apps/files_versions/lib/Sabre/VersionFile.php108
-rw-r--r--apps/files_versions/lib/Sabre/VersionHome.php82
-rw-r--r--apps/files_versions/lib/Sabre/VersionRoot.php81
-rw-r--r--apps/files_versions/lib/Storage.php1007
-rw-r--r--apps/files_versions/lib/Versions/BackendNotFoundException.php10
-rw-r--r--apps/files_versions/lib/Versions/IDeletableVersionBackend.php21
-rw-r--r--apps/files_versions/lib/Versions/IMetadataVersion.php31
-rw-r--r--apps/files_versions/lib/Versions/IMetadataVersionBackend.php29
-rw-r--r--apps/files_versions/lib/Versions/INameableVersion.php23
-rw-r--r--apps/files_versions/lib/Versions/INameableVersionBackend.php22
-rw-r--r--apps/files_versions/lib/Versions/INeedSyncVersionBackend.php25
-rw-r--r--apps/files_versions/lib/Versions/IVersion.php85
-rw-r--r--apps/files_versions/lib/Versions/IVersionBackend.php89
-rw-r--r--apps/files_versions/lib/Versions/IVersionManager.php31
-rw-r--r--apps/files_versions/lib/Versions/IVersionsImporterBackend.php33
-rw-r--r--apps/files_versions/lib/Versions/LegacyVersionsBackend.php395
-rw-r--r--apps/files_versions/lib/Versions/Version.php72
-rw-r--r--apps/files_versions/lib/Versions/VersionManager.php210
-rw-r--r--apps/files_versions/lib/backgroundjob/expireversions.php98
-rw-r--r--apps/files_versions/lib/capabilities.php43
-rw-r--r--apps/files_versions/lib/hooks.php170
-rw-r--r--apps/files_versions/lib/storage.php808
-rw-r--r--apps/files_versions/openapi.json164
-rw-r--r--apps/files_versions/openapi.json.license2
-rw-r--r--apps/files_versions/src/components/Version.vue393
-rw-r--r--apps/files_versions/src/components/VersionLabelDialog.vue123
-rw-r--r--apps/files_versions/src/components/VirtualScrolling.vue346
-rw-r--r--apps/files_versions/src/css/versions.css103
-rw-r--r--apps/files_versions/src/files_versions_tab.js62
-rw-r--r--apps/files_versions/src/utils/davClient.js29
-rw-r--r--apps/files_versions/src/utils/davRequest.js20
-rw-r--r--apps/files_versions/src/utils/logger.js11
-rw-r--r--apps/files_versions/src/utils/versions.ts133
-rw-r--r--apps/files_versions/src/views/VersionTab.vue307
-rw-r--r--apps/files_versions/tests/BackgroundJob/ExpireVersionsTest.php55
-rw-r--r--apps/files_versions/tests/Command/CleanupTest.php162
-rw-r--r--apps/files_versions/tests/Command/ExpireTest.php28
-rw-r--r--apps/files_versions/tests/Controller/PreviewControllerTest.php155
-rw-r--r--apps/files_versions/tests/ExpirationTest.php114
-rw-r--r--apps/files_versions/tests/StorageTest.php101
-rw-r--r--apps/files_versions/tests/VersioningTest.php998
-rw-r--r--apps/files_versions/tests/Versions/VersionManagerTest.php140
-rw-r--r--apps/files_versions/tests/command/cleanuptest.php170
-rw-r--r--apps/files_versions/tests/command/expiretest.php43
-rw-r--r--apps/files_versions/tests/expirationtest.php206
-rw-r--r--apps/files_versions/tests/js/versioncollectionSpec.js161
-rw-r--r--apps/files_versions/tests/js/versionmodelSpec.js96
-rw-r--r--apps/files_versions/tests/js/versionstabviewSpec.js208
-rw-r--r--apps/files_versions/tests/versions.php848
267 files changed, 11279 insertions, 5043 deletions
diff --git a/apps/files_versions/.l10nignore b/apps/files_versions/.l10nignore
new file mode 100644
index 00000000000..80d07dc83e7
--- /dev/null
+++ b/apps/files_versions/.l10nignore
@@ -0,0 +1,5 @@
+# SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+# SPDX-License-Identifier: AGPL-3.0-or-later
+# compiled vue templates
+js/
+src/templates.js
diff --git a/apps/files_versions/ajax/getVersions.php b/apps/files_versions/ajax/getVersions.php
deleted file mode 100644
index 7d704c14618..00000000000
--- a/apps/files_versions/ajax/getVersions.php
+++ /dev/null
@@ -1,55 +0,0 @@
-<?php
-/**
- * @author Bart Visscher <bartv@thisnet.nl>
- * @author Björn Schießle <schiessle@owncloud.com>
- * @author Frank Karlitschek <frank@owncloud.org>
- * @author Lukas Reschke <lukas@owncloud.com>
- * @author Sam Tuke <mail@samtuke.com>
- * @author Vincent Petry <pvince81@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-OCP\JSON::checkLoggedIn();
-OCP\JSON::callCheck();
-OCP\JSON::checkAppEnabled('files_versions');
-
-$source = (string)$_GET['source'];
-$start = (int)$_GET['start'];
-list ($uid, $filename) = OCA\Files_Versions\Storage::getUidAndFilename($source);
-$count = 5; //show the newest revisions
-$versions = OCA\Files_Versions\Storage::getVersions($uid, $filename, $source);
-if( $versions ) {
-
- $endReached = false;
- if (count($versions) <= $start+$count) {
- $endReached = true;
- }
-
- $versions = array_slice($versions, $start, $count);
-
- // remove owner path from request to not disclose it to the recipient
- foreach ($versions as $version) {
- unset($version['path']);
- }
-
- \OCP\JSON::success(array('data' => array('versions' => $versions, 'endReached' => $endReached)));
-
-} else {
-
- \OCP\JSON::success(array('data' => array('versions' => [], 'endReached' => true)));
-
-}
diff --git a/apps/files_versions/ajax/preview.php b/apps/files_versions/ajax/preview.php
deleted file mode 100644
index c0a9828b1f0..00000000000
--- a/apps/files_versions/ajax/preview.php
+++ /dev/null
@@ -1,65 +0,0 @@
-<?php
-/**
- * @author Björn Schießle <schiessle@owncloud.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <rullzer@owncloud.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <pvince81@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-\OC_Util::checkLoggedIn();
-
-if(!\OC_App::isEnabled('files_versions')){
- exit;
-}
-
-$file = array_key_exists('file', $_GET) ? (string) urldecode($_GET['file']) : '';
-$maxX = array_key_exists('x', $_GET) ? (int) $_GET['x'] : 44;
-$maxY = array_key_exists('y', $_GET) ? (int) $_GET['y'] : 44;
-$version = array_key_exists('version', $_GET) ? $_GET['version'] : '';
-$scalingUp = array_key_exists('scalingup', $_GET) ? (bool) $_GET['scalingup'] : true;
-
-if($file === '' && $version === '') {
- \OC_Response::setStatus(400); //400 Bad Request
- \OCP\Util::writeLog('versions-preview', 'No file parameter was passed', \OCP\Util::DEBUG);
- exit;
-}
-
-if($maxX === 0 || $maxY === 0) {
- \OC_Response::setStatus(400); //400 Bad Request
- \OCP\Util::writeLog('versions-preview', 'x and/or y set to 0', \OCP\Util::DEBUG);
- exit;
-}
-
-try {
- list($user, $file) = \OCA\Files_Versions\Storage::getUidAndFilename($file);
- $preview = new \OC\Preview($user, 'files_versions', $file.'.v'.$version);
- $mimetype = \OC::$server->getMimeTypeDetector()->detectPath($file);
- $preview->setMimetype($mimetype);
- $preview->setMaxX($maxX);
- $preview->setMaxY($maxY);
- $preview->setScalingUp($scalingUp);
-
- $preview->showPreview();
-} catch (\OCP\Files\NotFoundException $e) {
- \OC_Response::setStatus(404);
- \OCP\Util::writeLog('core', $e->getmessage(), \OCP\Util::DEBUG);
-} catch (\Exception $e) {
- \OC_Response::setStatus(500);
- \OCP\Util::writeLog('core', $e->getmessage(), \OCP\Util::DEBUG);
-}
diff --git a/apps/files_versions/ajax/rollbackVersion.php b/apps/files_versions/ajax/rollbackVersion.php
deleted file mode 100644
index 7700c03a144..00000000000
--- a/apps/files_versions/ajax/rollbackVersion.php
+++ /dev/null
@@ -1,40 +0,0 @@
-<?php
-/**
- * @author Bart Visscher <bartv@thisnet.nl>
- * @author Björn Schießle <schiessle@owncloud.com>
- * @author Frank Karlitschek <frank@owncloud.org>
- * @author Lukas Reschke <lukas@owncloud.com>
- * @author Robin Appelman <icewind@owncloud.com>
- * @author Sam Tuke <mail@samtuke.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Thomas Tanghus <thomas@tanghus.net>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-OCP\JSON::checkLoggedIn();
-OCP\JSON::checkAppEnabled('files_versions');
-OCP\JSON::callCheck();
-
-$file = (string)$_GET['file'];
-$revision=(int)$_GET['revision'];
-
-if(OCA\Files_Versions\Storage::rollback( $file, $revision )) {
- OCP\JSON::success(array("data" => array( "revision" => $revision, "file" => $file )));
-}else{
- $l = \OC::$server->getL10N('files_versions');
- OCP\JSON::error(array("data" => array( "message" => $l->t("Could not revert: %s", array($file) ))));
-}
diff --git a/apps/files_versions/appinfo/app.php b/apps/files_versions/appinfo/app.php
deleted file mode 100644
index b653d0d345b..00000000000
--- a/apps/files_versions/appinfo/app.php
+++ /dev/null
@@ -1,26 +0,0 @@
-<?php
-/**
- * @author Björn Schießle <schiessle@owncloud.com>
- * @author Frank Karlitschek <frank@owncloud.org>
- * @author Victor Dubiniuk <dubiniuk@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-\OCP\Util::addStyle('files_versions', 'versions');
-
-\OCA\Files_Versions\Hooks::connectHooks();
diff --git a/apps/files_versions/appinfo/application.php b/apps/files_versions/appinfo/application.php
deleted file mode 100644
index b32cf54729d..00000000000
--- a/apps/files_versions/appinfo/application.php
+++ /dev/null
@@ -1,49 +0,0 @@
-<?php
-/**
- * @author Roeland Jago Douma <rullzer@owncloud.com>
- * @author Victor Dubiniuk <dubiniuk@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\Files_Versions\AppInfo;
-
-use OCP\AppFramework\App;
-use OCA\Files_Versions\Expiration;
-
-class Application extends App {
- public function __construct(array $urlParams = array()) {
- parent::__construct('files_versions', $urlParams);
-
- $container = $this->getContainer();
-
- /*
- * Register capabilities
- */
- $container->registerCapability('OCA\Files_Versions\Capabilities');
-
- /*
- * Register expiration
- */
- $container->registerService('Expiration', function($c) {
- return new Expiration(
- $c->query('ServerContainer')->getConfig(),
- $c->query('OCP\AppFramework\Utility\ITimeFactory')
- );
- });
- }
-}
diff --git a/apps/files_versions/appinfo/info.xml b/apps/files_versions/appinfo/info.xml
index ad0354a5101..7bd11e40cd8 100644
--- a/apps/files_versions/appinfo/info.xml
+++ b/apps/files_versions/appinfo/info.xml
@@ -1,23 +1,55 @@
<?xml version="1.0"?>
-<info>
+<!--
+ - SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-FileCopyrightText: 2012-2016 owncloud, Inc.
+ - SPDX-License-Identifier: AGPL-3.0-only
+-->
+<info xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance"
+ xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
<id>files_versions</id>
<name>Versions</name>
- <licence>AGPL</licence>
- <author>Frank Karlitschek, Bjoern Schiessle</author>
+ <summary>This application automatically maintains older versions of files that are changed.</summary>
<description>
- This application enables ownCloud to automatically maintain older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user’s directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. ownCloud then automatically manages the versions folder to ensure the user doesn’t run out of Quota because of versions.
-In addition to the expiry of versions, ownCloud’s versions app makes certain never to use more than 50% of the user’s currently available free space. If stored versions exceed this limit, ownCloud will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation.
-
+ This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.
+ In addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation.
</description>
- <version>1.3.0</version>
+ <version>1.25.0</version>
+ <licence>agpl</licence>
+ <author>Frank Karlitschek</author>
+ <author>Bjoern Schiessle</author>
+ <namespace>Files_Versions</namespace>
<types>
<filesystem/>
+ <dav/>
</types>
- <dependencies>
- <owncloud min-version="9.1" max-version="9.1" />
- </dependencies>
<documentation>
<user>user-versions</user>
</documentation>
- <default_enable/>
+ <category>files</category>
+ <bugs>https://github.com/nextcloud/server/issues</bugs>
+ <dependencies>
+ <nextcloud min-version="32" max-version="32"/>
+ </dependencies>
+
+ <background-jobs>
+ <job>OCA\Files_Versions\BackgroundJob\ExpireVersions</job>
+ </background-jobs>
+
+ <commands>
+ <command>OCA\Files_Versions\Command\CleanUp</command>
+ <command>OCA\Files_Versions\Command\ExpireVersions</command>
+ </commands>
+
+ <sabre>
+ <plugins>
+ <plugin>OCA\Files_Versions\Sabre\Plugin</plugin>
+ </plugins>
+ <collections>
+ <collection>OCA\Files_Versions\Sabre\RootCollection</collection>
+ </collections>
+ </sabre>
+
+ <versions>
+ <backend for="OCP\Files\Storage\IStorage">OCA\Files_Versions\Versions\LegacyVersionsBackend</backend>
+ </versions>
</info>
diff --git a/apps/files_versions/appinfo/install.php b/apps/files_versions/appinfo/install.php
deleted file mode 100644
index 81ff6337aa0..00000000000
--- a/apps/files_versions/appinfo/install.php
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php
-/**
- * @author Victor Dubiniuk <dubiniuk@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-// Cron job for deleting expired trash items
-\OC::$server->getJobList()->add('OCA\Files_Versions\BackgroundJob\ExpireVersions');
diff --git a/apps/files_versions/appinfo/register_command.php b/apps/files_versions/appinfo/register_command.php
deleted file mode 100644
index ee0e860dfba..00000000000
--- a/apps/files_versions/appinfo/register_command.php
+++ /dev/null
@@ -1,28 +0,0 @@
-<?php
-/**
- * @author Björn Schießle <schiessle@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-
-use OCA\Files_Versions\Command\CleanUp;
-
-$userManager = OC::$server->getUserManager();
-$rootFolder = \OC::$server->getRootFolder();
-/** @var Symfony\Component\Console\Application $application */
-$application->add(new CleanUp($rootFolder, $userManager));
diff --git a/apps/files_versions/appinfo/routes.php b/apps/files_versions/appinfo/routes.php
index 4db52cb3870..22ded5e8723 100644
--- a/apps/files_versions/appinfo/routes.php
+++ b/apps/files_versions/appinfo/routes.php
@@ -1,44 +1,18 @@
<?php
+
/**
- * @author Björn Schießle <schiessle@owncloud.com>
- * @author Jörn Friedrich Dreyer <jfd@butonic.de>
- * @author Lukas Reschke <lukas@owncloud.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <rullzer@owncloud.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Tom Needham <tom@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
-
namespace OCA\Files_Versions\AppInfo;
-$application = new Application();
-
-/** @var $this \OCP\Route\IRouter */
-$this->create('core_ajax_versions_preview', '/preview')->action(
-function() {
- require_once __DIR__ . '/../ajax/preview.php';
-});
-
-$this->create('files_versions_download', 'download.php')
- ->actionInclude('files_versions/download.php');
-$this->create('files_versions_ajax_getVersions', 'ajax/getVersions.php')
- ->actionInclude('files_versions/ajax/getVersions.php');
-$this->create('files_versions_ajax_rollbackVersion', 'ajax/rollbackVersion.php')
- ->actionInclude('files_versions/ajax/rollbackVersion.php');
-
+return [
+ 'routes' => [
+ [
+ 'name' => 'Preview#getPreview',
+ 'url' => '/preview',
+ 'verb' => 'GET',
+ ],
+ ],
+];
diff --git a/apps/files_versions/appinfo/update.php b/apps/files_versions/appinfo/update.php
deleted file mode 100644
index 09ac6ce8a2f..00000000000
--- a/apps/files_versions/appinfo/update.php
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php
-/**
- * @author Victor Dubiniuk <dubiniuk@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-// Cron job for deleting expired trash items
-\OC::$server->getJobList()->add('OCA\Files_Versions\BackgroundJob\ExpireVersions');
diff --git a/apps/files_versions/command/cleanup.php b/apps/files_versions/command/cleanup.php
deleted file mode 100644
index 1abf62763b1..00000000000
--- a/apps/files_versions/command/cleanup.php
+++ /dev/null
@@ -1,114 +0,0 @@
-<?php
-/**
- * @author Björn Schießle <schiessle@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\Files_Versions\Command;
-
-
-use OCP\Files\IRootFolder;
-use OCP\IUserBackend;
-use OCP\IUserManager;
-use Symfony\Component\Console\Command\Command;
-use Symfony\Component\Console\Input\InputArgument;
-use Symfony\Component\Console\Input\InputInterface;
-use Symfony\Component\Console\Output\OutputInterface;
-
-class CleanUp extends Command {
-
- /** @var IUserManager */
- protected $userManager;
-
- /** @var IRootFolder */
- protected $rootFolder;
-
- /**
- * @param IRootFolder $rootFolder
- * @param IUserManager $userManager
- */
- function __construct(IRootFolder $rootFolder, IUserManager $userManager) {
- parent::__construct();
- $this->userManager = $userManager;
- $this->rootFolder = $rootFolder;
- }
-
- protected function configure() {
- $this
- ->setName('versions:cleanup')
- ->setDescription('Delete versions')
- ->addArgument(
- 'user_id',
- InputArgument::OPTIONAL | InputArgument::IS_ARRAY,
- 'delete versions of the given user(s), if no user is given all versions will be deleted'
- );
- }
-
-
- protected function execute(InputInterface $input, OutputInterface $output) {
-
- $users = $input->getArgument('user_id');
- if (!empty($users)) {
- foreach ($users as $user) {
- if ($this->userManager->userExists($user)) {
- $output->writeln("Delete versions of <info>$user</info>");
- $this->deleteVersions($user);
- } else {
- $output->writeln("<error>Unknown user $user</error>");
- }
- }
- } else {
- $output->writeln('Delete all versions');
- foreach ($this->userManager->getBackends() as $backend) {
- $name = get_class($backend);
-
- if ($backend instanceof IUserBackend) {
- $name = $backend->getBackendName();
- }
-
- $output->writeln("Delete versions for users on backend <info>$name</info>");
-
- $limit = 500;
- $offset = 0;
- do {
- $users = $backend->getUsers('', $limit, $offset);
- foreach ($users as $user) {
- $output->writeln(" <info>$user</info>");
- $this->deleteVersions($user);
- }
- $offset += $limit;
- } while (count($users) >= $limit);
- }
- }
- }
-
-
- /**
- * delete versions for the given user
- *
- * @param string $user
- */
- protected function deleteVersions($user) {
- \OC_Util::tearDownFS();
- \OC_Util::setupFS($user);
- if ($this->rootFolder->nodeExists('/' . $user . '/files_versions')) {
- $this->rootFolder->get('/' . $user . '/files_versions')->delete();
- }
- }
-
-}
diff --git a/apps/files_versions/command/expire.php b/apps/files_versions/command/expire.php
deleted file mode 100644
index b1f72980633..00000000000
--- a/apps/files_versions/command/expire.php
+++ /dev/null
@@ -1,64 +0,0 @@
-<?php
-/**
- * @author Joas Schilling <nickvergessen@owncloud.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <icewind@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\Files_Versions\Command;
-
-use OC\Command\FileAccess;
-use OCA\Files_Versions\Storage;
-use OCP\Command\ICommand;
-
-class Expire implements ICommand {
- use FileAccess;
-
- /**
- * @var string
- */
- private $fileName;
-
- /**
- * @var string
- */
- private $user;
-
- /**
- * @param string $user
- * @param string $fileName
- */
- function __construct($user, $fileName) {
- $this->user = $user;
- $this->fileName = $fileName;
- }
-
-
- public function handle() {
- $userManager = \OC::$server->getUserManager();
- if (!$userManager->userExists($this->user)) {
- // User has been deleted already
- return;
- }
-
- \OC_Util::setupFS($this->user);
- Storage::expire($this->fileName);
- \OC_Util::tearDownFS();
- }
-}
diff --git a/apps/files_versions/composer/autoload.php b/apps/files_versions/composer/autoload.php
new file mode 100644
index 00000000000..4505dd14227
--- /dev/null
+++ b/apps/files_versions/composer/autoload.php
@@ -0,0 +1,25 @@
+<?php
+
+// autoload.php @generated by Composer
+
+if (PHP_VERSION_ID < 50600) {
+ if (!headers_sent()) {
+ header('HTTP/1.1 500 Internal Server Error');
+ }
+ $err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
+ if (!ini_get('display_errors')) {
+ if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
+ fwrite(STDERR, $err);
+ } elseif (!headers_sent()) {
+ echo $err;
+ }
+ }
+ trigger_error(
+ $err,
+ E_USER_ERROR
+ );
+}
+
+require_once __DIR__ . '/composer/autoload_real.php';
+
+return ComposerAutoloaderInitFiles_Versions::getLoader();
diff --git a/apps/files_versions/composer/composer.json b/apps/files_versions/composer/composer.json
new file mode 100644
index 00000000000..790e9547113
--- /dev/null
+++ b/apps/files_versions/composer/composer.json
@@ -0,0 +1,13 @@
+{
+ "config" : {
+ "vendor-dir": ".",
+ "optimize-autoloader": true,
+ "classmap-authoritative": true,
+ "autoloader-suffix": "Files_Versions"
+ },
+ "autoload" : {
+ "psr-4": {
+ "OCA\\Files_Versions\\": "../lib/"
+ }
+ }
+}
diff --git a/apps/files_versions/composer/composer.lock b/apps/files_versions/composer/composer.lock
new file mode 100644
index 00000000000..fd0bcbcb753
--- /dev/null
+++ b/apps/files_versions/composer/composer.lock
@@ -0,0 +1,18 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "d751713988987e9331980363e24189ce",
+ "packages": [],
+ "packages-dev": [],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": [],
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": [],
+ "platform-dev": [],
+ "plugin-api-version": "2.1.0"
+}
diff --git a/apps/files_versions/composer/composer/ClassLoader.php b/apps/files_versions/composer/composer/ClassLoader.php
new file mode 100644
index 00000000000..7824d8f7eaf
--- /dev/null
+++ b/apps/files_versions/composer/composer/ClassLoader.php
@@ -0,0 +1,579 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ * Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Autoload;
+
+/**
+ * ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
+ *
+ * $loader = new \Composer\Autoload\ClassLoader();
+ *
+ * // register classes with namespaces
+ * $loader->add('Symfony\Component', __DIR__.'/component');
+ * $loader->add('Symfony', __DIR__.'/framework');
+ *
+ * // activate the autoloader
+ * $loader->register();
+ *
+ * // to enable searching the include path (eg. for PEAR packages)
+ * $loader->setUseIncludePath(true);
+ *
+ * In this example, if you try to use a class in the Symfony\Component
+ * namespace or one of its children (Symfony\Component\Console for instance),
+ * the autoloader will first look for the class under the component/
+ * directory, and it will then fallback to the framework/ directory if not
+ * found before giving up.
+ *
+ * This class is loosely based on the Symfony UniversalClassLoader.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ * @see https://www.php-fig.org/psr/psr-0/
+ * @see https://www.php-fig.org/psr/psr-4/
+ */
+class ClassLoader
+{
+ /** @var \Closure(string):void */
+ private static $includeFile;
+
+ /** @var string|null */
+ private $vendorDir;
+
+ // PSR-4
+ /**
+ * @var array<string, array<string, int>>
+ */
+ private $prefixLengthsPsr4 = array();
+ /**
+ * @var array<string, list<string>>
+ */
+ private $prefixDirsPsr4 = array();
+ /**
+ * @var list<string>
+ */
+ private $fallbackDirsPsr4 = array();
+
+ // PSR-0
+ /**
+ * List of PSR-0 prefixes
+ *
+ * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
+ *
+ * @var array<string, array<string, list<string>>>
+ */
+ private $prefixesPsr0 = array();
+ /**
+ * @var list<string>
+ */
+ private $fallbackDirsPsr0 = array();
+
+ /** @var bool */
+ private $useIncludePath = false;
+
+ /**
+ * @var array<string, string>
+ */
+ private $classMap = array();
+
+ /** @var bool */
+ private $classMapAuthoritative = false;
+
+ /**
+ * @var array<string, bool>
+ */
+ private $missingClasses = array();
+
+ /** @var string|null */
+ private $apcuPrefix;
+
+ /**
+ * @var array<string, self>
+ */
+ private static $registeredLoaders = array();
+
+ /**
+ * @param string|null $vendorDir
+ */
+ public function __construct($vendorDir = null)
+ {
+ $this->vendorDir = $vendorDir;
+ self::initializeIncludeClosure();
+ }
+
+ /**
+ * @return array<string, list<string>>
+ */
+ public function getPrefixes()
+ {
+ if (!empty($this->prefixesPsr0)) {
+ return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
+ }
+
+ return array();
+ }
+
+ /**
+ * @return array<string, list<string>>
+ */
+ public function getPrefixesPsr4()
+ {
+ return $this->prefixDirsPsr4;
+ }
+
+ /**
+ * @return list<string>
+ */
+ public function getFallbackDirs()
+ {
+ return $this->fallbackDirsPsr0;
+ }
+
+ /**
+ * @return list<string>
+ */
+ public function getFallbackDirsPsr4()
+ {
+ return $this->fallbackDirsPsr4;
+ }
+
+ /**
+ * @return array<string, string> Array of classname => path
+ */
+ public function getClassMap()
+ {
+ return $this->classMap;
+ }
+
+ /**
+ * @param array<string, string> $classMap Class to filename map
+ *
+ * @return void
+ */
+ public function addClassMap(array $classMap)
+ {
+ if ($this->classMap) {
+ $this->classMap = array_merge($this->classMap, $classMap);
+ } else {
+ $this->classMap = $classMap;
+ }
+ }
+
+ /**
+ * Registers a set of PSR-0 directories for a given prefix, either
+ * appending or prepending to the ones previously set for this prefix.
+ *
+ * @param string $prefix The prefix
+ * @param list<string>|string $paths The PSR-0 root directories
+ * @param bool $prepend Whether to prepend the directories
+ *
+ * @return void
+ */
+ public function add($prefix, $paths, $prepend = false)
+ {
+ $paths = (array) $paths;
+ if (!$prefix) {
+ if ($prepend) {
+ $this->fallbackDirsPsr0 = array_merge(
+ $paths,
+ $this->fallbackDirsPsr0
+ );
+ } else {
+ $this->fallbackDirsPsr0 = array_merge(
+ $this->fallbackDirsPsr0,
+ $paths
+ );
+ }
+
+ return;
+ }
+
+ $first = $prefix[0];
+ if (!isset($this->prefixesPsr0[$first][$prefix])) {
+ $this->prefixesPsr0[$first][$prefix] = $paths;
+
+ return;
+ }
+ if ($prepend) {
+ $this->prefixesPsr0[$first][$prefix] = array_merge(
+ $paths,
+ $this->prefixesPsr0[$first][$prefix]
+ );
+ } else {
+ $this->prefixesPsr0[$first][$prefix] = array_merge(
+ $this->prefixesPsr0[$first][$prefix],
+ $paths
+ );
+ }
+ }
+
+ /**
+ * Registers a set of PSR-4 directories for a given namespace, either
+ * appending or prepending to the ones previously set for this namespace.
+ *
+ * @param string $prefix The prefix/namespace, with trailing '\\'
+ * @param list<string>|string $paths The PSR-4 base directories
+ * @param bool $prepend Whether to prepend the directories
+ *
+ * @throws \InvalidArgumentException
+ *
+ * @return void
+ */
+ public function addPsr4($prefix, $paths, $prepend = false)
+ {
+ $paths = (array) $paths;
+ if (!$prefix) {
+ // Register directories for the root namespace.
+ if ($prepend) {
+ $this->fallbackDirsPsr4 = array_merge(
+ $paths,
+ $this->fallbackDirsPsr4
+ );
+ } else {
+ $this->fallbackDirsPsr4 = array_merge(
+ $this->fallbackDirsPsr4,
+ $paths
+ );
+ }
+ } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
+ // Register directories for a new namespace.
+ $length = strlen($prefix);
+ if ('\\' !== $prefix[$length - 1]) {
+ throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+ }
+ $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+ $this->prefixDirsPsr4[$prefix] = $paths;
+ } elseif ($prepend) {
+ // Prepend directories for an already registered namespace.
+ $this->prefixDirsPsr4[$prefix] = array_merge(
+ $paths,
+ $this->prefixDirsPsr4[$prefix]
+ );
+ } else {
+ // Append directories for an already registered namespace.
+ $this->prefixDirsPsr4[$prefix] = array_merge(
+ $this->prefixDirsPsr4[$prefix],
+ $paths
+ );
+ }
+ }
+
+ /**
+ * Registers a set of PSR-0 directories for a given prefix,
+ * replacing any others previously set for this prefix.
+ *
+ * @param string $prefix The prefix
+ * @param list<string>|string $paths The PSR-0 base directories
+ *
+ * @return void
+ */
+ public function set($prefix, $paths)
+ {
+ if (!$prefix) {
+ $this->fallbackDirsPsr0 = (array) $paths;
+ } else {
+ $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
+ }
+ }
+
+ /**
+ * Registers a set of PSR-4 directories for a given namespace,
+ * replacing any others previously set for this namespace.
+ *
+ * @param string $prefix The prefix/namespace, with trailing '\\'
+ * @param list<string>|string $paths The PSR-4 base directories
+ *
+ * @throws \InvalidArgumentException
+ *
+ * @return void
+ */
+ public function setPsr4($prefix, $paths)
+ {
+ if (!$prefix) {
+ $this->fallbackDirsPsr4 = (array) $paths;
+ } else {
+ $length = strlen($prefix);
+ if ('\\' !== $prefix[$length - 1]) {
+ throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+ }
+ $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+ $this->prefixDirsPsr4[$prefix] = (array) $paths;
+ }
+ }
+
+ /**
+ * Turns on searching the include path for class files.
+ *
+ * @param bool $useIncludePath
+ *
+ * @return void
+ */
+ public function setUseIncludePath($useIncludePath)
+ {
+ $this->useIncludePath = $useIncludePath;
+ }
+
+ /**
+ * Can be used to check if the autoloader uses the include path to check
+ * for classes.
+ *
+ * @return bool
+ */
+ public function getUseIncludePath()
+ {
+ return $this->useIncludePath;
+ }
+
+ /**
+ * Turns off searching the prefix and fallback directories for classes
+ * that have not been registered with the class map.
+ *
+ * @param bool $classMapAuthoritative
+ *
+ * @return void
+ */
+ public function setClassMapAuthoritative($classMapAuthoritative)
+ {
+ $this->classMapAuthoritative = $classMapAuthoritative;
+ }
+
+ /**
+ * Should class lookup fail if not found in the current class map?
+ *
+ * @return bool
+ */
+ public function isClassMapAuthoritative()
+ {
+ return $this->classMapAuthoritative;
+ }
+
+ /**
+ * APCu prefix to use to cache found/not-found classes, if the extension is enabled.
+ *
+ * @param string|null $apcuPrefix
+ *
+ * @return void
+ */
+ public function setApcuPrefix($apcuPrefix)
+ {
+ $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
+ }
+
+ /**
+ * The APCu prefix in use, or null if APCu caching is not enabled.
+ *
+ * @return string|null
+ */
+ public function getApcuPrefix()
+ {
+ return $this->apcuPrefix;
+ }
+
+ /**
+ * Registers this instance as an autoloader.
+ *
+ * @param bool $prepend Whether to prepend the autoloader or not
+ *
+ * @return void
+ */
+ public function register($prepend = false)
+ {
+ spl_autoload_register(array($this, 'loadClass'), true, $prepend);
+
+ if (null === $this->vendorDir) {
+ return;
+ }
+
+ if ($prepend) {
+ self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
+ } else {
+ unset(self::$registeredLoaders[$this->vendorDir]);
+ self::$registeredLoaders[$this->vendorDir] = $this;
+ }
+ }
+
+ /**
+ * Unregisters this instance as an autoloader.
+ *
+ * @return void
+ */
+ public function unregister()
+ {
+ spl_autoload_unregister(array($this, 'loadClass'));
+
+ if (null !== $this->vendorDir) {
+ unset(self::$registeredLoaders[$this->vendorDir]);
+ }
+ }
+
+ /**
+ * Loads the given class or interface.
+ *
+ * @param string $class The name of the class
+ * @return true|null True if loaded, null otherwise
+ */
+ public function loadClass($class)
+ {
+ if ($file = $this->findFile($class)) {
+ $includeFile = self::$includeFile;
+ $includeFile($file);
+
+ return true;
+ }
+
+ return null;
+ }
+
+ /**
+ * Finds the path to the file where the class is defined.
+ *
+ * @param string $class The name of the class
+ *
+ * @return string|false The path if found, false otherwise
+ */
+ public function findFile($class)
+ {
+ // class map lookup
+ if (isset($this->classMap[$class])) {
+ return $this->classMap[$class];
+ }
+ if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
+ return false;
+ }
+ if (null !== $this->apcuPrefix) {
+ $file = apcu_fetch($this->apcuPrefix.$class, $hit);
+ if ($hit) {
+ return $file;
+ }
+ }
+
+ $file = $this->findFileWithExtension($class, '.php');
+
+ // Search for Hack files if we are running on HHVM
+ if (false === $file && defined('HHVM_VERSION')) {
+ $file = $this->findFileWithExtension($class, '.hh');
+ }
+
+ if (null !== $this->apcuPrefix) {
+ apcu_add($this->apcuPrefix.$class, $file);
+ }
+
+ if (false === $file) {
+ // Remember that this class does not exist.
+ $this->missingClasses[$class] = true;
+ }
+
+ return $file;
+ }
+
+ /**
+ * Returns the currently registered loaders keyed by their corresponding vendor directories.
+ *
+ * @return array<string, self>
+ */
+ public static function getRegisteredLoaders()
+ {
+ return self::$registeredLoaders;
+ }
+
+ /**
+ * @param string $class
+ * @param string $ext
+ * @return string|false
+ */
+ private function findFileWithExtension($class, $ext)
+ {
+ // PSR-4 lookup
+ $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
+
+ $first = $class[0];
+ if (isset($this->prefixLengthsPsr4[$first])) {
+ $subPath = $class;
+ while (false !== $lastPos = strrpos($subPath, '\\')) {
+ $subPath = substr($subPath, 0, $lastPos);
+ $search = $subPath . '\\';
+ if (isset($this->prefixDirsPsr4[$search])) {
+ $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
+ foreach ($this->prefixDirsPsr4[$search] as $dir) {
+ if (file_exists($file = $dir . $pathEnd)) {
+ return $file;
+ }
+ }
+ }
+ }
+ }
+
+ // PSR-4 fallback dirs
+ foreach ($this->fallbackDirsPsr4 as $dir) {
+ if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
+ return $file;
+ }
+ }
+
+ // PSR-0 lookup
+ if (false !== $pos = strrpos($class, '\\')) {
+ // namespaced class name
+ $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
+ . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
+ } else {
+ // PEAR-like class name
+ $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
+ }
+
+ if (isset($this->prefixesPsr0[$first])) {
+ foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
+ if (0 === strpos($class, $prefix)) {
+ foreach ($dirs as $dir) {
+ if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+ return $file;
+ }
+ }
+ }
+ }
+ }
+
+ // PSR-0 fallback dirs
+ foreach ($this->fallbackDirsPsr0 as $dir) {
+ if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+ return $file;
+ }
+ }
+
+ // PSR-0 include paths.
+ if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
+ return $file;
+ }
+
+ return false;
+ }
+
+ /**
+ * @return void
+ */
+ private static function initializeIncludeClosure()
+ {
+ if (self::$includeFile !== null) {
+ return;
+ }
+
+ /**
+ * Scope isolated include.
+ *
+ * Prevents access to $this/self from included files.
+ *
+ * @param string $file
+ * @return void
+ */
+ self::$includeFile = \Closure::bind(static function($file) {
+ include $file;
+ }, null, null);
+ }
+}
diff --git a/apps/files_versions/composer/composer/InstalledVersions.php b/apps/files_versions/composer/composer/InstalledVersions.php
new file mode 100644
index 00000000000..51e734a774b
--- /dev/null
+++ b/apps/files_versions/composer/composer/InstalledVersions.php
@@ -0,0 +1,359 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ * Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer;
+
+use Composer\Autoload\ClassLoader;
+use Composer\Semver\VersionParser;
+
+/**
+ * This class is copied in every Composer installed project and available to all
+ *
+ * See also https://getcomposer.org/doc/07-runtime.md#installed-versions
+ *
+ * To require its presence, you can require `composer-runtime-api ^2.0`
+ *
+ * @final
+ */
+class InstalledVersions
+{
+ /**
+ * @var mixed[]|null
+ * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
+ */
+ private static $installed;
+
+ /**
+ * @var bool|null
+ */
+ private static $canGetVendors;
+
+ /**
+ * @var array[]
+ * @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
+ */
+ private static $installedByVendor = array();
+
+ /**
+ * Returns a list of all package names which are present, either by being installed, replaced or provided
+ *
+ * @return string[]
+ * @psalm-return list<string>
+ */
+ public static function getInstalledPackages()
+ {
+ $packages = array();
+ foreach (self::getInstalled() as $installed) {
+ $packages[] = array_keys($installed['versions']);
+ }
+
+ if (1 === \count($packages)) {
+ return $packages[0];
+ }
+
+ return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
+ }
+
+ /**
+ * Returns a list of all package names with a specific type e.g. 'library'
+ *
+ * @param string $type
+ * @return string[]
+ * @psalm-return list<string>
+ */
+ public static function getInstalledPackagesByType($type)
+ {
+ $packagesByType = array();
+
+ foreach (self::getInstalled() as $installed) {
+ foreach ($installed['versions'] as $name => $package) {
+ if (isset($package['type']) && $package['type'] === $type) {
+ $packagesByType[] = $name;
+ }
+ }
+ }
+
+ return $packagesByType;
+ }
+
+ /**
+ * Checks whether the given package is installed
+ *
+ * This also returns true if the package name is provided or replaced by another package
+ *
+ * @param string $packageName
+ * @param bool $includeDevRequirements
+ * @return bool
+ */
+ public static function isInstalled($packageName, $includeDevRequirements = true)
+ {
+ foreach (self::getInstalled() as $installed) {
+ if (isset($installed['versions'][$packageName])) {
+ return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks whether the given package satisfies a version constraint
+ *
+ * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
+ *
+ * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
+ *
+ * @param VersionParser $parser Install composer/semver to have access to this class and functionality
+ * @param string $packageName
+ * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
+ * @return bool
+ */
+ public static function satisfies(VersionParser $parser, $packageName, $constraint)
+ {
+ $constraint = $parser->parseConstraints((string) $constraint);
+ $provided = $parser->parseConstraints(self::getVersionRanges($packageName));
+
+ return $provided->matches($constraint);
+ }
+
+ /**
+ * Returns a version constraint representing all the range(s) which are installed for a given package
+ *
+ * It is easier to use this via isInstalled() with the $constraint argument if you need to check
+ * whether a given version of a package is installed, and not just whether it exists
+ *
+ * @param string $packageName
+ * @return string Version constraint usable with composer/semver
+ */
+ public static function getVersionRanges($packageName)
+ {
+ foreach (self::getInstalled() as $installed) {
+ if (!isset($installed['versions'][$packageName])) {
+ continue;
+ }
+
+ $ranges = array();
+ if (isset($installed['versions'][$packageName]['pretty_version'])) {
+ $ranges[] = $installed['versions'][$packageName]['pretty_version'];
+ }
+ if (array_key_exists('aliases', $installed['versions'][$packageName])) {
+ $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
+ }
+ if (array_key_exists('replaced', $installed['versions'][$packageName])) {
+ $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
+ }
+ if (array_key_exists('provided', $installed['versions'][$packageName])) {
+ $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
+ }
+
+ return implode(' || ', $ranges);
+ }
+
+ throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+ }
+
+ /**
+ * @param string $packageName
+ * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
+ */
+ public static function getVersion($packageName)
+ {
+ foreach (self::getInstalled() as $installed) {
+ if (!isset($installed['versions'][$packageName])) {
+ continue;
+ }
+
+ if (!isset($installed['versions'][$packageName]['version'])) {
+ return null;
+ }
+
+ return $installed['versions'][$packageName]['version'];
+ }
+
+ throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+ }
+
+ /**
+ * @param string $packageName
+ * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
+ */
+ public static function getPrettyVersion($packageName)
+ {
+ foreach (self::getInstalled() as $installed) {
+ if (!isset($installed['versions'][$packageName])) {
+ continue;
+ }
+
+ if (!isset($installed['versions'][$packageName]['pretty_version'])) {
+ return null;
+ }
+
+ return $installed['versions'][$packageName]['pretty_version'];
+ }
+
+ throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+ }
+
+ /**
+ * @param string $packageName
+ * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
+ */
+ public static function getReference($packageName)
+ {
+ foreach (self::getInstalled() as $installed) {
+ if (!isset($installed['versions'][$packageName])) {
+ continue;
+ }
+
+ if (!isset($installed['versions'][$packageName]['reference'])) {
+ return null;
+ }
+
+ return $installed['versions'][$packageName]['reference'];
+ }
+
+ throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+ }
+
+ /**
+ * @param string $packageName
+ * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
+ */
+ public static function getInstallPath($packageName)
+ {
+ foreach (self::getInstalled() as $installed) {
+ if (!isset($installed['versions'][$packageName])) {
+ continue;
+ }
+
+ return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
+ }
+
+ throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+ }
+
+ /**
+ * @return array
+ * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
+ */
+ public static function getRootPackage()
+ {
+ $installed = self::getInstalled();
+
+ return $installed[0]['root'];
+ }
+
+ /**
+ * Returns the raw installed.php data for custom implementations
+ *
+ * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
+ * @return array[]
+ * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
+ */
+ public static function getRawData()
+ {
+ @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
+
+ if (null === self::$installed) {
+ // only require the installed.php file if this file is loaded from its dumped location,
+ // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
+ if (substr(__DIR__, -8, 1) !== 'C') {
+ self::$installed = include __DIR__ . '/installed.php';
+ } else {
+ self::$installed = array();
+ }
+ }
+
+ return self::$installed;
+ }
+
+ /**
+ * Returns the raw data of all installed.php which are currently loaded for custom implementations
+ *
+ * @return array[]
+ * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
+ */
+ public static function getAllRawData()
+ {
+ return self::getInstalled();
+ }
+
+ /**
+ * Lets you reload the static array from another file
+ *
+ * This is only useful for complex integrations in which a project needs to use
+ * this class but then also needs to execute another project's autoloader in process,
+ * and wants to ensure both projects have access to their version of installed.php.
+ *
+ * A typical case would be PHPUnit, where it would need to make sure it reads all
+ * the data it needs from this class, then call reload() with
+ * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
+ * the project in which it runs can then also use this class safely, without
+ * interference between PHPUnit's dependencies and the project's dependencies.
+ *
+ * @param array[] $data A vendor/composer/installed.php data set
+ * @return void
+ *
+ * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
+ */
+ public static function reload($data)
+ {
+ self::$installed = $data;
+ self::$installedByVendor = array();
+ }
+
+ /**
+ * @return array[]
+ * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
+ */
+ private static function getInstalled()
+ {
+ if (null === self::$canGetVendors) {
+ self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
+ }
+
+ $installed = array();
+
+ if (self::$canGetVendors) {
+ foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
+ if (isset(self::$installedByVendor[$vendorDir])) {
+ $installed[] = self::$installedByVendor[$vendorDir];
+ } elseif (is_file($vendorDir.'/composer/installed.php')) {
+ /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
+ $required = require $vendorDir.'/composer/installed.php';
+ $installed[] = self::$installedByVendor[$vendorDir] = $required;
+ if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
+ self::$installed = $installed[count($installed) - 1];
+ }
+ }
+ }
+ }
+
+ if (null === self::$installed) {
+ // only require the installed.php file if this file is loaded from its dumped location,
+ // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
+ if (substr(__DIR__, -8, 1) !== 'C') {
+ /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
+ $required = require __DIR__ . '/installed.php';
+ self::$installed = $required;
+ } else {
+ self::$installed = array();
+ }
+ }
+
+ if (self::$installed !== array()) {
+ $installed[] = self::$installed;
+ }
+
+ return $installed;
+ }
+}
diff --git a/apps/files_versions/composer/composer/LICENSE b/apps/files_versions/composer/composer/LICENSE
new file mode 100644
index 00000000000..f27399a042d
--- /dev/null
+++ b/apps/files_versions/composer/composer/LICENSE
@@ -0,0 +1,21 @@
+
+Copyright (c) Nils Adermann, Jordi Boggiano
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
diff --git a/apps/files_versions/composer/composer/autoload_classmap.php b/apps/files_versions/composer/composer/autoload_classmap.php
new file mode 100644
index 00000000000..27e68decdcc
--- /dev/null
+++ b/apps/files_versions/composer/composer/autoload_classmap.php
@@ -0,0 +1,52 @@
+<?php
+
+// autoload_classmap.php @generated by Composer
+
+$vendorDir = dirname(__DIR__);
+$baseDir = $vendorDir;
+
+return array(
+ 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
+ 'OCA\\Files_Versions\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php',
+ 'OCA\\Files_Versions\\BackgroundJob\\ExpireVersions' => $baseDir . '/../lib/BackgroundJob/ExpireVersions.php',
+ 'OCA\\Files_Versions\\Capabilities' => $baseDir . '/../lib/Capabilities.php',
+ 'OCA\\Files_Versions\\Command\\CleanUp' => $baseDir . '/../lib/Command/CleanUp.php',
+ 'OCA\\Files_Versions\\Command\\Expire' => $baseDir . '/../lib/Command/Expire.php',
+ 'OCA\\Files_Versions\\Command\\ExpireVersions' => $baseDir . '/../lib/Command/ExpireVersions.php',
+ 'OCA\\Files_Versions\\Controller\\PreviewController' => $baseDir . '/../lib/Controller/PreviewController.php',
+ 'OCA\\Files_Versions\\Db\\VersionEntity' => $baseDir . '/../lib/Db/VersionEntity.php',
+ 'OCA\\Files_Versions\\Db\\VersionsMapper' => $baseDir . '/../lib/Db/VersionsMapper.php',
+ 'OCA\\Files_Versions\\Events\\CreateVersionEvent' => $baseDir . '/../lib/Events/CreateVersionEvent.php',
+ 'OCA\\Files_Versions\\Events\\VersionCreatedEvent' => $baseDir . '/../lib/Events/VersionCreatedEvent.php',
+ 'OCA\\Files_Versions\\Events\\VersionRestoredEvent' => $baseDir . '/../lib/Events/VersionRestoredEvent.php',
+ 'OCA\\Files_Versions\\Expiration' => $baseDir . '/../lib/Expiration.php',
+ 'OCA\\Files_Versions\\Listener\\FileEventsListener' => $baseDir . '/../lib/Listener/FileEventsListener.php',
+ 'OCA\\Files_Versions\\Listener\\LegacyRollbackListener' => $baseDir . '/../lib/Listener/LegacyRollbackListener.php',
+ 'OCA\\Files_Versions\\Listener\\LoadAdditionalListener' => $baseDir . '/../lib/Listener/LoadAdditionalListener.php',
+ 'OCA\\Files_Versions\\Listener\\LoadSidebarListener' => $baseDir . '/../lib/Listener/LoadSidebarListener.php',
+ 'OCA\\Files_Versions\\Listener\\VersionAuthorListener' => $baseDir . '/../lib/Listener/VersionAuthorListener.php',
+ 'OCA\\Files_Versions\\Listener\\VersionStorageMoveListener' => $baseDir . '/../lib/Listener/VersionStorageMoveListener.php',
+ 'OCA\\Files_Versions\\Migration\\Version1020Date20221114144058' => $baseDir . '/../lib/Migration/Version1020Date20221114144058.php',
+ 'OCA\\Files_Versions\\Sabre\\Plugin' => $baseDir . '/../lib/Sabre/Plugin.php',
+ 'OCA\\Files_Versions\\Sabre\\RestoreFolder' => $baseDir . '/../lib/Sabre/RestoreFolder.php',
+ 'OCA\\Files_Versions\\Sabre\\RootCollection' => $baseDir . '/../lib/Sabre/RootCollection.php',
+ 'OCA\\Files_Versions\\Sabre\\VersionCollection' => $baseDir . '/../lib/Sabre/VersionCollection.php',
+ 'OCA\\Files_Versions\\Sabre\\VersionFile' => $baseDir . '/../lib/Sabre/VersionFile.php',
+ 'OCA\\Files_Versions\\Sabre\\VersionHome' => $baseDir . '/../lib/Sabre/VersionHome.php',
+ 'OCA\\Files_Versions\\Sabre\\VersionRoot' => $baseDir . '/../lib/Sabre/VersionRoot.php',
+ 'OCA\\Files_Versions\\Storage' => $baseDir . '/../lib/Storage.php',
+ 'OCA\\Files_Versions\\Versions\\BackendNotFoundException' => $baseDir . '/../lib/Versions/BackendNotFoundException.php',
+ 'OCA\\Files_Versions\\Versions\\IDeletableVersionBackend' => $baseDir . '/../lib/Versions/IDeletableVersionBackend.php',
+ 'OCA\\Files_Versions\\Versions\\IMetadataVersion' => $baseDir . '/../lib/Versions/IMetadataVersion.php',
+ 'OCA\\Files_Versions\\Versions\\IMetadataVersionBackend' => $baseDir . '/../lib/Versions/IMetadataVersionBackend.php',
+ 'OCA\\Files_Versions\\Versions\\INameableVersion' => $baseDir . '/../lib/Versions/INameableVersion.php',
+ 'OCA\\Files_Versions\\Versions\\INameableVersionBackend' => $baseDir . '/../lib/Versions/INameableVersionBackend.php',
+ 'OCA\\Files_Versions\\Versions\\INeedSyncVersionBackend' => $baseDir . '/../lib/Versions/INeedSyncVersionBackend.php',
+ 'OCA\\Files_Versions\\Versions\\IVersion' => $baseDir . '/../lib/Versions/IVersion.php',
+ 'OCA\\Files_Versions\\Versions\\IVersionBackend' => $baseDir . '/../lib/Versions/IVersionBackend.php',
+ 'OCA\\Files_Versions\\Versions\\IVersionManager' => $baseDir . '/../lib/Versions/IVersionManager.php',
+ 'OCA\\Files_Versions\\Versions\\IVersionsImporterBackend' => $baseDir . '/../lib/Versions/IVersionsImporterBackend.php',
+ 'OCA\\Files_Versions\\Versions\\LegacyVersionsBackend' => $baseDir . '/../lib/Versions/LegacyVersionsBackend.php',
+ 'OCA\\Files_Versions\\Versions\\Version' => $baseDir . '/../lib/Versions/Version.php',
+ 'OCA\\Files_Versions\\Versions\\VersionManager' => $baseDir . '/../lib/Versions/VersionManager.php',
+);
diff --git a/apps/files_versions/composer/composer/autoload_namespaces.php b/apps/files_versions/composer/composer/autoload_namespaces.php
new file mode 100644
index 00000000000..3f5c9296251
--- /dev/null
+++ b/apps/files_versions/composer/composer/autoload_namespaces.php
@@ -0,0 +1,9 @@
+<?php
+
+// autoload_namespaces.php @generated by Composer
+
+$vendorDir = dirname(__DIR__);
+$baseDir = $vendorDir;
+
+return array(
+);
diff --git a/apps/files_versions/composer/composer/autoload_psr4.php b/apps/files_versions/composer/composer/autoload_psr4.php
new file mode 100644
index 00000000000..9630dd45b5a
--- /dev/null
+++ b/apps/files_versions/composer/composer/autoload_psr4.php
@@ -0,0 +1,10 @@
+<?php
+
+// autoload_psr4.php @generated by Composer
+
+$vendorDir = dirname(__DIR__);
+$baseDir = $vendorDir;
+
+return array(
+ 'OCA\\Files_Versions\\' => array($baseDir . '/../lib'),
+);
diff --git a/apps/files_versions/composer/composer/autoload_real.php b/apps/files_versions/composer/composer/autoload_real.php
new file mode 100644
index 00000000000..dad15f0b550
--- /dev/null
+++ b/apps/files_versions/composer/composer/autoload_real.php
@@ -0,0 +1,37 @@
+<?php
+
+// autoload_real.php @generated by Composer
+
+class ComposerAutoloaderInitFiles_Versions
+{
+ private static $loader;
+
+ public static function loadClassLoader($class)
+ {
+ if ('Composer\Autoload\ClassLoader' === $class) {
+ require __DIR__ . '/ClassLoader.php';
+ }
+ }
+
+ /**
+ * @return \Composer\Autoload\ClassLoader
+ */
+ public static function getLoader()
+ {
+ if (null !== self::$loader) {
+ return self::$loader;
+ }
+
+ spl_autoload_register(array('ComposerAutoloaderInitFiles_Versions', 'loadClassLoader'), true, true);
+ self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
+ spl_autoload_unregister(array('ComposerAutoloaderInitFiles_Versions', 'loadClassLoader'));
+
+ require __DIR__ . '/autoload_static.php';
+ call_user_func(\Composer\Autoload\ComposerStaticInitFiles_Versions::getInitializer($loader));
+
+ $loader->setClassMapAuthoritative(true);
+ $loader->register(true);
+
+ return $loader;
+ }
+}
diff --git a/apps/files_versions/composer/composer/autoload_static.php b/apps/files_versions/composer/composer/autoload_static.php
new file mode 100644
index 00000000000..8ecc1cb0137
--- /dev/null
+++ b/apps/files_versions/composer/composer/autoload_static.php
@@ -0,0 +1,78 @@
+<?php
+
+// autoload_static.php @generated by Composer
+
+namespace Composer\Autoload;
+
+class ComposerStaticInitFiles_Versions
+{
+ public static $prefixLengthsPsr4 = array (
+ 'O' =>
+ array (
+ 'OCA\\Files_Versions\\' => 19,
+ ),
+ );
+
+ public static $prefixDirsPsr4 = array (
+ 'OCA\\Files_Versions\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/../lib',
+ ),
+ );
+
+ public static $classMap = array (
+ 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
+ 'OCA\\Files_Versions\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php',
+ 'OCA\\Files_Versions\\BackgroundJob\\ExpireVersions' => __DIR__ . '/..' . '/../lib/BackgroundJob/ExpireVersions.php',
+ 'OCA\\Files_Versions\\Capabilities' => __DIR__ . '/..' . '/../lib/Capabilities.php',
+ 'OCA\\Files_Versions\\Command\\CleanUp' => __DIR__ . '/..' . '/../lib/Command/CleanUp.php',
+ 'OCA\\Files_Versions\\Command\\Expire' => __DIR__ . '/..' . '/../lib/Command/Expire.php',
+ 'OCA\\Files_Versions\\Command\\ExpireVersions' => __DIR__ . '/..' . '/../lib/Command/ExpireVersions.php',
+ 'OCA\\Files_Versions\\Controller\\PreviewController' => __DIR__ . '/..' . '/../lib/Controller/PreviewController.php',
+ 'OCA\\Files_Versions\\Db\\VersionEntity' => __DIR__ . '/..' . '/../lib/Db/VersionEntity.php',
+ 'OCA\\Files_Versions\\Db\\VersionsMapper' => __DIR__ . '/..' . '/../lib/Db/VersionsMapper.php',
+ 'OCA\\Files_Versions\\Events\\CreateVersionEvent' => __DIR__ . '/..' . '/../lib/Events/CreateVersionEvent.php',
+ 'OCA\\Files_Versions\\Events\\VersionCreatedEvent' => __DIR__ . '/..' . '/../lib/Events/VersionCreatedEvent.php',
+ 'OCA\\Files_Versions\\Events\\VersionRestoredEvent' => __DIR__ . '/..' . '/../lib/Events/VersionRestoredEvent.php',
+ 'OCA\\Files_Versions\\Expiration' => __DIR__ . '/..' . '/../lib/Expiration.php',
+ 'OCA\\Files_Versions\\Listener\\FileEventsListener' => __DIR__ . '/..' . '/../lib/Listener/FileEventsListener.php',
+ 'OCA\\Files_Versions\\Listener\\LegacyRollbackListener' => __DIR__ . '/..' . '/../lib/Listener/LegacyRollbackListener.php',
+ 'OCA\\Files_Versions\\Listener\\LoadAdditionalListener' => __DIR__ . '/..' . '/../lib/Listener/LoadAdditionalListener.php',
+ 'OCA\\Files_Versions\\Listener\\LoadSidebarListener' => __DIR__ . '/..' . '/../lib/Listener/LoadSidebarListener.php',
+ 'OCA\\Files_Versions\\Listener\\VersionAuthorListener' => __DIR__ . '/..' . '/../lib/Listener/VersionAuthorListener.php',
+ 'OCA\\Files_Versions\\Listener\\VersionStorageMoveListener' => __DIR__ . '/..' . '/../lib/Listener/VersionStorageMoveListener.php',
+ 'OCA\\Files_Versions\\Migration\\Version1020Date20221114144058' => __DIR__ . '/..' . '/../lib/Migration/Version1020Date20221114144058.php',
+ 'OCA\\Files_Versions\\Sabre\\Plugin' => __DIR__ . '/..' . '/../lib/Sabre/Plugin.php',
+ 'OCA\\Files_Versions\\Sabre\\RestoreFolder' => __DIR__ . '/..' . '/../lib/Sabre/RestoreFolder.php',
+ 'OCA\\Files_Versions\\Sabre\\RootCollection' => __DIR__ . '/..' . '/../lib/Sabre/RootCollection.php',
+ 'OCA\\Files_Versions\\Sabre\\VersionCollection' => __DIR__ . '/..' . '/../lib/Sabre/VersionCollection.php',
+ 'OCA\\Files_Versions\\Sabre\\VersionFile' => __DIR__ . '/..' . '/../lib/Sabre/VersionFile.php',
+ 'OCA\\Files_Versions\\Sabre\\VersionHome' => __DIR__ . '/..' . '/../lib/Sabre/VersionHome.php',
+ 'OCA\\Files_Versions\\Sabre\\VersionRoot' => __DIR__ . '/..' . '/../lib/Sabre/VersionRoot.php',
+ 'OCA\\Files_Versions\\Storage' => __DIR__ . '/..' . '/../lib/Storage.php',
+ 'OCA\\Files_Versions\\Versions\\BackendNotFoundException' => __DIR__ . '/..' . '/../lib/Versions/BackendNotFoundException.php',
+ 'OCA\\Files_Versions\\Versions\\IDeletableVersionBackend' => __DIR__ . '/..' . '/../lib/Versions/IDeletableVersionBackend.php',
+ 'OCA\\Files_Versions\\Versions\\IMetadataVersion' => __DIR__ . '/..' . '/../lib/Versions/IMetadataVersion.php',
+ 'OCA\\Files_Versions\\Versions\\IMetadataVersionBackend' => __DIR__ . '/..' . '/../lib/Versions/IMetadataVersionBackend.php',
+ 'OCA\\Files_Versions\\Versions\\INameableVersion' => __DIR__ . '/..' . '/../lib/Versions/INameableVersion.php',
+ 'OCA\\Files_Versions\\Versions\\INameableVersionBackend' => __DIR__ . '/..' . '/../lib/Versions/INameableVersionBackend.php',
+ 'OCA\\Files_Versions\\Versions\\INeedSyncVersionBackend' => __DIR__ . '/..' . '/../lib/Versions/INeedSyncVersionBackend.php',
+ 'OCA\\Files_Versions\\Versions\\IVersion' => __DIR__ . '/..' . '/../lib/Versions/IVersion.php',
+ 'OCA\\Files_Versions\\Versions\\IVersionBackend' => __DIR__ . '/..' . '/../lib/Versions/IVersionBackend.php',
+ 'OCA\\Files_Versions\\Versions\\IVersionManager' => __DIR__ . '/..' . '/../lib/Versions/IVersionManager.php',
+ 'OCA\\Files_Versions\\Versions\\IVersionsImporterBackend' => __DIR__ . '/..' . '/../lib/Versions/IVersionsImporterBackend.php',
+ 'OCA\\Files_Versions\\Versions\\LegacyVersionsBackend' => __DIR__ . '/..' . '/../lib/Versions/LegacyVersionsBackend.php',
+ 'OCA\\Files_Versions\\Versions\\Version' => __DIR__ . '/..' . '/../lib/Versions/Version.php',
+ 'OCA\\Files_Versions\\Versions\\VersionManager' => __DIR__ . '/..' . '/../lib/Versions/VersionManager.php',
+ );
+
+ public static function getInitializer(ClassLoader $loader)
+ {
+ return \Closure::bind(function () use ($loader) {
+ $loader->prefixLengthsPsr4 = ComposerStaticInitFiles_Versions::$prefixLengthsPsr4;
+ $loader->prefixDirsPsr4 = ComposerStaticInitFiles_Versions::$prefixDirsPsr4;
+ $loader->classMap = ComposerStaticInitFiles_Versions::$classMap;
+
+ }, null, ClassLoader::class);
+ }
+}
diff --git a/apps/files_versions/composer/composer/installed.json b/apps/files_versions/composer/composer/installed.json
new file mode 100644
index 00000000000..f20a6c47c6d
--- /dev/null
+++ b/apps/files_versions/composer/composer/installed.json
@@ -0,0 +1,5 @@
+{
+ "packages": [],
+ "dev": false,
+ "dev-package-names": []
+}
diff --git a/apps/files_versions/composer/composer/installed.php b/apps/files_versions/composer/composer/installed.php
new file mode 100644
index 00000000000..650df1d51b8
--- /dev/null
+++ b/apps/files_versions/composer/composer/installed.php
@@ -0,0 +1,23 @@
+<?php return array(
+ 'root' => array(
+ 'name' => '__root__',
+ 'pretty_version' => 'dev-master',
+ 'version' => 'dev-master',
+ 'reference' => '84930a207a8d5f0ef32320796fe188892b63fa19',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../',
+ 'aliases' => array(),
+ 'dev' => false,
+ ),
+ 'versions' => array(
+ '__root__' => array(
+ 'pretty_version' => 'dev-master',
+ 'version' => 'dev-master',
+ 'reference' => '84930a207a8d5f0ef32320796fe188892b63fa19',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../',
+ 'aliases' => array(),
+ 'dev_requirement' => false,
+ ),
+ ),
+);
diff --git a/apps/files_versions/css/versions.css b/apps/files_versions/css/versions.css
deleted file mode 100644
index b159de82ea3..00000000000
--- a/apps/files_versions/css/versions.css
+++ /dev/null
@@ -1,53 +0,0 @@
-.versionsTabView .clear-float {
- clear: both;
-}
-.versionsTabView li {
- width: 100%;
- cursor: default;
- height: 56px;
- float: left;
- border-bottom: 1px solid rgba(100,100,100,.1);
-}
-.versionsTabView li:last-child {
- border-bottom: none;
-}
-
-.versionsTabView li > * {
- vertical-align: middle;
- -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)";
- filter: alpha(opacity=50);
- opacity: .5;
-}
-
-.versionsTabView li > a,
-.versionsTabView li > span {
- padding: 15px 10px 11px;
-}
-
-.versionsTabView li > *:hover,
-.versionsTabView li > *:focus {
- -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";
- filter: alpha(opacity=100);
- opacity: 1;
-}
-
-.versionsTabView img {
- cursor: pointer;
- padding-right: 4px;
-}
-
-.versionsTabView img.preview {
- cursor: default;
- opacity: 1;
-}
-
-.versionsTabView .versiondate {
- min-width: 100px;
- vertical-align: super;
-}
-
-.versionsTabView .revertVersion {
- cursor: pointer;
- float: right;
- margin-right: -10px;
-}
diff --git a/apps/files_versions/download.php b/apps/files_versions/download.php
deleted file mode 100644
index b3742d79de1..00000000000
--- a/apps/files_versions/download.php
+++ /dev/null
@@ -1,48 +0,0 @@
-<?php
-/**
- * @author Andreas Fischer <bantu@owncloud.com>
- * @author Björn Schießle <schiessle@owncloud.com>
- * @author Lukas Reschke <lukas@owncloud.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <rullzer@owncloud.com>
- * @author Vincent Petry <pvince81@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-OCP\JSON::checkAppEnabled('files_versions');
-OCP\JSON::checkLoggedIn();
-
-$file = $_GET['file'];
-$revision=(int)$_GET['revision'];
-
-list($uid, $filename) = OCA\Files_Versions\Storage::getUidAndFilename($file);
-
-$versionName = '/'.$uid.'/files_versions/'.$filename.'.v'.$revision;
-
-$view = new OC\Files\View('/');
-
-$ftype = \OC::$server->getMimeTypeDetector()->getSecureMimeType($view->getMimeType('/'.$uid.'/files/'.$filename));
-
-header('Content-Type:'.$ftype);
-OCP\Response::setContentDispositionHeader(basename($filename), 'attachment');
-OCP\Response::disableCaching();
-OCP\Response::setContentLengthHeader($view->filesize($versionName));
-
-OC_Util::obEnd();
-
-$view->readfile($versionName);
diff --git a/apps/files_versions/img/app-dark.svg b/apps/files_versions/img/app-dark.svg
new file mode 100644
index 00000000000..ecddb64ce3a
--- /dev/null
+++ b/apps/files_versions/img/app-dark.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px"><path d="M479.79-408Q450-408 429-429.21t-21-51Q408-510 429.21-531t51-21Q510-552 531-530.79t21 51Q552-450 530.79-429t-51 21Zm.21 264q-140 0-238.5-98T144-480h72q2 110 78.5 187T480-216q110.31 0 187.16-76.78 76.84-76.78 76.84-187T667.16-667Q590.31-744 480-744q-59 0-111.5 25.5T277-648h107v72H144v-240h72v130q47.91-62.09 116.95-96.04Q402-816 480-816q70 0 131.13 26.6 61.14 26.6 106.4 71.87 45.27 45.26 71.87 106.4Q816-550 816-480t-26.6 131.13q-26.6 61.14-71.87 106.4-45.26 45.27-106.4 71.87Q550-144 480-144Z"/></svg> \ No newline at end of file
diff --git a/apps/files_versions/img/app.svg b/apps/files_versions/img/app.svg
index 862b0a6885e..651ed400e32 100644
--- a/apps/files_versions/img/app.svg
+++ b/apps/files_versions/img/app.svg
@@ -1,103 +1 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- height="16"
- width="16"
- version="1.0"
- id="svg2"
- inkscape:version="0.48.5 r10040"
- sodipodi:docname="app.svg">
- <metadata
- id="metadata20">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <defs
- id="defs18" />
- <sodipodi:namedview
- pagecolor="#ffffff"
- bordercolor="#666666"
- borderopacity="1"
- objecttolerance="10"
- gridtolerance="10"
- guidetolerance="10"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:window-width="1920"
- inkscape:window-height="1014"
- id="namedview16"
- showgrid="false"
- inkscape:zoom="14.75"
- inkscape:cx="-6.6440678"
- inkscape:cy="8"
- inkscape:window-x="0"
- inkscape:window-y="27"
- inkscape:window-maximized="1"
- inkscape:current-layer="svg2" />
- <rect
- rx=".5"
- ry=".5"
- height="4"
- width="4"
- y="1"
- x="1"
- id="rect4"
- style="fill:#ffffff;fill-opacity:1" />
- <rect
- rx=".5"
- ry=".5"
- height="1"
- width="9"
- y="2"
- x="6"
- id="rect6"
- style="fill:#ffffff;fill-opacity:1" />
- <rect
- rx=".5"
- ry=".5"
- height="4"
- width="4"
- y="6"
- x="1"
- id="rect8"
- style="fill:#ffffff;fill-opacity:1" />
- <rect
- rx=".5"
- ry=".5"
- height="1"
- width="9"
- y="7"
- x="6"
- id="rect10"
- style="fill:#ffffff;fill-opacity:1" />
- <rect
- rx=".5"
- ry=".5"
- height="4"
- width="4"
- y="11"
- x="1"
- id="rect12"
- style="fill:#ffffff;fill-opacity:1" />
- <rect
- rx=".5"
- ry=".5"
- height="1"
- width="9"
- y="12"
- x="6"
- id="rect14"
- style="fill:#ffffff;fill-opacity:1" />
-</svg>
+<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#fff"><path d="M479.79-408Q450-408 429-429.21t-21-51Q408-510 429.21-531t51-21Q510-552 531-530.79t21 51Q552-450 530.79-429t-51 21Zm.21 264q-140 0-238.5-98T144-480h72q2 110 78.5 187T480-216q110.31 0 187.16-76.78 76.84-76.78 76.84-187T667.16-667Q590.31-744 480-744q-59 0-111.5 25.5T277-648h107v72H144v-240h72v130q47.91-62.09 116.95-96.04Q402-816 480-816q70 0 131.13 26.6 61.14 26.6 106.4 71.87 45.27 45.26 71.87 106.4Q816-550 816-480t-26.6 131.13q-26.6 61.14-71.87 106.4-45.26 45.27-106.4 71.87Q550-144 480-144Z"/></svg> \ No newline at end of file
diff --git a/apps/files_versions/js/filesplugin.js b/apps/files_versions/js/filesplugin.js
deleted file mode 100644
index a9457c522d6..00000000000
--- a/apps/files_versions/js/filesplugin.js
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright (c) 2015
- *
- * This file is licensed under the Affero General Public License version 3
- * or later.
- *
- * See the COPYING-README file.
- *
- */
-
-(function() {
- OCA.Versions = OCA.Versions || {};
-
- /**
- * @namespace
- */
- OCA.Versions.Util = {
- /**
- * Initialize the versions plugin.
- *
- * @param {OCA.Files.FileList} fileList file list to be extended
- */
- attach: function(fileList) {
- if (fileList.id === 'trashbin' || fileList.id === 'files.public') {
- return;
- }
-
- fileList.registerTabView(new OCA.Versions.VersionsTabView('versionsTabView', {order: -10}));
- }
- };
-})();
-
-OC.Plugins.register('OCA.Files.FileList', OCA.Versions.Util);
-
diff --git a/apps/files_versions/js/versioncollection.js b/apps/files_versions/js/versioncollection.js
deleted file mode 100644
index fdb12bae0a9..00000000000
--- a/apps/files_versions/js/versioncollection.js
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * Copyright (c) 2015
- *
- * This file is licensed under the Affero General Public License version 3
- * or later.
- *
- * See the COPYING-README file.
- *
- */
-
-(function() {
- /**
- * @memberof OCA.Versions
- */
- var VersionCollection = OC.Backbone.Collection.extend({
- model: OCA.Versions.VersionModel,
-
- /**
- * @var OCA.Files.FileInfoModel
- */
- _fileInfo: null,
-
- _endReached: false,
- _currentIndex: 0,
-
- url: function() {
- var url = OC.generateUrl('/apps/files_versions/ajax/getVersions.php');
- var query = {
- source: this._fileInfo.getFullPath(),
- start: this._currentIndex
- };
- return url + '?' + OC.buildQueryString(query);
- },
-
- setFileInfo: function(fileInfo) {
- this._fileInfo = fileInfo;
- // reset
- this._endReached = false;
- this._currentIndex = 0;
- },
-
- getFileInfo: function() {
- return this._fileInfo;
- },
-
- hasMoreResults: function() {
- return !this._endReached;
- },
-
- fetch: function(options) {
- if (!options || options.remove) {
- this._currentIndex = 0;
- }
- return OC.Backbone.Collection.prototype.fetch.apply(this, arguments);
- },
-
- /**
- * Fetch the next set of results
- */
- fetchNext: function() {
- if (!this.hasMoreResults()) {
- return null;
- }
- if (this._currentIndex === 0) {
- return this.fetch();
- }
- return this.fetch({remove: false});
- },
-
- reset: function() {
- this._currentIndex = 0;
- OC.Backbone.Collection.prototype.reset.apply(this, arguments);
- },
-
- parse: function(result) {
- var fullPath = this._fileInfo.getFullPath();
- var results = _.map(result.data.versions, function(version) {
- var revision = parseInt(version.version, 10);
- return {
- id: revision,
- name: version.name,
- fullPath: fullPath,
- timestamp: revision,
- size: version.size
- };
- });
- this._endReached = result.data.endReached;
- this._currentIndex += results.length;
- return results;
- }
- });
-
- OCA.Versions = OCA.Versions || {};
-
- OCA.Versions.VersionCollection = VersionCollection;
-})();
-
diff --git a/apps/files_versions/js/versionmodel.js b/apps/files_versions/js/versionmodel.js
deleted file mode 100644
index dc610fc2144..00000000000
--- a/apps/files_versions/js/versionmodel.js
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright (c) 2015
- *
- * This file is licensed under the Affero General Public License version 3
- * or later.
- *
- * See the COPYING-README file.
- *
- */
-
-(function() {
- /**
- * @memberof OCA.Versions
- */
- var VersionModel = OC.Backbone.Model.extend({
-
- /**
- * Restores the original file to this revision
- */
- revert: function(options) {
- options = options ? _.clone(options) : {};
- var model = this;
- var file = this.getFullPath();
- var revision = this.get('timestamp');
-
- $.ajax({
- type: 'GET',
- url: OC.generateUrl('/apps/files_versions/ajax/rollbackVersion.php'),
- dataType: 'json',
- data: {
- file: file,
- revision: revision
- },
- success: function(response) {
- if (response.status === 'error') {
- if (options.error) {
- options.error.call(options.context, model, response, options);
- }
- model.trigger('error', model, response, options);
- } else {
- if (options.success) {
- options.success.call(options.context, model, response, options);
- }
- model.trigger('revert', model, response, options);
- }
- }
- });
- },
-
- getFullPath: function() {
- return this.get('fullPath');
- },
-
- getPreviewUrl: function() {
- var url = OC.generateUrl('/apps/files_versions/preview');
- var params = {
- file: this.get('fullPath'),
- version: this.get('timestamp')
- };
- return url + '?' + OC.buildQueryString(params);
- },
-
- getDownloadUrl: function() {
- var url = OC.generateUrl('/apps/files_versions/download.php');
- var params = {
- file: this.get('fullPath'),
- revision: this.get('timestamp')
- };
- return url + '?' + OC.buildQueryString(params);
- }
- });
-
- OCA.Versions = OCA.Versions || {};
-
- OCA.Versions.VersionModel = VersionModel;
-})();
-
diff --git a/apps/files_versions/js/versionstabview.js b/apps/files_versions/js/versionstabview.js
deleted file mode 100644
index 0e17fff466a..00000000000
--- a/apps/files_versions/js/versionstabview.js
+++ /dev/null
@@ -1,218 +0,0 @@
-/*
- * Copyright (c) 2015
- *
- * This file is licensed under the Affero General Public License version 3
- * or later.
- *
- * See the COPYING-README file.
- *
- */
-
-(function() {
- var TEMPLATE_ITEM =
- '<li data-revision="{{timestamp}}">' +
- '<img class="preview" src="{{previewUrl}}"/>' +
- '<a href="{{downloadUrl}}" class="downloadVersion"><img src="{{downloadIconUrl}}" />' +
- '<span class="versiondate has-tooltip" title="{{formattedTimestamp}}">{{relativeTimestamp}}</span>' +
- '</a>' +
- '<a href="#" class="revertVersion" title="{{revertLabel}}"><img src="{{revertIconUrl}}" /></a>' +
- '</li>';
-
- var TEMPLATE =
- '<ul class="versions"></ul>' +
- '<div class="clear-float"></div>' +
- '<div class="empty hidden">{{emptyResultLabel}}</div>' +
- '<input type="button" class="showMoreVersions hidden" value="{{moreVersionsLabel}}"' +
- ' name="show-more-versions" id="show-more-versions" />' +
- '<div class="loading hidden" style="height: 50px"></div>';
-
- /**
- * @memberof OCA.Versions
- */
- var VersionsTabView = OCA.Files.DetailTabView.extend(
- /** @lends OCA.Versions.VersionsTabView.prototype */ {
- id: 'versionsTabView',
- className: 'tab versionsTabView',
-
- _template: null,
-
- $versionsContainer: null,
-
- events: {
- 'click .revertVersion': '_onClickRevertVersion',
- 'click .showMoreVersions': '_onClickShowMoreVersions'
- },
-
- initialize: function() {
- OCA.Files.DetailTabView.prototype.initialize.apply(this, arguments);
- this.collection = new OCA.Versions.VersionCollection();
- this.collection.on('request', this._onRequest, this);
- this.collection.on('sync', this._onEndRequest, this);
- this.collection.on('update', this._onUpdate, this);
- this.collection.on('error', this._onError, this);
- this.collection.on('add', this._onAddModel, this);
- },
-
- getLabel: function() {
- return t('files_versions', 'Versions');
- },
-
- nextPage: function() {
- if (this._loading || !this.collection.hasMoreResults()) {
- return;
- }
-
- if (this.collection.getFileInfo() && this.collection.getFileInfo().isDirectory()) {
- return;
- }
- this.collection.fetchNext();
- },
-
- _onClickShowMoreVersions: function(ev) {
- ev.preventDefault();
- this.nextPage();
- },
-
- _onClickRevertVersion: function(ev) {
- var self = this;
- var $target = $(ev.target);
- var fileInfoModel = this.collection.getFileInfo();
- var revision;
- if (!$target.is('li')) {
- $target = $target.closest('li');
- }
-
- ev.preventDefault();
- revision = $target.attr('data-revision');
-
- this.$el.find('.versions, .showMoreVersions').addClass('hidden');
-
- var versionModel = this.collection.get(revision);
- versionModel.revert({
- success: function() {
- // reset and re-fetch the updated collection
- self.$versionsContainer.empty();
- self.collection.setFileInfo(fileInfoModel);
- self.collection.reset([], {silent: true});
- self.collection.fetchNext();
-
- self.$el.find('.versions').removeClass('hidden');
-
- // update original model
- fileInfoModel.trigger('busy', fileInfoModel, false);
- fileInfoModel.set({
- size: versionModel.get('size'),
- mtime: versionModel.get('timestamp') * 1000,
- // temp dummy, until we can do a PROPFIND
- etag: versionModel.get('id') + versionModel.get('timestamp')
- });
- },
-
- error: function() {
- OC.Notification.showTemporary(
- t('files_version', 'Failed to revert {file} to revision {timestamp}.', {
- file: versionModel.getFullPath(),
- timestamp: OC.Util.formatDate(versionModel.get('timestamp') * 1000)
- })
- );
- }
- });
-
- // spinner
- this._toggleLoading(true);
- fileInfoModel.trigger('busy', fileInfoModel, true);
- },
-
- _toggleLoading: function(state) {
- this._loading = state;
- this.$el.find('.loading').toggleClass('hidden', !state);
- },
-
- _onRequest: function() {
- this._toggleLoading(true);
- this.$el.find('.showMoreVersions').addClass('hidden');
- },
-
- _onEndRequest: function() {
- this._toggleLoading(false);
- this.$el.find('.empty').toggleClass('hidden', !!this.collection.length);
- this.$el.find('.showMoreVersions').toggleClass('hidden', !this.collection.hasMoreResults());
- },
-
- _onAddModel: function(model) {
- var $el = $(this.itemTemplate(this._formatItem(model)));
- this.$versionsContainer.append($el);
- $el.find('.has-tooltip').tooltip();
- },
-
- template: function(data) {
- if (!this._template) {
- this._template = Handlebars.compile(TEMPLATE);
- }
-
- return this._template(data);
- },
-
- itemTemplate: function(data) {
- if (!this._itemTemplate) {
- this._itemTemplate = Handlebars.compile(TEMPLATE_ITEM);
- }
-
- return this._itemTemplate(data);
- },
-
- setFileInfo: function(fileInfo) {
- if (fileInfo) {
- this.render();
- this.collection.setFileInfo(fileInfo);
- this.collection.reset([], {silent: true});
- this.nextPage();
- } else {
- this.render();
- this.collection.reset();
- }
- },
-
- _formatItem: function(version) {
- var timestamp = version.get('timestamp') * 1000;
- return _.extend({
- formattedTimestamp: OC.Util.formatDate(timestamp),
- relativeTimestamp: OC.Util.relativeModifiedDate(timestamp),
- downloadUrl: version.getDownloadUrl(),
- downloadIconUrl: OC.imagePath('core', 'actions/download'),
- revertIconUrl: OC.imagePath('core', 'actions/history'),
- previewUrl: version.getPreviewUrl(),
- revertLabel: t('files_versions', 'Restore'),
- }, version.attributes);
- },
-
- /**
- * Renders this details view
- */
- render: function() {
- this.$el.html(this.template({
- emptyResultLabel: t('files_versions', 'No other versions available'),
- moreVersionsLabel: t('files_versions', 'More versions...')
- }));
- this.$el.find('.has-tooltip').tooltip();
- this.$versionsContainer = this.$el.find('ul.versions');
- this.delegateEvents();
- },
-
- /**
- * Returns true for files, false for folders.
- *
- * @return {bool} true for files, false for folders
- */
- canDisplay: function(fileInfo) {
- if (!fileInfo) {
- return false;
- }
- return !fileInfo.isDirectory();
- }
- });
-
- OCA.Versions = OCA.Versions || {};
-
- OCA.Versions.VersionsTabView = VersionsTabView;
-})();
diff --git a/apps/files_versions/l10n/ar.js b/apps/files_versions/l10n/ar.js
index 40afe40221c..318ca0dbfe2 100644
--- a/apps/files_versions/l10n/ar.js
+++ b/apps/files_versions/l10n/ar.js
@@ -1,11 +1,29 @@
OC.L10N.register(
"files_versions",
{
- "Could not revert: %s" : "غير قادر على الاستعادة : %s",
- "Versions" : "الإصدارات",
- "Failed to revert {file} to revision {timestamp}." : "فشل في استعادة {ملف} لنتقيح {الطابع الزمني}",
- "Restore" : "استعادة ",
- "No other versions available" : "لا توجد إصدارات أخرى متاحة",
- "More versions..." : "المزيد من الإصدارات"
+ "Versions" : "النسخ",
+ "This application automatically maintains older versions of files that are changed." : "هذا التطبيق يقوم تلقائيا بحفظ النسخ الأقدم من الملفات التي يتم تعديلها.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "يحتفظ هذا التطبيق تلقائيًا بالنُّسخ القديمة من الملفات بعد تعديلها. عند تمكينه، يتم توفير مجلد الإصدارات المخفية في دليل كل مستخدم و يتم استخدامه لتخزين النسخ الأقدم للملفات. يمكن للمستخدم الرجوع إلى نسخة أقدم من خلال واجهة الويب في أي وقت. حيث يصبح الملف المستبدل نسخةً. يقوم التطبيق تلقائيًا بإدارة مجلد النُّسخ لضمان عدم نفاد الحصة النسبية للحساب بسبب النُّسخ. \t\t\nبالإضافة إلى انتهاء صلاحية النُّسخ، يتأكد تطبيق النُّسخ من عدم استخدام أكثر من 50% من المساحة المجانية المتوفرة حاليًا للحساب لغرض الاحتفاظ بالنُّسخ. إذا تجاوزت مساحة تخزين النُّسخ المخزنة هذا الحد، عندها يقوم التطبيق بحذف النُّسخ الأقدم أولاً حتى ترجع مساحة التخزين إلى الحد المسموح به. مزيد من المعلومات في توثيق التطبيق.",
+ "Current version" : "النسخة الحالية",
+ "Initial version" : "النسخة الأساسية",
+ "You" : "أنت",
+ "Actions for version from {versionHumanExplicitDate}" : "إجراءات على النسخة من {versionHumanExplicitDate}",
+ "Name this version" : "أعط اسماً لهذه النسخة",
+ "Edit version name" : "تعديل اسم النسخة",
+ "Compare to current version" : "قارِن مع النسخة الحاليّة",
+ "Restore version" : "استعادة النسخة",
+ "Download version" : "تنزيل النسخة",
+ "Delete version" : "حذف النسخة",
+ "Cancel" : "إلغاء",
+ "Remove version name" : "إلغ اسم النسخة",
+ "Save version name" : "إحفظ اسم النسخة",
+ "Version name" : "اسم النسخة",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "النسخ المسماة محفوظة ومستثناة من التنظيف التلقائي عند امتلاء الحصة التخزينية الخاصة بك.",
+ "Initial version restored" : "استعادة النسخة الأساسية",
+ "Version restored" : "تمّت استعادة النسخة",
+ "Could not restore version" : "تعذّرت استعادة النسخة",
+ "Could not set version label" : "تعذّر تعيين تسمية للنسخة",
+ "Could not delete version" : "تعذّر حذف النسخة",
+ "File versions" : "إصدارات الملف"
},
"nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;");
diff --git a/apps/files_versions/l10n/ar.json b/apps/files_versions/l10n/ar.json
index 03c743f039b..9473ac90f25 100644
--- a/apps/files_versions/l10n/ar.json
+++ b/apps/files_versions/l10n/ar.json
@@ -1,9 +1,27 @@
{ "translations": {
- "Could not revert: %s" : "غير قادر على الاستعادة : %s",
- "Versions" : "الإصدارات",
- "Failed to revert {file} to revision {timestamp}." : "فشل في استعادة {ملف} لنتقيح {الطابع الزمني}",
- "Restore" : "استعادة ",
- "No other versions available" : "لا توجد إصدارات أخرى متاحة",
- "More versions..." : "المزيد من الإصدارات"
+ "Versions" : "النسخ",
+ "This application automatically maintains older versions of files that are changed." : "هذا التطبيق يقوم تلقائيا بحفظ النسخ الأقدم من الملفات التي يتم تعديلها.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "يحتفظ هذا التطبيق تلقائيًا بالنُّسخ القديمة من الملفات بعد تعديلها. عند تمكينه، يتم توفير مجلد الإصدارات المخفية في دليل كل مستخدم و يتم استخدامه لتخزين النسخ الأقدم للملفات. يمكن للمستخدم الرجوع إلى نسخة أقدم من خلال واجهة الويب في أي وقت. حيث يصبح الملف المستبدل نسخةً. يقوم التطبيق تلقائيًا بإدارة مجلد النُّسخ لضمان عدم نفاد الحصة النسبية للحساب بسبب النُّسخ. \t\t\nبالإضافة إلى انتهاء صلاحية النُّسخ، يتأكد تطبيق النُّسخ من عدم استخدام أكثر من 50% من المساحة المجانية المتوفرة حاليًا للحساب لغرض الاحتفاظ بالنُّسخ. إذا تجاوزت مساحة تخزين النُّسخ المخزنة هذا الحد، عندها يقوم التطبيق بحذف النُّسخ الأقدم أولاً حتى ترجع مساحة التخزين إلى الحد المسموح به. مزيد من المعلومات في توثيق التطبيق.",
+ "Current version" : "النسخة الحالية",
+ "Initial version" : "النسخة الأساسية",
+ "You" : "أنت",
+ "Actions for version from {versionHumanExplicitDate}" : "إجراءات على النسخة من {versionHumanExplicitDate}",
+ "Name this version" : "أعط اسماً لهذه النسخة",
+ "Edit version name" : "تعديل اسم النسخة",
+ "Compare to current version" : "قارِن مع النسخة الحاليّة",
+ "Restore version" : "استعادة النسخة",
+ "Download version" : "تنزيل النسخة",
+ "Delete version" : "حذف النسخة",
+ "Cancel" : "إلغاء",
+ "Remove version name" : "إلغ اسم النسخة",
+ "Save version name" : "إحفظ اسم النسخة",
+ "Version name" : "اسم النسخة",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "النسخ المسماة محفوظة ومستثناة من التنظيف التلقائي عند امتلاء الحصة التخزينية الخاصة بك.",
+ "Initial version restored" : "استعادة النسخة الأساسية",
+ "Version restored" : "تمّت استعادة النسخة",
+ "Could not restore version" : "تعذّرت استعادة النسخة",
+ "Could not set version label" : "تعذّر تعيين تسمية للنسخة",
+ "Could not delete version" : "تعذّر حذف النسخة",
+ "File versions" : "إصدارات الملف"
},"pluralForm" :"nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;"
} \ No newline at end of file
diff --git a/apps/files_versions/l10n/ast.js b/apps/files_versions/l10n/ast.js
index 39c5fd98d42..4264f046b1f 100644
--- a/apps/files_versions/l10n/ast.js
+++ b/apps/files_versions/l10n/ast.js
@@ -1,11 +1,27 @@
OC.L10N.register(
"files_versions",
{
- "Could not revert: %s" : "Nun pudo revertise: %s",
"Versions" : "Versiones",
- "Failed to revert {file} to revision {timestamp}." : "Fallu al revertir {file} a la revisión {timestamp}.",
- "Restore" : "Restaurar",
- "No other versions available" : "Nun hai otres versiones disponibles",
- "More versions..." : "Más versiones..."
+ "This application automatically maintains older versions of files that are changed." : "Esta aplicación caltién automáticamente les versiones antigües de los ficheros que camudaron.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Esta aplicación caltién automáticamente les versiones antigües de los ficheros que camudaron. Cuando s'activa, crease una carpeta de versiones anubrida p'atroxar les versiones antigües de los ficheros. Los usuarios puen recuperar una versión anterior pente la interfaz web en cualesquier momentu y el ficheru trocáu conviértese nun versión. L'aplicación xestiona automáticamente la carpeta de versiones p'asegurar de que la cuenta nun escosa la cuota pola mor de les versiones.\n\t\tAmás de la caducidá de les versiones, l'aplicación Versiones garantiza qu'enxamás nun s'usa más del 50% d'espaciu llibre disponible pa la cuenta. Si les versiones atroxaes superen esta llende, l'aplicación va desaniciar les primeres versiones más antigües hasta que se respete esta llende. Tienes más información disponible na documentación de Versiones.",
+ "Current version" : "Versión actual",
+ "Initial version" : "Versión inicial",
+ "You" : "Tu",
+ "Name this version" : "Nomar esta versión",
+ "Edit version name" : "Editar el nome de la versión",
+ "Compare to current version" : "Comparar cola versión actual",
+ "Restore version" : "Restaurar la versión",
+ "Download version" : "Baxar la versión",
+ "Delete version" : "Desaniciar la versión",
+ "Cancel" : "Encaboxar",
+ "Remove version name" : "Quitar el nome de la versión",
+ "Save version name" : "Guardar el nome de la versiones",
+ "Version name" : "Nome de la versión",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Les versiones con nome caltiénense y esclúinse de los borraos automáticos cuando la cuota del discu ta completa.",
+ "Initial version restored" : "Restauróse la versión inicial",
+ "Version restored" : "Restauróse la versión",
+ "Could not restore version" : "Nun se pudo restaurar la versión",
+ "Could not set version label" : "Nun se pudo afitar la etiqueta de la versión",
+ "Could not delete version" : "Nun se pudo desaniciar la versión"
},
"nplurals=2; plural=(n != 1);");
diff --git a/apps/files_versions/l10n/ast.json b/apps/files_versions/l10n/ast.json
index 6a545ffb94b..27cdb853abc 100644
--- a/apps/files_versions/l10n/ast.json
+++ b/apps/files_versions/l10n/ast.json
@@ -1,9 +1,25 @@
{ "translations": {
- "Could not revert: %s" : "Nun pudo revertise: %s",
"Versions" : "Versiones",
- "Failed to revert {file} to revision {timestamp}." : "Fallu al revertir {file} a la revisión {timestamp}.",
- "Restore" : "Restaurar",
- "No other versions available" : "Nun hai otres versiones disponibles",
- "More versions..." : "Más versiones..."
+ "This application automatically maintains older versions of files that are changed." : "Esta aplicación caltién automáticamente les versiones antigües de los ficheros que camudaron.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Esta aplicación caltién automáticamente les versiones antigües de los ficheros que camudaron. Cuando s'activa, crease una carpeta de versiones anubrida p'atroxar les versiones antigües de los ficheros. Los usuarios puen recuperar una versión anterior pente la interfaz web en cualesquier momentu y el ficheru trocáu conviértese nun versión. L'aplicación xestiona automáticamente la carpeta de versiones p'asegurar de que la cuenta nun escosa la cuota pola mor de les versiones.\n\t\tAmás de la caducidá de les versiones, l'aplicación Versiones garantiza qu'enxamás nun s'usa más del 50% d'espaciu llibre disponible pa la cuenta. Si les versiones atroxaes superen esta llende, l'aplicación va desaniciar les primeres versiones más antigües hasta que se respete esta llende. Tienes más información disponible na documentación de Versiones.",
+ "Current version" : "Versión actual",
+ "Initial version" : "Versión inicial",
+ "You" : "Tu",
+ "Name this version" : "Nomar esta versión",
+ "Edit version name" : "Editar el nome de la versión",
+ "Compare to current version" : "Comparar cola versión actual",
+ "Restore version" : "Restaurar la versión",
+ "Download version" : "Baxar la versión",
+ "Delete version" : "Desaniciar la versión",
+ "Cancel" : "Encaboxar",
+ "Remove version name" : "Quitar el nome de la versión",
+ "Save version name" : "Guardar el nome de la versiones",
+ "Version name" : "Nome de la versión",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Les versiones con nome caltiénense y esclúinse de los borraos automáticos cuando la cuota del discu ta completa.",
+ "Initial version restored" : "Restauróse la versión inicial",
+ "Version restored" : "Restauróse la versión",
+ "Could not restore version" : "Nun se pudo restaurar la versión",
+ "Could not set version label" : "Nun se pudo afitar la etiqueta de la versión",
+ "Could not delete version" : "Nun se pudo desaniciar la versión"
},"pluralForm" :"nplurals=2; plural=(n != 1);"
} \ No newline at end of file
diff --git a/apps/files_versions/l10n/az.js b/apps/files_versions/l10n/az.js
deleted file mode 100644
index 171cb31ab1a..00000000000
--- a/apps/files_versions/l10n/az.js
+++ /dev/null
@@ -1,11 +0,0 @@
-OC.L10N.register(
- "files_versions",
- {
- "Could not revert: %s" : "Geri qaytarmaq olmur: %s",
- "Versions" : "Versiyaları",
- "Failed to revert {file} to revision {timestamp}." : "{timestamp} yenidən baxılması üçün {file} geri qaytarmaq mümkün olmadı.",
- "Restore" : "Geri qaytar",
- "No other versions available" : "Başqa versiyalar mövcud deyil",
- "More versions..." : "Əlavə versiyalar"
-},
-"nplurals=2; plural=(n != 1);");
diff --git a/apps/files_versions/l10n/az.json b/apps/files_versions/l10n/az.json
deleted file mode 100644
index 3f4e0275dd5..00000000000
--- a/apps/files_versions/l10n/az.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{ "translations": {
- "Could not revert: %s" : "Geri qaytarmaq olmur: %s",
- "Versions" : "Versiyaları",
- "Failed to revert {file} to revision {timestamp}." : "{timestamp} yenidən baxılması üçün {file} geri qaytarmaq mümkün olmadı.",
- "Restore" : "Geri qaytar",
- "No other versions available" : "Başqa versiyalar mövcud deyil",
- "More versions..." : "Əlavə versiyalar"
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
-} \ No newline at end of file
diff --git a/apps/files_versions/l10n/be.js b/apps/files_versions/l10n/be.js
new file mode 100644
index 00000000000..7e5200ecb83
--- /dev/null
+++ b/apps/files_versions/l10n/be.js
@@ -0,0 +1,17 @@
+OC.L10N.register(
+ "files_versions",
+ {
+ "Versions" : "Версіі",
+ "Current version" : "Бягучая версія",
+ "You" : "Вы",
+ "Delete version" : "Выдаліць версію",
+ "Cancel" : "Скасаваць",
+ "Remove version name" : "Выдаліць назву версіі",
+ "Save version name" : "Захаваць назву версіі",
+ "Version name" : "Назва версіі",
+ "Version restored" : "Версія адноўлена",
+ "Could not restore version" : "Не ўдалося аднавіць версію",
+ "Could not delete version" : "Не ўдалося выдаліць версію",
+ "File versions" : "Версіі файла"
+},
+"nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);");
diff --git a/apps/files_versions/l10n/be.json b/apps/files_versions/l10n/be.json
new file mode 100644
index 00000000000..27e4f646a97
--- /dev/null
+++ b/apps/files_versions/l10n/be.json
@@ -0,0 +1,15 @@
+{ "translations": {
+ "Versions" : "Версіі",
+ "Current version" : "Бягучая версія",
+ "You" : "Вы",
+ "Delete version" : "Выдаліць версію",
+ "Cancel" : "Скасаваць",
+ "Remove version name" : "Выдаліць назву версіі",
+ "Save version name" : "Захаваць назву версіі",
+ "Version name" : "Назва версіі",
+ "Version restored" : "Версія адноўлена",
+ "Could not restore version" : "Не ўдалося аднавіць версію",
+ "Could not delete version" : "Не ўдалося выдаліць версію",
+ "File versions" : "Версіі файла"
+},"pluralForm" :"nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);"
+} \ No newline at end of file
diff --git a/apps/files_versions/l10n/bg.js b/apps/files_versions/l10n/bg.js
new file mode 100644
index 00000000000..94fbc38c830
--- /dev/null
+++ b/apps/files_versions/l10n/bg.js
@@ -0,0 +1,22 @@
+OC.L10N.register(
+ "files_versions",
+ {
+ "Versions" : "Версии",
+ "This application automatically maintains older versions of files that are changed." : "Това приложение автоматично поддържа по-стари версии на файлове, които са променени.",
+ "Current version" : "Текуща версия",
+ "Name this version" : "Наименуване на тази версия",
+ "Edit version name" : "Редактиране на името на версия",
+ "Restore version" : "Възстановяване на версия",
+ "Download version" : "Изтегляне на версия",
+ "Delete version" : "Изтриване на версия",
+ "Cancel" : "Отказ",
+ "Remove version name" : "Премахване на име на версия",
+ "Save version name" : "Записване на име на версия",
+ "Version name" : "Име на версия",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Именуваните версии се запазват и се изключват от автоматичните почиствания, когато квотата за хранилище е пълна.",
+ "Initial version restored" : "Възстановяване на първоначалната версия",
+ "Version restored" : "Версията е възстановена",
+ "Could not restore version" : "Версията не можа да се възстанови",
+ "Could not delete version" : "Версията не можа да се изтрие"
+},
+"nplurals=2; plural=(n != 1);");
diff --git a/apps/files_versions/l10n/bg.json b/apps/files_versions/l10n/bg.json
new file mode 100644
index 00000000000..52243426e7d
--- /dev/null
+++ b/apps/files_versions/l10n/bg.json
@@ -0,0 +1,20 @@
+{ "translations": {
+ "Versions" : "Версии",
+ "This application automatically maintains older versions of files that are changed." : "Това приложение автоматично поддържа по-стари версии на файлове, които са променени.",
+ "Current version" : "Текуща версия",
+ "Name this version" : "Наименуване на тази версия",
+ "Edit version name" : "Редактиране на името на версия",
+ "Restore version" : "Възстановяване на версия",
+ "Download version" : "Изтегляне на версия",
+ "Delete version" : "Изтриване на версия",
+ "Cancel" : "Отказ",
+ "Remove version name" : "Премахване на име на версия",
+ "Save version name" : "Записване на име на версия",
+ "Version name" : "Име на версия",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Именуваните версии се запазват и се изключват от автоматичните почиствания, когато квотата за хранилище е пълна.",
+ "Initial version restored" : "Възстановяване на първоначалната версия",
+ "Version restored" : "Версията е възстановена",
+ "Could not restore version" : "Версията не можа да се възстанови",
+ "Could not delete version" : "Версията не можа да се изтрие"
+},"pluralForm" :"nplurals=2; plural=(n != 1);"
+} \ No newline at end of file
diff --git a/apps/files_versions/l10n/bg_BG.js b/apps/files_versions/l10n/bg_BG.js
deleted file mode 100644
index cea297b6974..00000000000
--- a/apps/files_versions/l10n/bg_BG.js
+++ /dev/null
@@ -1,11 +0,0 @@
-OC.L10N.register(
- "files_versions",
- {
- "Could not revert: %s" : "Грешка при връщане: %s",
- "Versions" : "Версии",
- "Failed to revert {file} to revision {timestamp}." : "Грешка при връщане на {file} към версия {timestamp}.",
- "Restore" : "Възтановяви",
- "No other versions available" : "Няма други налични версии",
- "More versions..." : "Още версии..."
-},
-"nplurals=2; plural=(n != 1);");
diff --git a/apps/files_versions/l10n/bg_BG.json b/apps/files_versions/l10n/bg_BG.json
deleted file mode 100644
index 826c7136d1a..00000000000
--- a/apps/files_versions/l10n/bg_BG.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{ "translations": {
- "Could not revert: %s" : "Грешка при връщане: %s",
- "Versions" : "Версии",
- "Failed to revert {file} to revision {timestamp}." : "Грешка при връщане на {file} към версия {timestamp}.",
- "Restore" : "Възтановяви",
- "No other versions available" : "Няма други налични версии",
- "More versions..." : "Още версии..."
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
-} \ No newline at end of file
diff --git a/apps/files_versions/l10n/bn_BD.js b/apps/files_versions/l10n/bn_BD.js
deleted file mode 100644
index e92e84268c1..00000000000
--- a/apps/files_versions/l10n/bn_BD.js
+++ /dev/null
@@ -1,11 +0,0 @@
-OC.L10N.register(
- "files_versions",
- {
- "Could not revert: %s" : "ফিরে যাওয়া গেলনা: %s",
- "Versions" : "সংষ্করন",
- "Failed to revert {file} to revision {timestamp}." : " {file} সংশোধিত {timestamp} এ ফিরে যেতে ব্যার্থ হলো।",
- "Restore" : "ফিরিয়ে দাও",
- "No other versions available" : "আর কোন সংষ্করণ প্রাপ্তব্য নয়",
- "More versions..." : "আরো সংষ্করণ...."
-},
-"nplurals=2; plural=(n != 1);");
diff --git a/apps/files_versions/l10n/bn_BD.json b/apps/files_versions/l10n/bn_BD.json
deleted file mode 100644
index ec8aa74e18b..00000000000
--- a/apps/files_versions/l10n/bn_BD.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{ "translations": {
- "Could not revert: %s" : "ফিরে যাওয়া গেলনা: %s",
- "Versions" : "সংষ্করন",
- "Failed to revert {file} to revision {timestamp}." : " {file} সংশোধিত {timestamp} এ ফিরে যেতে ব্যার্থ হলো।",
- "Restore" : "ফিরিয়ে দাও",
- "No other versions available" : "আর কোন সংষ্করণ প্রাপ্তব্য নয়",
- "More versions..." : "আরো সংষ্করণ...."
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
-} \ No newline at end of file
diff --git a/apps/files_versions/l10n/bn_IN.js b/apps/files_versions/l10n/bn_IN.js
deleted file mode 100644
index 34fce5d5662..00000000000
--- a/apps/files_versions/l10n/bn_IN.js
+++ /dev/null
@@ -1,11 +0,0 @@
-OC.L10N.register(
- "files_versions",
- {
- "Could not revert: %s" : "প্রত্যাবর্তন করা যায়নি: %s",
- "Versions" : "সংস্করণ",
- "Failed to revert {file} to revision {timestamp}." : "{ফাইল} প্রত্যাবর্তন থেকে পুনর্বিবেচনা {টাইমস্ট্যাম্প} করতে ব্যর্থ।",
- "Restore" : "পুনরুদ্ধার",
- "No other versions available" : "আর কোন সংস্করণ পাওয়া যাচ্ছে না",
- "More versions..." : "আরো সংস্করণ..."
-},
-"nplurals=2; plural=(n != 1);");
diff --git a/apps/files_versions/l10n/bn_IN.json b/apps/files_versions/l10n/bn_IN.json
deleted file mode 100644
index 34deb365a1e..00000000000
--- a/apps/files_versions/l10n/bn_IN.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{ "translations": {
- "Could not revert: %s" : "প্রত্যাবর্তন করা যায়নি: %s",
- "Versions" : "সংস্করণ",
- "Failed to revert {file} to revision {timestamp}." : "{ফাইল} প্রত্যাবর্তন থেকে পুনর্বিবেচনা {টাইমস্ট্যাম্প} করতে ব্যর্থ।",
- "Restore" : "পুনরুদ্ধার",
- "No other versions available" : "আর কোন সংস্করণ পাওয়া যাচ্ছে না",
- "More versions..." : "আরো সংস্করণ..."
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
-} \ No newline at end of file
diff --git a/apps/files_versions/l10n/bs.js b/apps/files_versions/l10n/bs.js
deleted file mode 100644
index fc207e1517c..00000000000
--- a/apps/files_versions/l10n/bs.js
+++ /dev/null
@@ -1,11 +0,0 @@
-OC.L10N.register(
- "files_versions",
- {
- "Could not revert: %s" : "Nije moguće vratiti: %s",
- "Versions" : "Verzije",
- "Failed to revert {file} to revision {timestamp}." : "Nije uspelo vraćanje {file} na reviziju {timestamp}.",
- "Restore" : "Obnovi",
- "No other versions available" : "Druge verzije su nedostupne",
- "More versions..." : "Više verzija..."
-},
-"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);");
diff --git a/apps/files_versions/l10n/bs.json b/apps/files_versions/l10n/bs.json
deleted file mode 100644
index 50d72404f2f..00000000000
--- a/apps/files_versions/l10n/bs.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{ "translations": {
- "Could not revert: %s" : "Nije moguće vratiti: %s",
- "Versions" : "Verzije",
- "Failed to revert {file} to revision {timestamp}." : "Nije uspelo vraćanje {file} na reviziju {timestamp}.",
- "Restore" : "Obnovi",
- "No other versions available" : "Druge verzije su nedostupne",
- "More versions..." : "Više verzija..."
-},"pluralForm" :"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"
-} \ No newline at end of file
diff --git a/apps/files_versions/l10n/ca.js b/apps/files_versions/l10n/ca.js
index 660db0d6f29..104759c334a 100644
--- a/apps/files_versions/l10n/ca.js
+++ b/apps/files_versions/l10n/ca.js
@@ -1,11 +1,28 @@
OC.L10N.register(
"files_versions",
{
- "Could not revert: %s" : "No s'ha pogut revertir: %s",
"Versions" : "Versions",
- "Failed to revert {file} to revision {timestamp}." : "Ha fallat en retornar {file} a la revisió {timestamp}",
- "Restore" : "Recupera",
- "No other versions available" : "No hi ha altres versions disponibles",
- "More versions..." : "Més versions..."
+ "This application automatically maintains older versions of files that are changed." : "Aquesta aplicació conserva automàticament les versions antigues dels fitxers modificats.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Aquesta aplicació conserva automàticament les versions antigues dels fitxers modificats. Si s'habilita, es crearà una carpeta de versions oculta a cada carpeta del compte i s'utilitzarà per a emmagatzemar les versions antigues dels fitxers. Els usuaris poden recuperar una versió anterior a través de la interfície web en qualsevol moment i el fitxer substituït es converteix en una versió. L'aplicació administra automàticament la carpeta de versions per a assegurar-se que l'usuari no es quedi sense espai per culpa de les versions.\n\t\tA més del venciment de les versions, l'aplicació Versions garanteix que no s'utilitzarà mai més del 50% de l'espai lliure disponible actualment per al compte. Si les versions emmagatzemades superen aquest límit, l'aplicació suprimirà primer les versions més antigues fins que es respecti aquest límit. Podeu trobar més informació a la documentació de Versions.",
+ "Current version" : "Versió actual",
+ "Initial version" : "Versió inicial",
+ "You" : "Vós",
+ "Actions for version from {versionHumanExplicitDate}" : "Accions per a la versió a partir de {versionHumanExplicitDate}",
+ "Name this version" : "Anomena aquesta versió",
+ "Edit version name" : "Edita el nom de la versió",
+ "Compare to current version" : "Compara amb la versió actual",
+ "Restore version" : "Recupera la versió",
+ "Download version" : "Baixa la versió",
+ "Delete version" : "Suprimeix la versió",
+ "Cancel" : "Cancel·la",
+ "Remove version name" : "Suprimeix el nom de la versió",
+ "Save version name" : "Desa el nom de la versió",
+ "Version name" : "Nom de la versió",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Les versions amb nom es conserven i s'exclouen de les neteges automàtiques quan teniu l'emmagatzematge ple.",
+ "Initial version restored" : "S'ha recuperat la versió inicial",
+ "Version restored" : "S'ha recuperat la versió",
+ "Could not restore version" : "No s'ha pogut recuperar la versió",
+ "Could not set version label" : "No s'ha pogut definir l'etiqueta de la versió",
+ "Could not delete version" : "No s'ha pogut suprimir la versió"
},
"nplurals=2; plural=(n != 1);");
diff --git a/apps/files_versions/l10n/ca.json b/apps/files_versions/l10n/ca.json
index 7cd2ddbcf51..f4818d3c7ad 100644
--- a/apps/files_versions/l10n/ca.json
+++ b/apps/files_versions/l10n/ca.json
@@ -1,9 +1,26 @@
{ "translations": {
- "Could not revert: %s" : "No s'ha pogut revertir: %s",
"Versions" : "Versions",
- "Failed to revert {file} to revision {timestamp}." : "Ha fallat en retornar {file} a la revisió {timestamp}",
- "Restore" : "Recupera",
- "No other versions available" : "No hi ha altres versions disponibles",
- "More versions..." : "Més versions..."
+ "This application automatically maintains older versions of files that are changed." : "Aquesta aplicació conserva automàticament les versions antigues dels fitxers modificats.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Aquesta aplicació conserva automàticament les versions antigues dels fitxers modificats. Si s'habilita, es crearà una carpeta de versions oculta a cada carpeta del compte i s'utilitzarà per a emmagatzemar les versions antigues dels fitxers. Els usuaris poden recuperar una versió anterior a través de la interfície web en qualsevol moment i el fitxer substituït es converteix en una versió. L'aplicació administra automàticament la carpeta de versions per a assegurar-se que l'usuari no es quedi sense espai per culpa de les versions.\n\t\tA més del venciment de les versions, l'aplicació Versions garanteix que no s'utilitzarà mai més del 50% de l'espai lliure disponible actualment per al compte. Si les versions emmagatzemades superen aquest límit, l'aplicació suprimirà primer les versions més antigues fins que es respecti aquest límit. Podeu trobar més informació a la documentació de Versions.",
+ "Current version" : "Versió actual",
+ "Initial version" : "Versió inicial",
+ "You" : "Vós",
+ "Actions for version from {versionHumanExplicitDate}" : "Accions per a la versió a partir de {versionHumanExplicitDate}",
+ "Name this version" : "Anomena aquesta versió",
+ "Edit version name" : "Edita el nom de la versió",
+ "Compare to current version" : "Compara amb la versió actual",
+ "Restore version" : "Recupera la versió",
+ "Download version" : "Baixa la versió",
+ "Delete version" : "Suprimeix la versió",
+ "Cancel" : "Cancel·la",
+ "Remove version name" : "Suprimeix el nom de la versió",
+ "Save version name" : "Desa el nom de la versió",
+ "Version name" : "Nom de la versió",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Les versions amb nom es conserven i s'exclouen de les neteges automàtiques quan teniu l'emmagatzematge ple.",
+ "Initial version restored" : "S'ha recuperat la versió inicial",
+ "Version restored" : "S'ha recuperat la versió",
+ "Could not restore version" : "No s'ha pogut recuperar la versió",
+ "Could not set version label" : "No s'ha pogut definir l'etiqueta de la versió",
+ "Could not delete version" : "No s'ha pogut suprimir la versió"
},"pluralForm" :"nplurals=2; plural=(n != 1);"
} \ No newline at end of file
diff --git a/apps/files_versions/l10n/cs.js b/apps/files_versions/l10n/cs.js
new file mode 100644
index 00000000000..b9192c54cf5
--- /dev/null
+++ b/apps/files_versions/l10n/cs.js
@@ -0,0 +1,29 @@
+OC.L10N.register(
+ "files_versions",
+ {
+ "Versions" : "Verze",
+ "This application automatically maintains older versions of files that are changed." : "Tato aplikace automaticky uchovává starší verze souborů, které se změnily.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Tato aplikace automaticky uchovává starší verze souborů, které se změnily. Když je zapnutá, je ve složce každého uživatele vytvořena skrytá složka pro verze a do ní jsou ukládány staré verze souborů. Uživatel se (prostřednictvím webového rozhraní) kdykoli může vrátit ke starší verzi s tím, že sám nahrazovaný nejnovější soubor se tak stane verzí. Aplikace automaticky spravuje složku s verzemi tak, aby bylo zajištěno, že účet kvůli starým verzím nevyčerpá přidělenou kvótu.\n\t\tKrom konců platností verzí, aplikace Verze zajišťuje, že nikdy nepoužije více než 50% právě dostupného volného prostoru, který má účet k dispozici. Pokud uložené verze přesáhnou tento limit, aplikace smaže ty nejstarší, aby se do tohoto limitu vešla. Podrobnosti naleznete v dokumentaci k aplikaci Verze.",
+ "Current version" : "Stávající verze",
+ "Initial version" : "Počáteční verze",
+ "You" : "Vy",
+ "Actions for version from {versionHumanExplicitDate}" : "Akce pro verzi od {versionHumanExplicitDate}",
+ "Name this version" : "Nazvat tuto verzi",
+ "Edit version name" : "Upravit název verze",
+ "Compare to current version" : "Porovnat se stávající verzí",
+ "Restore version" : "Obnovit verzi",
+ "Download version" : "Stáhnout verzi",
+ "Delete version" : "Smazat verzi",
+ "Cancel" : "Storno",
+ "Remove version name" : "Odebrat název verze",
+ "Save version name" : "Uložit název verze",
+ "Version name" : "Název verze",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Výslovně verze jsou trvalé a vynechány z automatického čištění při vyčerpání vaší kvóty na úložišti.",
+ "Initial version restored" : "Počáteční verze obnovena",
+ "Version restored" : "Verze obnovena",
+ "Could not restore version" : "Verzi se nedaří obnovit",
+ "Could not set version label" : "Nepodařilo se nastavit štítek verze",
+ "Could not delete version" : "Nedaří se smazat verzi",
+ "File versions" : "Verze souborů"
+},
+"nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n <= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;");
diff --git a/apps/files_versions/l10n/cs.json b/apps/files_versions/l10n/cs.json
new file mode 100644
index 00000000000..1d47a9c69dc
--- /dev/null
+++ b/apps/files_versions/l10n/cs.json
@@ -0,0 +1,27 @@
+{ "translations": {
+ "Versions" : "Verze",
+ "This application automatically maintains older versions of files that are changed." : "Tato aplikace automaticky uchovává starší verze souborů, které se změnily.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Tato aplikace automaticky uchovává starší verze souborů, které se změnily. Když je zapnutá, je ve složce každého uživatele vytvořena skrytá složka pro verze a do ní jsou ukládány staré verze souborů. Uživatel se (prostřednictvím webového rozhraní) kdykoli může vrátit ke starší verzi s tím, že sám nahrazovaný nejnovější soubor se tak stane verzí. Aplikace automaticky spravuje složku s verzemi tak, aby bylo zajištěno, že účet kvůli starým verzím nevyčerpá přidělenou kvótu.\n\t\tKrom konců platností verzí, aplikace Verze zajišťuje, že nikdy nepoužije více než 50% právě dostupného volného prostoru, který má účet k dispozici. Pokud uložené verze přesáhnou tento limit, aplikace smaže ty nejstarší, aby se do tohoto limitu vešla. Podrobnosti naleznete v dokumentaci k aplikaci Verze.",
+ "Current version" : "Stávající verze",
+ "Initial version" : "Počáteční verze",
+ "You" : "Vy",
+ "Actions for version from {versionHumanExplicitDate}" : "Akce pro verzi od {versionHumanExplicitDate}",
+ "Name this version" : "Nazvat tuto verzi",
+ "Edit version name" : "Upravit název verze",
+ "Compare to current version" : "Porovnat se stávající verzí",
+ "Restore version" : "Obnovit verzi",
+ "Download version" : "Stáhnout verzi",
+ "Delete version" : "Smazat verzi",
+ "Cancel" : "Storno",
+ "Remove version name" : "Odebrat název verze",
+ "Save version name" : "Uložit název verze",
+ "Version name" : "Název verze",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Výslovně verze jsou trvalé a vynechány z automatického čištění při vyčerpání vaší kvóty na úložišti.",
+ "Initial version restored" : "Počáteční verze obnovena",
+ "Version restored" : "Verze obnovena",
+ "Could not restore version" : "Verzi se nedaří obnovit",
+ "Could not set version label" : "Nepodařilo se nastavit štítek verze",
+ "Could not delete version" : "Nedaří se smazat verzi",
+ "File versions" : "Verze souborů"
+},"pluralForm" :"nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n <= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;"
+} \ No newline at end of file
diff --git a/apps/files_versions/l10n/cs_CZ.js b/apps/files_versions/l10n/cs_CZ.js
deleted file mode 100644
index bd7488fc543..00000000000
--- a/apps/files_versions/l10n/cs_CZ.js
+++ /dev/null
@@ -1,11 +0,0 @@
-OC.L10N.register(
- "files_versions",
- {
- "Could not revert: %s" : "Nelze vrátit: %s",
- "Versions" : "Verze",
- "Failed to revert {file} to revision {timestamp}." : "Selhalo vrácení souboru {file} na verzi {timestamp}.",
- "Restore" : "Obnovit",
- "No other versions available" : "Žádné další verze nejsou dostupné",
- "More versions..." : "Více verzí..."
-},
-"nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;");
diff --git a/apps/files_versions/l10n/cs_CZ.json b/apps/files_versions/l10n/cs_CZ.json
deleted file mode 100644
index 6af93e2bd5e..00000000000
--- a/apps/files_versions/l10n/cs_CZ.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{ "translations": {
- "Could not revert: %s" : "Nelze vrátit: %s",
- "Versions" : "Verze",
- "Failed to revert {file} to revision {timestamp}." : "Selhalo vrácení souboru {file} na verzi {timestamp}.",
- "Restore" : "Obnovit",
- "No other versions available" : "Žádné další verze nejsou dostupné",
- "More versions..." : "Více verzí..."
-},"pluralForm" :"nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;"
-} \ No newline at end of file
diff --git a/apps/files_versions/l10n/cy_GB.js b/apps/files_versions/l10n/cy_GB.js
deleted file mode 100644
index e5285e2e157..00000000000
--- a/apps/files_versions/l10n/cy_GB.js
+++ /dev/null
@@ -1,6 +0,0 @@
-OC.L10N.register(
- "files_versions",
- {
- "Restore" : "Adfer"
-},
-"nplurals=4; plural=(n==1) ? 0 : (n==2) ? 1 : (n != 8 && n != 11) ? 2 : 3;");
diff --git a/apps/files_versions/l10n/cy_GB.json b/apps/files_versions/l10n/cy_GB.json
deleted file mode 100644
index 5ad23a5ac6f..00000000000
--- a/apps/files_versions/l10n/cy_GB.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{ "translations": {
- "Restore" : "Adfer"
-},"pluralForm" :"nplurals=4; plural=(n==1) ? 0 : (n==2) ? 1 : (n != 8 && n != 11) ? 2 : 3;"
-} \ No newline at end of file
diff --git a/apps/files_versions/l10n/da.js b/apps/files_versions/l10n/da.js
index 9d6748f5bdc..7898c6c27b6 100644
--- a/apps/files_versions/l10n/da.js
+++ b/apps/files_versions/l10n/da.js
@@ -1,11 +1,29 @@
OC.L10N.register(
"files_versions",
{
- "Could not revert: %s" : "Kunne ikke genskabe: %s",
"Versions" : "Versioner",
- "Failed to revert {file} to revision {timestamp}." : "Kunne ikke tilbagerulle {file} til den tidligere udgave: {timestamp}.",
- "Restore" : "Gendan",
- "No other versions available" : "Ingen andre versioner tilgængelig",
- "More versions..." : "Flere versioner..."
+ "This application automatically maintains older versions of files that are changed." : "Denne applikation styrer automatisk tidligere versioner af ændrede filer.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Denne applikation vedligeholder automatisk ældre versioner af filer, der ændres. Når den er aktiveret, er der klargjort en skjult versionsmappe i hver brugers mappe, som anvendes til at gemme gamle filversioner. En bruger kan til enhver tid vende tilbage til en ældre version via webgrænsefladen, hvor den erstattede fil bliver til en version. App'en administrerer automatisk versionsmappen for at sikre, at kontoen ikke løber tør for kvote på grund af versioner..\n\t\tUd over udløbet af versioner sørger versionsappen for aldrig at bruge mere end 50 % af kontoens aktuelt ledige plads. Hvis lagrede versioner overskrider denne grænse, så vil app'en først slette de ældste versioner, indtil den når denne grænse. Mere information er tilgængelig i versionsdokumentationen.",
+ "Current version" : "Nuværende version",
+ "Initial version" : "Oprindelig version",
+ "You" : "Dig",
+ "Actions for version from {versionHumanExplicitDate}" : "Handlinger for version fra {versionHumanExplicitDate}",
+ "Name this version" : "Navngiv denne version",
+ "Edit version name" : "Rediger versionsnavn",
+ "Compare to current version" : "Sammenlign med den aktuelle version",
+ "Restore version" : "Gendan version",
+ "Download version" : "Download version",
+ "Delete version" : "Slet version",
+ "Cancel" : "Annuller",
+ "Remove version name" : "Fjern versionsnavn",
+ "Save version name" : "Gem versionsnavn",
+ "Version name" : "Versionsnavn",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Navngivne versioner bevares og udelukkes fra automatiske oprydninger, når din lagerkvote er fuld.",
+ "Initial version restored" : "Første version genoprettet",
+ "Version restored" : "Version genoprettet",
+ "Could not restore version" : "Kunne ikke genoprette version",
+ "Could not set version label" : "Kunne ikke indstille versionslabel",
+ "Could not delete version" : "Kunne ikke slette version",
+ "File versions" : "Fil versioner"
},
"nplurals=2; plural=(n != 1);");
diff --git a/apps/files_versions/l10n/da.json b/apps/files_versions/l10n/da.json
index 5630e3aba82..7da65d1ee3f 100644
--- a/apps/files_versions/l10n/da.json
+++ b/apps/files_versions/l10n/da.json
@@ -1,9 +1,27 @@
{ "translations": {
- "Could not revert: %s" : "Kunne ikke genskabe: %s",
"Versions" : "Versioner",
- "Failed to revert {file} to revision {timestamp}." : "Kunne ikke tilbagerulle {file} til den tidligere udgave: {timestamp}.",
- "Restore" : "Gendan",
- "No other versions available" : "Ingen andre versioner tilgængelig",
- "More versions..." : "Flere versioner..."
+ "This application automatically maintains older versions of files that are changed." : "Denne applikation styrer automatisk tidligere versioner af ændrede filer.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Denne applikation vedligeholder automatisk ældre versioner af filer, der ændres. Når den er aktiveret, er der klargjort en skjult versionsmappe i hver brugers mappe, som anvendes til at gemme gamle filversioner. En bruger kan til enhver tid vende tilbage til en ældre version via webgrænsefladen, hvor den erstattede fil bliver til en version. App'en administrerer automatisk versionsmappen for at sikre, at kontoen ikke løber tør for kvote på grund af versioner..\n\t\tUd over udløbet af versioner sørger versionsappen for aldrig at bruge mere end 50 % af kontoens aktuelt ledige plads. Hvis lagrede versioner overskrider denne grænse, så vil app'en først slette de ældste versioner, indtil den når denne grænse. Mere information er tilgængelig i versionsdokumentationen.",
+ "Current version" : "Nuværende version",
+ "Initial version" : "Oprindelig version",
+ "You" : "Dig",
+ "Actions for version from {versionHumanExplicitDate}" : "Handlinger for version fra {versionHumanExplicitDate}",
+ "Name this version" : "Navngiv denne version",
+ "Edit version name" : "Rediger versionsnavn",
+ "Compare to current version" : "Sammenlign med den aktuelle version",
+ "Restore version" : "Gendan version",
+ "Download version" : "Download version",
+ "Delete version" : "Slet version",
+ "Cancel" : "Annuller",
+ "Remove version name" : "Fjern versionsnavn",
+ "Save version name" : "Gem versionsnavn",
+ "Version name" : "Versionsnavn",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Navngivne versioner bevares og udelukkes fra automatiske oprydninger, når din lagerkvote er fuld.",
+ "Initial version restored" : "Første version genoprettet",
+ "Version restored" : "Version genoprettet",
+ "Could not restore version" : "Kunne ikke genoprette version",
+ "Could not set version label" : "Kunne ikke indstille versionslabel",
+ "Could not delete version" : "Kunne ikke slette version",
+ "File versions" : "Fil versioner"
},"pluralForm" :"nplurals=2; plural=(n != 1);"
} \ No newline at end of file
diff --git a/apps/files_versions/l10n/de.js b/apps/files_versions/l10n/de.js
index 285e42517b3..95d5da11277 100644
--- a/apps/files_versions/l10n/de.js
+++ b/apps/files_versions/l10n/de.js
@@ -1,11 +1,29 @@
OC.L10N.register(
"files_versions",
{
- "Could not revert: %s" : "Konnte %s nicht zurücksetzen",
"Versions" : "Versionen",
- "Failed to revert {file} to revision {timestamp}." : "Konnte {file} der Revision {timestamp} nicht rückgängig machen.",
- "Restore" : "Wiederherstellen",
- "No other versions available" : "Keine anderen Versionen verfügbar",
- "More versions..." : "Weitere Versionen…"
+ "This application automatically maintains older versions of files that are changed." : "Diese App verwaltet automatisch ältere Versionen geänderter Dateien.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Diese Anwendung verwaltet automatisch ältere Versionen von Dateien, die geändert wurden. Wenn aktiviert, wird ein Ordner mit versteckten Versionen im Verzeichnis jedes Benutzers bereitgestellt und wird zum Speichern alter Dateiversionen verwendet. Ein Benutzer kann jederzeit über die Web-Oberfläche auf eine ältere Version zurückgreifen, wobei die ersetzte Datei dann eine Version wird. Die App verwaltet automatisch den Versionsordner, um sicherzustellen, dass dem Konto nicht der Speicherplatz aufgrund von zu vielen Versionen ausgeht.\nZusätzlich zum Ablauf der Versionen stellt die Versions-App sicher, dass nie mehr als 50% des derzeit verfügbaren freien Speicherplatzes des Kontos für die Versionierung genutzt werden. Wenn gespeicherte Versionen diese Grenze überschreiten, löscht die App zuerst die ältesten Versionen, bis sie die 50% Grenze erreicht hat. Weitere Informationen findest du in der Versionsdokumentation.",
+ "Current version" : "Aktuelle Version",
+ "Initial version" : "Initiale Version",
+ "You" : "Du",
+ "Actions for version from {versionHumanExplicitDate}" : "Aktionen für Version vom {versionHumanExplicitDate}",
+ "Name this version" : "Diese Version benennen",
+ "Edit version name" : "Versionsnamen bearbeiten",
+ "Compare to current version" : "Mit aktueller Version vergleichen",
+ "Restore version" : "Version wiederherstellen",
+ "Download version" : "Version herunterladen",
+ "Delete version" : "Version löschen",
+ "Cancel" : "Abbrechen",
+ "Remove version name" : "Versionsnamen entfernen",
+ "Save version name" : "Versionsnamen speichern",
+ "Version name" : "Versionsname",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Benannte Versionen werden beibehalten und von automatischen Bereinigungen ausgeschlossen, wenn dein Speicherkontingent voll ist.",
+ "Initial version restored" : "Ursprüngliche Version wiederhergestellt",
+ "Version restored" : "Version wiederhergestellt",
+ "Could not restore version" : "Version konnte nicht wiederhergestellt werden",
+ "Could not set version label" : "Der Versionsname konnte nicht festgelegt werden",
+ "Could not delete version" : "Version konnte nicht gelöscht werden",
+ "File versions" : "Dateiversionen"
},
"nplurals=2; plural=(n != 1);");
diff --git a/apps/files_versions/l10n/de.json b/apps/files_versions/l10n/de.json
index a1f3c12649f..28becae635c 100644
--- a/apps/files_versions/l10n/de.json
+++ b/apps/files_versions/l10n/de.json
@@ -1,9 +1,27 @@
{ "translations": {
- "Could not revert: %s" : "Konnte %s nicht zurücksetzen",
"Versions" : "Versionen",
- "Failed to revert {file} to revision {timestamp}." : "Konnte {file} der Revision {timestamp} nicht rückgängig machen.",
- "Restore" : "Wiederherstellen",
- "No other versions available" : "Keine anderen Versionen verfügbar",
- "More versions..." : "Weitere Versionen…"
+ "This application automatically maintains older versions of files that are changed." : "Diese App verwaltet automatisch ältere Versionen geänderter Dateien.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Diese Anwendung verwaltet automatisch ältere Versionen von Dateien, die geändert wurden. Wenn aktiviert, wird ein Ordner mit versteckten Versionen im Verzeichnis jedes Benutzers bereitgestellt und wird zum Speichern alter Dateiversionen verwendet. Ein Benutzer kann jederzeit über die Web-Oberfläche auf eine ältere Version zurückgreifen, wobei die ersetzte Datei dann eine Version wird. Die App verwaltet automatisch den Versionsordner, um sicherzustellen, dass dem Konto nicht der Speicherplatz aufgrund von zu vielen Versionen ausgeht.\nZusätzlich zum Ablauf der Versionen stellt die Versions-App sicher, dass nie mehr als 50% des derzeit verfügbaren freien Speicherplatzes des Kontos für die Versionierung genutzt werden. Wenn gespeicherte Versionen diese Grenze überschreiten, löscht die App zuerst die ältesten Versionen, bis sie die 50% Grenze erreicht hat. Weitere Informationen findest du in der Versionsdokumentation.",
+ "Current version" : "Aktuelle Version",
+ "Initial version" : "Initiale Version",
+ "You" : "Du",
+ "Actions for version from {versionHumanExplicitDate}" : "Aktionen für Version vom {versionHumanExplicitDate}",
+ "Name this version" : "Diese Version benennen",
+ "Edit version name" : "Versionsnamen bearbeiten",
+ "Compare to current version" : "Mit aktueller Version vergleichen",
+ "Restore version" : "Version wiederherstellen",
+ "Download version" : "Version herunterladen",
+ "Delete version" : "Version löschen",
+ "Cancel" : "Abbrechen",
+ "Remove version name" : "Versionsnamen entfernen",
+ "Save version name" : "Versionsnamen speichern",
+ "Version name" : "Versionsname",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Benannte Versionen werden beibehalten und von automatischen Bereinigungen ausgeschlossen, wenn dein Speicherkontingent voll ist.",
+ "Initial version restored" : "Ursprüngliche Version wiederhergestellt",
+ "Version restored" : "Version wiederhergestellt",
+ "Could not restore version" : "Version konnte nicht wiederhergestellt werden",
+ "Could not set version label" : "Der Versionsname konnte nicht festgelegt werden",
+ "Could not delete version" : "Version konnte nicht gelöscht werden",
+ "File versions" : "Dateiversionen"
},"pluralForm" :"nplurals=2; plural=(n != 1);"
} \ No newline at end of file
diff --git a/apps/files_versions/l10n/de_DE.js b/apps/files_versions/l10n/de_DE.js
index 285e42517b3..7b290d51b25 100644
--- a/apps/files_versions/l10n/de_DE.js
+++ b/apps/files_versions/l10n/de_DE.js
@@ -1,11 +1,29 @@
OC.L10N.register(
"files_versions",
{
- "Could not revert: %s" : "Konnte %s nicht zurücksetzen",
"Versions" : "Versionen",
- "Failed to revert {file} to revision {timestamp}." : "Konnte {file} der Revision {timestamp} nicht rückgängig machen.",
- "Restore" : "Wiederherstellen",
- "No other versions available" : "Keine anderen Versionen verfügbar",
- "More versions..." : "Weitere Versionen…"
+ "This application automatically maintains older versions of files that are changed." : "Diese App verwaltet automatisch ältere Versionen geänderter Dateien.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Diese Anwendung verwaltet automatisch ältere Versionen von Dateien, die geändert wurden. Wenn aktiviert, wird ein Ordner mit versteckten Versionen im Verzeichnis jedes Benutzers bereitgestellt und wird zum Speichern alter Dateiversionen verwendet. Ein Benutzer kann jederzeit über die Web-Oberfläche auf eine ältere Version zurückgreifen, wobei die ersetzte Datei dann eine Version wird. Die App verwaltet automatisch den Versionsordner, um sicherzustellen, dass dem Konto nicht der Speicherplatz aufgrund von zu vielen Versionen ausgeht.\nZusätzlich zum Ablauf der Versionen stellt die Versions-App sicher, dass nie mehr als 50% des derzeit verfügbaren freien Speicherplatzes des Kontos für die Versionierung genutzt werden. Wenn gespeicherte Versionen diese Grenze überschreiten, löscht die App zuerst die ältesten Versionen, bis sie die 50% Grenze erreicht hat. Weitere Informationen finden Sie in der Versionsdokumentation.",
+ "Current version" : "Aktuelle Version",
+ "Initial version" : "Initiale Version",
+ "You" : "Sie",
+ "Actions for version from {versionHumanExplicitDate}" : "Aktionen für Version von {versionHumanExplicitDate}",
+ "Name this version" : "Diese Version benennen",
+ "Edit version name" : "Versionsnamen bearbeiten",
+ "Compare to current version" : "Mit aktueller Version vergleichen",
+ "Restore version" : "Version wiederherstellen",
+ "Download version" : "Version herunterladen",
+ "Delete version" : "Version löschen",
+ "Cancel" : "Abbrechen",
+ "Remove version name" : "Versionsnamen entfernen",
+ "Save version name" : "Versionsnamen speichern",
+ "Version name" : "Versionsname",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Benannte Versionen werden beibehalten und von automatischen Bereinigungen ausgeschlossen, wenn Ihr Speicherkontingent voll ist.",
+ "Initial version restored" : "Ursprüngliche Version wiederhergestellt",
+ "Version restored" : "Version wiederhergestellt",
+ "Could not restore version" : "Version konnte nicht wiederhergestellt werden",
+ "Could not set version label" : "Der Versionsname konnte nicht festgelegt werden",
+ "Could not delete version" : "Version konnte nicht gelöscht werden",
+ "File versions" : "Dateiversionen"
},
"nplurals=2; plural=(n != 1);");
diff --git a/apps/files_versions/l10n/de_DE.json b/apps/files_versions/l10n/de_DE.json
index a1f3c12649f..e08cc07dfc4 100644
--- a/apps/files_versions/l10n/de_DE.json
+++ b/apps/files_versions/l10n/de_DE.json
@@ -1,9 +1,27 @@
{ "translations": {
- "Could not revert: %s" : "Konnte %s nicht zurücksetzen",
"Versions" : "Versionen",
- "Failed to revert {file} to revision {timestamp}." : "Konnte {file} der Revision {timestamp} nicht rückgängig machen.",
- "Restore" : "Wiederherstellen",
- "No other versions available" : "Keine anderen Versionen verfügbar",
- "More versions..." : "Weitere Versionen…"
+ "This application automatically maintains older versions of files that are changed." : "Diese App verwaltet automatisch ältere Versionen geänderter Dateien.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Diese Anwendung verwaltet automatisch ältere Versionen von Dateien, die geändert wurden. Wenn aktiviert, wird ein Ordner mit versteckten Versionen im Verzeichnis jedes Benutzers bereitgestellt und wird zum Speichern alter Dateiversionen verwendet. Ein Benutzer kann jederzeit über die Web-Oberfläche auf eine ältere Version zurückgreifen, wobei die ersetzte Datei dann eine Version wird. Die App verwaltet automatisch den Versionsordner, um sicherzustellen, dass dem Konto nicht der Speicherplatz aufgrund von zu vielen Versionen ausgeht.\nZusätzlich zum Ablauf der Versionen stellt die Versions-App sicher, dass nie mehr als 50% des derzeit verfügbaren freien Speicherplatzes des Kontos für die Versionierung genutzt werden. Wenn gespeicherte Versionen diese Grenze überschreiten, löscht die App zuerst die ältesten Versionen, bis sie die 50% Grenze erreicht hat. Weitere Informationen finden Sie in der Versionsdokumentation.",
+ "Current version" : "Aktuelle Version",
+ "Initial version" : "Initiale Version",
+ "You" : "Sie",
+ "Actions for version from {versionHumanExplicitDate}" : "Aktionen für Version von {versionHumanExplicitDate}",
+ "Name this version" : "Diese Version benennen",
+ "Edit version name" : "Versionsnamen bearbeiten",
+ "Compare to current version" : "Mit aktueller Version vergleichen",
+ "Restore version" : "Version wiederherstellen",
+ "Download version" : "Version herunterladen",
+ "Delete version" : "Version löschen",
+ "Cancel" : "Abbrechen",
+ "Remove version name" : "Versionsnamen entfernen",
+ "Save version name" : "Versionsnamen speichern",
+ "Version name" : "Versionsname",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Benannte Versionen werden beibehalten und von automatischen Bereinigungen ausgeschlossen, wenn Ihr Speicherkontingent voll ist.",
+ "Initial version restored" : "Ursprüngliche Version wiederhergestellt",
+ "Version restored" : "Version wiederhergestellt",
+ "Could not restore version" : "Version konnte nicht wiederhergestellt werden",
+ "Could not set version label" : "Der Versionsname konnte nicht festgelegt werden",
+ "Could not delete version" : "Version konnte nicht gelöscht werden",
+ "File versions" : "Dateiversionen"
},"pluralForm" :"nplurals=2; plural=(n != 1);"
} \ No newline at end of file
diff --git a/apps/files_versions/l10n/el.js b/apps/files_versions/l10n/el.js
index ecfc322628e..fbe5807376b 100644
--- a/apps/files_versions/l10n/el.js
+++ b/apps/files_versions/l10n/el.js
@@ -1,11 +1,24 @@
OC.L10N.register(
"files_versions",
{
- "Could not revert: %s" : "Αδυναμία επαναφοράς: %s",
"Versions" : "Εκδόσεις",
- "Failed to revert {file} to revision {timestamp}." : "Αποτυχία επαναφοράς του {file} στην αναθεώρηση {timestamp}.",
- "Restore" : "Επαναφορά",
- "No other versions available" : "Δεν υπάρχουν άλλες εκδόσεις διαθέσιμες",
- "More versions..." : "Περισσότερες εκδόσεις..."
+ "This application automatically maintains older versions of files that are changed." : "Η εφαρμογή διατηρεί αυτόματα παλαιότερες εκδόσεις των αρχείων που έχουν αλλάξει.",
+ "Current version" : "Τρέχουσα έκδοση",
+ "Initial version" : "Αρχική έκδοση",
+ "You" : "Εσύ",
+ "Name this version" : "Ονομασία αυτής της έκδοσης",
+ "Edit version name" : "Επεξεργασία ονόματος έκδοσης",
+ "Compare to current version" : "Σύγκριση με την τρέχουσα έκδοση",
+ "Restore version" : "Επαναφορά έκδοσης",
+ "Download version" : "Λήψη έκδοσης",
+ "Delete version" : "Διαγραφή έκδοσης",
+ "Cancel" : "Ακύρωση",
+ "Remove version name" : "Αφαίρεση ονόματος έκδοσης",
+ "Save version name" : "Αποθήκευση ονόματος έκδοσης",
+ "Version name" : "Όνομα έκδοσης",
+ "Initial version restored" : "Η αρχική έκδοση επαναφέρθηκε",
+ "Version restored" : "Η έκδοση επαναφέρθηκε",
+ "Could not restore version" : "Δεν ήταν δυνατή η επαναφορά της έκδοσης",
+ "Could not delete version" : "Δεν ήταν δυνατή η διαγραφή της έκδοσης"
},
"nplurals=2; plural=(n != 1);");
diff --git a/apps/files_versions/l10n/el.json b/apps/files_versions/l10n/el.json
index 88b2fc1b3f0..490b5b52899 100644
--- a/apps/files_versions/l10n/el.json
+++ b/apps/files_versions/l10n/el.json
@@ -1,9 +1,22 @@
{ "translations": {
- "Could not revert: %s" : "Αδυναμία επαναφοράς: %s",
"Versions" : "Εκδόσεις",
- "Failed to revert {file} to revision {timestamp}." : "Αποτυχία επαναφοράς του {file} στην αναθεώρηση {timestamp}.",
- "Restore" : "Επαναφορά",
- "No other versions available" : "Δεν υπάρχουν άλλες εκδόσεις διαθέσιμες",
- "More versions..." : "Περισσότερες εκδόσεις..."
+ "This application automatically maintains older versions of files that are changed." : "Η εφαρμογή διατηρεί αυτόματα παλαιότερες εκδόσεις των αρχείων που έχουν αλλάξει.",
+ "Current version" : "Τρέχουσα έκδοση",
+ "Initial version" : "Αρχική έκδοση",
+ "You" : "Εσύ",
+ "Name this version" : "Ονομασία αυτής της έκδοσης",
+ "Edit version name" : "Επεξεργασία ονόματος έκδοσης",
+ "Compare to current version" : "Σύγκριση με την τρέχουσα έκδοση",
+ "Restore version" : "Επαναφορά έκδοσης",
+ "Download version" : "Λήψη έκδοσης",
+ "Delete version" : "Διαγραφή έκδοσης",
+ "Cancel" : "Ακύρωση",
+ "Remove version name" : "Αφαίρεση ονόματος έκδοσης",
+ "Save version name" : "Αποθήκευση ονόματος έκδοσης",
+ "Version name" : "Όνομα έκδοσης",
+ "Initial version restored" : "Η αρχική έκδοση επαναφέρθηκε",
+ "Version restored" : "Η έκδοση επαναφέρθηκε",
+ "Could not restore version" : "Δεν ήταν δυνατή η επαναφορά της έκδοσης",
+ "Could not delete version" : "Δεν ήταν δυνατή η διαγραφή της έκδοσης"
},"pluralForm" :"nplurals=2; plural=(n != 1);"
} \ No newline at end of file
diff --git a/apps/files_versions/l10n/en_GB.js b/apps/files_versions/l10n/en_GB.js
index 837b745b5fb..4b5161e3889 100644
--- a/apps/files_versions/l10n/en_GB.js
+++ b/apps/files_versions/l10n/en_GB.js
@@ -1,11 +1,29 @@
OC.L10N.register(
"files_versions",
{
- "Could not revert: %s" : "Could not revert: %s",
"Versions" : "Versions",
- "Failed to revert {file} to revision {timestamp}." : "Failed to revert {file} to revision {timestamp}.",
- "Restore" : "Restore",
- "No other versions available" : "No other versions available",
- "More versions..." : "More versions..."
+ "This application automatically maintains older versions of files that are changed." : "This application automatically maintains older versions of files that are changed.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation.",
+ "Current version" : "Current version",
+ "Initial version" : "Initial version",
+ "You" : "You",
+ "Actions for version from {versionHumanExplicitDate}" : "Actions for version from {versionHumanExplicitDate}",
+ "Name this version" : "Name this version",
+ "Edit version name" : "Edit version name",
+ "Compare to current version" : "Compare to current version",
+ "Restore version" : "Restore version",
+ "Download version" : "Download version",
+ "Delete version" : "Delete version",
+ "Cancel" : "Cancel",
+ "Remove version name" : "Remove version name",
+ "Save version name" : "Save version name",
+ "Version name" : "Version name",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full.",
+ "Initial version restored" : "Initial version restored",
+ "Version restored" : "Version restored",
+ "Could not restore version" : "Could not restore version",
+ "Could not set version label" : "Could not set version label",
+ "Could not delete version" : "Could not delete version",
+ "File versions" : "File versions"
},
"nplurals=2; plural=(n != 1);");
diff --git a/apps/files_versions/l10n/en_GB.json b/apps/files_versions/l10n/en_GB.json
index 84e55063f67..6daff63aade 100644
--- a/apps/files_versions/l10n/en_GB.json
+++ b/apps/files_versions/l10n/en_GB.json
@@ -1,9 +1,27 @@
{ "translations": {
- "Could not revert: %s" : "Could not revert: %s",
"Versions" : "Versions",
- "Failed to revert {file} to revision {timestamp}." : "Failed to revert {file} to revision {timestamp}.",
- "Restore" : "Restore",
- "No other versions available" : "No other versions available",
- "More versions..." : "More versions..."
+ "This application automatically maintains older versions of files that are changed." : "This application automatically maintains older versions of files that are changed.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation.",
+ "Current version" : "Current version",
+ "Initial version" : "Initial version",
+ "You" : "You",
+ "Actions for version from {versionHumanExplicitDate}" : "Actions for version from {versionHumanExplicitDate}",
+ "Name this version" : "Name this version",
+ "Edit version name" : "Edit version name",
+ "Compare to current version" : "Compare to current version",
+ "Restore version" : "Restore version",
+ "Download version" : "Download version",
+ "Delete version" : "Delete version",
+ "Cancel" : "Cancel",
+ "Remove version name" : "Remove version name",
+ "Save version name" : "Save version name",
+ "Version name" : "Version name",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full.",
+ "Initial version restored" : "Initial version restored",
+ "Version restored" : "Version restored",
+ "Could not restore version" : "Could not restore version",
+ "Could not set version label" : "Could not set version label",
+ "Could not delete version" : "Could not delete version",
+ "File versions" : "File versions"
},"pluralForm" :"nplurals=2; plural=(n != 1);"
} \ No newline at end of file
diff --git a/apps/files_versions/l10n/eo.js b/apps/files_versions/l10n/eo.js
deleted file mode 100644
index 665f69bbbda..00000000000
--- a/apps/files_versions/l10n/eo.js
+++ /dev/null
@@ -1,11 +0,0 @@
-OC.L10N.register(
- "files_versions",
- {
- "Could not revert: %s" : "Ne eblas malfari: %s",
- "Versions" : "Versioj",
- "Failed to revert {file} to revision {timestamp}." : "Malsukcesis returnigo de {file} al la revizio {timestamp}.",
- "Restore" : "Restaŭri",
- "No other versions available" : "Ne disponeblas aliaj versioj",
- "More versions..." : "Pli da versioj..."
-},
-"nplurals=2; plural=(n != 1);");
diff --git a/apps/files_versions/l10n/eo.json b/apps/files_versions/l10n/eo.json
deleted file mode 100644
index db72c173bfc..00000000000
--- a/apps/files_versions/l10n/eo.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{ "translations": {
- "Could not revert: %s" : "Ne eblas malfari: %s",
- "Versions" : "Versioj",
- "Failed to revert {file} to revision {timestamp}." : "Malsukcesis returnigo de {file} al la revizio {timestamp}.",
- "Restore" : "Restaŭri",
- "No other versions available" : "Ne disponeblas aliaj versioj",
- "More versions..." : "Pli da versioj..."
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
-} \ No newline at end of file
diff --git a/apps/files_versions/l10n/es.js b/apps/files_versions/l10n/es.js
index 54662b493da..0daaa820510 100644
--- a/apps/files_versions/l10n/es.js
+++ b/apps/files_versions/l10n/es.js
@@ -1,11 +1,29 @@
OC.L10N.register(
"files_versions",
{
- "Could not revert: %s" : "No se puede revertir: %s",
- "Versions" : "Revisiones",
- "Failed to revert {file} to revision {timestamp}." : "No se ha podido revertir {archivo} a revisión {timestamp}.",
- "Restore" : "Recuperar",
- "No other versions available" : "No hay otras versiones disponibles",
- "More versions..." : "Más versiones..."
+ "Versions" : "Versiones",
+ "This application automatically maintains older versions of files that are changed." : "Esta aplicación mantiene automáticamente versiones antiguas de los archivos que cambian.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Esta aplicación mantiene automáticamente versiones antiguas de los archivos que cambian. Cuando se activa, una carpeta escondida de versiones aparece en cada directorio del usuario y se usa para almacenar los archivos de versiones antiguas. Un usuario puede volver a una versión más antigua a través de la interfaz web en cualquier momento, convirtiéndose el archivo reemplazado en una versión. La app controla automáticamente la carpeta de versiones para asegurarse de que la cuenta no se queda sin cuota de disco por las versiones.\n\t\tAdemás de administrar la caducidad de las versiones, la app de versiones se asegura de no usar nunca más del 50% del espacio libre actualmente disponible para el usuario. Si las versiones almacenadas exceden este límite, la app borrará primero las versiones más viejas hasta alcanzar este límite. Más información disponible en la documentación de Versiones.",
+ "Current version" : "Versión actual",
+ "Initial version" : "Versión inicial",
+ "You" : "Usted",
+ "Actions for version from {versionHumanExplicitDate}" : "Acciones para la versión de {versionHumanExplicitDate}",
+ "Name this version" : "Nombrar esta versión",
+ "Edit version name" : "Editar nombre de versión",
+ "Compare to current version" : "Comparar con la versión actual",
+ "Restore version" : "Restaurar versión",
+ "Download version" : "Descargar versión",
+ "Delete version" : "Eliminar versión",
+ "Cancel" : "Cancelar",
+ "Remove version name" : "Quitar nombre de versión",
+ "Save version name" : "Guardar nombre de versión",
+ "Version name" : "Nombre de versión",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Las versiones nombradas son persistidas, y, son excluidas de las limpiezas automáticas cuando su cuota de almacenamiento está al límite.",
+ "Initial version restored" : "Versión inicial restaurada",
+ "Version restored" : "Versión restaurada",
+ "Could not restore version" : "No se ha podido restaurar la versión",
+ "Could not set version label" : "No se pudo establecer la etiqueta de la versión",
+ "Could not delete version" : "No se ha podido eliminar la versión",
+ "File versions" : "Versiones de archivo"
},
-"nplurals=2; plural=(n != 1);");
+"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;");
diff --git a/apps/files_versions/l10n/es.json b/apps/files_versions/l10n/es.json
index 692f890a880..28ab4a7611c 100644
--- a/apps/files_versions/l10n/es.json
+++ b/apps/files_versions/l10n/es.json
@@ -1,9 +1,27 @@
{ "translations": {
- "Could not revert: %s" : "No se puede revertir: %s",
- "Versions" : "Revisiones",
- "Failed to revert {file} to revision {timestamp}." : "No se ha podido revertir {archivo} a revisión {timestamp}.",
- "Restore" : "Recuperar",
- "No other versions available" : "No hay otras versiones disponibles",
- "More versions..." : "Más versiones..."
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
+ "Versions" : "Versiones",
+ "This application automatically maintains older versions of files that are changed." : "Esta aplicación mantiene automáticamente versiones antiguas de los archivos que cambian.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Esta aplicación mantiene automáticamente versiones antiguas de los archivos que cambian. Cuando se activa, una carpeta escondida de versiones aparece en cada directorio del usuario y se usa para almacenar los archivos de versiones antiguas. Un usuario puede volver a una versión más antigua a través de la interfaz web en cualquier momento, convirtiéndose el archivo reemplazado en una versión. La app controla automáticamente la carpeta de versiones para asegurarse de que la cuenta no se queda sin cuota de disco por las versiones.\n\t\tAdemás de administrar la caducidad de las versiones, la app de versiones se asegura de no usar nunca más del 50% del espacio libre actualmente disponible para el usuario. Si las versiones almacenadas exceden este límite, la app borrará primero las versiones más viejas hasta alcanzar este límite. Más información disponible en la documentación de Versiones.",
+ "Current version" : "Versión actual",
+ "Initial version" : "Versión inicial",
+ "You" : "Usted",
+ "Actions for version from {versionHumanExplicitDate}" : "Acciones para la versión de {versionHumanExplicitDate}",
+ "Name this version" : "Nombrar esta versión",
+ "Edit version name" : "Editar nombre de versión",
+ "Compare to current version" : "Comparar con la versión actual",
+ "Restore version" : "Restaurar versión",
+ "Download version" : "Descargar versión",
+ "Delete version" : "Eliminar versión",
+ "Cancel" : "Cancelar",
+ "Remove version name" : "Quitar nombre de versión",
+ "Save version name" : "Guardar nombre de versión",
+ "Version name" : "Nombre de versión",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Las versiones nombradas son persistidas, y, son excluidas de las limpiezas automáticas cuando su cuota de almacenamiento está al límite.",
+ "Initial version restored" : "Versión inicial restaurada",
+ "Version restored" : "Versión restaurada",
+ "Could not restore version" : "No se ha podido restaurar la versión",
+ "Could not set version label" : "No se pudo establecer la etiqueta de la versión",
+ "Could not delete version" : "No se ha podido eliminar la versión",
+ "File versions" : "Versiones de archivo"
+},"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"
} \ No newline at end of file
diff --git a/apps/files_versions/l10n/es_AR.js b/apps/files_versions/l10n/es_AR.js
deleted file mode 100644
index 49febb3dc6c..00000000000
--- a/apps/files_versions/l10n/es_AR.js
+++ /dev/null
@@ -1,11 +0,0 @@
-OC.L10N.register(
- "files_versions",
- {
- "Could not revert: %s" : "No se pudo revertir: %s ",
- "Versions" : "Versiones",
- "Failed to revert {file} to revision {timestamp}." : "Falló al revertir {file} a la revisión {timestamp}.",
- "Restore" : "Recuperar",
- "No other versions available" : "No hay más versiones disponibles",
- "More versions..." : "Más versiones..."
-},
-"nplurals=2; plural=(n != 1);");
diff --git a/apps/files_versions/l10n/es_AR.json b/apps/files_versions/l10n/es_AR.json
deleted file mode 100644
index 2177de87cb5..00000000000
--- a/apps/files_versions/l10n/es_AR.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{ "translations": {
- "Could not revert: %s" : "No se pudo revertir: %s ",
- "Versions" : "Versiones",
- "Failed to revert {file} to revision {timestamp}." : "Falló al revertir {file} a la revisión {timestamp}.",
- "Restore" : "Recuperar",
- "No other versions available" : "No hay más versiones disponibles",
- "More versions..." : "Más versiones..."
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
-} \ No newline at end of file
diff --git a/apps/files_versions/l10n/es_EC.js b/apps/files_versions/l10n/es_EC.js
new file mode 100644
index 00000000000..bcca68dd065
--- /dev/null
+++ b/apps/files_versions/l10n/es_EC.js
@@ -0,0 +1,22 @@
+OC.L10N.register(
+ "files_versions",
+ {
+ "Versions" : "Versiones",
+ "This application automatically maintains older versions of files that are changed." : "Esta aplicación mantiene automáticamente versiones anteriores de los archivos que son modificados.",
+ "Current version" : "Versión actual",
+ "Name this version" : "Nombrar esta versión",
+ "Edit version name" : "Editar nombre de versión",
+ "Restore version" : "Restaurar versión",
+ "Download version" : "Descargar versión",
+ "Delete version" : "Eliminar versión",
+ "Cancel" : "Cancelar",
+ "Remove version name" : "Eliminar nombre de versión",
+ "Save version name" : "Guardar nombre de versión",
+ "Version name" : "Nombre de la versión",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Las versiones con nombre se conservan y se excluyen de las limpiezas automáticas cuando tu cuota de almacenamiento está llena.",
+ "Initial version restored" : "Versión inicial restaurada",
+ "Version restored" : "Versión restaurada",
+ "Could not restore version" : "No se pudo restaurar la versión",
+ "Could not delete version" : "No se pudo eliminar la versión"
+},
+"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;");
diff --git a/apps/files_versions/l10n/es_EC.json b/apps/files_versions/l10n/es_EC.json
new file mode 100644
index 00000000000..46097bb89d6
--- /dev/null
+++ b/apps/files_versions/l10n/es_EC.json
@@ -0,0 +1,20 @@
+{ "translations": {
+ "Versions" : "Versiones",
+ "This application automatically maintains older versions of files that are changed." : "Esta aplicación mantiene automáticamente versiones anteriores de los archivos que son modificados.",
+ "Current version" : "Versión actual",
+ "Name this version" : "Nombrar esta versión",
+ "Edit version name" : "Editar nombre de versión",
+ "Restore version" : "Restaurar versión",
+ "Download version" : "Descargar versión",
+ "Delete version" : "Eliminar versión",
+ "Cancel" : "Cancelar",
+ "Remove version name" : "Eliminar nombre de versión",
+ "Save version name" : "Guardar nombre de versión",
+ "Version name" : "Nombre de la versión",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Las versiones con nombre se conservan y se excluyen de las limpiezas automáticas cuando tu cuota de almacenamiento está llena.",
+ "Initial version restored" : "Versión inicial restaurada",
+ "Version restored" : "Versión restaurada",
+ "Could not restore version" : "No se pudo restaurar la versión",
+ "Could not delete version" : "No se pudo eliminar la versión"
+},"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"
+} \ No newline at end of file
diff --git a/apps/files_versions/l10n/es_MX.js b/apps/files_versions/l10n/es_MX.js
index 54662b493da..67683cc4967 100644
--- a/apps/files_versions/l10n/es_MX.js
+++ b/apps/files_versions/l10n/es_MX.js
@@ -1,11 +1,26 @@
OC.L10N.register(
"files_versions",
{
- "Could not revert: %s" : "No se puede revertir: %s",
- "Versions" : "Revisiones",
- "Failed to revert {file} to revision {timestamp}." : "No se ha podido revertir {archivo} a revisión {timestamp}.",
- "Restore" : "Recuperar",
- "No other versions available" : "No hay otras versiones disponibles",
- "More versions..." : "Más versiones..."
+ "Versions" : "Versiones",
+ "This application automatically maintains older versions of files that are changed." : "Esta aplicación mantiene automáticamente versiones anteriores de archivos que fueron cambiados. ",
+ "Current version" : "Versión actual",
+ "Initial version" : "Versión inicial",
+ "You" : "Usted",
+ "Name this version" : "Nombrar esta versión",
+ "Edit version name" : "Editar el nombre de la versión",
+ "Compare to current version" : "Comparar con la versión actual",
+ "Restore version" : "Restaurar versión",
+ "Download version" : "Descargar versión",
+ "Delete version" : "Eliminar versión",
+ "Cancel" : "Cancelar",
+ "Remove version name" : "Eliminar el nombre de la versión",
+ "Save version name" : "Guardar el nombre de la versión",
+ "Version name" : "Nombre de la versión",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Las versiones con nombre se conservan y se excluyen de las limpiezas automáticas cuando su cuota de almacenamiento alcanzó el límite.",
+ "Initial version restored" : "Versión inicial restaurada",
+ "Version restored" : "Versión restaurada",
+ "Could not restore version" : "No se pudo restaurar la versión",
+ "Could not set version label" : "No se pudo establecer la etiqueta de la versión",
+ "Could not delete version" : "No se pudo eliminar la versión"
},
-"nplurals=2; plural=(n != 1);");
+"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;");
diff --git a/apps/files_versions/l10n/es_MX.json b/apps/files_versions/l10n/es_MX.json
index 692f890a880..403db393aaf 100644
--- a/apps/files_versions/l10n/es_MX.json
+++ b/apps/files_versions/l10n/es_MX.json
@@ -1,9 +1,24 @@
{ "translations": {
- "Could not revert: %s" : "No se puede revertir: %s",
- "Versions" : "Revisiones",
- "Failed to revert {file} to revision {timestamp}." : "No se ha podido revertir {archivo} a revisión {timestamp}.",
- "Restore" : "Recuperar",
- "No other versions available" : "No hay otras versiones disponibles",
- "More versions..." : "Más versiones..."
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
+ "Versions" : "Versiones",
+ "This application automatically maintains older versions of files that are changed." : "Esta aplicación mantiene automáticamente versiones anteriores de archivos que fueron cambiados. ",
+ "Current version" : "Versión actual",
+ "Initial version" : "Versión inicial",
+ "You" : "Usted",
+ "Name this version" : "Nombrar esta versión",
+ "Edit version name" : "Editar el nombre de la versión",
+ "Compare to current version" : "Comparar con la versión actual",
+ "Restore version" : "Restaurar versión",
+ "Download version" : "Descargar versión",
+ "Delete version" : "Eliminar versión",
+ "Cancel" : "Cancelar",
+ "Remove version name" : "Eliminar el nombre de la versión",
+ "Save version name" : "Guardar el nombre de la versión",
+ "Version name" : "Nombre de la versión",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Las versiones con nombre se conservan y se excluyen de las limpiezas automáticas cuando su cuota de almacenamiento alcanzó el límite.",
+ "Initial version restored" : "Versión inicial restaurada",
+ "Version restored" : "Versión restaurada",
+ "Could not restore version" : "No se pudo restaurar la versión",
+ "Could not set version label" : "No se pudo establecer la etiqueta de la versión",
+ "Could not delete version" : "No se pudo eliminar la versión"
+},"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"
} \ No newline at end of file
diff --git a/apps/files_versions/l10n/et_EE.js b/apps/files_versions/l10n/et_EE.js
index fe0616deeec..dadd03d14dc 100644
--- a/apps/files_versions/l10n/et_EE.js
+++ b/apps/files_versions/l10n/et_EE.js
@@ -1,11 +1,29 @@
OC.L10N.register(
"files_versions",
{
- "Could not revert: %s" : "Ei suuda taastada faili: %s",
"Versions" : "Versioonid",
- "Failed to revert {file} to revision {timestamp}." : "Ebaõnnestus faili {file} taastamine revisjonile {timestamp}",
- "Restore" : "Taasta",
- "No other versions available" : "Muid versioone pole saadaval",
- "More versions..." : "Rohkem versioone..."
+ "This application automatically maintains older versions of files that are changed." : "See rakendus haldab automaatselt muudetud failide vanemaid versioone.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "See rakendus säilitab automaatselt muudetud failide vanemaid versioone. Kui see on lubatud, luuakse iga kasutaja kataloogis peidetud versioonide kaust ja seda kasutatakse vanade failiversioonide salvestamiseks. Kasutaja saab veebiliidese kaudu igal ajal naasta vanemale versioonile, kusjuures asendatud failist saab versioon. Rakendus haldab automaatselt versioonide kausta, et kontol kvoot versioonide tõttu otsa ei saaks.\nLisaks versioonide aegumisele tagab versioonirakendus, et ei kasutataks kunagi rohkem kui 50% konto hetkel saadaolevast vabast ruumist. Kui salvestatud versioonid ületavad selle limiidi, kustutab rakendus kõigepealt vanimad versioonid, kuni see limiit jõuab. Lisateavet leiate versioonide dokumentatsioonist.",
+ "Current version" : "Hetkeversioon",
+ "Initial version" : "Algversioon",
+ "You" : "Sina",
+ "Actions for version from {versionHumanExplicitDate}" : "Tegevused versiooniga {versionHumanExplicitDate}-st",
+ "Name this version" : "Nimeta see versioon",
+ "Edit version name" : "Muuda versiooni nime",
+ "Compare to current version" : "Võrdle hetkeversiooniga",
+ "Restore version" : "Taasta versioon",
+ "Download version" : "Laadi versioon alla",
+ "Delete version" : "Kustuta versioon",
+ "Cancel" : "Tühista",
+ "Remove version name" : "Eemalda versiooni nimi",
+ "Save version name" : "Salvesta versiooni nimi",
+ "Version name" : "Versiooni nimi",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Nimelised versioonid hoitakse alles ja ei kustutata automaatselt, kui salvestusruum on täis.",
+ "Initial version restored" : "Algne versioon taastatud",
+ "Version restored" : "Versioon taastatud",
+ "Could not restore version" : "Versiooni taastamine ebaõnnestus",
+ "Could not set version label" : "Ei saanud määrata versiooni märget",
+ "Could not delete version" : "Versiooni kustutamine ebaõnnestus",
+ "File versions" : "Faili versioonid"
},
"nplurals=2; plural=(n != 1);");
diff --git a/apps/files_versions/l10n/et_EE.json b/apps/files_versions/l10n/et_EE.json
index 25f0b73c579..d2b4f6170f7 100644
--- a/apps/files_versions/l10n/et_EE.json
+++ b/apps/files_versions/l10n/et_EE.json
@@ -1,9 +1,27 @@
{ "translations": {
- "Could not revert: %s" : "Ei suuda taastada faili: %s",
"Versions" : "Versioonid",
- "Failed to revert {file} to revision {timestamp}." : "Ebaõnnestus faili {file} taastamine revisjonile {timestamp}",
- "Restore" : "Taasta",
- "No other versions available" : "Muid versioone pole saadaval",
- "More versions..." : "Rohkem versioone..."
+ "This application automatically maintains older versions of files that are changed." : "See rakendus haldab automaatselt muudetud failide vanemaid versioone.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "See rakendus säilitab automaatselt muudetud failide vanemaid versioone. Kui see on lubatud, luuakse iga kasutaja kataloogis peidetud versioonide kaust ja seda kasutatakse vanade failiversioonide salvestamiseks. Kasutaja saab veebiliidese kaudu igal ajal naasta vanemale versioonile, kusjuures asendatud failist saab versioon. Rakendus haldab automaatselt versioonide kausta, et kontol kvoot versioonide tõttu otsa ei saaks.\nLisaks versioonide aegumisele tagab versioonirakendus, et ei kasutataks kunagi rohkem kui 50% konto hetkel saadaolevast vabast ruumist. Kui salvestatud versioonid ületavad selle limiidi, kustutab rakendus kõigepealt vanimad versioonid, kuni see limiit jõuab. Lisateavet leiate versioonide dokumentatsioonist.",
+ "Current version" : "Hetkeversioon",
+ "Initial version" : "Algversioon",
+ "You" : "Sina",
+ "Actions for version from {versionHumanExplicitDate}" : "Tegevused versiooniga {versionHumanExplicitDate}-st",
+ "Name this version" : "Nimeta see versioon",
+ "Edit version name" : "Muuda versiooni nime",
+ "Compare to current version" : "Võrdle hetkeversiooniga",
+ "Restore version" : "Taasta versioon",
+ "Download version" : "Laadi versioon alla",
+ "Delete version" : "Kustuta versioon",
+ "Cancel" : "Tühista",
+ "Remove version name" : "Eemalda versiooni nimi",
+ "Save version name" : "Salvesta versiooni nimi",
+ "Version name" : "Versiooni nimi",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Nimelised versioonid hoitakse alles ja ei kustutata automaatselt, kui salvestusruum on täis.",
+ "Initial version restored" : "Algne versioon taastatud",
+ "Version restored" : "Versioon taastatud",
+ "Could not restore version" : "Versiooni taastamine ebaõnnestus",
+ "Could not set version label" : "Ei saanud määrata versiooni märget",
+ "Could not delete version" : "Versiooni kustutamine ebaõnnestus",
+ "File versions" : "Faili versioonid"
},"pluralForm" :"nplurals=2; plural=(n != 1);"
} \ No newline at end of file
diff --git a/apps/files_versions/l10n/eu.js b/apps/files_versions/l10n/eu.js
index 35d1861de2c..76e0383e4c8 100644
--- a/apps/files_versions/l10n/eu.js
+++ b/apps/files_versions/l10n/eu.js
@@ -1,11 +1,28 @@
OC.L10N.register(
"files_versions",
{
- "Could not revert: %s" : "Ezin izan da leheneratu: %s",
"Versions" : "Bertsioak",
- "Failed to revert {file} to revision {timestamp}." : "Errore bat izan da {fitxategia} {timestamp} bertsiora leheneratzean.",
- "Restore" : "Berrezarri",
- "No other versions available" : "Ez dago bertsio gehiago eskuragarri",
- "More versions..." : "Bertsio gehiago..."
+ "This application automatically maintains older versions of files that are changed." : "Aplikazio honek aldatzen diren fitxategien bertsio zaharrak mantentzen ditu automatikoki.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Aplikazio honek automatikoki mantentzen ditu aldatzen diren fitxategien bertsio zaharragoak. Gaituta dagoenean, ezkutuko bertsioen karpeta batez hornitzen da erabiltzaile bakoitzaren direktorioa, fitxategien bertsio zaharrak gordetzeko. Erabiltzaileak edozein unetan bertsio zaharrago batera leheneratu dezake web interfazearen bidez, ordeztutako fitxategia bertsio bihurtuz. Aplikazioak automatikoki kudeatzen du bertsioen karpeta, erabiltzailea bertsioak direla eta kuotarik gabe geratuko ez dela ziurtatzeko.\n\t\tBertsioak iraungitzeaz gain, bertsioen aplikazioak ziurtatzen du ez dela inoiz erabiliko kontuaren uneko espazio librearen %50 baino gehiago. Biltegiratutako bertsioek muga hori gainditzen badute, aplikazioak bertsio zaharrenak ezabatuko ditu, mugara jaitsi arte. Informazio gehiago dago eskuragarri bertsioen dokumentazioan.",
+ "Current version" : "Uneko bertsioa",
+ "Initial version" : "Hasierako bertsioa",
+ "You" : "Zu ",
+ "Actions for version from {versionHumanExplicitDate}" : "Bertsiorako ekintzak {versionHumanExplicitDate}(e)tik",
+ "Name this version" : "Eman izena bertsio honi",
+ "Edit version name" : "Editatu bertsioaren izena",
+ "Compare to current version" : "Konparatu uneko bertsioarekin",
+ "Restore version" : "Leheneratu bertsioa",
+ "Download version" : "Deskargatu bertsioa",
+ "Delete version" : "Ezabatu bertsioa",
+ "Cancel" : "Utzi",
+ "Remove version name" : "Kendu bertsioaren izena",
+ "Save version name" : "Gorde bertsioaren izena",
+ "Version name" : "Bertsioaren izena",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Izendatutako bertsioak mantentzen dira eta garbiketa automatikoetatik kanpo geratzen dira biltegiratze-kuota beteta dagoenean.",
+ "Initial version restored" : "Hasierako bertsioa ondo leheneratu da",
+ "Version restored" : "Bertsioa leheneratu da",
+ "Could not restore version" : "Ezin izan da bertsioa leheneratu",
+ "Could not set version label" : "Ezin izan da bertsio etiketa ezarri",
+ "Could not delete version" : "Ezin izan da bertsioa ezabatu"
},
"nplurals=2; plural=(n != 1);");
diff --git a/apps/files_versions/l10n/eu.json b/apps/files_versions/l10n/eu.json
index 3bd3c0dc7b7..59421f36b36 100644
--- a/apps/files_versions/l10n/eu.json
+++ b/apps/files_versions/l10n/eu.json
@@ -1,9 +1,26 @@
{ "translations": {
- "Could not revert: %s" : "Ezin izan da leheneratu: %s",
"Versions" : "Bertsioak",
- "Failed to revert {file} to revision {timestamp}." : "Errore bat izan da {fitxategia} {timestamp} bertsiora leheneratzean.",
- "Restore" : "Berrezarri",
- "No other versions available" : "Ez dago bertsio gehiago eskuragarri",
- "More versions..." : "Bertsio gehiago..."
+ "This application automatically maintains older versions of files that are changed." : "Aplikazio honek aldatzen diren fitxategien bertsio zaharrak mantentzen ditu automatikoki.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Aplikazio honek automatikoki mantentzen ditu aldatzen diren fitxategien bertsio zaharragoak. Gaituta dagoenean, ezkutuko bertsioen karpeta batez hornitzen da erabiltzaile bakoitzaren direktorioa, fitxategien bertsio zaharrak gordetzeko. Erabiltzaileak edozein unetan bertsio zaharrago batera leheneratu dezake web interfazearen bidez, ordeztutako fitxategia bertsio bihurtuz. Aplikazioak automatikoki kudeatzen du bertsioen karpeta, erabiltzailea bertsioak direla eta kuotarik gabe geratuko ez dela ziurtatzeko.\n\t\tBertsioak iraungitzeaz gain, bertsioen aplikazioak ziurtatzen du ez dela inoiz erabiliko kontuaren uneko espazio librearen %50 baino gehiago. Biltegiratutako bertsioek muga hori gainditzen badute, aplikazioak bertsio zaharrenak ezabatuko ditu, mugara jaitsi arte. Informazio gehiago dago eskuragarri bertsioen dokumentazioan.",
+ "Current version" : "Uneko bertsioa",
+ "Initial version" : "Hasierako bertsioa",
+ "You" : "Zu ",
+ "Actions for version from {versionHumanExplicitDate}" : "Bertsiorako ekintzak {versionHumanExplicitDate}(e)tik",
+ "Name this version" : "Eman izena bertsio honi",
+ "Edit version name" : "Editatu bertsioaren izena",
+ "Compare to current version" : "Konparatu uneko bertsioarekin",
+ "Restore version" : "Leheneratu bertsioa",
+ "Download version" : "Deskargatu bertsioa",
+ "Delete version" : "Ezabatu bertsioa",
+ "Cancel" : "Utzi",
+ "Remove version name" : "Kendu bertsioaren izena",
+ "Save version name" : "Gorde bertsioaren izena",
+ "Version name" : "Bertsioaren izena",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Izendatutako bertsioak mantentzen dira eta garbiketa automatikoetatik kanpo geratzen dira biltegiratze-kuota beteta dagoenean.",
+ "Initial version restored" : "Hasierako bertsioa ondo leheneratu da",
+ "Version restored" : "Bertsioa leheneratu da",
+ "Could not restore version" : "Ezin izan da bertsioa leheneratu",
+ "Could not set version label" : "Ezin izan da bertsio etiketa ezarri",
+ "Could not delete version" : "Ezin izan da bertsioa ezabatu"
},"pluralForm" :"nplurals=2; plural=(n != 1);"
} \ No newline at end of file
diff --git a/apps/files_versions/l10n/fa.js b/apps/files_versions/l10n/fa.js
index 33ef48a870a..6f50077efdc 100644
--- a/apps/files_versions/l10n/fa.js
+++ b/apps/files_versions/l10n/fa.js
@@ -1,11 +1,24 @@
OC.L10N.register(
"files_versions",
{
- "Could not revert: %s" : "بازگردانی امکان ناپذیر است: %s",
"Versions" : "نسخه ها",
- "Failed to revert {file} to revision {timestamp}." : "برگرداندن {file} به نسخه {timestamp} با شکست روبرو شد",
- "Restore" : "بازیابی",
- "No other versions available" : "نسخه ی دیگری در دسترس نیست",
- "More versions..." : "نسخه های بیشتر"
+ "This application automatically maintains older versions of files that are changed." : "این برنامه به طور خودکار نسخه های قدیمی تر پرونده های تغییر یافته را حفظ می کند.",
+ "Current version" : "نسخه فعلی",
+ "You" : "You",
+ "Name this version" : "Name this version",
+ "Edit version name" : "Edit version name",
+ "Compare to current version" : "Compare to current version",
+ "Restore version" : "Restore version",
+ "Download version" : "Download version",
+ "Delete version" : "Delete version",
+ "Cancel" : "منصرف شدن",
+ "Remove version name" : "Remove version name",
+ "Save version name" : "Save version name",
+ "Version name" : "Version name",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full.",
+ "Initial version restored" : "Initial version restored",
+ "Version restored" : "Version restored",
+ "Could not restore version" : "Could not restore version",
+ "Could not delete version" : "Could not delete version"
},
-"nplurals=1; plural=0;");
+"nplurals=2; plural=(n > 1);");
diff --git a/apps/files_versions/l10n/fa.json b/apps/files_versions/l10n/fa.json
index 3dbbde955a3..a96d8d4cd31 100644
--- a/apps/files_versions/l10n/fa.json
+++ b/apps/files_versions/l10n/fa.json
@@ -1,9 +1,22 @@
{ "translations": {
- "Could not revert: %s" : "بازگردانی امکان ناپذیر است: %s",
"Versions" : "نسخه ها",
- "Failed to revert {file} to revision {timestamp}." : "برگرداندن {file} به نسخه {timestamp} با شکست روبرو شد",
- "Restore" : "بازیابی",
- "No other versions available" : "نسخه ی دیگری در دسترس نیست",
- "More versions..." : "نسخه های بیشتر"
-},"pluralForm" :"nplurals=1; plural=0;"
+ "This application automatically maintains older versions of files that are changed." : "این برنامه به طور خودکار نسخه های قدیمی تر پرونده های تغییر یافته را حفظ می کند.",
+ "Current version" : "نسخه فعلی",
+ "You" : "You",
+ "Name this version" : "Name this version",
+ "Edit version name" : "Edit version name",
+ "Compare to current version" : "Compare to current version",
+ "Restore version" : "Restore version",
+ "Download version" : "Download version",
+ "Delete version" : "Delete version",
+ "Cancel" : "منصرف شدن",
+ "Remove version name" : "Remove version name",
+ "Save version name" : "Save version name",
+ "Version name" : "Version name",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full.",
+ "Initial version restored" : "Initial version restored",
+ "Version restored" : "Version restored",
+ "Could not restore version" : "Could not restore version",
+ "Could not delete version" : "Could not delete version"
+},"pluralForm" :"nplurals=2; plural=(n > 1);"
} \ No newline at end of file
diff --git a/apps/files_versions/l10n/fi.js b/apps/files_versions/l10n/fi.js
new file mode 100644
index 00000000000..62682858796
--- /dev/null
+++ b/apps/files_versions/l10n/fi.js
@@ -0,0 +1,25 @@
+OC.L10N.register(
+ "files_versions",
+ {
+ "Versions" : "Versiot",
+ "This application automatically maintains older versions of files that are changed." : "Tämä sovellus säilyttää automaattisesti vanhempia versioita muuttuneista tiedostoista.",
+ "Current version" : "Nykyinen versio",
+ "Initial version" : "Ensimmäinen versio",
+ "You" : "Sinä",
+ "Name this version" : "Nimeä tämä versio",
+ "Edit version name" : "Muokkaa version nimeä",
+ "Compare to current version" : "Vertaa nykyiseen versioon",
+ "Restore version" : "Palauta versio",
+ "Download version" : "Lataa versio",
+ "Delete version" : "Poista versio",
+ "Cancel" : "Peruuta",
+ "Remove version name" : "Poista version nimi",
+ "Save version name" : "Tallenna version nimi",
+ "Version name" : "Version nimi",
+ "Initial version restored" : "Ensimmäinen versio palautettu",
+ "Version restored" : "Versio palautettu",
+ "Could not restore version" : "Versiota ei voitu palauttaa",
+ "Could not delete version" : "Versiota ei voitu poistaa",
+ "File versions" : "Tiedoston versiot"
+},
+"nplurals=2; plural=(n != 1);");
diff --git a/apps/files_versions/l10n/fi.json b/apps/files_versions/l10n/fi.json
new file mode 100644
index 00000000000..492facfe430
--- /dev/null
+++ b/apps/files_versions/l10n/fi.json
@@ -0,0 +1,23 @@
+{ "translations": {
+ "Versions" : "Versiot",
+ "This application automatically maintains older versions of files that are changed." : "Tämä sovellus säilyttää automaattisesti vanhempia versioita muuttuneista tiedostoista.",
+ "Current version" : "Nykyinen versio",
+ "Initial version" : "Ensimmäinen versio",
+ "You" : "Sinä",
+ "Name this version" : "Nimeä tämä versio",
+ "Edit version name" : "Muokkaa version nimeä",
+ "Compare to current version" : "Vertaa nykyiseen versioon",
+ "Restore version" : "Palauta versio",
+ "Download version" : "Lataa versio",
+ "Delete version" : "Poista versio",
+ "Cancel" : "Peruuta",
+ "Remove version name" : "Poista version nimi",
+ "Save version name" : "Tallenna version nimi",
+ "Version name" : "Version nimi",
+ "Initial version restored" : "Ensimmäinen versio palautettu",
+ "Version restored" : "Versio palautettu",
+ "Could not restore version" : "Versiota ei voitu palauttaa",
+ "Could not delete version" : "Versiota ei voitu poistaa",
+ "File versions" : "Tiedoston versiot"
+},"pluralForm" :"nplurals=2; plural=(n != 1);"
+} \ No newline at end of file
diff --git a/apps/files_versions/l10n/fi_FI.js b/apps/files_versions/l10n/fi_FI.js
deleted file mode 100644
index e8e3f210500..00000000000
--- a/apps/files_versions/l10n/fi_FI.js
+++ /dev/null
@@ -1,11 +0,0 @@
-OC.L10N.register(
- "files_versions",
- {
- "Could not revert: %s" : "Palautus epäonnistui: %s",
- "Versions" : "Versiot",
- "Failed to revert {file} to revision {timestamp}." : "Tiedoston {file} palautus versioon {timestamp} epäonnistui.",
- "Restore" : "Palauta",
- "No other versions available" : "Ei muita versioita saatavilla",
- "More versions..." : "Lisää versioita..."
-},
-"nplurals=2; plural=(n != 1);");
diff --git a/apps/files_versions/l10n/fi_FI.json b/apps/files_versions/l10n/fi_FI.json
deleted file mode 100644
index 910a7606374..00000000000
--- a/apps/files_versions/l10n/fi_FI.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{ "translations": {
- "Could not revert: %s" : "Palautus epäonnistui: %s",
- "Versions" : "Versiot",
- "Failed to revert {file} to revision {timestamp}." : "Tiedoston {file} palautus versioon {timestamp} epäonnistui.",
- "Restore" : "Palauta",
- "No other versions available" : "Ei muita versioita saatavilla",
- "More versions..." : "Lisää versioita..."
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
-} \ No newline at end of file
diff --git a/apps/files_versions/l10n/fr.js b/apps/files_versions/l10n/fr.js
index 65a02256d79..88e5299734e 100644
--- a/apps/files_versions/l10n/fr.js
+++ b/apps/files_versions/l10n/fr.js
@@ -1,11 +1,29 @@
OC.L10N.register(
"files_versions",
{
- "Could not revert: %s" : "Impossible de restaurer %s",
"Versions" : "Versions",
- "Failed to revert {file} to revision {timestamp}." : "Échec du retour du fichier {file} à la révision {timestamp}.",
- "Restore" : "Restaurer",
- "No other versions available" : "Aucune autre version n'est disponible",
- "More versions..." : "Plus de versions..."
+ "This application automatically maintains older versions of files that are changed." : "Cette application conserve automatiquement des anciennes versions de fichiers qui ont été modifiés.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Cette application maintient automatiquement les anciennes versions des fichiers qui sont modifiés. Lorsqu'elle est activée, un dossier de versions cachées est provisionné dans le répertoire de chaque utilisateur et est utilisé pour stocker les anciennes versions de fichiers. Un utilisateur peut à tout moment revenir à une ancienne version via l'interface Web, le fichier remplacé devenant à son tour une version. L'application gère automatiquement le dossier des versions pour s'assurer que le compte ne manque pas de place à cause des versions stockées.\n\t\tEn plus de l'expiration des versions, l'application veille à ne jamais utiliser plus de 50% de l'espace libre actuellement disponible pour le compte. Si les versions stockées dépassent cette limite, l'application supprime les versions les plus anciennes en premier jusqu'à ce qu'elle atteigne cette limite. Plus d'informations sont disponibles dans la documentation Versions.",
+ "Current version" : "Version actuelle",
+ "Initial version" : "Version initiale",
+ "You" : "Vous",
+ "Actions for version from {versionHumanExplicitDate}" : "Actions pour la version du {versionHumanExplicitDate}",
+ "Name this version" : "Nommer cette version",
+ "Edit version name" : "Modifier le nom de version",
+ "Compare to current version" : "Comparer avec la version actuelle",
+ "Restore version" : "Restaurer la version",
+ "Download version" : "Télécharger la version",
+ "Delete version" : "Supprimer la version",
+ "Cancel" : "Annuler",
+ "Remove version name" : "Retirer le nom de version",
+ "Save version name" : "Sauvegarder le nom de version",
+ "Version name" : "Nom de la version",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Les versions nommées sont conservées, et exclues des nettoyages automatiques lorsque vos quotas de stockage sont atteints.",
+ "Initial version restored" : "Version initiale restaurée",
+ "Version restored" : "Version restaurée",
+ "Could not restore version" : "Impossible de restaurer la version",
+ "Could not set version label" : "Impossible de définir l'étiquette de version",
+ "Could not delete version" : "Impossible de supprimer la version",
+ "File versions" : "Versions des fichiers"
},
-"nplurals=2; plural=(n > 1);");
+"nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;");
diff --git a/apps/files_versions/l10n/fr.json b/apps/files_versions/l10n/fr.json
index 2c2664d01a0..e59d1f3e59c 100644
--- a/apps/files_versions/l10n/fr.json
+++ b/apps/files_versions/l10n/fr.json
@@ -1,9 +1,27 @@
{ "translations": {
- "Could not revert: %s" : "Impossible de restaurer %s",
"Versions" : "Versions",
- "Failed to revert {file} to revision {timestamp}." : "Échec du retour du fichier {file} à la révision {timestamp}.",
- "Restore" : "Restaurer",
- "No other versions available" : "Aucune autre version n'est disponible",
- "More versions..." : "Plus de versions..."
-},"pluralForm" :"nplurals=2; plural=(n > 1);"
+ "This application automatically maintains older versions of files that are changed." : "Cette application conserve automatiquement des anciennes versions de fichiers qui ont été modifiés.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Cette application maintient automatiquement les anciennes versions des fichiers qui sont modifiés. Lorsqu'elle est activée, un dossier de versions cachées est provisionné dans le répertoire de chaque utilisateur et est utilisé pour stocker les anciennes versions de fichiers. Un utilisateur peut à tout moment revenir à une ancienne version via l'interface Web, le fichier remplacé devenant à son tour une version. L'application gère automatiquement le dossier des versions pour s'assurer que le compte ne manque pas de place à cause des versions stockées.\n\t\tEn plus de l'expiration des versions, l'application veille à ne jamais utiliser plus de 50% de l'espace libre actuellement disponible pour le compte. Si les versions stockées dépassent cette limite, l'application supprime les versions les plus anciennes en premier jusqu'à ce qu'elle atteigne cette limite. Plus d'informations sont disponibles dans la documentation Versions.",
+ "Current version" : "Version actuelle",
+ "Initial version" : "Version initiale",
+ "You" : "Vous",
+ "Actions for version from {versionHumanExplicitDate}" : "Actions pour la version du {versionHumanExplicitDate}",
+ "Name this version" : "Nommer cette version",
+ "Edit version name" : "Modifier le nom de version",
+ "Compare to current version" : "Comparer avec la version actuelle",
+ "Restore version" : "Restaurer la version",
+ "Download version" : "Télécharger la version",
+ "Delete version" : "Supprimer la version",
+ "Cancel" : "Annuler",
+ "Remove version name" : "Retirer le nom de version",
+ "Save version name" : "Sauvegarder le nom de version",
+ "Version name" : "Nom de la version",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Les versions nommées sont conservées, et exclues des nettoyages automatiques lorsque vos quotas de stockage sont atteints.",
+ "Initial version restored" : "Version initiale restaurée",
+ "Version restored" : "Version restaurée",
+ "Could not restore version" : "Impossible de restaurer la version",
+ "Could not set version label" : "Impossible de définir l'étiquette de version",
+ "Could not delete version" : "Impossible de supprimer la version",
+ "File versions" : "Versions des fichiers"
+},"pluralForm" :"nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"
} \ No newline at end of file
diff --git a/apps/files_versions/l10n/ga.js b/apps/files_versions/l10n/ga.js
new file mode 100644
index 00000000000..f9bbe03d5fe
--- /dev/null
+++ b/apps/files_versions/l10n/ga.js
@@ -0,0 +1,29 @@
+OC.L10N.register(
+ "files_versions",
+ {
+ "Versions" : "Leaganacha",
+ "This application automatically maintains older versions of files that are changed." : "Coinníonn an feidhmchlár seo leaganacha níos sine de chomhaid a athraítear go huathoibríoch.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Coinníonn an feidhmchlár seo leaganacha níos sine de chomhaid a athraítear go huathoibríoch. Nuair atá sé cumasaithe, cuirtear fillteán leaganacha ceilte ar fáil i gcomhadlann gach úsáideora agus úsáidtear é chun seanleaganacha comhaid a stóráil. Is féidir le húsáideoir filleadh ar leagan níos sine tríd an gcomhéadan gréasáin am ar bith, agus an comhad a athsholáthar ina leagan. Bainistíonn an aip fillteán na leaganacha go huathoibríoch lena chinntiú nach n-éireoidh an cuntas as Cuóta mar gheall ar leaganacha.\n\t\t Chomh maith le dul in éag na leaganacha, cinntíonn an aip leaganacha nach n-úsáidfidh siad riamh níos mó ná 50% den spás saor in aisce atá ar fáil sa chuntas faoi láthair. Má sháraíonn na leaganacha stóráilte an teorainn seo, scriosfaidh an aip na leaganacha is sine ar dtús go dtí go gcomhlíonann sé an teorainn seo. Tá tuilleadh eolais ar fáil sna doiciméid Leaganacha.",
+ "Current version" : "Leagan reatha",
+ "Initial version" : "Leagan tosaigh",
+ "You" : "tú",
+ "Actions for version from {versionHumanExplicitDate}" : "Gníomhartha don leagan ó {versionHumanExplicitDate}",
+ "Name this version" : "Ainmnigh an leagan seo",
+ "Edit version name" : "Cuir ainm an leagain in eagar",
+ "Compare to current version" : "Cuir i gcomparáid leis an leagan reatha",
+ "Restore version" : "Athchóirigh leagan",
+ "Download version" : "Íoslódáil leagan",
+ "Delete version" : "Scrios leagan",
+ "Cancel" : "Cealaigh",
+ "Remove version name" : "Bain ainm an leagain",
+ "Save version name" : "Sábháil ainm an leagain",
+ "Version name" : "Ainm an leagain",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Leantar de leaganacha ainmnithe, agus fágtar iad as an áireamh ó ghlanadh uathoibríoch nuair a bhíonn do chuóta stórála lán.",
+ "Initial version restored" : "Athchóiríodh an leagan tosaigh",
+ "Version restored" : "Leagan athchóirithe",
+ "Could not restore version" : "Níorbh fhéidir an leagan a chur ar ais",
+ "Could not set version label" : "Níorbh fhéidir lipéad an leagain a shocrú",
+ "Could not delete version" : "Níorbh fhéidir an leagan a scriosadh",
+ "File versions" : "Leaganacha comhaid"
+},
+"nplurals=5; plural=(n==1 ? 0 : n==2 ? 1 : n<7 ? 2 : n<11 ? 3 : 4);");
diff --git a/apps/files_versions/l10n/ga.json b/apps/files_versions/l10n/ga.json
new file mode 100644
index 00000000000..38098d13973
--- /dev/null
+++ b/apps/files_versions/l10n/ga.json
@@ -0,0 +1,27 @@
+{ "translations": {
+ "Versions" : "Leaganacha",
+ "This application automatically maintains older versions of files that are changed." : "Coinníonn an feidhmchlár seo leaganacha níos sine de chomhaid a athraítear go huathoibríoch.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Coinníonn an feidhmchlár seo leaganacha níos sine de chomhaid a athraítear go huathoibríoch. Nuair atá sé cumasaithe, cuirtear fillteán leaganacha ceilte ar fáil i gcomhadlann gach úsáideora agus úsáidtear é chun seanleaganacha comhaid a stóráil. Is féidir le húsáideoir filleadh ar leagan níos sine tríd an gcomhéadan gréasáin am ar bith, agus an comhad a athsholáthar ina leagan. Bainistíonn an aip fillteán na leaganacha go huathoibríoch lena chinntiú nach n-éireoidh an cuntas as Cuóta mar gheall ar leaganacha.\n\t\t Chomh maith le dul in éag na leaganacha, cinntíonn an aip leaganacha nach n-úsáidfidh siad riamh níos mó ná 50% den spás saor in aisce atá ar fáil sa chuntas faoi láthair. Má sháraíonn na leaganacha stóráilte an teorainn seo, scriosfaidh an aip na leaganacha is sine ar dtús go dtí go gcomhlíonann sé an teorainn seo. Tá tuilleadh eolais ar fáil sna doiciméid Leaganacha.",
+ "Current version" : "Leagan reatha",
+ "Initial version" : "Leagan tosaigh",
+ "You" : "tú",
+ "Actions for version from {versionHumanExplicitDate}" : "Gníomhartha don leagan ó {versionHumanExplicitDate}",
+ "Name this version" : "Ainmnigh an leagan seo",
+ "Edit version name" : "Cuir ainm an leagain in eagar",
+ "Compare to current version" : "Cuir i gcomparáid leis an leagan reatha",
+ "Restore version" : "Athchóirigh leagan",
+ "Download version" : "Íoslódáil leagan",
+ "Delete version" : "Scrios leagan",
+ "Cancel" : "Cealaigh",
+ "Remove version name" : "Bain ainm an leagain",
+ "Save version name" : "Sábháil ainm an leagain",
+ "Version name" : "Ainm an leagain",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Leantar de leaganacha ainmnithe, agus fágtar iad as an áireamh ó ghlanadh uathoibríoch nuair a bhíonn do chuóta stórála lán.",
+ "Initial version restored" : "Athchóiríodh an leagan tosaigh",
+ "Version restored" : "Leagan athchóirithe",
+ "Could not restore version" : "Níorbh fhéidir an leagan a chur ar ais",
+ "Could not set version label" : "Níorbh fhéidir lipéad an leagain a shocrú",
+ "Could not delete version" : "Níorbh fhéidir an leagan a scriosadh",
+ "File versions" : "Leaganacha comhaid"
+},"pluralForm" :"nplurals=5; plural=(n==1 ? 0 : n==2 ? 1 : n<7 ? 2 : n<11 ? 3 : 4);"
+} \ No newline at end of file
diff --git a/apps/files_versions/l10n/gl.js b/apps/files_versions/l10n/gl.js
index 673d3a91f14..c9ab468c4c0 100644
--- a/apps/files_versions/l10n/gl.js
+++ b/apps/files_versions/l10n/gl.js
@@ -1,11 +1,28 @@
OC.L10N.register(
"files_versions",
{
- "Could not revert: %s" : "Non foi posíbel reverter: %s",
"Versions" : "Versións",
- "Failed to revert {file} to revision {timestamp}." : "Non foi posíbel reverter {file} á revisión {timestamp}.",
- "Restore" : "Restabelecer",
- "No other versions available" : "Non hai outras versións dispoñíbeis",
- "More versions..." : "Máis versións..."
+ "This application automatically maintains older versions of files that are changed." : "Esta aplicación mantén automaticamente versións antigas dos ficheiros que cambian.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Esta aplicación mantén automaticamente as versións anteriores dos ficheiros que se modifican. Ao activarse, crease un cartafol agochado de versións que se emprega para almacenar versións antigas de ficheiros. Un usuario pode reverter cara a unha versión anterior a través da interface web en calquera momento, e o ficheiro substituído pasa a ser unha versión. A aplicación xestiona automaticamente o cartafol de versións para garantir que a conta non esgota a cota de espazo por mor das versións.\n\t\tAdemais da caducidade de versións, a aplicación de versións asegurase de non empregar nunca máis do 50% do espazo libre dispoñíbel actualmente para unha conta. Se as versións almacenadas superan este límite, a aplicación eliminará primeiro as versións máis antigas ata acadar este límite. Hai máis información dispoñíbel na documentación de Versións.",
+ "Current version" : "Versión actual",
+ "Initial version" : "Versión inicial",
+ "You" : "Vde.",
+ "Actions for version from {versionHumanExplicitDate}" : "Accións para a versión a partir de {versionHumanExplicitDate}",
+ "Name this version" : "Nomear esta versión",
+ "Edit version name" : "Editar o nome da versión",
+ "Compare to current version" : "Comparar coa versión actual",
+ "Restore version" : "Restaurar a versión",
+ "Download version" : "Descargar a versión",
+ "Delete version" : "Eliminar a versión",
+ "Cancel" : "Cancelar",
+ "Remove version name" : "Retirar o nome da versión",
+ "Save version name" : "Gardar o nome da versión",
+ "Version name" : "Nome da versión",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "As versións con nome mantéñense e exclúense das limpezas automáticas cando a súa cota de almacenamento estea completa.",
+ "Initial version restored" : "A versión inicial foi restaurada",
+ "Version restored" : "A versión foi restaurada",
+ "Could not restore version" : "Non foi posíbel restaurar a versión",
+ "Could not set version label" : "Non foi posíbel definir a etiqueta de versión",
+ "Could not delete version" : "Non foi posíbel eliminar a versión"
},
"nplurals=2; plural=(n != 1);");
diff --git a/apps/files_versions/l10n/gl.json b/apps/files_versions/l10n/gl.json
index aaa6e528756..ec5a2856615 100644
--- a/apps/files_versions/l10n/gl.json
+++ b/apps/files_versions/l10n/gl.json
@@ -1,9 +1,26 @@
{ "translations": {
- "Could not revert: %s" : "Non foi posíbel reverter: %s",
"Versions" : "Versións",
- "Failed to revert {file} to revision {timestamp}." : "Non foi posíbel reverter {file} á revisión {timestamp}.",
- "Restore" : "Restabelecer",
- "No other versions available" : "Non hai outras versións dispoñíbeis",
- "More versions..." : "Máis versións..."
+ "This application automatically maintains older versions of files that are changed." : "Esta aplicación mantén automaticamente versións antigas dos ficheiros que cambian.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Esta aplicación mantén automaticamente as versións anteriores dos ficheiros que se modifican. Ao activarse, crease un cartafol agochado de versións que se emprega para almacenar versións antigas de ficheiros. Un usuario pode reverter cara a unha versión anterior a través da interface web en calquera momento, e o ficheiro substituído pasa a ser unha versión. A aplicación xestiona automaticamente o cartafol de versións para garantir que a conta non esgota a cota de espazo por mor das versións.\n\t\tAdemais da caducidade de versións, a aplicación de versións asegurase de non empregar nunca máis do 50% do espazo libre dispoñíbel actualmente para unha conta. Se as versións almacenadas superan este límite, a aplicación eliminará primeiro as versións máis antigas ata acadar este límite. Hai máis información dispoñíbel na documentación de Versións.",
+ "Current version" : "Versión actual",
+ "Initial version" : "Versión inicial",
+ "You" : "Vde.",
+ "Actions for version from {versionHumanExplicitDate}" : "Accións para a versión a partir de {versionHumanExplicitDate}",
+ "Name this version" : "Nomear esta versión",
+ "Edit version name" : "Editar o nome da versión",
+ "Compare to current version" : "Comparar coa versión actual",
+ "Restore version" : "Restaurar a versión",
+ "Download version" : "Descargar a versión",
+ "Delete version" : "Eliminar a versión",
+ "Cancel" : "Cancelar",
+ "Remove version name" : "Retirar o nome da versión",
+ "Save version name" : "Gardar o nome da versión",
+ "Version name" : "Nome da versión",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "As versións con nome mantéñense e exclúense das limpezas automáticas cando a súa cota de almacenamento estea completa.",
+ "Initial version restored" : "A versión inicial foi restaurada",
+ "Version restored" : "A versión foi restaurada",
+ "Could not restore version" : "Non foi posíbel restaurar a versión",
+ "Could not set version label" : "Non foi posíbel definir a etiqueta de versión",
+ "Could not delete version" : "Non foi posíbel eliminar a versión"
},"pluralForm" :"nplurals=2; plural=(n != 1);"
} \ No newline at end of file
diff --git a/apps/files_versions/l10n/he.js b/apps/files_versions/l10n/he.js
deleted file mode 100644
index c240aff76a9..00000000000
--- a/apps/files_versions/l10n/he.js
+++ /dev/null
@@ -1,11 +0,0 @@
-OC.L10N.register(
- "files_versions",
- {
- "Could not revert: %s" : "לא ניתן להחזיר: %s",
- "Versions" : "גרסאות",
- "Failed to revert {file} to revision {timestamp}." : "נכשל אחזור {file} לגרסה {timestamp}.",
- "Restore" : "שחזור",
- "No other versions available" : "אין גרסאות אחרות זמינות",
- "More versions..." : "גרסאות נוספות..."
-},
-"nplurals=2; plural=(n != 1);");
diff --git a/apps/files_versions/l10n/he.json b/apps/files_versions/l10n/he.json
deleted file mode 100644
index 11bd3077abf..00000000000
--- a/apps/files_versions/l10n/he.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{ "translations": {
- "Could not revert: %s" : "לא ניתן להחזיר: %s",
- "Versions" : "גרסאות",
- "Failed to revert {file} to revision {timestamp}." : "נכשל אחזור {file} לגרסה {timestamp}.",
- "Restore" : "שחזור",
- "No other versions available" : "אין גרסאות אחרות זמינות",
- "More versions..." : "גרסאות נוספות..."
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
-} \ No newline at end of file
diff --git a/apps/files_versions/l10n/hr.js b/apps/files_versions/l10n/hr.js
deleted file mode 100644
index 5e3bd4d90d9..00000000000
--- a/apps/files_versions/l10n/hr.js
+++ /dev/null
@@ -1,11 +0,0 @@
-OC.L10N.register(
- "files_versions",
- {
- "Could not revert: %s" : "Nije moguće vratiti: %s",
- "Versions" : "Verzije",
- "Failed to revert {file} to revision {timestamp}." : "Nije uspelo vraćanje {file} na reviziju {timestamp}.",
- "Restore" : "Obnovite",
- "No other versions available" : "Nikakve druge verzije nisu dostupne",
- "More versions..." : "Više verzija..."
-},
-"nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;");
diff --git a/apps/files_versions/l10n/hr.json b/apps/files_versions/l10n/hr.json
deleted file mode 100644
index ada27dc792a..00000000000
--- a/apps/files_versions/l10n/hr.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{ "translations": {
- "Could not revert: %s" : "Nije moguće vratiti: %s",
- "Versions" : "Verzije",
- "Failed to revert {file} to revision {timestamp}." : "Nije uspelo vraćanje {file} na reviziju {timestamp}.",
- "Restore" : "Obnovite",
- "No other versions available" : "Nikakve druge verzije nisu dostupne",
- "More versions..." : "Više verzija..."
-},"pluralForm" :"nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;"
-} \ No newline at end of file
diff --git a/apps/files_versions/l10n/hu.js b/apps/files_versions/l10n/hu.js
new file mode 100644
index 00000000000..d8c34d9aa53
--- /dev/null
+++ b/apps/files_versions/l10n/hu.js
@@ -0,0 +1,28 @@
+OC.L10N.register(
+ "files_versions",
+ {
+ "Versions" : "Verziók",
+ "This application automatically maintains older versions of files that are changed." : "Ez az alkalmazás automatikusan karbantartja a változtatott fájlok régebbi verzióit.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Ez az alkalmazás automatikusan karbantartja a változtatott fájlok régebbi verzióit. Amikor be van kapcsolva, egy rejtett verziómappa jön létre minden felhasználó tárhelyén, amit a korábbi verziók tárolására használ a rendszer. Egy felhasználó bármikor visszatérhet egy korábbi verzióhoz a webes felületen keresztül, és a lecserélt fájl maga is előzménnyé válik. Az alkalmazás automatikusan kezeli a verziómappát, biztosítva, hogy a fiók ne fusson ki a tárterületéből az előzmények miatt.\n\t\tA fájlelőzmények lejárati dátumán kívül az alkalmazás sose használ az elérhető szabad terület 50%-ánál többet. Ha a tárolt előzmények túllépik ezt a korlátot, az alkalmazás addig törli a legrégebbi verziókat, amíg nem felel meg a korlátozásnak. További információ érhető el a Verziók dokumentációjában.",
+ "Current version" : "Jelenlegi verzió",
+ "Initial version" : "Kezdeti verzió",
+ "You" : "Ön",
+ "Actions for version from {versionHumanExplicitDate}" : "A verzióra vonatkozó műveletek: {versionHumanExplicitDate}",
+ "Name this version" : "Verzió elnevezése",
+ "Edit version name" : "Verziónév szerkesztése",
+ "Compare to current version" : "Összehasonlítás a jelenlegi verzióval",
+ "Restore version" : "Verzió helyreállítása",
+ "Download version" : "Verzió letöltése",
+ "Delete version" : "Verzió törlése",
+ "Cancel" : "Mégse",
+ "Remove version name" : "Verziónév eltávolítása",
+ "Save version name" : "Verziónév mentése",
+ "Version name" : "Verziónév",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "A nevesített verziók meg lesznek őrizve, és ki lesznek zárva az automatikus takarítás alól, ha a tárkvóta betelik.",
+ "Initial version restored" : "Kezdeti verzió helyreállítva",
+ "Version restored" : "Verzió helyreállítva",
+ "Could not restore version" : "Nem sikerült helyreállítani a verziót",
+ "Could not set version label" : "Nem sikerült beállítani a verziócímkét",
+ "Could not delete version" : "Nem sikerült törölni a verziót"
+},
+"nplurals=2; plural=(n != 1);");
diff --git a/apps/files_versions/l10n/hu.json b/apps/files_versions/l10n/hu.json
new file mode 100644
index 00000000000..1f4f2a3b26e
--- /dev/null
+++ b/apps/files_versions/l10n/hu.json
@@ -0,0 +1,26 @@
+{ "translations": {
+ "Versions" : "Verziók",
+ "This application automatically maintains older versions of files that are changed." : "Ez az alkalmazás automatikusan karbantartja a változtatott fájlok régebbi verzióit.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Ez az alkalmazás automatikusan karbantartja a változtatott fájlok régebbi verzióit. Amikor be van kapcsolva, egy rejtett verziómappa jön létre minden felhasználó tárhelyén, amit a korábbi verziók tárolására használ a rendszer. Egy felhasználó bármikor visszatérhet egy korábbi verzióhoz a webes felületen keresztül, és a lecserélt fájl maga is előzménnyé válik. Az alkalmazás automatikusan kezeli a verziómappát, biztosítva, hogy a fiók ne fusson ki a tárterületéből az előzmények miatt.\n\t\tA fájlelőzmények lejárati dátumán kívül az alkalmazás sose használ az elérhető szabad terület 50%-ánál többet. Ha a tárolt előzmények túllépik ezt a korlátot, az alkalmazás addig törli a legrégebbi verziókat, amíg nem felel meg a korlátozásnak. További információ érhető el a Verziók dokumentációjában.",
+ "Current version" : "Jelenlegi verzió",
+ "Initial version" : "Kezdeti verzió",
+ "You" : "Ön",
+ "Actions for version from {versionHumanExplicitDate}" : "A verzióra vonatkozó műveletek: {versionHumanExplicitDate}",
+ "Name this version" : "Verzió elnevezése",
+ "Edit version name" : "Verziónév szerkesztése",
+ "Compare to current version" : "Összehasonlítás a jelenlegi verzióval",
+ "Restore version" : "Verzió helyreállítása",
+ "Download version" : "Verzió letöltése",
+ "Delete version" : "Verzió törlése",
+ "Cancel" : "Mégse",
+ "Remove version name" : "Verziónév eltávolítása",
+ "Save version name" : "Verziónév mentése",
+ "Version name" : "Verziónév",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "A nevesített verziók meg lesznek őrizve, és ki lesznek zárva az automatikus takarítás alól, ha a tárkvóta betelik.",
+ "Initial version restored" : "Kezdeti verzió helyreállítva",
+ "Version restored" : "Verzió helyreállítva",
+ "Could not restore version" : "Nem sikerült helyreállítani a verziót",
+ "Could not set version label" : "Nem sikerült beállítani a verziócímkét",
+ "Could not delete version" : "Nem sikerült törölni a verziót"
+},"pluralForm" :"nplurals=2; plural=(n != 1);"
+} \ No newline at end of file
diff --git a/apps/files_versions/l10n/hu_HU.js b/apps/files_versions/l10n/hu_HU.js
deleted file mode 100644
index 9eb8619375e..00000000000
--- a/apps/files_versions/l10n/hu_HU.js
+++ /dev/null
@@ -1,11 +0,0 @@
-OC.L10N.register(
- "files_versions",
- {
- "Could not revert: %s" : "Nem sikerült átállni a változatra: %s",
- "Versions" : "Az állományok korábbi változatai",
- "Failed to revert {file} to revision {timestamp}." : "Nem sikerült a(z) {file} állományt erre visszaállítani: {timestamp}.",
- "Restore" : "Visszaállítás",
- "No other versions available" : "Az állománynak nincs több változata",
- "More versions..." : "További változatok..."
-},
-"nplurals=2; plural=(n != 1);");
diff --git a/apps/files_versions/l10n/hu_HU.json b/apps/files_versions/l10n/hu_HU.json
deleted file mode 100644
index 3fb9deecf42..00000000000
--- a/apps/files_versions/l10n/hu_HU.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{ "translations": {
- "Could not revert: %s" : "Nem sikerült átállni a változatra: %s",
- "Versions" : "Az állományok korábbi változatai",
- "Failed to revert {file} to revision {timestamp}." : "Nem sikerült a(z) {file} állományt erre visszaállítani: {timestamp}.",
- "Restore" : "Visszaállítás",
- "No other versions available" : "Az állománynak nincs több változata",
- "More versions..." : "További változatok..."
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
-} \ No newline at end of file
diff --git a/apps/files_versions/l10n/hy.js b/apps/files_versions/l10n/hy.js
deleted file mode 100644
index d48487a9a84..00000000000
--- a/apps/files_versions/l10n/hy.js
+++ /dev/null
@@ -1,8 +0,0 @@
-OC.L10N.register(
- "files_versions",
- {
- "Versions" : "Տարբերակներ",
- "Restore" : "Վերականգնել",
- "No other versions available" : "Այլ տարբերակներ չկան"
-},
-"nplurals=2; plural=(n != 1);");
diff --git a/apps/files_versions/l10n/hy.json b/apps/files_versions/l10n/hy.json
deleted file mode 100644
index 579b9240310..00000000000
--- a/apps/files_versions/l10n/hy.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{ "translations": {
- "Versions" : "Տարբերակներ",
- "Restore" : "Վերականգնել",
- "No other versions available" : "Այլ տարբերակներ չկան"
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
-} \ No newline at end of file
diff --git a/apps/files_versions/l10n/id.js b/apps/files_versions/l10n/id.js
deleted file mode 100644
index 5d3579c3e62..00000000000
--- a/apps/files_versions/l10n/id.js
+++ /dev/null
@@ -1,11 +0,0 @@
-OC.L10N.register(
- "files_versions",
- {
- "Could not revert: %s" : "Tidak dapat mengembalikan: %s",
- "Versions" : "Versi",
- "Failed to revert {file} to revision {timestamp}." : "Gagal mengembalikan {file} ke revisi {timestamp}.",
- "Restore" : "Pulihkan",
- "No other versions available" : "Tidak ada versi lain yang tersedia",
- "More versions..." : "Versi lebih..."
-},
-"nplurals=1; plural=0;");
diff --git a/apps/files_versions/l10n/id.json b/apps/files_versions/l10n/id.json
deleted file mode 100644
index 7ab5a1638f2..00000000000
--- a/apps/files_versions/l10n/id.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{ "translations": {
- "Could not revert: %s" : "Tidak dapat mengembalikan: %s",
- "Versions" : "Versi",
- "Failed to revert {file} to revision {timestamp}." : "Gagal mengembalikan {file} ke revisi {timestamp}.",
- "Restore" : "Pulihkan",
- "No other versions available" : "Tidak ada versi lain yang tersedia",
- "More versions..." : "Versi lebih..."
-},"pluralForm" :"nplurals=1; plural=0;"
-} \ No newline at end of file
diff --git a/apps/files_versions/l10n/is.js b/apps/files_versions/l10n/is.js
index 542cf7fc303..5f995d19f2f 100644
--- a/apps/files_versions/l10n/is.js
+++ b/apps/files_versions/l10n/is.js
@@ -1,11 +1,27 @@
OC.L10N.register(
"files_versions",
{
- "Could not revert: %s" : "Gat ekki endurheimt: %s",
"Versions" : "Útgáfur",
- "Failed to revert {file} to revision {timestamp}." : "Mistókst að endurheimta {file} útgáfu {timestamp}.",
- "Restore" : "Endurheimta",
- "No other versions available" : "Engar aðrar útgáfur í boði",
- "More versions..." : "Fleiri útgáfur..."
+ "This application automatically maintains older versions of files that are changed." : "Þetta forrit viðheldur sjálfvirkt eldri útgáfum skráa sem er breytt.",
+ "Current version" : "Fyrirliggjandi útgáfa",
+ "Initial version" : "Upprunaleg útgáfa",
+ "You" : "Þú",
+ "Actions for version from {versionHumanExplicitDate}" : "Aðgerðir fyrir útgáfu frá {versionHumanExplicitDate}",
+ "Name this version" : "Settu heiti á þessa útgáfu",
+ "Edit version name" : "Breyta heiti útgáfunnar",
+ "Compare to current version" : "Bera saman við fyrirliggjandi útgáfu",
+ "Restore version" : "Endurheimta útgáfu",
+ "Download version" : "Sækja útgáfu",
+ "Delete version" : "Eyða útgáfu",
+ "Cancel" : "Hætta við",
+ "Remove version name" : "Fjarlægja heiti útgáfu",
+ "Save version name" : "Vista heiti útgáfu",
+ "Version name" : "Heiti útgáfu",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Nefndar útgáfur eru varanlegar og er undanþegnar við sjálfvirkar tiltektir þegar geymslukvóti fyllist.",
+ "Initial version restored" : "Upprunaleg útgáfa endurheimt",
+ "Version restored" : "Útgáfa endurheimt",
+ "Could not restore version" : "Gat ekki endurheimt útgáfu",
+ "Could not set version label" : "Gat ekki sett merkingu á útgáfu",
+ "Could not delete version" : "Gat ekki eytt útgáfu"
},
"nplurals=2; plural=(n % 10 != 1 || n % 100 == 11);");
diff --git a/apps/files_versions/l10n/is.json b/apps/files_versions/l10n/is.json
index 43b53501cd5..368351cdac7 100644
--- a/apps/files_versions/l10n/is.json
+++ b/apps/files_versions/l10n/is.json
@@ -1,9 +1,25 @@
{ "translations": {
- "Could not revert: %s" : "Gat ekki endurheimt: %s",
"Versions" : "Útgáfur",
- "Failed to revert {file} to revision {timestamp}." : "Mistókst að endurheimta {file} útgáfu {timestamp}.",
- "Restore" : "Endurheimta",
- "No other versions available" : "Engar aðrar útgáfur í boði",
- "More versions..." : "Fleiri útgáfur..."
+ "This application automatically maintains older versions of files that are changed." : "Þetta forrit viðheldur sjálfvirkt eldri útgáfum skráa sem er breytt.",
+ "Current version" : "Fyrirliggjandi útgáfa",
+ "Initial version" : "Upprunaleg útgáfa",
+ "You" : "Þú",
+ "Actions for version from {versionHumanExplicitDate}" : "Aðgerðir fyrir útgáfu frá {versionHumanExplicitDate}",
+ "Name this version" : "Settu heiti á þessa útgáfu",
+ "Edit version name" : "Breyta heiti útgáfunnar",
+ "Compare to current version" : "Bera saman við fyrirliggjandi útgáfu",
+ "Restore version" : "Endurheimta útgáfu",
+ "Download version" : "Sækja útgáfu",
+ "Delete version" : "Eyða útgáfu",
+ "Cancel" : "Hætta við",
+ "Remove version name" : "Fjarlægja heiti útgáfu",
+ "Save version name" : "Vista heiti útgáfu",
+ "Version name" : "Heiti útgáfu",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Nefndar útgáfur eru varanlegar og er undanþegnar við sjálfvirkar tiltektir þegar geymslukvóti fyllist.",
+ "Initial version restored" : "Upprunaleg útgáfa endurheimt",
+ "Version restored" : "Útgáfa endurheimt",
+ "Could not restore version" : "Gat ekki endurheimt útgáfu",
+ "Could not set version label" : "Gat ekki sett merkingu á útgáfu",
+ "Could not delete version" : "Gat ekki eytt útgáfu"
},"pluralForm" :"nplurals=2; plural=(n % 10 != 1 || n % 100 == 11);"
} \ No newline at end of file
diff --git a/apps/files_versions/l10n/it.js b/apps/files_versions/l10n/it.js
index cb6248c3f42..2e782c6d327 100644
--- a/apps/files_versions/l10n/it.js
+++ b/apps/files_versions/l10n/it.js
@@ -1,11 +1,29 @@
OC.L10N.register(
"files_versions",
{
- "Could not revert: %s" : "Impossibile ripristinare: %s",
"Versions" : "Versioni",
- "Failed to revert {file} to revision {timestamp}." : "Ripristino di {file} alla revisione {timestamp} non riuscito.",
- "Restore" : "Ripristina",
- "No other versions available" : "Non sono disponibili altre versioni",
- "More versions..." : "Altre versioni..."
+ "This application automatically maintains older versions of files that are changed." : "Questa applicazione mantiene automaticamente versioni più datate dei file modificati.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Questa applicazione conserva automaticamente le versioni più datate dei file che sono stati modificati. Se attivata, una cartella nascosta delle versioni viene generata in ogni cartella degli utenti ed è usata per archiviare le versioni datate dei file. Un utente può tornare a una versione più datata tramite l'interfaccia web in qualsiasi momento, con il file sostituito che diventa una versione. L'applicazione gestisce automaticamente la cartella delle versioni per assicurare che l'account non esaurisca mai la quota a causa delle versioni.\n\t\tIn aggiunta alla scadenza delle versioni, l'applicazione delle versioni si accerta che non sia utilizzato mai più del 50% dello spazio disponibile attualmente all'account. Se le versioni archiviate eccedono questo limite, l'applicazione eliminerà prima le versioni più vecchie, fino a raggiungere questo limite. Altre informazioni sono disponibili nella documentazione di Versioni.",
+ "Current version" : "Versione corrente",
+ "Initial version" : "Versione iniziale",
+ "You" : "Tu",
+ "Actions for version from {versionHumanExplicitDate}" : "Azioni per la versione da {versionHumanExplicitDate}",
+ "Name this version" : "Nome di questa versione",
+ "Edit version name" : "Modifica nome versione",
+ "Compare to current version" : "Confronta con la versione attuale",
+ "Restore version" : "Ripristina versione",
+ "Download version" : "Scarica versione",
+ "Delete version" : "Elimina versione",
+ "Cancel" : "Annulla",
+ "Remove version name" : "Rimuovi nome versione",
+ "Save version name" : "Salva nome versione",
+ "Version name" : "Nome versione",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Le versioni con nome sono persistenti ed escluse dalle pulizie automatiche quando lo spazio è pieno.",
+ "Initial version restored" : "Versione iniziale ripristinata",
+ "Version restored" : "Versione ripristinata",
+ "Could not restore version" : "Impossibile ripristinare la versione",
+ "Could not set version label" : "Impossibile impostare l'etichetta della versione",
+ "Could not delete version" : "Impossibile eliminare la versione",
+ "File versions" : "Versioni dei file"
},
-"nplurals=2; plural=(n != 1);");
+"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;");
diff --git a/apps/files_versions/l10n/it.json b/apps/files_versions/l10n/it.json
index f1479e7c114..0871aa8c115 100644
--- a/apps/files_versions/l10n/it.json
+++ b/apps/files_versions/l10n/it.json
@@ -1,9 +1,27 @@
{ "translations": {
- "Could not revert: %s" : "Impossibile ripristinare: %s",
"Versions" : "Versioni",
- "Failed to revert {file} to revision {timestamp}." : "Ripristino di {file} alla revisione {timestamp} non riuscito.",
- "Restore" : "Ripristina",
- "No other versions available" : "Non sono disponibili altre versioni",
- "More versions..." : "Altre versioni..."
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
+ "This application automatically maintains older versions of files that are changed." : "Questa applicazione mantiene automaticamente versioni più datate dei file modificati.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Questa applicazione conserva automaticamente le versioni più datate dei file che sono stati modificati. Se attivata, una cartella nascosta delle versioni viene generata in ogni cartella degli utenti ed è usata per archiviare le versioni datate dei file. Un utente può tornare a una versione più datata tramite l'interfaccia web in qualsiasi momento, con il file sostituito che diventa una versione. L'applicazione gestisce automaticamente la cartella delle versioni per assicurare che l'account non esaurisca mai la quota a causa delle versioni.\n\t\tIn aggiunta alla scadenza delle versioni, l'applicazione delle versioni si accerta che non sia utilizzato mai più del 50% dello spazio disponibile attualmente all'account. Se le versioni archiviate eccedono questo limite, l'applicazione eliminerà prima le versioni più vecchie, fino a raggiungere questo limite. Altre informazioni sono disponibili nella documentazione di Versioni.",
+ "Current version" : "Versione corrente",
+ "Initial version" : "Versione iniziale",
+ "You" : "Tu",
+ "Actions for version from {versionHumanExplicitDate}" : "Azioni per la versione da {versionHumanExplicitDate}",
+ "Name this version" : "Nome di questa versione",
+ "Edit version name" : "Modifica nome versione",
+ "Compare to current version" : "Confronta con la versione attuale",
+ "Restore version" : "Ripristina versione",
+ "Download version" : "Scarica versione",
+ "Delete version" : "Elimina versione",
+ "Cancel" : "Annulla",
+ "Remove version name" : "Rimuovi nome versione",
+ "Save version name" : "Salva nome versione",
+ "Version name" : "Nome versione",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Le versioni con nome sono persistenti ed escluse dalle pulizie automatiche quando lo spazio è pieno.",
+ "Initial version restored" : "Versione iniziale ripristinata",
+ "Version restored" : "Versione ripristinata",
+ "Could not restore version" : "Impossibile ripristinare la versione",
+ "Could not set version label" : "Impossibile impostare l'etichetta della versione",
+ "Could not delete version" : "Impossibile eliminare la versione",
+ "File versions" : "Versioni dei file"
+},"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"
} \ No newline at end of file
diff --git a/apps/files_versions/l10n/ja.js b/apps/files_versions/l10n/ja.js
index 01fe310df71..d4202f4050d 100644
--- a/apps/files_versions/l10n/ja.js
+++ b/apps/files_versions/l10n/ja.js
@@ -1,11 +1,29 @@
OC.L10N.register(
"files_versions",
{
- "Could not revert: %s" : "元に戻せませんでした: %s",
"Versions" : "バージョン",
- "Failed to revert {file} to revision {timestamp}." : "{file} を {timestamp} のリビジョンに戻すことができません。",
- "Restore" : "復元",
- "No other versions available" : "利用可能なバージョンはありません",
- "More versions..." : "他のバージョン..."
+ "This application automatically maintains older versions of files that are changed." : "このアプリケーションは、変更された古いバージョンのファイルを自動的に維持します。",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "このアプリケーションは、変更された古いバージョンのファイルを自動的に維持します。有効にすると、隠れバージョン用フォルダーがすべてのユーザーのディレクトリに用意され、古いファイルバージョンを格納するために使用されます。ユーザーはいつでもWebインターフェースから古いバージョンに戻すことができ、置き換えられたファイルはバージョン管理されます。アプリはバージョンフォルダーを自動的に管理して、アカウントがバージョン履歴のために容量を使い果たさないようにします。\nバージョンの有効期限に加えて、バージョンアプリは、アカウントの現在利用可能な空き容量の50%以上を使用しないように維持します。保存されたバージョンがこの制限を超えた場合、アプリはこの制限を満たすまで、最初に最も古いバージョンから削除します。詳細は、バージョンのドキュメントを参照してください。",
+ "Current version" : "現在のバージョン",
+ "Initial version" : "最初のバージョン",
+ "You" : "自分",
+ "Actions for version from {versionHumanExplicitDate}" : "{versionHumanExplicitDate}からのバージョンに対するアクション",
+ "Name this version" : "このバージョンに名前を付けます",
+ "Edit version name" : "バージョン名の編集",
+ "Compare to current version" : "現在のバージョンと比較",
+ "Restore version" : "このバージョンを復元する",
+ "Download version" : "このバージョンをダウンロード",
+ "Delete version" : "このバージョンを削除",
+ "Cancel" : "キャンセル",
+ "Remove version name" : "バージョン名を削除",
+ "Save version name" : "バージョン名を保存",
+ "Version name" : "名前付きバージョン",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "名前付きバージョンは永続化され、ストレージクォータがいっぱいになっていても自動クリーンアップから除外されます。",
+ "Initial version restored" : "初期バージョンを復旧",
+ "Version restored" : "指定のバージョンが復旧されました",
+ "Could not restore version" : "このバージョンを復旧できません",
+ "Could not set version label" : "バージョンラベルを付けることができませんでした",
+ "Could not delete version" : "指定のバージョンを削除できませんでした",
+ "File versions" : "ファイルバージョン"
},
"nplurals=1; plural=0;");
diff --git a/apps/files_versions/l10n/ja.json b/apps/files_versions/l10n/ja.json
index aa634ca6bea..2d877d60eb7 100644
--- a/apps/files_versions/l10n/ja.json
+++ b/apps/files_versions/l10n/ja.json
@@ -1,9 +1,27 @@
{ "translations": {
- "Could not revert: %s" : "元に戻せませんでした: %s",
"Versions" : "バージョン",
- "Failed to revert {file} to revision {timestamp}." : "{file} を {timestamp} のリビジョンに戻すことができません。",
- "Restore" : "復元",
- "No other versions available" : "利用可能なバージョンはありません",
- "More versions..." : "他のバージョン..."
+ "This application automatically maintains older versions of files that are changed." : "このアプリケーションは、変更された古いバージョンのファイルを自動的に維持します。",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "このアプリケーションは、変更された古いバージョンのファイルを自動的に維持します。有効にすると、隠れバージョン用フォルダーがすべてのユーザーのディレクトリに用意され、古いファイルバージョンを格納するために使用されます。ユーザーはいつでもWebインターフェースから古いバージョンに戻すことができ、置き換えられたファイルはバージョン管理されます。アプリはバージョンフォルダーを自動的に管理して、アカウントがバージョン履歴のために容量を使い果たさないようにします。\nバージョンの有効期限に加えて、バージョンアプリは、アカウントの現在利用可能な空き容量の50%以上を使用しないように維持します。保存されたバージョンがこの制限を超えた場合、アプリはこの制限を満たすまで、最初に最も古いバージョンから削除します。詳細は、バージョンのドキュメントを参照してください。",
+ "Current version" : "現在のバージョン",
+ "Initial version" : "最初のバージョン",
+ "You" : "自分",
+ "Actions for version from {versionHumanExplicitDate}" : "{versionHumanExplicitDate}からのバージョンに対するアクション",
+ "Name this version" : "このバージョンに名前を付けます",
+ "Edit version name" : "バージョン名の編集",
+ "Compare to current version" : "現在のバージョンと比較",
+ "Restore version" : "このバージョンを復元する",
+ "Download version" : "このバージョンをダウンロード",
+ "Delete version" : "このバージョンを削除",
+ "Cancel" : "キャンセル",
+ "Remove version name" : "バージョン名を削除",
+ "Save version name" : "バージョン名を保存",
+ "Version name" : "名前付きバージョン",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "名前付きバージョンは永続化され、ストレージクォータがいっぱいになっていても自動クリーンアップから除外されます。",
+ "Initial version restored" : "初期バージョンを復旧",
+ "Version restored" : "指定のバージョンが復旧されました",
+ "Could not restore version" : "このバージョンを復旧できません",
+ "Could not set version label" : "バージョンラベルを付けることができませんでした",
+ "Could not delete version" : "指定のバージョンを削除できませんでした",
+ "File versions" : "ファイルバージョン"
},"pluralForm" :"nplurals=1; plural=0;"
} \ No newline at end of file
diff --git a/apps/files_versions/l10n/ka.js b/apps/files_versions/l10n/ka.js
new file mode 100644
index 00000000000..92b8518a28f
--- /dev/null
+++ b/apps/files_versions/l10n/ka.js
@@ -0,0 +1,25 @@
+OC.L10N.register(
+ "files_versions",
+ {
+ "Versions" : "Versions",
+ "This application automatically maintains older versions of files that are changed." : "This application automatically maintains older versions of files that are changed.",
+ "Current version" : "Current version",
+ "Initial version" : "Initial version",
+ "You" : "You",
+ "Name this version" : "Name this version",
+ "Edit version name" : "Edit version name",
+ "Compare to current version" : "Compare to current version",
+ "Restore version" : "Restore version",
+ "Download version" : "Download version",
+ "Delete version" : "Delete version",
+ "Cancel" : "Cancel",
+ "Remove version name" : "Remove version name",
+ "Save version name" : "Save version name",
+ "Version name" : "Version name",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full.",
+ "Initial version restored" : "Initial version restored",
+ "Version restored" : "Version restored",
+ "Could not restore version" : "Could not restore version",
+ "Could not delete version" : "Could not delete version"
+},
+"nplurals=2; plural=(n!=1);");
diff --git a/apps/files_versions/l10n/ka.json b/apps/files_versions/l10n/ka.json
new file mode 100644
index 00000000000..fb116a07a9d
--- /dev/null
+++ b/apps/files_versions/l10n/ka.json
@@ -0,0 +1,23 @@
+{ "translations": {
+ "Versions" : "Versions",
+ "This application automatically maintains older versions of files that are changed." : "This application automatically maintains older versions of files that are changed.",
+ "Current version" : "Current version",
+ "Initial version" : "Initial version",
+ "You" : "You",
+ "Name this version" : "Name this version",
+ "Edit version name" : "Edit version name",
+ "Compare to current version" : "Compare to current version",
+ "Restore version" : "Restore version",
+ "Download version" : "Download version",
+ "Delete version" : "Delete version",
+ "Cancel" : "Cancel",
+ "Remove version name" : "Remove version name",
+ "Save version name" : "Save version name",
+ "Version name" : "Version name",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full.",
+ "Initial version restored" : "Initial version restored",
+ "Version restored" : "Version restored",
+ "Could not restore version" : "Could not restore version",
+ "Could not delete version" : "Could not delete version"
+},"pluralForm" :"nplurals=2; plural=(n!=1);"
+} \ No newline at end of file
diff --git a/apps/files_versions/l10n/ka_GE.js b/apps/files_versions/l10n/ka_GE.js
deleted file mode 100644
index d958b4f4dee..00000000000
--- a/apps/files_versions/l10n/ka_GE.js
+++ /dev/null
@@ -1,8 +0,0 @@
-OC.L10N.register(
- "files_versions",
- {
- "Could not revert: %s" : "ვერ მოხერხდა უკან დაბრუნება: %s",
- "Versions" : "ვერსიები",
- "Restore" : "აღდგენა"
-},
-"nplurals=1; plural=0;");
diff --git a/apps/files_versions/l10n/ka_GE.json b/apps/files_versions/l10n/ka_GE.json
deleted file mode 100644
index a3b2d0344de..00000000000
--- a/apps/files_versions/l10n/ka_GE.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{ "translations": {
- "Could not revert: %s" : "ვერ მოხერხდა უკან დაბრუნება: %s",
- "Versions" : "ვერსიები",
- "Restore" : "აღდგენა"
-},"pluralForm" :"nplurals=1; plural=0;"
-} \ No newline at end of file
diff --git a/apps/files_versions/l10n/km.js b/apps/files_versions/l10n/km.js
deleted file mode 100644
index 6b4f21e25ad..00000000000
--- a/apps/files_versions/l10n/km.js
+++ /dev/null
@@ -1,11 +0,0 @@
-OC.L10N.register(
- "files_versions",
- {
- "Could not revert: %s" : "មិន​អាច​ត្រឡប់៖ %s",
- "Versions" : "កំណែ",
- "Failed to revert {file} to revision {timestamp}." : "មិន​អាច​ត្រឡប់ {file} ទៅ​កំណែ​សម្រួល {timestamp} បាន​ទេ។",
- "Restore" : "ស្ដារ​មក​វិញ",
- "No other versions available" : "មិន​មាន​កំណែ​ផ្សេង​ទៀត​ទេ",
- "More versions..." : "កំណែ​ច្រើន​ទៀត..."
-},
-"nplurals=1; plural=0;");
diff --git a/apps/files_versions/l10n/km.json b/apps/files_versions/l10n/km.json
deleted file mode 100644
index 020b9e81ce5..00000000000
--- a/apps/files_versions/l10n/km.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{ "translations": {
- "Could not revert: %s" : "មិន​អាច​ត្រឡប់៖ %s",
- "Versions" : "កំណែ",
- "Failed to revert {file} to revision {timestamp}." : "មិន​អាច​ត្រឡប់ {file} ទៅ​កំណែ​សម្រួល {timestamp} បាន​ទេ។",
- "Restore" : "ស្ដារ​មក​វិញ",
- "No other versions available" : "មិន​មាន​កំណែ​ផ្សេង​ទៀត​ទេ",
- "More versions..." : "កំណែ​ច្រើន​ទៀត..."
-},"pluralForm" :"nplurals=1; plural=0;"
-} \ No newline at end of file
diff --git a/apps/files_versions/l10n/kn.js b/apps/files_versions/l10n/kn.js
deleted file mode 100644
index c7c255f5480..00000000000
--- a/apps/files_versions/l10n/kn.js
+++ /dev/null
@@ -1,11 +0,0 @@
-OC.L10N.register(
- "files_versions",
- {
- "Could not revert: %s" : "ಹಿಂತಿರುಗಲಾಗಲಿಲ್ಲ: %s",
- "Versions" : "ಆವೃತ್ತಿಗಳು",
- "Failed to revert {file} to revision {timestamp}." : "{timestamp} ದ ಪರಿಷ್ಕರಣೆ ಇಂದ {file} ಕಡತವನ್ನು ಹಿಂದಿರುಗಿಸಲು ವಿಫಲವಾಗಿದೆ.",
- "Restore" : "ಮರುಸ್ಥಾಪಿಸು",
- "No other versions available" : "ಇನ್ನಿತರೆ ಯಾವುದೇ ಆವೃತ್ತಿಗಳು ಲಭ್ಯವಿಲ್ಲ",
- "More versions..." : "ಇನ್ನಷ್ಟು ಆವೃತ್ತಿಗಳು ..."
-},
-"nplurals=1; plural=0;");
diff --git a/apps/files_versions/l10n/kn.json b/apps/files_versions/l10n/kn.json
deleted file mode 100644
index 37f1d57c6bd..00000000000
--- a/apps/files_versions/l10n/kn.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{ "translations": {
- "Could not revert: %s" : "ಹಿಂತಿರುಗಲಾಗಲಿಲ್ಲ: %s",
- "Versions" : "ಆವೃತ್ತಿಗಳು",
- "Failed to revert {file} to revision {timestamp}." : "{timestamp} ದ ಪರಿಷ್ಕರಣೆ ಇಂದ {file} ಕಡತವನ್ನು ಹಿಂದಿರುಗಿಸಲು ವಿಫಲವಾಗಿದೆ.",
- "Restore" : "ಮರುಸ್ಥಾಪಿಸು",
- "No other versions available" : "ಇನ್ನಿತರೆ ಯಾವುದೇ ಆವೃತ್ತಿಗಳು ಲಭ್ಯವಿಲ್ಲ",
- "More versions..." : "ಇನ್ನಷ್ಟು ಆವೃತ್ತಿಗಳು ..."
-},"pluralForm" :"nplurals=1; plural=0;"
-} \ No newline at end of file
diff --git a/apps/files_versions/l10n/ko.js b/apps/files_versions/l10n/ko.js
index 21c30390609..ec839dace65 100644
--- a/apps/files_versions/l10n/ko.js
+++ b/apps/files_versions/l10n/ko.js
@@ -1,11 +1,25 @@
OC.L10N.register(
"files_versions",
{
- "Could not revert: %s" : "되돌릴 수 없습니다: %s",
"Versions" : "버전",
- "Failed to revert {file} to revision {timestamp}." : "{file}을(를) 리비전 {timestamp}으(로) 되돌리는 데 실패했습니다.",
- "Restore" : "복원",
- "No other versions available" : "다른 버전을 사용할 수 없습니다",
- "More versions..." : "더 많은 버전..."
+ "This application automatically maintains older versions of files that are changed." : "이 앱은 변경된 파일의 이전 버전을 관리합니다.",
+ "Current version" : "현재 버전",
+ "Initial version" : "초기 버전",
+ "You" : "당신",
+ "Name this version" : "버전 이름 지정",
+ "Edit version name" : "버전 이름 수정",
+ "Compare to current version" : "현재 버전과 비교하기",
+ "Restore version" : "버전 복원",
+ "Download version" : "버전 다운로드",
+ "Delete version" : "버전 삭제",
+ "Cancel" : "취소",
+ "Remove version name" : "버전 이름 제거",
+ "Save version name" : "버전 이름 저장",
+ "Version name" : "버전 이름",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "이름이 지정된 버전은 유지되며, 저장 공간 할당량이 가득 찼을 때의 자동 정리 목록에서 제외됩니다.",
+ "Initial version restored" : "초기 버전이 복원됨",
+ "Version restored" : "버전이 복원됨",
+ "Could not restore version" : "버전을 복원할 수 없음",
+ "Could not delete version" : "버전을 삭제할 수 없음"
},
"nplurals=1; plural=0;");
diff --git a/apps/files_versions/l10n/ko.json b/apps/files_versions/l10n/ko.json
index 22ad4b685be..658529bf8bd 100644
--- a/apps/files_versions/l10n/ko.json
+++ b/apps/files_versions/l10n/ko.json
@@ -1,9 +1,23 @@
{ "translations": {
- "Could not revert: %s" : "되돌릴 수 없습니다: %s",
"Versions" : "버전",
- "Failed to revert {file} to revision {timestamp}." : "{file}을(를) 리비전 {timestamp}으(로) 되돌리는 데 실패했습니다.",
- "Restore" : "복원",
- "No other versions available" : "다른 버전을 사용할 수 없습니다",
- "More versions..." : "더 많은 버전..."
+ "This application automatically maintains older versions of files that are changed." : "이 앱은 변경된 파일의 이전 버전을 관리합니다.",
+ "Current version" : "현재 버전",
+ "Initial version" : "초기 버전",
+ "You" : "당신",
+ "Name this version" : "버전 이름 지정",
+ "Edit version name" : "버전 이름 수정",
+ "Compare to current version" : "현재 버전과 비교하기",
+ "Restore version" : "버전 복원",
+ "Download version" : "버전 다운로드",
+ "Delete version" : "버전 삭제",
+ "Cancel" : "취소",
+ "Remove version name" : "버전 이름 제거",
+ "Save version name" : "버전 이름 저장",
+ "Version name" : "버전 이름",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "이름이 지정된 버전은 유지되며, 저장 공간 할당량이 가득 찼을 때의 자동 정리 목록에서 제외됩니다.",
+ "Initial version restored" : "초기 버전이 복원됨",
+ "Version restored" : "버전이 복원됨",
+ "Could not restore version" : "버전을 복원할 수 없음",
+ "Could not delete version" : "버전을 삭제할 수 없음"
},"pluralForm" :"nplurals=1; plural=0;"
} \ No newline at end of file
diff --git a/apps/files_versions/l10n/ku_IQ.js b/apps/files_versions/l10n/ku_IQ.js
deleted file mode 100644
index 46d7e075356..00000000000
--- a/apps/files_versions/l10n/ku_IQ.js
+++ /dev/null
@@ -1,6 +0,0 @@
-OC.L10N.register(
- "files_versions",
- {
- "Versions" : "وه‌شان"
-},
-"nplurals=2; plural=(n != 1);");
diff --git a/apps/files_versions/l10n/ku_IQ.json b/apps/files_versions/l10n/ku_IQ.json
deleted file mode 100644
index 0a929bb22df..00000000000
--- a/apps/files_versions/l10n/ku_IQ.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{ "translations": {
- "Versions" : "وه‌شان"
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
-} \ No newline at end of file
diff --git a/apps/files_versions/l10n/lb.js b/apps/files_versions/l10n/lb.js
deleted file mode 100644
index e2ef61b370f..00000000000
--- a/apps/files_versions/l10n/lb.js
+++ /dev/null
@@ -1,11 +0,0 @@
-OC.L10N.register(
- "files_versions",
- {
- "Could not revert: %s" : "Konnt net zrécksetzen: %s",
- "Versions" : "Versiounen",
- "Failed to revert {file} to revision {timestamp}." : "Konnt {file} net op d'Versioun {timestamp} zrécksetzen.",
- "Restore" : "Zrécksetzen",
- "No other versions available" : "Keng aner Versiounen disponibel",
- "More versions..." : "Méi Versiounen..."
-},
-"nplurals=2; plural=(n != 1);");
diff --git a/apps/files_versions/l10n/lb.json b/apps/files_versions/l10n/lb.json
deleted file mode 100644
index 8265a6bb552..00000000000
--- a/apps/files_versions/l10n/lb.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{ "translations": {
- "Could not revert: %s" : "Konnt net zrécksetzen: %s",
- "Versions" : "Versiounen",
- "Failed to revert {file} to revision {timestamp}." : "Konnt {file} net op d'Versioun {timestamp} zrécksetzen.",
- "Restore" : "Zrécksetzen",
- "No other versions available" : "Keng aner Versiounen disponibel",
- "More versions..." : "Méi Versiounen..."
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
-} \ No newline at end of file
diff --git a/apps/files_versions/l10n/lt_LT.js b/apps/files_versions/l10n/lt_LT.js
index 25494614a96..101281dae52 100644
--- a/apps/files_versions/l10n/lt_LT.js
+++ b/apps/files_versions/l10n/lt_LT.js
@@ -1,11 +1,22 @@
OC.L10N.register(
"files_versions",
{
- "Could not revert: %s" : "Nepavyko atstatyti: %s",
"Versions" : "Versijos",
- "Failed to revert {file} to revision {timestamp}." : "Nepavyko atstatyti {file} į būseną {timestamp}.",
- "Restore" : "Atstatyti",
- "No other versions available" : "Nėra daugiau versijų",
- "More versions..." : "Daugiau versijų..."
+ "This application automatically maintains older versions of files that are changed." : "Ši programa automatiškai palaiko senesnes pakeistų failų versijas.",
+ "Current version" : "Dabartinė versija",
+ "Initial version" : "Pradinė versija",
+ "You" : "Jūs",
+ "Edit version name" : "Taisyti versijos pavadinimą",
+ "Compare to current version" : "Palyginti su dabartine versija",
+ "Restore version" : "Atkurti versiją",
+ "Download version" : "Atsisiųsti versiją",
+ "Delete version" : "Ištrinti versiją",
+ "Cancel" : "Atsisakyti",
+ "Remove version name" : "Šalinti versijos pavadinimą",
+ "Save version name" : "Įrašyti versijos pavadinimą",
+ "Version name" : "Versijos pavadinimas",
+ "Version restored" : "Versija atkurta",
+ "Could not restore version" : "Nepavyko atkurti versijos",
+ "Could not delete version" : "Nepavyko ištrinti versijos"
},
-"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2);");
+"nplurals=4; plural=(n % 10 == 1 && (n % 100 > 19 || n % 100 < 11) ? 0 : (n % 10 >= 2 && n % 10 <=9) && (n % 100 > 19 || n % 100 < 11) ? 1 : n % 1 != 0 ? 2: 3);");
diff --git a/apps/files_versions/l10n/lt_LT.json b/apps/files_versions/l10n/lt_LT.json
index 3b3a6feb0f5..6f0ce5d3c6d 100644
--- a/apps/files_versions/l10n/lt_LT.json
+++ b/apps/files_versions/l10n/lt_LT.json
@@ -1,9 +1,20 @@
{ "translations": {
- "Could not revert: %s" : "Nepavyko atstatyti: %s",
"Versions" : "Versijos",
- "Failed to revert {file} to revision {timestamp}." : "Nepavyko atstatyti {file} į būseną {timestamp}.",
- "Restore" : "Atstatyti",
- "No other versions available" : "Nėra daugiau versijų",
- "More versions..." : "Daugiau versijų..."
-},"pluralForm" :"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2);"
+ "This application automatically maintains older versions of files that are changed." : "Ši programa automatiškai palaiko senesnes pakeistų failų versijas.",
+ "Current version" : "Dabartinė versija",
+ "Initial version" : "Pradinė versija",
+ "You" : "Jūs",
+ "Edit version name" : "Taisyti versijos pavadinimą",
+ "Compare to current version" : "Palyginti su dabartine versija",
+ "Restore version" : "Atkurti versiją",
+ "Download version" : "Atsisiųsti versiją",
+ "Delete version" : "Ištrinti versiją",
+ "Cancel" : "Atsisakyti",
+ "Remove version name" : "Šalinti versijos pavadinimą",
+ "Save version name" : "Įrašyti versijos pavadinimą",
+ "Version name" : "Versijos pavadinimas",
+ "Version restored" : "Versija atkurta",
+ "Could not restore version" : "Nepavyko atkurti versijos",
+ "Could not delete version" : "Nepavyko ištrinti versijos"
+},"pluralForm" :"nplurals=4; plural=(n % 10 == 1 && (n % 100 > 19 || n % 100 < 11) ? 0 : (n % 10 >= 2 && n % 10 <=9) && (n % 100 > 19 || n % 100 < 11) ? 1 : n % 1 != 0 ? 2: 3);"
} \ No newline at end of file
diff --git a/apps/files_versions/l10n/lv.js b/apps/files_versions/l10n/lv.js
deleted file mode 100644
index 9db45fe7b4a..00000000000
--- a/apps/files_versions/l10n/lv.js
+++ /dev/null
@@ -1,11 +0,0 @@
-OC.L10N.register(
- "files_versions",
- {
- "Could not revert: %s" : "Nevarēja atgriezt — %s",
- "Versions" : "Versijas",
- "Failed to revert {file} to revision {timestamp}." : "Neizdevās atjaunot {file} no rediģējuma {timestamp} ",
- "Restore" : "Atjaunot",
- "No other versions available" : "Citas versijas nav pieejamas",
- "More versions..." : "Vairāk versiju..."
-},
-"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);");
diff --git a/apps/files_versions/l10n/lv.json b/apps/files_versions/l10n/lv.json
deleted file mode 100644
index f2d4c2fc316..00000000000
--- a/apps/files_versions/l10n/lv.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{ "translations": {
- "Could not revert: %s" : "Nevarēja atgriezt — %s",
- "Versions" : "Versijas",
- "Failed to revert {file} to revision {timestamp}." : "Neizdevās atjaunot {file} no rediģējuma {timestamp} ",
- "Restore" : "Atjaunot",
- "No other versions available" : "Citas versijas nav pieejamas",
- "More versions..." : "Vairāk versiju..."
-},"pluralForm" :"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);"
-} \ No newline at end of file
diff --git a/apps/files_versions/l10n/mk.js b/apps/files_versions/l10n/mk.js
index 32bd8ea5140..0e23cb9241d 100644
--- a/apps/files_versions/l10n/mk.js
+++ b/apps/files_versions/l10n/mk.js
@@ -1,11 +1,29 @@
OC.L10N.register(
"files_versions",
{
- "Could not revert: %s" : "Не можев да го вратам: %s",
"Versions" : "Верзии",
- "Failed to revert {file} to revision {timestamp}." : "Не успеав да го вратам {file} на ревизијата {timestamp}.",
- "Restore" : "Врати",
- "No other versions available" : "Не постојат други верзии",
- "More versions..." : "Повеќе верзии..."
+ "This application automatically maintains older versions of files that are changed." : "Оваа апликација автоматски ги менаџира постарите верзии на датотеките кој се изменети.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Оваа апликација автоматски ги менаџира постарите верзии на датотеките кој се изменети. Кога е овозможена, скриена папка се креира во секој кориснички директориум и се користи за зачувување на постарите верзии на датотеките. Корисникот има можност да врати стара верзија од датотеката во секое време преку веб-интерфејсот, со што се преклопува постоечката верзија на датотеката. Апликацијата автоматски се грижи за верзиите за да не дојде случај да се искористи просторот за складирање на корисникот.\n\t\tПокрај истекот на верзиите, апликацијата за верзии нема да користи повеќе од 50% од тековно достапната квота за верзии. Ако зачуваните верзии ја надминат оваа граница, апликацијата ги брише најстарите верзии на датотеки сè додека не се дојде под ова ограничување. Повеќе информации се достапни во документацијата за верзии на датотеки.",
+ "Current version" : "Моментална верзија",
+ "Initial version" : "Првична верзија",
+ "You" : "Вас",
+ "Actions for version from {versionHumanExplicitDate}" : "Акции за верзии од {versionHumanExplicitDate}",
+ "Name this version" : "Додај име на оваа верзијата",
+ "Edit version name" : "Промени име на верзијата",
+ "Compare to current version" : "Спореди со моменталната верзија",
+ "Restore version" : "Врати ја оваа верзија",
+ "Download version" : "Преземи верзија",
+ "Delete version" : "Избриши верзија",
+ "Cancel" : "Откажи",
+ "Remove version name" : "Избриши име на верзијата",
+ "Save version name" : "Зачувај име на верзија",
+ "Version name" : "Име на верзијата",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Именуваните верзии опстојуваат и се исклучени од автоматско чистење кога вашата квота за складирање е полна.",
+ "Initial version restored" : "Почетната верзијата е вратена",
+ "Version restored" : "Верзијата е вратена",
+ "Could not restore version" : "Неможе да се врати верзијата",
+ "Could not set version label" : "Неможе да се постави ознака на верзијата",
+ "Could not delete version" : "Неможе да се избрише верзијата",
+ "File versions" : "Верзии на датотеки"
},
"nplurals=2; plural=(n % 10 == 1 && n % 100 != 11) ? 0 : 1;");
diff --git a/apps/files_versions/l10n/mk.json b/apps/files_versions/l10n/mk.json
index 2c7b148e89a..0193422f6a0 100644
--- a/apps/files_versions/l10n/mk.json
+++ b/apps/files_versions/l10n/mk.json
@@ -1,9 +1,27 @@
{ "translations": {
- "Could not revert: %s" : "Не можев да го вратам: %s",
"Versions" : "Верзии",
- "Failed to revert {file} to revision {timestamp}." : "Не успеав да го вратам {file} на ревизијата {timestamp}.",
- "Restore" : "Врати",
- "No other versions available" : "Не постојат други верзии",
- "More versions..." : "Повеќе верзии..."
+ "This application automatically maintains older versions of files that are changed." : "Оваа апликација автоматски ги менаџира постарите верзии на датотеките кој се изменети.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Оваа апликација автоматски ги менаџира постарите верзии на датотеките кој се изменети. Кога е овозможена, скриена папка се креира во секој кориснички директориум и се користи за зачувување на постарите верзии на датотеките. Корисникот има можност да врати стара верзија од датотеката во секое време преку веб-интерфејсот, со што се преклопува постоечката верзија на датотеката. Апликацијата автоматски се грижи за верзиите за да не дојде случај да се искористи просторот за складирање на корисникот.\n\t\tПокрај истекот на верзиите, апликацијата за верзии нема да користи повеќе од 50% од тековно достапната квота за верзии. Ако зачуваните верзии ја надминат оваа граница, апликацијата ги брише најстарите верзии на датотеки сè додека не се дојде под ова ограничување. Повеќе информации се достапни во документацијата за верзии на датотеки.",
+ "Current version" : "Моментална верзија",
+ "Initial version" : "Првична верзија",
+ "You" : "Вас",
+ "Actions for version from {versionHumanExplicitDate}" : "Акции за верзии од {versionHumanExplicitDate}",
+ "Name this version" : "Додај име на оваа верзијата",
+ "Edit version name" : "Промени име на верзијата",
+ "Compare to current version" : "Спореди со моменталната верзија",
+ "Restore version" : "Врати ја оваа верзија",
+ "Download version" : "Преземи верзија",
+ "Delete version" : "Избриши верзија",
+ "Cancel" : "Откажи",
+ "Remove version name" : "Избриши име на верзијата",
+ "Save version name" : "Зачувај име на верзија",
+ "Version name" : "Име на верзијата",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Именуваните верзии опстојуваат и се исклучени од автоматско чистење кога вашата квота за складирање е полна.",
+ "Initial version restored" : "Почетната верзијата е вратена",
+ "Version restored" : "Верзијата е вратена",
+ "Could not restore version" : "Неможе да се врати верзијата",
+ "Could not set version label" : "Неможе да се постави ознака на верзијата",
+ "Could not delete version" : "Неможе да се избрише верзијата",
+ "File versions" : "Верзии на датотеки"
},"pluralForm" :"nplurals=2; plural=(n % 10 == 1 && n % 100 != 11) ? 0 : 1;"
} \ No newline at end of file
diff --git a/apps/files_versions/l10n/ms_MY.js b/apps/files_versions/l10n/ms_MY.js
deleted file mode 100644
index 3edb05dd0ab..00000000000
--- a/apps/files_versions/l10n/ms_MY.js
+++ /dev/null
@@ -1,11 +0,0 @@
-OC.L10N.register(
- "files_versions",
- {
- "Could not revert: %s" : "Tidak dapat kembalikan: %s",
- "Versions" : "Versi",
- "Failed to revert {file} to revision {timestamp}." : "Gagal kembalikan {file} ke semakan {timestamp}.",
- "Restore" : "Pulihkan",
- "No other versions available" : "Tiada lagi versi lain",
- "More versions..." : "Lagi versi..."
-},
-"nplurals=1; plural=0;");
diff --git a/apps/files_versions/l10n/ms_MY.json b/apps/files_versions/l10n/ms_MY.json
deleted file mode 100644
index 3cd889353a9..00000000000
--- a/apps/files_versions/l10n/ms_MY.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{ "translations": {
- "Could not revert: %s" : "Tidak dapat kembalikan: %s",
- "Versions" : "Versi",
- "Failed to revert {file} to revision {timestamp}." : "Gagal kembalikan {file} ke semakan {timestamp}.",
- "Restore" : "Pulihkan",
- "No other versions available" : "Tiada lagi versi lain",
- "More versions..." : "Lagi versi..."
-},"pluralForm" :"nplurals=1; plural=0;"
-} \ No newline at end of file
diff --git a/apps/files_versions/l10n/nb.js b/apps/files_versions/l10n/nb.js
new file mode 100644
index 00000000000..16e91d78621
--- /dev/null
+++ b/apps/files_versions/l10n/nb.js
@@ -0,0 +1,28 @@
+OC.L10N.register(
+ "files_versions",
+ {
+ "Versions" : "Versjoner",
+ "This application automatically maintains older versions of files that are changed." : "Dette programmet vedlikeholder eldre versjoner av endrede filer.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Dette programmet opprettholder automatisk eldre versjoner av filer som endres. Når den er aktivert, klargjøres en mappe for skjulte versjoner i hver brukers katalog og brukes til å lagre gamle filversjoner. En bruker kan når som helst gå tilbake til en eldre versjon via webgrensesnittet, og den erstattede filen blir en versjon. Appen administrerer automatisk versjonsmappen for å sikre at brukeren ikke går tom for kvote på grunn av versjoner.\n\t\tI tillegg til at versjoner opphører, sørger versjonsappen for aldri å bruke mer enn 50% av brukerens tilgjengelige ledige plass. Hvis lagrede versjoner overskrider denne grensen, sletter appen de eldste versjonene først til den oppfyller denne grensen. Du finner mer informasjon i dokumentasjonen for Versions.",
+ "Current version" : "Nåværende versjon",
+ "Initial version" : "Opprinnelig versjon",
+ "You" : "Du",
+ "Actions for version from {versionHumanExplicitDate}" : "Handlinger for versjon fra {versionHumanExplicitDate}",
+ "Name this version" : "Navngi denne versjonen",
+ "Edit version name" : "Rediger versjonsnavn",
+ "Compare to current version" : "Sammenlign med gjeldende versjon",
+ "Restore version" : "Gjenopprett versjon",
+ "Download version" : "Last ned versjon",
+ "Delete version" : "Slett versjon",
+ "Cancel" : "Avbryt",
+ "Remove version name" : "Fjern versjonsnavn",
+ "Save version name" : "Lagre versjonsnavn",
+ "Version name" : "Versjonsnavn",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Navngitte versjoner beholdes og utelates fra automatiske oppryddinger når lagringskvoten er full.",
+ "Initial version restored" : "Opprinnelig versjon opprettet",
+ "Version restored" : "Versjon gjenopprettet",
+ "Could not restore version" : "Kunne ikke gjenopprette versjon",
+ "Could not set version label" : "Kunne ikke angi versjonsetikett",
+ "Could not delete version" : "Kunne ikke slette versjon"
+},
+"nplurals=2; plural=(n != 1);");
diff --git a/apps/files_versions/l10n/nb.json b/apps/files_versions/l10n/nb.json
new file mode 100644
index 00000000000..a08659e07ac
--- /dev/null
+++ b/apps/files_versions/l10n/nb.json
@@ -0,0 +1,26 @@
+{ "translations": {
+ "Versions" : "Versjoner",
+ "This application automatically maintains older versions of files that are changed." : "Dette programmet vedlikeholder eldre versjoner av endrede filer.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Dette programmet opprettholder automatisk eldre versjoner av filer som endres. Når den er aktivert, klargjøres en mappe for skjulte versjoner i hver brukers katalog og brukes til å lagre gamle filversjoner. En bruker kan når som helst gå tilbake til en eldre versjon via webgrensesnittet, og den erstattede filen blir en versjon. Appen administrerer automatisk versjonsmappen for å sikre at brukeren ikke går tom for kvote på grunn av versjoner.\n\t\tI tillegg til at versjoner opphører, sørger versjonsappen for aldri å bruke mer enn 50% av brukerens tilgjengelige ledige plass. Hvis lagrede versjoner overskrider denne grensen, sletter appen de eldste versjonene først til den oppfyller denne grensen. Du finner mer informasjon i dokumentasjonen for Versions.",
+ "Current version" : "Nåværende versjon",
+ "Initial version" : "Opprinnelig versjon",
+ "You" : "Du",
+ "Actions for version from {versionHumanExplicitDate}" : "Handlinger for versjon fra {versionHumanExplicitDate}",
+ "Name this version" : "Navngi denne versjonen",
+ "Edit version name" : "Rediger versjonsnavn",
+ "Compare to current version" : "Sammenlign med gjeldende versjon",
+ "Restore version" : "Gjenopprett versjon",
+ "Download version" : "Last ned versjon",
+ "Delete version" : "Slett versjon",
+ "Cancel" : "Avbryt",
+ "Remove version name" : "Fjern versjonsnavn",
+ "Save version name" : "Lagre versjonsnavn",
+ "Version name" : "Versjonsnavn",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Navngitte versjoner beholdes og utelates fra automatiske oppryddinger når lagringskvoten er full.",
+ "Initial version restored" : "Opprinnelig versjon opprettet",
+ "Version restored" : "Versjon gjenopprettet",
+ "Could not restore version" : "Kunne ikke gjenopprette versjon",
+ "Could not set version label" : "Kunne ikke angi versjonsetikett",
+ "Could not delete version" : "Kunne ikke slette versjon"
+},"pluralForm" :"nplurals=2; plural=(n != 1);"
+} \ No newline at end of file
diff --git a/apps/files_versions/l10n/nb_NO.js b/apps/files_versions/l10n/nb_NO.js
deleted file mode 100644
index 8fd1fbc8409..00000000000
--- a/apps/files_versions/l10n/nb_NO.js
+++ /dev/null
@@ -1,11 +0,0 @@
-OC.L10N.register(
- "files_versions",
- {
- "Could not revert: %s" : "Klarte ikke å tilbakeføre: %s",
- "Versions" : "Versjoner",
- "Failed to revert {file} to revision {timestamp}." : "Klarte ikke å tilbakeføre {file} til revisjon {timestamp}.",
- "Restore" : "Gjenopprett",
- "No other versions available" : "Det finnes ingen andre versjoner",
- "More versions..." : "Flere versjoner"
-},
-"nplurals=2; plural=(n != 1);");
diff --git a/apps/files_versions/l10n/nb_NO.json b/apps/files_versions/l10n/nb_NO.json
deleted file mode 100644
index c68a10c8728..00000000000
--- a/apps/files_versions/l10n/nb_NO.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{ "translations": {
- "Could not revert: %s" : "Klarte ikke å tilbakeføre: %s",
- "Versions" : "Versjoner",
- "Failed to revert {file} to revision {timestamp}." : "Klarte ikke å tilbakeføre {file} til revisjon {timestamp}.",
- "Restore" : "Gjenopprett",
- "No other versions available" : "Det finnes ingen andre versjoner",
- "More versions..." : "Flere versjoner"
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
-} \ No newline at end of file
diff --git a/apps/files_versions/l10n/nl.js b/apps/files_versions/l10n/nl.js
index 14f7f06852a..7500ff8b9f4 100644
--- a/apps/files_versions/l10n/nl.js
+++ b/apps/files_versions/l10n/nl.js
@@ -1,11 +1,29 @@
OC.L10N.register(
"files_versions",
{
- "Could not revert: %s" : "Kon niet terugdraaien: %s",
"Versions" : "Versies",
- "Failed to revert {file} to revision {timestamp}." : "Kon {file} niet terugdraaien naar revisie {timestamp}.",
- "Restore" : "Herstellen",
- "No other versions available" : "Geen andere versies beschikbaar",
- "More versions..." : "Meer versies..."
+ "This application automatically maintains older versions of files that are changed." : "Deze applicatie beheert automatisch oudere versies van gewijzigde bestanden.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Deze applicatie bewaart automatisch oudere versies van bestanden die gewijzigd zijn. Als dit is ingeschakeld, wordt er in de map van elke gebruiker een map met verborgen versies gemaakt waarin oude bestandsversies worden opgeslagen. Een gebruiker kan op elk moment teruggaan naar een oudere versie via de webinterface, waarbij het vervangen bestand een versie wordt. De app beheert de versie map automatisch om ervoor te zorgen dat het account niet zonder quota komt te zitten door versies.\n\t\tNaast het verlopen van versies zorgt de versies-app ervoor dat nooit meer dan 50% van de op dat moment beschikbare vrije ruimte van het account wordt gebruikt. Als opgeslagen versies deze limiet overschrijden, zal de app eerst de oudste versies verwijderen totdat deze limiet is bereikt. Meer informatie is beschikbaar in de documentatie over versies.",
+ "Current version" : "Huidige versie",
+ "Initial version" : "Initiële versie",
+ "You" : "Jij",
+ "Actions for version from {versionHumanExplicitDate}" : "Acties voor versie van {versionHumanExplicitDate}",
+ "Name this version" : "Geef deze versie een naam",
+ "Edit version name" : "Bewerk versie naam",
+ "Compare to current version" : "Vergelijk met huidige versie",
+ "Restore version" : "Herstel versie",
+ "Download version" : "Download versie",
+ "Delete version" : "Verwijder versie",
+ "Cancel" : "Annuleren",
+ "Remove version name" : "Verwijder versie naam",
+ "Save version name" : "Sla versie naam op",
+ "Version name" : "Versie naam",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Versies met naam worden bewaard en uitgesloten van automatische opschoning wanneer je opslagquota vol is.",
+ "Initial version restored" : "Initiële versie hersteld",
+ "Version restored" : "Versie hersteld",
+ "Could not restore version" : "Kon versie niet herstellen",
+ "Could not set version label" : "Kon versie label niet instellen",
+ "Could not delete version" : "Kon versie niet verwijderen",
+ "File versions" : "Bestandsversies"
},
"nplurals=2; plural=(n != 1);");
diff --git a/apps/files_versions/l10n/nl.json b/apps/files_versions/l10n/nl.json
index 400b9d46730..da0eb03c679 100644
--- a/apps/files_versions/l10n/nl.json
+++ b/apps/files_versions/l10n/nl.json
@@ -1,9 +1,27 @@
{ "translations": {
- "Could not revert: %s" : "Kon niet terugdraaien: %s",
"Versions" : "Versies",
- "Failed to revert {file} to revision {timestamp}." : "Kon {file} niet terugdraaien naar revisie {timestamp}.",
- "Restore" : "Herstellen",
- "No other versions available" : "Geen andere versies beschikbaar",
- "More versions..." : "Meer versies..."
+ "This application automatically maintains older versions of files that are changed." : "Deze applicatie beheert automatisch oudere versies van gewijzigde bestanden.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Deze applicatie bewaart automatisch oudere versies van bestanden die gewijzigd zijn. Als dit is ingeschakeld, wordt er in de map van elke gebruiker een map met verborgen versies gemaakt waarin oude bestandsversies worden opgeslagen. Een gebruiker kan op elk moment teruggaan naar een oudere versie via de webinterface, waarbij het vervangen bestand een versie wordt. De app beheert de versie map automatisch om ervoor te zorgen dat het account niet zonder quota komt te zitten door versies.\n\t\tNaast het verlopen van versies zorgt de versies-app ervoor dat nooit meer dan 50% van de op dat moment beschikbare vrije ruimte van het account wordt gebruikt. Als opgeslagen versies deze limiet overschrijden, zal de app eerst de oudste versies verwijderen totdat deze limiet is bereikt. Meer informatie is beschikbaar in de documentatie over versies.",
+ "Current version" : "Huidige versie",
+ "Initial version" : "Initiële versie",
+ "You" : "Jij",
+ "Actions for version from {versionHumanExplicitDate}" : "Acties voor versie van {versionHumanExplicitDate}",
+ "Name this version" : "Geef deze versie een naam",
+ "Edit version name" : "Bewerk versie naam",
+ "Compare to current version" : "Vergelijk met huidige versie",
+ "Restore version" : "Herstel versie",
+ "Download version" : "Download versie",
+ "Delete version" : "Verwijder versie",
+ "Cancel" : "Annuleren",
+ "Remove version name" : "Verwijder versie naam",
+ "Save version name" : "Sla versie naam op",
+ "Version name" : "Versie naam",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Versies met naam worden bewaard en uitgesloten van automatische opschoning wanneer je opslagquota vol is.",
+ "Initial version restored" : "Initiële versie hersteld",
+ "Version restored" : "Versie hersteld",
+ "Could not restore version" : "Kon versie niet herstellen",
+ "Could not set version label" : "Kon versie label niet instellen",
+ "Could not delete version" : "Kon versie niet verwijderen",
+ "File versions" : "Bestandsversies"
},"pluralForm" :"nplurals=2; plural=(n != 1);"
} \ No newline at end of file
diff --git a/apps/files_versions/l10n/nn_NO.js b/apps/files_versions/l10n/nn_NO.js
deleted file mode 100644
index f83901dfb6b..00000000000
--- a/apps/files_versions/l10n/nn_NO.js
+++ /dev/null
@@ -1,11 +0,0 @@
-OC.L10N.register(
- "files_versions",
- {
- "Could not revert: %s" : "Klarte ikkje å tilbakestilla: %s",
- "Versions" : "Utgåver",
- "Failed to revert {file} to revision {timestamp}." : "Klarte ikkje å tilbakestilla {file} til utgåva {timestamp}.",
- "Restore" : "Gjenopprett",
- "No other versions available" : "Ingen andre utgåver tilgjengeleg",
- "More versions..." : "Fleire utgåver …"
-},
-"nplurals=2; plural=(n != 1);");
diff --git a/apps/files_versions/l10n/nn_NO.json b/apps/files_versions/l10n/nn_NO.json
deleted file mode 100644
index 7fb0b12986e..00000000000
--- a/apps/files_versions/l10n/nn_NO.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{ "translations": {
- "Could not revert: %s" : "Klarte ikkje å tilbakestilla: %s",
- "Versions" : "Utgåver",
- "Failed to revert {file} to revision {timestamp}." : "Klarte ikkje å tilbakestilla {file} til utgåva {timestamp}.",
- "Restore" : "Gjenopprett",
- "No other versions available" : "Ingen andre utgåver tilgjengeleg",
- "More versions..." : "Fleire utgåver …"
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
-} \ No newline at end of file
diff --git a/apps/files_versions/l10n/oc.js b/apps/files_versions/l10n/oc.js
deleted file mode 100644
index 7dbb7a243fd..00000000000
--- a/apps/files_versions/l10n/oc.js
+++ /dev/null
@@ -1,11 +0,0 @@
-OC.L10N.register(
- "files_versions",
- {
- "Could not revert: %s" : "Impossible de restablir %s",
- "Versions" : "Versions",
- "Failed to revert {file} to revision {timestamp}." : "Fracàs del retorn del fichièr {file} a la revision {timestamp}.",
- "Restore" : "Restablir",
- "No other versions available" : "Cap d'autra version es pas disponibla",
- "More versions..." : "Mai de versions..."
-},
-"nplurals=2; plural=(n > 1);");
diff --git a/apps/files_versions/l10n/oc.json b/apps/files_versions/l10n/oc.json
deleted file mode 100644
index a85d32b650a..00000000000
--- a/apps/files_versions/l10n/oc.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{ "translations": {
- "Could not revert: %s" : "Impossible de restablir %s",
- "Versions" : "Versions",
- "Failed to revert {file} to revision {timestamp}." : "Fracàs del retorn del fichièr {file} a la revision {timestamp}.",
- "Restore" : "Restablir",
- "No other versions available" : "Cap d'autra version es pas disponibla",
- "More versions..." : "Mai de versions..."
-},"pluralForm" :"nplurals=2; plural=(n > 1);"
-} \ No newline at end of file
diff --git a/apps/files_versions/l10n/pl.js b/apps/files_versions/l10n/pl.js
index f93cfc4845e..5ff9d71c737 100644
--- a/apps/files_versions/l10n/pl.js
+++ b/apps/files_versions/l10n/pl.js
@@ -1,11 +1,29 @@
OC.L10N.register(
"files_versions",
{
- "Could not revert: %s" : "Nie można było przywrócić: %s",
"Versions" : "Wersje",
- "Failed to revert {file} to revision {timestamp}." : "Nie udało się przywrócić {file} do wersji z {timestamp}.",
- "Restore" : "Przywróć",
- "No other versions available" : "Nie są dostępne żadne inne wersje",
- "More versions..." : "Więcej wersji..."
+ "This application automatically maintains older versions of files that are changed." : "Aplikacja przechowuje starsze wersje plików poddanych modyfikacji.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Ta aplikacja automatycznie utrzymuje starsze wersje plików, które zostały zmienione. Po włączeniu, w katalogu każdego użytkownika udostępniany jest katalog wersji ukrytych, który służy do przechowywania starych wersji plików. Użytkownik może w dowolnym momencie powrócić do starszej wersji za pośrednictwem interfejsu internetowego, a zastąpiony plik staje się wersją. Aplikacja automatycznie zarządza katalogiem wersji, aby mieć pewność, że na koncie nie zabraknie miejsca z powodu wersji.\n\t\tOprócz wygaszania wersji aplikacja sprawdza, by nigdy nie zostało wykorzystane więcej niż 50% aktualnie dostępnego wolnego miejsca na koncie. Jeśli przechowywane wersje przekroczą ten limit, aplikacja najpierw usunie najstarsze wersje, aż do osiągnięcia tego limitu. Więcej informacji można znaleźć w dokumentacji aplikacji wersji.",
+ "Current version" : "Obecna wersja",
+ "Initial version" : "Początkowa wersja",
+ "You" : "Ty",
+ "Actions for version from {versionHumanExplicitDate}" : "Działania dla wersji z {versionHumanExplicitDate}",
+ "Name this version" : "Nazwij tę wersję",
+ "Edit version name" : "Edytuj nazwę wersji",
+ "Compare to current version" : "Porównaj z aktualną wersją",
+ "Restore version" : "Przywróć wersję",
+ "Download version" : "Pobierz wersję",
+ "Delete version" : "Usuń wersję",
+ "Cancel" : "Anuluj",
+ "Remove version name" : "Usuń nazwę wersji",
+ "Save version name" : "Zapis nazwę wersji",
+ "Version name" : "Nazwa wersji",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Nazwane wersje są utrwalane i wykluczane z automatycznego czyszczenia, gdy limit przechowywania zostanie przekroczony.",
+ "Initial version restored" : "Przywrócono wersję początkową",
+ "Version restored" : "Wersja przywrócona",
+ "Could not restore version" : "Nie można przywrócić wersji",
+ "Could not set version label" : "Nie można ustawić etykiety wersji",
+ "Could not delete version" : "Nie można usunąć wersji",
+ "File versions" : "Wersje plików"
},
-"nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);");
+"nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);");
diff --git a/apps/files_versions/l10n/pl.json b/apps/files_versions/l10n/pl.json
index 974ba809b0a..27030065c9e 100644
--- a/apps/files_versions/l10n/pl.json
+++ b/apps/files_versions/l10n/pl.json
@@ -1,9 +1,27 @@
{ "translations": {
- "Could not revert: %s" : "Nie można było przywrócić: %s",
"Versions" : "Wersje",
- "Failed to revert {file} to revision {timestamp}." : "Nie udało się przywrócić {file} do wersji z {timestamp}.",
- "Restore" : "Przywróć",
- "No other versions available" : "Nie są dostępne żadne inne wersje",
- "More versions..." : "Więcej wersji..."
-},"pluralForm" :"nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"
+ "This application automatically maintains older versions of files that are changed." : "Aplikacja przechowuje starsze wersje plików poddanych modyfikacji.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Ta aplikacja automatycznie utrzymuje starsze wersje plików, które zostały zmienione. Po włączeniu, w katalogu każdego użytkownika udostępniany jest katalog wersji ukrytych, który służy do przechowywania starych wersji plików. Użytkownik może w dowolnym momencie powrócić do starszej wersji za pośrednictwem interfejsu internetowego, a zastąpiony plik staje się wersją. Aplikacja automatycznie zarządza katalogiem wersji, aby mieć pewność, że na koncie nie zabraknie miejsca z powodu wersji.\n\t\tOprócz wygaszania wersji aplikacja sprawdza, by nigdy nie zostało wykorzystane więcej niż 50% aktualnie dostępnego wolnego miejsca na koncie. Jeśli przechowywane wersje przekroczą ten limit, aplikacja najpierw usunie najstarsze wersje, aż do osiągnięcia tego limitu. Więcej informacji można znaleźć w dokumentacji aplikacji wersji.",
+ "Current version" : "Obecna wersja",
+ "Initial version" : "Początkowa wersja",
+ "You" : "Ty",
+ "Actions for version from {versionHumanExplicitDate}" : "Działania dla wersji z {versionHumanExplicitDate}",
+ "Name this version" : "Nazwij tę wersję",
+ "Edit version name" : "Edytuj nazwę wersji",
+ "Compare to current version" : "Porównaj z aktualną wersją",
+ "Restore version" : "Przywróć wersję",
+ "Download version" : "Pobierz wersję",
+ "Delete version" : "Usuń wersję",
+ "Cancel" : "Anuluj",
+ "Remove version name" : "Usuń nazwę wersji",
+ "Save version name" : "Zapis nazwę wersji",
+ "Version name" : "Nazwa wersji",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Nazwane wersje są utrwalane i wykluczane z automatycznego czyszczenia, gdy limit przechowywania zostanie przekroczony.",
+ "Initial version restored" : "Przywrócono wersję początkową",
+ "Version restored" : "Wersja przywrócona",
+ "Could not restore version" : "Nie można przywrócić wersji",
+ "Could not set version label" : "Nie można ustawić etykiety wersji",
+ "Could not delete version" : "Nie można usunąć wersji",
+ "File versions" : "Wersje plików"
+},"pluralForm" :"nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);"
} \ No newline at end of file
diff --git a/apps/files_versions/l10n/pt_BR.js b/apps/files_versions/l10n/pt_BR.js
index 9c3bce45e9b..bb0880450a1 100644
--- a/apps/files_versions/l10n/pt_BR.js
+++ b/apps/files_versions/l10n/pt_BR.js
@@ -1,11 +1,29 @@
OC.L10N.register(
"files_versions",
{
- "Could not revert: %s" : "Impossível reverter: %s",
"Versions" : "Versões",
- "Failed to revert {file} to revision {timestamp}." : "Falha ao reverter {file} para a revisão {timestamp}.",
- "Restore" : "Restaurar",
- "No other versions available" : "Nenhuma outra versão disponível",
- "More versions..." : "Mais versões..."
+ "This application automatically maintains older versions of files that are changed." : "Este aplicativo mantém automaticamente versões mais antigas de arquivos que são alterados.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Este aplicativo mantém automaticamente versões mais antigas de arquivos que foram alterados. Quando ativada, uma pasta de versões oculta é provisionada no diretório de cada usuário e usada para armazenar versões antigas de arquivos. Um usuário pode reverter para uma versão mais antiga através da interface web a qualquer momento, com o arquivo substituído se tornando uma versão. O aplicativo gerencia automaticamente a pasta de versões para garantir que a conta não fique sem cota por causa das versões. \n\t\tAlém da expiração das versões, o aplicativo de versões garante nunca usar mais de 50% do espaço livre disponível atualmente na conta. Se as versões armazenadas excederem esse limite, o aplicativo excluirá primeiro as versões mais antigas até atingir esse limite. Mais informações estão disponíveis na documentação de Versões.",
+ "Current version" : "Versão atual",
+ "Initial version" : "Versão inicial",
+ "You" : "Você",
+ "Actions for version from {versionHumanExplicitDate}" : "Ações para versão de {versionHumanExplicitDate}",
+ "Name this version" : "Nomear esta versão",
+ "Edit version name" : "Editar nome da versão",
+ "Compare to current version" : "Comparar com a versão atual",
+ "Restore version" : "Restaurar versão",
+ "Download version" : "Baixar versão",
+ "Delete version" : "Excluir versão",
+ "Cancel" : "Cancelar",
+ "Remove version name" : "Remover nome da versão",
+ "Save version name" : "Salvar nome da versão",
+ "Version name" : "Nome da versão",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "As versões nomeadas são mantidas e excluídas das limpezas automáticas quando sua cota de armazenamento está cheia.",
+ "Initial version restored" : "Versão inicial restaurada",
+ "Version restored" : "Versão restaurada",
+ "Could not restore version" : "Não foi possível restaurar esta versão",
+ "Could not set version label" : "Não foi possível definir o rótulo da versão",
+ "Could not delete version" : "Não foi possível excluir a versão",
+ "File versions" : "Versões do arquivo"
},
-"nplurals=2; plural=(n > 1);");
+"nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;");
diff --git a/apps/files_versions/l10n/pt_BR.json b/apps/files_versions/l10n/pt_BR.json
index b54c27c6b46..b213f3724a8 100644
--- a/apps/files_versions/l10n/pt_BR.json
+++ b/apps/files_versions/l10n/pt_BR.json
@@ -1,9 +1,27 @@
{ "translations": {
- "Could not revert: %s" : "Impossível reverter: %s",
"Versions" : "Versões",
- "Failed to revert {file} to revision {timestamp}." : "Falha ao reverter {file} para a revisão {timestamp}.",
- "Restore" : "Restaurar",
- "No other versions available" : "Nenhuma outra versão disponível",
- "More versions..." : "Mais versões..."
-},"pluralForm" :"nplurals=2; plural=(n > 1);"
+ "This application automatically maintains older versions of files that are changed." : "Este aplicativo mantém automaticamente versões mais antigas de arquivos que são alterados.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Este aplicativo mantém automaticamente versões mais antigas de arquivos que foram alterados. Quando ativada, uma pasta de versões oculta é provisionada no diretório de cada usuário e usada para armazenar versões antigas de arquivos. Um usuário pode reverter para uma versão mais antiga através da interface web a qualquer momento, com o arquivo substituído se tornando uma versão. O aplicativo gerencia automaticamente a pasta de versões para garantir que a conta não fique sem cota por causa das versões. \n\t\tAlém da expiração das versões, o aplicativo de versões garante nunca usar mais de 50% do espaço livre disponível atualmente na conta. Se as versões armazenadas excederem esse limite, o aplicativo excluirá primeiro as versões mais antigas até atingir esse limite. Mais informações estão disponíveis na documentação de Versões.",
+ "Current version" : "Versão atual",
+ "Initial version" : "Versão inicial",
+ "You" : "Você",
+ "Actions for version from {versionHumanExplicitDate}" : "Ações para versão de {versionHumanExplicitDate}",
+ "Name this version" : "Nomear esta versão",
+ "Edit version name" : "Editar nome da versão",
+ "Compare to current version" : "Comparar com a versão atual",
+ "Restore version" : "Restaurar versão",
+ "Download version" : "Baixar versão",
+ "Delete version" : "Excluir versão",
+ "Cancel" : "Cancelar",
+ "Remove version name" : "Remover nome da versão",
+ "Save version name" : "Salvar nome da versão",
+ "Version name" : "Nome da versão",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "As versões nomeadas são mantidas e excluídas das limpezas automáticas quando sua cota de armazenamento está cheia.",
+ "Initial version restored" : "Versão inicial restaurada",
+ "Version restored" : "Versão restaurada",
+ "Could not restore version" : "Não foi possível restaurar esta versão",
+ "Could not set version label" : "Não foi possível definir o rótulo da versão",
+ "Could not delete version" : "Não foi possível excluir a versão",
+ "File versions" : "Versões do arquivo"
+},"pluralForm" :"nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"
} \ No newline at end of file
diff --git a/apps/files_versions/l10n/pt_PT.js b/apps/files_versions/l10n/pt_PT.js
deleted file mode 100644
index cb058c8b02e..00000000000
--- a/apps/files_versions/l10n/pt_PT.js
+++ /dev/null
@@ -1,11 +0,0 @@
-OC.L10N.register(
- "files_versions",
- {
- "Could not revert: %s" : "Não foi possível reverter: %s",
- "Versions" : "Versões",
- "Failed to revert {file} to revision {timestamp}." : "Falhou a recuperação do ficheiro {file} para a revisão {timestamp}.",
- "Restore" : "Restaurar",
- "No other versions available" : "Não existem versões mais antigas",
- "More versions..." : "Mais versões..."
-},
-"nplurals=2; plural=(n != 1);");
diff --git a/apps/files_versions/l10n/pt_PT.json b/apps/files_versions/l10n/pt_PT.json
deleted file mode 100644
index 7dc6828a72f..00000000000
--- a/apps/files_versions/l10n/pt_PT.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{ "translations": {
- "Could not revert: %s" : "Não foi possível reverter: %s",
- "Versions" : "Versões",
- "Failed to revert {file} to revision {timestamp}." : "Falhou a recuperação do ficheiro {file} para a revisão {timestamp}.",
- "Restore" : "Restaurar",
- "No other versions available" : "Não existem versões mais antigas",
- "More versions..." : "Mais versões..."
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
-} \ No newline at end of file
diff --git a/apps/files_versions/l10n/ro.js b/apps/files_versions/l10n/ro.js
deleted file mode 100644
index 815c04e6519..00000000000
--- a/apps/files_versions/l10n/ro.js
+++ /dev/null
@@ -1,10 +0,0 @@
-OC.L10N.register(
- "files_versions",
- {
- "Could not revert: %s" : "Nu a putut reveni: %s",
- "Versions" : "Versiuni",
- "Restore" : "Restabilire",
- "No other versions available" : "Nu există alte versiuni disponibile",
- "More versions..." : "Mai multe versiuni..."
-},
-"nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?2:1));");
diff --git a/apps/files_versions/l10n/ro.json b/apps/files_versions/l10n/ro.json
deleted file mode 100644
index 78a4ecc4e7c..00000000000
--- a/apps/files_versions/l10n/ro.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{ "translations": {
- "Could not revert: %s" : "Nu a putut reveni: %s",
- "Versions" : "Versiuni",
- "Restore" : "Restabilire",
- "No other versions available" : "Nu există alte versiuni disponibile",
- "More versions..." : "Mai multe versiuni..."
-},"pluralForm" :"nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?2:1));"
-} \ No newline at end of file
diff --git a/apps/files_versions/l10n/ru.js b/apps/files_versions/l10n/ru.js
index 7809e4f1190..ecdf5913c52 100644
--- a/apps/files_versions/l10n/ru.js
+++ b/apps/files_versions/l10n/ru.js
@@ -1,11 +1,29 @@
OC.L10N.register(
"files_versions",
{
- "Could not revert: %s" : "Невозможно откатить: %s",
"Versions" : "Версии",
- "Failed to revert {file} to revision {timestamp}." : "Не удалось откатить {file} к ревизии {timestamp}.",
- "Restore" : "Откатить",
- "No other versions available" : "Других версий не доступно",
- "More versions..." : "Ещё версии..."
+ "This application automatically maintains older versions of files that are changed." : "Приложение служит для обработки старых версий изменённых файлов.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Это приложение автоматически поддерживает старые версии файлов, которые были изменены. Если оно включено, в каталоге каждого пользователя создаётся папка \"Скрытые версии\", которая используется для хранения старых версий файлов. Пользователь может в любой момент вернуться к более старой версии через веб-интерфейс, при этом заменённый файл станет версией. Приложение автоматически управляет папкой версий, чтобы гарантировать, что квота учётной записи не исчерпается из-за изменений версий.\n\t\tВ дополнение к истечению срока действия версий, приложение \"Версии\" гарантирует, что вы никогда не будете использовать более 50% доступного в данный момент свободного места в учётной записи. Если сохранённые версии превысят это ограничение, приложение сначала удалит самые старые версии, пока не достигнет этого ограничения. Более подробная информация доступна в документации по версиям.",
+ "Current version" : "Текущая версия",
+ "Initial version" : "Первоначальная версия",
+ "You" : "Вы",
+ "Actions for version from {versionHumanExplicitDate}" : "Действия для версии из {versionHumanExplicitDate}",
+ "Name this version" : "Обозначить версию",
+ "Edit version name" : "Изменить название версии",
+ "Compare to current version" : "Сравнить с текущей версией",
+ "Restore version" : "Востановить версию",
+ "Download version" : "Скачать версию",
+ "Delete version" : "Удалить версию",
+ "Cancel" : "Отмена",
+ "Remove version name" : "Удалить название версии",
+ "Save version name" : "Сохранить название версии",
+ "Version name" : "Название версии",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Именованные версии исключены из автоматического удаления старых версий файлов при исчерпании квоты дискового пространства.",
+ "Initial version restored" : "Восстановлена первоначальная версия",
+ "Version restored" : "Версия восстановлена",
+ "Could not restore version" : "Не удалось восстановить версию",
+ "Could not set version label" : "Не удалось установить метку версии",
+ "Could not delete version" : "Не удалось удалить версию",
+ "File versions" : "Версии файлов"
},
"nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);");
diff --git a/apps/files_versions/l10n/ru.json b/apps/files_versions/l10n/ru.json
index 595d2504319..96392c99021 100644
--- a/apps/files_versions/l10n/ru.json
+++ b/apps/files_versions/l10n/ru.json
@@ -1,9 +1,27 @@
{ "translations": {
- "Could not revert: %s" : "Невозможно откатить: %s",
"Versions" : "Версии",
- "Failed to revert {file} to revision {timestamp}." : "Не удалось откатить {file} к ревизии {timestamp}.",
- "Restore" : "Откатить",
- "No other versions available" : "Других версий не доступно",
- "More versions..." : "Ещё версии..."
+ "This application automatically maintains older versions of files that are changed." : "Приложение служит для обработки старых версий изменённых файлов.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Это приложение автоматически поддерживает старые версии файлов, которые были изменены. Если оно включено, в каталоге каждого пользователя создаётся папка \"Скрытые версии\", которая используется для хранения старых версий файлов. Пользователь может в любой момент вернуться к более старой версии через веб-интерфейс, при этом заменённый файл станет версией. Приложение автоматически управляет папкой версий, чтобы гарантировать, что квота учётной записи не исчерпается из-за изменений версий.\n\t\tВ дополнение к истечению срока действия версий, приложение \"Версии\" гарантирует, что вы никогда не будете использовать более 50% доступного в данный момент свободного места в учётной записи. Если сохранённые версии превысят это ограничение, приложение сначала удалит самые старые версии, пока не достигнет этого ограничения. Более подробная информация доступна в документации по версиям.",
+ "Current version" : "Текущая версия",
+ "Initial version" : "Первоначальная версия",
+ "You" : "Вы",
+ "Actions for version from {versionHumanExplicitDate}" : "Действия для версии из {versionHumanExplicitDate}",
+ "Name this version" : "Обозначить версию",
+ "Edit version name" : "Изменить название версии",
+ "Compare to current version" : "Сравнить с текущей версией",
+ "Restore version" : "Востановить версию",
+ "Download version" : "Скачать версию",
+ "Delete version" : "Удалить версию",
+ "Cancel" : "Отмена",
+ "Remove version name" : "Удалить название версии",
+ "Save version name" : "Сохранить название версии",
+ "Version name" : "Название версии",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Именованные версии исключены из автоматического удаления старых версий файлов при исчерпании квоты дискового пространства.",
+ "Initial version restored" : "Восстановлена первоначальная версия",
+ "Version restored" : "Версия восстановлена",
+ "Could not restore version" : "Не удалось восстановить версию",
+ "Could not set version label" : "Не удалось установить метку версии",
+ "Could not delete version" : "Не удалось удалить версию",
+ "File versions" : "Версии файлов"
},"pluralForm" :"nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);"
} \ No newline at end of file
diff --git a/apps/files_versions/l10n/si_LK.js b/apps/files_versions/l10n/si_LK.js
deleted file mode 100644
index 1d7ab4ef254..00000000000
--- a/apps/files_versions/l10n/si_LK.js
+++ /dev/null
@@ -1,6 +0,0 @@
-OC.L10N.register(
- "files_versions",
- {
- "Versions" : "අනුවාද"
-},
-"nplurals=2; plural=(n != 1);");
diff --git a/apps/files_versions/l10n/si_LK.json b/apps/files_versions/l10n/si_LK.json
deleted file mode 100644
index 161b51d25a4..00000000000
--- a/apps/files_versions/l10n/si_LK.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{ "translations": {
- "Versions" : "අනුවාද"
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
-} \ No newline at end of file
diff --git a/apps/files_versions/l10n/sk.js b/apps/files_versions/l10n/sk.js
new file mode 100644
index 00000000000..ff9ab4ed723
--- /dev/null
+++ b/apps/files_versions/l10n/sk.js
@@ -0,0 +1,29 @@
+OC.L10N.register(
+ "files_versions",
+ {
+ "Versions" : "Verzie",
+ "This application automatically maintains older versions of files that are changed." : "Táto aplikácia automaticky spravuje staršie verzie súborov, ktoré sú zmenené",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Táto aplikácia automaticky udržiava staršie verzie súborov, ktoré sú zmenené. Keď je táto možnosť povolená, v adresári každého užívateľa sa vytvorí adresáč skrytých verzií, ktorý sa používa na ukladanie starých verzií súborov. Užívateľ sa môže kedykoľvek vrátiť k staršej verzii prostredníctvom webového rozhrania, pričom nahradený súbor sa stane verziou. Aplikácia automaticky spravuje adresár verzií, aby sa zabezpečilo, že užívateľovi nevyčerpá kvóta kvôli verziám.\nOkrem vypršania platnosti verzií, aplikácia verzie zaisťuje, že nikdy nevyužije viac ako 50 % aktuálne dostupného voľného miesta užívateľa. Ak uložené verzie prekročia tento limit, aplikácia najskôr vymaže najstaršie verzie, kým tento limit nedosiahne. Ďalšie informácie sú k dispozícii v dokumentácii k verziám.",
+ "Current version" : "Aktuálna verzia",
+ "Initial version" : "Úvodná verzia",
+ "You" : "Vy",
+ "Actions for version from {versionHumanExplicitDate}" : "Akcie pre verziu od {versionHumanExplicitDate}",
+ "Name this version" : "Pomenovať túto verziu",
+ "Edit version name" : "Upraviť názov verzie",
+ "Compare to current version" : "Porovnať s aktuálnou verziou",
+ "Restore version" : "Obnoviť verziu",
+ "Download version" : "Stiahnuť verziu",
+ "Delete version" : "Odstrániť verziu",
+ "Cancel" : "Zrušiť",
+ "Remove version name" : "Odstrániť názov verzie",
+ "Save version name" : "Uložiť názov verzie",
+ "Version name" : "Názov verzie",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Pomenované verzie sa zachovajú a po naplnení kvóty úložiska budú vylúčené z automatického čistenia.",
+ "Initial version restored" : "Úvodná verzia bola obnovená",
+ "Version restored" : "Verzia bola obnovená",
+ "Could not restore version" : "Nepodarilo sa obnoviť verziu",
+ "Could not set version label" : "Nepodarilo sa nastaviť označenie verzie",
+ "Could not delete version" : "Nepodarilo sa odstrániť verziu",
+ "File versions" : "Verzie súboru"
+},
+"nplurals=4; plural=(n % 1 == 0 && n == 1 ? 0 : n % 1 == 0 && n >= 2 && n <= 4 ? 1 : n % 1 != 0 ? 2: 3);");
diff --git a/apps/files_versions/l10n/sk.json b/apps/files_versions/l10n/sk.json
new file mode 100644
index 00000000000..afe241520b7
--- /dev/null
+++ b/apps/files_versions/l10n/sk.json
@@ -0,0 +1,27 @@
+{ "translations": {
+ "Versions" : "Verzie",
+ "This application automatically maintains older versions of files that are changed." : "Táto aplikácia automaticky spravuje staršie verzie súborov, ktoré sú zmenené",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Táto aplikácia automaticky udržiava staršie verzie súborov, ktoré sú zmenené. Keď je táto možnosť povolená, v adresári každého užívateľa sa vytvorí adresáč skrytých verzií, ktorý sa používa na ukladanie starých verzií súborov. Užívateľ sa môže kedykoľvek vrátiť k staršej verzii prostredníctvom webového rozhrania, pričom nahradený súbor sa stane verziou. Aplikácia automaticky spravuje adresár verzií, aby sa zabezpečilo, že užívateľovi nevyčerpá kvóta kvôli verziám.\nOkrem vypršania platnosti verzií, aplikácia verzie zaisťuje, že nikdy nevyužije viac ako 50 % aktuálne dostupného voľného miesta užívateľa. Ak uložené verzie prekročia tento limit, aplikácia najskôr vymaže najstaršie verzie, kým tento limit nedosiahne. Ďalšie informácie sú k dispozícii v dokumentácii k verziám.",
+ "Current version" : "Aktuálna verzia",
+ "Initial version" : "Úvodná verzia",
+ "You" : "Vy",
+ "Actions for version from {versionHumanExplicitDate}" : "Akcie pre verziu od {versionHumanExplicitDate}",
+ "Name this version" : "Pomenovať túto verziu",
+ "Edit version name" : "Upraviť názov verzie",
+ "Compare to current version" : "Porovnať s aktuálnou verziou",
+ "Restore version" : "Obnoviť verziu",
+ "Download version" : "Stiahnuť verziu",
+ "Delete version" : "Odstrániť verziu",
+ "Cancel" : "Zrušiť",
+ "Remove version name" : "Odstrániť názov verzie",
+ "Save version name" : "Uložiť názov verzie",
+ "Version name" : "Názov verzie",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Pomenované verzie sa zachovajú a po naplnení kvóty úložiska budú vylúčené z automatického čistenia.",
+ "Initial version restored" : "Úvodná verzia bola obnovená",
+ "Version restored" : "Verzia bola obnovená",
+ "Could not restore version" : "Nepodarilo sa obnoviť verziu",
+ "Could not set version label" : "Nepodarilo sa nastaviť označenie verzie",
+ "Could not delete version" : "Nepodarilo sa odstrániť verziu",
+ "File versions" : "Verzie súboru"
+},"pluralForm" :"nplurals=4; plural=(n % 1 == 0 && n == 1 ? 0 : n % 1 == 0 && n >= 2 && n <= 4 ? 1 : n % 1 != 0 ? 2: 3);"
+} \ No newline at end of file
diff --git a/apps/files_versions/l10n/sk_SK.js b/apps/files_versions/l10n/sk_SK.js
deleted file mode 100644
index c4857784928..00000000000
--- a/apps/files_versions/l10n/sk_SK.js
+++ /dev/null
@@ -1,11 +0,0 @@
-OC.L10N.register(
- "files_versions",
- {
- "Could not revert: %s" : "Nemožno obnoviť: %s",
- "Versions" : "Verzie",
- "Failed to revert {file} to revision {timestamp}." : "Zlyhalo obnovenie súboru {file} na verziu {timestamp}.",
- "Restore" : "Obnoviť",
- "No other versions available" : "Žiadne ďalšie verzie nie sú dostupné",
- "More versions..." : "Viac verzií..."
-},
-"nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;");
diff --git a/apps/files_versions/l10n/sk_SK.json b/apps/files_versions/l10n/sk_SK.json
deleted file mode 100644
index 5ac48397699..00000000000
--- a/apps/files_versions/l10n/sk_SK.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{ "translations": {
- "Could not revert: %s" : "Nemožno obnoviť: %s",
- "Versions" : "Verzie",
- "Failed to revert {file} to revision {timestamp}." : "Zlyhalo obnovenie súboru {file} na verziu {timestamp}.",
- "Restore" : "Obnoviť",
- "No other versions available" : "Žiadne ďalšie verzie nie sú dostupné",
- "More versions..." : "Viac verzií..."
-},"pluralForm" :"nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;"
-} \ No newline at end of file
diff --git a/apps/files_versions/l10n/sl.js b/apps/files_versions/l10n/sl.js
index e04e39a3c62..0881c039161 100644
--- a/apps/files_versions/l10n/sl.js
+++ b/apps/files_versions/l10n/sl.js
@@ -1,11 +1,23 @@
OC.L10N.register(
"files_versions",
{
- "Could not revert: %s" : "Ni mogoče povrniti: %s",
"Versions" : "Različice",
- "Failed to revert {file} to revision {timestamp}." : "Povrnitev datoteke {file} na objavo {timestamp} je spodletelo.",
- "Restore" : "Obnovi",
- "No other versions available" : "Ni drugih različic",
- "More versions..." : "Več različic"
+ "This application automatically maintains older versions of files that are changed." : "Program samodejno ustvarja zaporedne različice sprememb datotek.",
+ "Current version" : "Trenutna različica",
+ "You" : "Jaz",
+ "Name this version" : "Poimenovanje razičice",
+ "Edit version name" : "Uredi ime različice",
+ "Restore version" : "Obnovi različico",
+ "Download version" : "Prejmi različico",
+ "Delete version" : "Izbriši različico",
+ "Cancel" : "Prekliči",
+ "Remove version name" : "Odstrani ime različice",
+ "Save version name" : "Shrani ime različice",
+ "Version name" : "Ime različice",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Poimenovane različice so trajne in niso vključene v samodejno čiščenje, če je prostora v oblaku malo.",
+ "Initial version restored" : "Začetna različica je obnovljena",
+ "Version restored" : "Različica je obnovljena",
+ "Could not restore version" : "Različice ni mogoče obnoviti",
+ "Could not delete version" : "Različice ji mogoče izbrisati"
},
"nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);");
diff --git a/apps/files_versions/l10n/sl.json b/apps/files_versions/l10n/sl.json
index a66e94e425d..21c2a0b2089 100644
--- a/apps/files_versions/l10n/sl.json
+++ b/apps/files_versions/l10n/sl.json
@@ -1,9 +1,21 @@
{ "translations": {
- "Could not revert: %s" : "Ni mogoče povrniti: %s",
"Versions" : "Različice",
- "Failed to revert {file} to revision {timestamp}." : "Povrnitev datoteke {file} na objavo {timestamp} je spodletelo.",
- "Restore" : "Obnovi",
- "No other versions available" : "Ni drugih različic",
- "More versions..." : "Več različic"
+ "This application automatically maintains older versions of files that are changed." : "Program samodejno ustvarja zaporedne različice sprememb datotek.",
+ "Current version" : "Trenutna različica",
+ "You" : "Jaz",
+ "Name this version" : "Poimenovanje razičice",
+ "Edit version name" : "Uredi ime različice",
+ "Restore version" : "Obnovi različico",
+ "Download version" : "Prejmi različico",
+ "Delete version" : "Izbriši različico",
+ "Cancel" : "Prekliči",
+ "Remove version name" : "Odstrani ime različice",
+ "Save version name" : "Shrani ime različice",
+ "Version name" : "Ime različice",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Poimenovane različice so trajne in niso vključene v samodejno čiščenje, če je prostora v oblaku malo.",
+ "Initial version restored" : "Začetna različica je obnovljena",
+ "Version restored" : "Različica je obnovljena",
+ "Could not restore version" : "Različice ni mogoče obnoviti",
+ "Could not delete version" : "Različice ji mogoče izbrisati"
},"pluralForm" :"nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);"
} \ No newline at end of file
diff --git a/apps/files_versions/l10n/sq.js b/apps/files_versions/l10n/sq.js
deleted file mode 100644
index 239946a7ef2..00000000000
--- a/apps/files_versions/l10n/sq.js
+++ /dev/null
@@ -1,11 +0,0 @@
-OC.L10N.register(
- "files_versions",
- {
- "Could not revert: %s" : "S’u rikthye dot: %s",
- "Versions" : "Versione",
- "Failed to revert {file} to revision {timestamp}." : "Dështoi në rikthimin e {file} te rishikimi {timestamp}.",
- "Restore" : "Riktheje",
- "No other versions available" : "Nuk ka versione të tjera të gatshme",
- "More versions..." : "Më shumë versione…"
-},
-"nplurals=2; plural=(n != 1);");
diff --git a/apps/files_versions/l10n/sq.json b/apps/files_versions/l10n/sq.json
deleted file mode 100644
index ae79f017be2..00000000000
--- a/apps/files_versions/l10n/sq.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{ "translations": {
- "Could not revert: %s" : "S’u rikthye dot: %s",
- "Versions" : "Versione",
- "Failed to revert {file} to revision {timestamp}." : "Dështoi në rikthimin e {file} te rishikimi {timestamp}.",
- "Restore" : "Riktheje",
- "No other versions available" : "Nuk ka versione të tjera të gatshme",
- "More versions..." : "Më shumë versione…"
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
-} \ No newline at end of file
diff --git a/apps/files_versions/l10n/sr.js b/apps/files_versions/l10n/sr.js
index 9b8c97592cc..9fae518228f 100644
--- a/apps/files_versions/l10n/sr.js
+++ b/apps/files_versions/l10n/sr.js
@@ -1,11 +1,29 @@
OC.L10N.register(
"files_versions",
{
- "Could not revert: %s" : "Не могу да вратим: %s",
"Versions" : "Верзије",
- "Failed to revert {file} to revision {timestamp}." : "Не могу да вратим {file} на ревизију {timestamp}.",
- "Restore" : "Врати",
- "No other versions available" : "Нема других верзија",
- "More versions..." : "Још верзија..."
+ "This application automatically maintains older versions of files that are changed." : "Ова апликација аутоматски одржава старије верзије измењених фајлова.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Ова апликација аутоматски одржава старије верзије фајлова који су измењени. Када се укључи, у сваком корисниковом директоријуму се креира скривени фолдер са верзијама. Он се користи за чување старијих верзија фајла. У било које време корисник може да се врати на старију верзију користећи веб интерфејс, тако што замењени фајл постаје нова верзија. Апликација аутоматски управља фолдером са верзијама тако да налог не потроши своју Квоту због верзија.\n\t\tУз време истека верзија, апликација обезбеђује да се никада не заузме више од 50% текућег слободног простора налога. Ако сачуване верзије прекораче ово ограничење, апликација ће најпре обриати најстарије верзије све док се не задовољи ограничење. Више информација се налази у документацији апликације Верзије.",
+ "Current version" : "Тренутна верзија",
+ "Initial version" : "Почетна верзија",
+ "You" : "Ви",
+ "Actions for version from {versionHumanExplicitDate}" : "Акције за верзију од {versionHumanExplicitDate}",
+ "Name this version" : "Назив ове верзије",
+ "Edit version name" : "Уреди назив верзије",
+ "Compare to current version" : "Упореди са текућом верзијом",
+ "Restore version" : "Рестаурирај верзију",
+ "Download version" : "Преузми верзију",
+ "Delete version" : "Обриши верзију",
+ "Cancel" : "Откажи",
+ "Remove version name" : "Уклони назив верзије",
+ "Save version name" : "Сачувај назив верзије",
+ "Version name" : "Назив верзије",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Именоване верзије су постојане и изузете су из аутоматског чишћења када се испуни ваша диск квота.",
+ "Initial version restored" : "Рестаурирана је почетна верзија",
+ "Version restored" : "Верзија је рестаурирана",
+ "Could not restore version" : "Верзија није могла да се рестаурира",
+ "Could not set version label" : "Није могла да се постави ознака назива верзије",
+ "Could not delete version" : "Верзија није могла да се обрише",
+ "File versions" : "Верзије фајла"
},
"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);");
diff --git a/apps/files_versions/l10n/sr.json b/apps/files_versions/l10n/sr.json
index 73484b6f3d5..c4d24c85abf 100644
--- a/apps/files_versions/l10n/sr.json
+++ b/apps/files_versions/l10n/sr.json
@@ -1,9 +1,27 @@
{ "translations": {
- "Could not revert: %s" : "Не могу да вратим: %s",
"Versions" : "Верзије",
- "Failed to revert {file} to revision {timestamp}." : "Не могу да вратим {file} на ревизију {timestamp}.",
- "Restore" : "Врати",
- "No other versions available" : "Нема других верзија",
- "More versions..." : "Још верзија..."
+ "This application automatically maintains older versions of files that are changed." : "Ова апликација аутоматски одржава старије верзије измењених фајлова.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Ова апликација аутоматски одржава старије верзије фајлова који су измењени. Када се укључи, у сваком корисниковом директоријуму се креира скривени фолдер са верзијама. Он се користи за чување старијих верзија фајла. У било које време корисник може да се врати на старију верзију користећи веб интерфејс, тако што замењени фајл постаје нова верзија. Апликација аутоматски управља фолдером са верзијама тако да налог не потроши своју Квоту због верзија.\n\t\tУз време истека верзија, апликација обезбеђује да се никада не заузме више од 50% текућег слободног простора налога. Ако сачуване верзије прекораче ово ограничење, апликација ће најпре обриати најстарије верзије све док се не задовољи ограничење. Више информација се налази у документацији апликације Верзије.",
+ "Current version" : "Тренутна верзија",
+ "Initial version" : "Почетна верзија",
+ "You" : "Ви",
+ "Actions for version from {versionHumanExplicitDate}" : "Акције за верзију од {versionHumanExplicitDate}",
+ "Name this version" : "Назив ове верзије",
+ "Edit version name" : "Уреди назив верзије",
+ "Compare to current version" : "Упореди са текућом верзијом",
+ "Restore version" : "Рестаурирај верзију",
+ "Download version" : "Преузми верзију",
+ "Delete version" : "Обриши верзију",
+ "Cancel" : "Откажи",
+ "Remove version name" : "Уклони назив верзије",
+ "Save version name" : "Сачувај назив верзије",
+ "Version name" : "Назив верзије",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Именоване верзије су постојане и изузете су из аутоматског чишћења када се испуни ваша диск квота.",
+ "Initial version restored" : "Рестаурирана је почетна верзија",
+ "Version restored" : "Верзија је рестаурирана",
+ "Could not restore version" : "Верзија није могла да се рестаурира",
+ "Could not set version label" : "Није могла да се постави ознака назива верзије",
+ "Could not delete version" : "Верзија није могла да се обрише",
+ "File versions" : "Верзије фајла"
},"pluralForm" :"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"
} \ No newline at end of file
diff --git a/apps/files_versions/l10n/sr@latin.js b/apps/files_versions/l10n/sr@latin.js
deleted file mode 100644
index 627d70eb51b..00000000000
--- a/apps/files_versions/l10n/sr@latin.js
+++ /dev/null
@@ -1,11 +0,0 @@
-OC.L10N.register(
- "files_versions",
- {
- "Could not revert: %s" : "Ne mogu da vratim: %s",
- "Versions" : "Verzije",
- "Failed to revert {file} to revision {timestamp}." : "Ne mogu da vratim {file} na reviziju {timestamp}.",
- "Restore" : "Vrati",
- "No other versions available" : "Nema drugih verzija",
- "More versions..." : "Još verzija..."
-},
-"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);");
diff --git a/apps/files_versions/l10n/sr@latin.json b/apps/files_versions/l10n/sr@latin.json
deleted file mode 100644
index 63fe55cf50e..00000000000
--- a/apps/files_versions/l10n/sr@latin.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{ "translations": {
- "Could not revert: %s" : "Ne mogu da vratim: %s",
- "Versions" : "Verzije",
- "Failed to revert {file} to revision {timestamp}." : "Ne mogu da vratim {file} na reviziju {timestamp}.",
- "Restore" : "Vrati",
- "No other versions available" : "Nema drugih verzija",
- "More versions..." : "Još verzija..."
-},"pluralForm" :"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"
-} \ No newline at end of file
diff --git a/apps/files_versions/l10n/sv.js b/apps/files_versions/l10n/sv.js
index 1b5d05f51ec..31711f5efef 100644
--- a/apps/files_versions/l10n/sv.js
+++ b/apps/files_versions/l10n/sv.js
@@ -1,11 +1,29 @@
OC.L10N.register(
"files_versions",
{
- "Could not revert: %s" : "Kunde inte återställa: %s",
"Versions" : "Versioner",
- "Failed to revert {file} to revision {timestamp}." : "Kunde inte återställa {file} till revision {timestamp}.",
- "Restore" : "Återskapa",
- "No other versions available" : "Inga andra versioner tillgängliga",
- "More versions..." : "Fler versioner..."
+ "This application automatically maintains older versions of files that are changed." : "Den här applikationen behåller automatiskt äldre versioner av filer som ändras.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Denna applikation hanterar automatiskt äldre versioner av filer när de ändras. När funktionen är aktiverad tillhandahålls en dold versionsmapp i varje användares katalog som används för att lagra gamla filversioner. En användare kan när som helst återgå till en äldre version via webbgränssnittet, när detta görs blir den ersatta filen en version. Appen hanterar automatiskt versionsmappen för att säkerställa att användaren inte får slut på sin kvot på grund av sparade versioner.\nUtöver hantering av utgångna versioner så förvissar sig appen för versionshantering att den inte använder sig av mer än 50% av användarens tillgängliga ledigt utrymme. Om lagrade versioner överskrider denna gräns, kommer appen att ta bort de äldsta versionerna först tills den inte längre överskrider gränsen. Mer information finns i Versionsdokumentationen.",
+ "Current version" : "Aktuell version",
+ "Initial version" : "Första versionen",
+ "You" : "Du",
+ "Actions for version from {versionHumanExplicitDate}" : "Åtgärder för version från {versionHumanExplicitDate}",
+ "Name this version" : "Namnge denna version",
+ "Edit version name" : "Ändra versionsnamn",
+ "Compare to current version" : "Jämför med nuvarande version",
+ "Restore version" : "Återställ version",
+ "Download version" : "Ladda ner version",
+ "Delete version" : "Ta bort version",
+ "Cancel" : "Avbryt",
+ "Remove version name" : "Ta bort versionsnamn",
+ "Save version name" : "Spara versionsnamn",
+ "Version name" : "Namn på version",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Namngivna versioner finns kvar och utesluts från automatiska rensningar när din lagringskvot är full.",
+ "Initial version restored" : "Ursprunglig version återställd",
+ "Version restored" : "Version återställd",
+ "Could not restore version" : "Kunde inte återställa versionen",
+ "Could not set version label" : "Det gick inte att ange versionsetikett",
+ "Could not delete version" : "Det gick inte att ta bort versionen",
+ "File versions" : "Filversioner"
},
"nplurals=2; plural=(n != 1);");
diff --git a/apps/files_versions/l10n/sv.json b/apps/files_versions/l10n/sv.json
index 30589a25bb5..98a4af8c801 100644
--- a/apps/files_versions/l10n/sv.json
+++ b/apps/files_versions/l10n/sv.json
@@ -1,9 +1,27 @@
{ "translations": {
- "Could not revert: %s" : "Kunde inte återställa: %s",
"Versions" : "Versioner",
- "Failed to revert {file} to revision {timestamp}." : "Kunde inte återställa {file} till revision {timestamp}.",
- "Restore" : "Återskapa",
- "No other versions available" : "Inga andra versioner tillgängliga",
- "More versions..." : "Fler versioner..."
+ "This application automatically maintains older versions of files that are changed." : "Den här applikationen behåller automatiskt äldre versioner av filer som ändras.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Denna applikation hanterar automatiskt äldre versioner av filer när de ändras. När funktionen är aktiverad tillhandahålls en dold versionsmapp i varje användares katalog som används för att lagra gamla filversioner. En användare kan när som helst återgå till en äldre version via webbgränssnittet, när detta görs blir den ersatta filen en version. Appen hanterar automatiskt versionsmappen för att säkerställa att användaren inte får slut på sin kvot på grund av sparade versioner.\nUtöver hantering av utgångna versioner så förvissar sig appen för versionshantering att den inte använder sig av mer än 50% av användarens tillgängliga ledigt utrymme. Om lagrade versioner överskrider denna gräns, kommer appen att ta bort de äldsta versionerna först tills den inte längre överskrider gränsen. Mer information finns i Versionsdokumentationen.",
+ "Current version" : "Aktuell version",
+ "Initial version" : "Första versionen",
+ "You" : "Du",
+ "Actions for version from {versionHumanExplicitDate}" : "Åtgärder för version från {versionHumanExplicitDate}",
+ "Name this version" : "Namnge denna version",
+ "Edit version name" : "Ändra versionsnamn",
+ "Compare to current version" : "Jämför med nuvarande version",
+ "Restore version" : "Återställ version",
+ "Download version" : "Ladda ner version",
+ "Delete version" : "Ta bort version",
+ "Cancel" : "Avbryt",
+ "Remove version name" : "Ta bort versionsnamn",
+ "Save version name" : "Spara versionsnamn",
+ "Version name" : "Namn på version",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Namngivna versioner finns kvar och utesluts från automatiska rensningar när din lagringskvot är full.",
+ "Initial version restored" : "Ursprunglig version återställd",
+ "Version restored" : "Version återställd",
+ "Could not restore version" : "Kunde inte återställa versionen",
+ "Could not set version label" : "Det gick inte att ange versionsetikett",
+ "Could not delete version" : "Det gick inte att ta bort versionen",
+ "File versions" : "Filversioner"
},"pluralForm" :"nplurals=2; plural=(n != 1);"
} \ No newline at end of file
diff --git a/apps/files_versions/l10n/sw.js b/apps/files_versions/l10n/sw.js
new file mode 100644
index 00000000000..193be71930c
--- /dev/null
+++ b/apps/files_versions/l10n/sw.js
@@ -0,0 +1,29 @@
+OC.L10N.register(
+ "files_versions",
+ {
+ "Versions" : "Matoleo",
+ "This application automatically maintains older versions of files that are changed." : "Programu hii hudumisha kiotomati matoleo ya zamani ya faili ambazo hubadilishwa.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Programu hii hudumisha kiotomati matoleo ya zamani ya faili ambazo hubadilishwa. Inapowashwa, folda ya matoleo yaliyofichwa hutolewa katika saraka ya kila mtumiaji na hutumiwa kuhifadhi matoleo ya zamani ya faili. Mtumiaji anaweza kurejesha toleo la zamani kupitia kiolesura cha wavuti wakati wowote, na faili iliyobadilishwa kuwa toleo. Programu hudhibiti folda ya matoleo kiotomatiki ili kuhakikisha kuwa akaunti haiishiwi na Kiasi kwa sababu ya matoleo.\n\t\tPamoja na kuisha kwa muda wa matoleo, programu ya matoleo huhakikisha kuwa haitawahi kutumia zaidi ya 50% ya nafasi ya bure inayopatikana kwa sasa ya akaunti. Ikiwa matoleo yaliyohifadhiwa yatazidi kikomo hiki, programu itafuta matoleo ya zamani kwanza hadi ifikie kikomo hiki. Maelezo zaidi yanapatikana katika hati za Matoleo.",
+ "Current version" : " Toleo la sasa",
+ "Initial version" : "Toleo la awali",
+ "You" : "Wewe",
+ "Actions for version from {versionHumanExplicitDate}" : "Vitendo vya toleo kutoka {versionHumanExplicitDate}",
+ "Name this version" : "Lipe jina toleo hili",
+ "Edit version name" : "Hariri jina la toleo",
+ "Compare to current version" : "Linganisha kwa toleo la sasa",
+ "Restore version" : "Hifadhi upya toleo",
+ "Download version" : "Pakua toleo",
+ "Delete version" : "Futa toleo",
+ "Cancel" : "Ghairi",
+ "Remove version name" : "Ondoa jina la toleo",
+ "Save version name" : "Hifadhi jina la toleo",
+ "Version name" : "Jina la toleo",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : " Matoleo yaliyotajwa yanaendelea, na hayajumuishwi katika usafishaji kiotomatiki wakati kiasi chako cha hifadhi kimejaa.",
+ "Initial version restored" : "Toleo la awali limerejeshwa",
+ "Version restored" : "Toleo limerejeshwa",
+ "Could not restore version" : "Haikuweza kurejesha toleo",
+ "Could not set version label" : "Haikuweza kuweka lebo ya toleo",
+ "Could not delete version" : "Haikuweza kufuta toleo",
+ "File versions" : "Matoleo ya faili"
+},
+"nplurals=2; plural=(n != 1);");
diff --git a/apps/files_versions/l10n/sw.json b/apps/files_versions/l10n/sw.json
new file mode 100644
index 00000000000..7a617882365
--- /dev/null
+++ b/apps/files_versions/l10n/sw.json
@@ -0,0 +1,27 @@
+{ "translations": {
+ "Versions" : "Matoleo",
+ "This application automatically maintains older versions of files that are changed." : "Programu hii hudumisha kiotomati matoleo ya zamani ya faili ambazo hubadilishwa.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Programu hii hudumisha kiotomati matoleo ya zamani ya faili ambazo hubadilishwa. Inapowashwa, folda ya matoleo yaliyofichwa hutolewa katika saraka ya kila mtumiaji na hutumiwa kuhifadhi matoleo ya zamani ya faili. Mtumiaji anaweza kurejesha toleo la zamani kupitia kiolesura cha wavuti wakati wowote, na faili iliyobadilishwa kuwa toleo. Programu hudhibiti folda ya matoleo kiotomatiki ili kuhakikisha kuwa akaunti haiishiwi na Kiasi kwa sababu ya matoleo.\n\t\tPamoja na kuisha kwa muda wa matoleo, programu ya matoleo huhakikisha kuwa haitawahi kutumia zaidi ya 50% ya nafasi ya bure inayopatikana kwa sasa ya akaunti. Ikiwa matoleo yaliyohifadhiwa yatazidi kikomo hiki, programu itafuta matoleo ya zamani kwanza hadi ifikie kikomo hiki. Maelezo zaidi yanapatikana katika hati za Matoleo.",
+ "Current version" : " Toleo la sasa",
+ "Initial version" : "Toleo la awali",
+ "You" : "Wewe",
+ "Actions for version from {versionHumanExplicitDate}" : "Vitendo vya toleo kutoka {versionHumanExplicitDate}",
+ "Name this version" : "Lipe jina toleo hili",
+ "Edit version name" : "Hariri jina la toleo",
+ "Compare to current version" : "Linganisha kwa toleo la sasa",
+ "Restore version" : "Hifadhi upya toleo",
+ "Download version" : "Pakua toleo",
+ "Delete version" : "Futa toleo",
+ "Cancel" : "Ghairi",
+ "Remove version name" : "Ondoa jina la toleo",
+ "Save version name" : "Hifadhi jina la toleo",
+ "Version name" : "Jina la toleo",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : " Matoleo yaliyotajwa yanaendelea, na hayajumuishwi katika usafishaji kiotomatiki wakati kiasi chako cha hifadhi kimejaa.",
+ "Initial version restored" : "Toleo la awali limerejeshwa",
+ "Version restored" : "Toleo limerejeshwa",
+ "Could not restore version" : "Haikuweza kurejesha toleo",
+ "Could not set version label" : "Haikuweza kuweka lebo ya toleo",
+ "Could not delete version" : "Haikuweza kufuta toleo",
+ "File versions" : "Matoleo ya faili"
+},"pluralForm" :"nplurals=2; plural=(n != 1);"
+} \ No newline at end of file
diff --git a/apps/files_versions/l10n/ta_LK.js b/apps/files_versions/l10n/ta_LK.js
deleted file mode 100644
index f03d2e0ff9d..00000000000
--- a/apps/files_versions/l10n/ta_LK.js
+++ /dev/null
@@ -1,6 +0,0 @@
-OC.L10N.register(
- "files_versions",
- {
- "Versions" : "பதிப்புகள்"
-},
-"nplurals=2; plural=(n != 1);");
diff --git a/apps/files_versions/l10n/ta_LK.json b/apps/files_versions/l10n/ta_LK.json
deleted file mode 100644
index 4418ed3b7fb..00000000000
--- a/apps/files_versions/l10n/ta_LK.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{ "translations": {
- "Versions" : "பதிப்புகள்"
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
-} \ No newline at end of file
diff --git a/apps/files_versions/l10n/th_TH.js b/apps/files_versions/l10n/th_TH.js
deleted file mode 100644
index 97d0539ca55..00000000000
--- a/apps/files_versions/l10n/th_TH.js
+++ /dev/null
@@ -1,11 +0,0 @@
-OC.L10N.register(
- "files_versions",
- {
- "Could not revert: %s" : "ไม่สามารถย้อนกลับ: %s",
- "Versions" : "รุ่น",
- "Failed to revert {file} to revision {timestamp}." : "{file} ล้มเหลวที่จะย้อนกลับ มีการแก้ไขเมื่อ {timestamp}",
- "Restore" : "คืนค่า",
- "No other versions available" : "ไม่มีรุ่นอื่นๆ",
- "More versions..." : "รุ่นอื่นๆ ..."
-},
-"nplurals=1; plural=0;");
diff --git a/apps/files_versions/l10n/th_TH.json b/apps/files_versions/l10n/th_TH.json
deleted file mode 100644
index a67fa3e17e0..00000000000
--- a/apps/files_versions/l10n/th_TH.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{ "translations": {
- "Could not revert: %s" : "ไม่สามารถย้อนกลับ: %s",
- "Versions" : "รุ่น",
- "Failed to revert {file} to revision {timestamp}." : "{file} ล้มเหลวที่จะย้อนกลับ มีการแก้ไขเมื่อ {timestamp}",
- "Restore" : "คืนค่า",
- "No other versions available" : "ไม่มีรุ่นอื่นๆ",
- "More versions..." : "รุ่นอื่นๆ ..."
-},"pluralForm" :"nplurals=1; plural=0;"
-} \ No newline at end of file
diff --git a/apps/files_versions/l10n/tr.js b/apps/files_versions/l10n/tr.js
index 00d5cbe15d4..dc216107c52 100644
--- a/apps/files_versions/l10n/tr.js
+++ b/apps/files_versions/l10n/tr.js
@@ -1,11 +1,29 @@
OC.L10N.register(
"files_versions",
{
- "Could not revert: %s" : "Geri alınamayan: %s",
"Versions" : "Sürümler",
- "Failed to revert {file} to revision {timestamp}." : "{file} dosyası {timestamp} gözden geçirmesine geri alınamadı.",
- "Restore" : "Geri yükle",
- "No other versions available" : "Başka sürüm mevcut değil",
- "More versions..." : "Daha fazla sürüm..."
+ "This application automatically maintains older versions of files that are changed." : "Bu uygulama değiştirilen dosyaların önceki sürümlerini otomatik olarak izler ve saklar.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Bu uygulama değiştirilen dosyaların önceki sürümlerini otomatik olarak izler ve saklar. Kullanıma alındığında, dosya sürümlerini saklamak için her kullanıcının klasörlerinde gizli bir versions klasörü oluşturulur. Kullanıcılar site arayüzünü kullanarak istedikleri zaman dosyalarının önceki sürümlerine geri dönebilirler. Uygulama versions klasörünü otomatik olarak düzenler ve önceki dosya sürümleri nedeniyle hesabın kotasının dolmasını engeller.\n\t\tSürümler uygulaması dosya sürümlerini belirli bir süreyle saklarken, hesabın depolama alanının en çok %50 oranındaki bölümünü kullanır. Depolanan sürüm dosyalarının boyutu bu sınırın üzerine çıkarsa, sınır değerine geri dönülene kadar en eski sürüm dosyaları silinir. Ayrıntılı bilgi almak için Sürümler uygulamasının belgelerine bakabilirsiniz.",
+ "Current version" : "Geçerli sürüm",
+ "Initial version" : "İlk sürüm",
+ "You" : "Siz",
+ "Actions for version from {versionHumanExplicitDate}" : "{versionHumanExplicitDate} tarihindeki sürüm için işlemler",
+ "Name this version" : "Bu sürümü adlandırın",
+ "Edit version name" : "Sürüm adını düzenle",
+ "Compare to current version" : "Geçerli sürüm ile karşılaştır",
+ "Restore version" : "Sürümü geri yükle",
+ "Download version" : "Sürümü indir",
+ "Delete version" : "Sürümü sil",
+ "Cancel" : "İptal",
+ "Remove version name" : "Sürüm adını kaldır",
+ "Save version name" : "Sürüm adını kaydet",
+ "Version name" : "Sürüm adı",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Adlandırılmış sürümler kalıcı olur ve depolama alanı kotanız dolduğunda otomatik temizleme işlemine katılmaz.",
+ "Initial version restored" : "İlk sürüm geri yüklendi",
+ "Version restored" : "Sürüm geri yüklendi",
+ "Could not restore version" : "Sürüm geri yüklenemedi",
+ "Could not set version label" : "Sürüm etiketi ayarlanamadı",
+ "Could not delete version" : "Sürüm silinemedi",
+ "File versions" : "Dosya sürümleri"
},
"nplurals=2; plural=(n > 1);");
diff --git a/apps/files_versions/l10n/tr.json b/apps/files_versions/l10n/tr.json
index 73aab761203..e914457e8bd 100644
--- a/apps/files_versions/l10n/tr.json
+++ b/apps/files_versions/l10n/tr.json
@@ -1,9 +1,27 @@
{ "translations": {
- "Could not revert: %s" : "Geri alınamayan: %s",
"Versions" : "Sürümler",
- "Failed to revert {file} to revision {timestamp}." : "{file} dosyası {timestamp} gözden geçirmesine geri alınamadı.",
- "Restore" : "Geri yükle",
- "No other versions available" : "Başka sürüm mevcut değil",
- "More versions..." : "Daha fazla sürüm..."
+ "This application automatically maintains older versions of files that are changed." : "Bu uygulama değiştirilen dosyaların önceki sürümlerini otomatik olarak izler ve saklar.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Bu uygulama değiştirilen dosyaların önceki sürümlerini otomatik olarak izler ve saklar. Kullanıma alındığında, dosya sürümlerini saklamak için her kullanıcının klasörlerinde gizli bir versions klasörü oluşturulur. Kullanıcılar site arayüzünü kullanarak istedikleri zaman dosyalarının önceki sürümlerine geri dönebilirler. Uygulama versions klasörünü otomatik olarak düzenler ve önceki dosya sürümleri nedeniyle hesabın kotasının dolmasını engeller.\n\t\tSürümler uygulaması dosya sürümlerini belirli bir süreyle saklarken, hesabın depolama alanının en çok %50 oranındaki bölümünü kullanır. Depolanan sürüm dosyalarının boyutu bu sınırın üzerine çıkarsa, sınır değerine geri dönülene kadar en eski sürüm dosyaları silinir. Ayrıntılı bilgi almak için Sürümler uygulamasının belgelerine bakabilirsiniz.",
+ "Current version" : "Geçerli sürüm",
+ "Initial version" : "İlk sürüm",
+ "You" : "Siz",
+ "Actions for version from {versionHumanExplicitDate}" : "{versionHumanExplicitDate} tarihindeki sürüm için işlemler",
+ "Name this version" : "Bu sürümü adlandırın",
+ "Edit version name" : "Sürüm adını düzenle",
+ "Compare to current version" : "Geçerli sürüm ile karşılaştır",
+ "Restore version" : "Sürümü geri yükle",
+ "Download version" : "Sürümü indir",
+ "Delete version" : "Sürümü sil",
+ "Cancel" : "İptal",
+ "Remove version name" : "Sürüm adını kaldır",
+ "Save version name" : "Sürüm adını kaydet",
+ "Version name" : "Sürüm adı",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Adlandırılmış sürümler kalıcı olur ve depolama alanı kotanız dolduğunda otomatik temizleme işlemine katılmaz.",
+ "Initial version restored" : "İlk sürüm geri yüklendi",
+ "Version restored" : "Sürüm geri yüklendi",
+ "Could not restore version" : "Sürüm geri yüklenemedi",
+ "Could not set version label" : "Sürüm etiketi ayarlanamadı",
+ "Could not delete version" : "Sürüm silinemedi",
+ "File versions" : "Dosya sürümleri"
},"pluralForm" :"nplurals=2; plural=(n > 1);"
} \ No newline at end of file
diff --git a/apps/files_versions/l10n/ug.js b/apps/files_versions/l10n/ug.js
index d0fc24b831f..5fb5ab3d7e3 100644
--- a/apps/files_versions/l10n/ug.js
+++ b/apps/files_versions/l10n/ug.js
@@ -1,7 +1,28 @@
OC.L10N.register(
"files_versions",
{
- "Could not revert: %s" : "ئەسلىگە قايتۇرالمايدۇ: %s",
- "Versions" : "نەشرى"
+ "Versions" : "نەشرى",
+ "This application automatically maintains older versions of files that are changed." : "بۇ پروگرامما ئۆزگەرتىلگەن ھۆججەتلەرنىڭ كونا نەشرىنى ئاپتوماتىك ساقلايدۇ.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "بۇ پروگرامما ئۆزگەرتىلگەن ھۆججەتلەرنىڭ كونا نەشرىنى ئاپتوماتىك ساقلايدۇ. قوزغىتىلغاندا ، ھەر بىر ئىشلەتكۈچىنىڭ مۇندەرىجىسىدە يوشۇرۇن نەشرى قىسقۇچ تەمىنلەنگەن بولۇپ ، كونا ھۆججەت نەشرىنى ساقلاشقا ئىشلىتىلىدۇ. ئىشلەتكۈچى خالىغان ۋاقىتتا تور كۆرۈنمە يۈزى ئارقىلىق كونا نەشرىگە قايتالايدۇ ، ئالماشتۇرۇلغان ھۆججەت نەشرىگە ئايلىنىدۇ. بۇ دېتال نەشرى قىسقۇچنى ئاپتوماتىك باشقۇرۇپ ، نەشرى سەۋەبىدىن ھېساباتنىڭ نورمىدىن ئېشىپ كەتمەسلىكىگە كاپالەتلىك قىلىدۇ.\n\t\tنەشرىنىڭ ۋاقتى توشقاندىن باشقا ، نەشىر دېتالى ھېساباتنىڭ ھازىر بار بولغان بوشلۇقنىڭ%50 تىن كۆپرەكىنى ھەرگىز ئىشلەتمەسلىككە كاپالەتلىك قىلىدۇ. ئەگەر ساقلانغان نەشرى بۇ چەكتىن ئېشىپ كەتسە ، ئەپ بۇ چەككە توشقۇچە ئەڭ كونا نەشرىنى ئۆچۈرۈۋېتىدۇ. تېخىمۇ كۆپ ئۇچۇرلار نەشىر ھۆججىتىدە بار.",
+ "Current version" : "نۆۋەتتىكى نەشرى",
+ "Initial version" : "دەسلەپكى نەشرى",
+ "You" : "سەن",
+ "Actions for version from {versionHumanExplicitDate}" : "{versionHumanExplicitDate} HumanExplicitDate from نەشرىدىكى مەشغۇلاتلار",
+ "Name this version" : "بۇ نەشرىگە ئىسىم قويۇڭ",
+ "Edit version name" : "نەشرىنىڭ نامىنى تەھرىرلەش",
+ "Compare to current version" : "ھازىرقى نەشرىگە سېلىشتۇرۇڭ",
+ "Restore version" : "نەشرىنى ئەسلىگە كەلتۈرۈش",
+ "Download version" : "نەشرىنى چۈشۈرۈش",
+ "Delete version" : "نەشرىنى ئۆچۈرۈڭ",
+ "Cancel" : "بىكار قىلىش",
+ "Remove version name" : "نەشرىنىڭ نامىنى ئۆچۈرۈڭ",
+ "Save version name" : "نەشرىنىڭ نامىنى ساقلاڭ",
+ "Version name" : "نەشرىنىڭ ئىسمى",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "ئىسمى بار نەشرى ساقلاپ قېلىنىدۇ ، ساقلاش نورمىسى توشقاندا ئاپتوماتىك تازىلاشتىن چىقىرىۋېتىلىدۇ.",
+ "Initial version restored" : "دەسلەپكى نەشرى ئەسلىگە كەلدى",
+ "Version restored" : "نەشرى ئەسلىگە كەلتۈرۈلدى",
+ "Could not restore version" : "نەشرىنى ئەسلىگە كەلتۈرەلمىدى",
+ "Could not set version label" : "نەشىر بەلگىسىنى تەڭشىيەلمىدى",
+ "Could not delete version" : "نەشرىنى ئۆچۈرەلمىدى"
},
-"nplurals=1; plural=0;");
+"nplurals=2; plural=(n != 1);");
diff --git a/apps/files_versions/l10n/ug.json b/apps/files_versions/l10n/ug.json
index 84f12cdc23a..29d3a9be9b6 100644
--- a/apps/files_versions/l10n/ug.json
+++ b/apps/files_versions/l10n/ug.json
@@ -1,5 +1,26 @@
{ "translations": {
- "Could not revert: %s" : "ئەسلىگە قايتۇرالمايدۇ: %s",
- "Versions" : "نەشرى"
-},"pluralForm" :"nplurals=1; plural=0;"
+ "Versions" : "نەشرى",
+ "This application automatically maintains older versions of files that are changed." : "بۇ پروگرامما ئۆزگەرتىلگەن ھۆججەتلەرنىڭ كونا نەشرىنى ئاپتوماتىك ساقلايدۇ.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "بۇ پروگرامما ئۆزگەرتىلگەن ھۆججەتلەرنىڭ كونا نەشرىنى ئاپتوماتىك ساقلايدۇ. قوزغىتىلغاندا ، ھەر بىر ئىشلەتكۈچىنىڭ مۇندەرىجىسىدە يوشۇرۇن نەشرى قىسقۇچ تەمىنلەنگەن بولۇپ ، كونا ھۆججەت نەشرىنى ساقلاشقا ئىشلىتىلىدۇ. ئىشلەتكۈچى خالىغان ۋاقىتتا تور كۆرۈنمە يۈزى ئارقىلىق كونا نەشرىگە قايتالايدۇ ، ئالماشتۇرۇلغان ھۆججەت نەشرىگە ئايلىنىدۇ. بۇ دېتال نەشرى قىسقۇچنى ئاپتوماتىك باشقۇرۇپ ، نەشرى سەۋەبىدىن ھېساباتنىڭ نورمىدىن ئېشىپ كەتمەسلىكىگە كاپالەتلىك قىلىدۇ.\n\t\tنەشرىنىڭ ۋاقتى توشقاندىن باشقا ، نەشىر دېتالى ھېساباتنىڭ ھازىر بار بولغان بوشلۇقنىڭ%50 تىن كۆپرەكىنى ھەرگىز ئىشلەتمەسلىككە كاپالەتلىك قىلىدۇ. ئەگەر ساقلانغان نەشرى بۇ چەكتىن ئېشىپ كەتسە ، ئەپ بۇ چەككە توشقۇچە ئەڭ كونا نەشرىنى ئۆچۈرۈۋېتىدۇ. تېخىمۇ كۆپ ئۇچۇرلار نەشىر ھۆججىتىدە بار.",
+ "Current version" : "نۆۋەتتىكى نەشرى",
+ "Initial version" : "دەسلەپكى نەشرى",
+ "You" : "سەن",
+ "Actions for version from {versionHumanExplicitDate}" : "{versionHumanExplicitDate} HumanExplicitDate from نەشرىدىكى مەشغۇلاتلار",
+ "Name this version" : "بۇ نەشرىگە ئىسىم قويۇڭ",
+ "Edit version name" : "نەشرىنىڭ نامىنى تەھرىرلەش",
+ "Compare to current version" : "ھازىرقى نەشرىگە سېلىشتۇرۇڭ",
+ "Restore version" : "نەشرىنى ئەسلىگە كەلتۈرۈش",
+ "Download version" : "نەشرىنى چۈشۈرۈش",
+ "Delete version" : "نەشرىنى ئۆچۈرۈڭ",
+ "Cancel" : "بىكار قىلىش",
+ "Remove version name" : "نەشرىنىڭ نامىنى ئۆچۈرۈڭ",
+ "Save version name" : "نەشرىنىڭ نامىنى ساقلاڭ",
+ "Version name" : "نەشرىنىڭ ئىسمى",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "ئىسمى بار نەشرى ساقلاپ قېلىنىدۇ ، ساقلاش نورمىسى توشقاندا ئاپتوماتىك تازىلاشتىن چىقىرىۋېتىلىدۇ.",
+ "Initial version restored" : "دەسلەپكى نەشرى ئەسلىگە كەلدى",
+ "Version restored" : "نەشرى ئەسلىگە كەلتۈرۈلدى",
+ "Could not restore version" : "نەشرىنى ئەسلىگە كەلتۈرەلمىدى",
+ "Could not set version label" : "نەشىر بەلگىسىنى تەڭشىيەلمىدى",
+ "Could not delete version" : "نەشرىنى ئۆچۈرەلمىدى"
+},"pluralForm" :"nplurals=2; plural=(n != 1);"
} \ No newline at end of file
diff --git a/apps/files_versions/l10n/uk.js b/apps/files_versions/l10n/uk.js
index 75335f25e55..8b98d0f28c6 100644
--- a/apps/files_versions/l10n/uk.js
+++ b/apps/files_versions/l10n/uk.js
@@ -1,11 +1,29 @@
OC.L10N.register(
"files_versions",
{
- "Could not revert: %s" : "Не вдалося відновити: %s",
"Versions" : "Версії",
- "Failed to revert {file} to revision {timestamp}." : "Не вдалося повернути {file} до ревізії {timestamp}.",
- "Restore" : "Відновити",
- "No other versions available" : "Інші версії недоступні",
- "More versions..." : "Більше версій ..."
+ "This application automatically maintains older versions of files that are changed." : "Цей застосунок підтримує роботу з попередніми версіями файлів, які було змінено.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Ця програма автоматично зберігає старі версії файлів, які були змінені. Коли ця функція увімкнена, у каталозі кожного користувача створюється прихований каталог «версії», яка використовується для зберігання старих версій файлів. Користувач може в будь-який час повернутися до старішої версії через веб-інтерфейс, а замінений файл стане версією. Програма автоматично керує каталогом «версії», щоб уникнути перевищення квоти облікового запису через версії.\n\nОкрім терміну дії версій, програма «Версії» гарантує, що ніколи не буде використовуватися більше 50% вільного місця, доступного на даний момент в обліковому записі. Якщо збережені версії перевищують цей ліміт, програма спочатку видалить найстаріші версії, доки не буде досягнуто цього ліміту. Більше інформації можна знайти в документації «Версії».",
+ "Current version" : "Поточна версія",
+ "Initial version" : "Початкова версія",
+ "You" : "Ви",
+ "Actions for version from {versionHumanExplicitDate}" : "Дії для версії від {versionHumanExplicitDate}",
+ "Name this version" : "Назвіть цю версію",
+ "Edit version name" : "Редагуйте назву версії",
+ "Compare to current version" : "Порівняти з поточною версією",
+ "Restore version" : "Відновити версію",
+ "Download version" : "Звантажити версію",
+ "Delete version" : "Вилучити версію",
+ "Cancel" : "Скасувати",
+ "Remove version name" : "Вилучити назву версії",
+ "Save version name" : "Зберегти назву версії",
+ "Version name" : "Назва версії",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Версії з назвами є постійними та їх виключено з автоматичного очищення після переповнення квоти у сховищі.",
+ "Initial version restored" : "Початкову версію відновлено",
+ "Version restored" : "Версію відновлено",
+ "Could not restore version" : "Не вдалося відновити версію",
+ "Could not set version label" : "Не вдалося встановити ярлик версії",
+ "Could not delete version" : "Не вдалося вилучити версію",
+ "File versions" : "Версії файлу"
},
-"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);");
+"nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != 11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || (n % 100 >=11 && n % 100 <=14 )) ? 2: 3);");
diff --git a/apps/files_versions/l10n/uk.json b/apps/files_versions/l10n/uk.json
index 54a0d7a816f..e9ec39ed3d9 100644
--- a/apps/files_versions/l10n/uk.json
+++ b/apps/files_versions/l10n/uk.json
@@ -1,9 +1,27 @@
{ "translations": {
- "Could not revert: %s" : "Не вдалося відновити: %s",
"Versions" : "Версії",
- "Failed to revert {file} to revision {timestamp}." : "Не вдалося повернути {file} до ревізії {timestamp}.",
- "Restore" : "Відновити",
- "No other versions available" : "Інші версії недоступні",
- "More versions..." : "Більше версій ..."
-},"pluralForm" :"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"
+ "This application automatically maintains older versions of files that are changed." : "Цей застосунок підтримує роботу з попередніми версіями файлів, які було змінено.",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "Ця програма автоматично зберігає старі версії файлів, які були змінені. Коли ця функція увімкнена, у каталозі кожного користувача створюється прихований каталог «версії», яка використовується для зберігання старих версій файлів. Користувач може в будь-який час повернутися до старішої версії через веб-інтерфейс, а замінений файл стане версією. Програма автоматично керує каталогом «версії», щоб уникнути перевищення квоти облікового запису через версії.\n\nОкрім терміну дії версій, програма «Версії» гарантує, що ніколи не буде використовуватися більше 50% вільного місця, доступного на даний момент в обліковому записі. Якщо збережені версії перевищують цей ліміт, програма спочатку видалить найстаріші версії, доки не буде досягнуто цього ліміту. Більше інформації можна знайти в документації «Версії».",
+ "Current version" : "Поточна версія",
+ "Initial version" : "Початкова версія",
+ "You" : "Ви",
+ "Actions for version from {versionHumanExplicitDate}" : "Дії для версії від {versionHumanExplicitDate}",
+ "Name this version" : "Назвіть цю версію",
+ "Edit version name" : "Редагуйте назву версії",
+ "Compare to current version" : "Порівняти з поточною версією",
+ "Restore version" : "Відновити версію",
+ "Download version" : "Звантажити версію",
+ "Delete version" : "Вилучити версію",
+ "Cancel" : "Скасувати",
+ "Remove version name" : "Вилучити назву версії",
+ "Save version name" : "Зберегти назву версії",
+ "Version name" : "Назва версії",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Версії з назвами є постійними та їх виключено з автоматичного очищення після переповнення квоти у сховищі.",
+ "Initial version restored" : "Початкову версію відновлено",
+ "Version restored" : "Версію відновлено",
+ "Could not restore version" : "Не вдалося відновити версію",
+ "Could not set version label" : "Не вдалося встановити ярлик версії",
+ "Could not delete version" : "Не вдалося вилучити версію",
+ "File versions" : "Версії файлу"
+},"pluralForm" :"nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != 11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || (n % 100 >=11 && n % 100 <=14 )) ? 2: 3);"
} \ No newline at end of file
diff --git a/apps/files_versions/l10n/ur_PK.js b/apps/files_versions/l10n/ur_PK.js
deleted file mode 100644
index ae55363a2dd..00000000000
--- a/apps/files_versions/l10n/ur_PK.js
+++ /dev/null
@@ -1,6 +0,0 @@
-OC.L10N.register(
- "files_versions",
- {
- "Restore" : "بحال"
-},
-"nplurals=2; plural=(n != 1);");
diff --git a/apps/files_versions/l10n/ur_PK.json b/apps/files_versions/l10n/ur_PK.json
deleted file mode 100644
index bfbcb42de28..00000000000
--- a/apps/files_versions/l10n/ur_PK.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{ "translations": {
- "Restore" : "بحال"
-},"pluralForm" :"nplurals=2; plural=(n != 1);"
-} \ No newline at end of file
diff --git a/apps/files_versions/l10n/vi.js b/apps/files_versions/l10n/vi.js
index 2d06f12d8c1..2b9a02aa4e1 100644
--- a/apps/files_versions/l10n/vi.js
+++ b/apps/files_versions/l10n/vi.js
@@ -1,11 +1,25 @@
OC.L10N.register(
"files_versions",
{
- "Could not revert: %s" : "Không thể khôi phục: %s",
"Versions" : "Phiên bản",
- "Failed to revert {file} to revision {timestamp}." : "Thất bại khi trở lại {file} khi sử đổi {timestamp}.",
- "Restore" : "Khôi phục",
- "No other versions available" : "Không có các phiên bản khác có sẵn",
- "More versions..." : "Nhiều phiên bản ..."
+ "This application automatically maintains older versions of files that are changed." : "ng dụng này tự động duy trì các phiên bản cũ hơn của tệp đã được thay đổi.",
+ "Current version" : "Phiên bản hiện tại",
+ "Initial version" : "Phiên bản đầu tiên",
+ "You" : "Bạn",
+ "Name this version" : "Đặt tên cho phiên bản này",
+ "Edit version name" : "Chỉnh sửa tên phiên bản",
+ "Compare to current version" : "So sánh với phiên bản hiện tại",
+ "Restore version" : "Khôi phục phiên bản",
+ "Download version" : "Tải xuống phiên bản",
+ "Delete version" : "Xóa phiên bản",
+ "Cancel" : "Hủy",
+ "Remove version name" : "Xóa tên phiên bản",
+ "Save version name" : "Lưu tên phiên bản",
+ "Version name" : "Tên phiên bản",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Các phiên bản được đặt tên vẫn được duy trì và bị loại khỏi quá trình dọn dẹp tự động khi hạn mức bộ nhớ của bạn đã đầy.",
+ "Initial version restored" : "Đã khôi phục phiên bản ban đầu",
+ "Version restored" : "Đã khôi phục phiên bản",
+ "Could not restore version" : "Không thể khôi phục phiên bản",
+ "Could not delete version" : "Không thể xóa phiên bản"
},
"nplurals=1; plural=0;");
diff --git a/apps/files_versions/l10n/vi.json b/apps/files_versions/l10n/vi.json
index e126e6e2d53..db50330b5dc 100644
--- a/apps/files_versions/l10n/vi.json
+++ b/apps/files_versions/l10n/vi.json
@@ -1,9 +1,23 @@
{ "translations": {
- "Could not revert: %s" : "Không thể khôi phục: %s",
"Versions" : "Phiên bản",
- "Failed to revert {file} to revision {timestamp}." : "Thất bại khi trở lại {file} khi sử đổi {timestamp}.",
- "Restore" : "Khôi phục",
- "No other versions available" : "Không có các phiên bản khác có sẵn",
- "More versions..." : "Nhiều phiên bản ..."
+ "This application automatically maintains older versions of files that are changed." : "ng dụng này tự động duy trì các phiên bản cũ hơn của tệp đã được thay đổi.",
+ "Current version" : "Phiên bản hiện tại",
+ "Initial version" : "Phiên bản đầu tiên",
+ "You" : "Bạn",
+ "Name this version" : "Đặt tên cho phiên bản này",
+ "Edit version name" : "Chỉnh sửa tên phiên bản",
+ "Compare to current version" : "So sánh với phiên bản hiện tại",
+ "Restore version" : "Khôi phục phiên bản",
+ "Download version" : "Tải xuống phiên bản",
+ "Delete version" : "Xóa phiên bản",
+ "Cancel" : "Hủy",
+ "Remove version name" : "Xóa tên phiên bản",
+ "Save version name" : "Lưu tên phiên bản",
+ "Version name" : "Tên phiên bản",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "Các phiên bản được đặt tên vẫn được duy trì và bị loại khỏi quá trình dọn dẹp tự động khi hạn mức bộ nhớ của bạn đã đầy.",
+ "Initial version restored" : "Đã khôi phục phiên bản ban đầu",
+ "Version restored" : "Đã khôi phục phiên bản",
+ "Could not restore version" : "Không thể khôi phục phiên bản",
+ "Could not delete version" : "Không thể xóa phiên bản"
},"pluralForm" :"nplurals=1; plural=0;"
} \ No newline at end of file
diff --git a/apps/files_versions/l10n/zh_CN.js b/apps/files_versions/l10n/zh_CN.js
index b94ec419d23..cdc937e9c11 100644
--- a/apps/files_versions/l10n/zh_CN.js
+++ b/apps/files_versions/l10n/zh_CN.js
@@ -1,11 +1,29 @@
OC.L10N.register(
"files_versions",
{
- "Could not revert: %s" : "无法恢复: %s",
"Versions" : "版本",
- "Failed to revert {file} to revision {timestamp}." : "无法恢复 {file} 到 {timestamp} 的版本。",
- "Restore" : "恢复",
- "No other versions available" : "无其他版本可用",
- "More versions..." : "更多版本..."
+ "This application automatically maintains older versions of files that are changed." : "此应用程序自动维护已更改文件的旧版本。",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "此应用程序将会自动保留已被更改的老版本文件。当此应用被启用时,每个用户的目录中都会呈现一个用于版本存储的隐藏文件夹。用户可以随时通过网页将文件恢复到旧版本,替换的文件将作为最新版本。应用程序将自动管理版本文件夹以确保账户不会因保留历史版本耗尽存储空间。\n\t\t除了版本有效期限以外,应用程序会确保保留历史版本的文件不会占用超过账户目前可用空间50%。如果存储的版本超出此限制,则版本管理会先删除最早的版本直至低于存储限制。更多资讯详见“版本”说明文档。",
+ "Current version" : "当前版本",
+ "Initial version" : "初始版本",
+ "You" : "您",
+ "Actions for version from {versionHumanExplicitDate}" : "{versionHumanExplicitDate} 版本的操作",
+ "Name this version" : "命名此版本",
+ "Edit version name" : "编辑版本名",
+ "Compare to current version" : "与当前版本比较",
+ "Restore version" : "恢复版本",
+ "Download version" : "下载版本",
+ "Delete version" : "删除版本",
+ "Cancel" : "取消",
+ "Remove version name" : "删除版本名",
+ "Save version name" : "保存版本名",
+ "Version name" : "版本名",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "命名版本会保留,且当你的存储配额已满时,它将被从自动清理中排除。",
+ "Initial version restored" : "已还原最初版本",
+ "Version restored" : "版本已还原",
+ "Could not restore version" : "无法还原版本",
+ "Could not set version label" : "无法设置版本标签",
+ "Could not delete version" : "无法删除版本",
+ "File versions" : "文件版本"
},
"nplurals=1; plural=0;");
diff --git a/apps/files_versions/l10n/zh_CN.json b/apps/files_versions/l10n/zh_CN.json
index 80f8624f9dd..24943a2fbf1 100644
--- a/apps/files_versions/l10n/zh_CN.json
+++ b/apps/files_versions/l10n/zh_CN.json
@@ -1,9 +1,27 @@
{ "translations": {
- "Could not revert: %s" : "无法恢复: %s",
"Versions" : "版本",
- "Failed to revert {file} to revision {timestamp}." : "无法恢复 {file} 到 {timestamp} 的版本。",
- "Restore" : "恢复",
- "No other versions available" : "无其他版本可用",
- "More versions..." : "更多版本..."
+ "This application automatically maintains older versions of files that are changed." : "此应用程序自动维护已更改文件的旧版本。",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "此应用程序将会自动保留已被更改的老版本文件。当此应用被启用时,每个用户的目录中都会呈现一个用于版本存储的隐藏文件夹。用户可以随时通过网页将文件恢复到旧版本,替换的文件将作为最新版本。应用程序将自动管理版本文件夹以确保账户不会因保留历史版本耗尽存储空间。\n\t\t除了版本有效期限以外,应用程序会确保保留历史版本的文件不会占用超过账户目前可用空间50%。如果存储的版本超出此限制,则版本管理会先删除最早的版本直至低于存储限制。更多资讯详见“版本”说明文档。",
+ "Current version" : "当前版本",
+ "Initial version" : "初始版本",
+ "You" : "您",
+ "Actions for version from {versionHumanExplicitDate}" : "{versionHumanExplicitDate} 版本的操作",
+ "Name this version" : "命名此版本",
+ "Edit version name" : "编辑版本名",
+ "Compare to current version" : "与当前版本比较",
+ "Restore version" : "恢复版本",
+ "Download version" : "下载版本",
+ "Delete version" : "删除版本",
+ "Cancel" : "取消",
+ "Remove version name" : "删除版本名",
+ "Save version name" : "保存版本名",
+ "Version name" : "版本名",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "命名版本会保留,且当你的存储配额已满时,它将被从自动清理中排除。",
+ "Initial version restored" : "已还原最初版本",
+ "Version restored" : "版本已还原",
+ "Could not restore version" : "无法还原版本",
+ "Could not set version label" : "无法设置版本标签",
+ "Could not delete version" : "无法删除版本",
+ "File versions" : "文件版本"
},"pluralForm" :"nplurals=1; plural=0;"
} \ No newline at end of file
diff --git a/apps/files_versions/l10n/zh_HK.js b/apps/files_versions/l10n/zh_HK.js
index bdee63494d7..48a566dc6cb 100644
--- a/apps/files_versions/l10n/zh_HK.js
+++ b/apps/files_versions/l10n/zh_HK.js
@@ -1,6 +1,29 @@
OC.L10N.register(
"files_versions",
{
- "Versions" : "版本"
+ "Versions" : "版本",
+ "This application automatically maintains older versions of files that are changed." : "本應用程式會自動保存修改前舊版本的檔案",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "此應用程式會自動維護已更改過的檔案的較舊版本。啟用後,每個用戶目錄中都會有一個隱藏的版本資料夾,此資料夾用於儲存較舊版本的檔案。用戶可以在任何時候透過網路界面還原到較舊的版本,取代目前版本成為新的版本。應用程式會自動管理版本資料夾以確保帳戶不會因為版本而用完配額。\n\t\t除了版本有效期限以外,版本應用程式還會確保不會使用超過帳戶目前可用空間的 50%。如果儲存的版本超出此限制,則應用程式會先刪除最舊的版本,直到低於此限制。更多資訊請見「版本」說明書。",
+ "Current version" : "目前版本",
+ "Initial version" : "初始版本",
+ "You" : "您",
+ "Actions for version from {versionHumanExplicitDate}" : "{versionHumanExplicitDate} 版本的操作",
+ "Name this version" : "為此版本命名",
+ "Edit version name" : "編輯版本名稱",
+ "Compare to current version" : "與目前版本比較",
+ "Restore version" : "還原版本",
+ "Download version" : "下載版本",
+ "Delete version" : "刪除版本",
+ "Cancel" : "取消",
+ "Remove version name" : "移除版本名稱",
+ "Save version name" : "儲存版本名稱",
+ "Version name" : "版本名稱",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "命名版本會保留,並在儲存配額已滿時從自動清理中排除。",
+ "Initial version restored" : "初始版本已還原",
+ "Version restored" : "版本已還原",
+ "Could not restore version" : "無法還原版本",
+ "Could not set version label" : "無法設置版本標籤",
+ "Could not delete version" : "無法刪除版本",
+ "File versions" : "檔案版本"
},
"nplurals=1; plural=0;");
diff --git a/apps/files_versions/l10n/zh_HK.json b/apps/files_versions/l10n/zh_HK.json
index bbf91817b11..b3ef15aae24 100644
--- a/apps/files_versions/l10n/zh_HK.json
+++ b/apps/files_versions/l10n/zh_HK.json
@@ -1,4 +1,27 @@
{ "translations": {
- "Versions" : "版本"
+ "Versions" : "版本",
+ "This application automatically maintains older versions of files that are changed." : "本應用程式會自動保存修改前舊版本的檔案",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "此應用程式會自動維護已更改過的檔案的較舊版本。啟用後,每個用戶目錄中都會有一個隱藏的版本資料夾,此資料夾用於儲存較舊版本的檔案。用戶可以在任何時候透過網路界面還原到較舊的版本,取代目前版本成為新的版本。應用程式會自動管理版本資料夾以確保帳戶不會因為版本而用完配額。\n\t\t除了版本有效期限以外,版本應用程式還會確保不會使用超過帳戶目前可用空間的 50%。如果儲存的版本超出此限制,則應用程式會先刪除最舊的版本,直到低於此限制。更多資訊請見「版本」說明書。",
+ "Current version" : "目前版本",
+ "Initial version" : "初始版本",
+ "You" : "您",
+ "Actions for version from {versionHumanExplicitDate}" : "{versionHumanExplicitDate} 版本的操作",
+ "Name this version" : "為此版本命名",
+ "Edit version name" : "編輯版本名稱",
+ "Compare to current version" : "與目前版本比較",
+ "Restore version" : "還原版本",
+ "Download version" : "下載版本",
+ "Delete version" : "刪除版本",
+ "Cancel" : "取消",
+ "Remove version name" : "移除版本名稱",
+ "Save version name" : "儲存版本名稱",
+ "Version name" : "版本名稱",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "命名版本會保留,並在儲存配額已滿時從自動清理中排除。",
+ "Initial version restored" : "初始版本已還原",
+ "Version restored" : "版本已還原",
+ "Could not restore version" : "無法還原版本",
+ "Could not set version label" : "無法設置版本標籤",
+ "Could not delete version" : "無法刪除版本",
+ "File versions" : "檔案版本"
},"pluralForm" :"nplurals=1; plural=0;"
} \ No newline at end of file
diff --git a/apps/files_versions/l10n/zh_TW.js b/apps/files_versions/l10n/zh_TW.js
index 72f086ff446..8cb5f7c5c32 100644
--- a/apps/files_versions/l10n/zh_TW.js
+++ b/apps/files_versions/l10n/zh_TW.js
@@ -1,11 +1,29 @@
OC.L10N.register(
"files_versions",
{
- "Could not revert: %s" : "無法還原:%s",
"Versions" : "版本",
- "Failed to revert {file} to revision {timestamp}." : "無法還原檔案 {file} 至版本 {timestamp}",
- "Restore" : "復原",
- "No other versions available" : "沒有其他版本了",
- "More versions..." : "更多版本…"
+ "This application automatically maintains older versions of files that are changed." : "本應用程式會自動維護檔案較舊的修改版本。",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "此應用程式會自動維護已變更過檔案的較舊版本。啟用後,每個使用者目錄中都會有一個隱藏的版本資料夾,此資料夾用於儲存較舊版本的檔案。使用者可以在任何時候透過網路介面還原到較舊的版本,取代目前版本成為新的版本。應用程式會自動管理版本資料夾以確保使用者不會因為版本而用完配額。\n\t\t除了版本有效期限以外,版本應用程式還會確保使用者永遠不會使用超過使用者目前可用空間的 50%。如果儲存的版本超出此限制,則應用程式會先刪除最舊的版本,直到低於此限制。更多資訊請見「版本」文件。",
+ "Current version" : "目前版本",
+ "Initial version" : "初始版本",
+ "You" : "您",
+ "Actions for version from {versionHumanExplicitDate}" : "來自 {versionHumanExplicitDate} 版本的動作",
+ "Name this version" : "為此版本命名",
+ "Edit version name" : "編輯版本名稱",
+ "Compare to current version" : "與目前版本比較",
+ "Restore version" : "還原版本",
+ "Download version" : "下載版本",
+ "Delete version" : "刪除版本",
+ "Cancel" : "取消",
+ "Remove version name" : "移除版本名稱",
+ "Save version name" : "儲存版本名稱",
+ "Version name" : "版本名稱",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "命名的版本會持續保留,並且在儲存容量限制已滿時,排除在自動清理外。",
+ "Initial version restored" : "已還原初始版本",
+ "Version restored" : "版本已還原",
+ "Could not restore version" : "無法還原版本",
+ "Could not set version label" : "無法設定版本標籤",
+ "Could not delete version" : "無法刪除版本",
+ "File versions" : "檔案版本"
},
"nplurals=1; plural=0;");
diff --git a/apps/files_versions/l10n/zh_TW.json b/apps/files_versions/l10n/zh_TW.json
index 37506095c20..f56231dcda0 100644
--- a/apps/files_versions/l10n/zh_TW.json
+++ b/apps/files_versions/l10n/zh_TW.json
@@ -1,9 +1,27 @@
{ "translations": {
- "Could not revert: %s" : "無法還原:%s",
"Versions" : "版本",
- "Failed to revert {file} to revision {timestamp}." : "無法還原檔案 {file} 至版本 {timestamp}",
- "Restore" : "復原",
- "No other versions available" : "沒有其他版本了",
- "More versions..." : "更多版本…"
+ "This application automatically maintains older versions of files that are changed." : "本應用程式會自動維護檔案較舊的修改版本。",
+ "This application automatically maintains older versions of files that are changed. When enabled, a hidden versions folder is provisioned in every user's directory and is used to store old file versions. A user can revert to an older version through the web interface at any time, with the replaced file becoming a version. The app automatically manages the versions folder to ensure the account does not run out of Quota because of versions.\n\t\tIn addition to the expiry of versions, the versions app makes certain never to use more than 50% of the account's currently available free space. If stored versions exceed this limit, the app will delete the oldest versions first until it meets this limit. More information is available in the Versions documentation." : "此應用程式會自動維護已變更過檔案的較舊版本。啟用後,每個使用者目錄中都會有一個隱藏的版本資料夾,此資料夾用於儲存較舊版本的檔案。使用者可以在任何時候透過網路介面還原到較舊的版本,取代目前版本成為新的版本。應用程式會自動管理版本資料夾以確保使用者不會因為版本而用完配額。\n\t\t除了版本有效期限以外,版本應用程式還會確保使用者永遠不會使用超過使用者目前可用空間的 50%。如果儲存的版本超出此限制,則應用程式會先刪除最舊的版本,直到低於此限制。更多資訊請見「版本」文件。",
+ "Current version" : "目前版本",
+ "Initial version" : "初始版本",
+ "You" : "您",
+ "Actions for version from {versionHumanExplicitDate}" : "來自 {versionHumanExplicitDate} 版本的動作",
+ "Name this version" : "為此版本命名",
+ "Edit version name" : "編輯版本名稱",
+ "Compare to current version" : "與目前版本比較",
+ "Restore version" : "還原版本",
+ "Download version" : "下載版本",
+ "Delete version" : "刪除版本",
+ "Cancel" : "取消",
+ "Remove version name" : "移除版本名稱",
+ "Save version name" : "儲存版本名稱",
+ "Version name" : "版本名稱",
+ "Named versions are persisted, and excluded from automatic cleanups when your storage quota is full." : "命名的版本會持續保留,並且在儲存容量限制已滿時,排除在自動清理外。",
+ "Initial version restored" : "已還原初始版本",
+ "Version restored" : "版本已還原",
+ "Could not restore version" : "無法還原版本",
+ "Could not set version label" : "無法設定版本標籤",
+ "Could not delete version" : "無法刪除版本",
+ "File versions" : "檔案版本"
},"pluralForm" :"nplurals=1; plural=0;"
} \ No newline at end of file
diff --git a/apps/files_versions/lib/AppInfo/Application.php b/apps/files_versions/lib/AppInfo/Application.php
new file mode 100644
index 00000000000..29158276415
--- /dev/null
+++ b/apps/files_versions/lib/AppInfo/Application.php
@@ -0,0 +1,151 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_Versions\AppInfo;
+
+use OC\KnownUser\KnownUserService;
+use OCA\DAV\CalDAV\Proxy\ProxyMapper;
+use OCA\DAV\Connector\Sabre\Principal;
+use OCA\Files\Event\LoadAdditionalScriptsEvent;
+use OCA\Files\Event\LoadSidebar;
+use OCA\Files_Versions\Capabilities;
+use OCA\Files_Versions\Events\VersionRestoredEvent;
+use OCA\Files_Versions\Listener\FileEventsListener;
+use OCA\Files_Versions\Listener\LegacyRollbackListener;
+use OCA\Files_Versions\Listener\LoadAdditionalListener;
+use OCA\Files_Versions\Listener\LoadSidebarListener;
+use OCA\Files_Versions\Listener\VersionAuthorListener;
+use OCA\Files_Versions\Listener\VersionStorageMoveListener;
+use OCA\Files_Versions\Versions\IVersionManager;
+use OCA\Files_Versions\Versions\VersionManager;
+use OCP\Accounts\IAccountManager;
+use OCP\App\IAppManager;
+use OCP\AppFramework\App;
+use OCP\AppFramework\Bootstrap\IBootContext;
+use OCP\AppFramework\Bootstrap\IBootstrap;
+use OCP\AppFramework\Bootstrap\IRegistrationContext;
+use OCP\Files\Events\Node\BeforeNodeCopiedEvent;
+use OCP\Files\Events\Node\BeforeNodeDeletedEvent;
+use OCP\Files\Events\Node\BeforeNodeRenamedEvent;
+use OCP\Files\Events\Node\BeforeNodeTouchedEvent;
+use OCP\Files\Events\Node\BeforeNodeWrittenEvent;
+use OCP\Files\Events\Node\NodeCopiedEvent;
+use OCP\Files\Events\Node\NodeCreatedEvent;
+use OCP\Files\Events\Node\NodeDeletedEvent;
+use OCP\Files\Events\Node\NodeRenamedEvent;
+use OCP\Files\Events\Node\NodeTouchedEvent;
+use OCP\Files\Events\Node\NodeWrittenEvent;
+use OCP\IConfig;
+use OCP\IGroupManager;
+use OCP\IServerContainer;
+use OCP\IUserManager;
+use OCP\IUserSession;
+use OCP\L10N\IFactory;
+use OCP\Server;
+use OCP\Share\IManager as IShareManager;
+use Psr\Container\ContainerInterface;
+use Psr\Log\LoggerInterface;
+
+class Application extends App implements IBootstrap {
+ public const APP_ID = 'files_versions';
+
+ public function __construct(array $urlParams = []) {
+ parent::__construct(self::APP_ID, $urlParams);
+ }
+
+ public function register(IRegistrationContext $context): void {
+ /**
+ * Register capabilities
+ */
+ $context->registerCapability(Capabilities::class);
+
+ /**
+ * Register $principalBackend for the DAV collection
+ */
+ $context->registerService('principalBackend', function (ContainerInterface $c) {
+ /** @var IServerContainer $server */
+ $server = $c->get(IServerContainer::class);
+ return new Principal(
+ $server->get(IUserManager::class),
+ $server->get(IGroupManager::class),
+ Server::get(IAccountManager::class),
+ $server->get(IShareManager::class),
+ $server->get(IUserSession::class),
+ $server->get(IAppManager::class),
+ $server->get(ProxyMapper::class),
+ $server->get(KnownUserService::class),
+ $server->get(IConfig::class),
+ $server->get(IFactory::class),
+ );
+ });
+
+ $context->registerServiceAlias(IVersionManager::class, VersionManager::class);
+
+ /**
+ * Register Events
+ */
+ $context->registerEventListener(LoadAdditionalScriptsEvent::class, LoadAdditionalListener::class);
+ $context->registerEventListener(LoadSidebar::class, LoadSidebarListener::class);
+
+ $context->registerEventListener(BeforeNodeRenamedEvent::class, VersionStorageMoveListener::class);
+ $context->registerEventListener(NodeRenamedEvent::class, VersionStorageMoveListener::class);
+ $context->registerEventListener(BeforeNodeCopiedEvent::class, VersionStorageMoveListener::class);
+ $context->registerEventListener(NodeCopiedEvent::class, VersionStorageMoveListener::class);
+
+ $context->registerEventListener(NodeCreatedEvent::class, FileEventsListener::class);
+ $context->registerEventListener(BeforeNodeTouchedEvent::class, FileEventsListener::class);
+ $context->registerEventListener(NodeTouchedEvent::class, FileEventsListener::class);
+ $context->registerEventListener(BeforeNodeWrittenEvent::class, FileEventsListener::class);
+ $context->registerEventListener(NodeWrittenEvent::class, FileEventsListener::class);
+ $context->registerEventListener(BeforeNodeDeletedEvent::class, FileEventsListener::class);
+ $context->registerEventListener(NodeDeletedEvent::class, FileEventsListener::class);
+ $context->registerEventListener(NodeRenamedEvent::class, FileEventsListener::class);
+ $context->registerEventListener(NodeCopiedEvent::class, FileEventsListener::class);
+ $context->registerEventListener(BeforeNodeRenamedEvent::class, FileEventsListener::class);
+ $context->registerEventListener(BeforeNodeCopiedEvent::class, FileEventsListener::class);
+
+ // we add the version author listener with lower priority to make sure new versions already are created by FileEventsListener
+ $context->registerEventListener(NodeWrittenEvent::class, VersionAuthorListener::class, -1);
+
+ $context->registerEventListener(VersionRestoredEvent::class, LegacyRollbackListener::class);
+ }
+
+ public function boot(IBootContext $context): void {
+ $context->injectFn(\Closure::fromCallable([$this, 'registerVersionBackends']));
+ }
+
+ public function registerVersionBackends(ContainerInterface $container, IAppManager $appManager, LoggerInterface $logger): void {
+ foreach ($appManager->getEnabledApps() as $app) {
+ $appInfo = $appManager->getAppInfo($app);
+ if (isset($appInfo['versions'])) {
+ $backends = $appInfo['versions'];
+ foreach ($backends as $backend) {
+ if (isset($backend['@value'])) {
+ $this->loadBackend($backend, $container, $logger);
+ } else {
+ foreach ($backend as $singleBackend) {
+ $this->loadBackend($singleBackend, $container, $logger);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private function loadBackend(array $backend, ContainerInterface $container, LoggerInterface $logger): void {
+ /** @var IVersionManager $versionManager */
+ $versionManager = $container->get(IVersionManager::class);
+ $class = $backend['@value'];
+ $for = $backend['@attributes']['for'];
+ try {
+ $backendObject = $container->get($class);
+ $versionManager->registerBackend($for, $backendObject);
+ } catch (\Exception $e) {
+ $logger->error($e->getMessage(), ['exception' => $e]);
+ }
+ }
+}
diff --git a/apps/files_versions/lib/BackgroundJob/ExpireVersions.php b/apps/files_versions/lib/BackgroundJob/ExpireVersions.php
new file mode 100644
index 00000000000..794cbc5b882
--- /dev/null
+++ b/apps/files_versions/lib/BackgroundJob/ExpireVersions.php
@@ -0,0 +1,68 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_Versions\BackgroundJob;
+
+use OC\Files\View;
+use OCA\Files_Versions\Expiration;
+use OCA\Files_Versions\Storage;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\BackgroundJob\TimedJob;
+use OCP\IConfig;
+use OCP\IUser;
+use OCP\IUserManager;
+
+class ExpireVersions extends TimedJob {
+ public const ITEMS_PER_SESSION = 1000;
+
+ public function __construct(
+ private IConfig $config,
+ private IUserManager $userManager,
+ private Expiration $expiration,
+ ITimeFactory $time,
+ ) {
+ parent::__construct($time);
+ // Run once per 30 minutes
+ $this->setInterval(60 * 30);
+ }
+
+ public function run($argument) {
+ $backgroundJob = $this->config->getAppValue('files_versions', 'background_job_expire_versions', 'yes');
+ if ($backgroundJob === 'no') {
+ return;
+ }
+
+ $maxAge = $this->expiration->getMaxAgeAsTimestamp();
+ if (!$maxAge) {
+ return;
+ }
+
+ $this->userManager->callForSeenUsers(function (IUser $user): void {
+ $uid = $user->getUID();
+ if (!$this->setupFS($uid)) {
+ return;
+ }
+ Storage::expireOlderThanMaxForUser($uid);
+ });
+ }
+
+ /**
+ * Act on behalf on trash item owner
+ */
+ protected function setupFS(string $user): bool {
+ \OC_Util::tearDownFS();
+ \OC_Util::setupFS($user);
+
+ // Check if this user has a versions directory
+ $view = new View('/' . $user);
+ if (!$view->is_dir('/files_versions')) {
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/apps/files_versions/lib/Capabilities.php b/apps/files_versions/lib/Capabilities.php
new file mode 100644
index 00000000000..cb6394f0a36
--- /dev/null
+++ b/apps/files_versions/lib/Capabilities.php
@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_Versions;
+
+use OCP\App\IAppManager;
+use OCP\Capabilities\ICapability;
+use OCP\IConfig;
+
+class Capabilities implements ICapability {
+ public function __construct(
+ private IConfig $config,
+ private IAppManager $appManager,
+ ) {
+ }
+
+ /**
+ * Return this classes capabilities
+ *
+ * @return array{files: array{versioning: bool, version_labeling: bool, version_deletion: bool}}
+ */
+ public function getCapabilities() {
+ return [
+ 'files' => [
+ 'versioning' => true,
+ 'version_labeling' => $this->config->getSystemValueBool('enable_version_labeling', true),
+ 'version_deletion' => $this->config->getSystemValueBool('enable_version_deletion', true),
+ ]
+ ];
+ }
+}
diff --git a/apps/files_versions/lib/Command/CleanUp.php b/apps/files_versions/lib/Command/CleanUp.php
new file mode 100644
index 00000000000..e8c46afef16
--- /dev/null
+++ b/apps/files_versions/lib/Command/CleanUp.php
@@ -0,0 +1,115 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_Versions\Command;
+
+use OCA\Files_Versions\Db\VersionsMapper;
+use OCP\Files\IRootFolder;
+use OCP\IUserBackend;
+use OCP\IUserManager;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class CleanUp extends Command {
+ public function __construct(
+ protected IRootFolder $rootFolder,
+ protected IUserManager $userManager,
+ protected VersionsMapper $versionMapper,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ $this
+ ->setName('versions:cleanup')
+ ->setDescription('Delete versions')
+ ->addArgument(
+ 'user_id',
+ InputArgument::OPTIONAL | InputArgument::IS_ARRAY,
+ 'delete versions of the given user(s), if no user is given all versions will be deleted'
+ )
+ ->addOption(
+ 'path',
+ 'p',
+ InputOption::VALUE_REQUIRED,
+ 'only delete versions of this path, e.g. --path="/alice/files/Music"'
+ );
+ }
+
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $users = $input->getArgument('user_id');
+
+ $path = $input->getOption('path');
+ if ($path) {
+ if (!preg_match('#^/([^/]+)/files(/.*)?$#', $path, $pathMatches)) {
+ $output->writeln('<error>Invalid path given</error>');
+ return self::FAILURE;
+ }
+
+ $users = [ $pathMatches[1] ];
+ $path = trim($pathMatches[2], '/');
+ }
+
+ if (!empty($users)) {
+ foreach ($users as $user) {
+ if (!$this->userManager->userExists($user)) {
+ $output->writeln("<error>Unknown user $user</error>");
+ return self::FAILURE;
+ }
+
+ $output->writeln("Delete versions of <info>$user</info>");
+ $this->deleteVersions($user, $path);
+ }
+ return self::SUCCESS;
+ }
+
+ $output->writeln('Delete all versions');
+ foreach ($this->userManager->getBackends() as $backend) {
+ $name = get_class($backend);
+
+ if ($backend instanceof IUserBackend) {
+ $name = $backend->getBackendName();
+ }
+
+ $output->writeln("Delete versions for users on backend <info>$name</info>");
+
+ $limit = 500;
+ $offset = 0;
+ do {
+ $users = $backend->getUsers('', $limit, $offset);
+ foreach ($users as $user) {
+ $output->writeln(" <info>$user</info>");
+ $this->deleteVersions($user);
+ }
+ $offset += $limit;
+ } while (count($users) >= $limit);
+ }
+
+ return self::SUCCESS;
+ }
+
+
+ /**
+ * delete versions for the given user
+ */
+ protected function deleteVersions(string $user, ?string $path = null): void {
+ \OC_Util::tearDownFS();
+ \OC_Util::setupFS($user);
+
+ $userHomeStorageId = $this->rootFolder->getUserFolder($user)->getStorage()->getCache()->getNumericStorageId();
+ $this->versionMapper->deleteAllVersionsForUser($userHomeStorageId, $path);
+
+ $fullPath = '/' . $user . '/files_versions' . ($path ? '/' . $path : '');
+ if ($this->rootFolder->nodeExists($fullPath)) {
+ $this->rootFolder->get($fullPath)->delete();
+ }
+ }
+}
diff --git a/apps/files_versions/lib/Command/Expire.php b/apps/files_versions/lib/Command/Expire.php
new file mode 100644
index 00000000000..a30e623c347
--- /dev/null
+++ b/apps/files_versions/lib/Command/Expire.php
@@ -0,0 +1,49 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_Versions\Command;
+
+use OC\Command\FileAccess;
+use OCA\Files_Versions\Storage;
+use OCP\Command\ICommand;
+use OCP\Files\StorageNotAvailableException;
+use OCP\IUserManager;
+use OCP\Server;
+use Psr\Log\LoggerInterface;
+
+class Expire implements ICommand {
+ use FileAccess;
+
+ public function __construct(
+ private string $user,
+ private string $fileName,
+ ) {
+ }
+
+ public function handle(): void {
+ /** @var IUserManager $userManager */
+ $userManager = Server::get(IUserManager::class);
+ if (!$userManager->userExists($this->user)) {
+ // User has been deleted already
+ return;
+ }
+
+ try {
+ Storage::expire($this->fileName, $this->user);
+ } catch (StorageNotAvailableException $e) {
+ // In case of external storage and session credentials, the expiration
+ // fails because the command does not have those credentials
+
+ $logger = Server::get(LoggerInterface::class);
+ $logger->warning($e->getMessage(), [
+ 'exception' => $e,
+ 'uid' => $this->user,
+ 'fileName' => $this->fileName,
+ ]);
+ }
+ }
+}
diff --git a/apps/files_versions/lib/Command/ExpireVersions.php b/apps/files_versions/lib/Command/ExpireVersions.php
new file mode 100644
index 00000000000..d3f341a21d2
--- /dev/null
+++ b/apps/files_versions/lib/Command/ExpireVersions.php
@@ -0,0 +1,96 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud GmbH.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_Versions\Command;
+
+use OC\Files\View;
+use OCA\Files_Versions\Expiration;
+use OCA\Files_Versions\Storage;
+use OCP\IUser;
+use OCP\IUserManager;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Helper\ProgressBar;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class ExpireVersions extends Command {
+ public function __construct(
+ private IUserManager $userManager,
+ private Expiration $expiration,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ $this
+ ->setName('versions:expire')
+ ->setDescription('Expires the users file versions')
+ ->addArgument(
+ 'user_id',
+ InputArgument::OPTIONAL | InputArgument::IS_ARRAY,
+ 'expire file versions of the given account(s), if no account is given file versions for all accounts will be expired.'
+ );
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $maxAge = $this->expiration->getMaxAgeAsTimestamp();
+ if (!$maxAge) {
+ $output->writeln('Auto expiration is configured - expiration will be handled automatically according to the expiration patterns detailed at the following link https://docs.nextcloud.com/server/latest/admin_manual/configuration_files/file_versioning.html.');
+ return self::FAILURE;
+ }
+
+ $users = $input->getArgument('user_id');
+ if (!empty($users)) {
+ foreach ($users as $user) {
+ if (!$this->userManager->userExists($user)) {
+ $output->writeln("<error>Unknown account $user</error>");
+ return self::FAILURE;
+ }
+
+ $output->writeln("Remove deleted files of <info>$user</info>");
+ $userObject = $this->userManager->get($user);
+ $this->expireVersionsForUser($userObject);
+ }
+ return self::SUCCESS;
+ }
+
+ $p = new ProgressBar($output);
+ $p->start();
+ $this->userManager->callForSeenUsers(function (IUser $user) use ($p): void {
+ $p->advance();
+ $this->expireVersionsForUser($user);
+ });
+ $p->finish();
+ $output->writeln('');
+ return self::SUCCESS;
+ }
+
+ public function expireVersionsForUser(IUser $user): void {
+ $uid = $user->getUID();
+ if (!$this->setupFS($uid)) {
+ return;
+ }
+ Storage::expireOlderThanMaxForUser($uid);
+ }
+
+ /**
+ * Act on behalf on versions item owner
+ */
+ protected function setupFS(string $user): bool {
+ \OC_Util::tearDownFS();
+ \OC_Util::setupFS($user);
+
+ // Check if this user has a version directory
+ $view = new View('/' . $user);
+ if (!$view->is_dir('/files_versions')) {
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/apps/files_versions/lib/Controller/PreviewController.php b/apps/files_versions/lib/Controller/PreviewController.php
new file mode 100644
index 00000000000..2c3ff8da70d
--- /dev/null
+++ b/apps/files_versions/lib/Controller/PreviewController.php
@@ -0,0 +1,92 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_Versions\Controller;
+
+use OCA\Files_Versions\Versions\IVersionManager;
+use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\Attribute\NoAdminRequired;
+use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
+use OCP\AppFramework\Http\Attribute\OpenAPI;
+use OCP\AppFramework\Http\DataResponse;
+use OCP\AppFramework\Http\FileDisplayResponse;
+use OCP\AppFramework\Http\RedirectResponse;
+use OCP\Files\IRootFolder;
+use OCP\Files\NotFoundException;
+use OCP\IPreview;
+use OCP\IRequest;
+use OCP\IUserSession;
+use OCP\Preview\IMimeIconProvider;
+
+#[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
+class PreviewController extends Controller {
+
+ public function __construct(
+ string $appName,
+ IRequest $request,
+ private IRootFolder $rootFolder,
+ private IUserSession $userSession,
+ private IVersionManager $versionManager,
+ private IPreview $previewManager,
+ private IMimeIconProvider $mimeIconProvider,
+ ) {
+ parent::__construct($appName, $request);
+ }
+
+ /**
+ * Get the preview for a file version
+ *
+ * @param string $file Path of the file
+ * @param int $x Width of the preview
+ * @param int $y Height of the preview
+ * @param string $version Version of the file to get the preview for
+ * @param bool $mimeFallback Whether to fallback to the mime icon if no preview is available
+ * @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND, list<empty>, array{}>|RedirectResponse<Http::STATUS_SEE_OTHER, array{}>
+ *
+ * 200: Preview returned
+ * 303: Redirect to the mime icon url if mimeFallback is true
+ * 400: Getting preview is not possible
+ * 404: Preview not found
+ */
+ #[NoAdminRequired]
+ #[NoCSRFRequired]
+ public function getPreview(
+ string $file = '',
+ int $x = 44,
+ int $y = 44,
+ string $version = '',
+ bool $mimeFallback = false,
+ ) {
+ if ($file === '' || $version === '' || $x === 0 || $y === 0) {
+ return new DataResponse([], Http::STATUS_BAD_REQUEST);
+ }
+
+ $versionFile = null;
+ try {
+ $user = $this->userSession->getUser();
+ $userFolder = $this->rootFolder->getUserFolder($user->getUID());
+ $file = $userFolder->get($file);
+ $versionFile = $this->versionManager->getVersionFile($user, $file, $version);
+ $preview = $this->previewManager->getPreview($versionFile, $x, $y, true, IPreview::MODE_FILL, $versionFile->getMimetype());
+ $response = new FileDisplayResponse($preview, Http::STATUS_OK, ['Content-Type' => $preview->getMimeType()]);
+ $response->cacheFor(3600 * 24, false, true);
+ return $response;
+ } catch (NotFoundException $e) {
+ // If we have no preview enabled, we can redirect to the mime icon if any
+ if ($mimeFallback && $versionFile !== null) {
+ $url = $this->mimeIconProvider->getMimeIconUrl($versionFile->getMimeType());
+ if ($url !== null) {
+ return new RedirectResponse($url);
+ }
+ }
+
+ return new DataResponse([], Http::STATUS_NOT_FOUND);
+ } catch (\InvalidArgumentException $e) {
+ return new DataResponse([], Http::STATUS_BAD_REQUEST);
+ }
+ }
+}
diff --git a/apps/files_versions/lib/Db/VersionEntity.php b/apps/files_versions/lib/Db/VersionEntity.php
new file mode 100644
index 00000000000..10f1dc8cbba
--- /dev/null
+++ b/apps/files_versions/lib/Db/VersionEntity.php
@@ -0,0 +1,74 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files_Versions\Db;
+
+use JsonSerializable;
+
+use OCP\AppFramework\Db\Entity;
+use OCP\DB\Types;
+
+/**
+ * @method int getFileId()
+ * @method void setFileId(int $fileId)
+ * @method int getTimestamp()
+ * @method void setTimestamp(int $timestamp)
+ * @method int|float getSize()
+ * @method void setSize(int|float $size)
+ * @method int getMimetype()
+ * @method void setMimetype(int $mimetype)
+ * @method array|null getMetadata()
+ * @method void setMetadata(array $metadata)
+ */
+class VersionEntity extends Entity implements JsonSerializable {
+ protected ?int $fileId = null;
+ protected ?int $timestamp = null;
+ protected ?int $size = null;
+ protected ?int $mimetype = null;
+ protected ?array $metadata = null;
+
+ public function __construct() {
+ $this->addType('id', Types::INTEGER);
+ $this->addType('file_id', Types::INTEGER);
+ $this->addType('timestamp', Types::INTEGER);
+ $this->addType('size', Types::INTEGER);
+ $this->addType('mimetype', Types::INTEGER);
+ $this->addType('metadata', Types::JSON);
+ }
+
+ public function jsonSerialize(): array {
+ return [
+ 'id' => $this->id,
+ 'file_id' => $this->fileId,
+ 'timestamp' => $this->timestamp,
+ 'size' => $this->size,
+ 'mimetype' => $this->mimetype,
+ 'metadata' => $this->metadata,
+ ];
+ }
+
+ /**
+ * @abstract given a key, return the value associated with the key in the metadata column
+ * if nothing is found, we return an empty string
+ * @param string $key key associated with the value
+ */
+ public function getMetadataValue(string $key): ?string {
+ return $this->metadata[$key] ?? null;
+ }
+
+ /**
+ * @abstract sets a key value pair in the metadata column
+ * @param string $key key associated with the value
+ * @param string $value value associated with the key
+ */
+ public function setMetadataValue(string $key, string $value): void {
+ $this->metadata[$key] = $value;
+ $this->markFieldUpdated('metadata');
+ }
+}
diff --git a/apps/files_versions/lib/Db/VersionsMapper.php b/apps/files_versions/lib/Db/VersionsMapper.php
new file mode 100644
index 00000000000..318dd8f0d82
--- /dev/null
+++ b/apps/files_versions/lib/Db/VersionsMapper.php
@@ -0,0 +1,104 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files_Versions\Db;
+
+use OCP\AppFramework\Db\QBMapper;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IDBConnection;
+
+/**
+ * @extends QBMapper<VersionEntity>
+ */
+class VersionsMapper extends QBMapper {
+ public function __construct(IDBConnection $db) {
+ parent::__construct($db, 'files_versions', VersionEntity::class);
+ }
+
+ /**
+ * @return VersionEntity[]
+ */
+ public function findAllVersionsForFileId(int $fileId): array {
+ $qb = $this->db->getQueryBuilder();
+
+ $qb->select('*')
+ ->from($this->getTableName())
+ ->where($qb->expr()->eq('file_id', $qb->createNamedParameter($fileId)));
+
+ return $this->findEntities($qb);
+ }
+
+ /**
+ * @return VersionEntity
+ */
+ public function findCurrentVersionForFileId(int $fileId): VersionEntity {
+ $qb = $this->db->getQueryBuilder();
+
+ $qb->select('*')
+ ->from($this->getTableName())
+ ->where($qb->expr()->eq('file_id', $qb->createNamedParameter($fileId)))
+ ->orderBy('timestamp', 'DESC')
+ ->setMaxResults(1);
+
+ return $this->findEntity($qb);
+ }
+
+ public function findVersionForFileId(int $fileId, int $timestamp): VersionEntity {
+ $qb = $this->db->getQueryBuilder();
+
+ $qb->select('*')
+ ->from($this->getTableName())
+ ->where($qb->expr()->eq('file_id', $qb->createNamedParameter($fileId)))
+ ->andWhere($qb->expr()->eq('timestamp', $qb->createNamedParameter($timestamp)));
+
+ return $this->findEntity($qb);
+ }
+
+ public function deleteAllVersionsForFileId(int $fileId): int {
+ $qb = $this->db->getQueryBuilder();
+
+ return $qb->delete($this->getTableName())
+ ->where($qb->expr()->eq('file_id', $qb->createNamedParameter($fileId)))
+ ->executeStatement();
+ }
+
+ public function deleteAllVersionsForUser(int $storageId, ?string $path = null): void {
+ $fileIdsGenerator = $this->getFileIdsGenerator($storageId, $path);
+
+ $versionEntitiesDeleteQuery = $this->db->getQueryBuilder();
+ $versionEntitiesDeleteQuery->delete($this->getTableName())
+ ->where($versionEntitiesDeleteQuery->expr()->in('file_id', $versionEntitiesDeleteQuery->createParameter('file_ids')));
+
+ foreach ($fileIdsGenerator as $fileIds) {
+ $versionEntitiesDeleteQuery->setParameter('file_ids', $fileIds, IQueryBuilder::PARAM_INT_ARRAY);
+ $versionEntitiesDeleteQuery->executeStatement();
+ }
+ }
+
+ private function getFileIdsGenerator(int $storageId, ?string $path): \Generator {
+ $offset = 0;
+ do {
+ $filesIdsSelect = $this->db->getQueryBuilder();
+ $filesIdsSelect->select('fileid')
+ ->from('filecache')
+ ->where($filesIdsSelect->expr()->eq('storage', $filesIdsSelect->createNamedParameter($storageId, IQueryBuilder::PARAM_STR)))
+ ->andWhere($filesIdsSelect->expr()->like('path', $filesIdsSelect->createNamedParameter('files' . ($path ? '/' . $this->db->escapeLikeParameter($path) : '') . '/%', IQueryBuilder::PARAM_STR)))
+ ->andWhere($filesIdsSelect->expr()->gt('fileid', $filesIdsSelect->createParameter('offset')))
+ ->setMaxResults(1000)
+ ->orderBy('fileid', 'ASC');
+
+ $filesIdsSelect->setParameter('offset', $offset, IQueryBuilder::PARAM_INT);
+ $result = $filesIdsSelect->executeQuery();
+ $fileIds = $result->fetchAll(\PDO::FETCH_COLUMN);
+ $offset = end($fileIds);
+
+ yield $fileIds;
+ } while (!empty($fileIds));
+ }
+}
diff --git a/apps/files_versions/lib/Events/CreateVersionEvent.php b/apps/files_versions/lib/Events/CreateVersionEvent.php
new file mode 100644
index 00000000000..92ed26b2dd6
--- /dev/null
+++ b/apps/files_versions/lib/Events/CreateVersionEvent.php
@@ -0,0 +1,60 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_Versions\Events;
+
+use OCP\EventDispatcher\Event;
+use OCP\Files\Node;
+
+/**
+ * Class CreateVersionEvent
+ *
+ * Event to allow other apps to disable versions for specific files
+ *
+ * @package OCA\Files_Versions
+ */
+class CreateVersionEvent extends Event {
+
+
+ /** @var bool */
+ private $createVersion;
+
+ /**
+ * CreateVersionEvent constructor.
+ *
+ * @param Node $node
+ */
+ public function __construct(
+ private Node $node,
+ ) {
+ $this->createVersion = true;
+ }
+
+ /**
+ * get Node of the file which should be versioned
+ *
+ * @return Node
+ */
+ public function getNode(): Node {
+ return $this->node;
+ }
+
+ /**
+ * disable versions for this file
+ */
+ public function disableVersions(): void {
+ $this->createVersion = false;
+ }
+
+ /**
+ * should a version be created for this file?
+ *
+ * @return bool
+ */
+ public function shouldCreateVersion(): bool {
+ return $this->createVersion;
+ }
+}
diff --git a/apps/files_versions/lib/Events/VersionCreatedEvent.php b/apps/files_versions/lib/Events/VersionCreatedEvent.php
new file mode 100644
index 00000000000..4dc7a7cb505
--- /dev/null
+++ b/apps/files_versions/lib/Events/VersionCreatedEvent.php
@@ -0,0 +1,39 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_Versions\Events;
+
+use OCA\Files_Versions\Versions\IVersion;
+use OCP\EventDispatcher\Event;
+use OCP\Files\Node;
+
+/**
+ * Event dispatched after a successful creation of a version
+ */
+class VersionCreatedEvent extends Event {
+ public function __construct(
+ private Node $node,
+ private IVersion $version,
+ ) {
+ parent::__construct();
+ }
+
+ /**
+ * Node of the file that has been versioned
+ */
+ public function getNode(): Node {
+ return $this->node;
+ }
+
+ /**
+ * Version of the file that was created
+ */
+ public function getVersion(): IVersion {
+ return $this->version;
+ }
+}
diff --git a/apps/files_versions/lib/Events/VersionRestoredEvent.php b/apps/files_versions/lib/Events/VersionRestoredEvent.php
new file mode 100644
index 00000000000..12e91bd258d
--- /dev/null
+++ b/apps/files_versions/lib/Events/VersionRestoredEvent.php
@@ -0,0 +1,33 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_Versions\Events;
+
+use OCA\Files_Versions\Versions\IVersion;
+use OCP\EventDispatcher\Event;
+
+/**
+ * Class VersionRestoredEvent
+ *
+ * Event that is called after a successful restore of a previous version
+ *
+ * @package OCA\Files_Versions
+ */
+class VersionRestoredEvent extends Event {
+ public function __construct(
+ private IVersion $version,
+ ) {
+ }
+
+ /**
+ * Version that was restored
+ */
+ public function getVersion(): IVersion {
+ return $this->version;
+ }
+}
diff --git a/apps/files_versions/lib/expiration.php b/apps/files_versions/lib/Expiration.php
index ffc7640e7f9..1e04d93379f 100644
--- a/apps/files_versions/lib/expiration.php
+++ b/apps/files_versions/lib/Expiration.php
@@ -1,36 +1,20 @@
<?php
+
/**
- * @author Victor Dubiniuk <dubiniuk@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
-
namespace OCA\Files_Versions;
-use \OCP\IConfig;
-use \OCP\AppFramework\Utility\ITimeFactory;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\IConfig;
+use Psr\Log\LoggerInterface;
class Expiration {
// how long do we keep files a version if no other value is defined in the config file (unit: days)
- const NO_OBLIGATION = -1;
-
- /** @var ITimeFactory */
- private $timeFactory;
+ public const NO_OBLIGATION = -1;
/** @var string */
private $retentionObligation;
@@ -44,8 +28,11 @@ class Expiration {
/** @var bool */
private $canPurgeToSaveSpace;
- public function __construct(IConfig $config,ITimeFactory $timeFactory){
- $this->timeFactory = $timeFactory;
+ public function __construct(
+ IConfig $config,
+ private ITimeFactory $timeFactory,
+ private LoggerInterface $logger,
+ ) {
$this->retentionObligation = $config->getSystemValue('versions_retention_obligation', 'auto');
if ($this->retentionObligation !== 'disabled') {
@@ -57,14 +44,14 @@ class Expiration {
* Is versions expiration enabled
* @return bool
*/
- public function isEnabled(){
+ public function isEnabled(): bool {
return $this->retentionObligation !== 'disabled';
}
/**
* Is default expiration active
*/
- public function shouldAutoExpire(){
+ public function shouldAutoExpire(): bool {
return $this->minAge === self::NO_OBLIGATION
|| $this->maxAge === self::NO_OBLIGATION;
}
@@ -75,7 +62,7 @@ class Expiration {
* @param bool $quotaExceeded
* @return bool
*/
- public function isExpired($timestamp, $quotaExceeded = false){
+ public function isExpired(int $timestamp, bool $quotaExceeded = false): bool {
// No expiration if disabled
if (!$this->isEnabled()) {
return false;
@@ -89,7 +76,7 @@ class Expiration {
$time = $this->timeFactory->getTime();
// Never expire dates in future e.g. misconfiguration or negative time
// adjustment
- if ($time<$timestamp) {
+ if ($time < $timestamp) {
return false;
}
@@ -113,10 +100,25 @@ class Expiration {
}
/**
+ * Get minimal retention obligation as a timestamp
+ *
+ * @return int|false
+ */
+ public function getMinAgeAsTimestamp() {
+ $minAge = false;
+ if ($this->isEnabled() && $this->minAge !== self::NO_OBLIGATION) {
+ $time = $this->timeFactory->getTime();
+ $minAge = $time - ($this->minAge * 86400);
+ }
+ return $minAge;
+ }
+
+ /**
* Get maximal retention obligation as a timestamp
- * @return int
+ *
+ * @return int|false
*/
- public function getMaxAgeAsTimestamp(){
+ public function getMaxAgeAsTimestamp() {
$maxAge = false;
if ($this->isEnabled() && $this->maxAge !== self::NO_OBLIGATION) {
$time = $this->timeFactory->getTime();
@@ -126,10 +128,10 @@ class Expiration {
}
/**
- * Read versions_retention_obligation, validate it
- * and set private members accordingly
- */
- private function parseRetentionObligation(){
+ * Read versions_retention_obligation, validate it
+ * and set private members accordingly
+ */
+ private function parseRetentionObligation(): void {
$splitValues = explode(',', $this->retentionObligation);
if (!isset($splitValues[0])) {
$minValue = 'auto';
@@ -147,21 +149,21 @@ class Expiration {
// Validate
if (!ctype_digit($minValue) && $minValue !== 'auto') {
$isValid = false;
- \OC::$server->getLogger()->warning(
- $minValue . ' is not a valid value for minimal versions retention obligation. Check versions_retention_obligation in your config.php. Falling back to auto.',
- ['app'=>'files_versions']
+ $this->logger->warning(
+ $minValue . ' is not a valid value for minimal versions retention obligation. Check versions_retention_obligation in your config.php. Falling back to auto.',
+ ['app' => 'files_versions']
);
}
if (!ctype_digit($maxValue) && $maxValue !== 'auto') {
$isValid = false;
- \OC::$server->getLogger()->warning(
- $maxValue . ' is not a valid value for maximal versions retention obligation. Check versions_retention_obligation in your config.php. Falling back to auto.',
- ['app'=>'files_versions']
+ $this->logger->warning(
+ $maxValue . ' is not a valid value for maximal versions retention obligation. Check versions_retention_obligation in your config.php. Falling back to auto.',
+ ['app' => 'files_versions']
);
}
- if (!$isValid){
+ if (!$isValid) {
$minValue = 'auto';
$maxValue = 'auto';
}
@@ -174,13 +176,13 @@ class Expiration {
$this->canPurgeToSaveSpace = true;
} elseif ($minValue !== 'auto' && $maxValue === 'auto') {
// Keep for X days but delete anytime if space needed
- $this->minAge = intval($minValue);
+ $this->minAge = (int)$minValue;
$this->maxAge = self::NO_OBLIGATION;
$this->canPurgeToSaveSpace = true;
} elseif ($minValue === 'auto' && $maxValue !== 'auto') {
// Delete anytime if space needed, Delete all older than max automatically
$this->minAge = self::NO_OBLIGATION;
- $this->maxAge = intval($maxValue);
+ $this->maxAge = (int)$maxValue;
$this->canPurgeToSaveSpace = true;
} elseif ($minValue !== 'auto' && $maxValue !== 'auto') {
// Delete all older than max OR older than min if space needed
@@ -190,8 +192,8 @@ class Expiration {
$maxValue = $minValue;
}
- $this->minAge = intval($minValue);
- $this->maxAge = intval($maxValue);
+ $this->minAge = (int)$minValue;
+ $this->maxAge = (int)$maxValue;
$this->canPurgeToSaveSpace = false;
}
}
diff --git a/apps/files_versions/lib/Listener/FileEventsListener.php b/apps/files_versions/lib/Listener/FileEventsListener.php
new file mode 100644
index 00000000000..969ca4ded45
--- /dev/null
+++ b/apps/files_versions/lib/Listener/FileEventsListener.php
@@ -0,0 +1,472 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_Versions\Listener;
+
+use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
+use OC\DB\Exceptions\DbalException;
+use OC\Files\Filesystem;
+use OC\Files\Mount\MoveableMount;
+use OC\Files\Node\NonExistingFile;
+use OC\Files\Node\NonExistingFolder;
+use OC\Files\View;
+use OCA\Files_Versions\Storage;
+use OCA\Files_Versions\Versions\INeedSyncVersionBackend;
+use OCA\Files_Versions\Versions\IVersionManager;
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\DB\Exception;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+use OCP\Files\Events\Node\BeforeNodeCopiedEvent;
+use OCP\Files\Events\Node\BeforeNodeDeletedEvent;
+use OCP\Files\Events\Node\BeforeNodeRenamedEvent;
+use OCP\Files\Events\Node\BeforeNodeTouchedEvent;
+use OCP\Files\Events\Node\BeforeNodeWrittenEvent;
+use OCP\Files\Events\Node\NodeCopiedEvent;
+use OCP\Files\Events\Node\NodeCreatedEvent;
+use OCP\Files\Events\Node\NodeDeletedEvent;
+use OCP\Files\Events\Node\NodeRenamedEvent;
+use OCP\Files\Events\Node\NodeTouchedEvent;
+use OCP\Files\Events\Node\NodeWrittenEvent;
+use OCP\Files\File;
+use OCP\Files\Folder;
+use OCP\Files\IMimeTypeLoader;
+use OCP\Files\IRootFolder;
+use OCP\Files\Node;
+use OCP\Files\NotFoundException;
+use OCP\IUserSession;
+use Psr\Log\LoggerInterface;
+
+/** @template-implements IEventListener<BeforeNodeCopiedEvent|BeforeNodeDeletedEvent|BeforeNodeRenamedEvent|BeforeNodeTouchedEvent|BeforeNodeWrittenEvent|NodeCopiedEvent|NodeCreatedEvent|NodeDeletedEvent|NodeRenamedEvent|NodeTouchedEvent|NodeWrittenEvent> */
+class FileEventsListener implements IEventListener {
+ /**
+ * @var array<int, array>
+ */
+ private array $writeHookInfo = [];
+ /**
+ * @var array<int, Node>
+ */
+ private array $nodesTouched = [];
+ /**
+ * @var array<string, Node>
+ */
+ private array $versionsDeleted = [];
+
+ public function __construct(
+ private IRootFolder $rootFolder,
+ private IVersionManager $versionManager,
+ private IMimeTypeLoader $mimeTypeLoader,
+ private IUserSession $userSession,
+ private LoggerInterface $logger,
+ ) {
+ }
+
+ public function handle(Event $event): void {
+ if ($event instanceof NodeCreatedEvent) {
+ $this->created($event->getNode());
+ }
+
+ if ($event instanceof BeforeNodeTouchedEvent) {
+ $this->pre_touch_hook($event->getNode());
+ }
+
+ if ($event instanceof NodeTouchedEvent) {
+ $this->touch_hook($event->getNode());
+ }
+
+ if ($event instanceof BeforeNodeWrittenEvent) {
+ $this->write_hook($event->getNode());
+ }
+
+ if ($event instanceof NodeWrittenEvent) {
+ $this->post_write_hook($event->getNode());
+ }
+
+ if ($event instanceof BeforeNodeDeletedEvent) {
+ $this->pre_remove_hook($event->getNode());
+ }
+
+ if ($event instanceof NodeDeletedEvent) {
+ $this->remove_hook($event->getNode());
+ }
+
+ if ($event instanceof NodeRenamedEvent) {
+ $this->rename_hook($event->getSource(), $event->getTarget());
+ }
+
+ if ($event instanceof NodeCopiedEvent) {
+ $this->copy_hook($event->getSource(), $event->getTarget());
+ }
+
+ if ($event instanceof BeforeNodeRenamedEvent) {
+ $this->pre_renameOrCopy_hook($event->getSource(), $event->getTarget());
+ }
+
+ if ($event instanceof BeforeNodeCopiedEvent) {
+ $this->pre_renameOrCopy_hook($event->getSource(), $event->getTarget());
+ }
+ }
+
+ public function pre_touch_hook(Node $node): void {
+ // Do not handle folders.
+ if ($node instanceof Folder) {
+ return;
+ }
+
+ // $node is a non-existing on file creation.
+ if ($node instanceof NonExistingFile) {
+ return;
+ }
+
+ $this->nodesTouched[$node->getId()] = $node;
+ }
+
+ public function touch_hook(Node $node): void {
+ // Do not handle folders.
+ if ($node instanceof Folder) {
+ return;
+ }
+
+ if ($node instanceof NonExistingFile) {
+ $this->logger->error(
+ 'Failed to create or update version for {path}, node does not exist',
+ [
+ 'path' => $node->getPath(),
+ ]
+ );
+
+ return;
+ }
+
+ $previousNode = $this->nodesTouched[$node->getId()] ?? null;
+
+ if ($previousNode === null) {
+ return;
+ }
+
+ unset($this->nodesTouched[$node->getId()]);
+
+ try {
+ if ($node instanceof File && $this->versionManager instanceof INeedSyncVersionBackend) {
+ $revision = $this->versionManager->getRevision($previousNode);
+
+ // We update the timestamp of the version entity associated with the previousNode.
+ $this->versionManager->updateVersionEntity($node, $revision, ['timestamp' => $node->getMTime()]);
+ }
+ } catch (DbalException $ex) {
+ // Ignore UniqueConstraintViolationException, as we are probably in the middle of a rollback
+ // Where the previous node would temporary have the mtime of the old version, so the rollback touches it to fix it.
+ if (!($ex->getPrevious() instanceof UniqueConstraintViolationException)) {
+ throw $ex;
+ }
+ } catch (DoesNotExistException $ex) {
+ // Ignore DoesNotExistException, as we are probably in the middle of a rollback
+ // Where the previous node would temporary have a wrong mtime, so the rollback touches it to fix it.
+ }
+ }
+
+ public function created(Node $node): void {
+ // Do not handle folders.
+ if (!($node instanceof File)) {
+ return;
+ }
+
+ if ($node instanceof NonExistingFile) {
+ $this->logger->error(
+ 'Failed to create version for {path}, node does not exist',
+ [
+ 'path' => $node->getPath(),
+ ]
+ );
+
+ return;
+ }
+
+ if ($this->versionManager instanceof INeedSyncVersionBackend) {
+ $this->versionManager->createVersionEntity($node);
+ }
+ }
+
+ /**
+ * listen to write event.
+ */
+ public function write_hook(Node $node): void {
+ // Do not handle folders.
+ if ($node instanceof Folder) {
+ return;
+ }
+
+ // $node is a non-existing on file creation.
+ if ($node instanceof NonExistingFile) {
+ return;
+ }
+
+ $path = $this->getPathForNode($node);
+ $result = Storage::store($path);
+
+ // Store the result of the version creation so it can be used in post_write_hook.
+ $this->writeHookInfo[$node->getId()] = [
+ 'previousNode' => $node,
+ 'versionCreated' => $result !== false
+ ];
+ }
+
+ /**
+ * listen to post_write event.
+ */
+ public function post_write_hook(Node $node): void {
+ // Do not handle folders.
+ if ($node instanceof Folder) {
+ return;
+ }
+
+ if ($node instanceof NonExistingFile) {
+ $this->logger->error(
+ 'Failed to create or update version for {path}, node does not exist',
+ [
+ 'path' => $node->getPath(),
+ ]
+ );
+
+ return;
+ }
+
+ $writeHookInfo = $this->writeHookInfo[$node->getId()] ?? null;
+
+ if ($writeHookInfo === null) {
+ return;
+ }
+
+ if (
+ $writeHookInfo['versionCreated']
+ && $node->getMTime() !== $writeHookInfo['previousNode']->getMTime()
+ ) {
+ // If a new version was created, insert a version in the DB for the current content.
+ // If both versions have the same mtime, it means the latest version file simply got overrode,
+ // so no need to create a new version.
+ $this->created($node);
+ } else {
+ try {
+ // If no new version was stored in the FS, no new version should be added in the DB.
+ // So we simply update the associated version.
+ if ($node instanceof File && $this->versionManager instanceof INeedSyncVersionBackend) {
+ $revision = $this->versionManager->getRevision($writeHookInfo['previousNode']);
+
+ $this->versionManager->updateVersionEntity(
+ $node,
+ $revision,
+ [
+ 'timestamp' => $node->getMTime(),
+ 'size' => $node->getSize(),
+ 'mimetype' => $this->mimeTypeLoader->getId($node->getMimetype()),
+ ],
+ );
+ }
+ } catch (DoesNotExistException $e) {
+ // This happens if the versions app was not enabled while the file was created or updated the last time.
+ // meaning there is no such revision and we need to create this file.
+ if ($writeHookInfo['versionCreated']) {
+ $this->created($node);
+ } else {
+ // Normally this should not happen so we re-throw the exception to not hide any potential issues.
+ throw $e;
+ }
+ } catch (Exception $e) {
+ $this->logger->error('Failed to update existing version for ' . $node->getPath(), [
+ 'exception' => $e,
+ 'versionCreated' => $writeHookInfo['versionCreated'],
+ 'previousNode' => [
+ 'size' => $writeHookInfo['previousNode']->getSize(),
+ 'mtime' => $writeHookInfo['previousNode']->getMTime(),
+ ],
+ 'node' => [
+ 'size' => $node->getSize(),
+ 'mtime' => $node->getMTime(),
+ ]
+ ]);
+ throw $e;
+ }
+ }
+
+ unset($this->writeHookInfo[$node->getId()]);
+ }
+
+ /**
+ * Erase versions of deleted file
+ *
+ * This function is connected to the NodeDeletedEvent event
+ * cleanup the versions directory if the actual file gets deleted
+ */
+ public function remove_hook(Node $node): void {
+ // Need to normalize the path as there is an issue with path concatenation in View.php::getAbsolutePath.
+ $path = Filesystem::normalizePath($node->getPath());
+ if (!array_key_exists($path, $this->versionsDeleted)) {
+ return;
+ }
+ $node = $this->versionsDeleted[$path];
+ $relativePath = $this->getPathForNode($node);
+ unset($this->versionsDeleted[$path]);
+ Storage::delete($relativePath);
+ // If no new version was stored in the FS, no new version should be added in the DB.
+ // So we simply update the associated version.
+ if ($node instanceof File && $this->versionManager instanceof INeedSyncVersionBackend) {
+ $this->versionManager->deleteVersionsEntity($node);
+ }
+ }
+
+ /**
+ * mark file as "deleted" so that we can clean up the versions if the file is gone
+ */
+ public function pre_remove_hook(Node $node): void {
+ $path = $this->getPathForNode($node);
+ Storage::markDeletedFile($path);
+ $this->versionsDeleted[$node->getPath()] = $node;
+ }
+
+ /**
+ * rename/move versions of renamed/moved files
+ *
+ * This function is connected to the NodeRenamedEvent event and adjust the name and location
+ * of the stored versions along the actual file
+ */
+ public function rename_hook(Node $source, Node $target): void {
+ $sourceBackend = $this->versionManager->getBackendForStorage($source->getParent()->getStorage());
+ $targetBackend = $this->versionManager->getBackendForStorage($target->getStorage());
+ // If different backends, do nothing.
+ if ($sourceBackend !== $targetBackend) {
+ return;
+ }
+
+ $oldPath = $this->getPathForNode($source);
+ $newPath = $this->getPathForNode($target);
+ Storage::renameOrCopy($oldPath, $newPath, 'rename');
+ }
+
+ /**
+ * copy versions of copied files
+ *
+ * This function is connected to the NodeCopiedEvent event and copies the
+ * the stored versions to the new location
+ */
+ public function copy_hook(Node $source, Node $target): void {
+ $sourceBackend = $this->versionManager->getBackendForStorage($source->getParent()->getStorage());
+ $targetBackend = $this->versionManager->getBackendForStorage($target->getStorage());
+ // If different backends, do nothing.
+ if ($sourceBackend !== $targetBackend) {
+ return;
+ }
+
+ $oldPath = $this->getPathForNode($source);
+ $newPath = $this->getPathForNode($target);
+ Storage::renameOrCopy($oldPath, $newPath, 'copy');
+ }
+
+ /**
+ * Remember owner and the owner path of the source file.
+ * If the file already exists, then it was a upload of a existing file
+ * over the web interface and we call Storage::store() directly
+ *
+ *
+ */
+ public function pre_renameOrCopy_hook(Node $source, Node $target): void {
+ $sourceBackend = $this->versionManager->getBackendForStorage($source->getStorage());
+ $targetBackend = $this->versionManager->getBackendForStorage($target->getParent()->getStorage());
+ // If different backends, do nothing.
+ if ($sourceBackend !== $targetBackend) {
+ return;
+ }
+
+ // if we rename a movable mount point, then the versions don't have to be renamed
+ $oldPath = $this->getPathForNode($source);
+ $newPath = $this->getPathForNode($target);
+ if ($oldPath === null || $newPath === null) {
+ return;
+ }
+
+ $user = $this->userSession->getUser()?->getUID();
+ if ($user === null) {
+ return;
+ }
+
+ $absOldPath = Filesystem::normalizePath('/' . $user . '/files' . $oldPath);
+ $manager = Filesystem::getMountManager();
+ $mount = $manager->find($absOldPath);
+ $internalPath = $mount->getInternalPath($absOldPath);
+ if ($internalPath === '' and $mount instanceof MoveableMount) {
+ return;
+ }
+
+ $view = new View($user . '/files');
+ if ($view->file_exists($newPath)) {
+ Storage::store($newPath);
+ } else {
+ Storage::setSourcePathAndUser($oldPath);
+ }
+ }
+
+ /**
+ * Retrieve the path relative to the current user root folder.
+ * If no user is connected, try to use the node's owner.
+ */
+ private function getPathForNode(Node $node): ?string {
+ $user = $this->userSession->getUser()?->getUID();
+ if ($user) {
+ $path = $this->rootFolder
+ ->getUserFolder($user)
+ ->getRelativePath($node->getPath());
+
+ if ($path !== null) {
+ return $path;
+ }
+ }
+
+ try {
+ $owner = $node->getOwner()?->getUid();
+ } catch (NotFoundException) {
+ $owner = null;
+ }
+
+ // If no owner, extract it from the path.
+ // e.g. /user/files/foobar.txt
+ if (!$owner) {
+ $parts = explode('/', $node->getPath(), 4);
+ if (count($parts) === 4) {
+ $owner = $parts[1];
+ }
+ }
+
+ if ($owner) {
+ $path = $this->rootFolder
+ ->getUserFolder($owner)
+ ->getRelativePath($node->getPath());
+
+ if ($path !== null) {
+ return $path;
+ }
+ }
+
+ if (!($node instanceof NonExistingFile) && !($node instanceof NonExistingFolder)) {
+ $this->logger->debug('Failed to compute path for node', [
+ 'node' => [
+ 'path' => $node->getPath(),
+ 'owner' => $owner,
+ 'fileid' => $node->getId(),
+ 'size' => $node->getSize(),
+ 'mtime' => $node->getMTime(),
+ ]
+ ]);
+ } else {
+ $this->logger->debug('Failed to compute path for node', [
+ 'node' => [
+ 'path' => $node->getPath(),
+ 'owner' => $owner,
+ ]
+ ]);
+ }
+ return null;
+ }
+}
diff --git a/apps/files_versions/lib/Listener/LegacyRollbackListener.php b/apps/files_versions/lib/Listener/LegacyRollbackListener.php
new file mode 100644
index 00000000000..072c1511caa
--- /dev/null
+++ b/apps/files_versions/lib/Listener/LegacyRollbackListener.php
@@ -0,0 +1,35 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_Versions\Listener;
+
+use OCA\Files_Versions\Events\VersionRestoredEvent;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+
+/**
+ * This listener is designed to be compatible with third-party code
+ * that can still use a hook. This listener will be removed in
+ * the next version and the rollback hook will stop working.
+ *
+ * @deprecated 32.0.0
+ * @template-implements IEventListener<VersionRestoredEvent>
+ */
+class LegacyRollbackListener implements IEventListener {
+ public function handle(Event $event): void {
+ if (!($event instanceof VersionRestoredEvent)) {
+ return;
+ }
+ $version = $event->getVersion();
+ \OC_Hook::emit('\OCP\Versions', 'rollback', [
+ 'path' => $version->getVersionPath(),
+ 'revision' => $version->getRevisionId(),
+ 'node' => $version->getSourceFile(),
+ ]);
+ }
+}
diff --git a/apps/files_versions/lib/Listener/LoadAdditionalListener.php b/apps/files_versions/lib/Listener/LoadAdditionalListener.php
new file mode 100644
index 00000000000..cb955629c0f
--- /dev/null
+++ b/apps/files_versions/lib/Listener/LoadAdditionalListener.php
@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_Versions\Listener;
+
+use OCA\Files\Event\LoadAdditionalScriptsEvent;
+use OCA\Files_Versions\AppInfo\Application;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+use OCP\Util;
+
+/** @template-implements IEventListener<LoadAdditionalScriptsEvent> */
+class LoadAdditionalListener implements IEventListener {
+ public function handle(Event $event): void {
+ if (!($event instanceof LoadAdditionalScriptsEvent)) {
+ return;
+ }
+
+ // TODO: make sure to only include the sidebar script when
+ // we properly split it between files list and sidebar
+ Util::addScript(Application::APP_ID, 'files_versions');
+ }
+}
diff --git a/apps/files_versions/lib/Listener/LoadSidebarListener.php b/apps/files_versions/lib/Listener/LoadSidebarListener.php
new file mode 100644
index 00000000000..b8d13fa4810
--- /dev/null
+++ b/apps/files_versions/lib/Listener/LoadSidebarListener.php
@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_Versions\Listener;
+
+use OCA\Files\Event\LoadSidebar;
+use OCA\Files_Versions\AppInfo\Application;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+use OCP\Util;
+
+/** @template-implements IEventListener<LoadSidebar> */
+class LoadSidebarListener implements IEventListener {
+ public function handle(Event $event): void {
+ if (!($event instanceof LoadSidebar)) {
+ return;
+ }
+
+ // TODO: make sure to only include the sidebar script when
+ // we properly split it between files list and sidebar
+ Util::addScript(Application::APP_ID, 'files_versions');
+ }
+}
diff --git a/apps/files_versions/lib/Listener/VersionAuthorListener.php b/apps/files_versions/lib/Listener/VersionAuthorListener.php
new file mode 100644
index 00000000000..9b93b1f888b
--- /dev/null
+++ b/apps/files_versions/lib/Listener/VersionAuthorListener.php
@@ -0,0 +1,56 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_Versions\Listener;
+
+use OC\Files\Node\Folder;
+use OCA\Files_Versions\Sabre\Plugin;
+use OCA\Files_Versions\Versions\IMetadataVersionBackend;
+use OCA\Files_Versions\Versions\IVersionManager;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+use OCP\Files\Events\Node\NodeWrittenEvent;
+use OCP\Files\Node;
+use OCP\IUserSession;
+
+/** @template-implements IEventListener<NodeWrittenEvent> */
+class VersionAuthorListener implements IEventListener {
+ public function __construct(
+ private IVersionManager $versionManager,
+ private IUserSession $userSession,
+ ) {
+ }
+
+ /**
+ * @abstract handles events from a nodes version being changed
+ * @param Event $event the event that triggered this listener to activate
+ */
+ public function handle(Event $event): void {
+ if ($event instanceof NodeWrittenEvent) {
+ $this->post_write_hook($event->getNode());
+ }
+ }
+
+ /**
+ * @abstract handles the NodeWrittenEvent, and sets the metadata for the associated node
+ * @param Node $node the node that is currently being written
+ */
+ public function post_write_hook(Node $node): void {
+ $user = $this->userSession->getUser();
+ // Do not handle folders or users that we cannot get metadata from
+ if ($node instanceof Folder || is_null($user)) {
+ return;
+ }
+ // check if our version manager supports setting the metadata
+ if ($this->versionManager instanceof IMetadataVersionBackend) {
+ $revision = $this->versionManager->getRevision($node);
+ $author = $user->getUID();
+ $this->versionManager->setMetadataValue($node, $revision, Plugin::AUTHOR, $author);
+ }
+ }
+}
diff --git a/apps/files_versions/lib/Listener/VersionStorageMoveListener.php b/apps/files_versions/lib/Listener/VersionStorageMoveListener.php
new file mode 100644
index 00000000000..d0a0bcf4a92
--- /dev/null
+++ b/apps/files_versions/lib/Listener/VersionStorageMoveListener.php
@@ -0,0 +1,140 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files_Versions\Listener;
+
+use Exception;
+use OC\Files\Node\NonExistingFile;
+use OC\Files\Node\NonExistingFolder;
+use OCA\Files_Versions\Versions\IVersionBackend;
+use OCA\Files_Versions\Versions\IVersionManager;
+use OCA\Files_Versions\Versions\IVersionsImporterBackend;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+use OCP\Files\Events\Node\AbstractNodesEvent;
+use OCP\Files\Events\Node\BeforeNodeRenamedEvent;
+use OCP\Files\Events\Node\NodeCopiedEvent;
+use OCP\Files\Events\Node\NodeRenamedEvent;
+use OCP\Files\File;
+use OCP\Files\Folder;
+use OCP\Files\Node;
+use OCP\Files\Storage\IStorage;
+use OCP\IUser;
+use OCP\IUserSession;
+
+/** @template-implements IEventListener<Event> */
+class VersionStorageMoveListener implements IEventListener {
+ /** @var File[] */
+ private array $movedNodes = [];
+
+ public function __construct(
+ private IVersionManager $versionManager,
+ private IUserSession $userSession,
+ ) {
+ }
+
+ /**
+ * @abstract Moves version across storages if necessary.
+ * @throws Exception No user in session
+ */
+ public function handle(Event $event): void {
+ if (!($event instanceof AbstractNodesEvent)) {
+ return;
+ }
+
+ $source = $event->getSource();
+ $target = $event->getTarget();
+
+ $sourceStorage = $this->getNodeStorage($source);
+ $targetStorage = $this->getNodeStorage($target);
+
+ $sourceBackend = $this->versionManager->getBackendForStorage($sourceStorage);
+ $targetBackend = $this->versionManager->getBackendForStorage($targetStorage);
+
+ // If same backend, nothing to do.
+ if ($sourceBackend === $targetBackend) {
+ return;
+ }
+
+ $user = $this->userSession->getUser() ?? $source->getOwner();
+
+ if ($user === null) {
+ throw new Exception('Cannot move versions across storages without a user.');
+ }
+
+ if ($event instanceof BeforeNodeRenamedEvent) {
+ $this->recursivelyPrepareMove($source);
+ } elseif ($event instanceof NodeRenamedEvent || $event instanceof NodeCopiedEvent) {
+ $this->recursivelyHandleMoveOrCopy($event, $user, $source, $target, $sourceBackend, $targetBackend);
+ }
+ }
+
+ /**
+ * Store all sub files in this->movedNodes so their info can be used after the operation.
+ */
+ private function recursivelyPrepareMove(Node $source): void {
+ if ($source instanceof File) {
+ $this->movedNodes[$source->getId()] = $source;
+ } elseif ($source instanceof Folder) {
+ foreach ($source->getDirectoryListing() as $child) {
+ $this->recursivelyPrepareMove($child);
+ }
+ }
+ }
+
+ /**
+ * Call handleMoveOrCopy on each sub files
+ * @param NodeRenamedEvent|NodeCopiedEvent $event
+ */
+ private function recursivelyHandleMoveOrCopy(Event $event, IUser $user, ?Node $source, Node $target, IVersionBackend $sourceBackend, IVersionBackend $targetBackend): void {
+ if ($target instanceof File) {
+ if ($event instanceof NodeRenamedEvent) {
+ $source = $this->movedNodes[$target->getId()];
+ }
+
+ /** @var File $source */
+ $this->handleMoveOrCopy($event, $user, $source, $target, $sourceBackend, $targetBackend);
+ } elseif ($target instanceof Folder) {
+ /** @var Folder $source */
+ foreach ($target->getDirectoryListing() as $targetChild) {
+ if ($event instanceof NodeCopiedEvent) {
+ $sourceChild = $source->get($targetChild->getName());
+ } else {
+ $sourceChild = null;
+ }
+
+ $this->recursivelyHandleMoveOrCopy($event, $user, $sourceChild, $targetChild, $sourceBackend, $targetBackend);
+ }
+ }
+ }
+
+ /**
+ * Called only during NodeRenamedEvent or NodeCopiedEvent
+ * Will send the source node versions to the new backend, and then delete them from the old backend.
+ * @param NodeRenamedEvent|NodeCopiedEvent $event
+ */
+ private function handleMoveOrCopy(Event $event, IUser $user, File $source, File $target, IVersionBackend $sourceBackend, IVersionBackend $targetBackend): void {
+ if ($targetBackend instanceof IVersionsImporterBackend) {
+ $versions = $sourceBackend->getVersionsForFile($user, $source);
+ $targetBackend->importVersionsForFile($user, $source, $target, $versions);
+ }
+
+ if ($event instanceof NodeRenamedEvent && $sourceBackend instanceof IVersionsImporterBackend) {
+ $sourceBackend->clearVersionsForFile($user, $source, $target);
+ }
+ }
+
+ private function getNodeStorage(Node $node): IStorage {
+ if ($node instanceof NonExistingFile || $node instanceof NonExistingFolder) {
+ return $node->getParent()->getStorage();
+ } else {
+ return $node->getStorage();
+ }
+ }
+}
diff --git a/apps/files_versions/lib/Migration/Version1020Date20221114144058.php b/apps/files_versions/lib/Migration/Version1020Date20221114144058.php
new file mode 100644
index 00000000000..77c8c2201f3
--- /dev/null
+++ b/apps/files_versions/lib/Migration/Version1020Date20221114144058.php
@@ -0,0 +1,67 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files_Versions\Migration;
+
+use Closure;
+use OCP\DB\ISchemaWrapper;
+use OCP\DB\Types;
+use OCP\Migration\IOutput;
+use OCP\Migration\SimpleMigrationStep;
+
+/**
+ * Auto-generated migration step: Please modify to your needs!
+ */
+class Version1020Date20221114144058 extends SimpleMigrationStep {
+ /**
+ * @param IOutput $output
+ * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
+ * @param array $options
+ * @return null|ISchemaWrapper
+ */
+ public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
+ /** @var ISchemaWrapper $schema */
+ $schema = $schemaClosure();
+
+ if ($schema->hasTable('files_versions')) {
+ return null;
+ }
+
+ $table = $schema->createTable('files_versions');
+ $table->addColumn('id', Types::BIGINT, [
+ 'autoincrement' => true,
+ 'notnull' => true,
+ 'length' => 20,
+ ]);
+ $table->addColumn('file_id', Types::BIGINT, [
+ 'notnull' => true,
+ 'length' => 20,
+ ]);
+ $table->addColumn('timestamp', Types::BIGINT, [
+ 'notnull' => true,
+ 'length' => 20,
+ ]);
+ $table->addColumn('size', Types::BIGINT, [
+ 'notnull' => true,
+ 'length' => 20,
+ ]);
+ $table->addColumn('mimetype', Types::BIGINT, [
+ 'notnull' => true,
+ 'length' => 20,
+ ]);
+ $table->addColumn('metadata', Types::JSON, [
+ 'notnull' => true,
+ ]);
+
+ $table->setPrimaryKey(['id']);
+ $table->addUniqueIndex(['file_id', 'timestamp'], 'files_versions_uniq_index');
+
+ return $schema;
+ }
+}
diff --git a/apps/files_versions/lib/Sabre/Plugin.php b/apps/files_versions/lib/Sabre/Plugin.php
new file mode 100644
index 00000000000..984c4a36e5b
--- /dev/null
+++ b/apps/files_versions/lib/Sabre/Plugin.php
@@ -0,0 +1,99 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_Versions\Sabre;
+
+use OC\AppFramework\Http\Request;
+use OCA\DAV\Connector\Sabre\FilesPlugin;
+use OCP\IPreview;
+use OCP\IRequest;
+use Sabre\DAV\Exception\NotFound;
+use Sabre\DAV\INode;
+use Sabre\DAV\PropFind;
+use Sabre\DAV\PropPatch;
+use Sabre\DAV\Server;
+use Sabre\DAV\ServerPlugin;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
+
+class Plugin extends ServerPlugin {
+ private Server $server;
+
+ public const LABEL = 'label';
+
+ public const AUTHOR = 'author';
+
+ public const VERSION_LABEL = '{http://nextcloud.org/ns}version-label';
+
+ public const VERSION_AUTHOR = '{http://nextcloud.org/ns}version-author'; // dav property for author
+
+ public function __construct(
+ private IRequest $request,
+ private IPreview $previewManager,
+ ) {
+ $this->request = $request;
+ }
+
+ public function initialize(Server $server) {
+ $this->server = $server;
+
+ $server->on('afterMethod:GET', [$this, 'afterGet']);
+ $server->on('propFind', [$this, 'propFind']);
+ $server->on('propPatch', [$this, 'propPatch']);
+ }
+
+ public function afterGet(RequestInterface $request, ResponseInterface $response) {
+ $path = $request->getPath();
+ if (!str_starts_with($path, 'versions')) {
+ return;
+ }
+
+ try {
+ $node = $this->server->tree->getNodeForPath($path);
+ } catch (NotFound $e) {
+ return;
+ }
+
+ if (!($node instanceof VersionFile)) {
+ return;
+ }
+
+ $filename = $node->getVersion()->getSourceFileName();
+
+ if ($this->request->isUserAgent(
+ [
+ Request::USER_AGENT_IE,
+ Request::USER_AGENT_ANDROID_MOBILE_CHROME,
+ Request::USER_AGENT_FREEBOX,
+ ])) {
+ $response->addHeader('Content-Disposition', 'attachment; filename="' . rawurlencode($filename) . '"');
+ } else {
+ $response->addHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . rawurlencode($filename)
+ . '; filename="' . rawurlencode($filename) . '"');
+ }
+ }
+
+ public function propFind(PropFind $propFind, INode $node): void {
+ if ($node instanceof VersionFile) {
+ $propFind->handle(self::VERSION_LABEL, fn () => $node->getMetadataValue(self::LABEL));
+ $propFind->handle(self::VERSION_AUTHOR, fn () => $node->getMetadataValue(self::AUTHOR));
+ $propFind->handle(
+ FilesPlugin::HAS_PREVIEW_PROPERTYNAME,
+ fn (): string => $this->previewManager->isMimeSupported($node->getContentType()) ? 'true' : 'false',
+ );
+ }
+ }
+
+ public function propPatch($path, PropPatch $propPatch): void {
+ $node = $this->server->tree->getNodeForPath($path);
+
+ if ($node instanceof VersionFile) {
+ $propPatch->handle(self::VERSION_LABEL, fn (string $label) => $node->setMetadataValue(self::LABEL, $label));
+ }
+ }
+}
diff --git a/apps/files_versions/lib/Sabre/RestoreFolder.php b/apps/files_versions/lib/Sabre/RestoreFolder.php
new file mode 100644
index 00000000000..7904b098a4f
--- /dev/null
+++ b/apps/files_versions/lib/Sabre/RestoreFolder.php
@@ -0,0 +1,61 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_Versions\Sabre;
+
+use Sabre\DAV\Exception\Forbidden;
+use Sabre\DAV\ICollection;
+use Sabre\DAV\IMoveTarget;
+use Sabre\DAV\INode;
+
+class RestoreFolder implements ICollection, IMoveTarget {
+ public function createFile($name, $data = null) {
+ throw new Forbidden();
+ }
+
+ public function createDirectory($name) {
+ throw new Forbidden();
+ }
+
+ public function getChild($name) {
+ return null;
+ }
+
+ public function delete() {
+ throw new Forbidden();
+ }
+
+ public function getName() {
+ return 'restore';
+ }
+
+ public function setName($name) {
+ throw new Forbidden();
+ }
+
+ public function getLastModified(): int {
+ return 0;
+ }
+
+ public function getChildren(): array {
+ return [];
+ }
+
+ public function childExists($name): bool {
+ return false;
+ }
+
+ public function moveInto($targetName, $sourcePath, INode $sourceNode): bool {
+ if (!($sourceNode instanceof VersionFile)) {
+ return false;
+ }
+
+ $sourceNode->rollBack();
+ return true;
+ }
+}
diff --git a/apps/files_versions/lib/Sabre/RootCollection.php b/apps/files_versions/lib/Sabre/RootCollection.php
new file mode 100644
index 00000000000..1e7129f23da
--- /dev/null
+++ b/apps/files_versions/lib/Sabre/RootCollection.php
@@ -0,0 +1,55 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_Versions\Sabre;
+
+use OCA\Files_Versions\Versions\IVersionManager;
+use OCP\Files\IRootFolder;
+use OCP\IConfig;
+use OCP\IUserManager;
+use OCP\IUserSession;
+use Sabre\DAV\INode;
+use Sabre\DAVACL\AbstractPrincipalCollection;
+use Sabre\DAVACL\PrincipalBackend;
+
+class RootCollection extends AbstractPrincipalCollection {
+
+ public function __construct(
+ PrincipalBackend\BackendInterface $principalBackend,
+ private IRootFolder $rootFolder,
+ IConfig $config,
+ private IUserManager $userManager,
+ private IVersionManager $versionManager,
+ private IUserSession $userSession,
+ ) {
+ parent::__construct($principalBackend, 'principals/users');
+
+ $this->disableListing = !$config->getSystemValue('debug', false);
+ }
+
+ /**
+ * This method returns a node for a principal.
+ *
+ * The passed array contains principal information, and is guaranteed to
+ * at least contain a uri item. Other properties may or may not be
+ * supplied by the authentication backend.
+ *
+ * @param array $principalInfo
+ * @return INode
+ */
+ public function getChildForPrincipal(array $principalInfo) {
+ [, $name] = \Sabre\Uri\split($principalInfo['uri']);
+ $user = $this->userSession->getUser();
+ if (is_null($user) || $name !== $user->getUID()) {
+ throw new \Sabre\DAV\Exception\Forbidden();
+ }
+ return new VersionHome($principalInfo, $this->rootFolder, $this->userManager, $this->versionManager);
+ }
+
+ public function getName() {
+ return 'versions';
+ }
+}
diff --git a/apps/files_versions/lib/Sabre/VersionCollection.php b/apps/files_versions/lib/Sabre/VersionCollection.php
new file mode 100644
index 00000000000..375d5cf99f2
--- /dev/null
+++ b/apps/files_versions/lib/Sabre/VersionCollection.php
@@ -0,0 +1,81 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_Versions\Sabre;
+
+use OCA\Files_Versions\Versions\IVersion;
+use OCA\Files_Versions\Versions\IVersionManager;
+use OCP\Files\File;
+use OCP\IUser;
+use Sabre\DAV\Exception\Forbidden;
+use Sabre\DAV\Exception\NotFound;
+use Sabre\DAV\ICollection;
+
+class VersionCollection implements ICollection {
+
+ public function __construct(
+ private File $file,
+ private IUser $user,
+ private IVersionManager $versionManager,
+ ) {
+ }
+
+ public function createFile($name, $data = null) {
+ throw new Forbidden();
+ }
+
+ public function createDirectory($name) {
+ throw new Forbidden();
+ }
+
+ public function getChild($name) {
+ /** @var VersionFile[] $versions */
+ $versions = $this->getChildren();
+
+ foreach ($versions as $version) {
+ if ($version->getName() === $name) {
+ return $version;
+ }
+ }
+
+ throw new NotFound();
+ }
+
+ public function getChildren(): array {
+ $versions = $this->versionManager->getVersionsForFile($this->user, $this->file);
+
+ return array_map(function (IVersion $version) {
+ return new VersionFile($version, $this->versionManager);
+ }, $versions);
+ }
+
+ public function childExists($name): bool {
+ try {
+ $this->getChild($name);
+ return true;
+ } catch (NotFound $e) {
+ return false;
+ }
+ }
+
+ public function delete() {
+ throw new Forbidden();
+ }
+
+ public function getName(): string {
+ return (string)$this->file->getId();
+ }
+
+ public function setName($name) {
+ throw new Forbidden();
+ }
+
+ public function getLastModified(): int {
+ return 0;
+ }
+}
diff --git a/apps/files_versions/lib/Sabre/VersionFile.php b/apps/files_versions/lib/Sabre/VersionFile.php
new file mode 100644
index 00000000000..faa03473648
--- /dev/null
+++ b/apps/files_versions/lib/Sabre/VersionFile.php
@@ -0,0 +1,108 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_Versions\Sabre;
+
+use OCA\Files_Versions\Versions\IDeletableVersionBackend;
+use OCA\Files_Versions\Versions\IMetadataVersion;
+use OCA\Files_Versions\Versions\IMetadataVersionBackend;
+use OCA\Files_Versions\Versions\INameableVersion;
+use OCA\Files_Versions\Versions\INameableVersionBackend;
+use OCA\Files_Versions\Versions\IVersion;
+use OCA\Files_Versions\Versions\IVersionManager;
+use OCP\Files\NotFoundException;
+use Sabre\DAV\Exception\Forbidden;
+use Sabre\DAV\Exception\NotFound;
+use Sabre\DAV\IFile;
+
+class VersionFile implements IFile {
+ public function __construct(
+ private IVersion $version,
+ private IVersionManager $versionManager,
+ ) {
+ }
+
+ public function put($data) {
+ throw new Forbidden();
+ }
+
+ public function get() {
+ try {
+ return $this->versionManager->read($this->version);
+ } catch (NotFoundException $e) {
+ throw new NotFound();
+ }
+ }
+
+ public function getContentType(): string {
+ return $this->version->getMimeType();
+ }
+
+ public function getETag(): string {
+ return (string)$this->version->getRevisionId();
+ }
+
+ /**
+ * @psalm-suppress ImplementedReturnTypeMismatch \Sabre\DAV\IFile::getSize signature does not support 32bit
+ * @return int|float
+ */
+ public function getSize(): int|float {
+ return $this->version->getSize();
+ }
+
+ public function delete() {
+ if ($this->versionManager instanceof IDeletableVersionBackend) {
+ $this->versionManager->deleteVersion($this->version);
+ } else {
+ throw new Forbidden();
+ }
+ }
+
+ public function getName(): string {
+ return (string)$this->version->getRevisionId();
+ }
+
+ public function setName($name) {
+ throw new Forbidden();
+ }
+
+ public function setMetadataValue(string $key, string $value): bool {
+ $backend = $this->version->getBackend();
+
+ if ($backend instanceof IMetadataVersionBackend) {
+ $backend->setMetadataValue($this->version->getSourceFile(), $this->version->getTimestamp(), $key, $value);
+ return true;
+ } elseif ($key === 'label' && $backend instanceof INameableVersionBackend) {
+ $backend->setVersionLabel($this->version, $value);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public function getMetadataValue(string $key): ?string {
+ if ($this->version instanceof IMetadataVersion) {
+ return $this->version->getMetadataValue($key);
+ } elseif ($key === 'label' && $this->version instanceof INameableVersion) {
+ return $this->version->getLabel();
+ }
+ return null;
+ }
+
+ public function getLastModified(): int {
+ return $this->version->getTimestamp();
+ }
+
+ public function rollBack() {
+ $this->versionManager->rollback($this->version);
+ }
+
+ public function getVersion(): IVersion {
+ return $this->version;
+ }
+}
diff --git a/apps/files_versions/lib/Sabre/VersionHome.php b/apps/files_versions/lib/Sabre/VersionHome.php
new file mode 100644
index 00000000000..07ac491f2a1
--- /dev/null
+++ b/apps/files_versions/lib/Sabre/VersionHome.php
@@ -0,0 +1,82 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_Versions\Sabre;
+
+use OC\User\NoUserException;
+use OCA\Files_Versions\Versions\IVersionManager;
+use OCP\Files\IRootFolder;
+use OCP\IUserManager;
+use Sabre\DAV\Exception\Forbidden;
+use Sabre\DAV\ICollection;
+
+class VersionHome implements ICollection {
+
+ public function __construct(
+ private array $principalInfo,
+ private IRootFolder $rootFolder,
+ private IUserManager $userManager,
+ private IVersionManager $versionManager,
+ ) {
+ }
+
+ private function getUser() {
+ [, $name] = \Sabre\Uri\split($this->principalInfo['uri']);
+ $user = $this->userManager->get($name);
+ if (!$user) {
+ throw new NoUserException();
+ }
+ return $user;
+ }
+
+ public function delete() {
+ throw new Forbidden();
+ }
+
+ public function getName(): string {
+ return $this->getUser()->getUID();
+ }
+
+ public function setName($name) {
+ throw new Forbidden();
+ }
+
+ public function createFile($name, $data = null) {
+ throw new Forbidden();
+ }
+
+ public function createDirectory($name) {
+ throw new Forbidden();
+ }
+
+ public function getChild($name) {
+ $user = $this->getUser();
+
+ if ($name === 'versions') {
+ return new VersionRoot($user, $this->rootFolder, $this->versionManager);
+ }
+ if ($name === 'restore') {
+ return new RestoreFolder();
+ }
+ }
+
+ public function getChildren() {
+ $user = $this->getUser();
+
+ return [
+ new VersionRoot($user, $this->rootFolder, $this->versionManager),
+ new RestoreFolder(),
+ ];
+ }
+
+ public function childExists($name) {
+ return $name === 'versions' || $name === 'restore';
+ }
+
+ public function getLastModified() {
+ return 0;
+ }
+}
diff --git a/apps/files_versions/lib/Sabre/VersionRoot.php b/apps/files_versions/lib/Sabre/VersionRoot.php
new file mode 100644
index 00000000000..7f7014fbee3
--- /dev/null
+++ b/apps/files_versions/lib/Sabre/VersionRoot.php
@@ -0,0 +1,81 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_Versions\Sabre;
+
+use OCA\Files_Versions\Versions\IVersionManager;
+use OCP\Files\File;
+use OCP\Files\IRootFolder;
+use OCP\IUser;
+use Sabre\DAV\Exception\Forbidden;
+use Sabre\DAV\Exception\NotFound;
+use Sabre\DAV\ICollection;
+
+class VersionRoot implements ICollection {
+
+ public function __construct(
+ private IUser $user,
+ private IRootFolder $rootFolder,
+ private IVersionManager $versionManager,
+ ) {
+ }
+
+ public function delete() {
+ throw new Forbidden();
+ }
+
+ public function getName(): string {
+ return 'versions';
+ }
+
+ public function setName($name) {
+ throw new Forbidden();
+ }
+
+ public function createFile($name, $data = null) {
+ throw new Forbidden();
+ }
+
+ public function createDirectory($name) {
+ throw new Forbidden();
+ }
+
+ public function getChild($name) {
+ $userFolder = $this->rootFolder->getUserFolder($this->user->getUID());
+
+ $fileId = (int)$name;
+ $node = $userFolder->getFirstNodeById($fileId);
+
+ if (!$node) {
+ throw new NotFound();
+ }
+
+ if (!$node instanceof File) {
+ throw new NotFound();
+ }
+
+ return new VersionCollection($node, $this->user, $this->versionManager);
+ }
+
+ public function getChildren(): array {
+ return [];
+ }
+
+ public function childExists($name): bool {
+ try {
+ $this->getChild($name);
+ return true;
+ } catch (NotFound $e) {
+ return false;
+ }
+ }
+
+ public function getLastModified(): int {
+ return 0;
+ }
+}
diff --git a/apps/files_versions/lib/Storage.php b/apps/files_versions/lib/Storage.php
new file mode 100644
index 00000000000..6d53a19a518
--- /dev/null
+++ b/apps/files_versions/lib/Storage.php
@@ -0,0 +1,1007 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace OCA\Files_Versions;
+
+use OC\Files\Filesystem;
+use OC\Files\ObjectStore\ObjectStoreStorage;
+use OC\Files\Search\SearchBinaryOperator;
+use OC\Files\Search\SearchComparison;
+use OC\Files\Search\SearchQuery;
+use OC\Files\View;
+use OC\User\NoUserException;
+use OC_User;
+use OCA\Files_Sharing\SharedMount;
+use OCA\Files_Versions\AppInfo\Application;
+use OCA\Files_Versions\Command\Expire;
+use OCA\Files_Versions\Db\VersionsMapper;
+use OCA\Files_Versions\Events\CreateVersionEvent;
+use OCA\Files_Versions\Versions\IVersionManager;
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\Command\IBus;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\Files;
+use OCP\Files\FileInfo;
+use OCP\Files\Folder;
+use OCP\Files\IMimeTypeDetector;
+use OCP\Files\IRootFolder;
+use OCP\Files\Node;
+use OCP\Files\NotFoundException;
+use OCP\Files\NotPermittedException;
+use OCP\Files\Search\ISearchBinaryOperator;
+use OCP\Files\Search\ISearchComparison;
+use OCP\Files\Storage\IWriteStreamStorage;
+use OCP\Files\StorageInvalidException;
+use OCP\Files\StorageNotAvailableException;
+use OCP\IURLGenerator;
+use OCP\IUser;
+use OCP\IUserManager;
+use OCP\Lock\ILockingProvider;
+use OCP\Server;
+use OCP\Util;
+use Psr\Log\LoggerInterface;
+
+class Storage {
+ public const DEFAULTENABLED = true;
+ public const DEFAULTMAXSIZE = 50; // unit: percentage; 50% of available disk space/quota
+ public const VERSIONS_ROOT = 'files_versions/';
+
+ public const DELETE_TRIGGER_MASTER_REMOVED = 0;
+ public const DELETE_TRIGGER_RETENTION_CONSTRAINT = 1;
+ public const DELETE_TRIGGER_QUOTA_EXCEEDED = 2;
+
+ // files for which we can remove the versions after the delete operation was successful
+ private static $deletedFiles = [];
+
+ private static $sourcePathAndUser = [];
+
+ private static $max_versions_per_interval = [
+ //first 10sec, one version every 2sec
+ 1 => ['intervalEndsAfter' => 10, 'step' => 2],
+ //next minute, one version every 10sec
+ 2 => ['intervalEndsAfter' => 60, 'step' => 10],
+ //next hour, one version every minute
+ 3 => ['intervalEndsAfter' => 3600, 'step' => 60],
+ //next 24h, one version every hour
+ 4 => ['intervalEndsAfter' => 86400, 'step' => 3600],
+ //next 30days, one version per day
+ 5 => ['intervalEndsAfter' => 2592000, 'step' => 86400],
+ //until the end one version per week
+ 6 => ['intervalEndsAfter' => -1, 'step' => 604800],
+ ];
+
+ /** @var Application */
+ private static $application;
+
+ /**
+ * get the UID of the owner of the file and the path to the file relative to
+ * owners files folder
+ *
+ * @param string $filename
+ * @return array
+ * @throws NoUserException
+ */
+ public static function getUidAndFilename($filename) {
+ $uid = Filesystem::getOwner($filename);
+ $userManager = Server::get(IUserManager::class);
+ // if the user with the UID doesn't exists, e.g. because the UID points
+ // to a remote user with a federated cloud ID we use the current logged-in
+ // user. We need a valid local user to create the versions
+ if (!$userManager->userExists($uid)) {
+ $uid = OC_User::getUser();
+ }
+ Filesystem::initMountPoints($uid);
+ if ($uid !== OC_User::getUser()) {
+ $info = Filesystem::getFileInfo($filename);
+ $ownerView = new View('/' . $uid . '/files');
+ try {
+ $filename = $ownerView->getPath($info['fileid']);
+ // make sure that the file name doesn't end with a trailing slash
+ // can for example happen single files shared across servers
+ $filename = rtrim($filename, '/');
+ } catch (NotFoundException $e) {
+ $filename = null;
+ }
+ }
+ return [$uid, $filename];
+ }
+
+ /**
+ * Remember the owner and the owner path of the source file
+ *
+ * @param string $source source path
+ */
+ public static function setSourcePathAndUser($source) {
+ [$uid, $path] = self::getUidAndFilename($source);
+ self::$sourcePathAndUser[$source] = ['uid' => $uid, 'path' => $path];
+ }
+
+ /**
+ * Gets the owner and the owner path from the source path
+ *
+ * @param string $source source path
+ * @return array with user id and path
+ */
+ public static function getSourcePathAndUser($source) {
+ if (isset(self::$sourcePathAndUser[$source])) {
+ $uid = self::$sourcePathAndUser[$source]['uid'];
+ $path = self::$sourcePathAndUser[$source]['path'];
+ unset(self::$sourcePathAndUser[$source]);
+ } else {
+ $uid = $path = false;
+ }
+ return [$uid, $path];
+ }
+
+ /**
+ * get current size of all versions from a given user
+ *
+ * @param string $user user who owns the versions
+ * @return int versions size
+ */
+ private static function getVersionsSize($user) {
+ $view = new View('/' . $user);
+ $fileInfo = $view->getFileInfo('/files_versions');
+ return isset($fileInfo['size']) ? $fileInfo['size'] : 0;
+ }
+
+ /**
+ * store a new version of a file.
+ */
+ public static function store($filename) {
+ // if the file gets streamed we need to remove the .part extension
+ // to get the right target
+ $ext = pathinfo($filename, PATHINFO_EXTENSION);
+ if ($ext === 'part') {
+ $filename = substr($filename, 0, -5);
+ }
+
+ // we only handle existing files
+ if (! Filesystem::file_exists($filename) || Filesystem::is_dir($filename)) {
+ return false;
+ }
+
+ // since hook paths are always relative to the "default filesystem view"
+ // we always use the owner from there to get the full node
+ $uid = Filesystem::getView()->getOwner('');
+
+ /** @var IRootFolder $rootFolder */
+ $rootFolder = Server::get(IRootFolder::class);
+ $userFolder = $rootFolder->getUserFolder($uid);
+
+ $eventDispatcher = Server::get(IEventDispatcher::class);
+ try {
+ $file = $userFolder->get($filename);
+ } catch (NotFoundException $e) {
+ return false;
+ }
+
+ $mount = $file->getMountPoint();
+ if ($mount instanceof SharedMount) {
+ $ownerFolder = $rootFolder->getUserFolder($mount->getShare()->getShareOwner());
+ $ownerNode = $ownerFolder->getFirstNodeById($file->getId());
+ if ($ownerNode) {
+ $file = $ownerNode;
+ $uid = $mount->getShare()->getShareOwner();
+ }
+ }
+
+ /** @var IUserManager $userManager */
+ $userManager = Server::get(IUserManager::class);
+ $user = $userManager->get($uid);
+
+ if (!$user) {
+ return false;
+ }
+
+ // no use making versions for empty files
+ if ($file->getSize() === 0) {
+ return false;
+ }
+
+ $event = new CreateVersionEvent($file);
+ $eventDispatcher->dispatch('OCA\Files_Versions::createVersion', $event);
+ if ($event->shouldCreateVersion() === false) {
+ return false;
+ }
+
+ /** @var IVersionManager $versionManager */
+ $versionManager = Server::get(IVersionManager::class);
+
+ $versionManager->createVersion($user, $file);
+ }
+
+
+ /**
+ * mark file as deleted so that we can remove the versions if the file is gone
+ * @param string $path
+ */
+ public static function markDeletedFile($path) {
+ [$uid, $filename] = self::getUidAndFilename($path);
+ self::$deletedFiles[$path] = [
+ 'uid' => $uid,
+ 'filename' => $filename];
+ }
+
+ /**
+ * delete the version from the storage and cache
+ *
+ * @param View $view
+ * @param string $path
+ */
+ protected static function deleteVersion($view, $path) {
+ $view->unlink($path);
+ /**
+ * @var \OC\Files\Storage\Storage $storage
+ * @var string $internalPath
+ */
+ [$storage, $internalPath] = $view->resolvePath($path);
+ $cache = $storage->getCache($internalPath);
+ $cache->remove($internalPath);
+ }
+
+ /**
+ * Delete versions of a file
+ */
+ public static function delete($path) {
+ $deletedFile = self::$deletedFiles[$path];
+ $uid = $deletedFile['uid'];
+ $filename = $deletedFile['filename'];
+
+ if (!Filesystem::file_exists($path)) {
+ $view = new View('/' . $uid . '/files_versions');
+
+ $versions = self::getVersions($uid, $filename);
+ if (!empty($versions)) {
+ foreach ($versions as $v) {
+ \OC_Hook::emit('\OCP\Versions', 'preDelete', ['path' => $path . $v['version'], 'trigger' => self::DELETE_TRIGGER_MASTER_REMOVED]);
+ self::deleteVersion($view, $filename . '.v' . $v['version']);
+ \OC_Hook::emit('\OCP\Versions', 'delete', ['path' => $path . $v['version'], 'trigger' => self::DELETE_TRIGGER_MASTER_REMOVED]);
+ }
+ }
+ }
+ unset(self::$deletedFiles[$path]);
+ }
+
+ /**
+ * Delete a version of a file
+ */
+ public static function deleteRevision(string $path, int $revision): void {
+ [$uid, $filename] = self::getUidAndFilename($path);
+ $view = new View('/' . $uid . '/files_versions');
+ \OC_Hook::emit('\OCP\Versions', 'preDelete', ['path' => $path . $revision, 'trigger' => self::DELETE_TRIGGER_MASTER_REMOVED]);
+ self::deleteVersion($view, $filename . '.v' . $revision);
+ \OC_Hook::emit('\OCP\Versions', 'delete', ['path' => $path . $revision, 'trigger' => self::DELETE_TRIGGER_MASTER_REMOVED]);
+ }
+
+ /**
+ * Rename or copy versions of a file of the given paths
+ *
+ * @param string $sourcePath source path of the file to move, relative to
+ * the currently logged in user's "files" folder
+ * @param string $targetPath target path of the file to move, relative to
+ * the currently logged in user's "files" folder
+ * @param string $operation can be 'copy' or 'rename'
+ */
+ public static function renameOrCopy($sourcePath, $targetPath, $operation) {
+ [$sourceOwner, $sourcePath] = self::getSourcePathAndUser($sourcePath);
+
+ // it was a upload of a existing file if no old path exists
+ // in this case the pre-hook already called the store method and we can
+ // stop here
+ if ($sourcePath === false) {
+ return true;
+ }
+
+ [$targetOwner, $targetPath] = self::getUidAndFilename($targetPath);
+
+ $sourcePath = ltrim($sourcePath, '/');
+ $targetPath = ltrim($targetPath, '/');
+
+ $rootView = new View('');
+
+ // did we move a directory ?
+ if ($rootView->is_dir('/' . $targetOwner . '/files/' . $targetPath)) {
+ // does the directory exists for versions too ?
+ if ($rootView->is_dir('/' . $sourceOwner . '/files_versions/' . $sourcePath)) {
+ // create missing dirs if necessary
+ self::createMissingDirectories($targetPath, new View('/' . $targetOwner));
+
+ // move the directory containing the versions
+ $rootView->$operation(
+ '/' . $sourceOwner . '/files_versions/' . $sourcePath,
+ '/' . $targetOwner . '/files_versions/' . $targetPath
+ );
+ }
+ } elseif ($versions = Storage::getVersions($sourceOwner, '/' . $sourcePath)) {
+ // create missing dirs if necessary
+ self::createMissingDirectories($targetPath, new View('/' . $targetOwner));
+
+ foreach ($versions as $v) {
+ // move each version one by one to the target directory
+ $rootView->$operation(
+ '/' . $sourceOwner . '/files_versions/' . $sourcePath . '.v' . $v['version'],
+ '/' . $targetOwner . '/files_versions/' . $targetPath . '.v' . $v['version']
+ );
+ }
+ }
+
+ // 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);
+ }
+ }
+
+ /**
+ * Rollback to an old version of a file.
+ *
+ * @param string $file file name
+ * @param int $revision revision timestamp
+ * @return bool
+ */
+ public static function rollback(string $file, int $revision, IUser $user) {
+ // add expected leading slash
+ $filename = '/' . ltrim($file, '/');
+
+ // Fetch the userfolder to trigger view hooks
+ $root = Server::get(IRootFolder::class);
+ $userFolder = $root->getUserFolder($user->getUID());
+
+ $users_view = new View('/' . $user->getUID());
+ $files_view = new View('/' . $user->getUID() . '/files');
+
+ $versionCreated = false;
+
+ $fileInfo = $files_view->getFileInfo($file);
+
+ // check if user has the permissions to revert a version
+ if (!$fileInfo->isUpdateable()) {
+ return false;
+ }
+
+ //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, 'files_versions' . $filename . '.v' . $users_view->filemtime('files' . $filename));
+ $versionCreated = true;
+ }
+
+ $fileToRestore = 'files_versions' . $filename . '.v' . $revision;
+
+ // Restore encrypted version of the old file for the newly restored file
+ // This has to happen manually here since the file is manually copied below
+ $oldVersion = $users_view->getFileInfo($fileToRestore)->getEncryptedVersion();
+ $oldFileInfo = $users_view->getFileInfo($fileToRestore);
+ $cache = $fileInfo->getStorage()->getCache();
+ $cache->update(
+ $fileInfo->getId(), [
+ 'encrypted' => $oldVersion,
+ 'encryptedVersion' => $oldVersion,
+ 'size' => $oldFileInfo->getData()['size'],
+ 'unencrypted_size' => $oldFileInfo->getData()['unencrypted_size'],
+ ]
+ );
+
+ // rollback
+ if (self::copyFileContents($users_view, $fileToRestore, 'files' . $filename)) {
+ $files_view->touch($file, $revision);
+ Storage::scheduleExpire($user->getUID(), $file);
+
+ return true;
+ } elseif ($versionCreated) {
+ self::deleteVersion($users_view, $version);
+ }
+
+ return false;
+ }
+
+ /**
+ * Stream copy file contents from $path1 to $path2
+ *
+ * @param View $view view to use for copying
+ * @param string $path1 source file to copy
+ * @param string $path2 target file
+ *
+ * @return bool true for success, false otherwise
+ */
+ private static function copyFileContents($view, $path1, $path2) {
+ /** @var \OC\Files\Storage\Storage $storage1 */
+ [$storage1, $internalPath1] = $view->resolvePath($path1);
+ /** @var \OC\Files\Storage\Storage $storage2 */
+ [$storage2, $internalPath2] = $view->resolvePath($path2);
+
+ $view->lockFile($path1, ILockingProvider::LOCK_EXCLUSIVE);
+ $view->lockFile($path2, ILockingProvider::LOCK_EXCLUSIVE);
+
+ try {
+ // TODO add a proper way of overwriting a file while maintaining file ids
+ if ($storage1->instanceOfStorage(ObjectStoreStorage::class)
+ || $storage2->instanceOfStorage(ObjectStoreStorage::class)
+ ) {
+ $source = $storage1->fopen($internalPath1, 'r');
+ $result = $source !== false;
+ if ($result) {
+ if ($storage2->instanceOfStorage(IWriteStreamStorage::class)) {
+ /** @var IWriteStreamStorage $storage2 */
+ $storage2->writeStream($internalPath2, $source);
+ } else {
+ $target = $storage2->fopen($internalPath2, 'w');
+ $result = $target !== false;
+ if ($result) {
+ [, $result] = Files::streamCopy($source, $target, true);
+ }
+ // explicit check as S3 library closes streams already
+ if (is_resource($target)) {
+ fclose($target);
+ }
+ }
+ }
+ // explicit check as S3 library closes streams already
+ if (is_resource($source)) {
+ fclose($source);
+ }
+
+ if ($result !== false) {
+ $storage1->unlink($internalPath1);
+ }
+ } else {
+ $result = $storage2->moveFromStorage($storage1, $internalPath1, $internalPath2);
+ }
+ } finally {
+ $view->unlockFile($path1, ILockingProvider::LOCK_EXCLUSIVE);
+ $view->unlockFile($path2, ILockingProvider::LOCK_EXCLUSIVE);
+ }
+
+ return ($result !== false);
+ }
+
+ /**
+ * get a list of all available 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
+ * @param string $userFullPath
+ * @return array versions newest version first
+ */
+ public static function getVersions($uid, $filename, $userFullPath = '') {
+ $versions = [];
+ if (empty($filename)) {
+ return $versions;
+ }
+ // fetch for old versions
+ $view = new View('/' . $uid . '/');
+
+ $pathinfo = pathinfo($filename);
+ $versionedFile = $pathinfo['basename'];
+
+ $dir = Filesystem::normalizePath(self::VERSIONS_ROOT . '/' . $pathinfo['dirname']);
+
+ $dirContent = false;
+ if ($view->is_dir($dir)) {
+ $dirContent = $view->opendir($dir);
+ }
+
+ if ($dirContent === false) {
+ return $versions;
+ }
+
+ if (is_resource($dirContent)) {
+ while (($entryName = readdir($dirContent)) !== false) {
+ if (!Filesystem::isIgnoredDir($entryName)) {
+ $pathparts = pathinfo($entryName);
+ $filename = $pathparts['filename'];
+ if ($filename === $versionedFile) {
+ $pathparts = pathinfo($entryName);
+ $timestamp = substr($pathparts['extension'] ?? '', 1);
+ if (!is_numeric($timestamp)) {
+ Server::get(LoggerInterface::class)->error(
+ 'Version file {path} has incorrect name format',
+ [
+ 'path' => $entryName,
+ 'app' => 'files_versions',
+ ]
+ );
+ continue;
+ }
+ $filename = $pathparts['filename'];
+ $key = $timestamp . '#' . $filename;
+ $versions[$key]['version'] = $timestamp;
+ $versions[$key]['humanReadableTimestamp'] = self::getHumanReadableTimestamp((int)$timestamp);
+ if (empty($userFullPath)) {
+ $versions[$key]['preview'] = '';
+ } else {
+ /** @var IURLGenerator $urlGenerator */
+ $urlGenerator = Server::get(IURLGenerator::class);
+ $versions[$key]['preview'] = $urlGenerator->linkToRoute('files_version.Preview.getPreview',
+ ['file' => $userFullPath, 'version' => $timestamp]);
+ }
+ $versions[$key]['path'] = Filesystem::normalizePath($pathinfo['dirname'] . '/' . $filename);
+ $versions[$key]['name'] = $versionedFile;
+ $versions[$key]['size'] = $view->filesize($dir . '/' . $entryName);
+ $versions[$key]['mimetype'] = Server::get(IMimeTypeDetector::class)->detectPath($versionedFile);
+ }
+ }
+ }
+ closedir($dirContent);
+ }
+
+ // sort with newest version first
+ krsort($versions);
+
+ return $versions;
+ }
+
+ /**
+ * Expire versions that older than max version retention time
+ *
+ * @param string $uid
+ */
+ public static function expireOlderThanMaxForUser($uid) {
+ /** @var IRootFolder $root */
+ $root = Server::get(IRootFolder::class);
+ try {
+ /** @var Folder $versionsRoot */
+ $versionsRoot = $root->get('/' . $uid . '/files_versions');
+ } catch (NotFoundException $e) {
+ return;
+ }
+
+ $expiration = self::getExpiration();
+ $threshold = $expiration->getMaxAgeAsTimestamp();
+ if (!$threshold) {
+ return;
+ }
+
+ $allVersions = $versionsRoot->search(new SearchQuery(
+ new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_NOT, [
+ new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', FileInfo::MIMETYPE_FOLDER),
+ ]),
+ 0,
+ 0,
+ []
+ ));
+
+ /** @var VersionsMapper $versionsMapper */
+ $versionsMapper = Server::get(VersionsMapper::class);
+ $userFolder = $root->getUserFolder($uid);
+ $versionEntities = [];
+
+ /** @var Node[] $versions */
+ $versions = array_filter($allVersions, function (Node $info) use ($threshold, $userFolder, $versionsMapper, $versionsRoot, &$versionEntities) {
+ // Check that the file match '*.v*'
+ $versionsBegin = strrpos($info->getName(), '.v');
+ if ($versionsBegin === false) {
+ return false;
+ }
+
+ $version = (int)substr($info->getName(), $versionsBegin + 2);
+
+ // Check that the version does not have a label.
+ $path = $versionsRoot->getRelativePath($info->getPath());
+ if ($path === null) {
+ throw new DoesNotExistException('Could not find relative path of (' . $info->getPath() . ')');
+ }
+
+ try {
+ $node = $userFolder->get(substr($path, 0, -strlen('.v' . $version)));
+ $versionEntity = $versionsMapper->findVersionForFileId($node->getId(), $version);
+ $versionEntities[$info->getId()] = $versionEntity;
+
+ if ($versionEntity->getMetadataValue('label') !== null && $versionEntity->getMetadataValue('label') !== '') {
+ return false;
+ }
+ } catch (NotFoundException $e) {
+ // Original node not found, delete the version
+ return true;
+ } catch (StorageNotAvailableException|StorageInvalidException $e) {
+ // Storage can't be used, but it might only be temporary so we can't always delete the version
+ // since we can't determine if the version is named we take the safe route and don't expire
+ return false;
+ } catch (DoesNotExistException $ex) {
+ // Version on FS can have no equivalent in the DB if they were created before the version naming feature.
+ // So we ignore DoesNotExistException.
+ }
+
+ // Check that the version's timestamp is lower than $threshold
+ return $version < $threshold;
+ });
+
+ foreach ($versions as $version) {
+ $internalPath = $version->getInternalPath();
+ \OC_Hook::emit('\OCP\Versions', 'preDelete', ['path' => $internalPath, 'trigger' => self::DELETE_TRIGGER_RETENTION_CONSTRAINT]);
+
+ $versionEntity = isset($versionEntities[$version->getId()]) ? $versionEntities[$version->getId()] : null;
+ if (!is_null($versionEntity)) {
+ $versionsMapper->delete($versionEntity);
+ }
+
+ try {
+ $version->delete();
+ \OC_Hook::emit('\OCP\Versions', 'delete', ['path' => $internalPath, 'trigger' => self::DELETE_TRIGGER_RETENTION_CONSTRAINT]);
+ } catch (NotPermittedException $e) {
+ Server::get(LoggerInterface::class)->error("Missing permissions to delete version: {$internalPath}", ['app' => 'files_versions', 'exception' => $e]);
+ }
+ }
+ }
+
+ /**
+ * translate a timestamp into a string like "5 days ago"
+ *
+ * @param int $timestamp
+ * @return string for example "5 days ago"
+ */
+ private static function getHumanReadableTimestamp(int $timestamp): string {
+ $diff = time() - $timestamp;
+
+ if ($diff < 60) { // first minute
+ return $diff . ' seconds ago';
+ } elseif ($diff < 3600) { //first hour
+ return round($diff / 60) . ' minutes ago';
+ } elseif ($diff < 86400) { // first day
+ return round($diff / 3600) . ' hours ago';
+ } elseif ($diff < 604800) { //first week
+ return round($diff / 86400) . ' days ago';
+ } elseif ($diff < 2419200) { //first month
+ return round($diff / 604800) . ' weeks ago';
+ } elseif ($diff < 29030400) { // first year
+ return round($diff / 2419200) . ' months ago';
+ } else {
+ return round($diff / 29030400) . ' years ago';
+ }
+ }
+
+ /**
+ * returns all stored file versions from a given user
+ * @param string $uid id of the user
+ * @return array with contains two arrays 'all' which contains all versions sorted by age and 'by_file' which contains all versions sorted by filename
+ */
+ private static function getAllVersions($uid) {
+ $view = new View('/' . $uid . '/');
+ $dirs = [self::VERSIONS_ROOT];
+ $versions = [];
+
+ while (!empty($dirs)) {
+ $dir = array_pop($dirs);
+ $files = $view->getDirectoryContent($dir);
+
+ foreach ($files as $file) {
+ $fileData = $file->getData();
+ $filePath = $dir . '/' . $fileData['name'];
+ if ($file['type'] === 'dir') {
+ $dirs[] = $filePath;
+ } else {
+ $versionsBegin = strrpos($filePath, '.v');
+ $relPathStart = strlen(self::VERSIONS_ROOT);
+ $version = substr($filePath, $versionsBegin + 2);
+ $relpath = substr($filePath, $relPathStart, $versionsBegin - $relPathStart);
+ $key = $version . '#' . $relpath;
+ $versions[$key] = ['path' => $relpath, 'timestamp' => $version];
+ }
+ }
+ }
+
+ // newest version first
+ krsort($versions);
+
+ $result = [
+ 'all' => [],
+ 'by_file' => [],
+ ];
+
+ foreach ($versions as $key => $value) {
+ $size = $view->filesize(self::VERSIONS_ROOT . '/' . $value['path'] . '.v' . $value['timestamp']);
+ $filename = $value['path'];
+
+ $result['all'][$key]['version'] = $value['timestamp'];
+ $result['all'][$key]['path'] = $filename;
+ $result['all'][$key]['size'] = $size;
+
+ $result['by_file'][$filename][$key]['version'] = $value['timestamp'];
+ $result['by_file'][$filename][$key]['path'] = $filename;
+ $result['by_file'][$filename][$key]['size'] = $size;
+ }
+
+ return $result;
+ }
+
+ /**
+ * get list of files we want to expire
+ * @param array $versions list of versions
+ * @param integer $time
+ * @param bool $quotaExceeded is versions storage limit reached
+ * @return array containing the list of to deleted versions and the size of them
+ */
+ protected static function getExpireList($time, $versions, $quotaExceeded = false) {
+ $expiration = self::getExpiration();
+
+ if ($expiration->shouldAutoExpire()) {
+ // Exclude versions that are newer than the minimum age from the auto expiration logic.
+ $minAge = $expiration->getMinAgeAsTimestamp();
+ if ($minAge !== false) {
+ $versionsToAutoExpire = array_filter($versions, fn ($version) => $version['version'] < $minAge);
+ } else {
+ $versionsToAutoExpire = $versions;
+ }
+
+ [$toDelete, $size] = self::getAutoExpireList($time, $versionsToAutoExpire);
+ } else {
+ $size = 0;
+ $toDelete = []; // versions we want to delete
+ }
+
+ foreach ($versions as $key => $version) {
+ if (!is_numeric($version['version'])) {
+ Server::get(LoggerInterface::class)->error(
+ 'Found a non-numeric timestamp version: ' . json_encode($version),
+ ['app' => 'files_versions']);
+ continue;
+ }
+ if ($expiration->isExpired((int)($version['version']), $quotaExceeded) && !isset($toDelete[$key])) {
+ $size += $version['size'];
+ $toDelete[$key] = $version['path'] . '.v' . $version['version'];
+ }
+ }
+
+ return [$toDelete, $size];
+ }
+
+ /**
+ * get list of files we want to expire
+ * @param array $versions list of versions
+ * @param integer $time
+ * @return array containing the list of to deleted versions and the size of them
+ */
+ protected static function getAutoExpireList($time, $versions) {
+ $size = 0;
+ $toDelete = []; // versions we want to delete
+
+ $interval = 1;
+ $step = Storage::$max_versions_per_interval[$interval]['step'];
+ if (Storage::$max_versions_per_interval[$interval]['intervalEndsAfter'] === -1) {
+ $nextInterval = -1;
+ } else {
+ $nextInterval = $time - Storage::$max_versions_per_interval[$interval]['intervalEndsAfter'];
+ }
+
+ $firstVersion = reset($versions);
+
+ if ($firstVersion === false) {
+ return [$toDelete, $size];
+ }
+
+ $firstKey = key($versions);
+ $prevTimestamp = $firstVersion['version'];
+ $nextVersion = $firstVersion['version'] - $step;
+ unset($versions[$firstKey]);
+
+ foreach ($versions as $key => $version) {
+ $newInterval = true;
+ while ($newInterval) {
+ if ($nextInterval === -1 || $prevTimestamp > $nextInterval) {
+ if ($version['version'] > $nextVersion) {
+ //distance between two version too small, mark to delete
+ $toDelete[$key] = $version['path'] . '.v' . $version['version'];
+ $size += $version['size'];
+ Server::get(LoggerInterface::class)->info('Mark to expire ' . $version['path'] . ' next version should be ' . $nextVersion . ' or smaller. (prevTimestamp: ' . $prevTimestamp . '; step: ' . $step, ['app' => 'files_versions']);
+ } else {
+ $nextVersion = $version['version'] - $step;
+ $prevTimestamp = $version['version'];
+ }
+ $newInterval = false; // version checked so we can move to the next one
+ } else { // time to move on to the next interval
+ $interval++;
+ $step = Storage::$max_versions_per_interval[$interval]['step'];
+ $nextVersion = $prevTimestamp - $step;
+ if (Storage::$max_versions_per_interval[$interval]['intervalEndsAfter'] === -1) {
+ $nextInterval = -1;
+ } else {
+ $nextInterval = $time - Storage::$max_versions_per_interval[$interval]['intervalEndsAfter'];
+ }
+ $newInterval = true; // we changed the interval -> check same version with new interval
+ }
+ }
+ }
+
+ return [$toDelete, $size];
+ }
+
+ /**
+ * Schedule versions expiration for the given file
+ *
+ * @param string $uid owner of the file
+ * @param string $fileName file/folder for which to schedule expiration
+ */
+ public static function scheduleExpire($uid, $fileName) {
+ // let the admin disable auto expire
+ $expiration = self::getExpiration();
+ if ($expiration->isEnabled()) {
+ $command = new Expire($uid, $fileName);
+ /** @var IBus $bus */
+ $bus = Server::get(IBus::class);
+ $bus->push($command);
+ }
+ }
+
+ /**
+ * Expire versions which exceed the quota.
+ *
+ * This will setup the filesystem for the given user but will not
+ * tear it down afterwards.
+ *
+ * @param string $filename path to file to expire
+ * @param string $uid user for which to expire the version
+ * @return bool|int|null
+ */
+ public static function expire($filename, $uid) {
+ $expiration = self::getExpiration();
+
+ /** @var LoggerInterface $logger */
+ $logger = Server::get(LoggerInterface::class);
+
+ if ($expiration->isEnabled()) {
+ // get available disk space for user
+ $user = Server::get(IUserManager::class)->get($uid);
+ if (is_null($user)) {
+ $logger->error('Backends provided no user object for ' . $uid, ['app' => 'files_versions']);
+ throw new NoUserException('Backends provided no user object for ' . $uid);
+ }
+
+ \OC_Util::setupFS($uid);
+
+ try {
+ if (!Filesystem::file_exists($filename)) {
+ return false;
+ }
+ } catch (StorageNotAvailableException $e) {
+ // if we can't check that the file hasn't been deleted we can only assume that it hasn't
+ // note that this `StorageNotAvailableException` is about the file the versions originate from,
+ // not the storage that the versions are stored on
+ }
+
+ if (empty($filename)) {
+ // file maybe renamed or deleted
+ return false;
+ }
+ $versionsFileview = new View('/' . $uid . '/files_versions');
+
+ $softQuota = true;
+ $quota = $user->getQuota();
+ if ($quota === null || $quota === 'none') {
+ $quota = Filesystem::free_space('/');
+ $softQuota = false;
+ } else {
+ $quota = Util::computerFileSize($quota);
+ }
+
+ // make sure that we have the current size of the version history
+ $versionsSize = self::getVersionsSize($uid);
+
+ // calculate available space for version history
+ // subtract size of files and current versions size from quota
+ if ($quota >= 0) {
+ if ($softQuota) {
+ $root = Server::get(IRootFolder::class);
+ $userFolder = $root->getUserFolder($uid);
+ if (is_null($userFolder)) {
+ $availableSpace = 0;
+ } else {
+ $free = $quota - $userFolder->getSize(false); // remaining free space for user
+ if ($free > 0) {
+ $availableSpace = ($free * self::DEFAULTMAXSIZE / 100) - $versionsSize; // how much space can be used for versions
+ } else {
+ $availableSpace = $free - $versionsSize;
+ }
+ }
+ } else {
+ $availableSpace = $quota;
+ }
+ } else {
+ $availableSpace = PHP_INT_MAX;
+ }
+
+ $allVersions = Storage::getVersions($uid, $filename);
+
+ $time = time();
+ [$toDelete, $sizeOfDeletedVersions] = self::getExpireList($time, $allVersions, $availableSpace <= 0);
+
+ $availableSpace = $availableSpace + $sizeOfDeletedVersions;
+ $versionsSize = $versionsSize - $sizeOfDeletedVersions;
+
+ // if still not enough free space we rearrange the versions from all files
+ if ($availableSpace <= 0) {
+ $result = self::getAllVersions($uid);
+ $allVersions = $result['all'];
+
+ foreach ($result['by_file'] as $versions) {
+ [$toDeleteNew, $size] = self::getExpireList($time, $versions, $availableSpace <= 0);
+ $toDelete = array_merge($toDelete, $toDeleteNew);
+ $sizeOfDeletedVersions += $size;
+ }
+ $availableSpace = $availableSpace + $sizeOfDeletedVersions;
+ $versionsSize = $versionsSize - $sizeOfDeletedVersions;
+ }
+
+ foreach ($toDelete as $key => $path) {
+ // Make sure to cleanup version table relations as expire does not pass deleteVersion
+ try {
+ /** @var VersionsMapper $versionsMapper */
+ $versionsMapper = Server::get(VersionsMapper::class);
+ $file = Server::get(IRootFolder::class)->getUserFolder($uid)->get($filename);
+ $pathparts = pathinfo($path);
+ $timestamp = (int)substr($pathparts['extension'] ?? '', 1);
+ $versionEntity = $versionsMapper->findVersionForFileId($file->getId(), $timestamp);
+ if ($versionEntity->getMetadataValue('label') !== null && $versionEntity->getMetadataValue('label') !== '') {
+ continue;
+ }
+ $versionsMapper->delete($versionEntity);
+ } catch (DoesNotExistException $e) {
+ }
+
+ \OC_Hook::emit('\OCP\Versions', 'preDelete', ['path' => $path, 'trigger' => self::DELETE_TRIGGER_QUOTA_EXCEEDED]);
+ self::deleteVersion($versionsFileview, $path);
+ \OC_Hook::emit('\OCP\Versions', 'delete', ['path' => $path, 'trigger' => self::DELETE_TRIGGER_QUOTA_EXCEEDED]);
+ unset($allVersions[$key]); // update array with the versions we keep
+ $logger->info('Expire: ' . $path, ['app' => 'files_versions']);
+ }
+
+ // Check if enough space is available after versions are rearranged.
+ // If not we delete the oldest versions until we meet the size limit for versions,
+ // but always keep the two latest versions
+ $numOfVersions = count($allVersions) - 2 ;
+ $i = 0;
+ // sort oldest first and make sure that we start at the first element
+ ksort($allVersions);
+ reset($allVersions);
+ while ($availableSpace < 0 && $i < $numOfVersions) {
+ $version = current($allVersions);
+ \OC_Hook::emit('\OCP\Versions', 'preDelete', ['path' => $version['path'] . '.v' . $version['version'], 'trigger' => self::DELETE_TRIGGER_QUOTA_EXCEEDED]);
+ self::deleteVersion($versionsFileview, $version['path'] . '.v' . $version['version']);
+ \OC_Hook::emit('\OCP\Versions', 'delete', ['path' => $version['path'] . '.v' . $version['version'], 'trigger' => self::DELETE_TRIGGER_QUOTA_EXCEEDED]);
+ $logger->info('running out of space! Delete oldest version: ' . $version['path'] . '.v' . $version['version'], ['app' => 'files_versions']);
+ $versionsSize -= $version['size'];
+ $availableSpace += $version['size'];
+ next($allVersions);
+ $i++;
+ }
+
+ return $versionsSize; // finally return the new size of the version history
+ }
+
+ return false;
+ }
+
+ /**
+ * Create recursively missing directories inside of files_versions
+ * that match the given path to a file.
+ *
+ * @param string $filename $path to a file, relative to the user's
+ * "files" folder
+ * @param View $view view on data/user/
+ */
+ public static function createMissingDirectories($filename, $view) {
+ $dirname = Filesystem::normalizePath(dirname($filename));
+ $dirParts = explode('/', $dirname);
+ $dir = '/files_versions';
+ foreach ($dirParts as $part) {
+ $dir = $dir . '/' . $part;
+ if (!$view->file_exists($dir)) {
+ $view->mkdir($dir);
+ }
+ }
+ }
+
+ /**
+ * Static workaround
+ * @return Expiration
+ */
+ protected static function getExpiration() {
+ if (self::$application === null) {
+ self::$application = Server::get(Application::class);
+ }
+ return self::$application->getContainer()->get(Expiration::class);
+ }
+}
diff --git a/apps/files_versions/lib/Versions/BackendNotFoundException.php b/apps/files_versions/lib/Versions/BackendNotFoundException.php
new file mode 100644
index 00000000000..f1fbecb852a
--- /dev/null
+++ b/apps/files_versions/lib/Versions/BackendNotFoundException.php
@@ -0,0 +1,10 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_Versions\Versions;
+
+class BackendNotFoundException extends \Exception {
+}
diff --git a/apps/files_versions/lib/Versions/IDeletableVersionBackend.php b/apps/files_versions/lib/Versions/IDeletableVersionBackend.php
new file mode 100644
index 00000000000..fefc038864f
--- /dev/null
+++ b/apps/files_versions/lib/Versions/IDeletableVersionBackend.php
@@ -0,0 +1,21 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_Versions\Versions;
+
+/**
+ * @since 26.0.0
+ */
+interface IDeletableVersionBackend {
+ /**
+ * Delete a version.
+ *
+ * @since 26.0.0
+ */
+ public function deleteVersion(IVersion $version): void;
+}
diff --git a/apps/files_versions/lib/Versions/IMetadataVersion.php b/apps/files_versions/lib/Versions/IMetadataVersion.php
new file mode 100644
index 00000000000..bc4cd77138b
--- /dev/null
+++ b/apps/files_versions/lib/Versions/IMetadataVersion.php
@@ -0,0 +1,31 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_Versions\Versions;
+
+/**
+ * This interface allows for just direct accessing of the metadata column JSON
+ * @since 29.0.0
+ */
+interface IMetadataVersion {
+ /**
+ * retrieves the all the metadata
+ *
+ * @return string[]
+ * @since 29.0.0
+ */
+ public function getMetadata(): array;
+
+ /**
+ * retrieves the metadata value from our $key param
+ *
+ * @param string $key the key for the json value of the metadata column
+ * @since 29.0.0
+ */
+ public function getMetadataValue(string $key): ?string;
+}
diff --git a/apps/files_versions/lib/Versions/IMetadataVersionBackend.php b/apps/files_versions/lib/Versions/IMetadataVersionBackend.php
new file mode 100644
index 00000000000..79db85e460b
--- /dev/null
+++ b/apps/files_versions/lib/Versions/IMetadataVersionBackend.php
@@ -0,0 +1,29 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_Versions\Versions;
+
+use OCP\Files\Node;
+
+/**
+ * This interface edits the metadata column of a node.
+ * Each column of the metadata has a key => value mapping.
+ * @since 29.0.0
+ */
+interface IMetadataVersionBackend {
+ /**
+ * Sets a key value pair in the metadata column corresponding to the node's version.
+ *
+ * @param Node $node the node that triggered the Metadata event listener, aka, the file version
+ * @param int $revision the key for the json value of the metadata column
+ * @param string $key the key for the json value of the metadata column
+ * @param string $value the value that corresponds to the key in the metadata column
+ * @since 29.0.0
+ */
+ public function setMetadataValue(Node $node, int $revision, string $key, string $value): void;
+}
diff --git a/apps/files_versions/lib/Versions/INameableVersion.php b/apps/files_versions/lib/Versions/INameableVersion.php
new file mode 100644
index 00000000000..a470239f128
--- /dev/null
+++ b/apps/files_versions/lib/Versions/INameableVersion.php
@@ -0,0 +1,23 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_Versions\Versions;
+
+/**
+ * @deprecated 29.0.0
+ * @since 26.0.0
+ */
+interface INameableVersion {
+ /**
+ * Get the user created label
+ * @deprecated 29.0.0
+ * @return string
+ * @since 26.0.0
+ */
+ public function getLabel(): string;
+}
diff --git a/apps/files_versions/lib/Versions/INameableVersionBackend.php b/apps/files_versions/lib/Versions/INameableVersionBackend.php
new file mode 100644
index 00000000000..d2ab7ed8135
--- /dev/null
+++ b/apps/files_versions/lib/Versions/INameableVersionBackend.php
@@ -0,0 +1,22 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_Versions\Versions;
+
+/**
+ * @deprecated 29.0.0
+ * @since 26.0.0
+ */
+interface INameableVersionBackend {
+ /**
+ * Set the label for a version.
+ * @deprecated 29.0.0
+ * @since 26.0.0
+ */
+ public function setVersionLabel(IVersion $version, string $label): void;
+}
diff --git a/apps/files_versions/lib/Versions/INeedSyncVersionBackend.php b/apps/files_versions/lib/Versions/INeedSyncVersionBackend.php
new file mode 100644
index 00000000000..e52e2f8e8bc
--- /dev/null
+++ b/apps/files_versions/lib/Versions/INeedSyncVersionBackend.php
@@ -0,0 +1,25 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_Versions\Versions;
+
+use OCA\Files_Versions\Db\VersionEntity;
+use OCP\Files\File;
+
+/**
+ * @since 28.0.0
+ */
+interface INeedSyncVersionBackend {
+ /**
+ * TODO: Convert return type to strong type once all implementations are fixed.
+ * @return null|VersionEntity
+ */
+ public function createVersionEntity(File $file);
+ public function updateVersionEntity(File $sourceFile, int $revision, array $properties): void;
+ public function deleteVersionsEntity(File $file): void;
+}
diff --git a/apps/files_versions/lib/Versions/IVersion.php b/apps/files_versions/lib/Versions/IVersion.php
new file mode 100644
index 00000000000..e5fd53d0157
--- /dev/null
+++ b/apps/files_versions/lib/Versions/IVersion.php
@@ -0,0 +1,85 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_Versions\Versions;
+
+use OCP\Files\FileInfo;
+use OCP\IUser;
+
+/**
+ * @since 15.0.0
+ */
+interface IVersion {
+ /**
+ * @return IVersionBackend
+ * @since 15.0.0
+ */
+ public function getBackend(): IVersionBackend;
+
+ /**
+ * Get the file info of the source file
+ *
+ * @return FileInfo
+ * @since 15.0.0
+ */
+ public function getSourceFile(): FileInfo;
+
+ /**
+ * Get the id of the revision for the file
+ *
+ * @return int|string
+ * @since 15.0.0
+ */
+ public function getRevisionId();
+
+ /**
+ * Get the timestamp this version was created
+ *
+ * @return int
+ * @since 15.0.0
+ */
+ public function getTimestamp(): int;
+
+ /**
+ * Get the size of this version
+ *
+ * @return int|float
+ * @since 15.0.0
+ */
+ public function getSize(): int|float;
+
+ /**
+ * Get the name of the source file at the time of making this version
+ *
+ * @return string
+ * @since 15.0.0
+ */
+ public function getSourceFileName(): string;
+
+ /**
+ * Get the mimetype of this version
+ *
+ * @return string
+ * @since 15.0.0
+ */
+ public function getMimeType(): string;
+
+ /**
+ * Get the path of this version
+ *
+ * @return string
+ * @since 15.0.0
+ */
+ public function getVersionPath(): string;
+
+ /**
+ * @return IUser
+ * @since 15.0.0
+ */
+ public function getUser(): IUser;
+}
diff --git a/apps/files_versions/lib/Versions/IVersionBackend.php b/apps/files_versions/lib/Versions/IVersionBackend.php
new file mode 100644
index 00000000000..18f8c17f0ac
--- /dev/null
+++ b/apps/files_versions/lib/Versions/IVersionBackend.php
@@ -0,0 +1,89 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_Versions\Versions;
+
+use OC\Files\Node\Node;
+use OCP\Files\File;
+use OCP\Files\FileInfo;
+use OCP\Files\NotFoundException;
+use OCP\Files\Storage\IStorage;
+use OCP\IUser;
+
+/**
+ * @since 15.0.0
+ */
+interface IVersionBackend {
+ /**
+ * Whether or not this version backend should be used for a storage
+ *
+ * If false is returned then the next applicable backend will be used
+ *
+ * @param IStorage $storage
+ * @return bool
+ * @since 17.0.0
+ */
+ public function useBackendForStorage(IStorage $storage): bool;
+
+ /**
+ * Get all versions for a file
+ *
+ * @param IUser $user
+ * @param FileInfo $file
+ * @return IVersion[]
+ * @since 15.0.0
+ */
+ public function getVersionsForFile(IUser $user, FileInfo $file): array;
+
+ /**
+ * Create a new version for a file
+ *
+ * @param IUser $user
+ * @param FileInfo $file
+ * @since 15.0.0
+ */
+ public function createVersion(IUser $user, FileInfo $file);
+
+ /**
+ * Restore this version
+ *
+ * @param IVersion $version
+ * @since 15.0.0
+ */
+ public function rollback(IVersion $version);
+
+ /**
+ * Open the file for reading
+ *
+ * @param IVersion $version
+ * @return resource|false
+ * @throws NotFoundException
+ * @since 15.0.0
+ */
+ public function read(IVersion $version);
+
+ /**
+ * Get the preview for a specific version of a file
+ *
+ * @param IUser $user
+ * @param FileInfo $sourceFile
+ * @param int|string $revision
+ *
+ * @return File
+ *
+ * @since 15.0.0
+ */
+ public function getVersionFile(IUser $user, FileInfo $sourceFile, $revision): File;
+
+ /**
+ * Get the revision for a node
+ *
+ * @since 32.0.0
+ */
+ public function getRevision(Node $node): int;
+}
diff --git a/apps/files_versions/lib/Versions/IVersionManager.php b/apps/files_versions/lib/Versions/IVersionManager.php
new file mode 100644
index 00000000000..ecd424d0cc1
--- /dev/null
+++ b/apps/files_versions/lib/Versions/IVersionManager.php
@@ -0,0 +1,31 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_Versions\Versions;
+
+use OCP\Files\Storage\IStorage;
+
+/**
+ * @since 15.0.0
+ */
+interface IVersionManager extends IVersionBackend {
+ /**
+ * Register a new backend
+ *
+ * @param string $storageType
+ * @param IVersionBackend $backend
+ * @since 15.0.0
+ */
+ public function registerBackend(string $storageType, IVersionBackend $backend);
+
+ /**
+ * @throws BackendNotFoundException
+ * @since 29.0.0
+ */
+ public function getBackendForStorage(IStorage $storage): IVersionBackend;
+}
diff --git a/apps/files_versions/lib/Versions/IVersionsImporterBackend.php b/apps/files_versions/lib/Versions/IVersionsImporterBackend.php
new file mode 100644
index 00000000000..db9349328e9
--- /dev/null
+++ b/apps/files_versions/lib/Versions/IVersionsImporterBackend.php
@@ -0,0 +1,33 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_Versions\Versions;
+
+use OCP\Files\Node;
+use OCP\IUser;
+
+/**
+ * @since 29.0.0
+ */
+interface IVersionsImporterBackend {
+ /**
+ * Import the given versions for the target file.
+ *
+ * @param Node $source - The source might not exist anymore.
+ * @param IVersion[] $versions
+ * @since 29.0.0
+ */
+ public function importVersionsForFile(IUser $user, Node $source, Node $target, array $versions): void;
+
+ /**
+ * Clear all versions for a file
+ *
+ * @since 29.0.0
+ */
+ public function clearVersionsForFile(IUser $user, Node $source, Node $target): void;
+}
diff --git a/apps/files_versions/lib/Versions/LegacyVersionsBackend.php b/apps/files_versions/lib/Versions/LegacyVersionsBackend.php
new file mode 100644
index 00000000000..48d69d31629
--- /dev/null
+++ b/apps/files_versions/lib/Versions/LegacyVersionsBackend.php
@@ -0,0 +1,395 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files_Versions\Versions;
+
+use Exception;
+use OC\Files\View;
+use OCA\DAV\Connector\Sabre\Exception\Forbidden;
+use OCA\Files_Versions\Db\VersionEntity;
+use OCA\Files_Versions\Db\VersionsMapper;
+use OCA\Files_Versions\Storage;
+use OCP\Constants;
+use OCP\Files\File;
+use OCP\Files\FileInfo;
+use OCP\Files\Folder;
+use OCP\Files\IMimeTypeLoader;
+use OCP\Files\IRootFolder;
+use OCP\Files\Node;
+use OCP\Files\NotFoundException;
+use OCP\Files\Storage\ISharedStorage;
+use OCP\Files\Storage\IStorage;
+use OCP\IUser;
+use OCP\IUserManager;
+use OCP\IUserSession;
+use Psr\Log\LoggerInterface;
+
+class LegacyVersionsBackend implements IVersionBackend, IDeletableVersionBackend, INeedSyncVersionBackend, IMetadataVersionBackend, IVersionsImporterBackend {
+ public function __construct(
+ private IRootFolder $rootFolder,
+ private IUserManager $userManager,
+ private VersionsMapper $versionsMapper,
+ private IMimeTypeLoader $mimeTypeLoader,
+ private IUserSession $userSession,
+ private LoggerInterface $logger,
+ ) {
+ }
+
+ public function useBackendForStorage(IStorage $storage): bool {
+ return true;
+ }
+
+ public function getVersionsForFile(IUser $user, FileInfo $file): array {
+ $storage = $file->getStorage();
+
+ if ($storage->instanceOfStorage(ISharedStorage::class)) {
+ $owner = $storage->getOwner('');
+ if ($owner === false) {
+ throw new NotFoundException('No owner for ' . $file->getPath());
+ }
+
+ $user = $this->userManager->get($owner);
+
+ $fileId = $file->getId();
+ if ($fileId === null) {
+ throw new NotFoundException("File not found ($fileId)");
+ }
+
+ if ($user === null) {
+ throw new NotFoundException("User $owner not found for $fileId");
+ }
+
+ $userFolder = $this->rootFolder->getUserFolder($user->getUID());
+
+ $file = $userFolder->getFirstNodeById($fileId);
+
+ if (!$file) {
+ throw new NotFoundException('version file not found for share owner');
+ }
+ } else {
+ $userFolder = $this->rootFolder->getUserFolder($user->getUID());
+ }
+
+ $fileId = $file->getId();
+ if ($fileId === null) {
+ throw new NotFoundException("File not found ($fileId)");
+ }
+
+ // Insert entries in the DB for existing versions.
+ $relativePath = $userFolder->getRelativePath($file->getPath());
+ if ($relativePath === null) {
+ throw new NotFoundException("Relative path not found for file $fileId (" . $file->getPath() . ')');
+ }
+
+ $currentVersion = [
+ 'version' => (string)$file->getMtime(),
+ 'size' => $file->getSize(),
+ 'mimetype' => $file->getMimetype(),
+ ];
+
+ $versionsInDB = $this->versionsMapper->findAllVersionsForFileId($file->getId());
+ /** @var array<int, array> */
+ $versionsInFS = array_values(Storage::getVersions($user->getUID(), $relativePath));
+
+ /** @var array<int, array{db: ?VersionEntity, fs: ?mixed}> */
+ $groupedVersions = [];
+ $davVersions = [];
+
+ foreach ($versionsInDB as $version) {
+ $revisionId = $version->getTimestamp();
+ $groupedVersions[$revisionId] = $groupedVersions[$revisionId] ?? [];
+ $groupedVersions[$revisionId]['db'] = $version;
+ }
+
+ foreach ([$currentVersion, ...$versionsInFS] as $version) {
+ $revisionId = $version['version'];
+ $groupedVersions[$revisionId] = $groupedVersions[$revisionId] ?? [];
+ $groupedVersions[$revisionId]['fs'] = $version;
+ }
+
+ /** @var array<string, array{db: ?VersionEntity, fs: ?mixed}> $groupedVersions */
+ foreach ($groupedVersions as $versions) {
+ if (empty($versions['db']) && !empty($versions['fs'])) {
+ $versions['db'] = new VersionEntity();
+ $versions['db']->setFileId($fileId);
+ $versions['db']->setTimestamp((int)$versions['fs']['version']);
+ $versions['db']->setSize((int)$versions['fs']['size']);
+ $versions['db']->setMimetype($this->mimeTypeLoader->getId($versions['fs']['mimetype']));
+ $versions['db']->setMetadata([]);
+ $this->versionsMapper->insert($versions['db']);
+ } elseif (!empty($versions['db']) && empty($versions['fs'])) {
+ $this->versionsMapper->delete($versions['db']);
+ continue;
+ }
+
+ $version = new Version(
+ $versions['db']->getTimestamp(),
+ $versions['db']->getTimestamp(),
+ $file->getName(),
+ $versions['db']->getSize(),
+ $this->mimeTypeLoader->getMimetypeById($versions['db']->getMimetype()),
+ $userFolder->getRelativePath($file->getPath()),
+ $file,
+ $this,
+ $user,
+ $versions['db']->getMetadata() ?? [],
+ );
+
+ array_push($davVersions, $version);
+ }
+
+ return $davVersions;
+ }
+
+ public function createVersion(IUser $user, FileInfo $file) {
+ $userFolder = $this->rootFolder->getUserFolder($user->getUID());
+ $relativePath = $userFolder->getRelativePath($file->getPath());
+ $userView = new View('/' . $user->getUID());
+ // create all parent folders
+ Storage::createMissingDirectories($relativePath, $userView);
+
+ Storage::scheduleExpire($user->getUID(), $relativePath);
+
+ // store a new version of a file
+ $userView->copy('files/' . $relativePath, 'files_versions/' . $relativePath . '.v' . $file->getMtime());
+ // ensure the file is scanned
+ $userView->getFileInfo('files_versions/' . $relativePath . '.v' . $file->getMtime());
+ }
+
+ public function rollback(IVersion $version) {
+ if (!$this->currentUserHasPermissions($version->getSourceFile(), Constants::PERMISSION_UPDATE)) {
+ throw new Forbidden('You cannot restore this version because you do not have update permissions on the source file.');
+ }
+
+ return Storage::rollback($version->getVersionPath(), $version->getRevisionId(), $version->getUser());
+ }
+
+ private function getVersionFolder(IUser $user): Folder {
+ $userRoot = $this->rootFolder->getUserFolder($user->getUID())
+ ->getParent();
+ try {
+ /** @var Folder $folder */
+ $folder = $userRoot->get('files_versions');
+ return $folder;
+ } catch (NotFoundException $e) {
+ return $userRoot->newFolder('files_versions');
+ }
+ }
+
+ public function read(IVersion $version) {
+ $versions = $this->getVersionFolder($version->getUser());
+ /** @var File $file */
+ $file = $versions->get($version->getVersionPath() . '.v' . $version->getRevisionId());
+ return $file->fopen('r');
+ }
+
+ public function getVersionFile(IUser $user, FileInfo $sourceFile, $revision): File {
+ $userFolder = $this->rootFolder->getUserFolder($user->getUID());
+ $owner = $sourceFile->getOwner();
+ $storage = $sourceFile->getStorage();
+
+ // Shared files have their versions in the owners root folder so we need to obtain them from there
+ if ($storage->instanceOfStorage(ISharedStorage::class) && $owner) {
+ /** @var ISharedStorage $storage */
+ $userFolder = $this->rootFolder->getUserFolder($owner->getUID());
+ $user = $owner;
+ $ownerPathInStorage = $sourceFile->getInternalPath();
+ $sourceFile = $storage->getShare()->getNode();
+ if ($sourceFile instanceof Folder) {
+ $sourceFile = $sourceFile->get($ownerPathInStorage);
+ }
+ }
+
+ $versionFolder = $this->getVersionFolder($user);
+ /** @var File $file */
+ $file = $versionFolder->get($userFolder->getRelativePath($sourceFile->getPath()) . '.v' . $revision);
+ return $file;
+ }
+
+ public function getRevision(Node $node): int {
+ return $node->getMTime();
+ }
+
+ public function deleteVersion(IVersion $version): void {
+ if (!$this->currentUserHasPermissions($version->getSourceFile(), Constants::PERMISSION_DELETE)) {
+ throw new Forbidden('You cannot delete this version because you do not have delete permissions on the source file.');
+ }
+
+ Storage::deleteRevision($version->getVersionPath(), $version->getRevisionId());
+ $versionEntity = $this->versionsMapper->findVersionForFileId(
+ $version->getSourceFile()->getId(),
+ $version->getTimestamp(),
+ );
+ $this->versionsMapper->delete($versionEntity);
+ }
+
+ public function createVersionEntity(File $file): ?VersionEntity {
+ $versionEntity = new VersionEntity();
+ $versionEntity->setFileId($file->getId());
+ $versionEntity->setTimestamp($file->getMTime());
+ $versionEntity->setSize($file->getSize());
+ $versionEntity->setMimetype($this->mimeTypeLoader->getId($file->getMimetype()));
+ $versionEntity->setMetadata([]);
+
+ $tries = 1;
+ while ($tries < 5) {
+ try {
+ $this->versionsMapper->insert($versionEntity);
+ return $versionEntity;
+ } catch (\OCP\DB\Exception $e) {
+ if (!in_array($e->getReason(), [
+ \OCP\DB\Exception::REASON_CONSTRAINT_VIOLATION,
+ \OCP\DB\Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION,
+ ])
+ ) {
+ throw $e;
+ }
+ /* Conflict with another version, increase mtime and try again */
+ $versionEntity->setTimestamp($versionEntity->getTimestamp() + 1);
+ $tries++;
+ $this->logger->warning('Constraint violation while inserting version, retrying with increased timestamp', ['exception' => $e]);
+ }
+ }
+
+ return null;
+ }
+
+ public function updateVersionEntity(File $sourceFile, int $revision, array $properties): void {
+ $versionEntity = $this->versionsMapper->findVersionForFileId($sourceFile->getId(), $revision);
+
+ if (isset($properties['timestamp'])) {
+ $versionEntity->setTimestamp($properties['timestamp']);
+ }
+
+ if (isset($properties['size'])) {
+ $versionEntity->setSize($properties['size']);
+ }
+
+ if (isset($properties['mimetype'])) {
+ $versionEntity->setMimetype($properties['mimetype']);
+ }
+
+ $this->versionsMapper->update($versionEntity);
+ }
+
+ public function deleteVersionsEntity(File $file): void {
+ $this->versionsMapper->deleteAllVersionsForFileId($file->getId());
+ }
+
+ private function currentUserHasPermissions(FileInfo $sourceFile, int $permissions): bool {
+ $currentUserId = $this->userSession->getUser()?->getUID();
+
+ if ($currentUserId === null) {
+ throw new NotFoundException('No user logged in');
+ }
+
+ if ($sourceFile->getOwner()?->getUID() === $currentUserId) {
+ return ($sourceFile->getPermissions() & $permissions) === $permissions;
+ }
+
+ $nodes = $this->rootFolder->getUserFolder($currentUserId)->getById($sourceFile->getId());
+
+ if (count($nodes) === 0) {
+ throw new NotFoundException('Version file not accessible by current user');
+ }
+
+ foreach ($nodes as $node) {
+ if (($node->getPermissions() & $permissions) === $permissions) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public function setMetadataValue(Node $node, int $revision, string $key, string $value): void {
+ if (!$this->currentUserHasPermissions($node, Constants::PERMISSION_UPDATE)) {
+ throw new Forbidden('You cannot update the version\'s metadata because you do not have update permissions on the source file.');
+ }
+
+ $versionEntity = $this->versionsMapper->findVersionForFileId($node->getId(), $revision);
+
+ $versionEntity->setMetadataValue($key, $value);
+ $this->versionsMapper->update($versionEntity);
+ }
+
+
+ /**
+ * @inheritdoc
+ */
+ public function importVersionsForFile(IUser $user, Node $source, Node $target, array $versions): void {
+ $userFolder = $this->rootFolder->getUserFolder($user->getUID());
+ $relativePath = $userFolder->getRelativePath($target->getPath());
+
+ if ($relativePath === null) {
+ throw new \Exception('Target does not have a relative path' . $target->getPath());
+ }
+
+ $userView = new View('/' . $user->getUID());
+ // create all parent folders
+ Storage::createMissingDirectories($relativePath, $userView);
+ Storage::scheduleExpire($user->getUID(), $relativePath);
+
+ foreach ($versions as $version) {
+ // 1. Import the file in its new location.
+ // Nothing to do for the current version.
+ if ($version->getTimestamp() !== $source->getMTime()) {
+ $backend = $version->getBackend();
+ $versionFile = $backend->getVersionFile($user, $source, $version->getRevisionId());
+ $newVersionPath = 'files_versions/' . $relativePath . '.v' . $version->getTimestamp();
+
+ $versionContent = $versionFile->fopen('r');
+ if ($versionContent === false) {
+ $this->logger->warning('Fail to open version file.', ['source' => $source, 'version' => $version, 'versionFile' => $versionFile]);
+ continue;
+ }
+
+ $userView->file_put_contents($newVersionPath, $versionContent);
+ // ensure the file is scanned
+ $userView->getFileInfo($newVersionPath);
+ }
+
+ // 2. Create the entity in the database
+ $versionEntity = new VersionEntity();
+ $versionEntity->setFileId($target->getId());
+ $versionEntity->setTimestamp($version->getTimestamp());
+ $versionEntity->setSize($version->getSize());
+ $versionEntity->setMimetype($this->mimeTypeLoader->getId($version->getMimetype()));
+ if ($version instanceof IMetadataVersion) {
+ $versionEntity->setMetadata($version->getMetadata());
+ }
+ $this->versionsMapper->insert($versionEntity);
+ }
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function clearVersionsForFile(IUser $user, Node $source, Node $target): void {
+ $userId = $user->getUID();
+ $userFolder = $this->rootFolder->getUserFolder($userId);
+
+ $relativePath = $userFolder->getRelativePath($source->getPath());
+ if ($relativePath === null) {
+ throw new Exception('Relative path not found for node with path: ' . $source->getPath());
+ }
+
+ $versionFolder = $this->rootFolder->get($userId . '/files_versions');
+ if (!$versionFolder instanceof Folder) {
+ throw new Exception('User versions folder does not exist');
+ }
+
+ $versions = Storage::getVersions($userId, $relativePath);
+ foreach ($versions as $version) {
+ $versionFolder->get($version['path'] . '.v' . (int)$version['version'])->delete();
+ }
+
+ $this->versionsMapper->deleteAllVersionsForFileId($target->getId());
+ }
+}
diff --git a/apps/files_versions/lib/Versions/Version.php b/apps/files_versions/lib/Versions/Version.php
new file mode 100644
index 00000000000..e202a69b7d7
--- /dev/null
+++ b/apps/files_versions/lib/Versions/Version.php
@@ -0,0 +1,72 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_Versions\Versions;
+
+use OCP\Files\FileInfo;
+use OCP\IUser;
+
+class Version implements IVersion, IMetadataVersion {
+ public function __construct(
+ private int $timestamp,
+ private int|string $revisionId,
+ private string $name,
+ private int|float $size,
+ private string $mimetype,
+ private string $path,
+ private FileInfo $sourceFileInfo,
+ private IVersionBackend $backend,
+ private IUser $user,
+ private array $metadata = [],
+ ) {
+ }
+
+ public function getBackend(): IVersionBackend {
+ return $this->backend;
+ }
+
+ public function getSourceFile(): FileInfo {
+ return $this->sourceFileInfo;
+ }
+
+ public function getRevisionId() {
+ return $this->revisionId;
+ }
+
+ public function getTimestamp(): int {
+ return $this->timestamp;
+ }
+
+ public function getSize(): int|float {
+ return $this->size;
+ }
+
+ public function getSourceFileName(): string {
+ return $this->name;
+ }
+
+ public function getMimeType(): string {
+ return $this->mimetype;
+ }
+
+ public function getVersionPath(): string {
+ return $this->path;
+ }
+
+ public function getUser(): IUser {
+ return $this->user;
+ }
+
+ public function getMetadata(): array {
+ return $this->metadata;
+ }
+
+ public function getMetadataValue(string $key): ?string {
+ return $this->metadata[$key] ?? null;
+ }
+}
diff --git a/apps/files_versions/lib/Versions/VersionManager.php b/apps/files_versions/lib/Versions/VersionManager.php
new file mode 100644
index 00000000000..9acea8c6513
--- /dev/null
+++ b/apps/files_versions/lib/Versions/VersionManager.php
@@ -0,0 +1,210 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_Versions\Versions;
+
+use OCA\Files_Versions\Db\VersionEntity;
+use OCA\Files_Versions\Events\VersionCreatedEvent;
+use OCA\Files_Versions\Events\VersionRestoredEvent;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\Files\File;
+use OCP\Files\FileInfo;
+use OCP\Files\IRootFolder;
+use OCP\Files\Lock\ILock;
+use OCP\Files\Lock\ILockManager;
+use OCP\Files\Lock\LockContext;
+use OCP\Files\Node;
+use OCP\Files\Storage\IStorage;
+use OCP\IUser;
+use OCP\Lock\ManuallyLockedException;
+use OCP\Server;
+
+class VersionManager implements IVersionManager, IDeletableVersionBackend, INeedSyncVersionBackend, IMetadataVersionBackend {
+
+ /** @var (IVersionBackend[])[] */
+ private $backends = [];
+
+ public function __construct(
+ private IEventDispatcher $dispatcher,
+ ) {
+ }
+
+ public function registerBackend(string $storageType, IVersionBackend $backend) {
+ if (!isset($this->backends[$storageType])) {
+ $this->backends[$storageType] = [];
+ }
+ $this->backends[$storageType][] = $backend;
+ }
+
+ /**
+ * @return (IVersionBackend[])[]
+ */
+ private function getBackends(): array {
+ return $this->backends;
+ }
+
+ /**
+ * @param IStorage $storage
+ * @return IVersionBackend
+ * @throws BackendNotFoundException
+ */
+ public function getBackendForStorage(IStorage $storage): IVersionBackend {
+ $fullType = get_class($storage);
+ $backends = $this->getBackends();
+
+ $foundType = '';
+ $foundBackend = null;
+
+ foreach ($backends as $type => $backendsForType) {
+ if (
+ $storage->instanceOfStorage($type)
+ && ($foundType === '' || is_subclass_of($type, $foundType))
+ ) {
+ foreach ($backendsForType as $backend) {
+ /** @var IVersionBackend $backend */
+ if ($backend->useBackendForStorage($storage)) {
+ $foundBackend = $backend;
+ $foundType = $type;
+ }
+ }
+ }
+ }
+
+ if ($foundType === '' || $foundBackend === null) {
+ throw new BackendNotFoundException("Version backend for $fullType not found");
+ } else {
+ return $foundBackend;
+ }
+ }
+
+ public function getVersionsForFile(IUser $user, FileInfo $file): array {
+ $backend = $this->getBackendForStorage($file->getStorage());
+ return $backend->getVersionsForFile($user, $file);
+ }
+
+ public function createVersion(IUser $user, FileInfo $file) {
+ $backend = $this->getBackendForStorage($file->getStorage());
+ $backend->createVersion($user, $file);
+ }
+
+ public function rollback(IVersion $version) {
+ $backend = $version->getBackend();
+ $result = self::handleAppLocks(fn (): ?bool => $backend->rollback($version));
+ // rollback doesn't have a return type yet and some implementations don't return anything
+ if ($result === null || $result === true) {
+ $this->dispatcher->dispatchTyped(new VersionRestoredEvent($version));
+ }
+ return $result;
+ }
+
+ public function read(IVersion $version) {
+ $backend = $version->getBackend();
+ return $backend->read($version);
+ }
+
+ public function getVersionFile(IUser $user, FileInfo $sourceFile, $revision): File {
+ $backend = $this->getBackendForStorage($sourceFile->getStorage());
+ return $backend->getVersionFile($user, $sourceFile, $revision);
+ }
+
+ public function getRevision(Node $node): int {
+ $backend = $this->getBackendForStorage($node->getStorage());
+ return $backend->getRevision($node);
+ }
+
+ public function useBackendForStorage(IStorage $storage): bool {
+ return false;
+ }
+
+ public function deleteVersion(IVersion $version): void {
+ $backend = $version->getBackend();
+ if ($backend instanceof IDeletableVersionBackend) {
+ $backend->deleteVersion($version);
+ }
+ }
+
+ public function createVersionEntity(File $file): void {
+ $backend = $this->getBackendForStorage($file->getStorage());
+ if ($backend instanceof INeedSyncVersionBackend) {
+ $versionEntity = $backend->createVersionEntity($file);
+
+ if ($versionEntity instanceof VersionEntity) {
+ foreach ($backend->getVersionsForFile($file->getOwner(), $file) as $version) {
+ if ($version->getRevisionId() === $versionEntity->getTimestamp()) {
+ $this->dispatcher->dispatchTyped(new VersionCreatedEvent($file, $version));
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ public function updateVersionEntity(File $sourceFile, int $revision, array $properties): void {
+ $backend = $this->getBackendForStorage($sourceFile->getStorage());
+ if ($backend instanceof INeedSyncVersionBackend) {
+ $backend->updateVersionEntity($sourceFile, $revision, $properties);
+ }
+ }
+
+ public function deleteVersionsEntity(File $file): void {
+ $backend = $this->getBackendForStorage($file->getStorage());
+ if ($backend instanceof INeedSyncVersionBackend) {
+ $backend->deleteVersionsEntity($file);
+ }
+ }
+
+ public function setMetadataValue(Node $node, int $revision, string $key, string $value): void {
+ $backend = $this->getBackendForStorage($node->getStorage());
+ if ($backend instanceof IMetadataVersionBackend) {
+ $backend->setMetadataValue($node, $revision, $key, $value);
+ }
+ }
+
+ /**
+ * Catch ManuallyLockedException and retry in app context if possible.
+ *
+ * Allow users to go back to old versions via the versions tab in the sidebar
+ * even when the file is opened in the viewer next to it.
+ *
+ * Context: If a file is currently opened for editing
+ * the files_lock app will throw ManuallyLockedExceptions.
+ * This prevented the user from rolling an opened file back to a previous version.
+ *
+ * Text and Richdocuments can handle changes of open files.
+ * So we execute the rollback under their lock context
+ * to let them handle the conflict.
+ *
+ * @param callable $callback function to run with app locks handled
+ * @return bool|null
+ * @throws ManuallyLockedException
+ *
+ */
+ private static function handleAppLocks(callable $callback): ?bool {
+ try {
+ return $callback();
+ } catch (ManuallyLockedException $e) {
+ $owner = (string)$e->getOwner();
+ $appsThatHandleUpdates = ['text', 'richdocuments'];
+ if (!in_array($owner, $appsThatHandleUpdates)) {
+ throw $e;
+ }
+ // The LockWrapper in the files_lock app only compares the lock type and owner
+ // when checking the lock against the current scope.
+ // So we do not need to get the actual node here
+ // and use the root node instead.
+ $root = Server::get(IRootFolder::class);
+ $lockContext = new LockContext($root, ILock::TYPE_APP, $owner);
+ $lockManager = Server::get(ILockManager::class);
+ $result = null;
+ $lockManager->runInScope($lockContext, function () use ($callback, &$result): void {
+ $result = $callback();
+ });
+ return $result;
+ }
+ }
+}
diff --git a/apps/files_versions/lib/backgroundjob/expireversions.php b/apps/files_versions/lib/backgroundjob/expireversions.php
deleted file mode 100644
index 5d8eef4e351..00000000000
--- a/apps/files_versions/lib/backgroundjob/expireversions.php
+++ /dev/null
@@ -1,98 +0,0 @@
-<?php
-/**
- * @author Victor Dubiniuk <dubiniuk@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\Files_Versions\BackgroundJob;
-
-use OCP\IUserManager;
-use OCA\Files_Versions\AppInfo\Application;
-use OCA\Files_Versions\Storage;
-use OCA\Files_Versions\Expiration;
-
-class ExpireVersions extends \OC\BackgroundJob\TimedJob {
-
- const ITEMS_PER_SESSION = 1000;
-
- /**
- * @var Expiration
- */
- private $expiration;
-
- /**
- * @var IUserManager
- */
- private $userManager;
-
- public function __construct(IUserManager $userManager = null, Expiration $expiration = null) {
- // Run once per 30 minutes
- $this->setInterval(60 * 30);
-
- if (is_null($expiration) || is_null($userManager)) {
- $this->fixDIForJobs();
- } else {
- $this->expiration = $expiration;
- $this->userManager = $userManager;
- }
- }
-
- protected function fixDIForJobs() {
- $application = new Application();
- $this->expiration = $application->getContainer()->query('Expiration');
- $this->userManager = \OC::$server->getUserManager();
- }
-
- protected function run($argument) {
- $maxAge = $this->expiration->getMaxAgeAsTimestamp();
- if (!$maxAge) {
- return;
- }
-
- $users = $this->userManager->search('');
- $isFSready = false;
- foreach ($users as $user) {
- $uid = $user->getUID();
- if (!$isFSready) {
- if (!$this->setupFS($uid)) {
- continue;
- }
- $isFSready = true;
- }
- Storage::expireOlderThanMaxForUser($uid);
- }
-
- \OC_Util::tearDownFS();
- }
-
- /**
- * Act on behalf on trash item owner
- * @param string $user
- * @return boolean
- */
- private function setupFS($user){
- if (!$this->userManager->userExists($user)) {
- return false;
- }
-
- \OC_Util::tearDownFS();
- \OC_Util::setupFS($user);
-
- return true;
- }
-}
diff --git a/apps/files_versions/lib/capabilities.php b/apps/files_versions/lib/capabilities.php
deleted file mode 100644
index 441b2adfba3..00000000000
--- a/apps/files_versions/lib/capabilities.php
+++ /dev/null
@@ -1,43 +0,0 @@
-<?php
-/**
- * @author Christopher Schäpers <kondou@ts.unde.re>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <rullzer@owncloud.com>
- * @author Tom Needham <tom@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\Files_Versions;
-
-use OCP\Capabilities\ICapability;
-
-class Capabilities implements ICapability {
-
- /**
- * Return this classes capabilities
- *
- * @return array
- */
- public function getCapabilities() {
- return [
- 'files' => [
- 'versioning' => true
- ]
- ];
- }
-}
diff --git a/apps/files_versions/lib/hooks.php b/apps/files_versions/lib/hooks.php
deleted file mode 100644
index beaf81c7471..00000000000
--- a/apps/files_versions/lib/hooks.php
+++ /dev/null
@@ -1,170 +0,0 @@
-<?php
-/**
- * @author Bart Visscher <bartv@thisnet.nl>
- * @author Björn Schießle <schiessle@owncloud.com>
- * @author Jörn Friedrich Dreyer <jfd@butonic.de>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <icewind@owncloud.com>
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- * @author Sam Tuke <mail@samtuke.com>
- * @author Vincent Petry <pvince81@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-/**
- * This class contains all hooks.
- */
-
-namespace OCA\Files_Versions;
-
-class Hooks {
-
- public static function connectHooks() {
- // Listen to write signals
- \OCP\Util::connectHook('OC_Filesystem', 'write', 'OCA\Files_Versions\Hooks', 'write_hook');
- // Listen to delete and rename signals
- \OCP\Util::connectHook('OC_Filesystem', 'post_delete', 'OCA\Files_Versions\Hooks', 'remove_hook');
- \OCP\Util::connectHook('OC_Filesystem', 'delete', 'OCA\Files_Versions\Hooks', 'pre_remove_hook');
- \OCP\Util::connectHook('OC_Filesystem', 'post_rename', 'OCA\Files_Versions\Hooks', 'rename_hook');
- \OCP\Util::connectHook('OC_Filesystem', 'post_copy', 'OCA\Files_Versions\Hooks', 'copy_hook');
- \OCP\Util::connectHook('OC_Filesystem', 'rename', 'OCA\Files_Versions\Hooks', 'pre_renameOrCopy_hook');
- \OCP\Util::connectHook('OC_Filesystem', 'copy', 'OCA\Files_Versions\Hooks', 'pre_renameOrCopy_hook');
-
- $eventDispatcher = \OC::$server->getEventDispatcher();
- $eventDispatcher->addListener('OCA\Files::loadAdditionalScripts', ['OCA\Files_Versions\Hooks', 'onLoadFilesAppScripts']);
- }
-
- /**
- * listen to write event.
- */
- public static function write_hook( $params ) {
-
- if (\OCP\App::isEnabled('files_versions')) {
- $path = $params[\OC\Files\Filesystem::signal_param_path];
- if($path<>'') {
- Storage::store($path);
- }
- }
- }
-
-
- /**
- * Erase versions of deleted file
- * @param array $params
- *
- * This function is connected to the delete signal of OC_Filesystem
- * cleanup the versions directory if the actual file gets deleted
- */
- public static function remove_hook($params) {
-
- if (\OCP\App::isEnabled('files_versions')) {
- $path = $params[\OC\Files\Filesystem::signal_param_path];
- if($path<>'') {
- Storage::delete($path);
- }
- }
- }
-
- /**
- * mark file as "deleted" so that we can clean up the versions if the file is gone
- * @param array $params
- */
- public static function pre_remove_hook($params) {
- $path = $params[\OC\Files\Filesystem::signal_param_path];
- if($path<>'') {
- Storage::markDeletedFile($path);
- }
- }
-
- /**
- * rename/move versions of renamed/moved files
- * @param array $params array with oldpath and newpath
- *
- * This function is connected to the rename signal of OC_Filesystem and adjust the name and location
- * of the stored versions along the actual file
- */
- public static function rename_hook($params) {
-
- if (\OCP\App::isEnabled('files_versions')) {
- $oldpath = $params['oldpath'];
- $newpath = $params['newpath'];
- if($oldpath<>'' && $newpath<>'') {
- Storage::renameOrCopy($oldpath, $newpath, 'rename');
- }
- }
- }
-
- /**
- * copy versions of copied files
- * @param array $params array with oldpath and newpath
- *
- * This function is connected to the copy signal of OC_Filesystem and copies the
- * the stored versions to the new location
- */
- public static function copy_hook($params) {
-
- if (\OCP\App::isEnabled('files_versions')) {
- $oldpath = $params['oldpath'];
- $newpath = $params['newpath'];
- if($oldpath<>'' && $newpath<>'') {
- Storage::renameOrCopy($oldpath, $newpath, 'copy');
- }
- }
- }
-
- /**
- * Remember owner and the owner path of the source file.
- * If the file already exists, then it was a upload of a existing file
- * over the web interface and we call Storage::store() directly
- *
- * @param array $params array with oldpath and newpath
- *
- */
- public static function pre_renameOrCopy_hook($params) {
- if (\OCP\App::isEnabled('files_versions')) {
-
- // if we rename a movable mount point, then the versions don't have
- // to be renamed
- $absOldPath = \OC\Files\Filesystem::normalizePath('/' . \OCP\User::getUser() . '/files' . $params['oldpath']);
- $manager = \OC\Files\Filesystem::getMountManager();
- $mount = $manager->find($absOldPath);
- $internalPath = $mount->getInternalPath($absOldPath);
- if ($internalPath === '' and $mount instanceof \OC\Files\Mount\MoveableMount) {
- return;
- }
-
- $view = new \OC\Files\View(\OCP\User::getUser() . '/files');
- if ($view->file_exists($params['newpath'])) {
- Storage::store($params['newpath']);
- } else {
- Storage::setSourcePathAndUser($params['oldpath']);
- }
-
- }
- }
-
- /**
- * Load additional scripts when the files app is visible
- */
- public static function onLoadFilesAppScripts() {
- \OCP\Util::addScript('files_versions', 'versionmodel');
- \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/storage.php b/apps/files_versions/lib/storage.php
deleted file mode 100644
index a213ea75238..00000000000
--- a/apps/files_versions/lib/storage.php
+++ /dev/null
@@ -1,808 +0,0 @@
-<?php
-/**
- * @author Arthur Schiwon <blizzz@owncloud.com>
- * @author Bart Visscher <bartv@thisnet.nl>
- * @author Björn Schießle <schiessle@owncloud.com>
- * @author Felix Moeller <mail@felixmoeller.de>
- * @author Florin Peter <github@florin-peter.de>
- * @author Georg Ehrke <georg@owncloud.com>
- * @author Joas Schilling <nickvergessen@owncloud.com>
- * @author Jörn Friedrich Dreyer <jfd@butonic.de>
- * @author Lukas Reschke <lukas@owncloud.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <icewind@owncloud.com>
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Victor Dubiniuk <dubiniuk@owncloud.com>
- * @author Vincent Petry <pvince81@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-/**
- * Versions
- *
- * A class to handle the versioning of files.
- */
-
-namespace OCA\Files_Versions;
-
-use OC\Files\Filesystem;
-use OC\Files\View;
-use OCA\Files_Versions\AppInfo\Application;
-use OCA\Files_Versions\Command\Expire;
-use OCP\Lock\ILockingProvider;
-use OCP\User;
-
-class Storage {
-
- const DEFAULTENABLED=true;
- const DEFAULTMAXSIZE=50; // unit: percentage; 50% of available disk space/quota
- const VERSIONS_ROOT = 'files_versions/';
-
- const DELETE_TRIGGER_MASTER_REMOVED = 0;
- const DELETE_TRIGGER_RETENTION_CONSTRAINT = 1;
- const DELETE_TRIGGER_QUOTA_EXCEEDED = 2;
-
- // files for which we can remove the versions after the delete operation was successful
- private static $deletedFiles = array();
-
- private static $sourcePathAndUser = array();
-
- private static $max_versions_per_interval = array(
- //first 10sec, one version every 2sec
- 1 => array('intervalEndsAfter' => 10, 'step' => 2),
- //next minute, one version every 10sec
- 2 => array('intervalEndsAfter' => 60, 'step' => 10),
- //next hour, one version every minute
- 3 => array('intervalEndsAfter' => 3600, 'step' => 60),
- //next 24h, one version every hour
- 4 => array('intervalEndsAfter' => 86400, 'step' => 3600),
- //next 30days, one version per day
- 5 => array('intervalEndsAfter' => 2592000, 'step' => 86400),
- //until the end one version per week
- 6 => array('intervalEndsAfter' => -1, 'step' => 604800),
- );
-
- /** @var \OCA\Files_Versions\AppInfo\Application */
- private static $application;
-
- /**
- * get the UID of the owner of the file and the path to the file relative to
- * owners files folder
- *
- * @param string $filename
- * @return array
- * @throws \OC\User\NoUserException
- */
- public static function getUidAndFilename($filename) {
- $uid = Filesystem::getOwner($filename);
- $userManager = \OC::$server->getUserManager();
- // if the user with the UID doesn't exists, e.g. because the UID points
- // to a remote user with a federated cloud ID we use the current logged-in
- // user. We need a valid local user to create the versions
- if (!$userManager->userExists($uid)) {
- $uid = User::getUser();
- }
- Filesystem::initMountPoints($uid);
- if ( $uid != User::getUser() ) {
- $info = Filesystem::getFileInfo($filename);
- $ownerView = new View('/'.$uid.'/files');
- try {
- $filename = $ownerView->getPath($info['fileid']);
- // make sure that the file name doesn't end with a trailing slash
- // can for example happen single files shared across servers
- $filename = rtrim($filename, '/');
- } catch (NotFoundException $e) {
- $filename = null;
- }
- }
- return [$uid, $filename];
- }
-
- /**
- * Remember the owner and the owner path of the source file
- *
- * @param string $source source path
- */
- public static function setSourcePathAndUser($source) {
- list($uid, $path) = self::getUidAndFilename($source);
- self::$sourcePathAndUser[$source] = array('uid' => $uid, 'path' => $path);
- }
-
- /**
- * Gets the owner and the owner path from the source path
- *
- * @param string $source source path
- * @return array with user id and path
- */
- public static function getSourcePathAndUser($source) {
-
- if (isset(self::$sourcePathAndUser[$source])) {
- $uid = self::$sourcePathAndUser[$source]['uid'];
- $path = self::$sourcePathAndUser[$source]['path'];
- unset(self::$sourcePathAndUser[$source]);
- } else {
- $uid = $path = false;
- }
- return array($uid, $path);
- }
-
- /**
- * get current size of all versions from a given user
- *
- * @param string $user user who owns the versions
- * @return int versions size
- */
- private static function getVersionsSize($user) {
- $view = new View('/' . $user);
- $fileInfo = $view->getFileInfo('/files_versions');
- return isset($fileInfo['size']) ? $fileInfo['size'] : 0;
- }
-
- /**
- * store a new version of a file.
- */
- public static function store($filename) {
- if(\OCP\Config::getSystemValue('files_versions', Storage::DEFAULTENABLED)=='true') {
-
- // if the file gets streamed we need to remove the .part extension
- // to get the right target
- $ext = pathinfo($filename, PATHINFO_EXTENSION);
- if ($ext === 'part') {
- $filename = substr($filename, 0, strlen($filename) - 5);
- }
-
- // we only handle existing files
- if (! Filesystem::file_exists($filename) || Filesystem::is_dir($filename)) {
- return false;
- }
-
- list($uid, $filename) = self::getUidAndFilename($filename);
-
- $files_view = new View('/'.$uid .'/files');
- $users_view = new View('/'.$uid);
-
- // no use making versions for empty files
- if ($files_view->filesize($filename) === 0) {
- return false;
- }
-
- // create all parent folders
- self::createMissingDirectories($filename, $users_view);
-
- self::scheduleExpire($uid, $filename);
-
- // store a new version of a file
- $mtime = $users_view->filemtime('files/' . $filename);
- $users_view->copy('files/' . $filename, 'files_versions/' . $filename . '.v' . $mtime);
- // call getFileInfo to enforce a file cache entry for the new version
- $users_view->getFileInfo('files_versions/' . $filename . '.v' . $mtime);
- }
- }
-
-
- /**
- * mark file as deleted so that we can remove the versions if the file is gone
- * @param string $path
- */
- public static function markDeletedFile($path) {
- list($uid, $filename) = self::getUidAndFilename($path);
- self::$deletedFiles[$path] = array(
- 'uid' => $uid,
- 'filename' => $filename);
- }
-
- /**
- * delete the version from the storage and cache
- *
- * @param View $view
- * @param string $path
- */
- protected static function deleteVersion($view, $path) {
- $view->unlink($path);
- /**
- * @var \OC\Files\Storage\Storage $storage
- * @var string $internalPath
- */
- list($storage, $internalPath) = $view->resolvePath($path);
- $cache = $storage->getCache($internalPath);
- $cache->remove($internalPath);
- }
-
- /**
- * Delete versions of a file
- */
- public static function delete($path) {
-
- $deletedFile = self::$deletedFiles[$path];
- $uid = $deletedFile['uid'];
- $filename = $deletedFile['filename'];
-
- if (!Filesystem::file_exists($path)) {
-
- $view = new View('/' . $uid . '/files_versions');
-
- $versions = self::getVersions($uid, $filename);
- if (!empty($versions)) {
- foreach ($versions as $v) {
- \OC_Hook::emit('\OCP\Versions', 'preDelete', array('path' => $path . $v['version'], 'trigger' => self::DELETE_TRIGGER_MASTER_REMOVED));
- self::deleteVersion($view, $filename . '.v' . $v['version']);
- \OC_Hook::emit('\OCP\Versions', 'delete', array('path' => $path . $v['version'], 'trigger' => self::DELETE_TRIGGER_MASTER_REMOVED));
- }
- }
- }
- unset(self::$deletedFiles[$path]);
- }
-
- /**
- * Rename or copy versions of a file of the given paths
- *
- * @param string $sourcePath source path of the file to move, relative to
- * the currently logged in user's "files" folder
- * @param string $targetPath target path of the file to move, relative to
- * the currently logged in user's "files" folder
- * @param string $operation can be 'copy' or 'rename'
- */
- public static function renameOrCopy($sourcePath, $targetPath, $operation) {
- list($sourceOwner, $sourcePath) = self::getSourcePathAndUser($sourcePath);
-
- // it was a upload of a existing file if no old path exists
- // in this case the pre-hook already called the store method and we can
- // stop here
- if ($sourcePath === false) {
- return true;
- }
-
- list($targetOwner, $targetPath) = self::getUidAndFilename($targetPath);
-
- $sourcePath = ltrim($sourcePath, '/');
- $targetPath = ltrim($targetPath, '/');
-
- $rootView = new View('');
-
- // did we move a directory ?
- if ($rootView->is_dir('/' . $targetOwner . '/files/' . $targetPath)) {
- // does the directory exists for versions too ?
- if ($rootView->is_dir('/' . $sourceOwner . '/files_versions/' . $sourcePath)) {
- // create missing dirs if necessary
- self::createMissingDirectories($targetPath, new View('/'. $targetOwner));
-
- // move the directory containing the versions
- $rootView->$operation(
- '/' . $sourceOwner . '/files_versions/' . $sourcePath,
- '/' . $targetOwner . '/files_versions/' . $targetPath
- );
- }
- } else if ($versions = Storage::getVersions($sourceOwner, '/' . $sourcePath)) {
- // create missing dirs if necessary
- self::createMissingDirectories($targetPath, new View('/'. $targetOwner));
-
- foreach ($versions as $v) {
- // move each version one by one to the target directory
- $rootView->$operation(
- '/' . $sourceOwner . '/files_versions/' . $sourcePath.'.v' . $v['version'],
- '/' . $targetOwner . '/files_versions/' . $targetPath.'.v'.$v['version']
- );
- }
- }
-
- // 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);
- }
-
- }
-
- /**
- * Rollback to an old version of a file.
- *
- * @param string $file file name
- * @param int $revision revision timestamp
- */
- public static function rollback($file, $revision) {
-
- if(\OCP\Config::getSystemValue('files_versions', Storage::DEFAULTENABLED)=='true') {
- // add expected leading slash
- $file = '/' . ltrim($file, '/');
- list($uid, $filename) = self::getUidAndFilename($file);
- $users_view = new View('/'.$uid);
- $files_view = new View('/'. User::getUser().'/files');
- $versionCreated = false;
-
- //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, 'files_versions'.$filename.'.v'.$users_view->filemtime('files'.$filename));
- $versionCreated = true;
- }
-
- $fileToRestore = 'files_versions' . $filename . '.v' . $revision;
-
- // Restore encrypted version of the old file for the newly restored file
- // This has to happen manually here since the file is manually copied below
- $oldVersion = $users_view->getFileInfo($fileToRestore)->getEncryptedVersion();
- $newFileInfo = $files_view->getFileInfo($filename);
- $cache = $newFileInfo->getStorage()->getCache();
- $cache->update($newFileInfo->getId(), ['encrypted' => $oldVersion, 'encryptedVersion' => $oldVersion]);
-
- // rollback
- if (self::copyFileContents($users_view, $fileToRestore, 'files' . $filename)) {
- $files_view->touch($file, $revision);
- Storage::scheduleExpire($uid, $file);
- \OC_Hook::emit('\OCP\Versions', 'rollback', array(
- 'path' => $filename,
- 'revision' => $revision,
- ));
- return true;
- } else if ($versionCreated) {
- self::deleteVersion($users_view, $version);
- }
- }
- return false;
-
- }
-
- /**
- * Stream copy file contents from $path1 to $path2
- *
- * @param View $view view to use for copying
- * @param string $path1 source file to copy
- * @param string $path2 target file
- *
- * @return bool true for success, false otherwise
- */
- private static function copyFileContents($view, $path1, $path2) {
- /** @var \OC\Files\Storage\Storage $storage1 */
- list($storage1, $internalPath1) = $view->resolvePath($path1);
- /** @var \OC\Files\Storage\Storage $storage2 */
- list($storage2, $internalPath2) = $view->resolvePath($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);
- }
-
- $view->unlockFile($path1, ILockingProvider::LOCK_EXCLUSIVE);
- $view->unlockFile($path2, ILockingProvider::LOCK_EXCLUSIVE);
-
- return ($result !== false);
- }
-
- /**
- * get a list of all available 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
- * @param string $userFullPath
- * @return array versions newest version first
- */
- public static function getVersions($uid, $filename, $userFullPath = '') {
- $versions = array();
- if (empty($filename)) {
- return $versions;
- }
- // fetch for old versions
- $view = new View('/' . $uid . '/');
-
- $pathinfo = pathinfo($filename);
- $versionedFile = $pathinfo['basename'];
-
- $dir = Filesystem::normalizePath(self::VERSIONS_ROOT . '/' . $pathinfo['dirname']);
-
- $dirContent = false;
- if ($view->is_dir($dir)) {
- $dirContent = $view->opendir($dir);
- }
-
- if ($dirContent === false) {
- return $versions;
- }
-
- if (is_resource($dirContent)) {
- while (($entryName = readdir($dirContent)) !== false) {
- if (!Filesystem::isIgnoredDir($entryName)) {
- $pathparts = pathinfo($entryName);
- $filename = $pathparts['filename'];
- if ($filename === $versionedFile) {
- $pathparts = pathinfo($entryName);
- $timestamp = substr($pathparts['extension'], 1);
- $filename = $pathparts['filename'];
- $key = $timestamp . '#' . $filename;
- $versions[$key]['version'] = $timestamp;
- $versions[$key]['humanReadableTimestamp'] = self::getHumanReadableTimestamp($timestamp);
- if (empty($userFullPath)) {
- $versions[$key]['preview'] = '';
- } else {
- $versions[$key]['preview'] = \OCP\Util::linkToRoute('core_ajax_versions_preview', array('file' => $userFullPath, 'version' => $timestamp));
- }
- $versions[$key]['path'] = Filesystem::normalizePath($pathinfo['dirname'] . '/' . $filename);
- $versions[$key]['name'] = $versionedFile;
- $versions[$key]['size'] = $view->filesize($dir . '/' . $entryName);
- }
- }
- }
- closedir($dirContent);
- }
-
- // sort with newest version first
- krsort($versions);
-
- return $versions;
- }
-
- /**
- * Expire versions that older than max version retention time
- * @param string $uid
- */
- public static function expireOlderThanMaxForUser($uid){
- $expiration = self::getExpiration();
- $threshold = $expiration->getMaxAgeAsTimestamp();
- $versions = self::getAllVersions($uid);
- if (!$threshold || !array_key_exists('all', $versions)) {
- return;
- }
-
- $toDelete = [];
- foreach (array_reverse($versions['all']) as $key => $version) {
- if (intval($version['version'])<$threshold) {
- $toDelete[$key] = $version;
- } else {
- //Versions are sorted by time - nothing mo to iterate.
- break;
- }
- }
-
- $view = new View('/' . $uid . '/files_versions');
- if (!empty($toDelete)) {
- foreach ($toDelete as $version) {
- \OC_Hook::emit('\OCP\Versions', 'preDelete', array('path' => $version['path'].'.v'.$version['version'], 'trigger' => self::DELETE_TRIGGER_RETENTION_CONSTRAINT));
- self::deleteVersion($view, $version['path'] . '.v' . $version['version']);
- \OC_Hook::emit('\OCP\Versions', 'delete', array('path' => $version['path'].'.v'.$version['version'], 'trigger' => self::DELETE_TRIGGER_RETENTION_CONSTRAINT));
- }
- }
- }
-
- /**
- * translate a timestamp into a string like "5 days ago"
- * @param int $timestamp
- * @return string for example "5 days ago"
- */
- private static function getHumanReadableTimestamp($timestamp) {
-
- $diff = time() - $timestamp;
-
- if ($diff < 60) { // first minute
- return $diff . " seconds ago";
- } elseif ($diff < 3600) { //first hour
- return round($diff / 60) . " minutes ago";
- } elseif ($diff < 86400) { // first day
- return round($diff / 3600) . " hours ago";
- } elseif ($diff < 604800) { //first week
- return round($diff / 86400) . " days ago";
- } elseif ($diff < 2419200) { //first month
- return round($diff / 604800) . " weeks ago";
- } elseif ($diff < 29030400) { // first year
- return round($diff / 2419200) . " months ago";
- } else {
- return round($diff / 29030400) . " years ago";
- }
-
- }
-
- /**
- * returns all stored file versions from a given user
- * @param string $uid id of the user
- * @return array with contains two arrays 'all' which contains all versions sorted by age and 'by_file' which contains all versions sorted by filename
- */
- private static function getAllVersions($uid) {
- $view = new View('/' . $uid . '/');
- $dirs = array(self::VERSIONS_ROOT);
- $versions = array();
-
- while (!empty($dirs)) {
- $dir = array_pop($dirs);
- $files = $view->getDirectoryContent($dir);
-
- foreach ($files as $file) {
- if ($file['type'] === 'dir') {
- array_push($dirs, $file['path']);
- } else {
- $versionsBegin = strrpos($file['path'], '.v');
- $relPathStart = strlen(self::VERSIONS_ROOT);
- $version = substr($file['path'], $versionsBegin + 2);
- $relpath = substr($file['path'], $relPathStart, $versionsBegin - $relPathStart);
- $key = $version . '#' . $relpath;
- $versions[$key] = array('path' => $relpath, 'timestamp' => $version);
- }
- }
- }
-
- // newest version first
- krsort($versions);
-
- $result = array();
-
- foreach ($versions as $key => $value) {
- $size = $view->filesize(self::VERSIONS_ROOT.'/'.$value['path'].'.v'.$value['timestamp']);
- $filename = $value['path'];
-
- $result['all'][$key]['version'] = $value['timestamp'];
- $result['all'][$key]['path'] = $filename;
- $result['all'][$key]['size'] = $size;
-
- $result['by_file'][$filename][$key]['version'] = $value['timestamp'];
- $result['by_file'][$filename][$key]['path'] = $filename;
- $result['by_file'][$filename][$key]['size'] = $size;
- }
-
- return $result;
- }
-
- /**
- * get list of files we want to expire
- * @param array $versions list of versions
- * @param integer $time
- * @param bool $quotaExceeded is versions storage limit reached
- * @return array containing the list of to deleted versions and the size of them
- */
- protected static function getExpireList($time, $versions, $quotaExceeded = false) {
- $expiration = self::getExpiration();
-
- if ($expiration->shouldAutoExpire()) {
- list($toDelete, $size) = self::getAutoExpireList($time, $versions);
- } else {
- $size = 0;
- $toDelete = []; // versions we want to delete
- }
-
- foreach ($versions as $key => $version) {
- if ($expiration->isExpired($version['version'], $quotaExceeded) && !isset($toDelete[$key])) {
- $size += $version['size'];
- $toDelete[$key] = $version['path'] . '.v' . $version['version'];
- }
- }
-
- return [$toDelete, $size];
- }
-
- /**
- * get list of files we want to expire
- * @param array $versions list of versions
- * @param integer $time
- * @return array containing the list of to deleted versions and the size of them
- */
- protected static function getAutoExpireList($time, $versions) {
- $size = 0;
- $toDelete = array(); // versions we want to delete
-
- $interval = 1;
- $step = Storage::$max_versions_per_interval[$interval]['step'];
- if (Storage::$max_versions_per_interval[$interval]['intervalEndsAfter'] == -1) {
- $nextInterval = -1;
- } else {
- $nextInterval = $time - Storage::$max_versions_per_interval[$interval]['intervalEndsAfter'];
- }
-
- $firstVersion = reset($versions);
- $firstKey = key($versions);
- $prevTimestamp = $firstVersion['version'];
- $nextVersion = $firstVersion['version'] - $step;
- unset($versions[$firstKey]);
-
- foreach ($versions as $key => $version) {
- $newInterval = true;
- while ($newInterval) {
- if ($nextInterval == -1 || $prevTimestamp > $nextInterval) {
- if ($version['version'] > $nextVersion) {
- //distance between two version too small, mark to delete
- $toDelete[$key] = $version['path'] . '.v' . $version['version'];
- $size += $version['size'];
- \OCP\Util::writeLog('files_versions', 'Mark to expire '. $version['path'] .' next version should be ' . $nextVersion . " or smaller. (prevTimestamp: " . $prevTimestamp . "; step: " . $step, \OCP\Util::DEBUG);
- } else {
- $nextVersion = $version['version'] - $step;
- $prevTimestamp = $version['version'];
- }
- $newInterval = false; // version checked so we can move to the next one
- } else { // time to move on to the next interval
- $interval++;
- $step = Storage::$max_versions_per_interval[$interval]['step'];
- $nextVersion = $prevTimestamp - $step;
- if (Storage::$max_versions_per_interval[$interval]['intervalEndsAfter'] == -1) {
- $nextInterval = -1;
- } else {
- $nextInterval = $time - Storage::$max_versions_per_interval[$interval]['intervalEndsAfter'];
- }
- $newInterval = true; // we changed the interval -> check same version with new interval
- }
- }
- }
-
- return array($toDelete, $size);
- }
-
- /**
- * Schedule versions expiration for the given file
- *
- * @param string $uid owner of the file
- * @param string $fileName file/folder for which to schedule expiration
- */
- private static function scheduleExpire($uid, $fileName) {
- // let the admin disable auto expire
- $expiration = self::getExpiration();
- if ($expiration->isEnabled()) {
- $command = new Expire($uid, $fileName);
- \OC::$server->getCommandBus()->push($command);
- }
- }
-
- /**
- * Expire versions which exceed the quota
- *
- * @param string $filename
- * @return bool|int|null
- */
- public static function expire($filename) {
- $config = \OC::$server->getConfig();
- $expiration = self::getExpiration();
-
- if($config->getSystemValue('files_versions', Storage::DEFAULTENABLED)=='true' && $expiration->isEnabled()) {
-
- if (!Filesystem::file_exists($filename)) {
- return false;
- }
-
- list($uid, $filename) = self::getUidAndFilename($filename);
- if (empty($filename)) {
- // file maybe renamed or deleted
- return false;
- }
- $versionsFileview = new View('/'.$uid.'/files_versions');
-
- // get available disk space for user
- $user = \OC::$server->getUserManager()->get($uid);
- $softQuota = true;
- $quota = $user->getQuota();
- if ( $quota === null || $quota === 'none' ) {
- $quota = Filesystem::free_space('/');
- $softQuota = false;
- } else {
- $quota = \OCP\Util::computerFileSize($quota);
- }
-
- // make sure that we have the current size of the version history
- $versionsSize = self::getVersionsSize($uid);
-
- // calculate available space for version history
- // subtract size of files and current versions size from quota
- if ($quota >= 0) {
- if ($softQuota) {
- $files_view = new View('/' . $uid . '/files');
- $rootInfo = $files_view->getFileInfo('/', false);
- $free = $quota - $rootInfo['size']; // remaining free space for user
- if ($free > 0) {
- $availableSpace = ($free * self::DEFAULTMAXSIZE / 100) - $versionsSize; // how much space can be used for versions
- } else {
- $availableSpace = $free - $versionsSize;
- }
- } else {
- $availableSpace = $quota;
- }
- } else {
- $availableSpace = PHP_INT_MAX;
- }
-
- $allVersions = Storage::getVersions($uid, $filename);
-
- $time = time();
- list($toDelete, $sizeOfDeletedVersions) = self::getExpireList($time, $allVersions, $availableSpace <= 0);
-
- $availableSpace = $availableSpace + $sizeOfDeletedVersions;
- $versionsSize = $versionsSize - $sizeOfDeletedVersions;
-
- // if still not enough free space we rearrange the versions from all files
- if ($availableSpace <= 0) {
- $result = Storage::getAllVersions($uid);
- $allVersions = $result['all'];
-
- foreach ($result['by_file'] as $versions) {
- list($toDeleteNew, $size) = self::getExpireList($time, $versions, $availableSpace <= 0);
- $toDelete = array_merge($toDelete, $toDeleteNew);
- $sizeOfDeletedVersions += $size;
- }
- $availableSpace = $availableSpace + $sizeOfDeletedVersions;
- $versionsSize = $versionsSize - $sizeOfDeletedVersions;
- }
-
- foreach($toDelete as $key => $path) {
- \OC_Hook::emit('\OCP\Versions', 'preDelete', array('path' => $path, 'trigger' => self::DELETE_TRIGGER_QUOTA_EXCEEDED));
- self::deleteVersion($versionsFileview, $path);
- \OC_Hook::emit('\OCP\Versions', 'delete', array('path' => $path, 'trigger' => self::DELETE_TRIGGER_QUOTA_EXCEEDED));
- unset($allVersions[$key]); // update array with the versions we keep
- \OCP\Util::writeLog('files_versions', "Expire: " . $path, \OCP\Util::DEBUG);
- }
-
- // Check if enough space is available after versions are rearranged.
- // If not we delete the oldest versions until we meet the size limit for versions,
- // but always keep the two latest versions
- $numOfVersions = count($allVersions) -2 ;
- $i = 0;
- // sort oldest first and make sure that we start at the first element
- ksort($allVersions);
- reset($allVersions);
- while ($availableSpace < 0 && $i < $numOfVersions) {
- $version = current($allVersions);
- \OC_Hook::emit('\OCP\Versions', 'preDelete', array('path' => $version['path'].'.v'.$version['version'], 'trigger' => self::DELETE_TRIGGER_QUOTA_EXCEEDED));
- self::deleteVersion($versionsFileview, $version['path'] . '.v' . $version['version']);
- \OC_Hook::emit('\OCP\Versions', 'delete', array('path' => $version['path'].'.v'.$version['version'], 'trigger' => self::DELETE_TRIGGER_QUOTA_EXCEEDED));
- \OCP\Util::writeLog('files_versions', 'running out of space! Delete oldest version: ' . $version['path'].'.v'.$version['version'] , \OCP\Util::DEBUG);
- $versionsSize -= $version['size'];
- $availableSpace += $version['size'];
- next($allVersions);
- $i++;
- }
-
- return $versionsSize; // finally return the new size of the version history
- }
-
- return false;
- }
-
- /**
- * Create recursively missing directories inside of files_versions
- * that match the given path to a file.
- *
- * @param string $filename $path to a file, relative to the user's
- * "files" folder
- * @param View $view view on data/user/
- */
- private static function createMissingDirectories($filename, $view) {
- $dirname = Filesystem::normalizePath(dirname($filename));
- $dirParts = explode('/', $dirname);
- $dir = "/files_versions";
- foreach ($dirParts as $part) {
- $dir = $dir . '/' . $part;
- if (!$view->file_exists($dir)) {
- $view->mkdir($dir);
- }
- }
- }
-
- /**
- * Static workaround
- * @return Expiration
- */
- protected static function getExpiration(){
- if (is_null(self::$application)) {
- self::$application = new Application();
- }
- return self::$application->getContainer()->query('Expiration');
- }
-
-}
diff --git a/apps/files_versions/openapi.json b/apps/files_versions/openapi.json
new file mode 100644
index 00000000000..aea18edf3ec
--- /dev/null
+++ b/apps/files_versions/openapi.json
@@ -0,0 +1,164 @@
+{
+ "openapi": "3.0.3",
+ "info": {
+ "title": "files_versions",
+ "version": "0.0.1",
+ "description": "This application automatically maintains older versions of files that are changed.",
+ "license": {
+ "name": "agpl"
+ }
+ },
+ "components": {
+ "securitySchemes": {
+ "basic_auth": {
+ "type": "http",
+ "scheme": "basic"
+ },
+ "bearer_auth": {
+ "type": "http",
+ "scheme": "bearer"
+ }
+ },
+ "schemas": {
+ "Capabilities": {
+ "type": "object",
+ "required": [
+ "files"
+ ],
+ "properties": {
+ "files": {
+ "type": "object",
+ "required": [
+ "versioning",
+ "version_labeling",
+ "version_deletion"
+ ],
+ "properties": {
+ "versioning": {
+ "type": "boolean"
+ },
+ "version_labeling": {
+ "type": "boolean"
+ },
+ "version_deletion": {
+ "type": "boolean"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "paths": {
+ "/index.php/apps/files_versions/preview": {
+ "get": {
+ "operationId": "preview-get-preview",
+ "summary": "Get the preview for a file version",
+ "tags": [
+ "preview"
+ ],
+ "security": [
+ {
+ "bearer_auth": []
+ },
+ {
+ "basic_auth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "file",
+ "in": "query",
+ "description": "Path of the file",
+ "schema": {
+ "type": "string",
+ "default": ""
+ }
+ },
+ {
+ "name": "x",
+ "in": "query",
+ "description": "Width of the preview",
+ "schema": {
+ "type": "integer",
+ "format": "int64",
+ "default": 44
+ }
+ },
+ {
+ "name": "y",
+ "in": "query",
+ "description": "Height of the preview",
+ "schema": {
+ "type": "integer",
+ "format": "int64",
+ "default": 44
+ }
+ },
+ {
+ "name": "version",
+ "in": "query",
+ "description": "Version of the file to get the preview for",
+ "schema": {
+ "type": "string",
+ "default": ""
+ }
+ },
+ {
+ "name": "mimeFallback",
+ "in": "query",
+ "description": "Whether to fallback to the mime icon if no preview is available",
+ "schema": {
+ "type": "integer",
+ "default": 0,
+ "enum": [
+ 0,
+ 1
+ ]
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Preview returned",
+ "content": {
+ "*/*": {
+ "schema": {
+ "type": "string",
+ "format": "binary"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Getting preview is not possible",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ },
+ "404": {
+ "description": "Preview not found",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ },
+ "303": {
+ "description": "Redirect to the mime icon url if mimeFallback is true",
+ "headers": {
+ "Location": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "tags": []
+}
diff --git a/apps/files_versions/openapi.json.license b/apps/files_versions/openapi.json.license
new file mode 100644
index 00000000000..83559daa9dc
--- /dev/null
+++ b/apps/files_versions/openapi.json.license
@@ -0,0 +1,2 @@
+SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+SPDX-License-Identifier: AGPL-3.0-or-later \ No newline at end of file
diff --git a/apps/files_versions/src/components/Version.vue b/apps/files_versions/src/components/Version.vue
new file mode 100644
index 00000000000..dc36e4134f9
--- /dev/null
+++ b/apps/files_versions/src/components/Version.vue
@@ -0,0 +1,393 @@
+<!--
+ - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+<template>
+ <NcListItem class="version"
+ :force-display-actions="true"
+ :actions-aria-label="t('files_versions', 'Actions for version from {versionHumanExplicitDate}', { versionHumanExplicitDate })"
+ :data-files-versions-version="version.fileVersion"
+ @click="click">
+ <!-- Icon -->
+ <template #icon>
+ <div v-if="!(loadPreview || previewLoaded)" class="version__image" />
+ <img v-else-if="version.previewUrl && !previewErrored"
+ :src="version.previewUrl"
+ alt=""
+ decoding="async"
+ fetchpriority="low"
+ loading="lazy"
+ class="version__image"
+ @load="previewLoaded = true"
+ @error="previewErrored = true">
+ <div v-else
+ class="version__image">
+ <ImageOffOutline :size="20" />
+ </div>
+ </template>
+
+ <!-- author -->
+ <template #name>
+ <div class="version__info">
+ <div v-if="versionLabel"
+ class="version__info__label"
+ data-cy-files-version-label
+ :title="versionLabel">
+ {{ versionLabel }}
+ </div>
+ <div v-if="versionAuthor"
+ class="version__info"
+ data-cy-files-version-author-name>
+ <span v-if="versionLabel">•</span>
+ <NcAvatar class="avatar"
+ :user="version.author"
+ :size="20"
+ disable-menu
+ disable-tooltip
+ :show-user-status="false" />
+ <div class="version__info__author_name"
+ :title="versionAuthor">
+ {{ versionAuthor }}
+ </div>
+ </div>
+ </div>
+ </template>
+
+ <!-- Version file size as subline -->
+ <template #subname>
+ <div class="version__info version__info__subline">
+ <NcDateTime class="version__info__date"
+ relative-time="short"
+ :timestamp="version.mtime" />
+ <!-- Separate dot to improve alignment -->
+ <span>•</span>
+ <span>{{ humanReadableSize }}</span>
+ </div>
+ </template>
+
+ <!-- Actions -->
+ <template #actions>
+ <NcActionButton v-if="enableLabeling && hasUpdatePermissions"
+ data-cy-files-versions-version-action="label"
+ :close-after-click="true"
+ @click="labelUpdate">
+ <template #icon>
+ <Pencil :size="22" />
+ </template>
+ {{ version.label === '' ? t('files_versions', 'Name this version') : t('files_versions', 'Edit version name') }}
+ </NcActionButton>
+ <NcActionButton v-if="!isCurrent && canView && canCompare"
+ data-cy-files-versions-version-action="compare"
+ :close-after-click="true"
+ @click="compareVersion">
+ <template #icon>
+ <FileCompare :size="22" />
+ </template>
+ {{ t('files_versions', 'Compare to current version') }}
+ </NcActionButton>
+ <NcActionButton v-if="!isCurrent && hasUpdatePermissions"
+ data-cy-files-versions-version-action="restore"
+ :close-after-click="true"
+ @click="restoreVersion">
+ <template #icon>
+ <BackupRestore :size="22" />
+ </template>
+ {{ t('files_versions', 'Restore version') }}
+ </NcActionButton>
+ <NcActionLink v-if="isDownloadable"
+ data-cy-files-versions-version-action="download"
+ :href="downloadURL"
+ :close-after-click="true"
+ :download="downloadURL">
+ <template #icon>
+ <Download :size="22" />
+ </template>
+ {{ t('files_versions', 'Download version') }}
+ </NcActionLink>
+ <NcActionButton v-if="!isCurrent && enableDeletion && hasDeletePermissions"
+ data-cy-files-versions-version-action="delete"
+ :close-after-click="true"
+ @click="deleteVersion">
+ <template #icon>
+ <Delete :size="22" />
+ </template>
+ {{ t('files_versions', 'Delete version') }}
+ </NcActionButton>
+ </template>
+ </NcListItem>
+</template>
+<script lang="ts">
+import type { PropType } from 'vue'
+import type { Version } from '../utils/versions'
+
+import { getCurrentUser } from '@nextcloud/auth'
+import { Permission, formatFileSize } from '@nextcloud/files'
+import { loadState } from '@nextcloud/initial-state'
+import { t } from '@nextcloud/l10n'
+import { joinPaths } from '@nextcloud/paths'
+import { getRootUrl, generateUrl } from '@nextcloud/router'
+import { defineComponent } from 'vue'
+
+import axios from '@nextcloud/axios'
+import moment from '@nextcloud/moment'
+import logger from '../utils/logger'
+
+import BackupRestore from 'vue-material-design-icons/BackupRestore.vue'
+import Delete from 'vue-material-design-icons/Delete.vue'
+import Download from 'vue-material-design-icons/Download.vue'
+import FileCompare from 'vue-material-design-icons/FileCompare.vue'
+import ImageOffOutline from 'vue-material-design-icons/ImageOffOutline.vue'
+import Pencil from 'vue-material-design-icons/PencilOutline.vue'
+
+import NcActionButton from '@nextcloud/vue/components/NcActionButton'
+import NcActionLink from '@nextcloud/vue/components/NcActionLink'
+import NcAvatar from '@nextcloud/vue/components/NcAvatar'
+import NcDateTime from '@nextcloud/vue/components/NcDateTime'
+import NcListItem from '@nextcloud/vue/components/NcListItem'
+import Tooltip from '@nextcloud/vue/directives/Tooltip'
+
+const hasPermission = (permissions: number, permission: number): boolean => (permissions & permission) !== 0
+
+export default defineComponent({
+ name: 'Version',
+
+ components: {
+ NcActionLink,
+ NcActionButton,
+ NcAvatar,
+ NcDateTime,
+ NcListItem,
+ BackupRestore,
+ Download,
+ FileCompare,
+ Pencil,
+ Delete,
+ ImageOffOutline,
+ },
+
+ directives: {
+ tooltip: Tooltip,
+ },
+
+ props: {
+ version: {
+ type: Object as PropType<Version>,
+ required: true,
+ },
+ fileInfo: {
+ type: Object,
+ required: true,
+ },
+ isCurrent: {
+ type: Boolean,
+ default: false,
+ },
+ isFirstVersion: {
+ type: Boolean,
+ default: false,
+ },
+ loadPreview: {
+ type: Boolean,
+ default: false,
+ },
+ canView: {
+ type: Boolean,
+ default: false,
+ },
+ canCompare: {
+ type: Boolean,
+ default: false,
+ },
+ },
+
+ emits: ['click', 'compare', 'restore', 'delete', 'label-update-request'],
+
+ data() {
+ return {
+ previewLoaded: false,
+ previewErrored: false,
+ capabilities: loadState('core', 'capabilities', { files: { version_labeling: false, version_deletion: false } }),
+ versionAuthor: '' as string | null,
+ }
+ },
+
+ computed: {
+ humanReadableSize() {
+ return formatFileSize(this.version.size)
+ },
+
+ versionLabel(): string {
+ const label = this.version.label ?? ''
+
+ if (this.isCurrent) {
+ if (label === '') {
+ return t('files_versions', 'Current version')
+ } else {
+ return `${label} (${t('files_versions', 'Current version')})`
+ }
+ }
+
+ if (this.isFirstVersion && label === '') {
+ return t('files_versions', 'Initial version')
+ }
+
+ return label
+ },
+
+ versionHumanExplicitDate(): string {
+ return moment(this.version.mtime).format('LLLL')
+ },
+
+ downloadURL(): string {
+ if (this.isCurrent) {
+ return getRootUrl() + joinPaths('/remote.php/webdav', this.fileInfo.path, this.fileInfo.name)
+ } else {
+ return getRootUrl() + this.version.url
+ }
+ },
+
+ enableLabeling(): boolean {
+ return this.capabilities.files.version_labeling === true
+ },
+
+ enableDeletion(): boolean {
+ return this.capabilities.files.version_deletion === true
+ },
+
+ hasDeletePermissions(): boolean {
+ return hasPermission(this.fileInfo.permissions, Permission.DELETE)
+ },
+
+ hasUpdatePermissions(): boolean {
+ return hasPermission(this.fileInfo.permissions, Permission.UPDATE)
+ },
+
+ isDownloadable(): boolean {
+ if ((this.fileInfo.permissions & Permission.READ) === 0) {
+ return false
+ }
+
+ // If the mount type is a share, ensure it got download permissions.
+ if (this.fileInfo.mountType === 'shared') {
+ const downloadAttribute = this.fileInfo.shareAttributes
+ .find((attribute) => attribute.scope === 'permissions' && attribute.key === 'download') || {}
+ // If the download attribute is set to false, the file is not downloadable
+ if (downloadAttribute?.value === false) {
+ return false
+ }
+ }
+
+ return true
+ },
+ },
+
+ created() {
+ this.fetchDisplayName()
+ },
+
+ methods: {
+ labelUpdate() {
+ this.$emit('label-update-request')
+ },
+
+ restoreVersion() {
+ this.$emit('restore', this.version)
+ },
+
+ async deleteVersion() {
+ // Let @nc-vue properly remove the popover before we delete the version.
+ // This prevents @nc-vue from throwing a error.
+ await this.$nextTick()
+ await this.$nextTick()
+ this.$emit('delete', this.version)
+ },
+
+ async fetchDisplayName() {
+ this.versionAuthor = null
+ if (!this.version.author) {
+ return
+ }
+
+ if (this.version.author === getCurrentUser()?.uid) {
+ this.versionAuthor = t('files_versions', 'You')
+ } else {
+ try {
+ const { data } = await axios.post(generateUrl('/displaynames'), { users: [this.version.author] })
+ this.versionAuthor = data.users[this.version.author]
+ } catch (error) {
+ logger.warn('Could not load user display name', { error })
+ }
+ }
+ },
+
+ click() {
+ if (!this.canView) {
+ window.location.href = this.downloadURL
+ return
+ }
+ this.$emit('click', { version: this.version })
+ },
+
+ compareVersion() {
+ if (!this.canView) {
+ throw new Error('Cannot compare version of this file')
+ }
+ this.$emit('compare', { version: this.version })
+ },
+
+ t,
+ },
+})
+</script>
+
+<style scoped lang="scss">
+.version {
+ display: flex;
+ flex-direction: row;
+
+ &__info {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 0.5rem;
+ color: var(--color-main-text);
+ font-weight: 500;
+ overflow: hidden;
+
+ &__label {
+ font-weight: 700;
+ // Fix overflow on narrow screens
+ overflow: hidden;
+ text-overflow: ellipsis;
+ min-width: 110px;
+ }
+
+ &__author_name {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ &__date {
+ // Fix overflow on narrow screens
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ &__subline {
+ color: var(--color-text-maxcontrast)
+ }
+ }
+
+ &__image {
+ width: 3rem;
+ height: 3rem;
+ border: 1px solid var(--color-border);
+ border-radius: var(--border-radius-large);
+
+ // Useful to display no preview icon.
+ display: flex;
+ justify-content: center;
+ color: var(--color-text-light);
+ }
+}
+</style>
diff --git a/apps/files_versions/src/components/VersionLabelDialog.vue b/apps/files_versions/src/components/VersionLabelDialog.vue
new file mode 100644
index 00000000000..760780cae61
--- /dev/null
+++ b/apps/files_versions/src/components/VersionLabelDialog.vue
@@ -0,0 +1,123 @@
+<!--
+ - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+<template>
+ <NcDialog :buttons="dialogButtons"
+ content-classes="version-label-modal"
+ is-form
+ :open="open"
+ size="normal"
+ :name="t('files_versions', 'Name this version')"
+ @update:open="$emit('update:open', $event)"
+ @submit="setVersionLabel(editedVersionLabel)">
+ <NcTextField ref="labelInput"
+ class="version-label-modal__input"
+ :label="t('files_versions', 'Version name')"
+ :placeholder="t('files_versions', 'Version name')"
+ :value.sync="editedVersionLabel" />
+
+ <p class="version-label-modal__info">
+ {{ t('files_versions', 'Named versions are persisted, and excluded from automatic cleanups when your storage quota is full.') }}
+ </p>
+ </NcDialog>
+</template>
+
+<script lang="ts">
+import { t } from '@nextcloud/l10n'
+import { defineComponent } from 'vue'
+import svgCheck from '@mdi/svg/svg/check.svg?raw'
+
+import NcDialog from '@nextcloud/vue/components/NcDialog'
+import NcTextField from '@nextcloud/vue/components/NcTextField'
+
+type Focusable = Vue & { focus: () => void }
+
+export default defineComponent({
+ name: 'VersionLabelDialog',
+ components: {
+ NcDialog,
+ NcTextField,
+ },
+ props: {
+ open: {
+ type: Boolean,
+ default: false,
+ },
+ versionLabel: {
+ type: String,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ editedVersionLabel: '',
+ }
+ },
+ computed: {
+ dialogButtons() {
+ const buttons: unknown[] = []
+ if (this.versionLabel.trim() === '') {
+ // If there is no label just offer a cancel action that just closes the dialog
+ buttons.push({
+ label: t('files_versions', 'Cancel'),
+ })
+ } else {
+ // If there is already a label set, offer to remove the version label
+ buttons.push({
+ label: t('files_versions', 'Remove version name'),
+ type: 'error',
+ nativeType: 'reset',
+ callback: () => { this.setVersionLabel('') },
+ })
+ }
+ return [
+ ...buttons,
+ {
+ label: t('files_versions', 'Save version name'),
+ type: 'primary',
+ nativeType: 'submit',
+ icon: svgCheck,
+ },
+ ]
+ },
+ },
+ watch: {
+ versionLabel: {
+ immediate: true,
+ handler(label) {
+ this.editedVersionLabel = label ?? ''
+ },
+ },
+ open: {
+ immediate: true,
+ handler(open) {
+ if (open) {
+ this.$nextTick(() => (this.$refs.labelInput as Focusable).focus())
+ }
+ this.editedVersionLabel = this.versionLabel
+ },
+ },
+ },
+ methods: {
+ setVersionLabel(label: string) {
+ this.$emit('label-update', label)
+ },
+
+ t,
+ },
+})
+</script>
+
+<style scoped lang="scss">
+.version-label-modal {
+ &__info {
+ color: var(--color-text-maxcontrast);
+ margin-block: calc(3 * var(--default-grid-baseline));
+ }
+
+ &__input {
+ margin-block-start: calc(2 * var(--default-grid-baseline));
+ }
+}
+</style>
diff --git a/apps/files_versions/src/components/VirtualScrolling.vue b/apps/files_versions/src/components/VirtualScrolling.vue
new file mode 100644
index 00000000000..5a502036839
--- /dev/null
+++ b/apps/files_versions/src/components/VirtualScrolling.vue
@@ -0,0 +1,346 @@
+<!--
+ - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+<template>
+ <div v-if="!useWindow && containerElement === null" ref="container" class="vs-container">
+ <div ref="rowsContainer"
+ class="vs-rows-container"
+ :style="rowsContainerStyle">
+ <slot :visible-sections="visibleSections" />
+ <slot name="loader" />
+ </div>
+ </div>
+ <div v-else
+ ref="rowsContainer"
+ class="vs-rows-container"
+ :style="rowsContainerStyle">
+ <slot :visible-sections="visibleSections" />
+ <slot name="loader" />
+ </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, type PropType } from 'vue'
+
+import logger from '../utils/logger.js'
+
+interface RowItem {
+ id: string // Unique id for the item.
+ key?: string // Unique key for the item.
+}
+
+interface Row {
+ key: string // Unique key for the row.
+ height: number // The height of the row.
+ sectionKey: string // Unique key for the row.
+ items: RowItem[] // List of items in the row.
+}
+
+interface VisibleRow extends Row {
+ distance: number // The distance from the visible viewport
+}
+
+interface Section {
+ key: string, // Unique key for the section.
+ rows: Row[], // The height of the row.
+ height: number, // Height of the section, excluding the header.
+}
+
+interface VisibleSection extends Section {
+ rows: VisibleRow[], // The height of the row.
+}
+
+export default defineComponent({
+ name: 'VirtualScrolling',
+
+ props: {
+ sections: {
+ type: Array as PropType<Section[]>,
+ required: true,
+ },
+
+ containerElement: {
+ type: HTMLElement,
+ default: null,
+ },
+
+ useWindow: {
+ type: Boolean,
+ default: false,
+ },
+
+ headerHeight: {
+ type: Number,
+ default: 75,
+ },
+ renderDistance: {
+ type: Number,
+ default: 0.5,
+ },
+ bottomBufferRatio: {
+ type: Number,
+ default: 2,
+ },
+ scrollToKey: {
+ type: String,
+ default: '',
+ },
+ },
+
+ data() {
+ return {
+ scrollPosition: 0,
+ containerHeight: 0,
+ rowsContainerHeight: 0,
+ resizeObserver: null as ResizeObserver|null,
+ }
+ },
+
+ computed: {
+ visibleSections(): VisibleSection[] {
+ logger.debug('[VirtualScrolling] Computing visible section', { sections: this.sections })
+
+ // Optimization: get those computed properties once to not go through vue's internal every time we need them.
+ const containerHeight = this.containerHeight
+ const containerTop = this.scrollPosition
+ const containerBottom = containerTop + containerHeight
+
+ let currentRowTop = 0
+ let currentRowBottom = 0
+
+ // Compute whether a row should be included in the DOM (shouldRender)
+ // And how visible the row is.
+ const visibleSections = this.sections
+ .map(section => {
+ currentRowBottom += this.headerHeight
+
+ return {
+ ...section,
+ rows: section.rows.reduce((visibleRows, row) => {
+ currentRowTop = currentRowBottom
+ currentRowBottom += row.height
+
+ let distance = 0
+
+ if (currentRowBottom < containerTop) {
+ distance = (containerTop - currentRowBottom) / containerHeight
+ } else if (currentRowTop > containerBottom) {
+ distance = (currentRowTop - containerBottom) / containerHeight
+ }
+
+ if (distance > this.renderDistance) {
+ return visibleRows
+ }
+
+ return [
+ ...visibleRows,
+ {
+ ...row,
+ distance,
+ },
+ ]
+ }, [] as VisibleRow[]),
+ }
+ })
+ .filter(section => section.rows.length > 0)
+
+ // To allow vue to recycle the DOM elements instead of adding and deleting new ones,
+ // we assign a random key to each items. When a item removed, we recycle its key for new items,
+ // so vue can replace the content of removed DOM elements with the content of new items, but keep the other DOM elements untouched.
+ const visibleItems = visibleSections
+ .flatMap(({ rows }) => rows)
+ .flatMap(({ items }) => items)
+
+ const rowIdToKeyMap = this._rowIdToKeyMap as {[key: string]: string}
+
+ visibleItems.forEach(item => (item.key = rowIdToKeyMap[item.id]))
+
+ const usedTokens = visibleItems
+ .map(({ key }) => key)
+ .filter(key => key !== undefined)
+
+ const unusedTokens = Object.values(rowIdToKeyMap).filter(key => !usedTokens.includes(key))
+
+ visibleItems
+ .filter(({ key }) => key === undefined)
+ .forEach(item => (item.key = unusedTokens.pop() ?? Math.random().toString(36).substr(2)))
+
+ // this._rowIdToKeyMap is created in the beforeCreate hook, so value changes are not tracked.
+ // Therefore, we wont trigger the computation of visibleSections again if we alter the value of this._rowIdToKeyMap.
+ // eslint-disable-next-line vue/no-side-effects-in-computed-properties
+ this._rowIdToKeyMap = visibleItems.reduce((finalMapping, { id, key }) => ({ ...finalMapping, [`${id}`]: key }), {})
+
+ return visibleSections
+ },
+
+ /**
+ * Total height of all the rows + some room for the loader.
+ */
+ totalHeight(): number {
+ const loaderHeight = 0
+
+ return this.sections
+ .map(section => this.headerHeight + section.height)
+ .reduce((totalHeight, sectionHeight) => totalHeight + sectionHeight, 0) + loaderHeight
+ },
+
+ paddingTop(): number {
+ if (this.visibleSections.length === 0) {
+ return 0
+ }
+
+ let paddingTop = 0
+
+ for (const section of this.sections) {
+ if (section.key !== this.visibleSections[0].rows[0].sectionKey) {
+ paddingTop += this.headerHeight + section.height
+ continue
+ }
+
+ for (const row of section.rows) {
+ if (row.key === this.visibleSections[0].rows[0].key) {
+ return paddingTop
+ }
+
+ paddingTop += row.height
+ }
+
+ paddingTop += this.headerHeight
+ }
+
+ return paddingTop
+ },
+
+ /**
+ * padding-top is used to replace not included item in the container.
+ */
+ rowsContainerStyle(): { height: string; paddingTop: string } {
+ return {
+ height: `${this.totalHeight}px`,
+ paddingTop: `${this.paddingTop}px`,
+ }
+ },
+
+ /**
+ * Whether the user is near the bottom.
+ * If true, then the need-content event will be emitted.
+ */
+ isNearBottom(): boolean {
+ const buffer = this.containerHeight * this.bottomBufferRatio
+ return this.scrollPosition + this.containerHeight >= this.totalHeight - buffer
+ },
+
+ container() {
+ logger.debug('[VirtualScrolling] Computing container')
+ if (this.containerElement !== null) {
+ return this.containerElement
+ } else if (this.useWindow) {
+ return window
+ } else {
+ return this.$refs.container as Element
+ }
+ },
+ },
+
+ watch: {
+ isNearBottom(value) {
+ logger.debug('[VirtualScrolling] isNearBottom changed', { value })
+ if (value) {
+ this.$emit('need-content')
+ }
+ },
+
+ visibleSections() {
+ // Re-emit need-content when rows is updated and isNearBottom is still true.
+ // If the height of added rows is under `bottomBufferRatio`, `isNearBottom` will still be true so we need more content.
+ if (this.isNearBottom) {
+ this.$emit('need-content')
+ }
+ },
+
+ scrollToKey(key) {
+ let currentRowTopDistanceFromTop = 0
+
+ for (const section of this.sections) {
+ if (section.key !== key) {
+ currentRowTopDistanceFromTop += this.headerHeight + section.height
+ continue
+ }
+
+ break
+ }
+
+ logger.debug('[VirtualScrolling] Scrolling to', { currentRowTopDistanceFromTop })
+ this.container.scrollTo({ top: currentRowTopDistanceFromTop, behavior: 'smooth' })
+ },
+ },
+
+ beforeCreate() {
+ this._rowIdToKeyMap = {}
+ },
+
+ mounted() {
+ this.resizeObserver = new ResizeObserver(entries => {
+ for (const entry of entries) {
+ const cr = entry.contentRect
+ if (entry.target === this.container) {
+ this.containerHeight = cr.height
+ }
+ if (entry.target.classList.contains('vs-rows-container')) {
+ this.rowsContainerHeight = cr.height
+ }
+ }
+ })
+
+ if (this.useWindow) {
+ window.addEventListener('resize', this.updateContainerSize, { passive: true })
+ this.containerHeight = window.innerHeight
+ } else {
+ this.resizeObserver.observe(this.container as HTMLElement|Element)
+ }
+
+ this.resizeObserver.observe(this.$refs.rowsContainer as Element)
+ this.container.addEventListener('scroll', this.updateScrollPosition, { passive: true })
+ },
+
+ beforeDestroy() {
+ if (this.useWindow) {
+ window.removeEventListener('resize', this.updateContainerSize)
+ }
+
+ this.resizeObserver?.disconnect()
+ this.container.removeEventListener('scroll', this.updateScrollPosition)
+ },
+
+ methods: {
+ updateScrollPosition() {
+ this._onScrollHandle ??= requestAnimationFrame(() => {
+ this._onScrollHandle = null
+ if (this.useWindow) {
+ this.scrollPosition = (this.container as Window).scrollY
+ } else {
+ this.scrollPosition = (this.container as HTMLElement|Element).scrollTop
+ }
+ })
+ },
+
+ updateContainerSize() {
+ this.containerHeight = window.innerHeight
+ },
+ },
+})
+</script>
+
+<style scoped lang="scss">
+.vs-container {
+ overflow-y: scroll;
+ height: 100%;
+}
+
+.vs-rows-container {
+ box-sizing: border-box;
+ will-change: scroll-position, padding;
+ contain: layout paint style;
+}
+</style>
diff --git a/apps/files_versions/src/css/versions.css b/apps/files_versions/src/css/versions.css
new file mode 100644
index 00000000000..1637394ef48
--- /dev/null
+++ b/apps/files_versions/src/css/versions.css
@@ -0,0 +1,103 @@
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2012-2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+.versionsTabView .clear-float {
+ clear: both;
+}
+
+.versionsTabView li {
+ width: 100%;
+ cursor: default;
+ height: 56px;
+ float: left;
+ border-bottom: 0;
+}
+
+.versionsTabView li:last-child {
+ border-bottom: none;
+}
+
+.versionsTabView a,
+.versionsTabView div > span {
+ vertical-align: middle;
+ opacity: .5;
+}
+
+.versionsTabView li a{
+ padding: 19px 10px 7px;
+}
+
+.versionsTabView a:hover,
+.versionsTabView a:focus {
+ opacity: 1;
+}
+
+.versionsTabView .preview-container {
+ display: inline-block;
+ vertical-align: top;
+}
+
+.versionsTabView img {
+ cursor: pointer;
+ padding-inline-end: 4px;
+}
+
+.versionsTabView img.preview {
+ position: relative;
+ top: 6px;
+ inset-inline-start: 10px;
+ border: 1px solid var(--color-border-dark);
+ cursor: default;
+ padding-inline-end: 0;
+}
+
+.versionsTabView .version-container {
+ display: inline-block;
+}
+
+.versionsTabView .versiondate {
+ min-width: 100px;
+ vertical-align: super;
+}
+
+.versionsTabView .version-details {
+ text-align: start;
+}
+
+.versionsTabView .version-details > span {
+ padding: 0 10px;
+}
+
+.versionsTabView .revertVersion {
+ cursor: pointer;
+ float: right;
+ margin-inline-end: 0;
+}
+
+.versionsTabView li.active .downloadVersion {
+ opacity: 1;
+}
+
+.versionsTabView li.active .version-details .size {
+ color: var(--color-main-text);
+ opacity: 1;
+}
+
+.versionsTabView li.active {
+ background-color: var(--color-primary-light);
+ border-radius: 16px;
+}
+
+.versionsTabView li.active a.revertVersion {
+ opacity: 1;
+}
+
+.version-container {
+ padding-inline-start: 5px;
+}
+
+.version-details {
+ margin-top: -7px;
+}
diff --git a/apps/files_versions/src/files_versions_tab.js b/apps/files_versions/src/files_versions_tab.js
new file mode 100644
index 00000000000..12f36bad24a
--- /dev/null
+++ b/apps/files_versions/src/files_versions_tab.js
@@ -0,0 +1,62 @@
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import Vue from 'vue'
+import { translate as t, translatePlural as n } from '@nextcloud/l10n'
+
+import VersionTab from './views/VersionTab.vue'
+import VTooltipPlugin from 'v-tooltip'
+// eslint-disable-next-line n/no-missing-import, import/no-unresolved
+import BackupRestore from '@mdi/svg/svg/backup-restore.svg?raw'
+
+Vue.prototype.t = t
+Vue.prototype.n = n
+
+Vue.use(VTooltipPlugin)
+
+// Init Sharing tab component
+const View = Vue.extend(VersionTab)
+let TabInstance = null
+
+window.addEventListener('DOMContentLoaded', function() {
+ if (OCA.Files?.Sidebar === undefined) {
+ return
+ }
+
+ OCA.Files.Sidebar.registerTab(new OCA.Files.Sidebar.Tab({
+ id: 'version_vue',
+ name: t('files_versions', 'Versions'),
+ iconSvg: BackupRestore,
+
+ async mount(el, fileInfo, context) {
+ if (TabInstance) {
+ TabInstance.$destroy()
+ }
+ TabInstance = new View({
+ // Better integration with vue parent component
+ parent: context,
+ })
+ // Only mount after we have all the info we need
+ await TabInstance.update(fileInfo)
+ TabInstance.$mount(el)
+ },
+ update(fileInfo) {
+ TabInstance.update(fileInfo)
+ },
+ setIsActive(isActive) {
+ if (!TabInstance) {
+ return
+ }
+ TabInstance.setIsActive(isActive)
+ },
+ destroy() {
+ TabInstance.$destroy()
+ TabInstance = null
+ },
+ enabled(fileInfo) {
+ return !(fileInfo?.isDirectory() ?? true)
+ },
+ }))
+})
diff --git a/apps/files_versions/src/utils/davClient.js b/apps/files_versions/src/utils/davClient.js
new file mode 100644
index 00000000000..029373e9193
--- /dev/null
+++ b/apps/files_versions/src/utils/davClient.js
@@ -0,0 +1,29 @@
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { createClient } from 'webdav'
+import { generateRemoteUrl } from '@nextcloud/router'
+import { getRequestToken, onRequestTokenUpdate } from '@nextcloud/auth'
+
+// init webdav client
+const rootPath = 'dav'
+const remote = generateRemoteUrl(rootPath)
+const client = createClient(remote)
+
+// set CSRF token header
+const setHeaders = (token) => {
+ client.setHeaders({
+ // Add this so the server knows it is an request from the browser
+ 'X-Requested-With': 'XMLHttpRequest',
+ // Inject user auth
+ requesttoken: token ?? '',
+ })
+}
+
+// refresh headers when request token changes
+onRequestTokenUpdate(setHeaders)
+setHeaders(getRequestToken())
+
+export default client
diff --git a/apps/files_versions/src/utils/davRequest.js b/apps/files_versions/src/utils/davRequest.js
new file mode 100644
index 00000000000..1dcf620564a
--- /dev/null
+++ b/apps/files_versions/src/utils/davRequest.js
@@ -0,0 +1,20 @@
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+export default `<?xml version="1.0"?>
+<d:propfind xmlns:d="DAV:"
+ xmlns:oc="http://owncloud.org/ns"
+ xmlns:nc="http://nextcloud.org/ns"
+ xmlns:ocs="http://open-collaboration-services.org/ns">
+ <d:prop>
+ <d:getcontentlength />
+ <d:getcontenttype />
+ <d:getlastmodified />
+ <d:getetag />
+ <nc:version-label />
+ <nc:version-author />
+ <nc:has-preview />
+ </d:prop>
+</d:propfind>`
diff --git a/apps/files_versions/src/utils/logger.js b/apps/files_versions/src/utils/logger.js
new file mode 100644
index 00000000000..f84cb969244
--- /dev/null
+++ b/apps/files_versions/src/utils/logger.js
@@ -0,0 +1,11 @@
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { getLoggerBuilder } from '@nextcloud/logger'
+
+export default getLoggerBuilder()
+ .setApp('files_version')
+ .detectUser()
+ .build()
diff --git a/apps/files_versions/src/utils/versions.ts b/apps/files_versions/src/utils/versions.ts
new file mode 100644
index 00000000000..6d5933f0bd9
--- /dev/null
+++ b/apps/files_versions/src/utils/versions.ts
@@ -0,0 +1,133 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+/* eslint-disable jsdoc/require-param */
+/* eslint-disable jsdoc/require-jsdoc */
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import type { FileStat, ResponseDataDetailed } from 'webdav'
+
+import { generateRemoteUrl, generateUrl } from '@nextcloud/router'
+import { getCurrentUser } from '@nextcloud/auth'
+import { joinPaths, encodePath } from '@nextcloud/paths'
+import moment from '@nextcloud/moment'
+
+import client from '../utils/davClient.js'
+import davRequest from '../utils/davRequest.js'
+import logger from '../utils/logger.js'
+
+export interface Version {
+ fileId: string, // The id of the file associated to the version.
+ label: string, // 'Current version' or ''
+ author: string|null, // UID for the author of the version
+ filename: string, // File name relative to the version DAV endpoint
+ basename: string, // A base name generated from the mtime
+ mime: string, // Empty for the current version, else the actual mime type of the version
+ etag: string, // Empty for the current version, else the actual mime type of the version
+ size: string, // Human readable size
+ type: string, // 'file'
+ mtime: number, // Version creation date as a timestamp
+ permissions: string, // Only readable: 'R'
+ previewUrl: string, // Preview URL of the version
+ url: string, // Download URL of the version
+ source: string, // The WebDAV endpoint of the ressource
+ fileVersion: string|null, // The version id, null for the current version
+}
+
+export async function fetchVersions(fileInfo: any): Promise<Version[]> {
+ const path = `/versions/${getCurrentUser()?.uid}/versions/${fileInfo.id}`
+
+ try {
+ const response = await client.getDirectoryContents(path, {
+ data: davRequest,
+ details: true,
+ }) as ResponseDataDetailed<FileStat[]>
+
+ return response.data
+ // Filter out root
+ .filter(({ mime }) => mime !== '')
+ .map(version => formatVersion(version, fileInfo))
+ } catch (exception) {
+ logger.error('Could not fetch version', { exception })
+ throw exception
+ }
+}
+
+/**
+ * Restore the given version
+ */
+export async function restoreVersion(version: Version) {
+ try {
+ logger.debug('Restoring version', { url: version.url })
+ await client.moveFile(
+ `/versions/${getCurrentUser()?.uid}/versions/${version.fileId}/${version.fileVersion}`,
+ `/versions/${getCurrentUser()?.uid}/restore/target`,
+ )
+ } catch (exception) {
+ logger.error('Could not restore version', { exception })
+ throw exception
+ }
+}
+
+/**
+ * Format version
+ */
+function formatVersion(version: any, fileInfo: any): Version {
+ const mtime = moment(version.lastmod).unix() * 1000
+ let previewUrl = ''
+
+ if (mtime === fileInfo.mtime) { // Version is the current one
+ previewUrl = generateUrl('/core/preview?fileId={fileId}&c={fileEtag}&x=250&y=250&forceIcon=0&a=0&forceIcon=1&mimeFallback=1', {
+ fileId: fileInfo.id,
+ fileEtag: fileInfo.etag,
+ })
+ } else {
+ previewUrl = generateUrl('/apps/files_versions/preview?file={file}&version={fileVersion}&mimeFallback=1', {
+ file: joinPaths(fileInfo.path, fileInfo.name),
+ fileVersion: version.basename,
+ })
+ }
+
+ return {
+ fileId: fileInfo.id,
+ // If version-label is defined make sure it is a string (prevent issue if the label is a number an PHP returns a number then)
+ label: version.props['version-label'] && String(version.props['version-label']),
+ author: version.props['version-author'] ?? null,
+ filename: version.filename,
+ basename: moment(mtime).format('LLL'),
+ mime: version.mime,
+ etag: `${version.props.getetag}`,
+ size: version.size,
+ type: version.type,
+ mtime,
+ permissions: 'R',
+ previewUrl,
+ url: joinPaths('/remote.php/dav', version.filename),
+ source: generateRemoteUrl('dav') + encodePath(version.filename),
+ fileVersion: version.basename,
+ }
+}
+
+export async function setVersionLabel(version: Version, newLabel: string) {
+ return await client.customRequest(
+ version.filename,
+ {
+ method: 'PROPPATCH',
+ data: `<?xml version="1.0"?>
+ <d:propertyupdate xmlns:d="DAV:"
+ xmlns:oc="http://owncloud.org/ns"
+ xmlns:nc="http://nextcloud.org/ns"
+ xmlns:ocs="http://open-collaboration-services.org/ns">
+ <d:set>
+ <d:prop>
+ <nc:version-label>${newLabel}</nc:version-label>
+ </d:prop>
+ </d:set>
+ </d:propertyupdate>`,
+ },
+ )
+}
+
+export async function deleteVersion(version: Version) {
+ await client.deleteFile(version.filename)
+}
diff --git a/apps/files_versions/src/views/VersionTab.vue b/apps/files_versions/src/views/VersionTab.vue
new file mode 100644
index 00000000000..a643aef439d
--- /dev/null
+++ b/apps/files_versions/src/views/VersionTab.vue
@@ -0,0 +1,307 @@
+<!--
+ - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+<template>
+ <div class="versions-tab__container">
+ <VirtualScrolling v-slot="{ visibleSections }"
+ :sections="sections"
+ :header-height="0">
+ <ul :aria-label="t('files_versions', 'File versions')" data-files-versions-versions-list>
+ <template v-if="visibleSections.length === 1">
+ <Version v-for="(row) of visibleSections[0].rows"
+ :key="row.items[0].mtime"
+ :can-view="canView"
+ :can-compare="canCompare"
+ :load-preview="isActive"
+ :version="row.items[0]"
+ :file-info="fileInfo"
+ :is-current="row.items[0].mtime === fileInfo.mtime"
+ :is-first-version="row.items[0].mtime === initialVersionMtime"
+ @click="openVersion"
+ @compare="compareVersion"
+ @restore="handleRestore"
+ @label-update-request="handleLabelUpdateRequest(row.items[0])"
+ @delete="handleDelete" />
+ </template>
+ </ul>
+ <NcLoadingIcon v-if="loading" slot="loader" class="files-list-viewer__loader" />
+ </VirtualScrolling>
+ <VersionLabelDialog v-if="editedVersion"
+ :open.sync="showVersionLabelForm"
+ :version-label="editedVersion.label"
+ @label-update="handleLabelUpdate" />
+ </div>
+</template>
+
+<script>
+import path from 'path'
+
+import { getCurrentUser } from '@nextcloud/auth'
+import { showError, showSuccess } from '@nextcloud/dialogs'
+import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
+import { useIsMobile } from '@nextcloud/vue/composables/useIsMobile'
+import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
+
+import { fetchVersions, deleteVersion, restoreVersion, setVersionLabel } from '../utils/versions.ts'
+import Version from '../components/Version.vue'
+import VirtualScrolling from '../components/VirtualScrolling.vue'
+import VersionLabelDialog from '../components/VersionLabelDialog.vue'
+
+export default {
+ name: 'VersionTab',
+ components: {
+ Version,
+ VirtualScrolling,
+ VersionLabelDialog,
+ NcLoadingIcon,
+ },
+
+ setup() {
+ return {
+ isMobile: useIsMobile(),
+ }
+ },
+
+ data() {
+ return {
+ fileInfo: null,
+ isActive: false,
+ /** @type {import('../utils/versions.ts').Version[]} */
+ versions: [],
+ loading: false,
+ showVersionLabelForm: false,
+ editedVersion: null,
+ }
+ },
+ computed: {
+ sections() {
+ const rows = this.orderedVersions.map(version => ({ key: version.mtime, height: 68, sectionKey: 'versions', items: [version] }))
+ return [{ key: 'versions', rows, height: 68 * this.orderedVersions.length }]
+ },
+
+ /**
+ * Order versions by mtime.
+ * Put the current version at the top.
+ *
+ * @return {import('../utils/versions.ts').Version[]}
+ */
+ orderedVersions() {
+ return [...this.versions].sort((a, b) => {
+ if (a.mtime === this.fileInfo.mtime) {
+ return -1
+ } else if (b.mtime === this.fileInfo.mtime) {
+ return 1
+ } else {
+ return b.mtime - a.mtime
+ }
+ })
+ },
+
+ /**
+ * Return the mtime of the first version to display "Initial version" label
+ *
+ * @return {number}
+ */
+ initialVersionMtime() {
+ return this.versions
+ .map(version => version.mtime)
+ .reduce((a, b) => Math.min(a, b))
+ },
+
+ viewerFileInfo() {
+ // We need to remap bitmask to dav permissions as the file info we have is converted through client.js
+ let davPermissions = ''
+ if (this.fileInfo.permissions & 1) {
+ davPermissions += 'R'
+ }
+ if (this.fileInfo.permissions & 2) {
+ davPermissions += 'W'
+ }
+ if (this.fileInfo.permissions & 8) {
+ davPermissions += 'D'
+ }
+ return {
+ ...this.fileInfo,
+ mime: this.fileInfo.mimetype,
+ basename: this.fileInfo.name,
+ filename: this.fileInfo.path + '/' + this.fileInfo.name,
+ permissions: davPermissions,
+ fileid: this.fileInfo.id,
+ }
+ },
+
+ /** @return {boolean} */
+ canView() {
+ return window.OCA.Viewer?.mimetypesCompare?.includes(this.fileInfo.mimetype)
+ },
+
+ canCompare() {
+ return !this.isMobile
+ },
+ },
+ mounted() {
+ subscribe('files_versions:restore:restored', this.fetchVersions)
+ },
+ beforeUnmount() {
+ unsubscribe('files_versions:restore:restored', this.fetchVersions)
+ },
+ methods: {
+ /**
+ * Update current fileInfo and fetch new data
+ *
+ * @param {object} fileInfo the current file FileInfo
+ */
+ async update(fileInfo) {
+ this.fileInfo = fileInfo
+ this.resetState()
+ this.fetchVersions()
+ },
+
+ /**
+ * @param {boolean} isActive whether the tab is active
+ */
+ async setIsActive(isActive) {
+ this.isActive = isActive
+ },
+
+ /**
+ * Get the existing versions infos
+ */
+ async fetchVersions() {
+ try {
+ this.loading = true
+ this.versions = await fetchVersions(this.fileInfo)
+ } finally {
+ this.loading = false
+ }
+ },
+
+ /**
+ * Handle restored event from Version.vue
+ *
+ * @param {import('../utils/versions.ts').Version} version The version to restore
+ */
+ async handleRestore(version) {
+ // Update local copy of fileInfo as rendering depends on it.
+ const oldFileInfo = this.fileInfo
+ this.fileInfo = {
+ ...this.fileInfo,
+ size: version.size,
+ mtime: version.mtime,
+ }
+
+ const restoreStartedEventState = {
+ preventDefault: false,
+ fileInfo: this.fileInfo,
+ version,
+ }
+ emit('files_versions:restore:requested', restoreStartedEventState)
+ if (restoreStartedEventState.preventDefault) {
+ return
+ }
+
+ try {
+ await restoreVersion(version)
+ if (version.label) {
+ showSuccess(t('files_versions', `${version.label} restored`))
+ } else if (version.mtime === this.initialVersionMtime) {
+ showSuccess(t('files_versions', 'Initial version restored'))
+ } else {
+ showSuccess(t('files_versions', 'Version restored'))
+ }
+ emit('files_versions:restore:restored', version)
+ } catch (exception) {
+ this.fileInfo = oldFileInfo
+ showError(t('files_versions', 'Could not restore version'))
+ emit('files_versions:restore:failed', version)
+ }
+ },
+
+ /**
+ * Handle label-updated event from Version.vue
+ * @param {import('../utils/versions.ts').Version} version The version to update
+ */
+ handleLabelUpdateRequest(version) {
+ this.showVersionLabelForm = true
+ this.editedVersion = version
+ },
+
+ /**
+ * Handle label-updated event from Version.vue
+ * @param {string} newLabel The new label
+ */
+ async handleLabelUpdate(newLabel) {
+ const oldLabel = this.editedVersion.label
+ this.editedVersion.label = newLabel
+ this.showVersionLabelForm = false
+
+ try {
+ await setVersionLabel(this.editedVersion, newLabel)
+ this.editedVersion = null
+ } catch (exception) {
+ this.editedVersion.label = oldLabel
+ showError(this.t('files_versions', 'Could not set version label'))
+ logger.error('Could not set version label', { exception })
+ }
+ },
+
+ /**
+ * Handle deleted event from Version.vue
+ *
+ * @param {import('../utils/versions.ts').Version} version The version to delete
+ */
+ async handleDelete(version) {
+ const index = this.versions.indexOf(version)
+ this.versions.splice(index, 1)
+
+ try {
+ await deleteVersion(version)
+ } catch (exception) {
+ this.versions.push(version)
+ showError(t('files_versions', 'Could not delete version'))
+ }
+ },
+
+ /**
+ * Reset the current view to its default state
+ */
+ resetState() {
+ this.$set(this, 'versions', [])
+ },
+
+ openVersion({ version }) {
+ // Open current file view instead of read only
+ if (version.mtime === this.fileInfo.mtime) {
+ OCA.Viewer.open({ fileInfo: this.viewerFileInfo })
+ return
+ }
+
+ // Versions previews are too small for our use case, so we override previewUrl
+ // which makes the viewer render the original file.
+ // We also point to the original filename if the version is the current one.
+ const versions = this.versions.map(version => ({
+ ...version,
+ filename: version.mtime === this.fileInfo.mtime ? path.join('files', getCurrentUser()?.uid ?? '', this.fileInfo.path, this.fileInfo.name) : version.filename,
+ previewUrl: undefined,
+ }))
+
+ OCA.Viewer.open({
+ fileInfo: versions.find(v => v.source === version.source),
+ enableSidebar: false,
+ })
+ },
+
+ compareVersion({ version }) {
+ const versions = this.versions.map(version => ({ ...version, previewUrl: undefined }))
+
+ OCA.Viewer.compare(this.viewerFileInfo, versions.find(v => v.source === version.source))
+ },
+ },
+}
+</script>
+<style lang="scss">
+.versions-tab__container {
+ height: 100%;
+}
+</style>
diff --git a/apps/files_versions/tests/BackgroundJob/ExpireVersionsTest.php b/apps/files_versions/tests/BackgroundJob/ExpireVersionsTest.php
new file mode 100644
index 00000000000..21e88e86f90
--- /dev/null
+++ b/apps/files_versions/tests/BackgroundJob/ExpireVersionsTest.php
@@ -0,0 +1,55 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files_Versions\Tests\BackgroundJob;
+
+use OCA\Files_Versions\BackgroundJob\ExpireVersions;
+use OCA\Files_Versions\Expiration;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\BackgroundJob\IJobList;
+use OCP\IConfig;
+use OCP\IUserManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class ExpireVersionsTest extends TestCase {
+ private IConfig&MockObject $config;
+ private IUserManager&MockObject $userManager;
+ private Expiration&MockObject $expiration;
+ private IJobList&MockObject $jobList;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->config = $this->createMock(IConfig::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->expiration = $this->createMock(Expiration::class);
+ $this->jobList = $this->createMock(IJobList::class);
+
+ $this->jobList->expects($this->once())
+ ->method('setLastRun');
+ $this->jobList->expects($this->once())
+ ->method('setExecutionTime');
+ }
+
+ public function testBackgroundJobDeactivated(): void {
+ $this->config->method('getAppValue')
+ ->with('files_versions', 'background_job_expire_versions', 'yes')
+ ->willReturn('no');
+ $this->expiration->expects($this->never())
+ ->method('getMaxAgeAsTimestamp');
+
+ $timeFactory = $this->createMock(ITimeFactory::class);
+ $timeFactory->method('getTime')
+ ->with()
+ ->willReturn(999999999);
+
+ $job = new ExpireVersions($this->config, $this->userManager, $this->expiration, $timeFactory);
+ $job->start($this->jobList);
+ }
+}
diff --git a/apps/files_versions/tests/Command/CleanupTest.php b/apps/files_versions/tests/Command/CleanupTest.php
new file mode 100644
index 00000000000..dd6665f5aef
--- /dev/null
+++ b/apps/files_versions/tests/Command/CleanupTest.php
@@ -0,0 +1,162 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_Versions\Tests\Command;
+
+use OC\User\Manager;
+use OCA\Files_Versions\Command\CleanUp;
+use OCA\Files_Versions\Db\VersionsMapper;
+use OCP\Files\Cache\ICache;
+use OCP\Files\Folder;
+use OCP\Files\IRootFolder;
+use OCP\Files\Storage\IStorage;
+use OCP\UserInterface;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+/**
+ * Class CleanupTest
+ *
+ * @group DB
+ *
+ * @package OCA\Files_Versions\Tests\Command
+ */
+class CleanupTest extends TestCase {
+ protected Manager&MockObject $userManager;
+ protected IRootFolder&MockObject $rootFolder;
+ protected VersionsMapper&MockObject $versionMapper;
+ protected CleanUp $cleanup;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->rootFolder = $this->createMock(IRootFolder::class);
+ $this->userManager = $this->createMock(Manager::class);
+ $this->versionMapper = $this->createMock(VersionsMapper::class);
+
+ $this->cleanup = new CleanUp($this->rootFolder, $this->userManager, $this->versionMapper);
+ }
+
+ /**
+ * @param boolean $nodeExists
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataTestDeleteVersions')]
+ public function testDeleteVersions(bool $nodeExists): void {
+ $this->rootFolder->expects($this->once())
+ ->method('nodeExists')
+ ->with('/testUser/files_versions')
+ ->willReturn($nodeExists);
+
+ $userFolder = $this->createMock(Folder::class);
+ $userHomeStorage = $this->createMock(IStorage::class);
+ $userHomeStorageCache = $this->createMock(ICache::class);
+ $this->rootFolder->expects($this->once())
+ ->method('getUserFolder')
+ ->willReturn($userFolder);
+ $userFolder->expects($this->once())
+ ->method('getStorage')
+ ->willReturn($userHomeStorage);
+ $userHomeStorage->expects($this->once())
+ ->method('getCache')
+ ->willReturn($userHomeStorageCache);
+ $userHomeStorageCache->expects($this->once())
+ ->method('getNumericStorageId')
+ ->willReturn(1);
+
+ if ($nodeExists) {
+ $this->rootFolder->expects($this->once())
+ ->method('get')
+ ->with('/testUser/files_versions')
+ ->willReturn($this->rootFolder);
+ $this->rootFolder->expects($this->once())
+ ->method('delete');
+ } else {
+ $this->rootFolder->expects($this->never())
+ ->method('get');
+ $this->rootFolder->expects($this->never())
+ ->method('delete');
+ }
+
+ $this->invokePrivate($this->cleanup, 'deleteVersions', ['testUser']);
+ }
+
+ public static function dataTestDeleteVersions(): array {
+ return [
+ [true],
+ [false]
+ ];
+ }
+
+
+ /**
+ * test delete versions from users given as parameter
+ */
+ public function testExecuteDeleteListOfUsers(): void {
+ $userIds = ['user1', 'user2', 'user3'];
+
+ $instance = $this->getMockBuilder(CleanUp::class)
+ ->onlyMethods(['deleteVersions'])
+ ->setConstructorArgs([$this->rootFolder, $this->userManager, $this->versionMapper])
+ ->getMock();
+ $instance->expects($this->exactly(count($userIds)))
+ ->method('deleteVersions')
+ ->willReturnCallback(function ($user) use ($userIds): void {
+ $this->assertTrue(in_array($user, $userIds));
+ });
+
+ $this->userManager->expects($this->exactly(count($userIds)))
+ ->method('userExists')->willReturn(true);
+
+ $inputInterface = $this->createMock(\Symfony\Component\Console\Input\InputInterface::class);
+ $inputInterface->expects($this->once())->method('getArgument')
+ ->with('user_id')
+ ->willReturn($userIds);
+
+ $outputInterface = $this->createMock(\Symfony\Component\Console\Output\OutputInterface::class);
+
+ $this->invokePrivate($instance, 'execute', [$inputInterface, $outputInterface]);
+ }
+
+ /**
+ * test delete versions of all users
+ */
+ public function testExecuteAllUsers(): void {
+ $userIds = [];
+ $backendUsers = ['user1', 'user2'];
+
+ $instance = $this->getMockBuilder(CleanUp::class)
+ ->onlyMethods(['deleteVersions'])
+ ->setConstructorArgs([$this->rootFolder, $this->userManager, $this->versionMapper])
+ ->getMock();
+
+ $backend = $this->getMockBuilder(UserInterface::class)
+ ->disableOriginalConstructor()->getMock();
+ $backend->expects($this->once())->method('getUsers')
+ ->with('', 500, 0)
+ ->willReturn($backendUsers);
+
+ $instance->expects($this->exactly(count($backendUsers)))
+ ->method('deleteVersions')
+ ->willReturnCallback(function ($user) use ($backendUsers): void {
+ $this->assertTrue(in_array($user, $backendUsers));
+ });
+
+ $inputInterface = $this->createMock(\Symfony\Component\Console\Input\InputInterface::class);
+ $inputInterface->expects($this->once())->method('getArgument')
+ ->with('user_id')
+ ->willReturn($userIds);
+
+ $outputInterface = $this->createMock(\Symfony\Component\Console\Output\OutputInterface::class);
+
+ $this->userManager->expects($this->once())
+ ->method('getBackends')
+ ->willReturn([$backend]);
+
+ $this->invokePrivate($instance, 'execute', [$inputInterface, $outputInterface]);
+ }
+}
diff --git a/apps/files_versions/tests/Command/ExpireTest.php b/apps/files_versions/tests/Command/ExpireTest.php
new file mode 100644
index 00000000000..b74457a7fd6
--- /dev/null
+++ b/apps/files_versions/tests/Command/ExpireTest.php
@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_Versions\Tests\Command;
+
+use OCA\Files_Versions\Command\Expire;
+use Test\TestCase;
+
+/**
+ * Class ExpireTest
+ *
+ * @group DB
+ *
+ * @package OCA\Files_Versions\Tests\Command
+ */
+class ExpireTest extends TestCase {
+ public function testExpireNonExistingUser(): void {
+ $command = new Expire($this->getUniqueID('test'), '');
+ $command->handle();
+
+ $this->addToAssertionCount(1);
+ }
+}
diff --git a/apps/files_versions/tests/Controller/PreviewControllerTest.php b/apps/files_versions/tests/Controller/PreviewControllerTest.php
new file mode 100644
index 00000000000..542ea2b6b34
--- /dev/null
+++ b/apps/files_versions/tests/Controller/PreviewControllerTest.php
@@ -0,0 +1,155 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files_Versions\Tests\Controller;
+
+use OCA\Files_Versions\Controller\PreviewController;
+use OCA\Files_Versions\Versions\IVersionManager;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\DataResponse;
+use OCP\Files\File;
+use OCP\Files\Folder;
+use OCP\Files\IRootFolder;
+use OCP\Files\NotFoundException;
+use OCP\Files\SimpleFS\ISimpleFile;
+use OCP\IPreview;
+use OCP\IRequest;
+use OCP\IUser;
+use OCP\IUserSession;
+use OCP\Preview\IMimeIconProvider;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class PreviewControllerTest extends TestCase {
+ private IRootFolder&MockObject $rootFolder;
+ private string $userId;
+ private IPreview&MockObject $previewManager;
+ private IUserSession&MockObject $userSession;
+ private IVersionManager&MockObject $versionManager;
+
+ private IMimeIconProvider&MockObject $mimeIconProvider;
+ private PreviewController $controller;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->rootFolder = $this->createMock(IRootFolder::class);
+ $this->userId = 'user';
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->any())
+ ->method('getUID')
+ ->willReturn($this->userId);
+ $this->previewManager = $this->createMock(IPreview::class);
+ $this->userSession = $this->createMock(IUserSession::class);
+ $this->userSession->expects($this->any())
+ ->method('getUser')
+ ->willReturn($user);
+ $this->versionManager = $this->createMock(IVersionManager::class);
+ $this->mimeIconProvider = $this->createMock(IMimeIconProvider::class);
+
+ $this->controller = new PreviewController(
+ 'files_versions',
+ $this->createMock(IRequest::class),
+ $this->rootFolder,
+ $this->userSession,
+ $this->versionManager,
+ $this->previewManager,
+ $this->mimeIconProvider,
+ );
+ }
+
+ public function testInvalidFile(): void {
+ $res = $this->controller->getPreview('');
+ $expected = new DataResponse([], Http::STATUS_BAD_REQUEST);
+
+ $this->assertEquals($expected, $res);
+ }
+
+ public function testInvalidWidth(): void {
+ $res = $this->controller->getPreview('file', 0);
+ $expected = new DataResponse([], Http::STATUS_BAD_REQUEST);
+
+ $this->assertEquals($expected, $res);
+ }
+
+ public function testInvalidHeight(): void {
+ $res = $this->controller->getPreview('file', 10, 0);
+ $expected = new DataResponse([], Http::STATUS_BAD_REQUEST);
+
+ $this->assertEquals($expected, $res);
+ }
+
+ public function testInvalidVersion(): void {
+ $res = $this->controller->getPreview('file', 10, 0);
+ $expected = new DataResponse([], Http::STATUS_BAD_REQUEST);
+
+ $this->assertEquals($expected, $res);
+ }
+
+ public function testValidPreview(): void {
+ $userFolder = $this->createMock(Folder::class);
+ $userRoot = $this->createMock(Folder::class);
+
+ $this->rootFolder->method('getUserFolder')
+ ->with($this->userId)
+ ->willReturn($userFolder);
+ $userFolder->method('getParent')
+ ->willReturn($userRoot);
+
+ $sourceFile = $this->createMock(File::class);
+ $userFolder->method('get')
+ ->with('file')
+ ->willReturn($sourceFile);
+
+ $file = $this->createMock(File::class);
+ $file->method('getMimetype')
+ ->willReturn('myMime');
+
+ $this->versionManager->method('getVersionFile')
+ ->willReturn($file);
+
+ $preview = $this->createMock(ISimpleFile::class);
+ $preview->method('getName')->willReturn('name');
+ $preview->method('getMTime')->willReturn(42);
+ $this->previewManager->method('getPreview')
+ ->with($this->equalTo($file), 10, 10, true, IPreview::MODE_FILL, 'myMime')
+ ->willReturn($preview);
+ $preview->method('getMimeType')
+ ->willReturn('previewMime');
+
+ $res = $this->controller->getPreview('file', 10, 10, '42');
+
+ $this->assertEquals('previewMime', $res->getHeaders()['Content-Type']);
+ $this->assertEquals(Http::STATUS_OK, $res->getStatus());
+ $this->assertEquals($preview, $this->invokePrivate($res, 'file'));
+ }
+
+ public function testVersionNotFound(): void {
+ $userFolder = $this->createMock(Folder::class);
+ $userRoot = $this->createMock(Folder::class);
+
+ $this->rootFolder->method('getUserFolder')
+ ->with($this->userId)
+ ->willReturn($userFolder);
+ $userFolder->method('getParent')
+ ->willReturn($userRoot);
+
+ $sourceFile = $this->createMock(File::class);
+ $userFolder->method('get')
+ ->with('file')
+ ->willReturn($sourceFile);
+
+ $this->versionManager->method('getVersionFile')
+ ->willThrowException(new NotFoundException());
+
+ $res = $this->controller->getPreview('file', 10, 10, '42');
+ $expected = new DataResponse([], Http::STATUS_NOT_FOUND);
+
+ $this->assertEquals($expected, $res);
+ }
+}
diff --git a/apps/files_versions/tests/ExpirationTest.php b/apps/files_versions/tests/ExpirationTest.php
new file mode 100644
index 00000000000..8cf412c3fe0
--- /dev/null
+++ b/apps/files_versions/tests/ExpirationTest.php
@@ -0,0 +1,114 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_Versions\Tests;
+
+use OCA\Files_Versions\Expiration;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\IConfig;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+
+class ExpirationTest extends \Test\TestCase {
+ public const SECONDS_PER_DAY = 86400; //60*60*24
+
+ public static function expirationData(): array {
+ $today = 100 * self::SECONDS_PER_DAY;
+ $back10Days = (100 - 10) * self::SECONDS_PER_DAY;
+ $back20Days = (100 - 20) * self::SECONDS_PER_DAY;
+ $back30Days = (100 - 30) * self::SECONDS_PER_DAY;
+ $back35Days = (100 - 35) * self::SECONDS_PER_DAY;
+
+ // it should never happen, but who knows :/
+ $ahead100Days = (100 + 100) * self::SECONDS_PER_DAY;
+
+ return [
+ // Expiration is disabled - always should return false
+ [ 'disabled', $today, $back10Days, false, false],
+ [ 'disabled', $today, $back10Days, true, false],
+ [ 'disabled', $today, $ahead100Days, true, false],
+
+ // Default: expire in 30 days or earlier when quota requirements are met
+ [ 'auto', $today, $back10Days, false, false],
+ [ 'auto', $today, $back35Days, false, false],
+ [ 'auto', $today, $back10Days, true, true],
+ [ 'auto', $today, $back35Days, true, true],
+ [ 'auto', $today, $ahead100Days, true, true],
+
+ // The same with 'auto'
+ [ 'auto, auto', $today, $back10Days, false, false],
+ [ 'auto, auto', $today, $back35Days, false, false],
+ [ 'auto, auto', $today, $back10Days, true, true],
+ [ 'auto, auto', $today, $back35Days, true, true],
+
+ // Keep for 15 days but expire anytime if space needed
+ [ '15, auto', $today, $back10Days, false, false],
+ [ '15, auto', $today, $back20Days, false, false],
+ [ '15, auto', $today, $back10Days, true, true],
+ [ '15, auto', $today, $back20Days, true, true],
+ [ '15, auto', $today, $ahead100Days, true, true],
+
+ // Expire anytime if space needed, Expire all older than max
+ [ 'auto, 15', $today, $back10Days, false, false],
+ [ 'auto, 15', $today, $back20Days, false, true],
+ [ 'auto, 15', $today, $back10Days, true, true],
+ [ 'auto, 15', $today, $back20Days, true, true],
+ [ 'auto, 15', $today, $ahead100Days, true, true],
+
+ // Expire all older than max OR older than min if space needed
+ [ '15, 25', $today, $back10Days, false, false],
+ [ '15, 25', $today, $back20Days, false, false],
+ [ '15, 25', $today, $back30Days, false, true],
+ [ '15, 25', $today, $back10Days, false, false],
+ [ '15, 25', $today, $back20Days, true, true],
+ [ '15, 25', $today, $back30Days, true, true],
+ [ '15, 25', $today, $ahead100Days, true, false],
+
+ // Expire all older than max OR older than min if space needed
+ // Max<Min case
+ [ '25, 15', $today, $back10Days, false, false],
+ [ '25, 15', $today, $back20Days, false, false],
+ [ '25, 15', $today, $back30Days, false, true],
+ [ '25, 15', $today, $back10Days, false, false],
+ [ '25, 15', $today, $back20Days, true, false],
+ [ '25, 15', $today, $back30Days, true, true],
+ [ '25, 15', $today, $ahead100Days, true, false],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('expirationData')]
+ public function testExpiration(string $retentionObligation, int $timeNow, int $timestamp, bool $quotaExceeded, bool $expectedResult): void {
+ $mockedConfig = $this->getMockedConfig($retentionObligation);
+ $mockedTimeFactory = $this->getMockedTimeFactory($timeNow);
+ $mockedLogger = $this->createMock(LoggerInterface::class);
+
+ $expiration = new Expiration($mockedConfig, $mockedTimeFactory, $mockedLogger);
+ $actualResult = $expiration->isExpired($timestamp, $quotaExceeded);
+
+ $this->assertEquals($expectedResult, $actualResult);
+ }
+
+
+ private function getMockedTimeFactory(int $time): ITimeFactory&MockObject {
+ $mockedTimeFactory = $this->createMock(ITimeFactory::class);
+ $mockedTimeFactory->expects($this->any())
+ ->method('getTime')
+ ->willReturn($time);
+
+ return $mockedTimeFactory;
+ }
+
+ private function getMockedConfig(string $returnValue): IConfig&MockObject {
+ $mockedConfig = $this->createMock(IConfig::class);
+ $mockedConfig->expects($this->any())
+ ->method('getSystemValue')
+ ->willReturn($returnValue);
+
+ return $mockedConfig;
+ }
+}
diff --git a/apps/files_versions/tests/StorageTest.php b/apps/files_versions/tests/StorageTest.php
new file mode 100644
index 00000000000..443cff3ee06
--- /dev/null
+++ b/apps/files_versions/tests/StorageTest.php
@@ -0,0 +1,101 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\files_versions\tests;
+
+use OCA\Files_Versions\Expiration;
+use OCA\Files_Versions\Storage;
+use OCP\Files\IRootFolder;
+use OCP\Files\NotFoundException;
+use OCP\Server;
+use Test\TestCase;
+use Test\Traits\UserTrait;
+
+/**
+ * @group DB
+ */
+class StorageTest extends TestCase {
+ use UserTrait;
+
+ private $versionsRoot;
+ private $userFolder;
+ private int $expireTimestamp = 10;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $expiration = $this->createMock(Expiration::class);
+ $expiration->method('getMaxAgeAsTimestamp')
+ ->willReturnCallback(function () {
+ return $this->expireTimestamp;
+ });
+ $this->overwriteService(Expiration::class, $expiration);
+
+ \OC::$server->boot();
+
+ $this->createUser('version_test', '');
+ $this->loginAsUser('version_test');
+ /** @var IRootFolder $root */
+ $root = Server::get(IRootFolder::class);
+ $this->userFolder = $root->getUserFolder('version_test');
+ }
+
+
+ protected function createPastFile(string $path, int $mtime): void {
+ try {
+ $file = $this->userFolder->get($path);
+ $file->putContent((string)$mtime);
+ } catch (NotFoundException $e) {
+ $file = $this->userFolder->newFile($path, (string)$mtime);
+ }
+ $file->touch($mtime);
+ }
+
+ public function testExpireMaxAge(): void {
+ $this->userFolder->newFolder('folder1');
+ $this->userFolder->newFolder('folder1/sub1');
+ $this->userFolder->newFolder('folder2');
+
+ $this->createPastFile('file1', 100);
+ $this->createPastFile('file1', 500);
+ $this->createPastFile('file1', 900);
+
+ $this->createPastFile('folder1/file2', 100);
+ $this->createPastFile('folder1/file2', 200);
+ $this->createPastFile('folder1/file2', 300);
+
+ $this->createPastFile('folder1/sub1/file3', 400);
+ $this->createPastFile('folder1/sub1/file3', 500);
+ $this->createPastFile('folder1/sub1/file3', 600);
+
+ $this->createPastFile('folder2/file4', 100);
+ $this->createPastFile('folder2/file4', 600);
+ $this->createPastFile('folder2/file4', 800);
+
+ $this->assertCount(2, Storage::getVersions('version_test', 'file1'));
+ $this->assertCount(2, Storage::getVersions('version_test', 'folder1/file2'));
+ $this->assertCount(2, Storage::getVersions('version_test', 'folder1/sub1/file3'));
+ $this->assertCount(2, Storage::getVersions('version_test', 'folder2/file4'));
+
+ $this->expireTimestamp = 150;
+ Storage::expireOlderThanMaxForUser('version_test');
+
+ $this->assertCount(1, Storage::getVersions('version_test', 'file1'));
+ $this->assertCount(1, Storage::getVersions('version_test', 'folder1/file2'));
+ $this->assertCount(2, Storage::getVersions('version_test', 'folder1/sub1/file3'));
+ $this->assertCount(1, Storage::getVersions('version_test', 'folder2/file4'));
+
+ $this->expireTimestamp = 550;
+ Storage::expireOlderThanMaxForUser('version_test');
+
+ $this->assertCount(0, Storage::getVersions('version_test', 'file1'));
+ $this->assertCount(0, Storage::getVersions('version_test', 'folder1/file2'));
+ $this->assertCount(0, Storage::getVersions('version_test', 'folder1/sub1/file3'));
+ $this->assertCount(1, Storage::getVersions('version_test', 'folder2/file4'));
+ }
+}
diff --git a/apps/files_versions/tests/VersioningTest.php b/apps/files_versions/tests/VersioningTest.php
new file mode 100644
index 00000000000..c9f7d27d7ab
--- /dev/null
+++ b/apps/files_versions/tests/VersioningTest.php
@@ -0,0 +1,998 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_Versions\Tests;
+
+use OC\AllConfig;
+use OC\Files\Cache\Watcher;
+use OC\Files\Filesystem;
+use OC\Files\Storage\Temporary;
+use OC\Files\View;
+use OC\SystemConfig;
+use OC\User\NoUserException;
+use OCA\Files_Sharing\AppInfo\Application;
+use OCA\Files_Versions\Db\VersionEntity;
+use OCA\Files_Versions\Db\VersionsMapper;
+use OCA\Files_Versions\Events\VersionRestoredEvent;
+use OCA\Files_Versions\Storage;
+use OCA\Files_Versions\Versions\IVersionManager;
+use OCP\Constants;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\Files\IMimeTypeLoader;
+use OCP\IConfig;
+use OCP\IUser;
+use OCP\IUserManager;
+use OCP\Server;
+use OCP\Share\IShare;
+use OCP\Util;
+
+/**
+ * Class Test_Files_versions
+ * this class provide basic files versions test
+ *
+ * @group DB
+ */
+class VersioningTest extends \Test\TestCase {
+ public const TEST_VERSIONS_USER = 'test-versions-user';
+ public const TEST_VERSIONS_USER2 = 'test-versions-user2';
+ public const USERS_VERSIONS_ROOT = '/test-versions-user/files_versions';
+
+ /**
+ * @var View
+ */
+ private $rootView;
+ /**
+ * @var VersionsMapper
+ */
+ private $versionsMapper;
+ /**
+ * @var IMimeTypeLoader
+ */
+ private $mimeTypeLoader;
+ private $user1;
+ private $user2;
+
+ public static function setUpBeforeClass(): void {
+ parent::setUpBeforeClass();
+
+ $application = new Application();
+
+ // create test user
+ self::loginHelper(self::TEST_VERSIONS_USER2, true);
+ self::loginHelper(self::TEST_VERSIONS_USER, true);
+ }
+
+ public static function tearDownAfterClass(): void {
+ // cleanup test user
+ $user = Server::get(IUserManager::class)->get(self::TEST_VERSIONS_USER);
+ if ($user !== null) {
+ $user->delete();
+ }
+ $user = Server::get(IUserManager::class)->get(self::TEST_VERSIONS_USER2);
+ if ($user !== null) {
+ $user->delete();
+ }
+
+ parent::tearDownAfterClass();
+ }
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $config = Server::get(IConfig::class);
+ $mockConfig = $this->getMockBuilder(AllConfig::class)
+ ->onlyMethods(['getSystemValue'])
+ ->setConstructorArgs([Server::get(SystemConfig::class)])
+ ->getMock();
+ $mockConfig->expects($this->any())
+ ->method('getSystemValue')
+ ->willReturnCallback(function ($key, $default) use ($config) {
+ if ($key === 'filesystem_check_changes') {
+ return Watcher::CHECK_ONCE;
+ } else {
+ return $config->getSystemValue($key, $default);
+ }
+ });
+ $this->overwriteService(AllConfig::class, $mockConfig);
+
+ // clear hooks
+ \OC_Hook::clear();
+ \OC::registerShareHooks(Server::get(SystemConfig::class));
+ \OC::$server->boot();
+
+ self::loginHelper(self::TEST_VERSIONS_USER);
+ $this->rootView = new View();
+ if (!$this->rootView->file_exists(self::USERS_VERSIONS_ROOT)) {
+ $this->rootView->mkdir(self::USERS_VERSIONS_ROOT);
+ }
+
+ $this->versionsMapper = Server::get(VersionsMapper::class);
+ $this->mimeTypeLoader = Server::get(IMimeTypeLoader::class);
+
+ $this->user1 = $this->createMock(IUser::class);
+ $this->user1->method('getUID')
+ ->willReturn(self::TEST_VERSIONS_USER);
+ $this->user2 = $this->createMock(IUser::class);
+ $this->user2->method('getUID')
+ ->willReturn(self::TEST_VERSIONS_USER2);
+ }
+
+ protected function tearDown(): void {
+ $this->restoreService(AllConfig::class);
+
+ if ($this->rootView) {
+ $this->rootView->deleteAll(self::TEST_VERSIONS_USER . '/files/');
+ $this->rootView->deleteAll(self::TEST_VERSIONS_USER2 . '/files/');
+ $this->rootView->deleteAll(self::TEST_VERSIONS_USER . '/files_versions/');
+ $this->rootView->deleteAll(self::TEST_VERSIONS_USER2 . '/files_versions/');
+ }
+
+ \OC_Hook::clear();
+
+ parent::tearDown();
+ }
+
+ /**
+ * @medium
+ * test expire logic
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('versionsProvider')]
+ public function testGetExpireList($versions, $sizeOfAllDeletedFiles): void {
+
+ // last interval end at 2592000
+ $startTime = 5000000;
+
+ $testClass = new VersionStorageToTest();
+ [$deleted, $size] = $testClass->callProtectedGetExpireList($startTime, $versions);
+
+ // we should have deleted 16 files each of the size 1
+ $this->assertEquals($sizeOfAllDeletedFiles, $size);
+
+ // the deleted array should only contain versions which should be deleted
+ foreach ($deleted as $key => $path) {
+ unset($versions[$key]);
+ $this->assertEquals('delete', substr($path, 0, strlen('delete')));
+ }
+
+ // the versions array should only contain versions which should be kept
+ foreach ($versions as $version) {
+ $this->assertEquals('keep', $version['path']);
+ }
+ }
+
+ public static function versionsProvider(): array {
+ return [
+ // first set of versions uniformly distributed versions
+ [
+ [
+ // first slice (10sec) keep one version every 2 seconds
+ ['version' => 4999999, 'path' => 'keep', 'size' => 1],
+ ['version' => 4999998, 'path' => 'delete', 'size' => 1],
+ ['version' => 4999997, 'path' => 'keep', 'size' => 1],
+ ['version' => 4999995, 'path' => 'keep', 'size' => 1],
+ ['version' => 4999994, 'path' => 'delete', 'size' => 1],
+ //next slice (60sec) starts at 4999990 keep one version every 10 secons
+ ['version' => 4999988, 'path' => 'keep', 'size' => 1],
+ ['version' => 4999978, 'path' => 'keep', 'size' => 1],
+ ['version' => 4999975, 'path' => 'delete', 'size' => 1],
+ ['version' => 4999972, 'path' => 'delete', 'size' => 1],
+ ['version' => 4999967, 'path' => 'keep', 'size' => 1],
+ ['version' => 4999958, 'path' => 'delete', 'size' => 1],
+ ['version' => 4999957, 'path' => 'keep', 'size' => 1],
+ //next slice (3600sec) start at 4999940 keep one version every 60 seconds
+ ['version' => 4999900, 'path' => 'keep', 'size' => 1],
+ ['version' => 4999841, 'path' => 'delete', 'size' => 1],
+ ['version' => 4999840, 'path' => 'keep', 'size' => 1],
+ ['version' => 4999780, 'path' => 'keep', 'size' => 1],
+ ['version' => 4996401, 'path' => 'keep', 'size' => 1],
+ // next slice (86400sec) start at 4996400 keep one version every 3600 seconds
+ ['version' => 4996350, 'path' => 'delete', 'size' => 1],
+ ['version' => 4992800, 'path' => 'keep', 'size' => 1],
+ ['version' => 4989800, 'path' => 'delete', 'size' => 1],
+ ['version' => 4989700, 'path' => 'delete', 'size' => 1],
+ ['version' => 4989200, 'path' => 'keep', 'size' => 1],
+ // next slice (2592000sec) start at 4913600 keep one version every 86400 seconds
+ ['version' => 4913600, 'path' => 'keep', 'size' => 1],
+ ['version' => 4852800, 'path' => 'delete', 'size' => 1],
+ ['version' => 4827201, 'path' => 'delete', 'size' => 1],
+ ['version' => 4827200, 'path' => 'keep', 'size' => 1],
+ ['version' => 4777201, 'path' => 'delete', 'size' => 1],
+ ['version' => 4777501, 'path' => 'delete', 'size' => 1],
+ ['version' => 4740000, 'path' => 'keep', 'size' => 1],
+ // final slice starts at 2408000 keep one version every 604800 secons
+ ['version' => 2408000, 'path' => 'keep', 'size' => 1],
+ ['version' => 1803201, 'path' => 'delete', 'size' => 1],
+ ['version' => 1803200, 'path' => 'keep', 'size' => 1],
+ ['version' => 1800199, 'path' => 'delete', 'size' => 1],
+ ['version' => 1800100, 'path' => 'delete', 'size' => 1],
+ ['version' => 1198300, 'path' => 'keep', 'size' => 1],
+ ],
+ 16 // size of all deleted files (every file has the size 1)
+ ],
+ // second set of versions, here we have only really old versions
+ [
+ [
+ // first slice (10sec) keep one version every 2 seconds
+ // next slice (60sec) starts at 4999990 keep one version every 10 secons
+ // next slice (3600sec) start at 4999940 keep one version every 60 seconds
+ // next slice (86400sec) start at 4996400 keep one version every 3600 seconds
+ ['version' => 4996400, 'path' => 'keep', 'size' => 1],
+ ['version' => 4996350, 'path' => 'delete', 'size' => 1],
+ ['version' => 4996350, 'path' => 'delete', 'size' => 1],
+ ['version' => 4992800, 'path' => 'keep', 'size' => 1],
+ ['version' => 4989800, 'path' => 'delete', 'size' => 1],
+ ['version' => 4989700, 'path' => 'delete', 'size' => 1],
+ ['version' => 4989200, 'path' => 'keep', 'size' => 1],
+ // next slice (2592000sec) start at 4913600 keep one version every 86400 seconds
+ ['version' => 4913600, 'path' => 'keep', 'size' => 1],
+ ['version' => 4852800, 'path' => 'delete', 'size' => 1],
+ ['version' => 4827201, 'path' => 'delete', 'size' => 1],
+ ['version' => 4827200, 'path' => 'keep', 'size' => 1],
+ ['version' => 4777201, 'path' => 'delete', 'size' => 1],
+ ['version' => 4777501, 'path' => 'delete', 'size' => 1],
+ ['version' => 4740000, 'path' => 'keep', 'size' => 1],
+ // final slice starts at 2408000 keep one version every 604800 secons
+ ['version' => 2408000, 'path' => 'keep', 'size' => 1],
+ ['version' => 1803201, 'path' => 'delete', 'size' => 1],
+ ['version' => 1803200, 'path' => 'keep', 'size' => 1],
+ ['version' => 1800199, 'path' => 'delete', 'size' => 1],
+ ['version' => 1800100, 'path' => 'delete', 'size' => 1],
+ ['version' => 1198300, 'path' => 'keep', 'size' => 1],
+ ],
+ 11 // size of all deleted files (every file has the size 1)
+ ],
+ // third set of versions, with some gaps between
+ [
+ [
+ // first slice (10sec) keep one version every 2 seconds
+ ['version' => 4999999, 'path' => 'keep', 'size' => 1],
+ ['version' => 4999998, 'path' => 'delete', 'size' => 1],
+ ['version' => 4999997, 'path' => 'keep', 'size' => 1],
+ ['version' => 4999995, 'path' => 'keep', 'size' => 1],
+ ['version' => 4999994, 'path' => 'delete', 'size' => 1],
+ //next slice (60sec) starts at 4999990 keep one version every 10 secons
+ ['version' => 4999988, 'path' => 'keep', 'size' => 1],
+ ['version' => 4999978, 'path' => 'keep', 'size' => 1],
+ //next slice (3600sec) start at 4999940 keep one version every 60 seconds
+ // next slice (86400sec) start at 4996400 keep one version every 3600 seconds
+ ['version' => 4989200, 'path' => 'keep', 'size' => 1],
+ // next slice (2592000sec) start at 4913600 keep one version every 86400 seconds
+ ['version' => 4913600, 'path' => 'keep', 'size' => 1],
+ ['version' => 4852800, 'path' => 'delete', 'size' => 1],
+ ['version' => 4827201, 'path' => 'delete', 'size' => 1],
+ ['version' => 4827200, 'path' => 'keep', 'size' => 1],
+ ['version' => 4777201, 'path' => 'delete', 'size' => 1],
+ ['version' => 4777501, 'path' => 'delete', 'size' => 1],
+ ['version' => 4740000, 'path' => 'keep', 'size' => 1],
+ // final slice starts at 2408000 keep one version every 604800 secons
+ ['version' => 2408000, 'path' => 'keep', 'size' => 1],
+ ['version' => 1803201, 'path' => 'delete', 'size' => 1],
+ ['version' => 1803200, 'path' => 'keep', 'size' => 1],
+ ['version' => 1800199, 'path' => 'delete', 'size' => 1],
+ ['version' => 1800100, 'path' => 'delete', 'size' => 1],
+ ['version' => 1198300, 'path' => 'keep', 'size' => 1],
+ ],
+ 9 // size of all deleted files (every file has the size 1)
+ ],
+ // fourth set of versions: empty (see issue #19066)
+ [
+ [],
+ 0
+ ]
+
+ ];
+ }
+
+ public function testRename(): void {
+ Filesystem::file_put_contents('test.txt', 'test file');
+
+ $t1 = time();
+ // second version is two weeks older, this way we make sure that no
+ // version will be expired
+ $t2 = $t1 - 60 * 60 * 24 * 14;
+
+ // create some versions
+ $v1 = self::USERS_VERSIONS_ROOT . '/test.txt.v' . $t1;
+ $v2 = self::USERS_VERSIONS_ROOT . '/test.txt.v' . $t2;
+ $v1Renamed = self::USERS_VERSIONS_ROOT . '/test2.txt.v' . $t1;
+ $v2Renamed = self::USERS_VERSIONS_ROOT . '/test2.txt.v' . $t2;
+
+ $this->rootView->file_put_contents($v1, 'version1');
+ $this->rootView->file_put_contents($v2, 'version2');
+
+ // execute rename hook of versions app
+ Filesystem::rename('test.txt', 'test2.txt');
+
+ $this->runCommands();
+
+ $this->assertFalse($this->rootView->file_exists($v1), 'version 1 of old file does not exist');
+ $this->assertFalse($this->rootView->file_exists($v2), 'version 2 of old file does not exist');
+
+ $this->assertTrue($this->rootView->file_exists($v1Renamed), 'version 1 of renamed file exists');
+ $this->assertTrue($this->rootView->file_exists($v2Renamed), 'version 2 of renamed file exists');
+ }
+
+ public function testRenameInSharedFolder(): void {
+ Filesystem::mkdir('folder1');
+ Filesystem::mkdir('folder1/folder2');
+ Filesystem::file_put_contents('folder1/test.txt', 'test file');
+
+ $t1 = time();
+ // second version is two weeks older, this way we make sure that no
+ // version will be expired
+ $t2 = $t1 - 60 * 60 * 24 * 14;
+
+ $this->rootView->mkdir(self::USERS_VERSIONS_ROOT . '/folder1');
+ // create some versions
+ $v1 = self::USERS_VERSIONS_ROOT . '/folder1/test.txt.v' . $t1;
+ $v2 = self::USERS_VERSIONS_ROOT . '/folder1/test.txt.v' . $t2;
+ $v1Renamed = self::USERS_VERSIONS_ROOT . '/folder1/folder2/test.txt.v' . $t1;
+ $v2Renamed = self::USERS_VERSIONS_ROOT . '/folder1/folder2/test.txt.v' . $t2;
+
+ $this->rootView->file_put_contents($v1, 'version1');
+ $this->rootView->file_put_contents($v2, 'version2');
+
+ $node = \OC::$server->getUserFolder(self::TEST_VERSIONS_USER)->get('folder1');
+ $share = Server::get(\OCP\Share\IManager::class)->newShare();
+ $share->setNode($node)
+ ->setShareType(IShare::TYPE_USER)
+ ->setSharedBy(self::TEST_VERSIONS_USER)
+ ->setSharedWith(self::TEST_VERSIONS_USER2)
+ ->setPermissions(Constants::PERMISSION_ALL);
+ $share = Server::get(\OCP\Share\IManager::class)->createShare($share);
+ Server::get(\OCP\Share\IManager::class)->acceptShare($share, self::TEST_VERSIONS_USER2);
+
+ self::loginHelper(self::TEST_VERSIONS_USER2);
+
+ $this->assertTrue(Filesystem::file_exists('folder1/test.txt'));
+
+ // execute rename hook of versions app
+ Filesystem::rename('/folder1/test.txt', '/folder1/folder2/test.txt');
+
+ $this->runCommands();
+
+ self::loginHelper(self::TEST_VERSIONS_USER);
+
+ $this->assertFalse($this->rootView->file_exists($v1), 'version 1 of old file does not exist');
+ $this->assertFalse($this->rootView->file_exists($v2), 'version 2 of old file does not exist');
+
+ $this->assertTrue($this->rootView->file_exists($v1Renamed), 'version 1 of renamed file exists');
+ $this->assertTrue($this->rootView->file_exists($v2Renamed), 'version 2 of renamed file exists');
+
+ Server::get(\OCP\Share\IManager::class)->deleteShare($share);
+ }
+
+ public function testMoveFolder(): void {
+ Filesystem::mkdir('folder1');
+ Filesystem::mkdir('folder2');
+ Filesystem::file_put_contents('folder1/test.txt', 'test file');
+
+ $t1 = time();
+ // second version is two weeks older, this way we make sure that no
+ // version will be expired
+ $t2 = $t1 - 60 * 60 * 24 * 14;
+
+ // create some versions
+ $this->rootView->mkdir(self::USERS_VERSIONS_ROOT . '/folder1');
+ $v1 = self::USERS_VERSIONS_ROOT . '/folder1/test.txt.v' . $t1;
+ $v2 = self::USERS_VERSIONS_ROOT . '/folder1/test.txt.v' . $t2;
+ $v1Renamed = self::USERS_VERSIONS_ROOT . '/folder2/folder1/test.txt.v' . $t1;
+ $v2Renamed = self::USERS_VERSIONS_ROOT . '/folder2/folder1/test.txt.v' . $t2;
+
+ $this->rootView->file_put_contents($v1, 'version1');
+ $this->rootView->file_put_contents($v2, 'version2');
+
+ // execute rename hook of versions app
+ Filesystem::rename('folder1', 'folder2/folder1');
+
+ $this->runCommands();
+
+ $this->assertFalse($this->rootView->file_exists($v1));
+ $this->assertFalse($this->rootView->file_exists($v2));
+
+ $this->assertTrue($this->rootView->file_exists($v1Renamed));
+ $this->assertTrue($this->rootView->file_exists($v2Renamed));
+ }
+
+
+ public function testMoveFileIntoSharedFolderAsRecipient(): void {
+ Filesystem::mkdir('folder1');
+ $fileInfo = Filesystem::getFileInfo('folder1');
+
+ $node = \OC::$server->getUserFolder(self::TEST_VERSIONS_USER)->get('folder1');
+ $share = Server::get(\OCP\Share\IManager::class)->newShare();
+ $share->setNode($node)
+ ->setShareType(IShare::TYPE_USER)
+ ->setSharedBy(self::TEST_VERSIONS_USER)
+ ->setSharedWith(self::TEST_VERSIONS_USER2)
+ ->setPermissions(Constants::PERMISSION_ALL);
+ $share = Server::get(\OCP\Share\IManager::class)->createShare($share);
+ Server::get(\OCP\Share\IManager::class)->acceptShare($share, self::TEST_VERSIONS_USER2);
+
+ self::loginHelper(self::TEST_VERSIONS_USER2);
+ $versionsFolder2 = '/' . self::TEST_VERSIONS_USER2 . '/files_versions';
+ Filesystem::file_put_contents('test.txt', 'test file');
+
+ $t1 = time();
+ // second version is two weeks older, this way we make sure that no
+ // version will be expired
+ $t2 = $t1 - 60 * 60 * 24 * 14;
+
+ $this->rootView->mkdir($versionsFolder2);
+ // create some versions
+ $v1 = $versionsFolder2 . '/test.txt.v' . $t1;
+ $v2 = $versionsFolder2 . '/test.txt.v' . $t2;
+
+ $this->rootView->file_put_contents($v1, 'version1');
+ $this->rootView->file_put_contents($v2, 'version2');
+
+ // move file into the shared folder as recipient
+ $success = Filesystem::rename('/test.txt', '/folder1/test.txt');
+
+ $this->assertTrue($success);
+ $this->assertFalse($this->rootView->file_exists($v1));
+ $this->assertFalse($this->rootView->file_exists($v2));
+
+ self::loginHelper(self::TEST_VERSIONS_USER);
+
+ $versionsFolder1 = '/' . self::TEST_VERSIONS_USER . '/files_versions';
+
+ $v1Renamed = $versionsFolder1 . '/folder1/test.txt.v' . $t1;
+ $v2Renamed = $versionsFolder1 . '/folder1/test.txt.v' . $t2;
+
+ $this->assertTrue($this->rootView->file_exists($v1Renamed));
+ $this->assertTrue($this->rootView->file_exists($v2Renamed));
+
+ Server::get(\OCP\Share\IManager::class)->deleteShare($share);
+ }
+
+ public function testMoveFolderIntoSharedFolderAsRecipient(): void {
+ Filesystem::mkdir('folder1');
+
+ $node = \OC::$server->getUserFolder(self::TEST_VERSIONS_USER)->get('folder1');
+ $share = Server::get(\OCP\Share\IManager::class)->newShare();
+ $share->setNode($node)
+ ->setShareType(IShare::TYPE_USER)
+ ->setSharedBy(self::TEST_VERSIONS_USER)
+ ->setSharedWith(self::TEST_VERSIONS_USER2)
+ ->setPermissions(Constants::PERMISSION_ALL);
+ $share = Server::get(\OCP\Share\IManager::class)->createShare($share);
+ Server::get(\OCP\Share\IManager::class)->acceptShare($share, self::TEST_VERSIONS_USER2);
+
+ self::loginHelper(self::TEST_VERSIONS_USER2);
+ $versionsFolder2 = '/' . self::TEST_VERSIONS_USER2 . '/files_versions';
+ Filesystem::mkdir('folder2');
+ Filesystem::file_put_contents('folder2/test.txt', 'test file');
+
+ $t1 = time();
+ // second version is two weeks older, this way we make sure that no
+ // version will be expired
+ $t2 = $t1 - 60 * 60 * 24 * 14;
+
+ $this->rootView->mkdir($versionsFolder2);
+ $this->rootView->mkdir($versionsFolder2 . '/folder2');
+ // create some versions
+ $v1 = $versionsFolder2 . '/folder2/test.txt.v' . $t1;
+ $v2 = $versionsFolder2 . '/folder2/test.txt.v' . $t2;
+
+ $this->rootView->file_put_contents($v1, 'version1');
+ $this->rootView->file_put_contents($v2, 'version2');
+
+ // move file into the shared folder as recipient
+ Filesystem::rename('/folder2', '/folder1/folder2');
+
+ $this->assertFalse($this->rootView->file_exists($v1));
+ $this->assertFalse($this->rootView->file_exists($v2));
+
+ self::loginHelper(self::TEST_VERSIONS_USER);
+
+ $versionsFolder1 = '/' . self::TEST_VERSIONS_USER . '/files_versions';
+
+ $v1Renamed = $versionsFolder1 . '/folder1/folder2/test.txt.v' . $t1;
+ $v2Renamed = $versionsFolder1 . '/folder1/folder2/test.txt.v' . $t2;
+
+ $this->assertTrue($this->rootView->file_exists($v1Renamed));
+ $this->assertTrue($this->rootView->file_exists($v2Renamed));
+
+ Server::get(\OCP\Share\IManager::class)->deleteShare($share);
+ }
+
+ public function testRenameSharedFile(): void {
+ Filesystem::file_put_contents('test.txt', 'test file');
+
+ $t1 = time();
+ // second version is two weeks older, this way we make sure that no
+ // version will be expired
+ $t2 = $t1 - 60 * 60 * 24 * 14;
+
+ $this->rootView->mkdir(self::USERS_VERSIONS_ROOT);
+ // create some versions
+ $v1 = self::USERS_VERSIONS_ROOT . '/test.txt.v' . $t1;
+ $v2 = self::USERS_VERSIONS_ROOT . '/test.txt.v' . $t2;
+ // the renamed versions should not exist! Because we only moved the mount point!
+ $v1Renamed = self::USERS_VERSIONS_ROOT . '/test2.txt.v' . $t1;
+ $v2Renamed = self::USERS_VERSIONS_ROOT . '/test2.txt.v' . $t2;
+
+ $this->rootView->file_put_contents($v1, 'version1');
+ $this->rootView->file_put_contents($v2, 'version2');
+
+ $node = \OC::$server->getUserFolder(self::TEST_VERSIONS_USER)->get('test.txt');
+ $share = Server::get(\OCP\Share\IManager::class)->newShare();
+ $share->setNode($node)
+ ->setShareType(IShare::TYPE_USER)
+ ->setSharedBy(self::TEST_VERSIONS_USER)
+ ->setSharedWith(self::TEST_VERSIONS_USER2)
+ ->setPermissions(Constants::PERMISSION_READ | Constants::PERMISSION_UPDATE | Constants::PERMISSION_SHARE);
+ $share = Server::get(\OCP\Share\IManager::class)->createShare($share);
+ Server::get(\OCP\Share\IManager::class)->acceptShare($share, self::TEST_VERSIONS_USER2);
+
+ self::loginHelper(self::TEST_VERSIONS_USER2);
+
+ $this->assertTrue(Filesystem::file_exists('test.txt'));
+
+ // execute rename hook of versions app
+ Filesystem::rename('test.txt', 'test2.txt');
+
+ self::loginHelper(self::TEST_VERSIONS_USER);
+
+ $this->runCommands();
+
+ $this->assertTrue($this->rootView->file_exists($v1));
+ $this->assertTrue($this->rootView->file_exists($v2));
+
+ $this->assertFalse($this->rootView->file_exists($v1Renamed));
+ $this->assertFalse($this->rootView->file_exists($v2Renamed));
+
+ Server::get(\OCP\Share\IManager::class)->deleteShare($share);
+ }
+
+ public function testCopy(): void {
+ Filesystem::file_put_contents('test.txt', 'test file');
+
+ $t1 = time();
+ // second version is two weeks older, this way we make sure that no
+ // version will be expired
+ $t2 = $t1 - 60 * 60 * 24 * 14;
+
+ // create some versions
+ $v1 = self::USERS_VERSIONS_ROOT . '/test.txt.v' . $t1;
+ $v2 = self::USERS_VERSIONS_ROOT . '/test.txt.v' . $t2;
+ $v1Copied = self::USERS_VERSIONS_ROOT . '/test2.txt.v' . $t1;
+ $v2Copied = self::USERS_VERSIONS_ROOT . '/test2.txt.v' . $t2;
+
+ $this->rootView->file_put_contents($v1, 'version1');
+ $this->rootView->file_put_contents($v2, 'version2');
+
+ // execute copy hook of versions app
+ Filesystem::copy('test.txt', 'test2.txt');
+
+ $this->runCommands();
+
+ $this->assertTrue($this->rootView->file_exists($v1), 'version 1 of original file exists');
+ $this->assertTrue($this->rootView->file_exists($v2), 'version 2 of original file exists');
+
+ $this->assertTrue($this->rootView->file_exists($v1Copied), 'version 1 of copied file exists');
+ $this->assertTrue($this->rootView->file_exists($v2Copied), 'version 2 of copied file exists');
+ }
+
+ /**
+ * test if we find all versions and if the versions array contain
+ * the correct 'path' and 'name'
+ */
+ public function testGetVersions(): void {
+ $t1 = time();
+ // second version is two weeks older, this way we make sure that no
+ // version will be expired
+ $t2 = $t1 - 60 * 60 * 24 * 14;
+
+ // create some versions
+ $v1 = self::USERS_VERSIONS_ROOT . '/subfolder/test.txt.v' . $t1;
+ $v2 = self::USERS_VERSIONS_ROOT . '/subfolder/test.txt.v' . $t2;
+
+ $this->rootView->mkdir(self::USERS_VERSIONS_ROOT . '/subfolder/');
+
+ $this->rootView->file_put_contents($v1, 'version1');
+ $this->rootView->file_put_contents($v2, 'version2');
+
+ // execute copy hook of versions app
+ $versions = Storage::getVersions(self::TEST_VERSIONS_USER, '/subfolder/test.txt');
+
+ $this->assertCount(2, $versions);
+
+ foreach ($versions as $version) {
+ $this->assertSame('/subfolder/test.txt', $version['path']);
+ $this->assertSame('test.txt', $version['name']);
+ }
+
+ //cleanup
+ $this->rootView->deleteAll(self::USERS_VERSIONS_ROOT . '/subfolder');
+ }
+
+ /**
+ * test if we find all versions and if the versions array contain
+ * the correct 'path' and 'name'
+ */
+ public function testGetVersionsEmptyFile(): void {
+ // execute copy hook of versions app
+ $versions = Storage::getVersions(self::TEST_VERSIONS_USER, '');
+ $this->assertCount(0, $versions);
+
+ $versions = Storage::getVersions(self::TEST_VERSIONS_USER, null);
+ $this->assertCount(0, $versions);
+ }
+
+ public function testExpireNonexistingFile(): void {
+ $this->logout();
+ // needed to have a FS setup (the background job does this)
+ \OC_Util::setupFS(self::TEST_VERSIONS_USER);
+
+ $this->assertFalse(Storage::expire('/void/unexist.txt', self::TEST_VERSIONS_USER));
+ }
+
+
+ public function testExpireNonexistingUser(): void {
+ $this->expectException(NoUserException::class);
+
+ $this->logout();
+ // needed to have a FS setup (the background job does this)
+ \OC_Util::setupFS(self::TEST_VERSIONS_USER);
+ Filesystem::file_put_contents('test.txt', 'test file');
+
+ $this->assertFalse(Storage::expire('test.txt', 'unexist'));
+ }
+
+ public function testRestoreSameStorage(): void {
+ Filesystem::mkdir('sub');
+ $this->doTestRestore();
+ }
+
+ public function testRestoreCrossStorage(): void {
+ $storage2 = new Temporary([]);
+ Filesystem::mount($storage2, [], self::TEST_VERSIONS_USER . '/files/sub');
+
+ $this->doTestRestore();
+ }
+
+ public function testRestoreNoPermission(): void {
+ $this->loginAsUser(self::TEST_VERSIONS_USER);
+
+ $userHome = \OC::$server->getUserFolder(self::TEST_VERSIONS_USER);
+ $node = $userHome->newFolder('folder');
+ $file = $node->newFile('test.txt');
+
+ $share = Server::get(\OCP\Share\IManager::class)->newShare();
+ $share->setNode($node)
+ ->setShareType(IShare::TYPE_USER)
+ ->setSharedBy(self::TEST_VERSIONS_USER)
+ ->setSharedWith(self::TEST_VERSIONS_USER2)
+ ->setPermissions(Constants::PERMISSION_READ);
+ $share = Server::get(\OCP\Share\IManager::class)->createShare($share);
+ Server::get(\OCP\Share\IManager::class)->acceptShare($share, self::TEST_VERSIONS_USER2);
+
+ $versions = $this->createAndCheckVersions(
+ Filesystem::getView(),
+ 'folder/test.txt'
+ );
+
+ $file->putContent('test file');
+
+ $this->loginAsUser(self::TEST_VERSIONS_USER2);
+
+ $firstVersion = current($versions);
+
+ $this->assertFalse(Storage::rollback('folder/test.txt', (int)$firstVersion['version'], $this->user2), 'Revert did not happen');
+
+ $this->loginAsUser(self::TEST_VERSIONS_USER);
+
+ Server::get(\OCP\Share\IManager::class)->deleteShare($share);
+ $this->assertEquals('test file', $file->getContent(), 'File content has not changed');
+ }
+
+ public function testRestoreMovedShare(): void {
+ $this->markTestSkipped('Unreliable test');
+ $this->loginAsUser(self::TEST_VERSIONS_USER);
+
+ $userHome = \OC::$server->getUserFolder(self::TEST_VERSIONS_USER);
+ $node = $userHome->newFolder('folder');
+ $file = $node->newFile('test.txt');
+
+ $userHome2 = \OC::$server->getUserFolder(self::TEST_VERSIONS_USER2);
+ $userHome2->newFolder('subfolder');
+
+ $share = Server::get(\OCP\Share\IManager::class)->newShare();
+ $share->setNode($node)
+ ->setShareType(IShare::TYPE_USER)
+ ->setSharedBy(self::TEST_VERSIONS_USER)
+ ->setSharedWith(self::TEST_VERSIONS_USER2)
+ ->setPermissions(Constants::PERMISSION_ALL);
+ $share = Server::get(\OCP\Share\IManager::class)->createShare($share);
+ $shareManager = Server::get(\OCP\Share\IManager::class);
+ $shareManager->acceptShare($share, self::TEST_VERSIONS_USER2);
+
+ $share->setTarget('subfolder/folder');
+ $shareManager->moveShare($share, self::TEST_VERSIONS_USER2);
+
+ $versions = $this->createAndCheckVersions(
+ Filesystem::getView(),
+ 'folder/test.txt'
+ );
+
+ $file->putContent('test file');
+
+ $this->loginAsUser(self::TEST_VERSIONS_USER2);
+
+ $firstVersion = current($versions);
+
+ $this->assertTrue(Storage::rollback('folder/test.txt', $firstVersion['version'], $this->user1));
+
+ $this->loginAsUser(self::TEST_VERSIONS_USER);
+
+ Server::get(\OCP\Share\IManager::class)->deleteShare($share);
+ $this->assertEquals('version 2', $file->getContent(), 'File content has not changed');
+ }
+
+ /**
+ * @param string $hookName name of hook called
+ * @param string $params variable to receive parameters provided by hook
+ */
+ private function connectMockHooks($hookName, &$params) {
+ if ($hookName === null) {
+ return;
+ }
+
+ $eventHandler = $this->getMockBuilder(DummyHookListener::class)
+ ->onlyMethods(['callback'])
+ ->getMock();
+
+ $eventHandler->expects($this->any())
+ ->method('callback')
+ ->willReturnCallback(
+ function ($p) use (&$params): void {
+ $params = $p;
+ }
+ );
+
+ Util::connectHook(
+ '\OCP\Versions',
+ $hookName,
+ $eventHandler,
+ 'callback'
+ );
+ }
+
+ private function doTestRestore(): void {
+ $filePath = self::TEST_VERSIONS_USER . '/files/sub/test.txt';
+ $this->rootView->file_put_contents($filePath, 'test file');
+
+ $fileInfo = $this->rootView->getFileInfo($filePath);
+ $t0 = $this->rootView->filemtime($filePath);
+
+ // not exactly the same timestamp as the file
+ $t1 = time() - 60;
+ // second version is two weeks older
+ $t2 = $t1 - 60 * 60 * 24 * 14;
+
+ // create some versions
+ $v1 = self::USERS_VERSIONS_ROOT . '/sub/test.txt.v' . $t1;
+ $v2 = self::USERS_VERSIONS_ROOT . '/sub/test.txt.v' . $t2;
+
+ $this->rootView->mkdir(self::USERS_VERSIONS_ROOT . '/sub');
+
+ $this->rootView->file_put_contents($v1, 'version1');
+ $fileInfoV1 = $this->rootView->getFileInfo($v1);
+ $versionEntity = new VersionEntity();
+ $versionEntity->setFileId($fileInfo->getId());
+ $versionEntity->setTimestamp($t1);
+ $versionEntity->setSize($fileInfoV1->getSize());
+ $versionEntity->setMimetype($this->mimeTypeLoader->getId($fileInfoV1->getMimetype()));
+ $versionEntity->setMetadata([]);
+ $this->versionsMapper->insert($versionEntity);
+
+ $this->rootView->file_put_contents($v2, 'version2');
+ $fileInfoV2 = $this->rootView->getFileInfo($v2);
+ $versionEntity = new VersionEntity();
+ $versionEntity->setFileId($fileInfo->getId());
+ $versionEntity->setTimestamp($t2);
+ $versionEntity->setSize($fileInfoV2->getSize());
+ $versionEntity->setMimetype($this->mimeTypeLoader->getId($fileInfoV2->getMimetype()));
+ $versionEntity->setMetadata([]);
+ $this->versionsMapper->insert($versionEntity);
+
+ $oldVersions = Storage::getVersions(
+ self::TEST_VERSIONS_USER, '/sub/test.txt'
+ );
+
+ $this->assertCount(2, $oldVersions);
+
+ $this->assertEquals('test file', $this->rootView->file_get_contents($filePath));
+ $info1 = $this->rootView->getFileInfo($filePath);
+
+ $eventDispatcher = Server::get(IEventDispatcher::class);
+ $eventFired = false;
+ $eventDispatcher->addListener(VersionRestoredEvent::class, function ($event) use (&$eventFired, $t2): void {
+ $eventFired = true;
+ $this->assertEquals('/sub/test.txt', $event->getVersion()->getVersionPath());
+ $this->assertTrue($event->getVersion()->getRevisionId() > 0);
+ });
+
+ $versionManager = Server::get(IVersionManager::class);
+ $versions = $versionManager->getVersionsForFile($this->user1, $info1);
+ $version = array_filter($versions, function ($version) use ($t2) {
+ return $version->getRevisionId() === $t2;
+ });
+ $this->assertTrue($versionManager->rollback(current($version)));
+
+ $this->assertTrue($eventFired, 'VersionRestoredEvent was not fired');
+
+ $this->assertEquals('version2', $this->rootView->file_get_contents($filePath));
+ $info2 = $this->rootView->getFileInfo($filePath);
+
+ $this->assertNotEquals(
+ $info2['etag'],
+ $info1['etag'],
+ 'Etag must change after rolling back version'
+ );
+ $this->assertEquals(
+ $info2['fileid'],
+ $info1['fileid'],
+ 'File id must not change after rolling back version'
+ );
+ $this->assertEquals(
+ $info2['mtime'],
+ $t2,
+ 'Restored file has mtime from version'
+ );
+
+ $newVersions = Storage::getVersions(
+ self::TEST_VERSIONS_USER, '/sub/test.txt'
+ );
+
+ $this->assertTrue(
+ $this->rootView->file_exists(self::USERS_VERSIONS_ROOT . '/sub/test.txt.v' . $t0),
+ 'A version file was created for the file before restoration'
+ );
+ $this->assertTrue(
+ $this->rootView->file_exists($v1),
+ 'Untouched version file is still there'
+ );
+ $this->assertFalse(
+ $this->rootView->file_exists($v2),
+ 'Restored version file gone from files_version folder'
+ );
+
+ $this->assertCount(2, $newVersions, 'Additional version created');
+
+ $this->assertTrue(
+ isset($newVersions[$t0 . '#' . 'test.txt']),
+ 'A version was created for the file before restoration'
+ );
+ $this->assertTrue(
+ isset($newVersions[$t1 . '#' . 'test.txt']),
+ 'Untouched version is still there'
+ );
+ $this->assertFalse(
+ isset($newVersions[$t2 . '#' . 'test.txt']),
+ 'Restored version is not in the list any more'
+ );
+ }
+
+ /**
+ * Test whether versions are created when overwriting as owner
+ */
+ public function testStoreVersionAsOwner(): void {
+ $this->loginAsUser(self::TEST_VERSIONS_USER);
+
+ $this->createAndCheckVersions(
+ Filesystem::getView(),
+ 'test.txt'
+ );
+ }
+
+ /**
+ * Test whether versions are created when overwriting as share recipient
+ */
+ public function testStoreVersionAsRecipient(): void {
+ $this->loginAsUser(self::TEST_VERSIONS_USER);
+
+ Filesystem::mkdir('folder');
+ Filesystem::file_put_contents('folder/test.txt', 'test file');
+
+ $node = \OC::$server->getUserFolder(self::TEST_VERSIONS_USER)->get('folder');
+ $share = Server::get(\OCP\Share\IManager::class)->newShare();
+ $share->setNode($node)
+ ->setShareType(IShare::TYPE_USER)
+ ->setSharedBy(self::TEST_VERSIONS_USER)
+ ->setSharedWith(self::TEST_VERSIONS_USER2)
+ ->setPermissions(Constants::PERMISSION_ALL);
+ $share = Server::get(\OCP\Share\IManager::class)->createShare($share);
+ Server::get(\OCP\Share\IManager::class)->acceptShare($share, self::TEST_VERSIONS_USER2);
+
+ $this->loginAsUser(self::TEST_VERSIONS_USER2);
+
+ $this->createAndCheckVersions(
+ Filesystem::getView(),
+ 'folder/test.txt'
+ );
+
+ Server::get(\OCP\Share\IManager::class)->deleteShare($share);
+ }
+
+ /**
+ * Test whether versions are created when overwriting anonymously.
+ *
+ * When uploading through a public link or publicwebdav, no user
+ * is logged in. File modification must still be able to find
+ * the owner and create versions.
+ */
+ public function testStoreVersionAsAnonymous(): void {
+ $this->logout();
+
+ // note: public link upload does this,
+ // needed to make the hooks fire
+ \OC_Util::setupFS(self::TEST_VERSIONS_USER);
+
+ $userView = new View('/' . self::TEST_VERSIONS_USER . '/files');
+ $this->createAndCheckVersions(
+ $userView,
+ 'test.txt'
+ );
+ }
+
+ private function createAndCheckVersions(View $view, string $path): array {
+ $view->file_put_contents($path, 'test file');
+ $view->file_put_contents($path, 'version 1');
+ $view->file_put_contents($path, 'version 2');
+
+ $this->loginAsUser(self::TEST_VERSIONS_USER);
+
+ // need to scan for the versions
+ [$rootStorage,] = $this->rootView->resolvePath(self::TEST_VERSIONS_USER . '/files_versions');
+ $rootStorage->getScanner()->scan('files_versions');
+
+ $versions = Storage::getVersions(
+ self::TEST_VERSIONS_USER, '/' . $path
+ );
+
+ // note: we cannot predict how many versions are created due to
+ // test run timing
+ $this->assertGreaterThan(0, count($versions));
+
+ return $versions;
+ }
+
+ public static function loginHelper(string $user, bool $create = false) {
+ if ($create) {
+ $backend = new \Test\Util\User\Dummy();
+ $backend->createUser($user, $user);
+ Server::get(IUserManager::class)->registerBackend($backend);
+ }
+
+ \OC_Util::tearDownFS();
+ \OC_User::setUserId('');
+ Filesystem::tearDown();
+ \OC_User::setUserId($user);
+ \OC_Util::setupFS($user);
+ \OC::$server->getUserFolder($user);
+ }
+}
+
+class DummyHookListener {
+ public function callback() {
+ }
+}
+
+// extend the original class to make it possible to test protected methods
+class VersionStorageToTest extends Storage {
+
+ /**
+ * @param integer $time
+ */
+ public function callProtectedGetExpireList($time, $versions) {
+ return self::getExpireList($time, $versions);
+ }
+}
diff --git a/apps/files_versions/tests/Versions/VersionManagerTest.php b/apps/files_versions/tests/Versions/VersionManagerTest.php
new file mode 100644
index 00000000000..79caa65d5f1
--- /dev/null
+++ b/apps/files_versions/tests/Versions/VersionManagerTest.php
@@ -0,0 +1,140 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_Versions\Tests\Versions;
+
+use OC\Files\Storage\Local;
+use OCA\Files_Versions\Events\VersionRestoredEvent;
+use OCA\Files_Versions\Versions\IVersion;
+use OCA\Files_Versions\Versions\IVersionBackend;
+use OCA\Files_Versions\Versions\VersionManager;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\Files\Storage\IStorage;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class VersionManagerTest extends TestCase {
+ private function getBackend(bool $shouldUse = true): IVersionBackend {
+ $backend = $this->createMock(IVersionBackend::class);
+ $backend->method('useBackendForStorage')
+ ->willReturn($shouldUse);
+ return $backend;
+ }
+
+ private function getStorage(string $class): IStorage&MockObject {
+ return $this->getMockBuilder($class)
+ ->disableOriginalConstructor()
+ ->onlyMethods(array_diff(get_class_methods($class), ['instanceOfStorage']))
+ ->getMock();
+ }
+
+ public function testGetBackendSingle(): void {
+ $dispatcher = $this->createMock(IEventDispatcher::class);
+ $manager = new VersionManager($dispatcher);
+ $backend = $this->getBackend();
+ $manager->registerBackend(IStorage::class, $backend);
+
+ $this->assertEquals($backend, $manager->getBackendForStorage($this->getStorage(Local::class)));
+ }
+
+ public function testGetBackendMoreSpecific(): void {
+ $dispatcher = $this->createMock(IEventDispatcher::class);
+ $manager = new VersionManager($dispatcher);
+ $backend1 = $this->getBackend();
+ $backend2 = $this->getBackend();
+ $manager->registerBackend(IStorage::class, $backend1);
+ $manager->registerBackend(Local::class, $backend2);
+
+ $this->assertEquals($backend2, $manager->getBackendForStorage($this->getStorage(Local::class)));
+ }
+
+ public function testGetBackendNoUse(): void {
+ $dispatcher = $this->createMock(IEventDispatcher::class);
+ $manager = new VersionManager($dispatcher);
+ $backend1 = $this->getBackend();
+ $backend2 = $this->getBackend(false);
+ $manager->registerBackend(IStorage::class, $backend1);
+ $manager->registerBackend(Local::class, $backend2);
+
+ $this->assertEquals($backend1, $manager->getBackendForStorage($this->getStorage(Local::class)));
+ }
+
+ public function testGetBackendMultiple(): void {
+ $dispatcher = $this->createMock(IEventDispatcher::class);
+ $manager = new VersionManager($dispatcher);
+ $backend1 = $this->getBackend();
+ $backend2 = $this->getBackend(false);
+ $backend3 = $this->getBackend();
+ $manager->registerBackend(IStorage::class, $backend1);
+ $manager->registerBackend(Local::class, $backend2);
+ $manager->registerBackend(Local::class, $backend3);
+
+ $this->assertEquals($backend3, $manager->getBackendForStorage($this->getStorage(Local::class)));
+ }
+
+ public function testRollbackSuccess(): void {
+ $versionMock = $this->createMock(IVersion::class);
+ $backendMock = $this->createMock(IVersionBackend::class);
+
+ $backendMock->expects($this->once())
+ ->method('rollback')
+ ->with($versionMock)
+ ->willReturn(true);
+
+ $versionMock->method('getBackend')->willReturn($backendMock);
+
+ $dispatcherMock = $this->createMock(IEventDispatcher::class);
+ $dispatcherMock->expects($this->once())
+ ->method('dispatchTyped')
+ ->with($this->isInstanceOf(VersionRestoredEvent::class));
+
+ $manager = new VersionManager($dispatcherMock);
+
+ $this->assertTrue($manager->rollback($versionMock));
+ }
+
+ public function testRollbackNull(): void {
+ $versionMock = $this->createMock(IVersion::class);
+ $backendMock = $this->createMock(IVersionBackend::class);
+
+ $backendMock->expects($this->once())
+ ->method('rollback')
+ ->with($versionMock)
+ ->willReturn(null);
+
+ $versionMock->method('getBackend')->willReturn($backendMock);
+
+ $dispatcherMock = $this->createMock(IEventDispatcher::class);
+ $dispatcherMock->expects($this->once())
+ ->method('dispatchTyped')
+ ->with($this->isInstanceOf(VersionRestoredEvent::class));
+
+ $manager = new VersionManager($dispatcherMock);
+
+ $this->assertNull($manager->rollback($versionMock));
+ }
+
+ public function testRollbackFailure(): void {
+ $versionMock = $this->createMock(IVersion::class);
+ $backendMock = $this->createMock(IVersionBackend::class);
+
+ $backendMock->expects($this->once())
+ ->method('rollback')
+ ->with($versionMock)
+ ->willReturn(false);
+
+ $versionMock->method('getBackend')->willReturn($backendMock);
+
+ $dispatcherMock = $this->createMock(IEventDispatcher::class);
+ $dispatcherMock->expects($this->never())->method('dispatchTyped');
+
+ $manager = new VersionManager($dispatcherMock);
+
+ $this->assertFalse($manager->rollback($versionMock));
+ }
+}
diff --git a/apps/files_versions/tests/command/cleanuptest.php b/apps/files_versions/tests/command/cleanuptest.php
deleted file mode 100644
index af217b18893..00000000000
--- a/apps/files_versions/tests/command/cleanuptest.php
+++ /dev/null
@@ -1,170 +0,0 @@
-<?php
-/**
- * @author Björn Schießle <schiessle@owncloud.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-
-namespace OCA\Files_Versions\Tests\Command;
-
-
-use OCA\Files_Versions\Command\CleanUp;
-use Test\TestCase;
-use OC\User\Manager;
-use OCP\Files\IRootFolder;
-
-/**
- * Class CleanupTest
- *
- * @group DB
- *
- * @package OCA\Files_Versions\Tests\Command
- */
-class CleanupTest extends TestCase {
-
- /** @var CleanUp */
- protected $cleanup;
-
- /** @var \PHPUnit_Framework_MockObject_MockObject | Manager */
- protected $userManager;
-
- /** @var \PHPUnit_Framework_MockObject_MockObject | IRootFolder */
- protected $rootFolder;
-
- public function setUp() {
- parent::setUp();
-
- $this->rootFolder = $this->getMockBuilder('OCP\Files\IRootFolder')
- ->disableOriginalConstructor()->getMock();
- $this->userManager = $this->getMockBuilder('OC\User\Manager')
- ->disableOriginalConstructor()->getMock();
-
-
- $this->cleanup = new CleanUp($this->rootFolder, $this->userManager);
- }
-
- /**
- * @dataProvider dataTestDeleteVersions
- * @param boolean $nodeExists
- */
- public function testDeleteVersions($nodeExists) {
-
- $this->rootFolder->expects($this->once())
- ->method('nodeExists')
- ->with('/testUser/files_versions')
- ->willReturn($nodeExists);
-
-
- if($nodeExists) {
- $this->rootFolder->expects($this->once())
- ->method('get')
- ->with('/testUser/files_versions')
- ->willReturn($this->rootFolder);
- $this->rootFolder->expects($this->once())
- ->method('delete');
- } else {
- $this->rootFolder->expects($this->never())
- ->method('get');
- $this->rootFolder->expects($this->never())
- ->method('delete');
- }
-
- $this->invokePrivate($this->cleanup, 'deleteVersions', ['testUser']);
- }
-
- public function dataTestDeleteVersions() {
- return array(
- array(true),
- array(false)
- );
- }
-
-
- /**
- * test delete versions from users given as parameter
- */
- public function testExecuteDeleteListOfUsers() {
- $userIds = ['user1', 'user2', 'user3'];
-
- $instance = $this->getMockBuilder('OCA\Files_Versions\Command\CleanUp')
- ->setMethods(['deleteVersions'])
- ->setConstructorArgs([$this->rootFolder, $this->userManager])
- ->getMock();
- $instance->expects($this->exactly(count($userIds)))
- ->method('deleteVersions')
- ->willReturnCallback(function ($user) use ($userIds) {
- $this->assertTrue(in_array($user, $userIds));
- });
-
- $this->userManager->expects($this->exactly(count($userIds)))
- ->method('userExists')->willReturn(true);
-
- $inputInterface = $this->getMockBuilder('\Symfony\Component\Console\Input\InputInterface')
- ->disableOriginalConstructor()->getMock();
- $inputInterface->expects($this->once())->method('getArgument')
- ->with('user_id')
- ->willReturn($userIds);
-
- $outputInterface = $this->getMockBuilder('\Symfony\Component\Console\Output\OutputInterface')
- ->disableOriginalConstructor()->getMock();
-
- $this->invokePrivate($instance, 'execute', [$inputInterface, $outputInterface]);
- }
-
- /**
- * test delete versions of all users
- */
- public function testExecuteAllUsers() {
- $userIds = [];
- $backendUsers = ['user1', 'user2'];
-
- $instance = $this->getMockBuilder('OCA\Files_Versions\Command\CleanUp')
- ->setMethods(['deleteVersions'])
- ->setConstructorArgs([$this->rootFolder, $this->userManager])
- ->getMock();
-
- $backend = $this->getMockBuilder('OC_User_Interface')
- ->disableOriginalConstructor()->getMock();
- $backend->expects($this->once())->method('getUsers')
- ->with('', 500, 0)
- ->willReturn($backendUsers);
-
- $instance->expects($this->exactly(count($backendUsers)))
- ->method('deleteVersions')
- ->willReturnCallback(function ($user) use ($backendUsers) {
- $this->assertTrue(in_array($user, $backendUsers));
- });
-
- $inputInterface = $this->getMockBuilder('\Symfony\Component\Console\Input\InputInterface')
- ->disableOriginalConstructor()->getMock();
- $inputInterface->expects($this->once())->method('getArgument')
- ->with('user_id')
- ->willReturn($userIds);
-
- $outputInterface = $this->getMockBuilder('\Symfony\Component\Console\Output\OutputInterface')
- ->disableOriginalConstructor()->getMock();
-
- $this->userManager->expects($this->once())
- ->method('getBackends')
- ->willReturn([$backend]);
-
- $this->invokePrivate($instance, 'execute', [$inputInterface, $outputInterface]);
- }
-
-}
diff --git a/apps/files_versions/tests/command/expiretest.php b/apps/files_versions/tests/command/expiretest.php
deleted file mode 100644
index f89ece82515..00000000000
--- a/apps/files_versions/tests/command/expiretest.php
+++ /dev/null
@@ -1,43 +0,0 @@
-<?php
-/**
- * @author Joas Schilling <nickvergessen@owncloud.com>
- * @author Jörn Friedrich Dreyer <jfd@butonic.de>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\Files_Versions\Tests\Command;
-
-use OCA\Files_Versions\Command\Expire;
-use Test\TestCase;
-
-/**
- * Class ExpireTest
- *
- * @group DB
- *
- * @package OCA\Files_Versions\Tests\Command
- */
-class ExpireTest extends TestCase {
- public function testExpireNonExistingUser() {
- $command = new Expire($this->getUniqueID('test'), '');
- $command->handle();
-
- $this->assertTrue(true);
- }
-}
diff --git a/apps/files_versions/tests/expirationtest.php b/apps/files_versions/tests/expirationtest.php
deleted file mode 100644
index 2dfff19f230..00000000000
--- a/apps/files_versions/tests/expirationtest.php
+++ /dev/null
@@ -1,206 +0,0 @@
-<?php
-/**
- * @author Joas Schilling <nickvergessen@owncloud.com>
- * @author Victor Dubiniuk <dubiniuk@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-namespace OCA\Files_Versions\Tests;
-
-use \OCA\Files_Versions\Expiration;
-
-class Expiration_Test extends \Test\TestCase {
- const SECONDS_PER_DAY = 86400; //60*60*24
-
- public function expirationData(){
- $today = 100*self::SECONDS_PER_DAY;
- $back10Days = (100-10)*self::SECONDS_PER_DAY;
- $back20Days = (100-20)*self::SECONDS_PER_DAY;
- $back30Days = (100-30)*self::SECONDS_PER_DAY;
- $back35Days = (100-35)*self::SECONDS_PER_DAY;
-
- // it should never happen, but who knows :/
- $ahead100Days = (100+100)*self::SECONDS_PER_DAY;
-
- return [
- // Expiration is disabled - always should return false
- [ 'disabled', $today, $back10Days, false, false],
- [ 'disabled', $today, $back10Days, true, false],
- [ 'disabled', $today, $ahead100Days, true, false],
-
- // Default: expire in 30 days or earlier when quota requirements are met
- [ 'auto', $today, $back10Days, false, false],
- [ 'auto', $today, $back35Days, false, false],
- [ 'auto', $today, $back10Days, true, true],
- [ 'auto', $today, $back35Days, true, true],
- [ 'auto', $today, $ahead100Days, true, true],
-
- // The same with 'auto'
- [ 'auto, auto', $today, $back10Days, false, false],
- [ 'auto, auto', $today, $back35Days, false, false],
- [ 'auto, auto', $today, $back10Days, true, true],
- [ 'auto, auto', $today, $back35Days, true, true],
-
- // Keep for 15 days but expire anytime if space needed
- [ '15, auto', $today, $back10Days, false, false],
- [ '15, auto', $today, $back20Days, false, false],
- [ '15, auto', $today, $back10Days, true, true],
- [ '15, auto', $today, $back20Days, true, true],
- [ '15, auto', $today, $ahead100Days, true, true],
-
- // Expire anytime if space needed, Expire all older than max
- [ 'auto, 15', $today, $back10Days, false, false],
- [ 'auto, 15', $today, $back20Days, false, true],
- [ 'auto, 15', $today, $back10Days, true, true],
- [ 'auto, 15', $today, $back20Days, true, true],
- [ 'auto, 15', $today, $ahead100Days, true, true],
-
- // Expire all older than max OR older than min if space needed
- [ '15, 25', $today, $back10Days, false, false],
- [ '15, 25', $today, $back20Days, false, false],
- [ '15, 25', $today, $back30Days, false, true],
- [ '15, 25', $today, $back10Days, false, false],
- [ '15, 25', $today, $back20Days, true, true],
- [ '15, 25', $today, $back30Days, true, true],
- [ '15, 25', $today, $ahead100Days, true, false],
-
- // Expire all older than max OR older than min if space needed
- // Max<Min case
- [ '25, 15', $today, $back10Days, false, false],
- [ '25, 15', $today, $back20Days, false, false],
- [ '25, 15', $today, $back30Days, false, true],
- [ '25, 15', $today, $back10Days, false, false],
- [ '25, 15', $today, $back20Days, true, false],
- [ '25, 15', $today, $back30Days, true, true],
- [ '25, 15', $today, $ahead100Days, true, false],
- ];
- }
-
- /**
- * @dataProvider expirationData
- *
- * @param string $retentionObligation
- * @param int $timeNow
- * @param int $timestamp
- * @param bool $quotaExceeded
- * @param string $expectedResult
- */
- public function testExpiration($retentionObligation, $timeNow, $timestamp, $quotaExceeded, $expectedResult){
- $mockedConfig = $this->getMockedConfig($retentionObligation);
- $mockedTimeFactory = $this->getMockedTimeFactory($timeNow);
-
- $expiration = new Expiration($mockedConfig, $mockedTimeFactory);
- $actualResult = $expiration->isExpired($timestamp, $quotaExceeded);
-
- $this->assertEquals($expectedResult, $actualResult);
- }
-
-
- public function configData(){
- return [
- [ 'disabled', null, null, null],
- [ 'auto', Expiration::NO_OBLIGATION, Expiration::NO_OBLIGATION, true ],
- [ 'auto,auto', Expiration::NO_OBLIGATION, Expiration::NO_OBLIGATION, true ],
- [ 'auto, auto', Expiration::NO_OBLIGATION, Expiration::NO_OBLIGATION, true ],
- [ 'auto, 3', Expiration::NO_OBLIGATION, 3, true ],
- [ '5, auto', 5, Expiration::NO_OBLIGATION, true ],
- [ '3, 5', 3, 5, false ],
- [ '10, 3', 10, 10, false ],
- [ 'g,a,r,b,a,g,e', Expiration::NO_OBLIGATION, Expiration::NO_OBLIGATION, true ],
- [ '-3,8', Expiration::NO_OBLIGATION, Expiration::NO_OBLIGATION, true ]
- ];
- }
-
-
- /**
- * @dataProvider configData
- *
- * @param string $configValue
- * @param int $expectedMinAge
- * @param int $expectedMaxAge
- * @param bool $expectedCanPurgeToSaveSpace
- */
- public function testParseRetentionObligation($configValue, $expectedMinAge, $expectedMaxAge, $expectedCanPurgeToSaveSpace){
- $mockedConfig = $this->getMockedConfig($configValue);
- $mockedTimeFactory = $this->getMockedTimeFactory(
- time()
- );
-
- $expiration = new Expiration($mockedConfig, $mockedTimeFactory);
- $this->assertAttributeEquals($expectedMinAge, 'minAge', $expiration);
- $this->assertAttributeEquals($expectedMaxAge, 'maxAge', $expiration);
- $this->assertAttributeEquals($expectedCanPurgeToSaveSpace, 'canPurgeToSaveSpace', $expiration);
- }
-
- /**
- *
- * @param int $time
- * @return \OCP\AppFramework\Utility\ITimeFactory
- */
- private function getMockedTimeFactory($time){
- $mockedTimeFactory = $this->getMockBuilder('\OCP\AppFramework\Utility\ITimeFactory')
- ->disableOriginalConstructor()
- ->setMethods(['getTime'])
- ->getMock()
- ;
- $mockedTimeFactory->expects($this->any())->method('getTime')->will(
- $this->returnValue($time)
- );
-
- return $mockedTimeFactory;
- }
-
- /**
- *
- * @param string $returnValue
- * @return \OCP\IConfig
- */
- private function getMockedConfig($returnValue){
- $mockedConfig = $this->getMockBuilder('\OCP\IConfig')
- ->disableOriginalConstructor()
- ->setMethods(
- [
- 'setSystemValues',
- 'setSystemValue',
- 'getSystemValue',
- 'getFilteredSystemValue',
- 'deleteSystemValue',
- 'getAppKeys',
- 'setAppValue',
- 'getAppValue',
- 'deleteAppValue',
- 'deleteAppValues',
- 'setUserValue',
- 'getUserValue',
- 'getUserValueForUsers',
- 'getUserKeys',
- 'deleteUserValue',
- 'deleteAllUserValues',
- 'deleteAppFromAllUsers',
- 'getUsersForUserValue'
- ]
- )
- ->getMock()
- ;
- $mockedConfig->expects($this->any())->method('getSystemValue')->will(
- $this->returnValue($returnValue)
- );
-
- return $mockedConfig;
- }
-}
diff --git a/apps/files_versions/tests/js/versioncollectionSpec.js b/apps/files_versions/tests/js/versioncollectionSpec.js
deleted file mode 100644
index 87065fa1d36..00000000000
--- a/apps/files_versions/tests/js/versioncollectionSpec.js
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * Copyright (c) 2015
- *
- * This file is licensed under the Affero General Public License version 3
- * or later.
- *
- * See the COPYING-README file.
- *
- */
-describe('OCA.Versions.VersionCollection', function() {
- var VersionCollection = OCA.Versions.VersionCollection;
- var collection, fileInfoModel;
-
- beforeEach(function() {
- fileInfoModel = new OCA.Files.FileInfoModel({
- path: '/subdir',
- name: 'some file.txt'
- });
- collection = new VersionCollection();
- collection.setFileInfo(fileInfoModel);
- });
- it('fetches the next page', function() {
- collection.fetchNext();
-
- expect(fakeServer.requests.length).toEqual(1);
- expect(fakeServer.requests[0].url).toEqual(
- OC.generateUrl('apps/files_versions/ajax/getVersions.php') +
- '?source=%2Fsubdir%2Fsome%20file.txt&start=0'
- );
- fakeServer.requests[0].respond(
- 200,
- { 'Content-Type': 'application/json' },
- JSON.stringify({
- status: 'success',
- data: {
- endReached: false,
- versions: [{
- version: 10000000,
- size: 123,
- name: 'some file.txt',
- fullPath: '/subdir/some file.txt'
- },{
- version: 15000000,
- size: 150,
- name: 'some file.txt',
- path: '/subdir/some file.txt'
- }]
- }
- })
- );
-
- expect(collection.length).toEqual(2);
- expect(collection.hasMoreResults()).toEqual(true);
-
- collection.fetchNext();
-
- expect(fakeServer.requests.length).toEqual(2);
- expect(fakeServer.requests[1].url).toEqual(
- OC.generateUrl('apps/files_versions/ajax/getVersions.php') +
- '?source=%2Fsubdir%2Fsome%20file.txt&start=2'
- );
- fakeServer.requests[1].respond(
- 200,
- { 'Content-Type': 'application/json' },
- JSON.stringify({
- status: 'success',
- data: {
- endReached: true,
- versions: [{
- version: 18000000,
- size: 123,
- name: 'some file.txt',
- path: '/subdir/some file.txt'
- }]
- }
- })
- );
-
- expect(collection.length).toEqual(3);
- expect(collection.hasMoreResults()).toEqual(false);
-
- collection.fetchNext();
-
- // no further requests
- expect(fakeServer.requests.length).toEqual(2);
- });
- it('properly parses the results', function() {
- collection.fetchNext();
-
- expect(fakeServer.requests.length).toEqual(1);
- expect(fakeServer.requests[0].url).toEqual(
- OC.generateUrl('apps/files_versions/ajax/getVersions.php') +
- '?source=%2Fsubdir%2Fsome%20file.txt&start=0'
- );
- fakeServer.requests[0].respond(
- 200,
- { 'Content-Type': 'application/json' },
- JSON.stringify({
- status: 'success',
- data: {
- endReached: false,
- versions: [{
- version: 10000000,
- size: 123,
- name: 'some file.txt',
- path: '/subdir/some file.txt'
- },{
- version: 15000000,
- size: 150,
- name: 'some file.txt',
- path: '/subdir/some file.txt'
- }]
- }
- })
- );
-
- expect(collection.length).toEqual(2);
-
- var model = collection.at(0);
- expect(model.get('id')).toEqual(10000000);
- expect(model.get('timestamp')).toEqual(10000000);
- expect(model.get('name')).toEqual('some file.txt');
- expect(model.get('fullPath')).toEqual('/subdir/some file.txt');
- expect(model.get('size')).toEqual(123);
-
- model = collection.at(1);
- expect(model.get('id')).toEqual(15000000);
- expect(model.get('timestamp')).toEqual(15000000);
- expect(model.get('name')).toEqual('some file.txt');
- expect(model.get('fullPath')).toEqual('/subdir/some file.txt');
- expect(model.get('size')).toEqual(150);
- });
- it('resets page counted when setting a new file info model', function() {
- collection.fetchNext();
-
- expect(fakeServer.requests.length).toEqual(1);
- fakeServer.requests[0].respond(
- 200,
- { 'Content-Type': 'application/json' },
- JSON.stringify({
- status: 'success',
- data: {
- endReached: true,
- versions: [{
- version: 18000000,
- size: 123,
- name: 'some file.txt',
- path: '/subdir/some file.txt'
- }]
- }
- })
- );
-
- expect(collection.hasMoreResults()).toEqual(false);
-
- collection.setFileInfo(fileInfoModel);
-
- expect(collection.hasMoreResults()).toEqual(true);
- });
-});
-
diff --git a/apps/files_versions/tests/js/versionmodelSpec.js b/apps/files_versions/tests/js/versionmodelSpec.js
deleted file mode 100644
index 0f1c06581d5..00000000000
--- a/apps/files_versions/tests/js/versionmodelSpec.js
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * Copyright (c) 2015
- *
- * This file is licensed under the Affero General Public License version 3
- * or later.
- *
- * See the COPYING-README file.
- *
- */
-describe('OCA.Versions.VersionModel', function() {
- var VersionModel = OCA.Versions.VersionModel;
- var model;
-
- beforeEach(function() {
- model = new VersionModel({
- id: 10000000,
- timestamp: 10000000,
- fullPath: '/subdir/some file.txt',
- name: 'some file.txt',
- size: 150
- });
- });
-
- it('returns the full path', function() {
- expect(model.getFullPath()).toEqual('/subdir/some file.txt');
- });
- it('returns the preview url', function() {
- expect(model.getPreviewUrl())
- .toEqual(OC.generateUrl('/apps/files_versions/preview') +
- '?file=%2Fsubdir%2Fsome%20file.txt&version=10000000'
- );
- });
- it('returns the download url', function() {
- expect(model.getDownloadUrl())
- .toEqual(OC.generateUrl('/apps/files_versions/download.php') +
- '?file=%2Fsubdir%2Fsome%20file.txt&revision=10000000'
- );
- });
- describe('reverting', function() {
- var revertEventStub;
- var successStub;
- var errorStub;
-
- beforeEach(function() {
- revertEventStub = sinon.stub();
- errorStub = sinon.stub();
- successStub = sinon.stub();
-
- model.on('revert', revertEventStub);
- model.on('error', errorStub);
- });
- it('tells the server to revert when calling the revert method', function() {
- model.revert({
- success: successStub
- });
-
- expect(fakeServer.requests.length).toEqual(1);
- expect(fakeServer.requests[0].url)
- .toEqual(
- OC.generateUrl('/apps/files_versions/ajax/rollbackVersion.php') +
- '?file=%2Fsubdir%2Fsome+file.txt&revision=10000000'
- );
-
- fakeServer.requests[0].respond(
- 200,
- { 'Content-Type': 'application/json' },
- JSON.stringify({
- status: 'success',
- })
- );
-
- expect(revertEventStub.calledOnce).toEqual(true);
- expect(successStub.calledOnce).toEqual(true);
- expect(errorStub.notCalled).toEqual(true);
- });
- it('triggers error event when server returns a failure', function() {
- model.revert({
- success: successStub
- });
-
- expect(fakeServer.requests.length).toEqual(1);
- fakeServer.requests[0].respond(
- 200,
- { 'Content-Type': 'application/json' },
- JSON.stringify({
- status: 'error',
- })
- );
-
- expect(revertEventStub.notCalled).toEqual(true);
- expect(successStub.notCalled).toEqual(true);
- expect(errorStub.calledOnce).toEqual(true);
- });
- });
-});
-
diff --git a/apps/files_versions/tests/js/versionstabviewSpec.js b/apps/files_versions/tests/js/versionstabviewSpec.js
deleted file mode 100644
index 306dd66be2a..00000000000
--- a/apps/files_versions/tests/js/versionstabviewSpec.js
+++ /dev/null
@@ -1,208 +0,0 @@
-/*
- * Copyright (c) 2015
- *
- * This file is licensed under the Affero General Public License version 3
- * or later.
- *
- * See the COPYING-README file.
- *
- */
-describe('OCA.Versions.VersionsTabView', function() {
- var VersionCollection = OCA.Versions.VersionCollection;
- var VersionModel = OCA.Versions.VersionModel;
- var VersionsTabView = OCA.Versions.VersionsTabView;
-
- var fetchStub, fileInfoModel, tabView, testVersions, clock;
-
- beforeEach(function() {
- clock = sinon.useFakeTimers(Date.UTC(2015, 6, 17, 1, 2, 0, 3));
- var time1 = Date.UTC(2015, 6, 17, 1, 2, 0, 3) / 1000;
- var time2 = Date.UTC(2015, 6, 15, 1, 2, 0, 3) / 1000;
-
- var version1 = new VersionModel({
- id: time1,
- timestamp: time1,
- name: 'some file.txt',
- size: 140,
- fullPath: '/subdir/some file.txt'
- });
- var version2 = new VersionModel({
- id: time2,
- timestamp: time2,
- name: 'some file.txt',
- size: 150,
- fullPath: '/subdir/some file.txt'
- });
-
- testVersions = [version1, version2];
-
- fetchStub = sinon.stub(VersionCollection.prototype, 'fetch');
- fileInfoModel = new OCA.Files.FileInfoModel({
- id: 123,
- name: 'test.txt'
- });
- tabView = new VersionsTabView();
- tabView.render();
- });
-
- afterEach(function() {
- fetchStub.restore();
- tabView.remove();
- clock.restore();
- });
-
- describe('rendering', function() {
- it('reloads matching versions when setting file info model', function() {
- tabView.setFileInfo(fileInfoModel);
- expect(fetchStub.calledOnce).toEqual(true);
- });
-
- it('renders loading icon while fetching versions', function() {
- tabView.setFileInfo(fileInfoModel);
- tabView.collection.trigger('request');
-
- expect(tabView.$el.find('.loading').length).toEqual(1);
- expect(tabView.$el.find('.versions li').length).toEqual(0);
- });
-
- it('renders versions', function() {
-
- tabView.setFileInfo(fileInfoModel);
- tabView.collection.set(testVersions);
-
- var version1 = testVersions[0];
- var version2 = testVersions[1];
- var $versions = tabView.$el.find('.versions>li');
- expect($versions.length).toEqual(2);
- var $item = $versions.eq(0);
- expect($item.find('.downloadVersion').attr('href')).toEqual(version1.getDownloadUrl());
- expect($item.find('.versiondate').text()).toEqual('seconds ago');
- expect($item.find('.revertVersion').length).toEqual(1);
- expect($item.find('.preview').attr('src')).toEqual(version1.getPreviewUrl());
-
- $item = $versions.eq(1);
- expect($item.find('.downloadVersion').attr('href')).toEqual(version2.getDownloadUrl());
- expect($item.find('.versiondate').text()).toEqual('2 days ago');
- expect($item.find('.revertVersion').length).toEqual(1);
- expect($item.find('.preview').attr('src')).toEqual(version2.getPreviewUrl());
- });
- });
-
- describe('More versions', function() {
- var hasMoreResultsStub;
-
- beforeEach(function() {
- tabView.collection.set(testVersions);
- hasMoreResultsStub = sinon.stub(VersionCollection.prototype, 'hasMoreResults');
- });
- afterEach(function() {
- hasMoreResultsStub.restore();
- });
-
- it('shows "More versions" button when more versions are available', function() {
- hasMoreResultsStub.returns(true);
- tabView.collection.trigger('sync');
-
- expect(tabView.$el.find('.showMoreVersions').hasClass('hidden')).toEqual(false);
- });
- it('does not show "More versions" button when more versions are available', function() {
- hasMoreResultsStub.returns(false);
- tabView.collection.trigger('sync');
-
- expect(tabView.$el.find('.showMoreVersions').hasClass('hidden')).toEqual(true);
- });
- it('fetches and appends the next page when clicking the "More" button', function() {
- hasMoreResultsStub.returns(true);
-
- expect(fetchStub.notCalled).toEqual(true);
-
- tabView.$el.find('.showMoreVersions').click();
-
- expect(fetchStub.calledOnce).toEqual(true);
- });
- it('appends version to the list when added to collection', function() {
- var time3 = Date.UTC(2015, 6, 10, 1, 0, 0, 0) / 1000;
-
- var version3 = new VersionModel({
- id: time3,
- timestamp: time3,
- name: 'some file.txt',
- size: 54,
- fullPath: '/subdir/some file.txt'
- });
-
- tabView.collection.add(version3);
-
- expect(tabView.$el.find('.versions>li').length).toEqual(3);
-
- var $item = tabView.$el.find('.versions>li').eq(2);
- expect($item.find('.downloadVersion').attr('href')).toEqual(version3.getDownloadUrl());
- expect($item.find('.versiondate').text()).toEqual('7 days ago');
- expect($item.find('.revertVersion').length).toEqual(1);
- expect($item.find('.preview').attr('src')).toEqual(version3.getPreviewUrl());
- });
- });
-
- describe('Reverting', function() {
- var revertStub;
-
- beforeEach(function() {
- revertStub = sinon.stub(VersionModel.prototype, 'revert');
- tabView.setFileInfo(fileInfoModel);
- tabView.collection.set(testVersions);
- });
-
- afterEach(function() {
- revertStub.restore();
- });
-
- it('tells the model to revert when clicking "Revert"', function() {
- tabView.$el.find('.revertVersion').eq(1).click();
-
- expect(revertStub.calledOnce).toEqual(true);
- });
- it('triggers busy state during revert', function() {
- var busyStub = sinon.stub();
- fileInfoModel.on('busy', busyStub);
-
- tabView.$el.find('.revertVersion').eq(1).click();
-
- expect(busyStub.calledOnce).toEqual(true);
- expect(busyStub.calledWith(fileInfoModel, true)).toEqual(true);
-
- busyStub.reset();
- revertStub.getCall(0).args[0].success();
-
- expect(busyStub.calledOnce).toEqual(true);
- expect(busyStub.calledWith(fileInfoModel, false)).toEqual(true);
- });
- it('updates the file info model with the information from the reverted revision', function() {
- var changeStub = sinon.stub();
- fileInfoModel.on('change', changeStub);
-
- tabView.$el.find('.revertVersion').eq(1).click();
-
- expect(changeStub.notCalled).toEqual(true);
-
- revertStub.getCall(0).args[0].success();
-
- expect(changeStub.calledOnce).toEqual(true);
- var changes = changeStub.getCall(0).args[0].changed;
- expect(changes.size).toEqual(150);
- expect(changes.mtime).toEqual(testVersions[1].get('timestamp') * 1000);
- expect(changes.etag).toBeDefined();
- });
- it('shows notification on revert error', function() {
- var notificationStub = sinon.stub(OC.Notification, 'showTemporary');
-
- tabView.$el.find('.revertVersion').eq(1).click();
-
- revertStub.getCall(0).args[0].error();
-
- expect(notificationStub.calledOnce).toEqual(true);
-
- notificationStub.restore();
- });
- });
-});
-
diff --git a/apps/files_versions/tests/versions.php b/apps/files_versions/tests/versions.php
deleted file mode 100644
index f6658e092bd..00000000000
--- a/apps/files_versions/tests/versions.php
+++ /dev/null
@@ -1,848 +0,0 @@
-<?php
-/**
- * @author Arthur Schiwon <blizzz@owncloud.com>
- * @author Björn Schießle <schiessle@owncloud.com>
- * @author Georg Ehrke <georg@owncloud.com>
- * @author Joas Schilling <nickvergessen@owncloud.com>
- * @author Jörn Friedrich Dreyer <jfd@butonic.de>
- * @author Lukas Reschke <lukas@owncloud.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <icewind@owncloud.com>
- * @author Roeland Jago Douma <rullzer@owncloud.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <pvince81@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
-
-require_once __DIR__ . '/../appinfo/app.php';
-
-use OC\Files\Storage\Temporary;
-
-/**
- * Class Test_Files_versions
- * this class provide basic files versions test
- *
- * @group DB
- */
-class Test_Files_Versioning extends \Test\TestCase {
-
- const TEST_VERSIONS_USER = 'test-versions-user';
- const TEST_VERSIONS_USER2 = 'test-versions-user2';
- const USERS_VERSIONS_ROOT = '/test-versions-user/files_versions';
-
- /**
- * @var \OC\Files\View
- */
- private $rootView;
-
- public static function setUpBeforeClass() {
- parent::setUpBeforeClass();
-
- $application = new \OCA\Files_Sharing\AppInfo\Application();
- $application->registerMountProviders();
-
- // create test user
- self::loginHelper(self::TEST_VERSIONS_USER2, true);
- self::loginHelper(self::TEST_VERSIONS_USER, true);
- }
-
- public static function tearDownAfterClass() {
- // cleanup test user
- $user = \OC::$server->getUserManager()->get(self::TEST_VERSIONS_USER);
- if ($user !== null) { $user->delete(); }
- $user = \OC::$server->getUserManager()->get(self::TEST_VERSIONS_USER2);
- if ($user !== null) { $user->delete(); }
-
- parent::tearDownAfterClass();
- }
-
- protected function setUp() {
- parent::setUp();
-
- $config = \OC::$server->getConfig();
- $mockConfig = $this->getMock('\OCP\IConfig');
- $mockConfig->expects($this->any())
- ->method('getSystemValue')
- ->will($this->returnCallback(function ($key, $default) use ($config) {
- if ($key === 'filesystem_check_changes') {
- return \OC\Files\Cache\Watcher::CHECK_ONCE;
- } else {
- return $config->getSystemValue($key, $default);
- }
- }));
- $this->overwriteService('AllConfig', $mockConfig);
-
- // clear hooks
- \OC_Hook::clear();
- \OC::registerShareHooks();
- \OCA\Files_Versions\Hooks::connectHooks();
-
- self::loginHelper(self::TEST_VERSIONS_USER);
- $this->rootView = new \OC\Files\View();
- if (!$this->rootView->file_exists(self::USERS_VERSIONS_ROOT)) {
- $this->rootView->mkdir(self::USERS_VERSIONS_ROOT);
- }
- }
-
- protected function tearDown() {
- $this->restoreService('AllConfig');
-
- if ($this->rootView) {
- $this->rootView->deleteAll(self::TEST_VERSIONS_USER . '/files/');
- $this->rootView->deleteAll(self::TEST_VERSIONS_USER2 . '/files/');
- $this->rootView->deleteAll(self::TEST_VERSIONS_USER . '/files_versions/');
- $this->rootView->deleteAll(self::TEST_VERSIONS_USER2 . '/files_versions/');
- }
-
- \OC_Hook::clear();
-
- parent::tearDown();
- }
-
- /**
- * @medium
- * test expire logic
- * @dataProvider versionsProvider
- */
- public function testGetExpireList($versions, $sizeOfAllDeletedFiles) {
-
- // last interval end at 2592000
- $startTime = 5000000;
-
- $testClass = new VersionStorageToTest();
- list($deleted, $size) = $testClass->callProtectedGetExpireList($startTime, $versions);
-
- // we should have deleted 16 files each of the size 1
- $this->assertEquals($sizeOfAllDeletedFiles, $size);
-
- // the deleted array should only contain versions which should be deleted
- foreach($deleted as $key => $path) {
- unset($versions[$key]);
- $this->assertEquals("delete", substr($path, 0, strlen("delete")));
- }
-
- // the versions array should only contain versions which should be kept
- foreach ($versions as $version) {
- $this->assertEquals("keep", $version['path']);
- }
-
- }
-
- public function versionsProvider() {
- return array(
- // first set of versions uniformly distributed versions
- array(
- array(
- // first slice (10sec) keep one version every 2 seconds
- array("version" => 4999999, "path" => "keep", "size" => 1),
- array("version" => 4999998, "path" => "delete", "size" => 1),
- array("version" => 4999997, "path" => "keep", "size" => 1),
- array("version" => 4999995, "path" => "keep", "size" => 1),
- array("version" => 4999994, "path" => "delete", "size" => 1),
- //next slice (60sec) starts at 4999990 keep one version every 10 secons
- array("version" => 4999988, "path" => "keep", "size" => 1),
- array("version" => 4999978, "path" => "keep", "size" => 1),
- array("version" => 4999975, "path" => "delete", "size" => 1),
- array("version" => 4999972, "path" => "delete", "size" => 1),
- array("version" => 4999967, "path" => "keep", "size" => 1),
- array("version" => 4999958, "path" => "delete", "size" => 1),
- array("version" => 4999957, "path" => "keep", "size" => 1),
- //next slice (3600sec) start at 4999940 keep one version every 60 seconds
- array("version" => 4999900, "path" => "keep", "size" => 1),
- array("version" => 4999841, "path" => "delete", "size" => 1),
- array("version" => 4999840, "path" => "keep", "size" => 1),
- array("version" => 4999780, "path" => "keep", "size" => 1),
- array("version" => 4996401, "path" => "keep", "size" => 1),
- // next slice (86400sec) start at 4996400 keep one version every 3600 seconds
- array("version" => 4996350, "path" => "delete", "size" => 1),
- array("version" => 4992800, "path" => "keep", "size" => 1),
- array("version" => 4989800, "path" => "delete", "size" => 1),
- array("version" => 4989700, "path" => "delete", "size" => 1),
- array("version" => 4989200, "path" => "keep", "size" => 1),
- // next slice (2592000sec) start at 4913600 keep one version every 86400 seconds
- array("version" => 4913600, "path" => "keep", "size" => 1),
- array("version" => 4852800, "path" => "delete", "size" => 1),
- array("version" => 4827201, "path" => "delete", "size" => 1),
- array("version" => 4827200, "path" => "keep", "size" => 1),
- array("version" => 4777201, "path" => "delete", "size" => 1),
- array("version" => 4777501, "path" => "delete", "size" => 1),
- array("version" => 4740000, "path" => "keep", "size" => 1),
- // final slice starts at 2408000 keep one version every 604800 secons
- array("version" => 2408000, "path" => "keep", "size" => 1),
- array("version" => 1803201, "path" => "delete", "size" => 1),
- array("version" => 1803200, "path" => "keep", "size" => 1),
- array("version" => 1800199, "path" => "delete", "size" => 1),
- array("version" => 1800100, "path" => "delete", "size" => 1),
- array("version" => 1198300, "path" => "keep", "size" => 1),
- ),
- 16 // size of all deleted files (every file has the size 1)
- ),
- // second set of versions, here we have only really old versions
- array(
- array(
- // first slice (10sec) keep one version every 2 seconds
- // next slice (60sec) starts at 4999990 keep one version every 10 secons
- // next slice (3600sec) start at 4999940 keep one version every 60 seconds
- // next slice (86400sec) start at 4996400 keep one version every 3600 seconds
- array("version" => 4996400, "path" => "keep", "size" => 1),
- array("version" => 4996350, "path" => "delete", "size" => 1),
- array("version" => 4996350, "path" => "delete", "size" => 1),
- array("version" => 4992800, "path" => "keep", "size" => 1),
- array("version" => 4989800, "path" => "delete", "size" => 1),
- array("version" => 4989700, "path" => "delete", "size" => 1),
- array("version" => 4989200, "path" => "keep", "size" => 1),
- // next slice (2592000sec) start at 4913600 keep one version every 86400 seconds
- array("version" => 4913600, "path" => "keep", "size" => 1),
- array("version" => 4852800, "path" => "delete", "size" => 1),
- array("version" => 4827201, "path" => "delete", "size" => 1),
- array("version" => 4827200, "path" => "keep", "size" => 1),
- array("version" => 4777201, "path" => "delete", "size" => 1),
- array("version" => 4777501, "path" => "delete", "size" => 1),
- array("version" => 4740000, "path" => "keep", "size" => 1),
- // final slice starts at 2408000 keep one version every 604800 secons
- array("version" => 2408000, "path" => "keep", "size" => 1),
- array("version" => 1803201, "path" => "delete", "size" => 1),
- array("version" => 1803200, "path" => "keep", "size" => 1),
- array("version" => 1800199, "path" => "delete", "size" => 1),
- array("version" => 1800100, "path" => "delete", "size" => 1),
- array("version" => 1198300, "path" => "keep", "size" => 1),
- ),
- 11 // size of all deleted files (every file has the size 1)
- ),
- // third set of versions, with some gaps inbetween
- array(
- array(
- // first slice (10sec) keep one version every 2 seconds
- array("version" => 4999999, "path" => "keep", "size" => 1),
- array("version" => 4999998, "path" => "delete", "size" => 1),
- array("version" => 4999997, "path" => "keep", "size" => 1),
- array("version" => 4999995, "path" => "keep", "size" => 1),
- array("version" => 4999994, "path" => "delete", "size" => 1),
- //next slice (60sec) starts at 4999990 keep one version every 10 secons
- array("version" => 4999988, "path" => "keep", "size" => 1),
- array("version" => 4999978, "path" => "keep", "size" => 1),
- //next slice (3600sec) start at 4999940 keep one version every 60 seconds
- // next slice (86400sec) start at 4996400 keep one version every 3600 seconds
- array("version" => 4989200, "path" => "keep", "size" => 1),
- // next slice (2592000sec) start at 4913600 keep one version every 86400 seconds
- array("version" => 4913600, "path" => "keep", "size" => 1),
- array("version" => 4852800, "path" => "delete", "size" => 1),
- array("version" => 4827201, "path" => "delete", "size" => 1),
- array("version" => 4827200, "path" => "keep", "size" => 1),
- array("version" => 4777201, "path" => "delete", "size" => 1),
- array("version" => 4777501, "path" => "delete", "size" => 1),
- array("version" => 4740000, "path" => "keep", "size" => 1),
- // final slice starts at 2408000 keep one version every 604800 secons
- array("version" => 2408000, "path" => "keep", "size" => 1),
- array("version" => 1803201, "path" => "delete", "size" => 1),
- array("version" => 1803200, "path" => "keep", "size" => 1),
- array("version" => 1800199, "path" => "delete", "size" => 1),
- array("version" => 1800100, "path" => "delete", "size" => 1),
- array("version" => 1198300, "path" => "keep", "size" => 1),
- ),
- 9 // size of all deleted files (every file has the size 1)
- ),
-
- );
- }
-
- public function testRename() {
-
- \OC\Files\Filesystem::file_put_contents("test.txt", "test file");
-
- $t1 = time();
- // second version is two weeks older, this way we make sure that no
- // version will be expired
- $t2 = $t1 - 60 * 60 * 24 * 14;
-
- // create some versions
- $v1 = self::USERS_VERSIONS_ROOT . '/test.txt.v' . $t1;
- $v2 = self::USERS_VERSIONS_ROOT . '/test.txt.v' . $t2;
- $v1Renamed = self::USERS_VERSIONS_ROOT . '/test2.txt.v' . $t1;
- $v2Renamed = self::USERS_VERSIONS_ROOT . '/test2.txt.v' . $t2;
-
- $this->rootView->file_put_contents($v1, 'version1');
- $this->rootView->file_put_contents($v2, 'version2');
-
- // execute rename hook of versions app
- \OC\Files\Filesystem::rename("test.txt", "test2.txt");
-
- $this->runCommands();
-
- $this->assertFalse($this->rootView->file_exists($v1));
- $this->assertFalse($this->rootView->file_exists($v2));
-
- $this->assertTrue($this->rootView->file_exists($v1Renamed));
- $this->assertTrue($this->rootView->file_exists($v2Renamed));
- }
-
- public function testRenameInSharedFolder() {
-
- \OC\Files\Filesystem::mkdir('folder1');
- \OC\Files\Filesystem::mkdir('folder1/folder2');
- \OC\Files\Filesystem::file_put_contents("folder1/test.txt", "test file");
-
- $fileInfo = \OC\Files\Filesystem::getFileInfo('folder1');
-
- $t1 = time();
- // second version is two weeks older, this way we make sure that no
- // version will be expired
- $t2 = $t1 - 60 * 60 * 24 * 14;
-
- $this->rootView->mkdir(self::USERS_VERSIONS_ROOT . '/folder1');
- // create some versions
- $v1 = self::USERS_VERSIONS_ROOT . '/folder1/test.txt.v' . $t1;
- $v2 = self::USERS_VERSIONS_ROOT . '/folder1/test.txt.v' . $t2;
- $v1Renamed = self::USERS_VERSIONS_ROOT . '/folder1/folder2/test.txt.v' . $t1;
- $v2Renamed = self::USERS_VERSIONS_ROOT . '/folder1/folder2/test.txt.v' . $t2;
-
- $this->rootView->file_put_contents($v1, 'version1');
- $this->rootView->file_put_contents($v2, 'version2');
-
- \OCP\Share::shareItem('folder', $fileInfo['fileid'], \OCP\Share::SHARE_TYPE_USER, self::TEST_VERSIONS_USER2, \OCP\Constants::PERMISSION_ALL);
-
- self::loginHelper(self::TEST_VERSIONS_USER2);
-
- $this->assertTrue(\OC\Files\Filesystem::file_exists('folder1/test.txt'));
-
- // execute rename hook of versions app
- \OC\Files\Filesystem::rename('/folder1/test.txt', '/folder1/folder2/test.txt');
-
- $this->runCommands();
-
- self::loginHelper(self::TEST_VERSIONS_USER);
-
- $this->assertFalse($this->rootView->file_exists($v1));
- $this->assertFalse($this->rootView->file_exists($v2));
-
- $this->assertTrue($this->rootView->file_exists($v1Renamed));
- $this->assertTrue($this->rootView->file_exists($v2Renamed));
- }
-
- public function testMoveFolder() {
-
- \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 two weeks older, this way we make sure that no
- // version will be expired
- $t2 = $t1 - 60 * 60 * 24 * 14;
-
- // create some versions
- $this->rootView->mkdir(self::USERS_VERSIONS_ROOT . '/folder1');
- $v1 = self::USERS_VERSIONS_ROOT . '/folder1/test.txt.v' . $t1;
- $v2 = self::USERS_VERSIONS_ROOT . '/folder1/test.txt.v' . $t2;
- $v1Renamed = self::USERS_VERSIONS_ROOT . '/folder2/folder1/test.txt.v' . $t1;
- $v2Renamed = self::USERS_VERSIONS_ROOT . '/folder2/folder1/test.txt.v' . $t2;
-
- $this->rootView->file_put_contents($v1, 'version1');
- $this->rootView->file_put_contents($v2, 'version2');
-
- // execute rename hook of versions app
- \OC\Files\Filesystem::rename('folder1', 'folder2/folder1');
-
- $this->runCommands();
-
- $this->assertFalse($this->rootView->file_exists($v1));
- $this->assertFalse($this->rootView->file_exists($v2));
-
- $this->assertTrue($this->rootView->file_exists($v1Renamed));
- $this->assertTrue($this->rootView->file_exists($v2Renamed));
- }
-
-
- public function testMoveFileIntoSharedFolderAsRecipient() {
-
- \OC\Files\Filesystem::mkdir('folder1');
- $fileInfo = \OC\Files\Filesystem::getFileInfo('folder1');
-
- \OCP\Share::shareItem(
- 'folder',
- $fileInfo['fileid'],
- \OCP\Share::SHARE_TYPE_USER,
- self::TEST_VERSIONS_USER2,
- \OCP\Constants::PERMISSION_ALL
- );
-
- self::loginHelper(self::TEST_VERSIONS_USER2);
- $versionsFolder2 = '/' . self::TEST_VERSIONS_USER2 . '/files_versions';
- \OC\Files\Filesystem::file_put_contents('test.txt', 'test file');
-
- $t1 = time();
- // second version is two weeks older, this way we make sure that no
- // version will be expired
- $t2 = $t1 - 60 * 60 * 24 * 14;
-
- $this->rootView->mkdir($versionsFolder2);
- // create some versions
- $v1 = $versionsFolder2 . '/test.txt.v' . $t1;
- $v2 = $versionsFolder2 . '/test.txt.v' . $t2;
-
- $this->rootView->file_put_contents($v1, 'version1');
- $this->rootView->file_put_contents($v2, 'version2');
-
- // move file into the shared folder as recipient
- \OC\Files\Filesystem::rename('/test.txt', '/folder1/test.txt');
-
- $this->assertFalse($this->rootView->file_exists($v1));
- $this->assertFalse($this->rootView->file_exists($v2));
-
- self::loginHelper(self::TEST_VERSIONS_USER);
-
- $versionsFolder1 = '/' . self::TEST_VERSIONS_USER . '/files_versions';
-
- $v1Renamed = $versionsFolder1 . '/folder1/test.txt.v' . $t1;
- $v2Renamed = $versionsFolder1 . '/folder1/test.txt.v' . $t2;
-
- $this->assertTrue($this->rootView->file_exists($v1Renamed));
- $this->assertTrue($this->rootView->file_exists($v2Renamed));
- }
-
- public function testMoveFolderIntoSharedFolderAsRecipient() {
-
- \OC\Files\Filesystem::mkdir('folder1');
- $fileInfo = \OC\Files\Filesystem::getFileInfo('folder1');
-
- \OCP\Share::shareItem(
- 'folder',
- $fileInfo['fileid'],
- \OCP\Share::SHARE_TYPE_USER,
- self::TEST_VERSIONS_USER2,
- \OCP\Constants::PERMISSION_ALL
- );
-
- self::loginHelper(self::TEST_VERSIONS_USER2);
- $versionsFolder2 = '/' . self::TEST_VERSIONS_USER2 . '/files_versions';
- \OC\Files\Filesystem::mkdir('folder2');
- \OC\Files\Filesystem::file_put_contents('folder2/test.txt', 'test file');
-
- $t1 = time();
- // second version is two weeks older, this way we make sure that no
- // version will be expired
- $t2 = $t1 - 60 * 60 * 24 * 14;
-
- $this->rootView->mkdir($versionsFolder2);
- $this->rootView->mkdir($versionsFolder2 . '/folder2');
- // create some versions
- $v1 = $versionsFolder2 . '/folder2/test.txt.v' . $t1;
- $v2 = $versionsFolder2 . '/folder2/test.txt.v' . $t2;
-
- $this->rootView->file_put_contents($v1, 'version1');
- $this->rootView->file_put_contents($v2, 'version2');
-
- // move file into the shared folder as recipient
- \OC\Files\Filesystem::rename('/folder2', '/folder1/folder2');
-
- $this->assertFalse($this->rootView->file_exists($v1));
- $this->assertFalse($this->rootView->file_exists($v2));
-
- self::loginHelper(self::TEST_VERSIONS_USER);
-
- $versionsFolder1 = '/' . self::TEST_VERSIONS_USER . '/files_versions';
-
- $v1Renamed = $versionsFolder1 . '/folder1/folder2/test.txt.v' . $t1;
- $v2Renamed = $versionsFolder1 . '/folder1/folder2/test.txt.v' . $t2;
-
- $this->assertTrue($this->rootView->file_exists($v1Renamed));
- $this->assertTrue($this->rootView->file_exists($v2Renamed));
- }
-
- public function testRenameSharedFile() {
-
- \OC\Files\Filesystem::file_put_contents("test.txt", "test file");
-
- $fileInfo = \OC\Files\Filesystem::getFileInfo('test.txt');
-
- $t1 = time();
- // second version is two weeks older, this way we make sure that no
- // version will be expired
- $t2 = $t1 - 60 * 60 * 24 * 14;
-
- $this->rootView->mkdir(self::USERS_VERSIONS_ROOT);
- // create some versions
- $v1 = self::USERS_VERSIONS_ROOT . '/test.txt.v' . $t1;
- $v2 = self::USERS_VERSIONS_ROOT . '/test.txt.v' . $t2;
- // the renamed versions should not exist! Because we only moved the mount point!
- $v1Renamed = self::USERS_VERSIONS_ROOT . '/test2.txt.v' . $t1;
- $v2Renamed = self::USERS_VERSIONS_ROOT . '/test2.txt.v' . $t2;
-
- $this->rootView->file_put_contents($v1, 'version1');
- $this->rootView->file_put_contents($v2, 'version2');
-
- \OCP\Share::shareItem('file', $fileInfo['fileid'], \OCP\Share::SHARE_TYPE_USER, self::TEST_VERSIONS_USER2, \OCP\Constants::PERMISSION_ALL);
-
- self::loginHelper(self::TEST_VERSIONS_USER2);
-
- $this->assertTrue(\OC\Files\Filesystem::file_exists('test.txt'));
-
- // execute rename hook of versions app
- \OC\Files\Filesystem::rename('test.txt', 'test2.txt');
-
- self::loginHelper(self::TEST_VERSIONS_USER);
-
- $this->runCommands();
-
- $this->assertTrue($this->rootView->file_exists($v1));
- $this->assertTrue($this->rootView->file_exists($v2));
-
- $this->assertFalse($this->rootView->file_exists($v1Renamed));
- $this->assertFalse($this->rootView->file_exists($v2Renamed));
- }
-
- public function testCopy() {
-
- \OC\Files\Filesystem::file_put_contents("test.txt", "test file");
-
- $t1 = time();
- // second version is two weeks older, this way we make sure that no
- // version will be expired
- $t2 = $t1 - 60 * 60 * 24 * 14;
-
- // create some versions
- $v1 = self::USERS_VERSIONS_ROOT . '/test.txt.v' . $t1;
- $v2 = self::USERS_VERSIONS_ROOT . '/test.txt.v' . $t2;
- $v1Copied = self::USERS_VERSIONS_ROOT . '/test2.txt.v' . $t1;
- $v2Copied = self::USERS_VERSIONS_ROOT . '/test2.txt.v' . $t2;
-
- $this->rootView->file_put_contents($v1, 'version1');
- $this->rootView->file_put_contents($v2, 'version2');
-
- // execute copy hook of versions app
- \OC\Files\Filesystem::copy("test.txt", "test2.txt");
-
- $this->runCommands();
-
- $this->assertTrue($this->rootView->file_exists($v1));
- $this->assertTrue($this->rootView->file_exists($v2));
-
- $this->assertTrue($this->rootView->file_exists($v1Copied));
- $this->assertTrue($this->rootView->file_exists($v2Copied));
- }
-
- /**
- * test if we find all versions and if the versions array contain
- * the correct 'path' and 'name'
- */
- public function testGetVersions() {
-
- $t1 = time();
- // second version is two weeks older, this way we make sure that no
- // version will be expired
- $t2 = $t1 - 60 * 60 * 24 * 14;
-
- // create some versions
- $v1 = self::USERS_VERSIONS_ROOT . '/subfolder/test.txt.v' . $t1;
- $v2 = self::USERS_VERSIONS_ROOT . '/subfolder/test.txt.v' . $t2;
-
- $this->rootView->mkdir(self::USERS_VERSIONS_ROOT . '/subfolder/');
-
- $this->rootView->file_put_contents($v1, 'version1');
- $this->rootView->file_put_contents($v2, 'version2');
-
- // execute copy hook of versions app
- $versions = \OCA\Files_Versions\Storage::getVersions(self::TEST_VERSIONS_USER, '/subfolder/test.txt');
-
- $this->assertCount(2, $versions);
-
- foreach ($versions as $version) {
- $this->assertSame('/subfolder/test.txt', $version['path']);
- $this->assertSame('test.txt', $version['name']);
- }
-
- //cleanup
- $this->rootView->deleteAll(self::USERS_VERSIONS_ROOT . '/subfolder');
- }
-
- /**
- * test if we find all versions and if the versions array contain
- * the correct 'path' and 'name'
- */
- public function testGetVersionsEmptyFile() {
- // execute copy hook of versions app
- $versions = \OCA\Files_Versions\Storage::getVersions(self::TEST_VERSIONS_USER, '');
- $this->assertCount(0, $versions);
-
- $versions = \OCA\Files_Versions\Storage::getVersions(self::TEST_VERSIONS_USER, null);
- $this->assertCount(0, $versions);
- }
-
- public function testExpireNonexistingFile() {
- $this->logout();
- // needed to have a FS setup (the background job does this)
- \OC_Util::setupFS(self::TEST_VERSIONS_USER);
-
- $this->assertFalse(\OCA\Files_Versions\Storage::expire('/void/unexist.txt'));
- }
-
- public function testRestoreSameStorage() {
- \OC\Files\Filesystem::mkdir('sub');
- $this->doTestRestore();
- }
-
- public function testRestoreCrossStorage() {
- $storage2 = new Temporary(array());
- \OC\Files\Filesystem::mount($storage2, array(), self::TEST_VERSIONS_USER . '/files/sub');
-
- $this->doTestRestore();
- }
-
- /**
- * @param string $hookName name of hook called
- * @param string $params variable to recieve parameters provided by hook
- */
- private function connectMockHooks($hookName, &$params) {
- if ($hookName === null) {
- return;
- }
-
- $eventHandler = $this->getMockBuilder('\stdclass')
- ->setMethods(['callback'])
- ->getMock();
-
- $eventHandler->expects($this->any())
- ->method('callback')
- ->will($this->returnCallback(
- function($p) use (&$params) {
- $params = $p;
- }
- ));
-
- \OCP\Util::connectHook(
- '\OCP\Versions',
- $hookName,
- $eventHandler,
- 'callback'
- );
- }
-
- private function doTestRestore() {
- $filePath = self::TEST_VERSIONS_USER . '/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
- $t1 = time() - 60;
- // second version is two weeks older
- $t2 = $t1 - 60 * 60 * 24 * 14;
-
- // create some versions
- $v1 = self::USERS_VERSIONS_ROOT . '/sub/test.txt.v' . $t1;
- $v2 = self::USERS_VERSIONS_ROOT . '/sub/test.txt.v' . $t2;
-
- $this->rootView->mkdir(self::USERS_VERSIONS_ROOT . '/sub');
- $this->rootView->file_put_contents($v1, 'version1');
- $this->rootView->file_put_contents($v2, 'version2');
-
- $oldVersions = \OCA\Files_Versions\Storage::getVersions(
- self::TEST_VERSIONS_USER, '/sub/test.txt'
- );
-
- $this->assertCount(2, $oldVersions);
-
- $this->assertEquals('test file', $this->rootView->file_get_contents($filePath));
- $info1 = $this->rootView->getFileInfo($filePath);
-
- $params = array();
- $this->connectMockHooks('rollback', $params);
-
- \OCA\Files_Versions\Storage::rollback('sub/test.txt', $t2);
- $expectedParams = array(
- 'path' => '/sub/test.txt',
- );
-
- $this->assertEquals($expectedParams['path'], $params['path']);
- $this->assertTrue(array_key_exists('revision', $params));
- $this->assertTrue($params['revision'] > 0);
-
- $this->assertEquals('version2', $this->rootView->file_get_contents($filePath));
- $info2 = $this->rootView->getFileInfo($filePath);
-
- $this->assertNotEquals(
- $info2['etag'],
- $info1['etag'],
- 'Etag must change after rolling back version'
- );
- $this->assertEquals(
- $info2['fileid'],
- $info1['fileid'],
- 'File id must not change after rolling back version'
- );
- $this->assertEquals(
- $info2['mtime'],
- $t2,
- 'Restored file has mtime from version'
- );
-
- $newVersions = \OCA\Files_Versions\Storage::getVersions(
- self::TEST_VERSIONS_USER, '/sub/test.txt'
- );
-
- $this->assertTrue(
- $this->rootView->file_exists(self::USERS_VERSIONS_ROOT . '/sub/test.txt.v' . $t0),
- 'A version file was created for the file before restoration'
- );
- $this->assertTrue(
- $this->rootView->file_exists($v1),
- 'Untouched version file is still there'
- );
- $this->assertFalse(
- $this->rootView->file_exists($v2),
- 'Restored version file gone from files_version folder'
- );
-
- $this->assertCount(2, $newVersions, 'Additional version created');
-
- $this->assertTrue(
- isset($newVersions[$t0 . '#' . 'test.txt']),
- 'A version was created for the file before restoration'
- );
- $this->assertTrue(
- isset($newVersions[$t1 . '#' . 'test.txt']),
- 'Untouched version is still there'
- );
- $this->assertFalse(
- isset($newVersions[$t2 . '#' . 'test.txt']),
- 'Restored version is not in the list any more'
- );
- }
-
- /**
- * Test whether versions are created when overwriting as owner
- */
- public function testStoreVersionAsOwner() {
- $this->loginAsUser(self::TEST_VERSIONS_USER);
-
- $this->createAndCheckVersions(
- \OC\Files\Filesystem::getView(),
- 'test.txt'
- );
- }
-
- /**
- * Test whether versions are created when overwriting as share recipient
- */
- public function testStoreVersionAsRecipient() {
- $this->loginAsUser(self::TEST_VERSIONS_USER);
-
- \OC\Files\Filesystem::mkdir('folder');
- \OC\Files\Filesystem::file_put_contents('folder/test.txt', 'test file');
- $fileInfo = \OC\Files\Filesystem::getFileInfo('folder');
-
- \OCP\Share::shareItem(
- 'folder',
- $fileInfo['fileid'],
- \OCP\Share::SHARE_TYPE_USER,
- self::TEST_VERSIONS_USER2,
- \OCP\Constants::PERMISSION_ALL
- );
-
- $this->loginAsUser(self::TEST_VERSIONS_USER2);
-
- $this->createAndCheckVersions(
- \OC\Files\Filesystem::getView(),
- 'folder/test.txt'
- );
- }
-
- /**
- * Test whether versions are created when overwriting anonymously.
- *
- * When uploading through a public link or publicwebdav, no user
- * is logged in. File modification must still be able to find
- * the owner and create versions.
- */
- public function testStoreVersionAsAnonymous() {
- $this->logout();
-
- // note: public link upload does this,
- // needed to make the hooks fire
- \OC_Util::setupFS(self::TEST_VERSIONS_USER);
-
- $userView = new \OC\Files\View('/' . self::TEST_VERSIONS_USER . '/files');
- $this->createAndCheckVersions(
- $userView,
- 'test.txt'
- );
- }
-
- /**
- * @param \OC\Files\View $view
- * @param string $path
- */
- private function createAndCheckVersions(\OC\Files\View $view, $path) {
- $view->file_put_contents($path, 'test file');
- $view->file_put_contents($path, 'version 1');
- $view->file_put_contents($path, 'version 2');
-
- $this->loginAsUser(self::TEST_VERSIONS_USER);
-
- // need to scan for the versions
- list($rootStorage,) = $this->rootView->resolvePath(self::TEST_VERSIONS_USER . '/files_versions');
- $rootStorage->getScanner()->scan('files_versions');
-
- $versions = \OCA\Files_Versions\Storage::getVersions(
- self::TEST_VERSIONS_USER, '/' . $path
- );
-
- // note: we cannot predict how many versions are created due to
- // test run timing
- $this->assertGreaterThan(0, count($versions));
- }
-
- /**
- * @param string $user
- * @param bool $create
- */
- public static function loginHelper($user, $create = false) {
-
- if ($create) {
- $backend = new \Test\Util\User\Dummy();
- $backend->createUser($user, $user);
- \OC::$server->getUserManager()->registerBackend($backend);
- }
-
- $storage = new \ReflectionClass('\OC\Files\Storage\Shared');
- $isInitialized = $storage->getProperty('initialized');
- $isInitialized->setAccessible(true);
- $isInitialized->setValue($storage, false);
- $isInitialized->setAccessible(false);
-
- \OC_Util::tearDownFS();
- \OC_User::setUserId('');
- \OC\Files\Filesystem::tearDown();
- \OC_User::setUserId($user);
- \OC_Util::setupFS($user);
- \OC::$server->getUserFolder($user);
- }
-
-}
-
-// extend the original class to make it possible to test protected methods
-class VersionStorageToTest extends \OCA\Files_Versions\Storage {
-
- /**
- * @param integer $time
- */
- public function callProtectedGetExpireList($time, $versions) {
- return self::getExpireList($time, $versions);
-
- }
-}