diff options
155 files changed, 1523 insertions, 406 deletions
diff --git a/apps/dav/appinfo/info.xml b/apps/dav/appinfo/info.xml index 9021ba98a0f..baf1021d3e6 100644 --- a/apps/dav/appinfo/info.xml +++ b/apps/dav/appinfo/info.xml @@ -10,7 +10,7 @@ <name>WebDAV</name> <summary>WebDAV endpoint</summary> <description>WebDAV endpoint</description> - <version>1.34.0</version> + <version>1.34.1</version> <licence>agpl</licence> <author>owncloud.org</author> <namespace>DAV</namespace> diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php index b9708ea5589..9eab0456159 100644 --- a/apps/dav/composer/composer/autoload_classmap.php +++ b/apps/dav/composer/composer/autoload_classmap.php @@ -217,6 +217,7 @@ return array( 'OCA\\DAV\\Connector\\Sabre\\ObjectTree' => $baseDir . '/../lib/Connector/Sabre/ObjectTree.php', 'OCA\\DAV\\Connector\\Sabre\\Principal' => $baseDir . '/../lib/Connector/Sabre/Principal.php', 'OCA\\DAV\\Connector\\Sabre\\PropFindMonitorPlugin' => $baseDir . '/../lib/Connector/Sabre/PropFindMonitorPlugin.php', + 'OCA\\DAV\\Connector\\Sabre\\PropFindPreloadNotifyPlugin' => $baseDir . '/../lib/Connector/Sabre/PropFindPreloadNotifyPlugin.php', 'OCA\\DAV\\Connector\\Sabre\\PropfindCompressionPlugin' => $baseDir . '/../lib/Connector/Sabre/PropfindCompressionPlugin.php', 'OCA\\DAV\\Connector\\Sabre\\PublicAuth' => $baseDir . '/../lib/Connector/Sabre/PublicAuth.php', 'OCA\\DAV\\Connector\\Sabre\\QuotaPlugin' => $baseDir . '/../lib/Connector/Sabre/QuotaPlugin.php', @@ -354,6 +355,7 @@ return array( 'OCA\\DAV\\Migration\\Version1029Date20231004091403' => $baseDir . '/../lib/Migration/Version1029Date20231004091403.php', 'OCA\\DAV\\Migration\\Version1030Date20240205103243' => $baseDir . '/../lib/Migration/Version1030Date20240205103243.php', 'OCA\\DAV\\Migration\\Version1031Date20240610134258' => $baseDir . '/../lib/Migration/Version1031Date20240610134258.php', + 'OCA\\DAV\\Migration\\Version1034Date20250813093701' => $baseDir . '/../lib/Migration/Version1034Date20250813093701.php', 'OCA\\DAV\\Model\\ExampleEvent' => $baseDir . '/../lib/Model/ExampleEvent.php', 'OCA\\DAV\\Paginate\\LimitedCopyIterator' => $baseDir . '/../lib/Paginate/LimitedCopyIterator.php', 'OCA\\DAV\\Paginate\\PaginateCache' => $baseDir . '/../lib/Paginate/PaginateCache.php', diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php index 75ac3350160..e9a0ef01c07 100644 --- a/apps/dav/composer/composer/autoload_static.php +++ b/apps/dav/composer/composer/autoload_static.php @@ -232,6 +232,7 @@ class ComposerStaticInitDAV 'OCA\\DAV\\Connector\\Sabre\\ObjectTree' => __DIR__ . '/..' . '/../lib/Connector/Sabre/ObjectTree.php', 'OCA\\DAV\\Connector\\Sabre\\Principal' => __DIR__ . '/..' . '/../lib/Connector/Sabre/Principal.php', 'OCA\\DAV\\Connector\\Sabre\\PropFindMonitorPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/PropFindMonitorPlugin.php', + 'OCA\\DAV\\Connector\\Sabre\\PropFindPreloadNotifyPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/PropFindPreloadNotifyPlugin.php', 'OCA\\DAV\\Connector\\Sabre\\PropfindCompressionPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/PropfindCompressionPlugin.php', 'OCA\\DAV\\Connector\\Sabre\\PublicAuth' => __DIR__ . '/..' . '/../lib/Connector/Sabre/PublicAuth.php', 'OCA\\DAV\\Connector\\Sabre\\QuotaPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/QuotaPlugin.php', @@ -369,6 +370,7 @@ class ComposerStaticInitDAV 'OCA\\DAV\\Migration\\Version1029Date20231004091403' => __DIR__ . '/..' . '/../lib/Migration/Version1029Date20231004091403.php', 'OCA\\DAV\\Migration\\Version1030Date20240205103243' => __DIR__ . '/..' . '/../lib/Migration/Version1030Date20240205103243.php', 'OCA\\DAV\\Migration\\Version1031Date20240610134258' => __DIR__ . '/..' . '/../lib/Migration/Version1031Date20240610134258.php', + 'OCA\\DAV\\Migration\\Version1034Date20250813093701' => __DIR__ . '/..' . '/../lib/Migration/Version1034Date20250813093701.php', 'OCA\\DAV\\Model\\ExampleEvent' => __DIR__ . '/..' . '/../lib/Model/ExampleEvent.php', 'OCA\\DAV\\Paginate\\LimitedCopyIterator' => __DIR__ . '/..' . '/../lib/Paginate/LimitedCopyIterator.php', 'OCA\\DAV\\Paginate\\PaginateCache' => __DIR__ . '/..' . '/../lib/Paginate/PaginateCache.php', diff --git a/apps/dav/l10n/pl.js b/apps/dav/l10n/pl.js index 3c9929cc943..8d72b6d7632 100644 --- a/apps/dav/l10n/pl.js +++ b/apps/dav/l10n/pl.js @@ -79,6 +79,9 @@ OC.L10N.register( "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["Za tydzień, dnia %1$s, przez cały dzień","Za %n tygodnie, dnia %1$s, przez cały dzień","Za %n tygodni, dnia %1$s, przez cały dzień","Za %n tygodni, dnia %1$s, przez cały dzień"], "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["W ciągu miesiąca, dnia %1$s przez cały dzień","W ciągu %n miesięcy, dnia %1$s przez cały dzień","W ciągu %n miesięcy, dnia %1$s przez cały dzień","W ciągu %n miesięcy, dnia %1$s przez cały dzień"], "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["Za rok dnia %1$s przez cały dzień","Za %n lata dnia %1$s przez cały dzień","Za %n lat dnia %1$s przez cały dzień","Za %n lat dnia %1$s przez cały dzień"], + "In the past on %1$s between %2$s - %3$s" : "W przeszłości dnia %1$s między %2$s - %3$s", + "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["Za minutę dnia %1$s między %2$s - %3$s","Za %n minuty dnia %1$s między %2$s - %3$s","Za %n minut dnia %1$s między %2$s - %3$s","Za %n minut dnia %1$s między %2$s - %3$s"], + "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["Za godzinę dnia %1$s między %2$s - %3$s","Za %n godziny dnia %1$s między %2$s - %3$s","Za %n godzin dnia %1$s między %2$s - %3$s","Za %n godzin dnia %1$s między %2$s - %3$s"], "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["Za dzień, dnia %1$s między %2$s - %3$s","Za %n dni dnia %1$s między %2$s - %3$s","Za %n dni dnia %1$s między %2$s - %3$s","Za %n dni dnia %1$s między %2$s - %3$s"], "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["Za tydzień dnia %1$s między %2$s - %3$s","Za %n tygodnie dnia %1$s między %2$s - %3$s","Za %n tygodni dnia %1$s między %2$s - %3$s","Za %n tygodni dnia %1$s między %2$s - %3$s"], "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["Za miesiąc dnia %1$s między %2$s - %3$s","Za %n miesięcy dnia %1$s między %2$s - %3$s","Za %n miesięcy dnia %1$s między %2$s - %3$s","Za %n miesięcy dnia %1$s między %2$s - %3$s"], @@ -87,18 +90,38 @@ OC.L10N.register( "Every Day for the entire day" : "Codziennie przez cały dzień", "Every Day for the entire day until %1$s" : "Codziennie przez cały dzień do %1$s", "Every Day between %1$s - %2$s" : "Codziennie między %1$s – %2$s", + "Every Day between %1$s - %2$s until %3$s" : "Codziennie między %1$s - %2$s do %3$s", "Every %1$d Days for the entire day" : "Co %1$d dni przez cały dzień", "Every %1$d Days for the entire day until %2$s" : "Co %1$d dni przez cały dzień aż do %2$s", "Every %1$d Days between %2$s - %3$s" : "Co %1$d dni pomiędzy %2$s - %3$s", "Every %1$d Days between %2$s - %3$s until %4$s" : "Co %1$d dni, pomiędzy %2$s - %3$s aż do %4$s", "Could not generate event recurrence statement" : "Nie można wygenerować zestawienia powtórzeń zdarzenia", "Every Week on %1$s for the entire day" : "Każdego tygodnia w %1$s przez cały dzień", + "Every Week on %1$s for the entire day until %2$s" : "Co tydzień w %1$s przez cały dzień do %2$s", + "Every Week on %1$s between %2$s - %3$s" : "Co tydzień w %1$s między %2$s - %3$s", + "Every Week on %1$s between %2$s - %3$s until %4$s" : "Co tydzień w %1$s między %2$s - %3$s do %4$s", + "Every %1$d Weeks on %2$s for the entire day" : "Co %1$d tygodni w %2$s przez cały dzień", + "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Co %1$d tygodnie w %2$s przez cały dzień do %3$s", + "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Co %1$d tygodnie w %2$s między %3$s - %4$s", + "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Co %1$d tygodnie w %2$s między %3$s - %4$s do %5$s", + "Every Month on the %1$s for the entire day" : "Co miesiąc dnia %1$s przez cały dzień", + "Every Month on the %1$s for the entire day until %2$s" : "Co miesiąc dnia %1$s przez cały dzień do %2$s", + "Every Month on the %1$s between %2$s - %3$s" : "Co miesiąc dnia %1$s między %2$s - %3$s", + "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Co miesiąc dnia %1$s między %2$s - %3$s do %4$s", + "Every %1$d Months on the %2$s for the entire day" : "Co %1$d miesiący dnia %2$s przez cały dzień", + "Every %1$d Months on the %2$s for the entire day until %3$s" : "Co %1$d miesięcy dnia %2$s przez cały dzień do %3$s", + "Every %1$d Months on the %2$s between %3$s - %4$s" : "Co %1$d miesięcy dnia %2$s między %3$s - %4$s", "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Każdego %1$d miesiąca dnia %2$s między %3$s - %4$s do %5$s", "Every Year in %1$s on the %2$s for the entire day" : "Co rok w %1$s dnia %2$s przez cały dzień", "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Co rok w %1$s dnia %2$s przez cały dzień do %3$s", "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Co roku za %1$s dnia %2$s między %3$s - %4$s", "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Co roku za %1$s dnia %2$s między %3$s - %4$s do %5$s", "Every %1$d Years in %2$s on the %3$s for the entire day" : "Co %1$d lat dnia %2$s o %3$s przez cały dzień", + "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "Co %1$d lat w %2$s dnia %3$s przez cały dzień do %4$s", + "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "Co %1$d lat w %2$s dnia %3$s między %4$s - %5$s", + "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "Co %1$d lat w %2$s dnia %3$s między %4$s - %5$s do %6$s", + "On specific dates for the entire day until %1$s" : "W określonych datach przez cały dzień do %1$s", + "On specific dates between %1$s - %2$s until %3$s" : "W określonych datach między %1$s - %2$s do %3$s", "In the past on %1$s" : "W przeszłości dnia %1$s", "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["Za minutę dnia %1$s","Za %n minut dnia %1$s","Za %n minut dnia %1$s","Za %n minut dnia %1$s"], "_In a hour on %1$s_::_In %n hours on %1$s_" : ["Za godzinę dnia %1$s","Za %n godziny dnia %1$s","Za %n godzin dnia %1$s","Za %n godzin dnia %1$s"], @@ -108,6 +131,18 @@ OC.L10N.register( "_In a year on %1$s_::_In %n years on %1$s_" : ["Za rok dnia %1$s","Za %n lat dnia %1$s","Za %n lat dnia %1$s","Za %n lat dnia %1$s"], "In the past on %1$s then on %2$s" : "W przeszłości dnia %1$s, a następnie %2$s", "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["Za minutę dnia %1$s, a następnie %2$s","Za %n minuty dnia %1$s, a następnie %2$s","Za %n minut dnia %1$s, a następnie %2$s","Za %n minut dnia %1$s, a następnie %2$s"], + "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["Za godzinę dnia %1$s, następnie dnia %2$s","Za %n godziny dnia %1$s, następnie dnia %2$s","Za %n godzin dnia %1$s, następnie dnia %2$s","Za %n godzin dnia %1$s, następnie dnia %2$s"], + "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["Jutro dnia %1$s, następnie dnia %2$s","Za %n dni dnia %1$s, następnie dnia %2$s","Za %n dni dnia %1$s, następnie dnia %2$s","Za %n dni dnia %1$s, następnie dnia %2$s"], + "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["Za tydzień dnia %1$s, następnie dnia %2$s","Za %n tygodnie dnia %1$s, następnie dnia %2$s","Za %n tygodni dnia %1$s, następnie dnia %2$s","Za %n tygodni dnia %1$s, następnie dnia %2$s"], + "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["Za miesiąc dnia %1$s, następnie dnia %2$s","Za %n miesiące dnia %1$s, następnie dnia %2$s","Za %n miesięcy dnia %1$s, następnie dnia %2$s","Za %n miesięcy dnia %1$s, następnie dnia %2$s"], + "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["Za rok dnia %1$s, następnie dnia %2$s","Za %n lata dnia %1$s, następnie dnia %2$s","Za %n lat dnia %1$s, następnie dnia %2$s","Za %n lat dnia %1$s, następnie dnia %2$s"], + "In the past on %1$s then on %2$s and %3$s" : "W przeszłości dnia %1$s, następnie dnia %2$s i %3$s", + "_In a minute on %1$s then on %2$s and %3$s_::_In %n minutes on %1$s then on %2$s and %3$s_" : ["Za minutę dnia %1$s, następnie dnia %2$s i %3$s","Za %n minuty dnia %1$s, następnie dnia %2$s i %3$s","Za %n minut dnia %1$s, następnie dnia %2$s i %3$s","Za %n minut dnia %1$s, następnie dnia %2$s i %3$s"], + "_In a hour on %1$s then on %2$s and %3$s_::_In %n hours on %1$s then on %2$s and %3$s_" : ["Za godzinę dnia %1$s, następnie dnia %2$s i %3$s","Za %%ngodziny dnia %1$s, następnie dnia %2$s i %3$s","Za %n godzin dnia %1$s, następnie dnia %2$s i %3$s","Za %n godzin dnia %1$s, następnie dnia %2$s i %3$s"], + "_In a day on %1$s then on %2$s and %3$s_::_In %n days on %1$s then on %2$s and %3$s_" : ["Jutro, dnia %1$s, następnie dnia %2$s i %3$s","Za %n dni dnia %1$s, następnie dnia %2$s i %3$s","Za %n dni dnia %1$s, następnie dnia %2$s i %3$s","Za %n dni dnia %1$s, następnie dnia %2$s i %3$s"], + "_In a week on %1$s then on %2$s and %3$s_::_In %n weeks on %1$s then on %2$s and %3$s_" : ["Za tydzień dnia %1$s, następnie dnia %2$s i %3$s","Za %n tygodnie dnia %1$s, następnie dnia %2$s i %3$s","Za %n tygodni dnia %1$s, następnie dnia %2$s i %3$s","Za %n tygodni dnia %1$s, następnie dnia %2$s i %3$s"], + "_In a month on %1$s then on %2$s and %3$s_::_In %n months on %1$s then on %2$s and %3$s_" : ["Za miesiąc dnia %1$s, następnie dnia %2$s i %3$s","Za %n miesiące dnia %1$s, następnie dnia %2$s i %3$s","Za %n miesięcy dnia %1$s, następnie dnia %2$s i %3$s","Za %n miesięcy dnia %1$s, następnie dnia %2$s i %3$s"], + "_In a year on %1$s then on %2$s and %3$s_::_In %n years on %1$s then on %2$s and %3$s_" : ["Za rok dnia %1$s, następnie dnia %2$s i %3$s","Za %n lata dnia %1$s, następnie dnia %2$s i %3$s","Za %n lat dnia %1$s, następnie dnia %2$s i %3$s","Za %n lat dnia %1$s, następnie dnia %2$s i %3$s"], "Could not generate next recurrence statement" : "Nie można wygenerować następnej instrukcji powtarzania", "Cancelled: %1$s" : "Anulowane: %1$s", "\"%1$s\" has been canceled" : "\"%1$s\" zostało anulowane", diff --git a/apps/dav/l10n/pl.json b/apps/dav/l10n/pl.json index 4b2ffc40dc0..242c36b857a 100644 --- a/apps/dav/l10n/pl.json +++ b/apps/dav/l10n/pl.json @@ -77,6 +77,9 @@ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["Za tydzień, dnia %1$s, przez cały dzień","Za %n tygodnie, dnia %1$s, przez cały dzień","Za %n tygodni, dnia %1$s, przez cały dzień","Za %n tygodni, dnia %1$s, przez cały dzień"], "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["W ciągu miesiąca, dnia %1$s przez cały dzień","W ciągu %n miesięcy, dnia %1$s przez cały dzień","W ciągu %n miesięcy, dnia %1$s przez cały dzień","W ciągu %n miesięcy, dnia %1$s przez cały dzień"], "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["Za rok dnia %1$s przez cały dzień","Za %n lata dnia %1$s przez cały dzień","Za %n lat dnia %1$s przez cały dzień","Za %n lat dnia %1$s przez cały dzień"], + "In the past on %1$s between %2$s - %3$s" : "W przeszłości dnia %1$s między %2$s - %3$s", + "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["Za minutę dnia %1$s między %2$s - %3$s","Za %n minuty dnia %1$s między %2$s - %3$s","Za %n minut dnia %1$s między %2$s - %3$s","Za %n minut dnia %1$s między %2$s - %3$s"], + "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["Za godzinę dnia %1$s między %2$s - %3$s","Za %n godziny dnia %1$s między %2$s - %3$s","Za %n godzin dnia %1$s między %2$s - %3$s","Za %n godzin dnia %1$s między %2$s - %3$s"], "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["Za dzień, dnia %1$s między %2$s - %3$s","Za %n dni dnia %1$s między %2$s - %3$s","Za %n dni dnia %1$s między %2$s - %3$s","Za %n dni dnia %1$s między %2$s - %3$s"], "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["Za tydzień dnia %1$s między %2$s - %3$s","Za %n tygodnie dnia %1$s między %2$s - %3$s","Za %n tygodni dnia %1$s między %2$s - %3$s","Za %n tygodni dnia %1$s między %2$s - %3$s"], "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["Za miesiąc dnia %1$s między %2$s - %3$s","Za %n miesięcy dnia %1$s między %2$s - %3$s","Za %n miesięcy dnia %1$s między %2$s - %3$s","Za %n miesięcy dnia %1$s między %2$s - %3$s"], @@ -85,18 +88,38 @@ "Every Day for the entire day" : "Codziennie przez cały dzień", "Every Day for the entire day until %1$s" : "Codziennie przez cały dzień do %1$s", "Every Day between %1$s - %2$s" : "Codziennie między %1$s – %2$s", + "Every Day between %1$s - %2$s until %3$s" : "Codziennie między %1$s - %2$s do %3$s", "Every %1$d Days for the entire day" : "Co %1$d dni przez cały dzień", "Every %1$d Days for the entire day until %2$s" : "Co %1$d dni przez cały dzień aż do %2$s", "Every %1$d Days between %2$s - %3$s" : "Co %1$d dni pomiędzy %2$s - %3$s", "Every %1$d Days between %2$s - %3$s until %4$s" : "Co %1$d dni, pomiędzy %2$s - %3$s aż do %4$s", "Could not generate event recurrence statement" : "Nie można wygenerować zestawienia powtórzeń zdarzenia", "Every Week on %1$s for the entire day" : "Każdego tygodnia w %1$s przez cały dzień", + "Every Week on %1$s for the entire day until %2$s" : "Co tydzień w %1$s przez cały dzień do %2$s", + "Every Week on %1$s between %2$s - %3$s" : "Co tydzień w %1$s między %2$s - %3$s", + "Every Week on %1$s between %2$s - %3$s until %4$s" : "Co tydzień w %1$s między %2$s - %3$s do %4$s", + "Every %1$d Weeks on %2$s for the entire day" : "Co %1$d tygodni w %2$s przez cały dzień", + "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Co %1$d tygodnie w %2$s przez cały dzień do %3$s", + "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Co %1$d tygodnie w %2$s między %3$s - %4$s", + "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Co %1$d tygodnie w %2$s między %3$s - %4$s do %5$s", + "Every Month on the %1$s for the entire day" : "Co miesiąc dnia %1$s przez cały dzień", + "Every Month on the %1$s for the entire day until %2$s" : "Co miesiąc dnia %1$s przez cały dzień do %2$s", + "Every Month on the %1$s between %2$s - %3$s" : "Co miesiąc dnia %1$s między %2$s - %3$s", + "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Co miesiąc dnia %1$s między %2$s - %3$s do %4$s", + "Every %1$d Months on the %2$s for the entire day" : "Co %1$d miesiący dnia %2$s przez cały dzień", + "Every %1$d Months on the %2$s for the entire day until %3$s" : "Co %1$d miesięcy dnia %2$s przez cały dzień do %3$s", + "Every %1$d Months on the %2$s between %3$s - %4$s" : "Co %1$d miesięcy dnia %2$s między %3$s - %4$s", "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Każdego %1$d miesiąca dnia %2$s między %3$s - %4$s do %5$s", "Every Year in %1$s on the %2$s for the entire day" : "Co rok w %1$s dnia %2$s przez cały dzień", "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Co rok w %1$s dnia %2$s przez cały dzień do %3$s", "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Co roku za %1$s dnia %2$s między %3$s - %4$s", "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Co roku za %1$s dnia %2$s między %3$s - %4$s do %5$s", "Every %1$d Years in %2$s on the %3$s for the entire day" : "Co %1$d lat dnia %2$s o %3$s przez cały dzień", + "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "Co %1$d lat w %2$s dnia %3$s przez cały dzień do %4$s", + "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "Co %1$d lat w %2$s dnia %3$s między %4$s - %5$s", + "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "Co %1$d lat w %2$s dnia %3$s między %4$s - %5$s do %6$s", + "On specific dates for the entire day until %1$s" : "W określonych datach przez cały dzień do %1$s", + "On specific dates between %1$s - %2$s until %3$s" : "W określonych datach między %1$s - %2$s do %3$s", "In the past on %1$s" : "W przeszłości dnia %1$s", "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["Za minutę dnia %1$s","Za %n minut dnia %1$s","Za %n minut dnia %1$s","Za %n minut dnia %1$s"], "_In a hour on %1$s_::_In %n hours on %1$s_" : ["Za godzinę dnia %1$s","Za %n godziny dnia %1$s","Za %n godzin dnia %1$s","Za %n godzin dnia %1$s"], @@ -106,6 +129,18 @@ "_In a year on %1$s_::_In %n years on %1$s_" : ["Za rok dnia %1$s","Za %n lat dnia %1$s","Za %n lat dnia %1$s","Za %n lat dnia %1$s"], "In the past on %1$s then on %2$s" : "W przeszłości dnia %1$s, a następnie %2$s", "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["Za minutę dnia %1$s, a następnie %2$s","Za %n minuty dnia %1$s, a następnie %2$s","Za %n minut dnia %1$s, a następnie %2$s","Za %n minut dnia %1$s, a następnie %2$s"], + "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["Za godzinę dnia %1$s, następnie dnia %2$s","Za %n godziny dnia %1$s, następnie dnia %2$s","Za %n godzin dnia %1$s, następnie dnia %2$s","Za %n godzin dnia %1$s, następnie dnia %2$s"], + "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["Jutro dnia %1$s, następnie dnia %2$s","Za %n dni dnia %1$s, następnie dnia %2$s","Za %n dni dnia %1$s, następnie dnia %2$s","Za %n dni dnia %1$s, następnie dnia %2$s"], + "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["Za tydzień dnia %1$s, następnie dnia %2$s","Za %n tygodnie dnia %1$s, następnie dnia %2$s","Za %n tygodni dnia %1$s, następnie dnia %2$s","Za %n tygodni dnia %1$s, następnie dnia %2$s"], + "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["Za miesiąc dnia %1$s, następnie dnia %2$s","Za %n miesiące dnia %1$s, następnie dnia %2$s","Za %n miesięcy dnia %1$s, następnie dnia %2$s","Za %n miesięcy dnia %1$s, następnie dnia %2$s"], + "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["Za rok dnia %1$s, następnie dnia %2$s","Za %n lata dnia %1$s, następnie dnia %2$s","Za %n lat dnia %1$s, następnie dnia %2$s","Za %n lat dnia %1$s, następnie dnia %2$s"], + "In the past on %1$s then on %2$s and %3$s" : "W przeszłości dnia %1$s, następnie dnia %2$s i %3$s", + "_In a minute on %1$s then on %2$s and %3$s_::_In %n minutes on %1$s then on %2$s and %3$s_" : ["Za minutę dnia %1$s, następnie dnia %2$s i %3$s","Za %n minuty dnia %1$s, następnie dnia %2$s i %3$s","Za %n minut dnia %1$s, następnie dnia %2$s i %3$s","Za %n minut dnia %1$s, następnie dnia %2$s i %3$s"], + "_In a hour on %1$s then on %2$s and %3$s_::_In %n hours on %1$s then on %2$s and %3$s_" : ["Za godzinę dnia %1$s, następnie dnia %2$s i %3$s","Za %%ngodziny dnia %1$s, następnie dnia %2$s i %3$s","Za %n godzin dnia %1$s, następnie dnia %2$s i %3$s","Za %n godzin dnia %1$s, następnie dnia %2$s i %3$s"], + "_In a day on %1$s then on %2$s and %3$s_::_In %n days on %1$s then on %2$s and %3$s_" : ["Jutro, dnia %1$s, następnie dnia %2$s i %3$s","Za %n dni dnia %1$s, następnie dnia %2$s i %3$s","Za %n dni dnia %1$s, następnie dnia %2$s i %3$s","Za %n dni dnia %1$s, następnie dnia %2$s i %3$s"], + "_In a week on %1$s then on %2$s and %3$s_::_In %n weeks on %1$s then on %2$s and %3$s_" : ["Za tydzień dnia %1$s, następnie dnia %2$s i %3$s","Za %n tygodnie dnia %1$s, następnie dnia %2$s i %3$s","Za %n tygodni dnia %1$s, następnie dnia %2$s i %3$s","Za %n tygodni dnia %1$s, następnie dnia %2$s i %3$s"], + "_In a month on %1$s then on %2$s and %3$s_::_In %n months on %1$s then on %2$s and %3$s_" : ["Za miesiąc dnia %1$s, następnie dnia %2$s i %3$s","Za %n miesiące dnia %1$s, następnie dnia %2$s i %3$s","Za %n miesięcy dnia %1$s, następnie dnia %2$s i %3$s","Za %n miesięcy dnia %1$s, następnie dnia %2$s i %3$s"], + "_In a year on %1$s then on %2$s and %3$s_::_In %n years on %1$s then on %2$s and %3$s_" : ["Za rok dnia %1$s, następnie dnia %2$s i %3$s","Za %n lata dnia %1$s, następnie dnia %2$s i %3$s","Za %n lat dnia %1$s, następnie dnia %2$s i %3$s","Za %n lat dnia %1$s, następnie dnia %2$s i %3$s"], "Could not generate next recurrence statement" : "Nie można wygenerować następnej instrukcji powtarzania", "Cancelled: %1$s" : "Anulowane: %1$s", "\"%1$s\" has been canceled" : "\"%1$s\" zostało anulowane", diff --git a/apps/dav/lib/CalDAV/Calendar.php b/apps/dav/lib/CalDAV/Calendar.php index dd3a4cf3f69..deb00caa93d 100644 --- a/apps/dav/lib/CalDAV/Calendar.php +++ b/apps/dav/lib/CalDAV/Calendar.php @@ -36,7 +36,7 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable public function __construct( BackendInterface $caldavBackend, - $calendarInfo, + array $calendarInfo, IL10N $l10n, private IConfig $config, private LoggerInterface $logger, @@ -60,6 +60,10 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable $this->l10n = $l10n; } + public function getUri(): string { + return $this->calendarInfo['uri']; + } + /** * {@inheritdoc} * @throws Forbidden diff --git a/apps/dav/lib/CalDAV/CalendarProvider.php b/apps/dav/lib/CalDAV/CalendarProvider.php index 3cc4039ed36..a8b818e59aa 100644 --- a/apps/dav/lib/CalDAV/CalendarProvider.php +++ b/apps/dav/lib/CalDAV/CalendarProvider.php @@ -36,9 +36,14 @@ class CalendarProvider implements ICalendarProvider { }); } + $additionalProperties = $this->getAdditionalPropertiesForCalendars($calendarInfos); $iCalendars = []; foreach ($calendarInfos as $calendarInfo) { - $calendarInfo = array_merge($calendarInfo, $this->getAdditionalProperties($calendarInfo['principaluri'], $calendarInfo['uri'])); + $user = str_replace('principals/users/', '', $calendarInfo['principaluri']); + $path = 'calendars/' . $user . '/' . $calendarInfo['uri']; + + $calendarInfo = array_merge($calendarInfo, $additionalProperties[$path] ?? []); + $calendar = new Calendar($this->calDavBackend, $calendarInfo, $this->l10n, $this->config, $this->logger); $iCalendars[] = new CalendarImpl( $calendar, @@ -49,16 +54,34 @@ class CalendarProvider implements ICalendarProvider { return $iCalendars; } - public function getAdditionalProperties(string $principalUri, string $calendarUri): array { - $user = str_replace('principals/users/', '', $principalUri); - $path = 'calendars/' . $user . '/' . $calendarUri; + /** + * @param array{ + * principaluri: string, + * uri: string, + * }[] $uris + * @return array<string, array<string, string|bool>> + */ + private function getAdditionalPropertiesForCalendars(array $uris): array { + $calendars = []; + foreach ($uris as $uri) { + /** @var string $user */ + $user = str_replace('principals/users/', '', $uri['principaluri']); + if (!array_key_exists($user, $calendars)) { + $calendars[$user] = []; + } + $calendars[$user][] = 'calendars/' . $user . '/' . $uri['uri']; + } - $properties = $this->propertyMapper->findPropertiesByPath($user, $path); + $properties = $this->propertyMapper->findPropertiesByPathsAndUsers($calendars); $list = []; foreach ($properties as $property) { if ($property instanceof Property) { - $list[$property->getPropertyname()] = match ($property->getPropertyname()) { + if (!isset($list[$property->getPropertypath()])) { + $list[$property->getPropertypath()] = []; + } + + $list[$property->getPropertypath()][$property->getPropertyname()] = match ($property->getPropertyname()) { '{http://owncloud.org/ns}calendar-enabled' => (bool)$property->getPropertyvalue(), default => $property->getPropertyvalue() }; diff --git a/apps/dav/lib/CalDAV/EmbeddedCalDavServer.php b/apps/dav/lib/CalDAV/EmbeddedCalDavServer.php index 21d8c06fa99..d9d6d840c5e 100644 --- a/apps/dav/lib/CalDAV/EmbeddedCalDavServer.php +++ b/apps/dav/lib/CalDAV/EmbeddedCalDavServer.php @@ -20,6 +20,7 @@ use OCA\DAV\Connector\Sabre\DavAclPlugin; use OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin; use OCA\DAV\Connector\Sabre\LockPlugin; use OCA\DAV\Connector\Sabre\MaintenancePlugin; +use OCA\DAV\Connector\Sabre\PropFindPreloadNotifyPlugin; use OCA\DAV\Events\SabrePluginAuthInitEvent; use OCA\DAV\RootCollection; use OCA\Theming\ThemingDefaults; @@ -96,6 +97,9 @@ class EmbeddedCalDavServer { $this->server->addPlugin(Server::get(\OCA\DAV\CalDAV\Schedule\IMipPlugin::class)); } + // collection preload plugin + $this->server->addPlugin(new PropFindPreloadNotifyPlugin()); + // wait with registering these until auth is handled and the filesystem is setup $this->server->on('beforeMethod:*', function () use ($root): void { // register plugins from apps diff --git a/apps/dav/lib/Connector/Sabre/CommentPropertiesPlugin.php b/apps/dav/lib/Connector/Sabre/CommentPropertiesPlugin.php index e4b6c2636da..ef9bd1ae472 100644 --- a/apps/dav/lib/Connector/Sabre/CommentPropertiesPlugin.php +++ b/apps/dav/lib/Connector/Sabre/CommentPropertiesPlugin.php @@ -10,6 +10,7 @@ namespace OCA\DAV\Connector\Sabre; use OCP\Comments\ICommentsManager; use OCP\IUserSession; +use Sabre\DAV\ICollection; use Sabre\DAV\PropFind; use Sabre\DAV\Server; use Sabre\DAV\ServerPlugin; @@ -21,6 +22,7 @@ class CommentPropertiesPlugin extends ServerPlugin { protected ?Server $server = null; private array $cachedUnreadCount = []; + private array $cachedDirectories = []; public function __construct( private ICommentsManager $commentsManager, @@ -41,6 +43,8 @@ class CommentPropertiesPlugin extends ServerPlugin { */ public function initialize(\Sabre\DAV\Server $server) { $this->server = $server; + + $this->server->on('preloadCollection', $this->preloadCollection(...)); $this->server->on('propFind', [$this, 'handleGetProperties']); } @@ -69,6 +73,21 @@ class CommentPropertiesPlugin extends ServerPlugin { } } + private function preloadCollection(PropFind $propFind, ICollection $collection): + void { + if (!($collection instanceof Directory)) { + return; + } + + $collectionPath = $collection->getPath(); + if (!isset($this->cachedDirectories[$collectionPath]) && $propFind->getStatus( + self::PROPERTY_NAME_UNREAD + ) !== null) { + $this->cacheDirectory($collection); + $this->cachedDirectories[$collectionPath] = true; + } + } + /** * Adds tags and favorites properties to the response, * if requested. @@ -85,14 +104,6 @@ class CommentPropertiesPlugin extends ServerPlugin { return; } - // need prefetch ? - if ($node instanceof Directory - && $propFind->getDepth() !== 0 - && !is_null($propFind->getStatus(self::PROPERTY_NAME_UNREAD)) - ) { - $this->cacheDirectory($node); - } - $propFind->handle(self::PROPERTY_NAME_COUNT, function () use ($node): int { return $this->commentsManager->getNumberOfCommentsForObject('files', (string)$node->getId()); }); diff --git a/apps/dav/lib/Connector/Sabre/PropFindMonitorPlugin.php b/apps/dav/lib/Connector/Sabre/PropFindMonitorPlugin.php index 130d4562146..38538fdcff0 100644 --- a/apps/dav/lib/Connector/Sabre/PropFindMonitorPlugin.php +++ b/apps/dav/lib/Connector/Sabre/PropFindMonitorPlugin.php @@ -48,30 +48,34 @@ class PropFindMonitorPlugin extends ServerPlugin { if (empty($pluginQueries)) { return; } - $maxDepth = max(0, ...array_keys($pluginQueries)); - // entries at the top are usually not interesting - unset($pluginQueries[$maxDepth]); $logger = $this->server->getLogger(); - foreach ($pluginQueries as $depth => $propFinds) { - foreach ($propFinds as $pluginName => $propFind) { - [ - 'queries' => $queries, - 'nodes' => $nodes - ] = $propFind; - if ($queries === 0 || $nodes > $queries || $nodes < self::THRESHOLD_NODES - || $queries < $nodes * self::THRESHOLD_QUERY_FACTOR) { - continue; + foreach ($pluginQueries as $eventName => $eventQueries) { + $maxDepth = max(0, ...array_keys($eventQueries)); + // entries at the top are usually not interesting + unset($eventQueries[$maxDepth]); + foreach ($eventQueries as $depth => $propFinds) { + foreach ($propFinds as $pluginName => $propFind) { + [ + 'queries' => $queries, + 'nodes' => $nodes + ] = $propFind; + if ($queries === 0 || $nodes > $queries || $nodes < self::THRESHOLD_NODES + || $queries < $nodes * self::THRESHOLD_QUERY_FACTOR) { + continue; + } + $logger->error( + '{name}:{event} scanned {scans} nodes with {count} queries in depth {depth}/{maxDepth}. This is bad for performance, please report to the plugin developer!', + [ + 'name' => $pluginName, + 'scans' => $nodes, + 'count' => $queries, + 'depth' => $depth, + 'maxDepth' => $maxDepth, + 'event' => $eventName, + ] + ); } - $logger->error( - '{name} scanned {scans} nodes with {count} queries in depth {depth}/{maxDepth}. This is bad for performance, please report to the plugin developer!', [ - 'name' => $pluginName, - 'scans' => $nodes, - 'count' => $queries, - 'depth' => $depth, - 'maxDepth' => $maxDepth, - ] - ); } } } diff --git a/apps/dav/lib/Connector/Sabre/PropFindPreloadNotifyPlugin.php b/apps/dav/lib/Connector/Sabre/PropFindPreloadNotifyPlugin.php new file mode 100644 index 00000000000..c7b0c64132c --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/PropFindPreloadNotifyPlugin.php @@ -0,0 +1,55 @@ +<?php + +declare(strict_types = 1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Connector\Sabre; + +use Sabre\DAV\ICollection; +use Sabre\DAV\INode; +use Sabre\DAV\PropFind; +use Sabre\DAV\Server; +use Sabre\DAV\ServerPlugin; + +/** + * This plugin asks other plugins to preload data for a collection, so that + * subsequent PROPFIND handlers for children do not query the DB on a per-node + * basis. + */ +class PropFindPreloadNotifyPlugin extends ServerPlugin { + + private Server $server; + + public function initialize(Server $server): void { + $this->server = $server; + $this->server->on('propFind', [$this, 'collectionPreloadNotifier' ], 1); + } + + /** + * Uses the server instance to emit a `preloadCollection` event to signal + * to interested plugins that a collection can be preloaded. + * + * NOTE: this can be emitted several times, so ideally every plugin + * should cache what they need and check if a cache exists before + * re-fetching. + */ + public function collectionPreloadNotifier(PropFind $propFind, INode $node): bool { + if (!$this->shouldPreload($propFind, $node)) { + return true; + } + + return $this->server->emit('preloadCollection', [$propFind, $node]); + } + + private function shouldPreload( + PropFind $propFind, + INode $node, + ): bool { + $depth = $propFind->getDepth(); + return $node instanceof ICollection + && ($depth === Server::DEPTH_INFINITY || $depth > 0); + } +} diff --git a/apps/dav/lib/Connector/Sabre/Server.php b/apps/dav/lib/Connector/Sabre/Server.php index dda9c29b763..eef65000131 100644 --- a/apps/dav/lib/Connector/Sabre/Server.php +++ b/apps/dav/lib/Connector/Sabre/Server.php @@ -27,7 +27,8 @@ class Server extends \Sabre\DAV\Server { /** * Tracks queries done by plugins. - * @var array<int, array<string, array{nodes:int, queries:int}>> + * @var array<string, array<int, array<string, array{nodes:int, + * queries:int}>>> The keys represent: event name, depth and plugin name */ private array $pluginQueries = []; @@ -50,8 +51,8 @@ class Server extends \Sabre\DAV\Server { ): void { $this->debugEnabled ? $this->monitorPropfindQueries( parent::once(...), - ...func_get_args(), - ) : parent::once(...func_get_args()); + ...\func_get_args(), + ) : parent::once(...\func_get_args()); } #[Override] @@ -62,8 +63,8 @@ class Server extends \Sabre\DAV\Server { ): void { $this->debugEnabled ? $this->monitorPropfindQueries( parent::on(...), - ...func_get_args(), - ) : parent::on(...func_get_args()); + ...\func_get_args(), + ) : parent::on(...\func_get_args()); } /** @@ -76,13 +77,17 @@ class Server extends \Sabre\DAV\Server { callable $callBack, int $priority = 100, ): void { - if ($eventName !== 'propFind') { + $pluginName = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3)[2]['class'] ?? 'unknown'; + // The NotifyPlugin needs to be excluded as it emits the + // `preloadCollection` event, which causes many plugins run queries. + /** @psalm-suppress TypeDoesNotContainType */ + if ($pluginName === PropFindPreloadNotifyPlugin::class || ($eventName !== 'propFind' + && $eventName !== 'preloadCollection')) { $parentFn($eventName, $callBack, $priority); return; } - $pluginName = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3)[2]['class'] ?? 'unknown'; - $callback = $this->getMonitoredCallback($callBack, $pluginName); + $callback = $this->getMonitoredCallback($callBack, $pluginName, $eventName); $parentFn($eventName, $callback, $priority); } @@ -94,22 +99,26 @@ class Server extends \Sabre\DAV\Server { private function getMonitoredCallback( callable $callBack, string $pluginName, + string $eventName, ): callable { return function (PropFind $propFind, INode $node) use ( $callBack, $pluginName, - ) { + $eventName, + ): bool { $connection = \OCP\Server::get(Connection::class); $queriesBefore = $connection->getStats()['executed']; $result = $callBack($propFind, $node); $queriesAfter = $connection->getStats()['executed']; $this->trackPluginQueries( $pluginName, + $eventName, $queriesAfter - $queriesBefore, $propFind->getDepth() ); - return $result; + // many callbacks don't care about returning a bool + return $result ?? true; }; } @@ -118,6 +127,7 @@ class Server extends \Sabre\DAV\Server { */ private function trackPluginQueries( string $pluginName, + string $eventName, int $queriesExecuted, int $depth, ): void { @@ -126,11 +136,11 @@ class Server extends \Sabre\DAV\Server { return; } - $this->pluginQueries[$depth][$pluginName]['nodes'] - = ($this->pluginQueries[$depth][$pluginName]['nodes'] ?? 0) + 1; + $this->pluginQueries[$eventName][$depth][$pluginName]['nodes'] + = ($this->pluginQueries[$eventName][$depth][$pluginName]['nodes'] ?? 0) + 1; - $this->pluginQueries[$depth][$pluginName]['queries'] - = ($this->pluginQueries[$depth][$pluginName]['queries'] ?? 0) + $queriesExecuted; + $this->pluginQueries[$eventName][$depth][$pluginName]['queries'] + = ($this->pluginQueries[$eventName][$depth][$pluginName]['queries'] ?? 0) + $queriesExecuted; } /** @@ -221,8 +231,8 @@ class Server extends \Sabre\DAV\Server { /** * Returns queries executed by registered plugins. - * - * @return array<int, array<string, array{nodes:int, queries:int}>> + * @return array<string, array<int, array<string, array{nodes:int, + * queries:int}>>> The keys represent: event name, depth and plugin name */ public function getPluginQueries(): array { return $this->pluginQueries; diff --git a/apps/dav/lib/Connector/Sabre/ServerFactory.php b/apps/dav/lib/Connector/Sabre/ServerFactory.php index a6a27057177..1b4de841ec6 100644 --- a/apps/dav/lib/Connector/Sabre/ServerFactory.php +++ b/apps/dav/lib/Connector/Sabre/ServerFactory.php @@ -14,6 +14,7 @@ use OCA\DAV\CalDAV\DefaultCalendarValidator; use OCA\DAV\CalDAV\Proxy\ProxyMapper; use OCA\DAV\DAV\CustomPropertiesBackend; use OCA\DAV\DAV\ViewOnlyPlugin; +use OCA\DAV\Db\PropertyMapper; use OCA\DAV\Files\BrowserErrorPagePlugin; use OCA\DAV\Files\Sharing\RootCollection; use OCA\DAV\Upload\CleanupService; @@ -94,6 +95,8 @@ class ServerFactory { $server->debugEnabled = $debugEnabled; $server->addPlugin(new PropFindMonitorPlugin()); } + + $server->addPlugin(new PropFindPreloadNotifyPlugin()); // FIXME: The following line is a workaround for legacy components relying on being able to send a GET to / $server->addPlugin(new DummyGetResponsePlugin()); $server->addPlugin(new ExceptionLoggerPlugin('webdav', $this->logger)); @@ -226,6 +229,7 @@ class ServerFactory { $tree, $this->databaseConnection, $this->userSession->getUser(), + \OCP\Server::get(PropertyMapper::class), \OCP\Server::get(DefaultCalendarValidator::class), ) ) diff --git a/apps/dav/lib/Connector/Sabre/SharesPlugin.php b/apps/dav/lib/Connector/Sabre/SharesPlugin.php index f49e85333f3..11e50362dc2 100644 --- a/apps/dav/lib/Connector/Sabre/SharesPlugin.php +++ b/apps/dav/lib/Connector/Sabre/SharesPlugin.php @@ -15,6 +15,7 @@ use OCP\Files\NotFoundException; use OCP\IUserSession; use OCP\Share\IManager; use OCP\Share\IShare; +use Sabre\DAV\ICollection; use Sabre\DAV\PropFind; use Sabre\DAV\Server; use Sabre\DAV\Tree; @@ -38,7 +39,14 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin { /** @var IShare[][] */ private array $cachedShares = []; - /** @var string[] */ + + /** + * Tracks which folders have been cached. + * When a folder is cached, it will appear with its path as key and true + * as value. + * + * @var bool[] + */ private array $cachedFolders = []; public function __construct( @@ -67,6 +75,7 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin { $server->protectedProperties[] = self::SHAREES_PROPERTYNAME; $this->server = $server; + $this->server->on('preloadCollection', $this->preloadCollection(...)); $this->server->on('propFind', [$this, 'handleGetProperties']); } @@ -89,28 +98,28 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin { ]; foreach ($requestedShareTypes as $requestedShareType) { - $result = array_merge($result, $this->shareManager->getSharesBy( + $result[] = $this->shareManager->getSharesBy( $this->userId, $requestedShareType, $node, false, -1 - )); + ); // Also check for shares where the user is the recipient try { - $result = array_merge($result, $this->shareManager->getSharedWith( + $result[] = $this->shareManager->getSharedWith( $this->userId, $requestedShareType, $node, -1 - )); + ); } catch (BackendError $e) { // ignore } } - return $result; + return array_merge(...$result); } /** @@ -141,7 +150,7 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin { // if we already cached the folder containing this file // then we already know there are no shares here. - if (array_search($parentPath, $this->cachedFolders) === false) { + if (!isset($this->cachedFolders[$parentPath])) { try { $node = $sabreNode->getNode(); } catch (NotFoundException $e) { @@ -156,6 +165,27 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin { return []; } + private function preloadCollection(PropFind $propFind, ICollection $collection): void { + if (!$collection instanceof Directory + || isset($this->cachedFolders[$collection->getPath()]) + || ( + $propFind->getStatus(self::SHARETYPES_PROPERTYNAME) === null + && $propFind->getStatus(self::SHAREES_PROPERTYNAME) === null + ) + ) { + return; + } + + // If the node is a directory and we are requesting share types or sharees + // then we get all the shares in the folder and cache them. + // This is more performant than iterating each files afterwards. + $folderNode = $collection->getNode(); + $this->cachedFolders[$collection->getPath()] = true; + foreach ($this->getSharesFolder($folderNode) as $id => $shares) { + $this->cachedShares[$id] = $shares; + } + } + /** * Adds shares to propfind response * @@ -170,24 +200,6 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin { return; } - // If the node is a directory and we are requesting share types or sharees - // then we get all the shares in the folder and cache them. - // This is more performant than iterating each files afterwards. - if ($sabreNode instanceof Directory - && $propFind->getDepth() !== 0 - && ( - !is_null($propFind->getStatus(self::SHARETYPES_PROPERTYNAME)) - || !is_null($propFind->getStatus(self::SHAREES_PROPERTYNAME)) - ) - ) { - $folderNode = $sabreNode->getNode(); - $this->cachedFolders[] = $sabreNode->getPath(); - $childShares = $this->getSharesFolder($folderNode); - foreach ($childShares as $id => $shares) { - $this->cachedShares[$id] = $shares; - } - } - $propFind->handle(self::SHARETYPES_PROPERTYNAME, function () use ($sabreNode): ShareTypeList { $shares = $this->getShares($sabreNode); diff --git a/apps/dav/lib/Connector/Sabre/TagsPlugin.php b/apps/dav/lib/Connector/Sabre/TagsPlugin.php index 25c1633df36..ec3e6fc5320 100644 --- a/apps/dav/lib/Connector/Sabre/TagsPlugin.php +++ b/apps/dav/lib/Connector/Sabre/TagsPlugin.php @@ -31,6 +31,7 @@ use OCP\EventDispatcher\IEventDispatcher; use OCP\ITagManager; use OCP\ITags; use OCP\IUserSession; +use Sabre\DAV\ICollection; use Sabre\DAV\PropFind; use Sabre\DAV\PropPatch; @@ -61,6 +62,7 @@ class TagsPlugin extends \Sabre\DAV\ServerPlugin { * @var array */ private $cachedTags; + private array $cachedDirectories; /** * @param \Sabre\DAV\Tree $tree tree @@ -92,6 +94,7 @@ class TagsPlugin extends \Sabre\DAV\ServerPlugin { $server->xml->elementMap[self::TAGS_PROPERTYNAME] = TagList::class; $this->server = $server; + $this->server->on('preloadCollection', $this->preloadCollection(...)); $this->server->on('propFind', [$this, 'handleGetProperties']); $this->server->on('propPatch', [$this, 'handleUpdateProperties']); $this->server->on('preloadProperties', [$this, 'handlePreloadProperties']); @@ -194,6 +197,29 @@ class TagsPlugin extends \Sabre\DAV\ServerPlugin { } } + private function preloadCollection(PropFind $propFind, ICollection $collection): + void { + if (!($collection instanceof Node)) { + return; + } + + // need prefetch ? + if ($collection instanceof Directory + && !isset($this->cachedDirectories[$collection->getPath()]) + && (!is_null($propFind->getStatus(self::TAGS_PROPERTYNAME)) + || !is_null($propFind->getStatus(self::FAVORITE_PROPERTYNAME)) + )) { + // note: pre-fetching only supported for depth <= 1 + $folderContent = $collection->getChildren(); + $fileIds = [(int)$collection->getId()]; + foreach ($folderContent as $info) { + $fileIds[] = (int)$info->getId(); + } + $this->prefetchTagsForFileIds($fileIds); + $this->cachedDirectories[$collection->getPath()] = true; + } + } + /** * Adds tags and favorites properties to the response, * if requested. @@ -210,21 +236,6 @@ class TagsPlugin extends \Sabre\DAV\ServerPlugin { return; } - // need prefetch ? - if ($node instanceof Directory - && $propFind->getDepth() !== 0 - && (!is_null($propFind->getStatus(self::TAGS_PROPERTYNAME)) - || !is_null($propFind->getStatus(self::FAVORITE_PROPERTYNAME)) - )) { - // note: pre-fetching only supported for depth <= 1 - $folderContent = $node->getChildren(); - $fileIds = [(int)$node->getId()]; - foreach ($folderContent as $info) { - $fileIds[] = (int)$info->getId(); - } - $this->prefetchTagsForFileIds($fileIds); - } - $isFav = null; $propFind->handle(self::TAGS_PROPERTYNAME, function () use (&$isFav, $node) { diff --git a/apps/dav/lib/DAV/CustomPropertiesBackend.php b/apps/dav/lib/DAV/CustomPropertiesBackend.php index f9a4f8ee986..be7345f25df 100644 --- a/apps/dav/lib/DAV/CustomPropertiesBackend.php +++ b/apps/dav/lib/DAV/CustomPropertiesBackend.php @@ -9,13 +9,20 @@ namespace OCA\DAV\DAV; use Exception; +use OCA\DAV\CalDAV\CalDavBackend; use OCA\DAV\CalDAV\Calendar; +use OCA\DAV\CalDAV\CalendarHome; use OCA\DAV\CalDAV\CalendarObject; use OCA\DAV\CalDAV\DefaultCalendarValidator; +use OCA\DAV\CalDAV\Integration\ExternalCalendar; +use OCA\DAV\CalDAV\Outbox; +use OCA\DAV\CalDAV\Trashbin\TrashbinHome; use OCA\DAV\Connector\Sabre\Directory; +use OCA\DAV\Db\PropertyMapper; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; use OCP\IUser; +use Sabre\CalDAV\Schedule\Inbox; use Sabre\DAV\Exception as DavException; use Sabre\DAV\PropertyStorage\Backend\BackendInterface; use Sabre\DAV\PropFind; @@ -97,11 +104,17 @@ class CustomPropertiesBackend implements BackendInterface { ]; /** + * Map of well-known property names to default values + */ + private const PROPERTY_DEFAULT_VALUES = [ + '{http://owncloud.org/ns}calendar-enabled' => '1', + ]; + + /** * Properties cache - * - * @var array */ - private $userCache = []; + private array $userCache = []; + private array $publishedCache = []; private XmlService $xmlService; /** @@ -114,6 +127,7 @@ class CustomPropertiesBackend implements BackendInterface { private Tree $tree, private IDBConnection $connection, private IUser $user, + private PropertyMapper $propertyMapper, private DefaultCalendarValidator $defaultCalendarValidator, ) { $this->xmlService = new XmlService(); @@ -197,6 +211,13 @@ class CustomPropertiesBackend implements BackendInterface { $this->cacheDirectory($path, $node); } + if ($node instanceof CalendarHome && $propFind->getDepth() !== 0) { + $backend = $node->getCalDAVBackend(); + if ($backend instanceof CalDavBackend) { + $this->cacheCalendars($node, $requestedProps); + } + } + if ($node instanceof CalendarObject) { // No custom properties supported on individual events return; @@ -316,6 +337,10 @@ class CustomPropertiesBackend implements BackendInterface { return []; } + if (isset($this->publishedCache[$path])) { + return $this->publishedCache[$path]; + } + $qb = $this->connection->getQueryBuilder(); $qb->select('*') ->from(self::TABLE_NAME) @@ -326,6 +351,7 @@ class CustomPropertiesBackend implements BackendInterface { $props[$row['propertyname']] = $this->decodeValueFromDatabase($row['propertyvalue'], $row['valuetype']); } $result->closeCursor(); + $this->publishedCache[$path] = $props; return $props; } @@ -364,6 +390,62 @@ class CustomPropertiesBackend implements BackendInterface { $this->userCache = array_merge($this->userCache, $propsByPath); } + private function cacheCalendars(CalendarHome $node, array $requestedProperties): void { + $calendars = $node->getChildren(); + + $users = []; + foreach ($calendars as $calendar) { + if ($calendar instanceof Calendar) { + $user = str_replace('principals/users/', '', $calendar->getPrincipalURI()); + if (!isset($users[$user])) { + $users[$user] = ['calendars/' . $user]; + } + $users[$user][] = 'calendars/' . $user . '/' . $calendar->getUri(); + } elseif ($calendar instanceof Inbox || $calendar instanceof Outbox || $calendar instanceof TrashbinHome || $calendar instanceof ExternalCalendar) { + if ($calendar->getOwner()) { + $user = str_replace('principals/users/', '', $calendar->getOwner()); + if (!isset($users[$user])) { + $users[$user] = ['calendars/' . $user]; + } + $users[$user][] = 'calendars/' . $user . '/' . $calendar->getName(); + } + } + } + + // user properties + $properties = $this->propertyMapper->findPropertiesByPathsAndUsers($users); + + $propsByPath = []; + foreach ($users as $paths) { + foreach ($paths as $path) { + $propsByPath[$path] = []; + } + } + + foreach ($properties as $property) { + $propsByPath[$property->getPropertypath()][$property->getPropertyname()] = $this->decodeValueFromDatabase($property->getPropertyvalue(), $property->getValuetype()); + } + $this->userCache = array_merge($this->userCache, $propsByPath); + + // published properties + $allowedProps = array_intersect(self::PUBLISHED_READ_ONLY_PROPERTIES, $requestedProperties); + if (empty($allowedProps)) { + return; + } + $paths = []; + foreach ($users as $nestedPaths) { + $paths = array_merge($paths, $nestedPaths); + } + $paths = array_unique($paths); + + $propsByPath = array_fill_keys(array_values($paths), []); + $properties = $this->propertyMapper->findPropertiesByPaths($paths, $allowedProps); + foreach ($properties as $property) { + $propsByPath[$property->getPropertypath()][$property->getPropertyname()] = $this->decodeValueFromDatabase($property->getPropertyvalue(), $property->getValuetype()); + } + $this->publishedCache = array_merge($this->publishedCache, $propsByPath); + } + /** * Returns a list of properties for the given path and current user * @@ -410,6 +492,14 @@ class CustomPropertiesBackend implements BackendInterface { return $props; } + private function isPropertyDefaultValue(string $name, mixed $value): bool { + if (!isset(self::PROPERTY_DEFAULT_VALUES[$name])) { + return false; + } + + return self::PROPERTY_DEFAULT_VALUES[$name] === $value; + } + /** * @throws Exception */ @@ -426,8 +516,8 @@ class CustomPropertiesBackend implements BackendInterface { 'propertyName' => $propertyName, ]; - // If it was null, we need to delete the property - if (is_null($propertyValue)) { + // If it was null or set to the default value, we need to delete the property + if (is_null($propertyValue) || $this->isPropertyDefaultValue($propertyName, $propertyValue)) { if (array_key_exists($propertyName, $existing)) { $deleteQuery = $deleteQuery ?? $this->createDeleteQuery(); $deleteQuery diff --git a/apps/dav/lib/DAV/Sharing/Plugin.php b/apps/dav/lib/DAV/Sharing/Plugin.php index 03e63813bab..82b000bc8ce 100644 --- a/apps/dav/lib/DAV/Sharing/Plugin.php +++ b/apps/dav/lib/DAV/Sharing/Plugin.php @@ -16,6 +16,7 @@ use OCP\AppFramework\Http; use OCP\IConfig; use OCP\IRequest; use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\ICollection; use Sabre\DAV\INode; use Sabre\DAV\PropFind; use Sabre\DAV\Server; @@ -89,6 +90,7 @@ class Plugin extends ServerPlugin { $this->server->xml->elementMap['{' . Plugin::NS_OWNCLOUD . '}invite'] = Invite::class; $this->server->on('method:POST', [$this, 'httpPost']); + $this->server->on('preloadCollection', $this->preloadCollection(...)); $this->server->on('propFind', [$this, 'propFind']); } @@ -168,6 +170,24 @@ class Plugin extends ServerPlugin { } } + private function preloadCollection(PropFind $propFind, ICollection $collection): void { + if (!$collection instanceof CalendarHome || $propFind->getDepth() !== 1) { + return; + } + + $backend = $collection->getCalDAVBackend(); + if (!$backend instanceof CalDavBackend) { + return; + } + + $calendars = $collection->getChildren(); + $calendars = array_filter($calendars, static fn (INode $node) => $node instanceof IShareable); + /** @var int[] $resourceIds */ + $resourceIds = array_map( + static fn (IShareable $node) => $node->getResourceId(), $calendars); + $backend->preloadShares($resourceIds); + } + /** * This event is triggered when properties are requested for a certain * node. @@ -179,20 +199,6 @@ class Plugin extends ServerPlugin { * @return void */ public function propFind(PropFind $propFind, INode $node) { - if ($node instanceof CalendarHome && $propFind->getDepth() === 1) { - $backend = $node->getCalDAVBackend(); - if ($backend instanceof CalDavBackend) { - $calendars = $node->getChildren(); - $calendars = array_filter($calendars, function (INode $node) { - return $node instanceof IShareable; - }); - /** @var int[] $resourceIds */ - $resourceIds = array_map(function (IShareable $node) { - return $node->getResourceId(); - }, $calendars); - $backend->preloadShares($resourceIds); - } - } if ($node instanceof IShareable) { $propFind->handle('{' . Plugin::NS_OWNCLOUD . '}invite', function () use ($node) { return new Invite( diff --git a/apps/dav/lib/Db/Property.php b/apps/dav/lib/Db/Property.php index 96c5f75ef4f..6c1e249ac47 100644 --- a/apps/dav/lib/Db/Property.php +++ b/apps/dav/lib/Db/Property.php @@ -16,6 +16,7 @@ use OCP\AppFramework\Db\Entity; * @method string getPropertypath() * @method string getPropertyname() * @method string getPropertyvalue() + * @method int getValuetype() */ class Property extends Entity { diff --git a/apps/dav/lib/Db/PropertyMapper.php b/apps/dav/lib/Db/PropertyMapper.php index 1789194ee7a..a3dbdaa7d98 100644 --- a/apps/dav/lib/Db/PropertyMapper.php +++ b/apps/dav/lib/Db/PropertyMapper.php @@ -10,6 +10,7 @@ declare(strict_types=1); namespace OCA\DAV\Db; use OCP\AppFramework\Db\QBMapper; +use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; /** @@ -39,17 +40,43 @@ class PropertyMapper extends QBMapper { } /** + * @param array<string, string[]> $calendars * @return Property[] + * @throws \OCP\DB\Exception */ - public function findPropertiesByPath(string $userId, string $path): array { + public function findPropertiesByPathsAndUsers(array $calendars): array { $selectQb = $this->db->getQueryBuilder(); $selectQb->select('*') - ->from(self::TABLE_NAME) - ->where( - $selectQb->expr()->eq('userid', $selectQb->createNamedParameter($userId)), - $selectQb->expr()->eq('propertypath', $selectQb->createNamedParameter($path)), + ->from(self::TABLE_NAME); + + foreach ($calendars as $user => $paths) { + $selectQb->orWhere( + $selectQb->expr()->andX( + $selectQb->expr()->eq('userid', $selectQb->createNamedParameter($user)), + $selectQb->expr()->in('propertypath', $selectQb->createNamedParameter($paths, IQueryBuilder::PARAM_STR_ARRAY)), + ) ); + } + return $this->findEntities($selectQb); } + /** + * @param string[] $calendars + * @param string[] $allowedProperties + * @return Property[] + * @throws \OCP\DB\Exception + */ + public function findPropertiesByPaths(array $calendars, array $allowedProperties = []): array { + $selectQb = $this->db->getQueryBuilder(); + $selectQb->select('*') + ->from(self::TABLE_NAME) + ->where($selectQb->expr()->in('propertypath', $selectQb->createNamedParameter($calendars, IQueryBuilder::PARAM_STR_ARRAY))); + + if ($allowedProperties) { + $selectQb->andWhere($selectQb->expr()->in('propertyname', $selectQb->createNamedParameter($allowedProperties, IQueryBuilder::PARAM_STR_ARRAY))); + } + + return $this->findEntities($selectQb); + } } diff --git a/apps/dav/lib/Migration/Version1034Date20250813093701.php b/apps/dav/lib/Migration/Version1034Date20250813093701.php new file mode 100644 index 00000000000..10be71f067b --- /dev/null +++ b/apps/dav/lib/Migration/Version1034Date20250813093701.php @@ -0,0 +1,53 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; +use Override; + +class Version1034Date20250813093701 extends SimpleMigrationStep { + public function __construct( + private IDBConnection $db, + ) { + } + + /** + * @param IOutput $output + * @param Closure(): ISchemaWrapper $schemaClosure + * @param array $options + */ + #[Override] + public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { + $qb = $this->db->getQueryBuilder(); + $qb->delete('properties') + ->where($qb->expr()->eq( + 'propertyname', + $qb->createNamedParameter( + '{http://owncloud.org/ns}calendar-enabled', + IQueryBuilder::PARAM_STR, + ), + IQueryBuilder::PARAM_STR, + )) + ->andWhere($qb->expr()->eq( + 'propertyvalue', + $qb->createNamedParameter( + '1', + IQueryBuilder::PARAM_STR, + ), + IQueryBuilder::PARAM_STR, + )) + ->executeStatement(); + } +} diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php index a92e162f1b0..9b4a1b3d33c 100644 --- a/apps/dav/lib/Server.php +++ b/apps/dav/lib/Server.php @@ -46,6 +46,7 @@ use OCA\DAV\Connector\Sabre\LockPlugin; use OCA\DAV\Connector\Sabre\MaintenancePlugin; use OCA\DAV\Connector\Sabre\PropfindCompressionPlugin; use OCA\DAV\Connector\Sabre\PropFindMonitorPlugin; +use OCA\DAV\Connector\Sabre\PropFindPreloadNotifyPlugin; use OCA\DAV\Connector\Sabre\QuotaPlugin; use OCA\DAV\Connector\Sabre\RequestIdHeaderPlugin; use OCA\DAV\Connector\Sabre\SharesPlugin; @@ -54,6 +55,7 @@ use OCA\DAV\Connector\Sabre\ZipFolderPlugin; use OCA\DAV\DAV\CustomPropertiesBackend; use OCA\DAV\DAV\PublicAuth; use OCA\DAV\DAV\ViewOnlyPlugin; +use OCA\DAV\Db\PropertyMapper; use OCA\DAV\Events\SabrePluginAddEvent; use OCA\DAV\Events\SabrePluginAuthInitEvent; use OCA\DAV\Files\BrowserErrorPagePlugin; @@ -237,6 +239,7 @@ class Server { \OCP\Server::get(IUserSession::class) )); + // performance improvement plugins $this->server->addPlugin(new CopyEtagHeaderPlugin()); $this->server->addPlugin(new RequestIdHeaderPlugin(\OCP\Server::get(IRequest::class))); $this->server->addPlugin(new UploadAutoMkcolPlugin()); @@ -248,6 +251,7 @@ class Server { $eventDispatcher, )); $this->server->addPlugin(\OCP\Server::get(PaginatePlugin::class)); + $this->server->addPlugin(new PropFindPreloadNotifyPlugin()); // allow setup of additional plugins $eventDispatcher->dispatch('OCA\DAV\Connector\Sabre::addPlugin', $event); @@ -306,6 +310,7 @@ class Server { $this->server->tree, \OCP\Server::get(IDBConnection::class), \OCP\Server::get(IUserSession::class)->getUser(), + \OCP\Server::get(PropertyMapper::class), \OCP\Server::get(DefaultCalendarValidator::class), ) ) diff --git a/apps/dav/lib/SystemTag/SystemTagPlugin.php b/apps/dav/lib/SystemTag/SystemTagPlugin.php index 4d4499c7559..6be3e8bd1a2 100644 --- a/apps/dav/lib/SystemTag/SystemTagPlugin.php +++ b/apps/dav/lib/SystemTag/SystemTagPlugin.php @@ -27,6 +27,7 @@ use Sabre\DAV\Exception\BadRequest; use Sabre\DAV\Exception\Conflict; use Sabre\DAV\Exception\Forbidden; use Sabre\DAV\Exception\UnsupportedMediaType; +use Sabre\DAV\ICollection; use Sabre\DAV\PropFind; use Sabre\DAV\PropPatch; use Sabre\HTTP\RequestInterface; @@ -94,6 +95,7 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin { $server->protectedProperties[] = self::ID_PROPERTYNAME; + $server->on('preloadCollection', $this->preloadCollection(...)); $server->on('propFind', [$this, 'handleGetProperties']); $server->on('propPatch', [$this, 'handleUpdateProperties']); $server->on('method:POST', [$this, 'httpPost']); @@ -199,6 +201,40 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin { } } + private function preloadCollection( + PropFind $propFind, + ICollection $collection, + ): void { + if (!$collection instanceof Node) { + return; + } + + if ($collection instanceof Directory + && !isset($this->cachedTagMappings[$collection->getId()]) + && $propFind->getStatus( + self::SYSTEM_TAGS_PROPERTYNAME + ) !== null) { + $fileIds = [$collection->getId()]; + + // note: pre-fetching only supported for depth <= 1 + $folderContent = $collection->getChildren(); + foreach ($folderContent as $info) { + if ($info instanceof Node) { + $fileIds[] = $info->getId(); + } + } + + $tags = $this->tagMapper->getTagIdsForObjects($fileIds, 'files'); + + $this->cachedTagMappings += $tags; + $emptyFileIds = array_diff($fileIds, array_keys($tags)); + + // also cache the ones that were not found + foreach ($emptyFileIds as $fileId) { + $this->cachedTagMappings[$fileId] = []; + } + } + } /** * Retrieves system tag properties @@ -297,29 +333,6 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin { } private function propfindForFile(PropFind $propFind, Node $node): void { - if ($node instanceof Directory - && $propFind->getDepth() !== 0 - && !is_null($propFind->getStatus(self::SYSTEM_TAGS_PROPERTYNAME))) { - $fileIds = [$node->getId()]; - - // note: pre-fetching only supported for depth <= 1 - $folderContent = $node->getChildren(); - foreach ($folderContent as $info) { - if ($info instanceof Node) { - $fileIds[] = $info->getId(); - } - } - - $tags = $this->tagMapper->getTagIdsForObjects($fileIds, 'files'); - - $this->cachedTagMappings = $this->cachedTagMappings + $tags; - $emptyFileIds = array_diff($fileIds, array_keys($tags)); - - // also cache the ones that were not found - foreach ($emptyFileIds as $fileId) { - $this->cachedTagMappings[$fileId] = []; - } - } $propFind->handle(self::SYSTEM_TAGS_PROPERTYNAME, function () use ($node) { $user = $this->userSession->getUser(); diff --git a/apps/dav/tests/unit/Connector/Sabre/CustomPropertiesBackendTest.php b/apps/dav/tests/unit/Connector/Sabre/CustomPropertiesBackendTest.php index d4021a66299..cafbdd3ca40 100644 --- a/apps/dav/tests/unit/Connector/Sabre/CustomPropertiesBackendTest.php +++ b/apps/dav/tests/unit/Connector/Sabre/CustomPropertiesBackendTest.php @@ -12,6 +12,7 @@ use OCA\DAV\CalDAV\DefaultCalendarValidator; use OCA\DAV\Connector\Sabre\Directory; use OCA\DAV\Connector\Sabre\File; use OCA\DAV\DAV\CustomPropertiesBackend; +use OCA\DAV\Db\PropertyMapper; use OCP\IDBConnection; use OCP\IUser; use OCP\Server; @@ -52,6 +53,7 @@ class CustomPropertiesBackendTest extends \Test\TestCase { $this->tree, Server::get(IDBConnection::class), $this->user, + Server::get(PropertyMapper::class), $this->defaultCalendarValidator, ); } diff --git a/apps/dav/tests/unit/Connector/Sabre/PropFindMonitorPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/PropFindMonitorPluginTest.php index b528c3d731c..9d22befa201 100644 --- a/apps/dav/tests/unit/Connector/Sabre/PropFindMonitorPluginTest.php +++ b/apps/dav/tests/unit/Connector/Sabre/PropFindMonitorPluginTest.php @@ -29,66 +29,76 @@ class PropFindMonitorPluginTest extends TestCase { 'No queries logged' => [[], 0], 'Plugins with queries in less than threshold nodes should not be logged' => [ [ - [ - 'PluginName' => ['queries' => 100, 'nodes' - => PropFindMonitorPlugin::THRESHOLD_NODES - 1] - ], - [], + 'propFind' => [ + [ + 'PluginName' => [ + 'queries' => 100, + 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES - 1] + ], + [], + ] ], 0 ], 'Plugins with query-to-node ratio less than threshold should not be logged' => [ [ - [ - 'PluginName' => [ - 'queries' => $minQueriesTrigger - 1, - 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES ], - ], - [], + 'propFind' => [ + [ + 'PluginName' => [ + 'queries' => $minQueriesTrigger - 1, + 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES ], + ], + [], + ] ], 0 ], 'Plugins with more nodes scanned than queries executed should not be logged' => [ [ - [ - 'PluginName' => [ - 'queries' => $minQueriesTrigger, - 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES * 2], - ], - [], + 'propFind' => [ + [ + 'PluginName' => [ + 'queries' => $minQueriesTrigger, + 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES * 2], + ], + [],] ], 0 ], 'Plugins with queries only in highest depth level should not be logged' => [ [ - [ - 'PluginName' => [ - 'queries' => $minQueriesTrigger, - 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES - 1 - ] - ], - [ - 'PluginName' => [ - 'queries' => $minQueriesTrigger * 2, - 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES - ] + 'propFind' => [ + [ + 'PluginName' => [ + 'queries' => $minQueriesTrigger, + 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES - 1 + ] + ], + [ + 'PluginName' => [ + 'queries' => $minQueriesTrigger * 2, + 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES + ] + ], ] ], 0 ], 'Plugins with too many queries should be logged' => [ [ - [ - 'FirstPlugin' => [ - 'queries' => $minQueriesTrigger, - 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES, + 'propFind' => [ + [ + 'FirstPlugin' => [ + 'queries' => $minQueriesTrigger, + 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES, + ], + 'SecondPlugin' => [ + 'queries' => $minQueriesTrigger, + 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES, + ] ], - 'SecondPlugin' => [ - 'queries' => $minQueriesTrigger, - 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES, - ] - ], - [] + [], + ] ], 2 ] diff --git a/apps/dav/tests/unit/Connector/Sabre/PropFindPreloadNotifyPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/PropFindPreloadNotifyPluginTest.php new file mode 100644 index 00000000000..52fe3eba5bf --- /dev/null +++ b/apps/dav/tests/unit/Connector/Sabre/PropFindPreloadNotifyPluginTest.php @@ -0,0 +1,92 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\Connector\Sabre; + +use OCA\DAV\Connector\Sabre\PropFindPreloadNotifyPlugin; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\DAV\ICollection; +use Sabre\DAV\IFile; +use Sabre\DAV\PropFind; +use Sabre\DAV\Server; +use Test\TestCase; + +class PropFindPreloadNotifyPluginTest extends TestCase { + + private Server&MockObject $server; + private PropFindPreloadNotifyPlugin $plugin; + + protected function setUp(): void { + parent::setUp(); + + $this->server = $this->createMock(Server::class); + $this->plugin = new PropFindPreloadNotifyPlugin(); + } + + public function testInitialize(): void { + $this->server + ->expects(self::once()) + ->method('on') + ->with('propFind', + $this->anything(), 1); + $this->plugin->initialize($this->server); + } + + public static function dataTestCollectionPreloadNotifier(): array { + return [ + 'When node is not a collection, should not emit' => [ + IFile::class, + 1, + false, + true + ], + 'When node is a collection but depth is zero, should not emit' => [ + ICollection::class, + 0, + false, + true + ], + 'When node is a collection, and depth > 0, should emit' => [ + ICollection::class, + 1, + true, + true + ], + 'When node is a collection, and depth is infinite, should emit' + => [ + ICollection::class, + Server::DEPTH_INFINITY, + true, + true + ], + 'When called called handler returns false, it should be returned' + => [ + ICollection::class, + 1, + true, + false + ] + ]; + } + + #[DataProvider(methodName: 'dataTestCollectionPreloadNotifier')] + public function testCollectionPreloadNotifier(string $nodeType, int $depth, bool $shouldEmit, bool $emitReturns): + void { + $this->plugin->initialize($this->server); + $propFind = $this->createMock(PropFind::class); + $propFind->expects(self::any())->method('getDepth')->willReturn($depth); + $node = $this->createMock($nodeType); + + $expectation = $shouldEmit ? self::once() : self::never(); + $this->server->expects($expectation)->method('emit')->with('preloadCollection', + [$propFind, $node])->willReturn($emitReturns); + $return = $this->plugin->collectionPreloadNotifier($propFind, $node); + $this->assertEquals($emitReturns, $return); + } +} diff --git a/apps/dav/tests/unit/Connector/Sabre/SharesPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/SharesPluginTest.php index 1c8e29dab38..33f579eb913 100644 --- a/apps/dav/tests/unit/Connector/Sabre/SharesPluginTest.php +++ b/apps/dav/tests/unit/Connector/Sabre/SharesPluginTest.php @@ -223,6 +223,7 @@ class SharesPluginTest extends \Test\TestCase { 0 ); + $this->server->emit('preloadCollection', [$propFindRoot, $sabreNode]); $this->plugin->handleGetProperties( $propFindRoot, $sabreNode diff --git a/apps/dav/tests/unit/Connector/Sabre/TagsPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/TagsPluginTest.php index 5003280bfdc..554a4a1424e 100644 --- a/apps/dav/tests/unit/Connector/Sabre/TagsPluginTest.php +++ b/apps/dav/tests/unit/Connector/Sabre/TagsPluginTest.php @@ -147,6 +147,8 @@ class TagsPluginTest extends \Test\TestCase { 0 ); + $this->server->emit('preloadCollection', [$propFindRoot, $node]); + $this->plugin->handleGetProperties( $propFindRoot, $node diff --git a/apps/dav/tests/unit/DAV/CustomPropertiesBackendTest.php b/apps/dav/tests/unit/DAV/CustomPropertiesBackendTest.php index 2a85c0cbecd..517969fc9a3 100644 --- a/apps/dav/tests/unit/DAV/CustomPropertiesBackendTest.php +++ b/apps/dav/tests/unit/DAV/CustomPropertiesBackendTest.php @@ -10,6 +10,7 @@ namespace OCA\DAV\Tests\unit\DAV; use OCA\DAV\CalDAV\Calendar; use OCA\DAV\CalDAV\DefaultCalendarValidator; use OCA\DAV\DAV\CustomPropertiesBackend; +use OCA\DAV\Db\PropertyMapper; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; use OCP\IUser; @@ -36,6 +37,7 @@ class CustomPropertiesBackendTest extends TestCase { private IUser&MockObject $user; private DefaultCalendarValidator&MockObject $defaultCalendarValidator; private CustomPropertiesBackend $backend; + private PropertyMapper $propertyMapper; protected function setUp(): void { parent::setUp(); @@ -49,6 +51,7 @@ class CustomPropertiesBackendTest extends TestCase { ->with() ->willReturn('dummy_user_42'); $this->dbConnection = \OCP\Server::get(IDBConnection::class); + $this->propertyMapper = \OCP\Server::get(PropertyMapper::class); $this->defaultCalendarValidator = $this->createMock(DefaultCalendarValidator::class); $this->backend = new CustomPropertiesBackend( @@ -56,6 +59,7 @@ class CustomPropertiesBackendTest extends TestCase { $this->tree, $this->dbConnection, $this->user, + $this->propertyMapper, $this->defaultCalendarValidator, ); } @@ -129,6 +133,7 @@ class CustomPropertiesBackendTest extends TestCase { $this->tree, $db, $this->user, + $this->propertyMapper, $this->defaultCalendarValidator, ); diff --git a/apps/federatedfilesharing/l10n/es.js b/apps/federatedfilesharing/l10n/es.js index 4fae04b87fd..50687352aec 100644 --- a/apps/federatedfilesharing/l10n/es.js +++ b/apps/federatedfilesharing/l10n/es.js @@ -47,6 +47,7 @@ OC.L10N.register( "Share with me through my #Nextcloud Federated Cloud ID, see {url}" : "Comparte conmigo a través de mi ID de Nube Federada #Nextcloud, ve {url}", "Share with me through my #Nextcloud Federated Cloud ID" : "Compartirlo conmigo a través de mi ID de Nube Federada #Nextcloud", "Share with me via Nextcloud" : "Compartirlo conmigo vía Nextcloud", + "Cloud ID copied" : "ID de Nube copiado", "Copy" : "Copiar", "Clipboard not available. Please copy the cloud ID manually." : "Portapapeles no disponible. Por favor, copia el ID de nube manualmente.", "Copied!" : "¡Copiado!", diff --git a/apps/federatedfilesharing/l10n/es.json b/apps/federatedfilesharing/l10n/es.json index 30e7ea50167..f5131a67260 100644 --- a/apps/federatedfilesharing/l10n/es.json +++ b/apps/federatedfilesharing/l10n/es.json @@ -45,6 +45,7 @@ "Share with me through my #Nextcloud Federated Cloud ID, see {url}" : "Comparte conmigo a través de mi ID de Nube Federada #Nextcloud, ve {url}", "Share with me through my #Nextcloud Federated Cloud ID" : "Compartirlo conmigo a través de mi ID de Nube Federada #Nextcloud", "Share with me via Nextcloud" : "Compartirlo conmigo vía Nextcloud", + "Cloud ID copied" : "ID de Nube copiado", "Copy" : "Copiar", "Clipboard not available. Please copy the cloud ID manually." : "Portapapeles no disponible. Por favor, copia el ID de nube manualmente.", "Copied!" : "¡Copiado!", diff --git a/apps/federatedfilesharing/l10n/pt_BR.js b/apps/federatedfilesharing/l10n/pt_BR.js index b6af476dffe..4d09230d39e 100644 --- a/apps/federatedfilesharing/l10n/pt_BR.js +++ b/apps/federatedfilesharing/l10n/pt_BR.js @@ -47,6 +47,7 @@ OC.L10N.register( "Share with me through my #Nextcloud Federated Cloud ID, see {url}" : "Compartilhe comigo por meio do meu ID de Nuvem Federada #Nextcloud, consulte {url}", "Share with me through my #Nextcloud Federated Cloud ID" : "Compartilhe comigo através do meu ID de Nuvem Federada #Nextcloud", "Share with me via Nextcloud" : "Compartilhe comigo via Nextcloud", + "Cloud ID copied" : "ID da nuvem copiada", "Copy" : "Copiar", "Clipboard not available. Please copy the cloud ID manually." : "Área de transferência indisponível. Copie o ID de Nuvem manualmente.", "Copied!" : "Copiado!", diff --git a/apps/federatedfilesharing/l10n/pt_BR.json b/apps/federatedfilesharing/l10n/pt_BR.json index b668cdc5513..7d7c89d5329 100644 --- a/apps/federatedfilesharing/l10n/pt_BR.json +++ b/apps/federatedfilesharing/l10n/pt_BR.json @@ -45,6 +45,7 @@ "Share with me through my #Nextcloud Federated Cloud ID, see {url}" : "Compartilhe comigo por meio do meu ID de Nuvem Federada #Nextcloud, consulte {url}", "Share with me through my #Nextcloud Federated Cloud ID" : "Compartilhe comigo através do meu ID de Nuvem Federada #Nextcloud", "Share with me via Nextcloud" : "Compartilhe comigo via Nextcloud", + "Cloud ID copied" : "ID da nuvem copiada", "Copy" : "Copiar", "Clipboard not available. Please copy the cloud ID manually." : "Área de transferência indisponível. Copie o ID de Nuvem manualmente.", "Copied!" : "Copiado!", diff --git a/apps/federatedfilesharing/l10n/sr.js b/apps/federatedfilesharing/l10n/sr.js index d64d4e59d19..2200c42b9ee 100644 --- a/apps/federatedfilesharing/l10n/sr.js +++ b/apps/federatedfilesharing/l10n/sr.js @@ -58,6 +58,7 @@ OC.L10N.register( "X (formerly Twitter)" : "X (бивши Twitter)", "formerly Twitter" : "бивши Twitter", "Mastodon" : "Mastodon", + "Bluesky" : "Bluesky", "Add to your website" : "Додај на свој веб сајт", "HTML Code:" : "ХТМЛ кôд:", "Cancel" : "Одустани", diff --git a/apps/federatedfilesharing/l10n/sr.json b/apps/federatedfilesharing/l10n/sr.json index c8816b63785..85e3a9ca73f 100644 --- a/apps/federatedfilesharing/l10n/sr.json +++ b/apps/federatedfilesharing/l10n/sr.json @@ -56,6 +56,7 @@ "X (formerly Twitter)" : "X (бивши Twitter)", "formerly Twitter" : "бивши Twitter", "Mastodon" : "Mastodon", + "Bluesky" : "Bluesky", "Add to your website" : "Додај на свој веб сајт", "HTML Code:" : "ХТМЛ кôд:", "Cancel" : "Одустани", diff --git a/apps/federatedfilesharing/l10n/zh_CN.js b/apps/federatedfilesharing/l10n/zh_CN.js index e219d97787e..a1490efa431 100644 --- a/apps/federatedfilesharing/l10n/zh_CN.js +++ b/apps/federatedfilesharing/l10n/zh_CN.js @@ -47,6 +47,7 @@ OC.L10N.register( "Share with me through my #Nextcloud Federated Cloud ID, see {url}" : "通过我的 #Nextcloud 联合云 ID 与我分享文件,链接 {url}", "Share with me through my #Nextcloud Federated Cloud ID" : "通过我的 #Nextcloud 联合云 ID 与我共享", "Share with me via Nextcloud" : "通过联合云与我共享", + "Cloud ID copied" : "云 ID 已复制", "Copy" : "复制", "Clipboard not available. Please copy the cloud ID manually." : "剪贴板不可用,请手动复制云 ID。", "Copied!" : "已复制!", diff --git a/apps/federatedfilesharing/l10n/zh_CN.json b/apps/federatedfilesharing/l10n/zh_CN.json index dfa053bd36d..488053b3e26 100644 --- a/apps/federatedfilesharing/l10n/zh_CN.json +++ b/apps/federatedfilesharing/l10n/zh_CN.json @@ -45,6 +45,7 @@ "Share with me through my #Nextcloud Federated Cloud ID, see {url}" : "通过我的 #Nextcloud 联合云 ID 与我分享文件,链接 {url}", "Share with me through my #Nextcloud Federated Cloud ID" : "通过我的 #Nextcloud 联合云 ID 与我共享", "Share with me via Nextcloud" : "通过联合云与我共享", + "Cloud ID copied" : "云 ID 已复制", "Copy" : "复制", "Clipboard not available. Please copy the cloud ID manually." : "剪贴板不可用,请手动复制云 ID。", "Copied!" : "已复制!", diff --git a/apps/files/l10n/de.js b/apps/files/l10n/de.js index a8ddd02495c..9a23be9ba4f 100644 --- a/apps/files/l10n/de.js +++ b/apps/files/l10n/de.js @@ -115,7 +115,7 @@ OC.L10N.register( "Name" : "Name", "File type" : "Dateityp", "Size" : "Größe", - "{displayName}: failed on some elements" : "{displayName}: Ist bei einigen Elementen fehlgeschlagen", + "{displayName}: failed on some elements" : "{displayName}: Ist bei einigen Elementen fehlgeschlagen", "{displayName}: done" : "{displayName}: Abgeschlossen", "{displayName}: failed" : "{displayName}: Fehlgeschlagen", "Actions" : "Aktionen", @@ -127,7 +127,7 @@ OC.L10N.register( "File not found" : "Datei nicht gefunden", "_{count} selected_::_{count} selected_" : ["{count} ausgewählt","{count} ausgewählt"], "Search everywhere …" : "Überall suchen …", - "Search here …" : "Hier suchen …", + "Search here …" : "Hier suchen …", "Search scope options" : "Suchbereichsoptionen", "Search here" : "Hier suchen", "{usedQuotaByte} used" : "{usedQuotaByte} verwendet", @@ -224,7 +224,7 @@ OC.L10N.register( "Selection" : "Auswahl", "Select all files" : "Alle Dateien auswählen", "Deselect all" : "Auswahl aufheben", - "Select or deselect" : "Aus- oder Abwählen", + "Select or deselect" : "Aus- oder abwählen", "Select a range" : "Einen Bereich auswählen", "Navigation" : "Navigation", "Go to parent folder" : "Zum übergeordneten Ordner wechseln", diff --git a/apps/files/l10n/de.json b/apps/files/l10n/de.json index 254f405cd12..02f84eac3cc 100644 --- a/apps/files/l10n/de.json +++ b/apps/files/l10n/de.json @@ -113,7 +113,7 @@ "Name" : "Name", "File type" : "Dateityp", "Size" : "Größe", - "{displayName}: failed on some elements" : "{displayName}: Ist bei einigen Elementen fehlgeschlagen", + "{displayName}: failed on some elements" : "{displayName}: Ist bei einigen Elementen fehlgeschlagen", "{displayName}: done" : "{displayName}: Abgeschlossen", "{displayName}: failed" : "{displayName}: Fehlgeschlagen", "Actions" : "Aktionen", @@ -125,7 +125,7 @@ "File not found" : "Datei nicht gefunden", "_{count} selected_::_{count} selected_" : ["{count} ausgewählt","{count} ausgewählt"], "Search everywhere …" : "Überall suchen …", - "Search here …" : "Hier suchen …", + "Search here …" : "Hier suchen …", "Search scope options" : "Suchbereichsoptionen", "Search here" : "Hier suchen", "{usedQuotaByte} used" : "{usedQuotaByte} verwendet", @@ -222,7 +222,7 @@ "Selection" : "Auswahl", "Select all files" : "Alle Dateien auswählen", "Deselect all" : "Auswahl aufheben", - "Select or deselect" : "Aus- oder Abwählen", + "Select or deselect" : "Aus- oder abwählen", "Select a range" : "Einen Bereich auswählen", "Navigation" : "Navigation", "Go to parent folder" : "Zum übergeordneten Ordner wechseln", diff --git a/apps/files/l10n/de_DE.js b/apps/files/l10n/de_DE.js index 4b897553f31..f5b58fcde18 100644 --- a/apps/files/l10n/de_DE.js +++ b/apps/files/l10n/de_DE.js @@ -115,7 +115,7 @@ OC.L10N.register( "Name" : "Name", "File type" : "Dateityp", "Size" : "Größe", - "{displayName}: failed on some elements" : "{displayName}: Ist bei einigen Elementen fehlgeschlagen", + "{displayName}: failed on some elements" : "{displayName}: Ist bei einigen Elementen fehlgeschlagen", "{displayName}: done" : "{displayName}: Abgeschlossen", "{displayName}: failed" : "{displayName}: Fehlgeschlagen", "Actions" : "Aktionen", @@ -126,8 +126,8 @@ OC.L10N.register( "This list is not fully rendered for performance reasons. The files will be rendered as you navigate through the list." : "Diese Liste ist aus Performance-Gründen nicht vollständig gerendert. Die Dateien werden gerendert, wenn Sie durch die Liste navigieren.", "File not found" : "Datei nicht gefunden", "_{count} selected_::_{count} selected_" : ["{count} ausgewählt","{count} ausgewählt"], - "Search everywhere …" : "Überall suchen …", - "Search here …" : "Hier suchen …", + "Search everywhere …" : "Überall suchen …", + "Search here …" : "Hier suchen …", "Search scope options" : "Suchbereichsoptionen", "Search here" : "Hier suchen", "{usedQuotaByte} used" : "{usedQuotaByte} verwendet", @@ -224,7 +224,7 @@ OC.L10N.register( "Selection" : "Auswahl", "Select all files" : "Alle Dateien auswählen", "Deselect all" : "Auswahl aufheben", - "Select or deselect" : "Aus- oder Abwählen", + "Select or deselect" : "Aus- oder abwählen", "Select a range" : "Einen Bereich auswählen", "Navigation" : "Navigation", "Go to parent folder" : "Zum übergeordneten Ordner wechseln", diff --git a/apps/files/l10n/de_DE.json b/apps/files/l10n/de_DE.json index 26b5abdbd75..403987f92f0 100644 --- a/apps/files/l10n/de_DE.json +++ b/apps/files/l10n/de_DE.json @@ -113,7 +113,7 @@ "Name" : "Name", "File type" : "Dateityp", "Size" : "Größe", - "{displayName}: failed on some elements" : "{displayName}: Ist bei einigen Elementen fehlgeschlagen", + "{displayName}: failed on some elements" : "{displayName}: Ist bei einigen Elementen fehlgeschlagen", "{displayName}: done" : "{displayName}: Abgeschlossen", "{displayName}: failed" : "{displayName}: Fehlgeschlagen", "Actions" : "Aktionen", @@ -124,8 +124,8 @@ "This list is not fully rendered for performance reasons. The files will be rendered as you navigate through the list." : "Diese Liste ist aus Performance-Gründen nicht vollständig gerendert. Die Dateien werden gerendert, wenn Sie durch die Liste navigieren.", "File not found" : "Datei nicht gefunden", "_{count} selected_::_{count} selected_" : ["{count} ausgewählt","{count} ausgewählt"], - "Search everywhere …" : "Überall suchen …", - "Search here …" : "Hier suchen …", + "Search everywhere …" : "Überall suchen …", + "Search here …" : "Hier suchen …", "Search scope options" : "Suchbereichsoptionen", "Search here" : "Hier suchen", "{usedQuotaByte} used" : "{usedQuotaByte} verwendet", @@ -222,7 +222,7 @@ "Selection" : "Auswahl", "Select all files" : "Alle Dateien auswählen", "Deselect all" : "Auswahl aufheben", - "Select or deselect" : "Aus- oder Abwählen", + "Select or deselect" : "Aus- oder abwählen", "Select a range" : "Einen Bereich auswählen", "Navigation" : "Navigation", "Go to parent folder" : "Zum übergeordneten Ordner wechseln", diff --git a/apps/files/l10n/es.js b/apps/files/l10n/es.js index 9601f32db31..b464d60386b 100644 --- a/apps/files/l10n/es.js +++ b/apps/files/l10n/es.js @@ -458,7 +458,7 @@ OC.L10N.register( "Enable the grid view" : "Habilitar vista de cuadrícula", "Enable folder tree" : "Habilitar el árbol de carpetas", "Copy to clipboard" : "Copiar al portapapeles", - "Use this address to access your Files via WebDAV" : "Use esta dirección para acceder a tus archivos vía WebDAV", + "Use this address to access your Files via WebDAV" : "Use esta dirección para acceder a sus archivos vía WebDAV", "If you have enabled 2FA, you must create and use a new app password by clicking here." : "Si ha habilitado 2FA, debe crear y utilizar una nueva contraseña de aplicación haciendo clic aquí.", "Deletion cancelled" : "Eliminación cancelada", "Move cancelled" : "Se canceló la movida", @@ -482,7 +482,7 @@ OC.L10N.register( "Filter file names …" : "Filtrar nombres de archivo …", "Prevent warning dialogs from open or reenable them." : "Evitar que se abran los diálogos de advertencia o volver a habilitarlos.", "Show a warning dialog when changing a file extension." : "Mostrar un diálogo de advertencia cuando se cambia la extensión de un archivo.", - "Speed up your Files experience with these quick shortcuts." : "Acelere su experiencia con Archivos con esos rápidos atajos de teclado.", + "Speed up your Files experience with these quick shortcuts." : "Acelere su experiencia con Archivos con estos rápidos atajos de teclado.", "Open the actions menu for a file" : "Abrir el menú de acciones para un archivo", "Rename a file" : "Renombrar un archivo", "Delete a file" : "Eliminar un archivo", diff --git a/apps/files/l10n/es.json b/apps/files/l10n/es.json index 935df9c3971..54af2b317ca 100644 --- a/apps/files/l10n/es.json +++ b/apps/files/l10n/es.json @@ -456,7 +456,7 @@ "Enable the grid view" : "Habilitar vista de cuadrícula", "Enable folder tree" : "Habilitar el árbol de carpetas", "Copy to clipboard" : "Copiar al portapapeles", - "Use this address to access your Files via WebDAV" : "Use esta dirección para acceder a tus archivos vía WebDAV", + "Use this address to access your Files via WebDAV" : "Use esta dirección para acceder a sus archivos vía WebDAV", "If you have enabled 2FA, you must create and use a new app password by clicking here." : "Si ha habilitado 2FA, debe crear y utilizar una nueva contraseña de aplicación haciendo clic aquí.", "Deletion cancelled" : "Eliminación cancelada", "Move cancelled" : "Se canceló la movida", @@ -480,7 +480,7 @@ "Filter file names …" : "Filtrar nombres de archivo …", "Prevent warning dialogs from open or reenable them." : "Evitar que se abran los diálogos de advertencia o volver a habilitarlos.", "Show a warning dialog when changing a file extension." : "Mostrar un diálogo de advertencia cuando se cambia la extensión de un archivo.", - "Speed up your Files experience with these quick shortcuts." : "Acelere su experiencia con Archivos con esos rápidos atajos de teclado.", + "Speed up your Files experience with these quick shortcuts." : "Acelere su experiencia con Archivos con estos rápidos atajos de teclado.", "Open the actions menu for a file" : "Abrir el menú de acciones para un archivo", "Rename a file" : "Renombrar un archivo", "Delete a file" : "Eliminar un archivo", diff --git a/apps/files/l10n/pt_BR.js b/apps/files/l10n/pt_BR.js index 6b64267ec74..86681234e45 100644 --- a/apps/files/l10n/pt_BR.js +++ b/apps/files/l10n/pt_BR.js @@ -115,6 +115,9 @@ OC.L10N.register( "Name" : "Nome", "File type" : "Tipo de arquivo", "Size" : "Tamanho", + "{displayName}: failed on some elements" : "{displayName}: falhou em alguns elementos", + "{displayName}: done" : "{displayName}: concluído", + "{displayName}: failed" : "{displayName}: falhou", "Actions" : "Ações", "(selected)" : "(selecionados)", "List of files and folders." : "Lista de arquivos e pastas.", @@ -123,7 +126,10 @@ OC.L10N.register( "This list is not fully rendered for performance reasons. The files will be rendered as you navigate through the list." : "Esta lista não é totalmente renderizada por motivos de desempenho. Os arquivos serão renderizados à medida que você navegar pela lista.", "File not found" : "Arquivo não encontrado", "_{count} selected_::_{count} selected_" : ["{count} selecionado","{count} selecionados","{count} selecionados"], + "Search everywhere …" : "Pesquisar em qualquer lugar…", + "Search here …" : "Pesquisar aqui…", "Search scope options" : "Opções de escopo da pesquisa", + "Search here" : "Pesquisar aqui", "{usedQuotaByte} used" : "{usedQuotaByte} usado", "{used} of {quota} used" : "{used} de {quota} usados", "{relative}% used" : "{relative}% usado", @@ -187,6 +193,7 @@ OC.L10N.register( "No search results for “{query}”" : "Sem resultados de pesquisa para \"{query}\"", "Search for files" : "Pesquisar arquivos", "Clipboard is not available" : "A área de transferência não está disponível", + "WebDAV URL copied" : "URL WebDAV copiado", "General" : "Geral", "Default view" : "Visualização padrão", "All files" : "Todos os arquivos", @@ -197,24 +204,37 @@ OC.L10N.register( "Appearance" : "Aparência", "Show hidden files" : "Mostrar arquivos ocultos", "Show file type column" : "Mostrar coluna de tipo de arquivo", + "Show file extensions" : "Mostrar extensões de arquivo", "Crop image previews" : "Cortar visualizações de imagem", "Additional settings" : "Configurações adicionais", "WebDAV" : "WebDAV", "WebDAV URL" : "URL WebDAV", "Copy" : "Copiar", + "How to access files using WebDAV" : "Como acessar arquivos usando WebDAV", "Two-Factor Authentication is enabled for your account, and therefore you need to use an app password to connect an external WebDAV client." : "A Autenticação de Dois Fatores está ativada para sua conta e, portanto, você precisa usar uma senha de aplicativo para conectar um cliente WebDAV externo.", "Warnings" : "Avisos", + "Warn before changing a file extension" : "Avisar antes de alterar uma extensão de arquivo", + "Warn before deleting files" : "Avisar antes de excluir arquivos", "Keyboard shortcuts" : "Atalhos do teclado", "File actions" : "Ações de arquivos", "Rename" : "Renomear", "Delete" : "Excluir", + "Add or remove favorite" : "Adicionar ou remover favoritos", "Manage tags" : "Gerenciar etiquetas", "Selection" : "Seleção", "Select all files" : "Selecionar todos os arquivos", "Deselect all" : "Desselecionar todos", + "Select or deselect" : "Selecionar ou desmarcar", + "Select a range" : "Selecione um intervalo", "Navigation" : "Navegação", + "Go to parent folder" : "Ir para a pasta mãe", + "Go to file above" : "Vá para o arquivo acima", + "Go to file below" : "Vá para o arquivo abaixo", + "Go left in grid" : "Vá para a esquerda na grade", + "Go right in grid" : "Vá para a direita na grade", "View" : "Visualização", "Toggle grid view" : "Alternar a visão em grade", + "Open file sidebar" : "Abrir barra lateral de arquivo", "Show those shortcuts" : "Mostrar esses atalhos", "You" : "Você", "Shared multiple times with different people" : "Compartilhado várias vezes com pessoas diferentes", @@ -322,6 +342,7 @@ OC.L10N.register( "The name \"{newName}\" is already used in the folder \"{dir}\". Please choose a different name." : "O nome \"{newName}\" já é utilizado na pasta \"{dir}\". Escolha um nome diferente.", "Could not rename \"{oldName}\"" : "Não foi possível renomear \"{oldName}\"", "This operation is forbidden" : "Esta operação é proibida", + "This folder is unavailable, please try again later or contact the administration" : "Esta pasta não está disponível, tente novamente mais tarde ou entre em contato com a administração", "Storage is temporarily not available" : "O armazenamento está temporariamente indisponível", "Unexpected error: {error}" : "Erro inesperado: {error}", "_%n file_::_%n files_" : ["%n arquivo","%n de arquivos","%n arquivos"], diff --git a/apps/files/l10n/pt_BR.json b/apps/files/l10n/pt_BR.json index dd50c32b179..ac3ded88f3f 100644 --- a/apps/files/l10n/pt_BR.json +++ b/apps/files/l10n/pt_BR.json @@ -113,6 +113,9 @@ "Name" : "Nome", "File type" : "Tipo de arquivo", "Size" : "Tamanho", + "{displayName}: failed on some elements" : "{displayName}: falhou em alguns elementos", + "{displayName}: done" : "{displayName}: concluído", + "{displayName}: failed" : "{displayName}: falhou", "Actions" : "Ações", "(selected)" : "(selecionados)", "List of files and folders." : "Lista de arquivos e pastas.", @@ -121,7 +124,10 @@ "This list is not fully rendered for performance reasons. The files will be rendered as you navigate through the list." : "Esta lista não é totalmente renderizada por motivos de desempenho. Os arquivos serão renderizados à medida que você navegar pela lista.", "File not found" : "Arquivo não encontrado", "_{count} selected_::_{count} selected_" : ["{count} selecionado","{count} selecionados","{count} selecionados"], + "Search everywhere …" : "Pesquisar em qualquer lugar…", + "Search here …" : "Pesquisar aqui…", "Search scope options" : "Opções de escopo da pesquisa", + "Search here" : "Pesquisar aqui", "{usedQuotaByte} used" : "{usedQuotaByte} usado", "{used} of {quota} used" : "{used} de {quota} usados", "{relative}% used" : "{relative}% usado", @@ -185,6 +191,7 @@ "No search results for “{query}”" : "Sem resultados de pesquisa para \"{query}\"", "Search for files" : "Pesquisar arquivos", "Clipboard is not available" : "A área de transferência não está disponível", + "WebDAV URL copied" : "URL WebDAV copiado", "General" : "Geral", "Default view" : "Visualização padrão", "All files" : "Todos os arquivos", @@ -195,24 +202,37 @@ "Appearance" : "Aparência", "Show hidden files" : "Mostrar arquivos ocultos", "Show file type column" : "Mostrar coluna de tipo de arquivo", + "Show file extensions" : "Mostrar extensões de arquivo", "Crop image previews" : "Cortar visualizações de imagem", "Additional settings" : "Configurações adicionais", "WebDAV" : "WebDAV", "WebDAV URL" : "URL WebDAV", "Copy" : "Copiar", + "How to access files using WebDAV" : "Como acessar arquivos usando WebDAV", "Two-Factor Authentication is enabled for your account, and therefore you need to use an app password to connect an external WebDAV client." : "A Autenticação de Dois Fatores está ativada para sua conta e, portanto, você precisa usar uma senha de aplicativo para conectar um cliente WebDAV externo.", "Warnings" : "Avisos", + "Warn before changing a file extension" : "Avisar antes de alterar uma extensão de arquivo", + "Warn before deleting files" : "Avisar antes de excluir arquivos", "Keyboard shortcuts" : "Atalhos do teclado", "File actions" : "Ações de arquivos", "Rename" : "Renomear", "Delete" : "Excluir", + "Add or remove favorite" : "Adicionar ou remover favoritos", "Manage tags" : "Gerenciar etiquetas", "Selection" : "Seleção", "Select all files" : "Selecionar todos os arquivos", "Deselect all" : "Desselecionar todos", + "Select or deselect" : "Selecionar ou desmarcar", + "Select a range" : "Selecione um intervalo", "Navigation" : "Navegação", + "Go to parent folder" : "Ir para a pasta mãe", + "Go to file above" : "Vá para o arquivo acima", + "Go to file below" : "Vá para o arquivo abaixo", + "Go left in grid" : "Vá para a esquerda na grade", + "Go right in grid" : "Vá para a direita na grade", "View" : "Visualização", "Toggle grid view" : "Alternar a visão em grade", + "Open file sidebar" : "Abrir barra lateral de arquivo", "Show those shortcuts" : "Mostrar esses atalhos", "You" : "Você", "Shared multiple times with different people" : "Compartilhado várias vezes com pessoas diferentes", @@ -320,6 +340,7 @@ "The name \"{newName}\" is already used in the folder \"{dir}\". Please choose a different name." : "O nome \"{newName}\" já é utilizado na pasta \"{dir}\". Escolha um nome diferente.", "Could not rename \"{oldName}\"" : "Não foi possível renomear \"{oldName}\"", "This operation is forbidden" : "Esta operação é proibida", + "This folder is unavailable, please try again later or contact the administration" : "Esta pasta não está disponível, tente novamente mais tarde ou entre em contato com a administração", "Storage is temporarily not available" : "O armazenamento está temporariamente indisponível", "Unexpected error: {error}" : "Erro inesperado: {error}", "_%n file_::_%n files_" : ["%n arquivo","%n de arquivos","%n arquivos"], diff --git a/apps/files/l10n/sr.js b/apps/files/l10n/sr.js index da03fc7ccc4..50b07bbe6ca 100644 --- a/apps/files/l10n/sr.js +++ b/apps/files/l10n/sr.js @@ -115,6 +115,9 @@ OC.L10N.register( "Name" : "Назив", "File type" : "Тип фајла", "Size" : "Величина", + "{displayName}: failed on some elements" : "{displayName}: није успело на неким елементима", + "{displayName}: done" : "{displayName}: завршено", + "{displayName}: failed" : "{displayName}: није успело", "Actions" : "Радње", "(selected)" : "(изабрано)", "List of files and folders." : "Листа фајлова и фолдера.", @@ -123,7 +126,10 @@ OC.L10N.register( "This list is not fully rendered for performance reasons. The files will be rendered as you navigate through the list." : "Ова листа није у потпуности приказана из разлога перформанси. Фајлови ће се приказивати како се крећете кроз листу.", "File not found" : "Фајл није нађен", "_{count} selected_::_{count} selected_" : ["изабран је {count}","изабрана су {count}","изабрано је {count}"], + "Search everywhere …" : "Претражи свуда", + "Search here …" : "Претражи овде…", "Search scope options" : "Опције опсега претраге", + "Search here" : "Претражи овде", "{usedQuotaByte} used" : "{usedQuotaByte} искоришћено", "{used} of {quota} used" : "{used} од {quota} искоришћено", "{relative}% used" : "{relative}% искоришћено", @@ -135,6 +141,7 @@ OC.L10N.register( "Create new folder" : "Направи нову фасциклу", "This name is already in use." : "Ово име се већ користи.", "Create" : "Направи", + "Files starting with a dot are hidden by default" : "Фајлови који почињу тачком су подразумевано скривени", "Fill template fields" : "Попуните поља шаблона", "Submitting fields …" : "Поља се подносе…", "Submit" : "Пошаљи", @@ -186,32 +193,48 @@ OC.L10N.register( "No search results for “{query}”" : "Није нађен ниједан резултат за „{query}”", "Search for files" : "Претражи фајлове", "Clipboard is not available" : "Клипборд није доступан", + "WebDAV URL copied" : "WebDAV URL је копиран", "General" : "Опште", "Default view" : "Подразумевани поглед", "All files" : "Сви фајлови", "Personal files" : "Лични фајлови", "Sort favorites first" : "Сортирај прво омиљене", "Sort folders before files" : "Поређај фолдере испред фајлова", + "Folder tree" : "Стабло фолдера", "Appearance" : "Изглед", "Show hidden files" : "Прикажи скривене фајлове", "Show file type column" : "Прикажи колону са типом фајла", + "Show file extensions" : "Прикажи екстензије фајла", "Crop image previews" : "Опсецање прегледа слика", "Additional settings" : "Додатне поставке", "WebDAV" : "ВебДАВ", "WebDAV URL" : "WebDAV URL", "Copy" : "Копирај", + "How to access files using WebDAV" : "Како да приступите фајловима преко WebDAV протокола", + "Two-Factor Authentication is enabled for your account, and therefore you need to use an app password to connect an external WebDAV client." : "Двофакторска потврда идентитета је укључена за ваш налог, тако да морате користити лозинку апликације да повежете спољни WebDAV клијент.", "Warnings" : "Упозорења", + "Warn before changing a file extension" : "Упозори пре измене екстензије фајла", + "Warn before deleting files" : "Упозори пре брисања фајлова", "Keyboard shortcuts" : "Пречице на тастатури", "File actions" : "Фајл акције", "Rename" : "Преименуј", "Delete" : "Обриши", + "Add or remove favorite" : "Додај или уклони омиљене", "Manage tags" : "Управљање ознакама", "Selection" : "Избор", "Select all files" : "Изабери све фајлове", "Deselect all" : "Поништи цео избор", + "Select or deselect" : "Изабери или уклони избор", + "Select a range" : "Изабери опсег", "Navigation" : "Навигација", + "Go to parent folder" : "Иди на фолдер родитељ", + "Go to file above" : "Иди на фајл изнад", + "Go to file below" : "Иди на фајл испод", + "Go left in grid" : "Иди на лево у мрежи", + "Go right in grid" : "Иди на десно у мреж", "View" : "Погледај", "Toggle grid view" : "Укључи/искључи приказ мреже", + "Open file sidebar" : "Отвори бочну траку за фајл", "Show those shortcuts" : "Прикажи те пречице", "You" : "Ви", "Shared multiple times with different people" : "Дељено више пута са разним људима", @@ -300,6 +323,7 @@ OC.L10N.register( "Templates" : "Шаблони", "New template folder" : "Нови фолдер шаблона", "In folder" : "У фолдеру", + "Search in all files" : "Претражи у свим фајловима", "Search in folder: {folder}" : "Претрага у фолдеру: {folder}", "One of the dropped files could not be processed" : "Један од упуштених фајлова не може да се обради", "Your browser does not support the Filesystem API. Directories will not be uploaded" : "Ваш интернет прегледач не подржава Filesystem API. Нећете моћи да отпремате директоријуме", @@ -318,6 +342,7 @@ OC.L10N.register( "The name \"{newName}\" is already used in the folder \"{dir}\". Please choose a different name." : "Назив „{newName}” се већ користи у директоријуму „{dir}”. Молимо вас да изаберете неко друго име.", "Could not rename \"{oldName}\"" : "Не може да се промени име фајла „{oldName}”", "This operation is forbidden" : "Ова радња је забрањена", + "This folder is unavailable, please try again later or contact the administration" : "Овај фолдер није доступан, молимо вас да покушате касније или да контактирате администрацију", "Storage is temporarily not available" : "Складиште привремено није доступно", "Unexpected error: {error}" : "Неочекивана грешка: {error}", "_%n file_::_%n files_" : ["%n фајл","%n фајла","%n фајлова"], diff --git a/apps/files/l10n/sr.json b/apps/files/l10n/sr.json index b97d3ffc9b0..8746aa285a7 100644 --- a/apps/files/l10n/sr.json +++ b/apps/files/l10n/sr.json @@ -113,6 +113,9 @@ "Name" : "Назив", "File type" : "Тип фајла", "Size" : "Величина", + "{displayName}: failed on some elements" : "{displayName}: није успело на неким елементима", + "{displayName}: done" : "{displayName}: завршено", + "{displayName}: failed" : "{displayName}: није успело", "Actions" : "Радње", "(selected)" : "(изабрано)", "List of files and folders." : "Листа фајлова и фолдера.", @@ -121,7 +124,10 @@ "This list is not fully rendered for performance reasons. The files will be rendered as you navigate through the list." : "Ова листа није у потпуности приказана из разлога перформанси. Фајлови ће се приказивати како се крећете кроз листу.", "File not found" : "Фајл није нађен", "_{count} selected_::_{count} selected_" : ["изабран је {count}","изабрана су {count}","изабрано је {count}"], + "Search everywhere …" : "Претражи свуда", + "Search here …" : "Претражи овде…", "Search scope options" : "Опције опсега претраге", + "Search here" : "Претражи овде", "{usedQuotaByte} used" : "{usedQuotaByte} искоришћено", "{used} of {quota} used" : "{used} од {quota} искоришћено", "{relative}% used" : "{relative}% искоришћено", @@ -133,6 +139,7 @@ "Create new folder" : "Направи нову фасциклу", "This name is already in use." : "Ово име се већ користи.", "Create" : "Направи", + "Files starting with a dot are hidden by default" : "Фајлови који почињу тачком су подразумевано скривени", "Fill template fields" : "Попуните поља шаблона", "Submitting fields …" : "Поља се подносе…", "Submit" : "Пошаљи", @@ -184,32 +191,48 @@ "No search results for “{query}”" : "Није нађен ниједан резултат за „{query}”", "Search for files" : "Претражи фајлове", "Clipboard is not available" : "Клипборд није доступан", + "WebDAV URL copied" : "WebDAV URL је копиран", "General" : "Опште", "Default view" : "Подразумевани поглед", "All files" : "Сви фајлови", "Personal files" : "Лични фајлови", "Sort favorites first" : "Сортирај прво омиљене", "Sort folders before files" : "Поређај фолдере испред фајлова", + "Folder tree" : "Стабло фолдера", "Appearance" : "Изглед", "Show hidden files" : "Прикажи скривене фајлове", "Show file type column" : "Прикажи колону са типом фајла", + "Show file extensions" : "Прикажи екстензије фајла", "Crop image previews" : "Опсецање прегледа слика", "Additional settings" : "Додатне поставке", "WebDAV" : "ВебДАВ", "WebDAV URL" : "WebDAV URL", "Copy" : "Копирај", + "How to access files using WebDAV" : "Како да приступите фајловима преко WebDAV протокола", + "Two-Factor Authentication is enabled for your account, and therefore you need to use an app password to connect an external WebDAV client." : "Двофакторска потврда идентитета је укључена за ваш налог, тако да морате користити лозинку апликације да повежете спољни WebDAV клијент.", "Warnings" : "Упозорења", + "Warn before changing a file extension" : "Упозори пре измене екстензије фајла", + "Warn before deleting files" : "Упозори пре брисања фајлова", "Keyboard shortcuts" : "Пречице на тастатури", "File actions" : "Фајл акције", "Rename" : "Преименуј", "Delete" : "Обриши", + "Add or remove favorite" : "Додај или уклони омиљене", "Manage tags" : "Управљање ознакама", "Selection" : "Избор", "Select all files" : "Изабери све фајлове", "Deselect all" : "Поништи цео избор", + "Select or deselect" : "Изабери или уклони избор", + "Select a range" : "Изабери опсег", "Navigation" : "Навигација", + "Go to parent folder" : "Иди на фолдер родитељ", + "Go to file above" : "Иди на фајл изнад", + "Go to file below" : "Иди на фајл испод", + "Go left in grid" : "Иди на лево у мрежи", + "Go right in grid" : "Иди на десно у мреж", "View" : "Погледај", "Toggle grid view" : "Укључи/искључи приказ мреже", + "Open file sidebar" : "Отвори бочну траку за фајл", "Show those shortcuts" : "Прикажи те пречице", "You" : "Ви", "Shared multiple times with different people" : "Дељено више пута са разним људима", @@ -298,6 +321,7 @@ "Templates" : "Шаблони", "New template folder" : "Нови фолдер шаблона", "In folder" : "У фолдеру", + "Search in all files" : "Претражи у свим фајловима", "Search in folder: {folder}" : "Претрага у фолдеру: {folder}", "One of the dropped files could not be processed" : "Један од упуштених фајлова не може да се обради", "Your browser does not support the Filesystem API. Directories will not be uploaded" : "Ваш интернет прегледач не подржава Filesystem API. Нећете моћи да отпремате директоријуме", @@ -316,6 +340,7 @@ "The name \"{newName}\" is already used in the folder \"{dir}\". Please choose a different name." : "Назив „{newName}” се већ користи у директоријуму „{dir}”. Молимо вас да изаберете неко друго име.", "Could not rename \"{oldName}\"" : "Не може да се промени име фајла „{oldName}”", "This operation is forbidden" : "Ова радња је забрањена", + "This folder is unavailable, please try again later or contact the administration" : "Овај фолдер није доступан, молимо вас да покушате касније или да контактирате администрацију", "Storage is temporarily not available" : "Складиште привремено није доступно", "Unexpected error: {error}" : "Неочекивана грешка: {error}", "_%n file_::_%n files_" : ["%n фајл","%n фајла","%n фајлова"], diff --git a/apps/files/l10n/zh_CN.js b/apps/files/l10n/zh_CN.js index a160a86f978..1450370e916 100644 --- a/apps/files/l10n/zh_CN.js +++ b/apps/files/l10n/zh_CN.js @@ -1,10 +1,10 @@ OC.L10N.register( "files", { - "Added to favorites" : "已添加到收藏夹", - "Removed from favorites" : "从收藏夹移除", - "You added {file} to your favorites" : "您已经添加 {file} 到您的收藏夹", - "You removed {file} from your favorites" : "您已从收藏夹中移除 {file}", + "Added to favorites" : "已添加到收藏", + "Removed from favorites" : "已从收藏中移除", + "You added {file} to your favorites" : "您已将 {file} 添加到收藏", + "You removed {file} from your favorites" : "您已从收藏中移除 {file}", "Favorites" : "收藏", "File changes" : "文件变动", "Created by {user}" : "由 {user} 创建", @@ -115,6 +115,9 @@ OC.L10N.register( "Name" : "名称", "File type" : "文件类型", "Size" : "大小", + "{displayName}: failed on some elements" : "{displayName}:在部分上元素失败", + "{displayName}: done" : "{displayName}:完成", + "{displayName}: failed" : "{displayName}:失败", "Actions" : "操作", "(selected)" : "(已选择)", "List of files and folders." : "文件与文件夹列表。", @@ -123,7 +126,10 @@ OC.L10N.register( "This list is not fully rendered for performance reasons. The files will be rendered as you navigate through the list." : "出于性能考虑,此列表未完全呈现。文件将在您浏览列表时呈现。", "File not found" : "文件未找到", "_{count} selected_::_{count} selected_" : ["已选择 {count} 个"], + "Search everywhere …" : "在所有位置搜索…", + "Search here …" : "在此搜索…", "Search scope options" : "搜索范围选项", + "Search here" : "在此搜索", "{usedQuotaByte} used" : "已使用 {usedQuotaByte}", "{used} of {quota} used" : "已使用 {used}(共 {quota})", "{relative}% used" : "已使用 {relative}%", @@ -187,6 +193,7 @@ OC.L10N.register( "No search results for “{query}”" : "没有“{query}”的搜索结果", "Search for files" : "搜索文件", "Clipboard is not available" : "剪贴板不可用", + "WebDAV URL copied" : "WebDAV URL 已复制", "General" : "常规", "Default view" : "默认视图", "All files" : "全部文件", @@ -197,23 +204,37 @@ OC.L10N.register( "Appearance" : "外观", "Show hidden files" : "显示隐藏文件", "Show file type column" : "显示文件类型列", + "Show file extensions" : "显示文件扩展名", "Crop image previews" : "裁剪图片预览", "Additional settings" : "其他设置", "WebDAV" : "WebDAV", "WebDAV URL" : "WebDAV URL", "Copy" : "复制", + "How to access files using WebDAV" : "如何使用 WebDAV 访问文件", "Two-Factor Authentication is enabled for your account, and therefore you need to use an app password to connect an external WebDAV client." : "您的账号已启用双因素身份验证,因此您需要使用应用密码来连接外部 WebDAV 客户端。", "Warnings" : "警告", + "Warn before changing a file extension" : "更改文件扩展名前发出警告", + "Warn before deleting files" : "删除文件前发出警告", "Keyboard shortcuts" : "键盘快捷键", + "File actions" : "文件操作", "Rename" : "重命名", "Delete" : "删除", + "Add or remove favorite" : "添加或移除收藏", "Manage tags" : "管理标签", "Selection" : "选择", "Select all files" : "选择所有文件", "Deselect all" : "全部取消选择", + "Select or deselect" : "选择或取消选择", + "Select a range" : "选择范围", "Navigation" : "导航", + "Go to parent folder" : "转到上层文件夹", + "Go to file above" : "转到上方文件", + "Go to file below" : "转到下方文件", + "Go left in grid" : "在网格中向左", + "Go right in grid" : "在网格中向右", "View" : "查看", "Toggle grid view" : "切换网格视图", + "Open file sidebar" : "打开文件侧边栏", "Show those shortcuts" : "显示快捷键", "You" : "您", "Shared multiple times with different people" : "与不同的用户多次分享", @@ -221,7 +242,7 @@ OC.L10N.register( "Error while loading the file data" : "加载文件数据时出错", "Owner" : "拥有者", "Remove from favorites" : "从收藏中移除", - "Add to favorites" : "添加到收藏夹", + "Add to favorites" : "添加到收藏", "Tags" : "标签", "Blank" : "空白", "Unable to create new file from template" : "无法从模板创建新文件", @@ -311,8 +332,8 @@ OC.L10N.register( "Some files could not be uploaded" : "部分文件无法上传", "Files uploaded successfully" : "上传文件成功", "No files to process" : "没有需要处理的文件", - "Some files could not be copied" : "无法复制某些文件", - "Some files could not be moved" : "某些文件无法被移动", + "Some files could not be copied" : "无法复制部分文件", + "Some files could not be moved" : "无法移动部分文件", "Files copied successfully" : "复制文件成功", "Files moved successfully" : "移动文件成功", "Conflicts resolution skipped" : "已跳过冲突解决", @@ -321,6 +342,7 @@ OC.L10N.register( "The name \"{newName}\" is already used in the folder \"{dir}\". Please choose a different name." : "此名称“{newName}”在这个文件夹“{dir}”已经被使用。请选择其他名称。", "Could not rename \"{oldName}\"" : "无法重命名“{oldName}”", "This operation is forbidden" : "该操作被禁止", + "This folder is unavailable, please try again later or contact the administration" : "此文件夹不可用,请稍后重试或联系管理员", "Storage is temporarily not available" : "存储空间暂时不可用", "Unexpected error: {error}" : "意外错误:{error}", "_%n file_::_%n files_" : ["%n 个文件"], @@ -428,7 +450,7 @@ OC.L10N.register( "Upload (max. %s)" : "上传 (最大 %s)", "\"{displayName}\" action executed successfully" : "“{displayName}”操作执行成功", "\"{displayName}\" action failed" : "“{displayName}”操作执行失败", - "\"{displayName}\" failed on some elements" : "“{displayName}”在某些元素上失败", + "\"{displayName}\" failed on some elements" : "“{displayName}”在部分元素上失败", "\"{displayName}\" batch action executed successfully" : "批量操作“{displayName}”运行成功", "Submitting fields…" : "提交字段...", "Filter filenames…" : "过滤文件名...", diff --git a/apps/files/l10n/zh_CN.json b/apps/files/l10n/zh_CN.json index 661f89e775a..c38c6268e82 100644 --- a/apps/files/l10n/zh_CN.json +++ b/apps/files/l10n/zh_CN.json @@ -1,8 +1,8 @@ { "translations": { - "Added to favorites" : "已添加到收藏夹", - "Removed from favorites" : "从收藏夹移除", - "You added {file} to your favorites" : "您已经添加 {file} 到您的收藏夹", - "You removed {file} from your favorites" : "您已从收藏夹中移除 {file}", + "Added to favorites" : "已添加到收藏", + "Removed from favorites" : "已从收藏中移除", + "You added {file} to your favorites" : "您已将 {file} 添加到收藏", + "You removed {file} from your favorites" : "您已从收藏中移除 {file}", "Favorites" : "收藏", "File changes" : "文件变动", "Created by {user}" : "由 {user} 创建", @@ -113,6 +113,9 @@ "Name" : "名称", "File type" : "文件类型", "Size" : "大小", + "{displayName}: failed on some elements" : "{displayName}:在部分上元素失败", + "{displayName}: done" : "{displayName}:完成", + "{displayName}: failed" : "{displayName}:失败", "Actions" : "操作", "(selected)" : "(已选择)", "List of files and folders." : "文件与文件夹列表。", @@ -121,7 +124,10 @@ "This list is not fully rendered for performance reasons. The files will be rendered as you navigate through the list." : "出于性能考虑,此列表未完全呈现。文件将在您浏览列表时呈现。", "File not found" : "文件未找到", "_{count} selected_::_{count} selected_" : ["已选择 {count} 个"], + "Search everywhere …" : "在所有位置搜索…", + "Search here …" : "在此搜索…", "Search scope options" : "搜索范围选项", + "Search here" : "在此搜索", "{usedQuotaByte} used" : "已使用 {usedQuotaByte}", "{used} of {quota} used" : "已使用 {used}(共 {quota})", "{relative}% used" : "已使用 {relative}%", @@ -185,6 +191,7 @@ "No search results for “{query}”" : "没有“{query}”的搜索结果", "Search for files" : "搜索文件", "Clipboard is not available" : "剪贴板不可用", + "WebDAV URL copied" : "WebDAV URL 已复制", "General" : "常规", "Default view" : "默认视图", "All files" : "全部文件", @@ -195,23 +202,37 @@ "Appearance" : "外观", "Show hidden files" : "显示隐藏文件", "Show file type column" : "显示文件类型列", + "Show file extensions" : "显示文件扩展名", "Crop image previews" : "裁剪图片预览", "Additional settings" : "其他设置", "WebDAV" : "WebDAV", "WebDAV URL" : "WebDAV URL", "Copy" : "复制", + "How to access files using WebDAV" : "如何使用 WebDAV 访问文件", "Two-Factor Authentication is enabled for your account, and therefore you need to use an app password to connect an external WebDAV client." : "您的账号已启用双因素身份验证,因此您需要使用应用密码来连接外部 WebDAV 客户端。", "Warnings" : "警告", + "Warn before changing a file extension" : "更改文件扩展名前发出警告", + "Warn before deleting files" : "删除文件前发出警告", "Keyboard shortcuts" : "键盘快捷键", + "File actions" : "文件操作", "Rename" : "重命名", "Delete" : "删除", + "Add or remove favorite" : "添加或移除收藏", "Manage tags" : "管理标签", "Selection" : "选择", "Select all files" : "选择所有文件", "Deselect all" : "全部取消选择", + "Select or deselect" : "选择或取消选择", + "Select a range" : "选择范围", "Navigation" : "导航", + "Go to parent folder" : "转到上层文件夹", + "Go to file above" : "转到上方文件", + "Go to file below" : "转到下方文件", + "Go left in grid" : "在网格中向左", + "Go right in grid" : "在网格中向右", "View" : "查看", "Toggle grid view" : "切换网格视图", + "Open file sidebar" : "打开文件侧边栏", "Show those shortcuts" : "显示快捷键", "You" : "您", "Shared multiple times with different people" : "与不同的用户多次分享", @@ -219,7 +240,7 @@ "Error while loading the file data" : "加载文件数据时出错", "Owner" : "拥有者", "Remove from favorites" : "从收藏中移除", - "Add to favorites" : "添加到收藏夹", + "Add to favorites" : "添加到收藏", "Tags" : "标签", "Blank" : "空白", "Unable to create new file from template" : "无法从模板创建新文件", @@ -309,8 +330,8 @@ "Some files could not be uploaded" : "部分文件无法上传", "Files uploaded successfully" : "上传文件成功", "No files to process" : "没有需要处理的文件", - "Some files could not be copied" : "无法复制某些文件", - "Some files could not be moved" : "某些文件无法被移动", + "Some files could not be copied" : "无法复制部分文件", + "Some files could not be moved" : "无法移动部分文件", "Files copied successfully" : "复制文件成功", "Files moved successfully" : "移动文件成功", "Conflicts resolution skipped" : "已跳过冲突解决", @@ -319,6 +340,7 @@ "The name \"{newName}\" is already used in the folder \"{dir}\". Please choose a different name." : "此名称“{newName}”在这个文件夹“{dir}”已经被使用。请选择其他名称。", "Could not rename \"{oldName}\"" : "无法重命名“{oldName}”", "This operation is forbidden" : "该操作被禁止", + "This folder is unavailable, please try again later or contact the administration" : "此文件夹不可用,请稍后重试或联系管理员", "Storage is temporarily not available" : "存储空间暂时不可用", "Unexpected error: {error}" : "意外错误:{error}", "_%n file_::_%n files_" : ["%n 个文件"], @@ -426,7 +448,7 @@ "Upload (max. %s)" : "上传 (最大 %s)", "\"{displayName}\" action executed successfully" : "“{displayName}”操作执行成功", "\"{displayName}\" action failed" : "“{displayName}”操作执行失败", - "\"{displayName}\" failed on some elements" : "“{displayName}”在某些元素上失败", + "\"{displayName}\" failed on some elements" : "“{displayName}”在部分元素上失败", "\"{displayName}\" batch action executed successfully" : "批量操作“{displayName}”运行成功", "Submitting fields…" : "提交字段...", "Filter filenames…" : "过滤文件名...", diff --git a/apps/files_reminders/l10n/pt_BR.js b/apps/files_reminders/l10n/pt_BR.js index ac662d677af..3ed3e3e2949 100644 --- a/apps/files_reminders/l10n/pt_BR.js +++ b/apps/files_reminders/l10n/pt_BR.js @@ -11,6 +11,7 @@ OC.L10N.register( "Set file reminders" : "Defina lembretes de arquivo", "**📣 File reminders**\n\nSet file reminders.\n\nNote: to use the `File reminders` app, ensure that the `Notifications` app is installed and enabled. The `Notifications` app provides the necessary APIs for the `File reminders` app to work correctly." : "**📣 Lembretes de arquivos** \n\nDefina lembretes de arquivos. \n\nObservação: para usar o aplicativo `Lembretes de arquivos`, certifique-se de que o aplicativo `Notificações` esteja instalado e habilitado. O aplicativo `Notificações` fornece as APIs necessárias para que o aplicativo `Lembretes de arquivos` funcione corretamente.", "Set reminder for \"{fileName}\"" : "Definir lembrete para \"{fileName}\"", + "Reminder at custom date & time" : "Lembrete em data e hora personalizadas", "Clear reminder" : "Limpar lembrete", "Please choose a valid date & time" : "Por favor escolha uma data & hora válida.", "Reminder set for \"{fileName}\"" : "Lembrete definido para \"{fileName}\"", @@ -21,6 +22,7 @@ OC.L10N.register( "Cancel" : "Cancelar", "Set reminder" : "Definir lembrete", "Reminder set" : "Lembrete definido", + "Custom reminder" : "Lembrete personalizado", "Later today" : "Hoje mais tarde", "Set reminder for later today" : "Definir lembrete para hoje mais tarde", "Tomorrow" : "Amanhã", diff --git a/apps/files_reminders/l10n/pt_BR.json b/apps/files_reminders/l10n/pt_BR.json index d4a04ec5eec..e5be7701fae 100644 --- a/apps/files_reminders/l10n/pt_BR.json +++ b/apps/files_reminders/l10n/pt_BR.json @@ -9,6 +9,7 @@ "Set file reminders" : "Defina lembretes de arquivo", "**📣 File reminders**\n\nSet file reminders.\n\nNote: to use the `File reminders` app, ensure that the `Notifications` app is installed and enabled. The `Notifications` app provides the necessary APIs for the `File reminders` app to work correctly." : "**📣 Lembretes de arquivos** \n\nDefina lembretes de arquivos. \n\nObservação: para usar o aplicativo `Lembretes de arquivos`, certifique-se de que o aplicativo `Notificações` esteja instalado e habilitado. O aplicativo `Notificações` fornece as APIs necessárias para que o aplicativo `Lembretes de arquivos` funcione corretamente.", "Set reminder for \"{fileName}\"" : "Definir lembrete para \"{fileName}\"", + "Reminder at custom date & time" : "Lembrete em data e hora personalizadas", "Clear reminder" : "Limpar lembrete", "Please choose a valid date & time" : "Por favor escolha uma data & hora válida.", "Reminder set for \"{fileName}\"" : "Lembrete definido para \"{fileName}\"", @@ -19,6 +20,7 @@ "Cancel" : "Cancelar", "Set reminder" : "Definir lembrete", "Reminder set" : "Lembrete definido", + "Custom reminder" : "Lembrete personalizado", "Later today" : "Hoje mais tarde", "Set reminder for later today" : "Definir lembrete para hoje mais tarde", "Tomorrow" : "Amanhã", diff --git a/apps/files_reminders/lib/Dav/PropFindPlugin.php b/apps/files_reminders/lib/Dav/PropFindPlugin.php index 014e636eb2d..7fa45a4b854 100644 --- a/apps/files_reminders/lib/Dav/PropFindPlugin.php +++ b/apps/files_reminders/lib/Dav/PropFindPlugin.php @@ -16,6 +16,7 @@ use OCA\FilesReminders\Service\ReminderService; use OCP\Files\Folder; use OCP\IUser; use OCP\IUserSession; +use Sabre\DAV\ICollection; use Sabre\DAV\INode; use Sabre\DAV\PropFind; use Sabre\DAV\Server; @@ -32,9 +33,22 @@ class PropFindPlugin extends ServerPlugin { } public function initialize(Server $server): void { + $server->on('preloadCollection', $this->preloadCollection(...)); $server->on('propFind', [$this, 'propFind']); } + private function preloadCollection( + PropFind $propFind, + ICollection $collection, + ): void { + if ($collection instanceof Directory && $propFind->getStatus( + static::REMINDER_DUE_DATE_PROPERTY + ) !== null) { + $folder = $collection->getNode(); + $this->cacheFolder($folder); + } + } + public function propFind(PropFind $propFind, INode $node) { if (!in_array(static::REMINDER_DUE_DATE_PROPERTY, $propFind->getRequestedProperties())) { return; @@ -44,15 +58,6 @@ class PropFindPlugin extends ServerPlugin { return; } - if ( - $node instanceof Directory - && $propFind->getDepth() > 0 - && $propFind->getStatus(static::REMINDER_DUE_DATE_PROPERTY) !== null - ) { - $folder = $node->getNode(); - $this->cacheFolder($folder); - } - $propFind->handle( static::REMINDER_DUE_DATE_PROPERTY, function () use ($node) { diff --git a/apps/files_sharing/l10n/de.js b/apps/files_sharing/l10n/de.js index 79ec648162d..f0634117708 100644 --- a/apps/files_sharing/l10n/de.js +++ b/apps/files_sharing/l10n/de.js @@ -304,13 +304,13 @@ OC.L10N.register( "Link shares" : "Freigaben teilen", "Shares" : "Freigaben", "Share files within your organization. Recipients who can already view the file can also use this link for easy access." : "Dateien innerhalb Ihrer Organisation teilen. Auch Empfänger, die auf die Datei bereits zugreifen können, können diesen Link für einen einfachen Zugriff nutzen.", - "Share files with others outside your organization via public links and email addresses. You can also share to Nextcloud accounts on other instances using their federated cloud ID." : "Dateien über öffentliche Links und E-Mail-Adressen mit anderen außerhalb Ihrer Organisation teilen. Du kannst Nextcloud-Konten auch auf anderen Instanzen mithilfe der föderierten Cloud-ID teilen.", + "Share files with others outside your organization via public links and email addresses. You can also share to Nextcloud accounts on other instances using their federated cloud ID." : "Dateien über öffentliche Links und E-Mail-Adressen mit anderen außerhalb Ihrer Organisation teilen. Du kannst Nextcloud-Konten auch auf anderen Instanzen mithilfe der Federated-Cloud-ID teilen.", "Shares from apps or other sources which are not included in internal or external shares." : "Freigaben aus Apps oder anderen Quellen, die nicht in internen oder externen Freigaben enthalten sind.", - "Type names, teams, federated cloud IDs" : "Namen, Teams oder Federierte Cloud-IDs eingeben", - "Type names or teams" : "Namen oder Federierte Cloud-IDs eingeben", - "Type a federated cloud ID" : "Eine Federierte Cloud-ID eingeben", - "Type an email" : "Eine E-Mailadresse eingeben", - "Type an email or federated cloud ID" : "Eine E-Mailadresse oder eine Federierte Cloud-ID eingeben", + "Type names, teams, federated cloud IDs" : "Namen, Teams oder Federated-Cloud-IDs eingeben", + "Type names or teams" : "Namen oder Federated-Cloud-IDs eingeben", + "Type a federated cloud ID" : "Eine Federated-Cloud-ID eingeben", + "Type an email" : "Eine E-Mail-Adresse eingeben", + "Type an email or federated cloud ID" : "Eine E-Mail-Adresse oder eine Federated-Cloud-ID eingeben", "Unable to load the shares list" : "Liste der Freigaben konnte nicht geladen werden", "Expires {relativetime}" : "Läuft {relativetime} ab", "this share just expired." : "Diese Freigabe ist gerade abgelaufen.", diff --git a/apps/files_sharing/l10n/de.json b/apps/files_sharing/l10n/de.json index 79e463107ea..161077411d2 100644 --- a/apps/files_sharing/l10n/de.json +++ b/apps/files_sharing/l10n/de.json @@ -302,13 +302,13 @@ "Link shares" : "Freigaben teilen", "Shares" : "Freigaben", "Share files within your organization. Recipients who can already view the file can also use this link for easy access." : "Dateien innerhalb Ihrer Organisation teilen. Auch Empfänger, die auf die Datei bereits zugreifen können, können diesen Link für einen einfachen Zugriff nutzen.", - "Share files with others outside your organization via public links and email addresses. You can also share to Nextcloud accounts on other instances using their federated cloud ID." : "Dateien über öffentliche Links und E-Mail-Adressen mit anderen außerhalb Ihrer Organisation teilen. Du kannst Nextcloud-Konten auch auf anderen Instanzen mithilfe der föderierten Cloud-ID teilen.", + "Share files with others outside your organization via public links and email addresses. You can also share to Nextcloud accounts on other instances using their federated cloud ID." : "Dateien über öffentliche Links und E-Mail-Adressen mit anderen außerhalb Ihrer Organisation teilen. Du kannst Nextcloud-Konten auch auf anderen Instanzen mithilfe der Federated-Cloud-ID teilen.", "Shares from apps or other sources which are not included in internal or external shares." : "Freigaben aus Apps oder anderen Quellen, die nicht in internen oder externen Freigaben enthalten sind.", - "Type names, teams, federated cloud IDs" : "Namen, Teams oder Federierte Cloud-IDs eingeben", - "Type names or teams" : "Namen oder Federierte Cloud-IDs eingeben", - "Type a federated cloud ID" : "Eine Federierte Cloud-ID eingeben", - "Type an email" : "Eine E-Mailadresse eingeben", - "Type an email or federated cloud ID" : "Eine E-Mailadresse oder eine Federierte Cloud-ID eingeben", + "Type names, teams, federated cloud IDs" : "Namen, Teams oder Federated-Cloud-IDs eingeben", + "Type names or teams" : "Namen oder Federated-Cloud-IDs eingeben", + "Type a federated cloud ID" : "Eine Federated-Cloud-ID eingeben", + "Type an email" : "Eine E-Mail-Adresse eingeben", + "Type an email or federated cloud ID" : "Eine E-Mail-Adresse oder eine Federated-Cloud-ID eingeben", "Unable to load the shares list" : "Liste der Freigaben konnte nicht geladen werden", "Expires {relativetime}" : "Läuft {relativetime} ab", "this share just expired." : "Diese Freigabe ist gerade abgelaufen.", diff --git a/apps/files_sharing/l10n/de_DE.js b/apps/files_sharing/l10n/de_DE.js index a3507239899..a7dc879a8f0 100644 --- a/apps/files_sharing/l10n/de_DE.js +++ b/apps/files_sharing/l10n/de_DE.js @@ -304,13 +304,13 @@ OC.L10N.register( "Link shares" : "Freigaben teilen", "Shares" : "Freigaben", "Share files within your organization. Recipients who can already view the file can also use this link for easy access." : "Dateien innerhalb Ihrer Organisation teilen. Auch Empfänger, die auf die Datei bereits zugreifen können, können diesen Link für einen einfachen Zugriff nutzen.", - "Share files with others outside your organization via public links and email addresses. You can also share to Nextcloud accounts on other instances using their federated cloud ID." : "Dateien über öffentliche Links und E-Mail-Adressen mit anderen außerhalb Ihrer Organisation teilen. Sie können Nextcloud-Konten auch auf anderen Instanzen mithilfe ihrer föderierten Cloud-ID teilen.", + "Share files with others outside your organization via public links and email addresses. You can also share to Nextcloud accounts on other instances using their federated cloud ID." : "Dateien über öffentliche Links und E-Mail-Adressen mit anderen außerhalb Ihrer Organisation teilen. Sie können Nextcloud-Konten auch auf anderen Instanzen mithilfe ihrer Federated-Cloud-ID teilen.", "Shares from apps or other sources which are not included in internal or external shares." : "Freigaben aus Apps oder anderen Quellen, die nicht in internen oder externen Freigaben enthalten sind.", - "Type names, teams, federated cloud IDs" : "Namen, Teams oder Federierte Cloud-IDs eingeben", - "Type names or teams" : "Namen oder Federierte Cloud-IDs eingeben", - "Type a federated cloud ID" : "Eine Federierte Cloud-ID eingeben", - "Type an email" : "Eine E-Mailadresse eingeben", - "Type an email or federated cloud ID" : "Eine E-Mailadresse oder eine Federierte Cloud-ID eingeben", + "Type names, teams, federated cloud IDs" : "Namen, Teams oder Federated-Cloud-IDs eingeben", + "Type names or teams" : "Namen oder Teams eingeben", + "Type a federated cloud ID" : "Eine Federated-Cloud-ID eingeben", + "Type an email" : "Eine E-Mail-Adresse eingeben", + "Type an email or federated cloud ID" : "Eine E-Mail-Adresse oder eine Federated-Cloud-ID eingeben", "Unable to load the shares list" : "Liste der Freigaben kann nicht geladen werden", "Expires {relativetime}" : "Läuft {relativetime} ab", "this share just expired." : "Diese Freigabe ist gerade abgelaufen.", diff --git a/apps/files_sharing/l10n/de_DE.json b/apps/files_sharing/l10n/de_DE.json index 41d191e8166..7e4298f986a 100644 --- a/apps/files_sharing/l10n/de_DE.json +++ b/apps/files_sharing/l10n/de_DE.json @@ -302,13 +302,13 @@ "Link shares" : "Freigaben teilen", "Shares" : "Freigaben", "Share files within your organization. Recipients who can already view the file can also use this link for easy access." : "Dateien innerhalb Ihrer Organisation teilen. Auch Empfänger, die auf die Datei bereits zugreifen können, können diesen Link für einen einfachen Zugriff nutzen.", - "Share files with others outside your organization via public links and email addresses. You can also share to Nextcloud accounts on other instances using their federated cloud ID." : "Dateien über öffentliche Links und E-Mail-Adressen mit anderen außerhalb Ihrer Organisation teilen. Sie können Nextcloud-Konten auch auf anderen Instanzen mithilfe ihrer föderierten Cloud-ID teilen.", + "Share files with others outside your organization via public links and email addresses. You can also share to Nextcloud accounts on other instances using their federated cloud ID." : "Dateien über öffentliche Links und E-Mail-Adressen mit anderen außerhalb Ihrer Organisation teilen. Sie können Nextcloud-Konten auch auf anderen Instanzen mithilfe ihrer Federated-Cloud-ID teilen.", "Shares from apps or other sources which are not included in internal or external shares." : "Freigaben aus Apps oder anderen Quellen, die nicht in internen oder externen Freigaben enthalten sind.", - "Type names, teams, federated cloud IDs" : "Namen, Teams oder Federierte Cloud-IDs eingeben", - "Type names or teams" : "Namen oder Federierte Cloud-IDs eingeben", - "Type a federated cloud ID" : "Eine Federierte Cloud-ID eingeben", - "Type an email" : "Eine E-Mailadresse eingeben", - "Type an email or federated cloud ID" : "Eine E-Mailadresse oder eine Federierte Cloud-ID eingeben", + "Type names, teams, federated cloud IDs" : "Namen, Teams oder Federated-Cloud-IDs eingeben", + "Type names or teams" : "Namen oder Teams eingeben", + "Type a federated cloud ID" : "Eine Federated-Cloud-ID eingeben", + "Type an email" : "Eine E-Mail-Adresse eingeben", + "Type an email or federated cloud ID" : "Eine E-Mail-Adresse oder eine Federated-Cloud-ID eingeben", "Unable to load the shares list" : "Liste der Freigaben kann nicht geladen werden", "Expires {relativetime}" : "Läuft {relativetime} ab", "this share just expired." : "Diese Freigabe ist gerade abgelaufen.", diff --git a/apps/files_sharing/l10n/pt_BR.js b/apps/files_sharing/l10n/pt_BR.js index 4f9db865df5..c308cfdb122 100644 --- a/apps/files_sharing/l10n/pt_BR.js +++ b/apps/files_sharing/l10n/pt_BR.js @@ -202,6 +202,7 @@ OC.L10N.register( "Unshare" : "Descompartilhar", "Cannot copy, please copy the link manually" : "Não é possível copiar, copie o link manualmente", "Copy internal link" : "Copiar link interno", + "For people who already have access" : "Para pessoas que já têm acesso", "Internal link" : "Link interno", "{shareWith} by {initiator}" : "{shareWith} por {initiator}", "Shared via link by {initiator}" : "Compartilhado via link por {initiator}", @@ -212,6 +213,7 @@ OC.L10N.register( "Share link ({index})" : "Link de compartilhamento ({index})", "Create public link" : "Criar link público", "Actions for \"{title}\"" : "Ações para \"{title}\"", + "Copy public link of \"{title}\"" : "Copiar link público de \"{title}\"", "Error, please enter proper password and/or expiration date" : "Erro, digite a senha correta e/ou a data de validade", "Link share created" : "Compartilhamento por link criado", "Error while creating the share" : "Erro ao criar o compartilhamento", @@ -256,6 +258,7 @@ OC.L10N.register( "Successfully uploaded files" : "Arquivos carregados com sucesso", "View terms of service" : "Ver os termos de serviço", "Terms of service" : "Termos de serviço", + "Share with {user}" : "Compartilhe com {user}", "Share with email {email}" : "Compartilhar com e-mail {email}", "Share with group" : "Compartilhar com grupo", "Share in conversation" : "Compartilhar na conversa", @@ -300,6 +303,14 @@ OC.L10N.register( "Unable to fetch inherited shares" : "Não foi possível buscar compartilhamentos herdados", "Link shares" : "Compartilhamentos por link", "Shares" : "Compartilhamentos", + "Share files within your organization. Recipients who can already view the file can also use this link for easy access." : "Compartilhe arquivos dentro da sua organização. Os destinatários que já conseguem visualizar o arquivo também podem usar este link para facilitar o acesso.", + "Share files with others outside your organization via public links and email addresses. You can also share to Nextcloud accounts on other instances using their federated cloud ID." : "Compartilhe arquivos com outras pessoas fora da sua organização por meio de links públicos e endereços de e-mail. Você também pode compartilhar com contas Nextcloud em outras instâncias usando o ID de nuvem federado delas.", + "Shares from apps or other sources which are not included in internal or external shares." : "Compartilhamentos de aplicativos ou outras fontes que não estão incluídos em compartilhamentos internos ou externos.", + "Type names, teams, federated cloud IDs" : "Digite nomes, equipes, IDs de nuvem federada", + "Type names or teams" : "Digite nomes ou equipes", + "Type a federated cloud ID" : "Digite um ID de nuvem federada", + "Type an email" : "Digite um e-mail", + "Type an email or federated cloud ID" : "Digite um e-mail ou ID de nuvem federada", "Unable to load the shares list" : "Não foi possível carregar a lista de compartilhamentos", "Expires {relativetime}" : "Expira {relativetime}", "this share just expired." : "esse compartilhamento acabou de expirar.", diff --git a/apps/files_sharing/l10n/pt_BR.json b/apps/files_sharing/l10n/pt_BR.json index 1d6f17a194f..27b631d6137 100644 --- a/apps/files_sharing/l10n/pt_BR.json +++ b/apps/files_sharing/l10n/pt_BR.json @@ -200,6 +200,7 @@ "Unshare" : "Descompartilhar", "Cannot copy, please copy the link manually" : "Não é possível copiar, copie o link manualmente", "Copy internal link" : "Copiar link interno", + "For people who already have access" : "Para pessoas que já têm acesso", "Internal link" : "Link interno", "{shareWith} by {initiator}" : "{shareWith} por {initiator}", "Shared via link by {initiator}" : "Compartilhado via link por {initiator}", @@ -210,6 +211,7 @@ "Share link ({index})" : "Link de compartilhamento ({index})", "Create public link" : "Criar link público", "Actions for \"{title}\"" : "Ações para \"{title}\"", + "Copy public link of \"{title}\"" : "Copiar link público de \"{title}\"", "Error, please enter proper password and/or expiration date" : "Erro, digite a senha correta e/ou a data de validade", "Link share created" : "Compartilhamento por link criado", "Error while creating the share" : "Erro ao criar o compartilhamento", @@ -254,6 +256,7 @@ "Successfully uploaded files" : "Arquivos carregados com sucesso", "View terms of service" : "Ver os termos de serviço", "Terms of service" : "Termos de serviço", + "Share with {user}" : "Compartilhe com {user}", "Share with email {email}" : "Compartilhar com e-mail {email}", "Share with group" : "Compartilhar com grupo", "Share in conversation" : "Compartilhar na conversa", @@ -298,6 +301,14 @@ "Unable to fetch inherited shares" : "Não foi possível buscar compartilhamentos herdados", "Link shares" : "Compartilhamentos por link", "Shares" : "Compartilhamentos", + "Share files within your organization. Recipients who can already view the file can also use this link for easy access." : "Compartilhe arquivos dentro da sua organização. Os destinatários que já conseguem visualizar o arquivo também podem usar este link para facilitar o acesso.", + "Share files with others outside your organization via public links and email addresses. You can also share to Nextcloud accounts on other instances using their federated cloud ID." : "Compartilhe arquivos com outras pessoas fora da sua organização por meio de links públicos e endereços de e-mail. Você também pode compartilhar com contas Nextcloud em outras instâncias usando o ID de nuvem federado delas.", + "Shares from apps or other sources which are not included in internal or external shares." : "Compartilhamentos de aplicativos ou outras fontes que não estão incluídos em compartilhamentos internos ou externos.", + "Type names, teams, federated cloud IDs" : "Digite nomes, equipes, IDs de nuvem federada", + "Type names or teams" : "Digite nomes ou equipes", + "Type a federated cloud ID" : "Digite um ID de nuvem federada", + "Type an email" : "Digite um e-mail", + "Type an email or federated cloud ID" : "Digite um e-mail ou ID de nuvem federada", "Unable to load the shares list" : "Não foi possível carregar a lista de compartilhamentos", "Expires {relativetime}" : "Expira {relativetime}", "this share just expired." : "esse compartilhamento acabou de expirar.", diff --git a/apps/files_sharing/l10n/sr.js b/apps/files_sharing/l10n/sr.js index 894bb17b920..a32961e5ab2 100644 --- a/apps/files_sharing/l10n/sr.js +++ b/apps/files_sharing/l10n/sr.js @@ -202,6 +202,7 @@ OC.L10N.register( "Unshare" : "Укини дељење", "Cannot copy, please copy the link manually" : "Не могу да копирам, копирајте везу ручно", "Copy internal link" : "Копирај интерну везу", + "For people who already have access" : "Особе које већ имају приступ", "Internal link" : "Интерна веза", "{shareWith} by {initiator}" : "{shareWith} od {initiator}", "Shared via link by {initiator}" : "{initiator} поделио преко везе", diff --git a/apps/files_sharing/l10n/sr.json b/apps/files_sharing/l10n/sr.json index 4a59406e5ac..af0e916dc89 100644 --- a/apps/files_sharing/l10n/sr.json +++ b/apps/files_sharing/l10n/sr.json @@ -200,6 +200,7 @@ "Unshare" : "Укини дељење", "Cannot copy, please copy the link manually" : "Не могу да копирам, копирајте везу ручно", "Copy internal link" : "Копирај интерну везу", + "For people who already have access" : "Особе које већ имају приступ", "Internal link" : "Интерна веза", "{shareWith} by {initiator}" : "{shareWith} od {initiator}", "Shared via link by {initiator}" : "{initiator} поделио преко везе", diff --git a/apps/files_sharing/l10n/zh_CN.js b/apps/files_sharing/l10n/zh_CN.js index caceb9e99af..67f2717fa86 100644 --- a/apps/files_sharing/l10n/zh_CN.js +++ b/apps/files_sharing/l10n/zh_CN.js @@ -202,6 +202,7 @@ OC.L10N.register( "Unshare" : "取消共享", "Cannot copy, please copy the link manually" : "无法复制,请手动复制链接", "Copy internal link" : "复制内部链接", + "For people who already have access" : "对于已经拥有访问权限的人员", "Internal link" : "内部链接", "{shareWith} by {initiator}" : "由 {initiator} 通过 {shareWith} 共享", "Shared via link by {initiator}" : "由 {initiator} 通过链接共享", @@ -212,6 +213,7 @@ OC.L10N.register( "Share link ({index})" : "分享链接({index})", "Create public link" : "生成公开链接地址", "Actions for \"{title}\"" : "“{title}”的动作", + "Copy public link of \"{title}\"" : "复制“{title}”的公开链接", "Error, please enter proper password and/or expiration date" : "错误,请输入正确的密码和/或过期日期", "Link share created" : "已创建链接分享", "Error while creating the share" : "创建共享时出错", @@ -256,6 +258,7 @@ OC.L10N.register( "Successfully uploaded files" : "已成功上传文件", "View terms of service" : "查看服务条款", "Terms of service" : "服务条款", + "Share with {user}" : "与 {user} 分享", "Share with email {email}" : "与邮箱 {email} 分享", "Share with group" : "分享至群组", "Share in conversation" : "分享至对话", @@ -300,6 +303,14 @@ OC.L10N.register( "Unable to fetch inherited shares" : "无法获取继承的共享", "Link shares" : "链接共享", "Shares" : "共享", + "Share files within your organization. Recipients who can already view the file can also use this link for easy access." : "在您的组织内共享文件。已经可以查看该文件的接收者也可以使用此链接轻松访问。", + "Share files with others outside your organization via public links and email addresses. You can also share to Nextcloud accounts on other instances using their federated cloud ID." : "通过公开链接和电子邮件地址与您的组织外部的其他人共享文件。您还可以使用其他实例上的联合云 ID 将其共享给 Nextcloud 账号。", + "Shares from apps or other sources which are not included in internal or external shares." : "来自应用或其他来源的共享,不包括在内部或外部共享中。", + "Type names, teams, federated cloud IDs" : "输入名称、团队、联合云 ID", + "Type names or teams" : "输入名称或团队", + "Type a federated cloud ID" : "输入联合云 ID", + "Type an email" : "输入电子邮件", + "Type an email or federated cloud ID" : "输入电子邮件或联合云 ID", "Unable to load the shares list" : "无法加载共享列表", "Expires {relativetime}" : "过期 {relativetime}", "this share just expired." : "此共享已过期。", @@ -318,6 +329,7 @@ OC.L10N.register( "Shared" : "已共享", "Shared by {ownerDisplayName}" : "由 {ownerDisplayName} 分享", "Shared multiple times with different people" : "与不同的用户多次分享", + "Sharing options" : "共享选项", "Shared with others" : "你的共享", "Create file request" : "创建文件请求", "Upload files to {foldername}" : "将文件上传到 {foldername}", diff --git a/apps/files_sharing/l10n/zh_CN.json b/apps/files_sharing/l10n/zh_CN.json index 39c2876cfd4..6c561b3d5ed 100644 --- a/apps/files_sharing/l10n/zh_CN.json +++ b/apps/files_sharing/l10n/zh_CN.json @@ -200,6 +200,7 @@ "Unshare" : "取消共享", "Cannot copy, please copy the link manually" : "无法复制,请手动复制链接", "Copy internal link" : "复制内部链接", + "For people who already have access" : "对于已经拥有访问权限的人员", "Internal link" : "内部链接", "{shareWith} by {initiator}" : "由 {initiator} 通过 {shareWith} 共享", "Shared via link by {initiator}" : "由 {initiator} 通过链接共享", @@ -210,6 +211,7 @@ "Share link ({index})" : "分享链接({index})", "Create public link" : "生成公开链接地址", "Actions for \"{title}\"" : "“{title}”的动作", + "Copy public link of \"{title}\"" : "复制“{title}”的公开链接", "Error, please enter proper password and/or expiration date" : "错误,请输入正确的密码和/或过期日期", "Link share created" : "已创建链接分享", "Error while creating the share" : "创建共享时出错", @@ -254,6 +256,7 @@ "Successfully uploaded files" : "已成功上传文件", "View terms of service" : "查看服务条款", "Terms of service" : "服务条款", + "Share with {user}" : "与 {user} 分享", "Share with email {email}" : "与邮箱 {email} 分享", "Share with group" : "分享至群组", "Share in conversation" : "分享至对话", @@ -298,6 +301,14 @@ "Unable to fetch inherited shares" : "无法获取继承的共享", "Link shares" : "链接共享", "Shares" : "共享", + "Share files within your organization. Recipients who can already view the file can also use this link for easy access." : "在您的组织内共享文件。已经可以查看该文件的接收者也可以使用此链接轻松访问。", + "Share files with others outside your organization via public links and email addresses. You can also share to Nextcloud accounts on other instances using their federated cloud ID." : "通过公开链接和电子邮件地址与您的组织外部的其他人共享文件。您还可以使用其他实例上的联合云 ID 将其共享给 Nextcloud 账号。", + "Shares from apps or other sources which are not included in internal or external shares." : "来自应用或其他来源的共享,不包括在内部或外部共享中。", + "Type names, teams, federated cloud IDs" : "输入名称、团队、联合云 ID", + "Type names or teams" : "输入名称或团队", + "Type a federated cloud ID" : "输入联合云 ID", + "Type an email" : "输入电子邮件", + "Type an email or federated cloud ID" : "输入电子邮件或联合云 ID", "Unable to load the shares list" : "无法加载共享列表", "Expires {relativetime}" : "过期 {relativetime}", "this share just expired." : "此共享已过期。", @@ -316,6 +327,7 @@ "Shared" : "已共享", "Shared by {ownerDisplayName}" : "由 {ownerDisplayName} 分享", "Shared multiple times with different people" : "与不同的用户多次分享", + "Sharing options" : "共享选项", "Shared with others" : "你的共享", "Create file request" : "创建文件请求", "Upload files to {foldername}" : "将文件上传到 {foldername}", diff --git a/apps/files_sharing/lib/MountProvider.php b/apps/files_sharing/lib/MountProvider.php index 7a0b1f135a6..b7b0582493e 100644 --- a/apps/files_sharing/lib/MountProvider.php +++ b/apps/files_sharing/lib/MountProvider.php @@ -46,13 +46,14 @@ class MountProvider implements IMountProvider { * @return IMountPoint[] */ public function getMountsForUser(IUser $user, IStorageFactory $loader) { - $shares = $this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_USER, null, -1); - $shares = array_merge($shares, $this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_GROUP, null, -1)); - $shares = array_merge($shares, $this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_CIRCLE, null, -1)); - $shares = array_merge($shares, $this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_ROOM, null, -1)); - $shares = array_merge($shares, $this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_DECK, null, -1)); - $shares = array_merge($shares, $this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_SCIENCEMESH, null, -1)); - + $shares = array_merge( + $this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_USER, null, -1), + $this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_GROUP, null, -1), + $this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_CIRCLE, null, -1), + $this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_ROOM, null, -1), + $this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_DECK, null, -1), + $this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_SCIENCEMESH, null, -1), + ); // filter out excluded shares and group shares that includes self $shares = array_filter($shares, function (IShare $share) use ($user) { diff --git a/apps/files_trashbin/l10n/es.js b/apps/files_trashbin/l10n/es.js index 7a539db183c..21b6a9498b3 100644 --- a/apps/files_trashbin/l10n/es.js +++ b/apps/files_trashbin/l10n/es.js @@ -8,7 +8,7 @@ OC.L10N.register( "This application enables people to restore files that were deleted from the system. It displays a list of deleted files in the web interface, and has options to restore those deleted files back to the people file directories or remove them permanently from the system. Restoring a file also restores related file versions, if the versions application is enabled. When a file is deleted from a share, it can be restored in the same manner, though it is no longer shared. By default, these files remain in the trash bin for 30 days.\nTo prevent an account from running out of disk space, the Deleted files app will not utilize more than 50% of the currently available free quota for deleted files. If the deleted files exceed this limit, the app deletes the oldest files until it gets below this limit. More information is available in the Deleted Files documentation." : "Esta aplicación permite a los usuarios restaurar archivos que hayan sido eliminados del sistema. Muestra una lista de archivos eliminados en la interfaz web y ofrece opciones para restaurar esos archivos eliminados a los directorios de los usuarios o eliminarlos permanentemente del sistema. Al restaurar un archivo, también se restauran las versiones relacionadas del archivo, si la aplicación de versiones está habilitada. Cuando se elimina un archivo de un recurso compartido, también se puede restaurar de la misma manera, aunque ya no estará compartido. Por defecto, estos archivos permanecen en la papelera de reciclaje durante 30 días.\nPara evitar que una cuenta se quede sin espacio en el disco, la aplicación de archivos eliminados no utilizará más del 50% de la cuota disponible actualmente para los archivos eliminados. Si los archivos eliminados superan este límite, la aplicación eliminará los archivos más antiguos hasta que esté por debajo de este límite. Más información está disponible en la documentación de Archivos Eliminados.", "Restore" : "Recuperar", "Not enough free space to restore the file/folder" : "No hay espacio libre suficiente para restaurar el archivo/carpeta", - "Empty deleted files" : "Archivos eliminados vacío", + "Empty deleted files" : "Vaciar archivos eliminados", "Confirm permanent deletion" : "Confimar borrado permanente", "Are you sure you want to permanently delete all files and folders in the trash? This cannot be undone." : "¿Estás seguro de que quieres eliminar permanentemente todos los archivos y carpetas de la papelera? Esta acción no se puede deshacer.", "Cancel" : "Cancelar", diff --git a/apps/files_trashbin/l10n/es.json b/apps/files_trashbin/l10n/es.json index 98e08e54e8a..26834a43645 100644 --- a/apps/files_trashbin/l10n/es.json +++ b/apps/files_trashbin/l10n/es.json @@ -6,7 +6,7 @@ "This application enables people to restore files that were deleted from the system. It displays a list of deleted files in the web interface, and has options to restore those deleted files back to the people file directories or remove them permanently from the system. Restoring a file also restores related file versions, if the versions application is enabled. When a file is deleted from a share, it can be restored in the same manner, though it is no longer shared. By default, these files remain in the trash bin for 30 days.\nTo prevent an account from running out of disk space, the Deleted files app will not utilize more than 50% of the currently available free quota for deleted files. If the deleted files exceed this limit, the app deletes the oldest files until it gets below this limit. More information is available in the Deleted Files documentation." : "Esta aplicación permite a los usuarios restaurar archivos que hayan sido eliminados del sistema. Muestra una lista de archivos eliminados en la interfaz web y ofrece opciones para restaurar esos archivos eliminados a los directorios de los usuarios o eliminarlos permanentemente del sistema. Al restaurar un archivo, también se restauran las versiones relacionadas del archivo, si la aplicación de versiones está habilitada. Cuando se elimina un archivo de un recurso compartido, también se puede restaurar de la misma manera, aunque ya no estará compartido. Por defecto, estos archivos permanecen en la papelera de reciclaje durante 30 días.\nPara evitar que una cuenta se quede sin espacio en el disco, la aplicación de archivos eliminados no utilizará más del 50% de la cuota disponible actualmente para los archivos eliminados. Si los archivos eliminados superan este límite, la aplicación eliminará los archivos más antiguos hasta que esté por debajo de este límite. Más información está disponible en la documentación de Archivos Eliminados.", "Restore" : "Recuperar", "Not enough free space to restore the file/folder" : "No hay espacio libre suficiente para restaurar el archivo/carpeta", - "Empty deleted files" : "Archivos eliminados vacío", + "Empty deleted files" : "Vaciar archivos eliminados", "Confirm permanent deletion" : "Confimar borrado permanente", "Are you sure you want to permanently delete all files and folders in the trash? This cannot be undone." : "¿Estás seguro de que quieres eliminar permanentemente todos los archivos y carpetas de la papelera? Esta acción no se puede deshacer.", "Cancel" : "Cancelar", diff --git a/apps/provisioning_api/l10n/es.js b/apps/provisioning_api/l10n/es.js index c8706c3cde7..2231da87776 100644 --- a/apps/provisioning_api/l10n/es.js +++ b/apps/provisioning_api/l10n/es.js @@ -11,13 +11,16 @@ OC.L10N.register( "Cannot create sub-admins for admin group" : "No se pueden crear subadministradores para el grupo admin", "No permissions to promote sub-admins" : "No tiene permisos para promover subadministradores", "Invalid password value" : "Valor de contraseña inválido", + "An email address is required, to send a password link to the user." : "Se requiere una dirección de correo electrónico, para enviar un enlace de gestión de contraseña al usuario.", "Required email address was not provided" : "La dirección de correo electrónico requerida no fue proporcionada", + "User creation failed" : "Fallo la creación del usuario", "Invalid quota value: %1$s" : "Valor de cuota inválido: %1$s", "Invalid quota value. %1$s is exceeding the maximum quota" : "Valor de cuota inválido. %1$s excede la cuota máxima", "Unlimited quota is forbidden on this instance" : "La cuota ilimitada está prohibida en esta instancia", "Setting the password is not supported by the users backend" : "El backend de los usuarios no soporta establecer la contraseña", "Invalid language" : "Idioma inválido", "Invalid locale" : "Configuración regional inválida", + "Invalid first day of week" : "Primer día de la semana inválido", "Cannot remove yourself from the admin group" : "No se puede remover a Ud. mismo del grupo de administración", "Cannot remove yourself from this group as you are a sub-admin" : "No se puede remover así mismo de este grupo dado que Ud. es un subadministrador", "Not viable to remove user from the last group you are sub-admin of" : "No es viable remover el usuario del último grupo del que Ud. es subadministrador", diff --git a/apps/provisioning_api/l10n/es.json b/apps/provisioning_api/l10n/es.json index 791714eae46..869f3b9ab9d 100644 --- a/apps/provisioning_api/l10n/es.json +++ b/apps/provisioning_api/l10n/es.json @@ -9,13 +9,16 @@ "Cannot create sub-admins for admin group" : "No se pueden crear subadministradores para el grupo admin", "No permissions to promote sub-admins" : "No tiene permisos para promover subadministradores", "Invalid password value" : "Valor de contraseña inválido", + "An email address is required, to send a password link to the user." : "Se requiere una dirección de correo electrónico, para enviar un enlace de gestión de contraseña al usuario.", "Required email address was not provided" : "La dirección de correo electrónico requerida no fue proporcionada", + "User creation failed" : "Fallo la creación del usuario", "Invalid quota value: %1$s" : "Valor de cuota inválido: %1$s", "Invalid quota value. %1$s is exceeding the maximum quota" : "Valor de cuota inválido. %1$s excede la cuota máxima", "Unlimited quota is forbidden on this instance" : "La cuota ilimitada está prohibida en esta instancia", "Setting the password is not supported by the users backend" : "El backend de los usuarios no soporta establecer la contraseña", "Invalid language" : "Idioma inválido", "Invalid locale" : "Configuración regional inválida", + "Invalid first day of week" : "Primer día de la semana inválido", "Cannot remove yourself from the admin group" : "No se puede remover a Ud. mismo del grupo de administración", "Cannot remove yourself from this group as you are a sub-admin" : "No se puede remover así mismo de este grupo dado que Ud. es un subadministrador", "Not viable to remove user from the last group you are sub-admin of" : "No es viable remover el usuario del último grupo del que Ud. es subadministrador", diff --git a/apps/settings/l10n/de.js b/apps/settings/l10n/de.js index 0f6c53e01fc..8d05ef4fd89 100644 --- a/apps/settings/l10n/de.js +++ b/apps/settings/l10n/de.js @@ -110,7 +110,7 @@ OC.L10N.register( "Administration" : "Administration", "Users" : "Konten", "Additional settings" : "Zusätzliche Einstellungen", - "Assistant" : "Benötigt keine Übersetzung. Hier wird nur die formelle Übersetzung verwendet (de_DE).", + "Assistant" : "Assistent", "Administration privileges" : "Administrationsrechte", "Groupware" : "Groupware", "Overview" : "Übersicht", @@ -420,10 +420,12 @@ OC.L10N.register( "Excluded groups" : "Ausgeschlossene Gruppen", "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced." : "Bei der Auswahl/Abwahl von Gruppen wird folgende Logik verwendet, um festzustellen, ob ein Konto 2FA verwenden muss: Wenn keine Gruppe ausgewählt ist, dann wird 2FA für alle Konten aktiviert, außer für solche der ausgenommenen Gruppen. Sind Gruppen ausgewählt, so wird 2FA für alle Kontendieser Gruppen aktiviert. Ist ein Konto sowohl in einer ausgewählten als auch in einer ausgenommenen Gruppe, so hat die Auswahl Vorrang und 2FA wird aktiviert.", "Save changes" : "Änderungen speichern", + "Choose Deploy Daemon for {appName}" : "Bereitstellungs-Daemon für {appName} wählen", "Default" : "Standard", "Registered Deploy daemons list" : "Liste von registrierten Bereitstellungsdaemons", "No Deploy daemons configured" : "Keine Bereitstellungsdämonen konfiguriert", - "Register a custom one or setup from available templates" : "Registriere eine benutzerdefinierte Vorlage oder richte sie aus verfügbaren Vorlagen ein", + "Register a custom one or setup from available templates" : "Eine benutzerdefinierte Vorlage registrieren oder aus einer verfügbaren Vorlagen einrichten", + "Manage Deploy daemons" : "Bereitstellungs-Daemons verwalten", "Show details for {appName} app" : "Details der App {appName} anzeigen", "Update to {update}" : "Aktualisieren auf {update}", "Remove" : "Entfernen", diff --git a/apps/settings/l10n/de.json b/apps/settings/l10n/de.json index 81fe3298433..878c2822a49 100644 --- a/apps/settings/l10n/de.json +++ b/apps/settings/l10n/de.json @@ -108,7 +108,7 @@ "Administration" : "Administration", "Users" : "Konten", "Additional settings" : "Zusätzliche Einstellungen", - "Assistant" : "Benötigt keine Übersetzung. Hier wird nur die formelle Übersetzung verwendet (de_DE).", + "Assistant" : "Assistent", "Administration privileges" : "Administrationsrechte", "Groupware" : "Groupware", "Overview" : "Übersicht", @@ -418,10 +418,12 @@ "Excluded groups" : "Ausgeschlossene Gruppen", "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced." : "Bei der Auswahl/Abwahl von Gruppen wird folgende Logik verwendet, um festzustellen, ob ein Konto 2FA verwenden muss: Wenn keine Gruppe ausgewählt ist, dann wird 2FA für alle Konten aktiviert, außer für solche der ausgenommenen Gruppen. Sind Gruppen ausgewählt, so wird 2FA für alle Kontendieser Gruppen aktiviert. Ist ein Konto sowohl in einer ausgewählten als auch in einer ausgenommenen Gruppe, so hat die Auswahl Vorrang und 2FA wird aktiviert.", "Save changes" : "Änderungen speichern", + "Choose Deploy Daemon for {appName}" : "Bereitstellungs-Daemon für {appName} wählen", "Default" : "Standard", "Registered Deploy daemons list" : "Liste von registrierten Bereitstellungsdaemons", "No Deploy daemons configured" : "Keine Bereitstellungsdämonen konfiguriert", - "Register a custom one or setup from available templates" : "Registriere eine benutzerdefinierte Vorlage oder richte sie aus verfügbaren Vorlagen ein", + "Register a custom one or setup from available templates" : "Eine benutzerdefinierte Vorlage registrieren oder aus einer verfügbaren Vorlagen einrichten", + "Manage Deploy daemons" : "Bereitstellungs-Daemons verwalten", "Show details for {appName} app" : "Details der App {appName} anzeigen", "Update to {update}" : "Aktualisieren auf {update}", "Remove" : "Entfernen", diff --git a/apps/settings/l10n/de_DE.js b/apps/settings/l10n/de_DE.js index 8372aec845d..4b20c111333 100644 --- a/apps/settings/l10n/de_DE.js +++ b/apps/settings/l10n/de_DE.js @@ -420,10 +420,12 @@ OC.L10N.register( "Excluded groups" : "Ausgeschlossene Gruppen", "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced." : "Bei der Auswahl/Abwahl von Gruppen wird folgende Logik verwendet, um festzustellen, ob ein Konto 2FA verwenden muss: Wenn keine Gruppe ausgewählt ist, dann wird 2FA für alle Konten aktiviert, außer für Konten der ausgenommenen Gruppen. Sind Gruppen ausgewählt, so wird 2FA für alle Konten dieser Gruppen aktiviert. Ist ein Konto sowohl in einer ausgewählten als auch in einer ausgenommenen Gruppe, so hat die Auswahl Vorrang und 2FA wird aktiviert.", "Save changes" : "Änderungen speichern ", + "Choose Deploy Daemon for {appName}" : "Bereitstellungs-Daemon für {appName} wählen", "Default" : "Standard", "Registered Deploy daemons list" : "Liste von registrierten Bereitstellungsdaemons", - "No Deploy daemons configured" : "Keine Bereitstellungsdämonen konfiguriert", - "Register a custom one or setup from available templates" : "Registrieren Sie eine benutzerdefinierte Vorlage oder richten Sie sie aus verfügbaren Vorlagen ein", + "No Deploy daemons configured" : "Keine Bereitstellungs-Daemons konfiguriert", + "Register a custom one or setup from available templates" : "Eine benutzerdefinierte Vorlage registrieren oder aus einer der verfügbaren Vorlagen einrichten", + "Manage Deploy daemons" : "Bereitstellungs-Daemone verwalten", "Show details for {appName} app" : "Details für die App {appName} anzeigen", "Update to {update}" : "Aktualisieren auf {update}", "Remove" : "Entfernen", diff --git a/apps/settings/l10n/de_DE.json b/apps/settings/l10n/de_DE.json index 3a9fd905c36..8b67f6c8169 100644 --- a/apps/settings/l10n/de_DE.json +++ b/apps/settings/l10n/de_DE.json @@ -418,10 +418,12 @@ "Excluded groups" : "Ausgeschlossene Gruppen", "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced." : "Bei der Auswahl/Abwahl von Gruppen wird folgende Logik verwendet, um festzustellen, ob ein Konto 2FA verwenden muss: Wenn keine Gruppe ausgewählt ist, dann wird 2FA für alle Konten aktiviert, außer für Konten der ausgenommenen Gruppen. Sind Gruppen ausgewählt, so wird 2FA für alle Konten dieser Gruppen aktiviert. Ist ein Konto sowohl in einer ausgewählten als auch in einer ausgenommenen Gruppe, so hat die Auswahl Vorrang und 2FA wird aktiviert.", "Save changes" : "Änderungen speichern ", + "Choose Deploy Daemon for {appName}" : "Bereitstellungs-Daemon für {appName} wählen", "Default" : "Standard", "Registered Deploy daemons list" : "Liste von registrierten Bereitstellungsdaemons", - "No Deploy daemons configured" : "Keine Bereitstellungsdämonen konfiguriert", - "Register a custom one or setup from available templates" : "Registrieren Sie eine benutzerdefinierte Vorlage oder richten Sie sie aus verfügbaren Vorlagen ein", + "No Deploy daemons configured" : "Keine Bereitstellungs-Daemons konfiguriert", + "Register a custom one or setup from available templates" : "Eine benutzerdefinierte Vorlage registrieren oder aus einer der verfügbaren Vorlagen einrichten", + "Manage Deploy daemons" : "Bereitstellungs-Daemone verwalten", "Show details for {appName} app" : "Details für die App {appName} anzeigen", "Update to {update}" : "Aktualisieren auf {update}", "Remove" : "Entfernen", diff --git a/apps/settings/l10n/en_GB.js b/apps/settings/l10n/en_GB.js index d3657944451..72ce4dd2a2d 100644 --- a/apps/settings/l10n/en_GB.js +++ b/apps/settings/l10n/en_GB.js @@ -420,10 +420,12 @@ OC.L10N.register( "Excluded groups" : "Excluded groups", "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced." : "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced.", "Save changes" : "Save changes", + "Choose Deploy Daemon for {appName}" : "Choose Deploy Daemon for {appName}", "Default" : "Default", "Registered Deploy daemons list" : "Registered Deploy daemons list", "No Deploy daemons configured" : "No Deploy daemons configured", "Register a custom one or setup from available templates" : "Register a custom one or setup from available templates", + "Manage Deploy daemons" : "Manage Deploy daemons", "Show details for {appName} app" : "Show details for {appName} app", "Update to {update}" : "Update to {update}", "Remove" : "Remove", diff --git a/apps/settings/l10n/en_GB.json b/apps/settings/l10n/en_GB.json index cc5761aef9a..7032d0834cf 100644 --- a/apps/settings/l10n/en_GB.json +++ b/apps/settings/l10n/en_GB.json @@ -418,10 +418,12 @@ "Excluded groups" : "Excluded groups", "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced." : "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced.", "Save changes" : "Save changes", + "Choose Deploy Daemon for {appName}" : "Choose Deploy Daemon for {appName}", "Default" : "Default", "Registered Deploy daemons list" : "Registered Deploy daemons list", "No Deploy daemons configured" : "No Deploy daemons configured", "Register a custom one or setup from available templates" : "Register a custom one or setup from available templates", + "Manage Deploy daemons" : "Manage Deploy daemons", "Show details for {appName} app" : "Show details for {appName} app", "Update to {update}" : "Update to {update}", "Remove" : "Remove", diff --git a/apps/settings/l10n/es.js b/apps/settings/l10n/es.js index 5e8c5938a8c..819255af0a4 100644 --- a/apps/settings/l10n/es.js +++ b/apps/settings/l10n/es.js @@ -420,10 +420,12 @@ OC.L10N.register( "Excluded groups" : "Grupos excluidos", "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced." : "Cuando los grupos se seleccionan/excluyen, usan la siguiente lógica para determinar si ua cuenta tiene obligada la 2FA: si no hay grupos seleccionados, 2FA está activa para todos excepto los miembros de los grupos excluidos. Si hay grupos seleccionados, 2FA está activa para todos los miembros de estos. Si una cuenta está a la vez en un grupo seleccionado y otro excluido, el seleccionado tiene preferencia y se obliga la 2FA.", "Save changes" : "Guardar cambios", + "Choose Deploy Daemon for {appName}" : "Escoge el Daemon de despliegue para {appName}", "Default" : "Predeterminado", "Registered Deploy daemons list" : "Lista de daemons de despliegue registrados", "No Deploy daemons configured" : "No hay daemons de despliegue configurados", "Register a custom one or setup from available templates" : "Registre uno personalizado, o, configure desde las plantillas disponibles", + "Manage Deploy daemons" : "Gestionar Daemons de despliegue", "Show details for {appName} app" : "Mostrar detalles para la app {appName}", "Update to {update}" : "Actualizar a {update}", "Remove" : "Eliminar", diff --git a/apps/settings/l10n/es.json b/apps/settings/l10n/es.json index d79ca75b6d6..642c4c5d3b4 100644 --- a/apps/settings/l10n/es.json +++ b/apps/settings/l10n/es.json @@ -418,10 +418,12 @@ "Excluded groups" : "Grupos excluidos", "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced." : "Cuando los grupos se seleccionan/excluyen, usan la siguiente lógica para determinar si ua cuenta tiene obligada la 2FA: si no hay grupos seleccionados, 2FA está activa para todos excepto los miembros de los grupos excluidos. Si hay grupos seleccionados, 2FA está activa para todos los miembros de estos. Si una cuenta está a la vez en un grupo seleccionado y otro excluido, el seleccionado tiene preferencia y se obliga la 2FA.", "Save changes" : "Guardar cambios", + "Choose Deploy Daemon for {appName}" : "Escoge el Daemon de despliegue para {appName}", "Default" : "Predeterminado", "Registered Deploy daemons list" : "Lista de daemons de despliegue registrados", "No Deploy daemons configured" : "No hay daemons de despliegue configurados", "Register a custom one or setup from available templates" : "Registre uno personalizado, o, configure desde las plantillas disponibles", + "Manage Deploy daemons" : "Gestionar Daemons de despliegue", "Show details for {appName} app" : "Mostrar detalles para la app {appName}", "Update to {update}" : "Actualizar a {update}", "Remove" : "Eliminar", diff --git a/apps/settings/l10n/pl.js b/apps/settings/l10n/pl.js index 97798f4033a..fa8e66874c1 100644 --- a/apps/settings/l10n/pl.js +++ b/apps/settings/l10n/pl.js @@ -420,7 +420,12 @@ OC.L10N.register( "Excluded groups" : "Wyłączone grupy", "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced." : "Gdy grupy są zaznaczone lub wykluczone, stosowana jest następująca logika w celu określenia, czy dla konta ma być wymuszona 2FA:. Jeśli nie wybrano żadnych grup, 2FA jest włączone dla wszystkich, z wyjątkiem członków wykluczonych grup. Jeśli grupy są wybrane, 2FA jest włączone dla wszystkich ich członków. Jeśli konto należy zarówno do grupy wybranej, jak i wykluczonej, priorytet ma grupa wybrana i 2FA jest wymuszane.", "Save changes" : "Zapisz zmiany", + "Choose Deploy Daemon for {appName}" : "Wybierz demona wdrożeniowego dla {appName}", "Default" : "Domyślny", + "Registered Deploy daemons list" : "Lista zarejestrowanych demonów wdrożeniowych", + "No Deploy daemons configured" : "Brak skonfigurowanych demonów wdrożeniowych", + "Register a custom one or setup from available templates" : "Zarejestruj własnego lub skonfiguruj z dostępnych szablonów", + "Manage Deploy daemons" : "Zarządzaj demonami wdrożeniowymi", "Show details for {appName} app" : "Pokaż szczegóły aplikacji {appName}", "Update to {update}" : "Zaktualizuj do {update}", "Remove" : "Usuń", diff --git a/apps/settings/l10n/pl.json b/apps/settings/l10n/pl.json index d22c9232ce9..db6334e6d5d 100644 --- a/apps/settings/l10n/pl.json +++ b/apps/settings/l10n/pl.json @@ -418,7 +418,12 @@ "Excluded groups" : "Wyłączone grupy", "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced." : "Gdy grupy są zaznaczone lub wykluczone, stosowana jest następująca logika w celu określenia, czy dla konta ma być wymuszona 2FA:. Jeśli nie wybrano żadnych grup, 2FA jest włączone dla wszystkich, z wyjątkiem członków wykluczonych grup. Jeśli grupy są wybrane, 2FA jest włączone dla wszystkich ich członków. Jeśli konto należy zarówno do grupy wybranej, jak i wykluczonej, priorytet ma grupa wybrana i 2FA jest wymuszane.", "Save changes" : "Zapisz zmiany", + "Choose Deploy Daemon for {appName}" : "Wybierz demona wdrożeniowego dla {appName}", "Default" : "Domyślny", + "Registered Deploy daemons list" : "Lista zarejestrowanych demonów wdrożeniowych", + "No Deploy daemons configured" : "Brak skonfigurowanych demonów wdrożeniowych", + "Register a custom one or setup from available templates" : "Zarejestruj własnego lub skonfiguruj z dostępnych szablonów", + "Manage Deploy daemons" : "Zarządzaj demonami wdrożeniowymi", "Show details for {appName} app" : "Pokaż szczegóły aplikacji {appName}", "Update to {update}" : "Zaktualizuj do {update}", "Remove" : "Usuń", diff --git a/apps/settings/l10n/pt_BR.js b/apps/settings/l10n/pt_BR.js index 0484828ab7d..b6bd95b29da 100644 --- a/apps/settings/l10n/pt_BR.js +++ b/apps/settings/l10n/pt_BR.js @@ -345,6 +345,7 @@ OC.L10N.register( "Unified task processing" : "Processamento unificado de tarefas", "AI tasks can be implemented by different apps. Here you can set which app should be used for which task." : "As tarefas de IA podem ser implementadas por aplicativos diferentes. Aqui você pode definir qual aplicativo deve ser usado para qual tarefa.", "Allow AI usage for guest users" : "Permitir o uso de IA para usuários convidados", + "Provider for Task types" : "Provedor para tipos de tarefas", "Enable" : "Ativar", "None of your currently installed apps provide Task processing functionality" : "Nenhum dos seus aplicativos instalados atualmente oferece funcionalidade de processamento de Tarefas", "Machine translation" : "Traduções automáticas", @@ -419,10 +420,12 @@ OC.L10N.register( "Excluded groups" : "Grupos excluídos", "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced." : "Quando os grupos são selecionados/excluídos, eles usam a seguinte lógica para determinar se uma conta tem a 2FA aplicada: Se nenhum grupo for selecionado, a 2FA será habilitada para todos, exceto os membros dos grupos excluídos. Se grupos forem selecionados, a 2FA será habilitada para todos os membros deles. Se uma conta estiver em um grupo selecionado e excluído, o selecionado terá precedência e a 2FA será aplicada.", "Save changes" : "Salvar alterações", + "Choose Deploy Daemon for {appName}" : "Escolha Implantar Daemon para{appName}", "Default" : "Padrão", "Registered Deploy daemons list" : "Lista de Deploy daemons registrados", "No Deploy daemons configured" : "Nenhum Deploy daemon configurado", "Register a custom one or setup from available templates" : "Registre um personalizado ou configure a partir dos modelos disponíveis", + "Manage Deploy daemons" : "Gerenciar daemons de implantação", "Show details for {appName} app" : "Mostrar detalhes do aplicativo {appName}", "Update to {update}" : "Atualizar para {update}", "Remove" : "Excluir", @@ -624,6 +627,7 @@ OC.L10N.register( "Your biography. Markdown is supported." : "Sua biografia. Markdown é suportado.", "Unable to update date of birth" : "Não foi possível atualizar a data de nascimento", "Enter your date of birth" : "Digite sua data de nascimento", + "Bluesky handle" : "Alça Bluesky", "You are using {s}{usage}{/s}" : "Você está usando {s}{usage}{/s}", "You are using {s}{usage}{/s} of {s}{totalSpace}{/s} ({s}{usageRelative}%{/s})" : "Você está usando {s}{usage}{/s} de {s}{totalSpace}{/s} ({s}{usageRelative}%{/s})", "You are a member of the following groups:" : "Você é membro dos seguintes grupos:", diff --git a/apps/settings/l10n/pt_BR.json b/apps/settings/l10n/pt_BR.json index ba161dd1c8f..9742ef9df72 100644 --- a/apps/settings/l10n/pt_BR.json +++ b/apps/settings/l10n/pt_BR.json @@ -343,6 +343,7 @@ "Unified task processing" : "Processamento unificado de tarefas", "AI tasks can be implemented by different apps. Here you can set which app should be used for which task." : "As tarefas de IA podem ser implementadas por aplicativos diferentes. Aqui você pode definir qual aplicativo deve ser usado para qual tarefa.", "Allow AI usage for guest users" : "Permitir o uso de IA para usuários convidados", + "Provider for Task types" : "Provedor para tipos de tarefas", "Enable" : "Ativar", "None of your currently installed apps provide Task processing functionality" : "Nenhum dos seus aplicativos instalados atualmente oferece funcionalidade de processamento de Tarefas", "Machine translation" : "Traduções automáticas", @@ -417,10 +418,12 @@ "Excluded groups" : "Grupos excluídos", "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced." : "Quando os grupos são selecionados/excluídos, eles usam a seguinte lógica para determinar se uma conta tem a 2FA aplicada: Se nenhum grupo for selecionado, a 2FA será habilitada para todos, exceto os membros dos grupos excluídos. Se grupos forem selecionados, a 2FA será habilitada para todos os membros deles. Se uma conta estiver em um grupo selecionado e excluído, o selecionado terá precedência e a 2FA será aplicada.", "Save changes" : "Salvar alterações", + "Choose Deploy Daemon for {appName}" : "Escolha Implantar Daemon para{appName}", "Default" : "Padrão", "Registered Deploy daemons list" : "Lista de Deploy daemons registrados", "No Deploy daemons configured" : "Nenhum Deploy daemon configurado", "Register a custom one or setup from available templates" : "Registre um personalizado ou configure a partir dos modelos disponíveis", + "Manage Deploy daemons" : "Gerenciar daemons de implantação", "Show details for {appName} app" : "Mostrar detalhes do aplicativo {appName}", "Update to {update}" : "Atualizar para {update}", "Remove" : "Excluir", @@ -622,6 +625,7 @@ "Your biography. Markdown is supported." : "Sua biografia. Markdown é suportado.", "Unable to update date of birth" : "Não foi possível atualizar a data de nascimento", "Enter your date of birth" : "Digite sua data de nascimento", + "Bluesky handle" : "Alça Bluesky", "You are using {s}{usage}{/s}" : "Você está usando {s}{usage}{/s}", "You are using {s}{usage}{/s} of {s}{totalSpace}{/s} ({s}{usageRelative}%{/s})" : "Você está usando {s}{usage}{/s} de {s}{totalSpace}{/s} ({s}{usageRelative}%{/s})", "You are a member of the following groups:" : "Você é membro dos seguintes grupos:", diff --git a/apps/settings/l10n/sr.js b/apps/settings/l10n/sr.js index 99361f3eefe..19a35ecb06f 100644 --- a/apps/settings/l10n/sr.js +++ b/apps/settings/l10n/sr.js @@ -345,6 +345,7 @@ OC.L10N.register( "Unified task processing" : "Обједињена обрада задатака", "AI tasks can be implemented by different apps. Here you can set which app should be used for which task." : "AI задатке могу да имплементирају разне апликације. Овде можете да подесите која ће се користити за који задатак.", "Allow AI usage for guest users" : "Дозволи да корисници гости користе AI", + "Provider for Task types" : "Пружалац услуге за Типове задатака", "Enable" : "Укључи", "None of your currently installed apps provide Task processing functionality" : "Ниједна од ваших тренутно инсталираних апликација не пружа функционалност обраде задатака", "Machine translation" : "Машинско превођење", @@ -371,6 +372,8 @@ OC.L10N.register( "Users will still be able to screenshot or record the screen. This does not provide any definitive protection." : "Корисници ће ипак моћи да направе слику екрана или да сниме екран. Ово не представља дефинитивну заштиту.", "Allow users to share via link and emails" : "Дозволи корисницима да деле путем линка и и-мејлова", "Allow public uploads" : "Дозволи јавна отпремања", + "Allow public shares to be added to other clouds by federation." : "Дозволи да се јавна дељења здруживањем додају на остале облаке.", + "This will add share permissions to all newly created link shares." : "Ово ће додати дозволе дељења свим новокреираним дељењима линком.", "Always ask for a password" : "Увек питај за лозинку", "Enforce password protection" : "Захтевај заштиту лозинком", "Exclude groups from password requirements" : "Изузми групе из захтева за лозинком", @@ -394,6 +397,7 @@ OC.L10N.register( "Default expiration time of remote shares in days" : "Подразумевано време трајања удаљених дељења у данима", "Expire remote shares after x days" : "Нека удаљена дељења истекну након х дана", "Set default expiration date for shares via link or mail" : "Постави подразумевано време истека за дељења путем линка или мејла", + "Enforce expiration date for link or mail shares" : "Форсирај датум истека за дељења линком или мејлом", "Default expiration time of shares in days" : "Подразумевано време трајања дељења у данима", "Privacy settings for sharing" : "Поставке приватности за дељење", "Allow account name autocompletion in share dialog and allow access to the system address book" : "Омогући аутоматско довршавање имена налога у дијалогу дељења и дозволи приступ системском адресару", @@ -416,10 +420,12 @@ OC.L10N.register( "Excluded groups" : "Искључене групе", "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced." : "Када се групе означе/искључе, користи се следећа логика да се закључи да ли се за налог захтева 2FA: ако није одабрана ниједна група, 2FA је укључен свима осим члановима искључених група. Ако има одабраних група, 2FA је укључен само њиховим члановима. Ако је налог у исто време у одабраној и у искљученој групи, одабрана група има предност и 2FA се захтева.", "Save changes" : "Сними измене", + "Choose Deploy Daemon for {appName}" : "Изабери Даемон за постављање за {appName}", "Default" : "Подразумевано", "Registered Deploy daemons list" : "Листа регистрованих Даемона за постављање", "No Deploy daemons configured" : "Није подешен ниједан Даемон за постављање", "Register a custom one or setup from available templates" : "Региструјте произвољни или подесите неки према доступном шаблону", + "Manage Deploy daemons" : "Управљај Даемонима за постављање", "Show details for {appName} app" : "Прикажи детаље апликације {appName}", "Update to {update}" : "Ажурирај на {update}", "Remove" : "Уклони", @@ -621,6 +627,7 @@ OC.L10N.register( "Your biography. Markdown is supported." : "Ваша биографија. Подржава се употреба Markdown означавања.", "Unable to update date of birth" : "Није успело ажурирање датума рођења", "Enter your date of birth" : "Унесите датум свог рођења", + "Bluesky handle" : "Bluesky ручка", "You are using {s}{usage}{/s}" : "Користите {s}{usage}{/s}", "You are using {s}{usage}{/s} of {s}{totalSpace}{/s} ({s}{usageRelative}%{/s})" : "Користите {s}{usage}{/s} од {s}{totalSpace}{/s} ({s}{usageRelative}%{/s})", "You are a member of the following groups:" : "Члан сте следећих група:", @@ -812,6 +819,7 @@ OC.L10N.register( "Pronouns" : "Заменице", "Role" : "Улога", "X (formerly Twitter)" : "X (бивши Twitter)", + "Bluesky" : "Bluesky", "Website" : "Веб сајт", "Profile visibility" : "Видљивост профила", "Locale" : "Локалитет", diff --git a/apps/settings/l10n/sr.json b/apps/settings/l10n/sr.json index a19dc3c091b..507cd5e5c41 100644 --- a/apps/settings/l10n/sr.json +++ b/apps/settings/l10n/sr.json @@ -343,6 +343,7 @@ "Unified task processing" : "Обједињена обрада задатака", "AI tasks can be implemented by different apps. Here you can set which app should be used for which task." : "AI задатке могу да имплементирају разне апликације. Овде можете да подесите која ће се користити за који задатак.", "Allow AI usage for guest users" : "Дозволи да корисници гости користе AI", + "Provider for Task types" : "Пружалац услуге за Типове задатака", "Enable" : "Укључи", "None of your currently installed apps provide Task processing functionality" : "Ниједна од ваших тренутно инсталираних апликација не пружа функционалност обраде задатака", "Machine translation" : "Машинско превођење", @@ -369,6 +370,8 @@ "Users will still be able to screenshot or record the screen. This does not provide any definitive protection." : "Корисници ће ипак моћи да направе слику екрана или да сниме екран. Ово не представља дефинитивну заштиту.", "Allow users to share via link and emails" : "Дозволи корисницима да деле путем линка и и-мејлова", "Allow public uploads" : "Дозволи јавна отпремања", + "Allow public shares to be added to other clouds by federation." : "Дозволи да се јавна дељења здруживањем додају на остале облаке.", + "This will add share permissions to all newly created link shares." : "Ово ће додати дозволе дељења свим новокреираним дељењима линком.", "Always ask for a password" : "Увек питај за лозинку", "Enforce password protection" : "Захтевај заштиту лозинком", "Exclude groups from password requirements" : "Изузми групе из захтева за лозинком", @@ -392,6 +395,7 @@ "Default expiration time of remote shares in days" : "Подразумевано време трајања удаљених дељења у данима", "Expire remote shares after x days" : "Нека удаљена дељења истекну након х дана", "Set default expiration date for shares via link or mail" : "Постави подразумевано време истека за дељења путем линка или мејла", + "Enforce expiration date for link or mail shares" : "Форсирај датум истека за дељења линком или мејлом", "Default expiration time of shares in days" : "Подразумевано време трајања дељења у данима", "Privacy settings for sharing" : "Поставке приватности за дељење", "Allow account name autocompletion in share dialog and allow access to the system address book" : "Омогући аутоматско довршавање имена налога у дијалогу дељења и дозволи приступ системском адресару", @@ -414,10 +418,12 @@ "Excluded groups" : "Искључене групе", "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced." : "Када се групе означе/искључе, користи се следећа логика да се закључи да ли се за налог захтева 2FA: ако није одабрана ниједна група, 2FA је укључен свима осим члановима искључених група. Ако има одабраних група, 2FA је укључен само њиховим члановима. Ако је налог у исто време у одабраној и у искљученој групи, одабрана група има предност и 2FA се захтева.", "Save changes" : "Сними измене", + "Choose Deploy Daemon for {appName}" : "Изабери Даемон за постављање за {appName}", "Default" : "Подразумевано", "Registered Deploy daemons list" : "Листа регистрованих Даемона за постављање", "No Deploy daemons configured" : "Није подешен ниједан Даемон за постављање", "Register a custom one or setup from available templates" : "Региструјте произвољни или подесите неки према доступном шаблону", + "Manage Deploy daemons" : "Управљај Даемонима за постављање", "Show details for {appName} app" : "Прикажи детаље апликације {appName}", "Update to {update}" : "Ажурирај на {update}", "Remove" : "Уклони", @@ -619,6 +625,7 @@ "Your biography. Markdown is supported." : "Ваша биографија. Подржава се употреба Markdown означавања.", "Unable to update date of birth" : "Није успело ажурирање датума рођења", "Enter your date of birth" : "Унесите датум свог рођења", + "Bluesky handle" : "Bluesky ручка", "You are using {s}{usage}{/s}" : "Користите {s}{usage}{/s}", "You are using {s}{usage}{/s} of {s}{totalSpace}{/s} ({s}{usageRelative}%{/s})" : "Користите {s}{usage}{/s} од {s}{totalSpace}{/s} ({s}{usageRelative}%{/s})", "You are a member of the following groups:" : "Члан сте следећих група:", @@ -810,6 +817,7 @@ "Pronouns" : "Заменице", "Role" : "Улога", "X (formerly Twitter)" : "X (бивши Twitter)", + "Bluesky" : "Bluesky", "Website" : "Веб сајт", "Profile visibility" : "Видљивост профила", "Locale" : "Локалитет", diff --git a/apps/settings/l10n/uk.js b/apps/settings/l10n/uk.js index 46eae517b63..d3888dd835a 100644 --- a/apps/settings/l10n/uk.js +++ b/apps/settings/l10n/uk.js @@ -420,10 +420,12 @@ OC.L10N.register( "Excluded groups" : "Виключені групи", "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced." : "Якщо вибрано/виключено групи, то застосовується така логіка визначення, чи до користувача застосовується двофакторна авторизація: якщо не вибрано жодної групи, двофакторну авторизацію ввімкнено для всіх, крім учасників виключених груп. Якщо вибрано групи, для всіх учасників увімкнено двофакторну авторизацію. Якщо користувач одночасно входить до вибраної групи та виключеної групи, то вибрана група має пріоритет і в такому випадку до цього користувача застосовується двофакторна авторизація.", "Save changes" : "Зберегти зміни", + "Choose Deploy Daemon for {appName}" : "Виберіть Deploy Daemon для {appName}", "Default" : "Типово", "Registered Deploy daemons list" : "Список зареєстрованих демонів розгортання", "No Deploy daemons configured" : "Не налаштовано демонів розгортання", "Register a custom one or setup from available templates" : "Зареєструвати власний або налаштувати з доступних шаблонів", + "Manage Deploy daemons" : "Керувати демонами розгортання", "Show details for {appName} app" : "Показати деталі для застосунку {appName}", "Update to {update}" : "Оновити до {update}", "Remove" : "Вилучити", diff --git a/apps/settings/l10n/uk.json b/apps/settings/l10n/uk.json index a818cf9def4..bbdbde52932 100644 --- a/apps/settings/l10n/uk.json +++ b/apps/settings/l10n/uk.json @@ -418,10 +418,12 @@ "Excluded groups" : "Виключені групи", "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced." : "Якщо вибрано/виключено групи, то застосовується така логіка визначення, чи до користувача застосовується двофакторна авторизація: якщо не вибрано жодної групи, двофакторну авторизацію ввімкнено для всіх, крім учасників виключених груп. Якщо вибрано групи, для всіх учасників увімкнено двофакторну авторизацію. Якщо користувач одночасно входить до вибраної групи та виключеної групи, то вибрана група має пріоритет і в такому випадку до цього користувача застосовується двофакторна авторизація.", "Save changes" : "Зберегти зміни", + "Choose Deploy Daemon for {appName}" : "Виберіть Deploy Daemon для {appName}", "Default" : "Типово", "Registered Deploy daemons list" : "Список зареєстрованих демонів розгортання", "No Deploy daemons configured" : "Не налаштовано демонів розгортання", "Register a custom one or setup from available templates" : "Зареєструвати власний або налаштувати з доступних шаблонів", + "Manage Deploy daemons" : "Керувати демонами розгортання", "Show details for {appName} app" : "Показати деталі для застосунку {appName}", "Update to {update}" : "Оновити до {update}", "Remove" : "Вилучити", diff --git a/apps/settings/l10n/zh_CN.js b/apps/settings/l10n/zh_CN.js index 0ada283f2ac..2d7a0a20e26 100644 --- a/apps/settings/l10n/zh_CN.js +++ b/apps/settings/l10n/zh_CN.js @@ -345,6 +345,7 @@ OC.L10N.register( "Unified task processing" : "统一任务处理", "AI tasks can be implemented by different apps. Here you can set which app should be used for which task." : "AI 任务可以由不同的应用程序执行。在这里您可以设置哪个应用程序应该用于哪个任务。", "Allow AI usage for guest users" : "允许访客用户使用 AI", + "Provider for Task types" : "任务类型提供者", "Enable" : "启用", "None of your currently installed apps provide Task processing functionality" : "您当前安装的应用程序均不提供任务处理功能", "Machine translation" : "机器翻译", @@ -419,10 +420,12 @@ OC.L10N.register( "Excluded groups" : "排除群组", "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced." : "选择/排除 组时,它们使用以下逻辑来确定帐户是否强制执行 2FA:如果未选择任何组,则将为除排除组的成员之外的所有人启用 2FA。 如果选择组,则会为其中的所有成员启用 2FA。 如果账户同时位于选定组和排除组中,则选定组优先并强制执行 2FA。", "Save changes" : "保存修改", + "Choose Deploy Daemon for {appName}" : "为 {appName} 选择部署守护进程", "Default" : "默认", "Registered Deploy daemons list" : "已注册的部署守护进程列表", "No Deploy daemons configured" : "没有部署守护进程配置", "Register a custom one or setup from available templates" : "注册自定义模板或从可用模板进行安装", + "Manage Deploy daemons" : "管理部署守护进程", "Show details for {appName} app" : "显示应用 {appName} 的详细信息", "Update to {update}" : "更新至 {update}", "Remove" : "移除", @@ -505,7 +508,7 @@ OC.L10N.register( "No results" : "没有结果", "Update to {version}" : "更新至 {version}", "Deploy options" : "部署选项", - "Default Deploy daemon is not accessible" : "默认部署守护程序不可访问", + "Default Deploy daemon is not accessible" : "默认部署守护进程不可访问", "Delete data on remove" : "移除时删除数据", "This app has no minimum Nextcloud version assigned. This will be an error in the future." : "该应用没有指定支持的 Nextcloud 最低版本。可能会在将来出现问题。", "This app has no maximum Nextcloud version assigned. This will be an error in the future." : "该应用没有指定支持的 Nextcloud 最高版本。可能会在将来出现问题。", diff --git a/apps/settings/l10n/zh_CN.json b/apps/settings/l10n/zh_CN.json index c22a50fefb7..056d1cf7ef1 100644 --- a/apps/settings/l10n/zh_CN.json +++ b/apps/settings/l10n/zh_CN.json @@ -343,6 +343,7 @@ "Unified task processing" : "统一任务处理", "AI tasks can be implemented by different apps. Here you can set which app should be used for which task." : "AI 任务可以由不同的应用程序执行。在这里您可以设置哪个应用程序应该用于哪个任务。", "Allow AI usage for guest users" : "允许访客用户使用 AI", + "Provider for Task types" : "任务类型提供者", "Enable" : "启用", "None of your currently installed apps provide Task processing functionality" : "您当前安装的应用程序均不提供任务处理功能", "Machine translation" : "机器翻译", @@ -417,10 +418,12 @@ "Excluded groups" : "排除群组", "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced." : "选择/排除 组时,它们使用以下逻辑来确定帐户是否强制执行 2FA:如果未选择任何组,则将为除排除组的成员之外的所有人启用 2FA。 如果选择组,则会为其中的所有成员启用 2FA。 如果账户同时位于选定组和排除组中,则选定组优先并强制执行 2FA。", "Save changes" : "保存修改", + "Choose Deploy Daemon for {appName}" : "为 {appName} 选择部署守护进程", "Default" : "默认", "Registered Deploy daemons list" : "已注册的部署守护进程列表", "No Deploy daemons configured" : "没有部署守护进程配置", "Register a custom one or setup from available templates" : "注册自定义模板或从可用模板进行安装", + "Manage Deploy daemons" : "管理部署守护进程", "Show details for {appName} app" : "显示应用 {appName} 的详细信息", "Update to {update}" : "更新至 {update}", "Remove" : "移除", @@ -503,7 +506,7 @@ "No results" : "没有结果", "Update to {version}" : "更新至 {version}", "Deploy options" : "部署选项", - "Default Deploy daemon is not accessible" : "默认部署守护程序不可访问", + "Default Deploy daemon is not accessible" : "默认部署守护进程不可访问", "Delete data on remove" : "移除时删除数据", "This app has no minimum Nextcloud version assigned. This will be an error in the future." : "该应用没有指定支持的 Nextcloud 最低版本。可能会在将来出现问题。", "This app has no maximum Nextcloud version assigned. This will be an error in the future." : "该应用没有指定支持的 Nextcloud 最高版本。可能会在将来出现问题。", diff --git a/apps/settings/l10n/zh_HK.js b/apps/settings/l10n/zh_HK.js index 775b486d6d6..1d19804ea48 100644 --- a/apps/settings/l10n/zh_HK.js +++ b/apps/settings/l10n/zh_HK.js @@ -420,10 +420,12 @@ OC.L10N.register( "Excluded groups" : "排除的群組", "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced." : "選擇/排除群組時,其使用以下邏輯來確定帳戶是否強制啟用雙因素驗證:如果沒有選擇群組,雙因素驗證對所有除了排除群組以外的成員啟用。若選擇了群組,則雙因素驗證會對所有該群組的成員啟用。如果某個帳戶同時位在選擇與排除的群組,則被選擇的群組優先程度較高,因此會強制啟用雙因素驗證。", "Save changes" : "儲存變更", + "Choose Deploy Daemon for {appName}" : "選擇 {appName} 的部署幕後程式", "Default" : "默認", "Registered Deploy daemons list" : "已註冊的部署幕後程式清單", "No Deploy daemons configured" : "未設定部署幕後程式", "Register a custom one or setup from available templates" : "從可用範本註冊自訂範本或設定", + "Manage Deploy daemons" : "管理部署幕後程式", "Show details for {appName} app" : "顯示 {appName} 應用程式的詳細資訊", "Update to {update}" : "更新到 {update}", "Remove" : "移除", diff --git a/apps/settings/l10n/zh_HK.json b/apps/settings/l10n/zh_HK.json index 9c9dbb6f271..286202d71ff 100644 --- a/apps/settings/l10n/zh_HK.json +++ b/apps/settings/l10n/zh_HK.json @@ -418,10 +418,12 @@ "Excluded groups" : "排除的群組", "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced." : "選擇/排除群組時,其使用以下邏輯來確定帳戶是否強制啟用雙因素驗證:如果沒有選擇群組,雙因素驗證對所有除了排除群組以外的成員啟用。若選擇了群組,則雙因素驗證會對所有該群組的成員啟用。如果某個帳戶同時位在選擇與排除的群組,則被選擇的群組優先程度較高,因此會強制啟用雙因素驗證。", "Save changes" : "儲存變更", + "Choose Deploy Daemon for {appName}" : "選擇 {appName} 的部署幕後程式", "Default" : "默認", "Registered Deploy daemons list" : "已註冊的部署幕後程式清單", "No Deploy daemons configured" : "未設定部署幕後程式", "Register a custom one or setup from available templates" : "從可用範本註冊自訂範本或設定", + "Manage Deploy daemons" : "管理部署幕後程式", "Show details for {appName} app" : "顯示 {appName} 應用程式的詳細資訊", "Update to {update}" : "更新到 {update}", "Remove" : "移除", diff --git a/apps/settings/l10n/zh_TW.js b/apps/settings/l10n/zh_TW.js index 9806745beea..18a6b553e05 100644 --- a/apps/settings/l10n/zh_TW.js +++ b/apps/settings/l10n/zh_TW.js @@ -420,10 +420,12 @@ OC.L10N.register( "Excluded groups" : "排除的群組", "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced." : "選取/排除群組時,其使用以下邏輯來確定帳號是否強制啟用雙因素驗證:如果沒有選取群組,雙因素驗證對所有除了排除群組以外的成員啟用。若選取了群組,則雙因素驗證會對所有該群組的成員啟用。如果某個帳號同時位在選取與排除的群組,則被選取的群組優先程度較高,因此會強制啟用雙因素驗證。", "Save changes" : "儲存變更", + "Choose Deploy Daemon for {appName}" : "選擇 {appName} 的部署幕後程式", "Default" : "預設", "Registered Deploy daemons list" : "已註冊的部署幕後程式清單", "No Deploy daemons configured" : "未設定部署幕後程式", "Register a custom one or setup from available templates" : "從可用範本註冊自訂範本或設定", + "Manage Deploy daemons" : "管理部署幕後程式", "Show details for {appName} app" : "顯示 {appName} 應用程式的詳細資訊", "Update to {update}" : "更新到 {update}", "Remove" : "移除", diff --git a/apps/settings/l10n/zh_TW.json b/apps/settings/l10n/zh_TW.json index 9f4cbb94344..4e1ee7c15e5 100644 --- a/apps/settings/l10n/zh_TW.json +++ b/apps/settings/l10n/zh_TW.json @@ -418,10 +418,12 @@ "Excluded groups" : "排除的群組", "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced." : "選取/排除群組時,其使用以下邏輯來確定帳號是否強制啟用雙因素驗證:如果沒有選取群組,雙因素驗證對所有除了排除群組以外的成員啟用。若選取了群組,則雙因素驗證會對所有該群組的成員啟用。如果某個帳號同時位在選取與排除的群組,則被選取的群組優先程度較高,因此會強制啟用雙因素驗證。", "Save changes" : "儲存變更", + "Choose Deploy Daemon for {appName}" : "選擇 {appName} 的部署幕後程式", "Default" : "預設", "Registered Deploy daemons list" : "已註冊的部署幕後程式清單", "No Deploy daemons configured" : "未設定部署幕後程式", "Register a custom one or setup from available templates" : "從可用範本註冊自訂範本或設定", + "Manage Deploy daemons" : "管理部署幕後程式", "Show details for {appName} app" : "顯示 {appName} 應用程式的詳細資訊", "Update to {update}" : "更新到 {update}", "Remove" : "移除", diff --git a/apps/systemtags/l10n/es.js b/apps/systemtags/l10n/es.js index 2f468ae5de1..794f7e05cb5 100644 --- a/apps/systemtags/l10n/es.js +++ b/apps/systemtags/l10n/es.js @@ -80,6 +80,7 @@ OC.L10N.register( "Search tag" : "Buscar etiqueta", "Change tag color" : "Cambiar color de etiqueta", "Create new tag" : "Crear nueva etiqueta", + "Choose tags for the selected files" : "Escoja las etiquetas para los archivos seleccionados", "Cancel" : "Cancelar", "Apply" : "Aplicar", "Failed to load tags" : "Fallo al cargar etiquetas", diff --git a/apps/systemtags/l10n/es.json b/apps/systemtags/l10n/es.json index 8b152161291..3f7d6785875 100644 --- a/apps/systemtags/l10n/es.json +++ b/apps/systemtags/l10n/es.json @@ -78,6 +78,7 @@ "Search tag" : "Buscar etiqueta", "Change tag color" : "Cambiar color de etiqueta", "Create new tag" : "Crear nueva etiqueta", + "Choose tags for the selected files" : "Escoja las etiquetas para los archivos seleccionados", "Cancel" : "Cancelar", "Apply" : "Aplicar", "Failed to load tags" : "Fallo al cargar etiquetas", diff --git a/apps/systemtags/l10n/pt_BR.js b/apps/systemtags/l10n/pt_BR.js index 6f931085ed1..4b8ad122508 100644 --- a/apps/systemtags/l10n/pt_BR.js +++ b/apps/systemtags/l10n/pt_BR.js @@ -80,6 +80,7 @@ OC.L10N.register( "Search tag" : "Pesquisar etiqueta", "Change tag color" : "Alterar cor da etiqueta", "Create new tag" : "Criar etiqueta nova", + "Choose tags for the selected files" : "Escolha tags para os arquivos selecionados", "Cancel" : "Cancelar", "Apply" : "Aplicar", "Failed to load tags" : "Erro ao carregar etiquetas", diff --git a/apps/systemtags/l10n/pt_BR.json b/apps/systemtags/l10n/pt_BR.json index c5bc3eeedd5..a2a6ae74c6a 100644 --- a/apps/systemtags/l10n/pt_BR.json +++ b/apps/systemtags/l10n/pt_BR.json @@ -78,6 +78,7 @@ "Search tag" : "Pesquisar etiqueta", "Change tag color" : "Alterar cor da etiqueta", "Create new tag" : "Criar etiqueta nova", + "Choose tags for the selected files" : "Escolha tags para os arquivos selecionados", "Cancel" : "Cancelar", "Apply" : "Aplicar", "Failed to load tags" : "Erro ao carregar etiquetas", diff --git a/apps/systemtags/l10n/zh_CN.js b/apps/systemtags/l10n/zh_CN.js index 4a0f97f6f4c..3387ce7c0b0 100644 --- a/apps/systemtags/l10n/zh_CN.js +++ b/apps/systemtags/l10n/zh_CN.js @@ -80,6 +80,7 @@ OC.L10N.register( "Search tag" : "搜索标签", "Change tag color" : "更改标签颜色", "Create new tag" : "创建新标签", + "Choose tags for the selected files" : "为所选文件选择标签", "Cancel" : "取消", "Apply" : "应用", "Failed to load tags" : "加载标签失败", diff --git a/apps/systemtags/l10n/zh_CN.json b/apps/systemtags/l10n/zh_CN.json index 7bda00e0274..ca25825545e 100644 --- a/apps/systemtags/l10n/zh_CN.json +++ b/apps/systemtags/l10n/zh_CN.json @@ -78,6 +78,7 @@ "Search tag" : "搜索标签", "Change tag color" : "更改标签颜色", "Create new tag" : "创建新标签", + "Choose tags for the selected files" : "为所选文件选择标签", "Cancel" : "取消", "Apply" : "应用", "Failed to load tags" : "加载标签失败", diff --git a/apps/theming/l10n/es.js b/apps/theming/l10n/es.js index e7cd2cf669a..99c2628472e 100644 --- a/apps/theming/l10n/es.js +++ b/apps/theming/l10n/es.js @@ -75,7 +75,9 @@ OC.L10N.register( "Background and login image" : "Imágen de fondo y de inicio de sesión", "Advanced options" : "Opciones avanzadas", "Install the ImageMagick PHP extension with support for SVG images to automatically generate favicons based on the uploaded logo and color." : "Instale la extensión PHP ImageMagick con soporte a imágenes SVG para generar automáticamente favicons basados en el logo cargado y el color.", + "Universal access is very important to us. We follow web standards and check to make everything usable also without mouse, and assistive software such as screenreaders. We aim to be compliant with the {linkstart}Web Content Accessibility Guidelines{linkend} 2.1 on AA level, with the high contrast theme even on AAA level." : "El acceso universal es muy importante para nosotros. Seguimos los estándares web y verificamos para asegurarnos de que todo sea utilizable incluso sin ratón y con software de asistencia tales como lectores de pantalla. Nuestro objetivo es cumplir con las {linkstart}Directrices de Accesibilidad para el Contenido Web{linkend} 2.1 en el nivel AA, incluso con el tema de alto contraste en el nivel AAA.", "If you find any issues, do not hesitate to report them on {issuetracker}our issue tracker{linkend}. And if you want to get involved, come join {designteam}our design team{linkend}!" : "Si encuentra cualquier problema, no dude en reportarlo en {issuetracker}nuestra herramienta de rastreo de problemas{linkend}. Y, si desea involucrarse, ¡venga y únase a {designteam},nuestro equipo de diseño{linkend}!", + "Unable to apply the setting." : "No fue posible aplicar el ajuste.", "Appearance and accessibility settings" : "Ajustes de apariencia y accesibilidad", "Misc accessibility options" : "Opciones de accesibilidad misceláneas", "Enable blur background filter (may increase GPU load)" : "Activar el desenfoque del fondo (puede aumentar el uso de GPU)", @@ -117,6 +119,7 @@ OC.L10N.register( "Reset to default" : "Vovler a configuración por defecto", "Upload" : "Subir", "Remove background image" : "Eliminar imagen de fondo", - "Universal access is very important to us. We follow web standards and check to make everything usable also without mouse, and assistive software such as screenreaders. We aim to be compliant with the {guidelines}Web Content Accessibility Guidelines{linkend} 2.1 on AA level, with the high contrast theme even on AAA level." : "El acceso universal es muy importante para nosotros. Seguimos los estándares web y verificamos para asegurarnos de que todo sea utilizable incluso sin ratón y con software de asistencia tales como lectores de pantalla. Nuestro objetivo es cumplir con las {guidelines}Directrices de Accesibilidad para el Contenido Web{linkend} 2.1 en el nivel AA, incluso con el tema de alto contraste en el nivel AAA." + "Universal access is very important to us. We follow web standards and check to make everything usable also without mouse, and assistive software such as screenreaders. We aim to be compliant with the {guidelines}Web Content Accessibility Guidelines{linkend} 2.1 on AA level, with the high contrast theme even on AAA level." : "El acceso universal es muy importante para nosotros. Seguimos los estándares web y verificamos para asegurarnos de que todo sea utilizable incluso sin ratón y con software de asistencia tales como lectores de pantalla. Nuestro objetivo es cumplir con las {guidelines}Directrices de Accesibilidad para el Contenido Web{linkend} 2.1 en el nivel AA, incluso con el tema de alto contraste en el nivel AAA.", + ". Unable to apply the setting." : ". No fue posible aplicar el ajuste." }, "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/theming/l10n/es.json b/apps/theming/l10n/es.json index c9f5ed3b1a7..740a376c13a 100644 --- a/apps/theming/l10n/es.json +++ b/apps/theming/l10n/es.json @@ -73,7 +73,9 @@ "Background and login image" : "Imágen de fondo y de inicio de sesión", "Advanced options" : "Opciones avanzadas", "Install the ImageMagick PHP extension with support for SVG images to automatically generate favicons based on the uploaded logo and color." : "Instale la extensión PHP ImageMagick con soporte a imágenes SVG para generar automáticamente favicons basados en el logo cargado y el color.", + "Universal access is very important to us. We follow web standards and check to make everything usable also without mouse, and assistive software such as screenreaders. We aim to be compliant with the {linkstart}Web Content Accessibility Guidelines{linkend} 2.1 on AA level, with the high contrast theme even on AAA level." : "El acceso universal es muy importante para nosotros. Seguimos los estándares web y verificamos para asegurarnos de que todo sea utilizable incluso sin ratón y con software de asistencia tales como lectores de pantalla. Nuestro objetivo es cumplir con las {linkstart}Directrices de Accesibilidad para el Contenido Web{linkend} 2.1 en el nivel AA, incluso con el tema de alto contraste en el nivel AAA.", "If you find any issues, do not hesitate to report them on {issuetracker}our issue tracker{linkend}. And if you want to get involved, come join {designteam}our design team{linkend}!" : "Si encuentra cualquier problema, no dude en reportarlo en {issuetracker}nuestra herramienta de rastreo de problemas{linkend}. Y, si desea involucrarse, ¡venga y únase a {designteam},nuestro equipo de diseño{linkend}!", + "Unable to apply the setting." : "No fue posible aplicar el ajuste.", "Appearance and accessibility settings" : "Ajustes de apariencia y accesibilidad", "Misc accessibility options" : "Opciones de accesibilidad misceláneas", "Enable blur background filter (may increase GPU load)" : "Activar el desenfoque del fondo (puede aumentar el uso de GPU)", @@ -115,6 +117,7 @@ "Reset to default" : "Vovler a configuración por defecto", "Upload" : "Subir", "Remove background image" : "Eliminar imagen de fondo", - "Universal access is very important to us. We follow web standards and check to make everything usable also without mouse, and assistive software such as screenreaders. We aim to be compliant with the {guidelines}Web Content Accessibility Guidelines{linkend} 2.1 on AA level, with the high contrast theme even on AAA level." : "El acceso universal es muy importante para nosotros. Seguimos los estándares web y verificamos para asegurarnos de que todo sea utilizable incluso sin ratón y con software de asistencia tales como lectores de pantalla. Nuestro objetivo es cumplir con las {guidelines}Directrices de Accesibilidad para el Contenido Web{linkend} 2.1 en el nivel AA, incluso con el tema de alto contraste en el nivel AAA." + "Universal access is very important to us. We follow web standards and check to make everything usable also without mouse, and assistive software such as screenreaders. We aim to be compliant with the {guidelines}Web Content Accessibility Guidelines{linkend} 2.1 on AA level, with the high contrast theme even on AAA level." : "El acceso universal es muy importante para nosotros. Seguimos los estándares web y verificamos para asegurarnos de que todo sea utilizable incluso sin ratón y con software de asistencia tales como lectores de pantalla. Nuestro objetivo es cumplir con las {guidelines}Directrices de Accesibilidad para el Contenido Web{linkend} 2.1 en el nivel AA, incluso con el tema de alto contraste en el nivel AAA.", + ". Unable to apply the setting." : ". No fue posible aplicar el ajuste." },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" }
\ No newline at end of file diff --git a/apps/user_status/l10n/es.js b/apps/user_status/l10n/es.js index d76dd2511f5..952bd011543 100644 --- a/apps/user_status/l10n/es.js +++ b/apps/user_status/l10n/es.js @@ -10,6 +10,7 @@ OC.L10N.register( "Out of office" : "Fuera de la oficina", "Working remotely" : "Teletrabajando", "In a call" : "En una llamada", + "Be right back" : "Vuelvo ahora mismo", "User status" : "Estado del usuario", "Clear status after" : "Eliminar el estado después de", "Emoji for your status message" : "Emoji para sus mensaje de estado", @@ -25,6 +26,8 @@ OC.L10N.register( "There was an error reverting the status" : "Ocurrió un error al revertir el estado", "Online status" : "Estado en línea", "Status message" : "Mensaje de estado", + "Set absence period" : "Establecer período de ausencia", + "Set absence period and replacement" : "Establecer período de ausencia y el sustituto", "Your status was set automatically" : "Su estado fue definido automáticamente", "Clear status message" : "Borrar mensaje de estado", "Set status message" : "Ajustar el mensaje de estado", diff --git a/apps/user_status/l10n/es.json b/apps/user_status/l10n/es.json index d743ee8a033..93299df5b27 100644 --- a/apps/user_status/l10n/es.json +++ b/apps/user_status/l10n/es.json @@ -8,6 +8,7 @@ "Out of office" : "Fuera de la oficina", "Working remotely" : "Teletrabajando", "In a call" : "En una llamada", + "Be right back" : "Vuelvo ahora mismo", "User status" : "Estado del usuario", "Clear status after" : "Eliminar el estado después de", "Emoji for your status message" : "Emoji para sus mensaje de estado", @@ -23,6 +24,8 @@ "There was an error reverting the status" : "Ocurrió un error al revertir el estado", "Online status" : "Estado en línea", "Status message" : "Mensaje de estado", + "Set absence period" : "Establecer período de ausencia", + "Set absence period and replacement" : "Establecer período de ausencia y el sustituto", "Your status was set automatically" : "Su estado fue definido automáticamente", "Clear status message" : "Borrar mensaje de estado", "Set status message" : "Ajustar el mensaje de estado", diff --git a/apps/user_status/l10n/et_EE.js b/apps/user_status/l10n/et_EE.js index 18c3c88e824..79d1c125875 100644 --- a/apps/user_status/l10n/et_EE.js +++ b/apps/user_status/l10n/et_EE.js @@ -32,7 +32,7 @@ OC.L10N.register( "Clear status message" : "Eemalda olekuteade", "Set status message" : "Lisa olekusõnum", "Don't clear" : "Ära kustuta", - "Today" : "Tänast", + "Today" : "Täna", "This week" : "Käesoleval nädalal", "Online" : "Võrgus", "Away" : "Eemal", diff --git a/apps/user_status/l10n/et_EE.json b/apps/user_status/l10n/et_EE.json index 903466eef24..38c0880aa79 100644 --- a/apps/user_status/l10n/et_EE.json +++ b/apps/user_status/l10n/et_EE.json @@ -30,7 +30,7 @@ "Clear status message" : "Eemalda olekuteade", "Set status message" : "Lisa olekusõnum", "Don't clear" : "Ära kustuta", - "Today" : "Tänast", + "Today" : "Täna", "This week" : "Käesoleval nädalal", "Online" : "Võrgus", "Away" : "Eemal", diff --git a/apps/weather_status/l10n/es.js b/apps/weather_status/l10n/es.js index 60ea90dcddb..1f31e29e450 100644 --- a/apps/weather_status/l10n/es.js +++ b/apps/weather_status/l10n/es.js @@ -14,12 +14,16 @@ OC.L10N.register( "{temperature} {unit} cloudy" : "{temperature} {unit} Nublado", "{temperature} {unit} snow and thunder later today" : "{temperature}{unit} Nieve y truenos más tarde hoy", "{temperature} {unit} snow and thunder" : "{temperature} {unit} Nieve y truenos", - "{temperature} {unit} snow showers and thunder later today" : "{temperature} {unit} Chubascos de nieve y truenos más tarde hoy", - "{temperature} {unit} snow showers and thunder" : "{temperature} {unit} Chubascos de nieve y truenos", - "{temperature} {unit} snow showers, thunder and polar twilight later today" : "{temperature} {unit} Chubascos de nieve, truenos y crepúsculo polar más tarde hoy", - "{temperature} {unit} snow showers, thunder and polar twilight" : "{temperature} {unit} Chubascos de nieve, truenos y crepúsculo polar", - "{temperature} {unit} snow showers later today" : "{temperature} {unit} Chubascos de nieve más tarde hoy", - "{temperature} {unit} snow showers" : "{temperature} {unit} Chubascos de nieve", + "{temperature} {unit} snow showers and thunder later today" : "{temperature} {unit} chubascos de nieve y truenos más tarde hoy", + "{temperature} {unit} snow showers and thunder" : "{temperature} {unit} chubascos de nieve y truenos", + "{temperature} {unit} snow showers, thunder and polar twilight later today" : "{temperature} {unit} chubascos de nieve, truenos y crepúsculo polar más tarde hoy", + "{temperature} {unit} snow showers, thunder and polar twilight" : "{temperature} {unit} chubascos de nieve, truenos y crepúsculo polar", + "{temperature} {unit} snow showers later today" : "{temperature} {unit} chubascos de nieve más tarde hoy", + "{temperature} {unit} snow showers" : "{temperature} {unit} chubascos de nieve", + "{temperature} {unit} snow showers and polar twilight later today" : "{temperature} {unit} chubascos de nieve y crepúsculo polar más tarde hoy", + "{temperature} {unit} snow showers and polar twilight" : "{temperature} {unit} chubascos de nieve y crepúsculo polar", + "{temperature} {unit} snow later today" : "{temperature} {unit} nieve más tarde hoy", + "{temperature} {unit} snow" : "{temperature} {unit} nieve", "{temperature} {unit} fair weather later today" : "{temperature} {unit} buen clima más tarde hoy", "{temperature} {unit} fair weather" : "{temperature} {unit} Buen clima", "{temperature} {unit} partly cloudy later today" : "{temperature} {unit} parcialmente nublado más tarde hoy", diff --git a/apps/weather_status/l10n/es.json b/apps/weather_status/l10n/es.json index 0a4ae70d259..1ade5c35130 100644 --- a/apps/weather_status/l10n/es.json +++ b/apps/weather_status/l10n/es.json @@ -12,12 +12,16 @@ "{temperature} {unit} cloudy" : "{temperature} {unit} Nublado", "{temperature} {unit} snow and thunder later today" : "{temperature}{unit} Nieve y truenos más tarde hoy", "{temperature} {unit} snow and thunder" : "{temperature} {unit} Nieve y truenos", - "{temperature} {unit} snow showers and thunder later today" : "{temperature} {unit} Chubascos de nieve y truenos más tarde hoy", - "{temperature} {unit} snow showers and thunder" : "{temperature} {unit} Chubascos de nieve y truenos", - "{temperature} {unit} snow showers, thunder and polar twilight later today" : "{temperature} {unit} Chubascos de nieve, truenos y crepúsculo polar más tarde hoy", - "{temperature} {unit} snow showers, thunder and polar twilight" : "{temperature} {unit} Chubascos de nieve, truenos y crepúsculo polar", - "{temperature} {unit} snow showers later today" : "{temperature} {unit} Chubascos de nieve más tarde hoy", - "{temperature} {unit} snow showers" : "{temperature} {unit} Chubascos de nieve", + "{temperature} {unit} snow showers and thunder later today" : "{temperature} {unit} chubascos de nieve y truenos más tarde hoy", + "{temperature} {unit} snow showers and thunder" : "{temperature} {unit} chubascos de nieve y truenos", + "{temperature} {unit} snow showers, thunder and polar twilight later today" : "{temperature} {unit} chubascos de nieve, truenos y crepúsculo polar más tarde hoy", + "{temperature} {unit} snow showers, thunder and polar twilight" : "{temperature} {unit} chubascos de nieve, truenos y crepúsculo polar", + "{temperature} {unit} snow showers later today" : "{temperature} {unit} chubascos de nieve más tarde hoy", + "{temperature} {unit} snow showers" : "{temperature} {unit} chubascos de nieve", + "{temperature} {unit} snow showers and polar twilight later today" : "{temperature} {unit} chubascos de nieve y crepúsculo polar más tarde hoy", + "{temperature} {unit} snow showers and polar twilight" : "{temperature} {unit} chubascos de nieve y crepúsculo polar", + "{temperature} {unit} snow later today" : "{temperature} {unit} nieve más tarde hoy", + "{temperature} {unit} snow" : "{temperature} {unit} nieve", "{temperature} {unit} fair weather later today" : "{temperature} {unit} buen clima más tarde hoy", "{temperature} {unit} fair weather" : "{temperature} {unit} Buen clima", "{temperature} {unit} partly cloudy later today" : "{temperature} {unit} parcialmente nublado más tarde hoy", diff --git a/apps/webhook_listeners/l10n/es.js b/apps/webhook_listeners/l10n/es.js new file mode 100644 index 00000000000..ea01d510388 --- /dev/null +++ b/apps/webhook_listeners/l10n/es.js @@ -0,0 +1,7 @@ +OC.L10N.register( + "webhook_listeners", + { + "Webhooks" : "Webhooks", + "Nextcloud webhook support" : "Soporte webhook de Nextcloud" +}, +"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); diff --git a/apps/webhook_listeners/l10n/es.json b/apps/webhook_listeners/l10n/es.json new file mode 100644 index 00000000000..66b77529392 --- /dev/null +++ b/apps/webhook_listeners/l10n/es.json @@ -0,0 +1,5 @@ +{ "translations": { + "Webhooks" : "Webhooks", + "Nextcloud webhook support" : "Soporte webhook de Nextcloud" +},"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" +}
\ No newline at end of file diff --git a/core/Command/TaskProcessing/Cleanup.php b/core/Command/TaskProcessing/Cleanup.php new file mode 100644 index 00000000000..2ed2cbdec94 --- /dev/null +++ b/core/Command/TaskProcessing/Cleanup.php @@ -0,0 +1,93 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Core\Command\TaskProcessing; + +use OC\Core\Command\Base; +use OC\TaskProcessing\Db\TaskMapper; +use OC\TaskProcessing\Manager; +use OCP\Files\AppData\IAppDataFactory; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class Cleanup extends Base { + private \OCP\Files\IAppData $appData; + + public function __construct( + protected Manager $taskProcessingManager, + private TaskMapper $taskMapper, + private LoggerInterface $logger, + IAppDataFactory $appDataFactory, + ) { + parent::__construct(); + $this->appData = $appDataFactory->get('core'); + } + + protected function configure() { + $this + ->setName('taskprocessing:task:cleanup') + ->setDescription('cleanup old tasks') + ->addArgument( + 'maxAgeSeconds', + InputArgument::OPTIONAL, + // default is not defined as an argument default value because we want to show a nice "4 months" value + 'delete tasks that are older than this number of seconds, defaults to ' . Manager::MAX_TASK_AGE_SECONDS . ' (4 months)', + ); + parent::configure(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $maxAgeSeconds = $input->getArgument('maxAgeSeconds') ?? Manager::MAX_TASK_AGE_SECONDS; + $output->writeln('<comment>Cleanup up tasks older than ' . $maxAgeSeconds . ' seconds and the related output files</comment>'); + + $taskIdsToCleanup = []; + try { + $fileCleanupGenerator = $this->taskProcessingManager->cleanupTaskProcessingTaskFiles($maxAgeSeconds); + foreach ($fileCleanupGenerator as $cleanedUpEntry) { + $output->writeln( + "<info>\t - " . 'Deleted appData/core/TaskProcessing/' . $cleanedUpEntry['file_name'] + . ' (fileId: ' . $cleanedUpEntry['file_id'] . ', taskId: ' . $cleanedUpEntry['task_id'] . ')</info>' + ); + } + $taskIdsToCleanup = $fileCleanupGenerator->getReturn(); + } catch (\Exception $e) { + $this->logger->warning('Failed to delete stale task processing tasks files', ['exception' => $e]); + $output->writeln('<warning>Failed to delete stale task processing tasks files</warning>'); + } + try { + $deletedTaskCount = $this->taskMapper->deleteOlderThan($maxAgeSeconds); + foreach ($taskIdsToCleanup as $taskId) { + $output->writeln("<info>\t - " . 'Deleted task ' . $taskId . ' from the database</info>'); + } + $output->writeln("<comment>\t - " . 'Deleted ' . $deletedTaskCount . ' tasks from the database</comment>'); + } catch (\OCP\DB\Exception $e) { + $this->logger->warning('Failed to delete stale task processing tasks', ['exception' => $e]); + $output->writeln('<warning>Failed to delete stale task processing tasks</warning>'); + } + try { + $textToImageDeletedFileNames = $this->taskProcessingManager->clearFilesOlderThan($this->appData->getFolder('text2image'), $maxAgeSeconds); + foreach ($textToImageDeletedFileNames as $entry) { + $output->writeln("<info>\t - " . 'Deleted appData/core/text2image/' . $entry . '</info>'); + } + } catch (\OCP\Files\NotFoundException $e) { + // noop + } + try { + $audioToTextDeletedFileNames = $this->taskProcessingManager->clearFilesOlderThan($this->appData->getFolder('audio2text'), $maxAgeSeconds); + foreach ($audioToTextDeletedFileNames as $entry) { + $output->writeln("<info>\t - " . 'Deleted appData/core/audio2text/' . $entry . '</info>'); + } + } catch (\OCP\Files\NotFoundException $e) { + // noop + } + + return 0; + } +} diff --git a/core/Command/TaskProcessing/EnabledCommand.php b/core/Command/TaskProcessing/EnabledCommand.php index 0d4b831812c..a99e3e001b9 100644 --- a/core/Command/TaskProcessing/EnabledCommand.php +++ b/core/Command/TaskProcessing/EnabledCommand.php @@ -1,5 +1,7 @@ <?php +declare(strict_types=1); + /** * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/core/Command/TaskProcessing/GetCommand.php b/core/Command/TaskProcessing/GetCommand.php index 5c4fd17f2f8..f97556281a1 100644 --- a/core/Command/TaskProcessing/GetCommand.php +++ b/core/Command/TaskProcessing/GetCommand.php @@ -1,5 +1,7 @@ <?php +declare(strict_types=1); + /** * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/core/Command/TaskProcessing/ListCommand.php b/core/Command/TaskProcessing/ListCommand.php index 81eb258d35d..50a694c1a6e 100644 --- a/core/Command/TaskProcessing/ListCommand.php +++ b/core/Command/TaskProcessing/ListCommand.php @@ -1,5 +1,7 @@ <?php +declare(strict_types=1); + /** * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/core/Command/TaskProcessing/Statistics.php b/core/Command/TaskProcessing/Statistics.php index 86478b34db1..6aa4cd5bf52 100644 --- a/core/Command/TaskProcessing/Statistics.php +++ b/core/Command/TaskProcessing/Statistics.php @@ -1,5 +1,7 @@ <?php +declare(strict_types=1); + /** * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/core/Controller/TaskProcessingApiController.php b/core/Controller/TaskProcessingApiController.php index 90a0e9ba14a..82b4d786914 100644 --- a/core/Controller/TaskProcessingApiController.php +++ b/core/Controller/TaskProcessingApiController.php @@ -31,7 +31,6 @@ use OCP\Files\NotPermittedException; use OCP\IL10N; use OCP\IRequest; use OCP\Lock\LockedException; -use OCP\TaskProcessing\EShapeType; use OCP\TaskProcessing\Exception\Exception; use OCP\TaskProcessing\Exception\NotFoundException; use OCP\TaskProcessing\Exception\PreConditionNotMetException; @@ -391,7 +390,7 @@ class TaskProcessingApiController extends OCSController { * @return StreamResponse<Http::STATUS_OK, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}> */ private function getFileContentsInternal(Task $task, int $fileId): StreamResponse|DataResponse { - $ids = $this->extractFileIdsFromTask($task); + $ids = $this->taskProcessingManager->extractFileIdsFromTask($task); if (!in_array($fileId, $ids)) { return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND); } @@ -429,45 +428,6 @@ class TaskProcessingApiController extends OCSController { } /** - * @param Task $task - * @return list<int> - * @throws NotFoundException - */ - private function extractFileIdsFromTask(Task $task): array { - $ids = []; - $taskTypes = $this->taskProcessingManager->getAvailableTaskTypes(); - if (!isset($taskTypes[$task->getTaskTypeId()])) { - throw new NotFoundException('Could not find task type'); - } - $taskType = $taskTypes[$task->getTaskTypeId()]; - foreach ($taskType['inputShape'] + $taskType['optionalInputShape'] as $key => $descriptor) { - if (in_array(EShapeType::getScalarType($descriptor->getShapeType()), [EShapeType::File, EShapeType::Image, EShapeType::Audio, EShapeType::Video], true)) { - /** @var int|list<int> $inputSlot */ - $inputSlot = $task->getInput()[$key]; - if (is_array($inputSlot)) { - $ids = array_merge($inputSlot, $ids); - } else { - $ids[] = $inputSlot; - } - } - } - if ($task->getOutput() !== null) { - foreach ($taskType['outputShape'] + $taskType['optionalOutputShape'] as $key => $descriptor) { - if (in_array(EShapeType::getScalarType($descriptor->getShapeType()), [EShapeType::File, EShapeType::Image, EShapeType::Audio, EShapeType::Video], true)) { - /** @var int|list<int> $outputSlot */ - $outputSlot = $task->getOutput()[$key]; - if (is_array($outputSlot)) { - $ids = array_merge($outputSlot, $ids); - } else { - $ids[] = $outputSlot; - } - } - } - } - return $ids; - } - - /** * Sets the task progress * * @param int $taskId The id of the task diff --git a/core/Migrations/Version32000Date20250806110519.php b/core/Migrations/Version32000Date20250806110519.php new file mode 100644 index 00000000000..c498a1cc820 --- /dev/null +++ b/core/Migrations/Version32000Date20250806110519.php @@ -0,0 +1,49 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Core\Migrations; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\Attributes\AddColumn; +use OCP\Migration\Attributes\ColumnType; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * + */ +#[AddColumn(table: 'taskprocessing_tasks', name: 'allow_cleanup', type: ColumnType::SMALLINT)] +class Version32000Date20250806110519 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('taskprocessing_tasks')) { + $table = $schema->getTable('taskprocessing_tasks'); + if (!$table->hasColumn('allow_cleanup')) { + $table->addColumn('allow_cleanup', Types::SMALLINT, [ + 'notnull' => true, + 'default' => 1, + 'unsigned' => true, + ]); + return $schema; + } + } + + return null; + } +} diff --git a/core/ResponseDefinitions.php b/core/ResponseDefinitions.php index 4bf1112a482..5a0c00524ee 100644 --- a/core/ResponseDefinitions.php +++ b/core/ResponseDefinitions.php @@ -201,6 +201,7 @@ namespace OC\Core; * scheduledAt: ?int, * startedAt: ?int, * endedAt: ?int, + * allowCleanup: bool, * } * * @psalm-type CoreProfileAction = array{ diff --git a/core/l10n/de.js b/core/l10n/de.js index ca78017e14b..fa6260109ee 100644 --- a/core/l10n/de.js +++ b/core/l10n/de.js @@ -254,6 +254,7 @@ OC.L10N.register( "Search people" : "Personen suchen", "People" : "Personen", "Filter in current view" : "Filter in aktueller Ansicht", + "Search connected services" : "Verbundene Dienste durchsuchen", "Results" : "Ergebnisse", "Load more results" : "Weitere Ergebnisse laden", "Search in" : "Suche in", diff --git a/core/l10n/de.json b/core/l10n/de.json index 065edb289f6..503cfda5316 100644 --- a/core/l10n/de.json +++ b/core/l10n/de.json @@ -252,6 +252,7 @@ "Search people" : "Personen suchen", "People" : "Personen", "Filter in current view" : "Filter in aktueller Ansicht", + "Search connected services" : "Verbundene Dienste durchsuchen", "Results" : "Ergebnisse", "Load more results" : "Weitere Ergebnisse laden", "Search in" : "Suche in", diff --git a/core/l10n/de_DE.js b/core/l10n/de_DE.js index 7aa73669136..58305e46326 100644 --- a/core/l10n/de_DE.js +++ b/core/l10n/de_DE.js @@ -254,6 +254,7 @@ OC.L10N.register( "Search people" : "Personen suchen", "People" : "Personen", "Filter in current view" : "Filter in aktueller Ansicht", + "Search connected services" : "Verbundene Dienste durchsuchen", "Results" : "Ergebnisse", "Load more results" : "Weitere Ergebnisse laden", "Search in" : "Suche in", diff --git a/core/l10n/de_DE.json b/core/l10n/de_DE.json index db90a92a1ba..1395757c619 100644 --- a/core/l10n/de_DE.json +++ b/core/l10n/de_DE.json @@ -252,6 +252,7 @@ "Search people" : "Personen suchen", "People" : "Personen", "Filter in current view" : "Filter in aktueller Ansicht", + "Search connected services" : "Verbundene Dienste durchsuchen", "Results" : "Ergebnisse", "Load more results" : "Weitere Ergebnisse laden", "Search in" : "Suche in", diff --git a/core/l10n/en_GB.js b/core/l10n/en_GB.js index 665ec6ba17a..35f37856c7c 100644 --- a/core/l10n/en_GB.js +++ b/core/l10n/en_GB.js @@ -254,6 +254,7 @@ OC.L10N.register( "Search people" : "Search people", "People" : "People", "Filter in current view" : "Filter in current view", + "Search connected services" : "Search connected services", "Results" : "Results", "Load more results" : "Load more results", "Search in" : "Search in", diff --git a/core/l10n/en_GB.json b/core/l10n/en_GB.json index d6578743667..358c71632f8 100644 --- a/core/l10n/en_GB.json +++ b/core/l10n/en_GB.json @@ -252,6 +252,7 @@ "Search people" : "Search people", "People" : "People", "Filter in current view" : "Filter in current view", + "Search connected services" : "Search connected services", "Results" : "Results", "Load more results" : "Load more results", "Search in" : "Search in", diff --git a/core/l10n/es.js b/core/l10n/es.js index 0ff5317118c..be8b459626d 100644 --- a/core/l10n/es.js +++ b/core/l10n/es.js @@ -254,6 +254,7 @@ OC.L10N.register( "Search people" : "Buscar personas", "People" : "Personas", "Filter in current view" : "Filtrar en la vista actual", + "Search connected services" : "Buscar servicios conectados", "Results" : "Resultados", "Load more results" : "Cargar más resultados", "Search in" : "Buscar en", diff --git a/core/l10n/es.json b/core/l10n/es.json index 054a0389acb..3b109e41290 100644 --- a/core/l10n/es.json +++ b/core/l10n/es.json @@ -252,6 +252,7 @@ "Search people" : "Buscar personas", "People" : "Personas", "Filter in current view" : "Filtrar en la vista actual", + "Search connected services" : "Buscar servicios conectados", "Results" : "Resultados", "Load more results" : "Cargar más resultados", "Search in" : "Buscar en", diff --git a/core/l10n/et_EE.js b/core/l10n/et_EE.js index 8bc6419636e..5166f1c79d4 100644 --- a/core/l10n/et_EE.js +++ b/core/l10n/et_EE.js @@ -230,6 +230,7 @@ OC.L10N.register( "Federated user" : "Kasutaja liitpilves", "user@your-nextcloud.org" : "kasutaja@sinu-nextcloud.ee", "Create share" : "Lisa jagamine", + "Direct link copied" : "Otselink on kopeeritud", "Please copy the link manually:" : "Palun kopeeri link käsitsi:", "Custom date range" : "Sinu valitud kuupäevavahemik", "Pick start date" : "Vali algkuupäev", @@ -253,6 +254,7 @@ OC.L10N.register( "Search people" : "Otsi inimesi", "People" : "Inimesed", "Filter in current view" : "Filtreeri selles vaates", + "Search connected services" : "Otsi ühendatud seadmeid", "Results" : "Tulemused", "Load more results" : "Laadi veel tulemusi", "Search in" : "Otsi siin:", diff --git a/core/l10n/et_EE.json b/core/l10n/et_EE.json index 05149ea7a23..f35b8a436c7 100644 --- a/core/l10n/et_EE.json +++ b/core/l10n/et_EE.json @@ -228,6 +228,7 @@ "Federated user" : "Kasutaja liitpilves", "user@your-nextcloud.org" : "kasutaja@sinu-nextcloud.ee", "Create share" : "Lisa jagamine", + "Direct link copied" : "Otselink on kopeeritud", "Please copy the link manually:" : "Palun kopeeri link käsitsi:", "Custom date range" : "Sinu valitud kuupäevavahemik", "Pick start date" : "Vali algkuupäev", @@ -251,6 +252,7 @@ "Search people" : "Otsi inimesi", "People" : "Inimesed", "Filter in current view" : "Filtreeri selles vaates", + "Search connected services" : "Otsi ühendatud seadmeid", "Results" : "Tulemused", "Load more results" : "Laadi veel tulemusi", "Search in" : "Otsi siin:", diff --git a/core/l10n/ja.js b/core/l10n/ja.js index 9edebafe544..69f67ebe562 100644 --- a/core/l10n/ja.js +++ b/core/l10n/ja.js @@ -230,6 +230,7 @@ OC.L10N.register( "Federated user" : "フェデレーションユーザー", "user@your-nextcloud.org" : "user@your-nextcloud.org", "Create share" : "共有を作成", + "Direct link copied" : "ダイレクトリンクをコピーしました", "Please copy the link manually:" : "手動でリンクをコピーしてください:", "Custom date range" : "カスタム日付の範囲", "Pick start date" : "開始日を指定", @@ -253,6 +254,7 @@ OC.L10N.register( "Search people" : "人物を検索", "People" : "ユーザー", "Filter in current view" : "現在のビューでフィルタ", + "Search connected services" : "関連サービスを検索", "Results" : "結果", "Load more results" : "次の結果を読み込む", "Search in" : "検索対象", diff --git a/core/l10n/ja.json b/core/l10n/ja.json index 0394920e547..ca4326642f7 100644 --- a/core/l10n/ja.json +++ b/core/l10n/ja.json @@ -228,6 +228,7 @@ "Federated user" : "フェデレーションユーザー", "user@your-nextcloud.org" : "user@your-nextcloud.org", "Create share" : "共有を作成", + "Direct link copied" : "ダイレクトリンクをコピーしました", "Please copy the link manually:" : "手動でリンクをコピーしてください:", "Custom date range" : "カスタム日付の範囲", "Pick start date" : "開始日を指定", @@ -251,6 +252,7 @@ "Search people" : "人物を検索", "People" : "ユーザー", "Filter in current view" : "現在のビューでフィルタ", + "Search connected services" : "関連サービスを検索", "Results" : "結果", "Load more results" : "次の結果を読み込む", "Search in" : "検索対象", diff --git a/core/l10n/pt_BR.js b/core/l10n/pt_BR.js index 3919a9f7eb7..c5b8b84a383 100644 --- a/core/l10n/pt_BR.js +++ b/core/l10n/pt_BR.js @@ -230,6 +230,7 @@ OC.L10N.register( "Federated user" : "Usuário federado", "user@your-nextcloud.org" : "user@your-nextcloud.org", "Create share" : "Criar compartilhamento", + "Direct link copied" : "Link direto copiado", "Please copy the link manually:" : "Copie o link manualmente:", "Custom date range" : "Intervalo personalizado", "Pick start date" : "Escolha uma data de início", diff --git a/core/l10n/pt_BR.json b/core/l10n/pt_BR.json index 743a69027a1..6231bda28fa 100644 --- a/core/l10n/pt_BR.json +++ b/core/l10n/pt_BR.json @@ -228,6 +228,7 @@ "Federated user" : "Usuário federado", "user@your-nextcloud.org" : "user@your-nextcloud.org", "Create share" : "Criar compartilhamento", + "Direct link copied" : "Link direto copiado", "Please copy the link manually:" : "Copie o link manualmente:", "Custom date range" : "Intervalo personalizado", "Pick start date" : "Escolha uma data de início", diff --git a/core/l10n/sr.js b/core/l10n/sr.js index 390e6a89f0f..a0ddaffde6a 100644 --- a/core/l10n/sr.js +++ b/core/l10n/sr.js @@ -230,6 +230,7 @@ OC.L10N.register( "Federated user" : "Федерисани корисник", "user@your-nextcloud.org" : "korisnik@vas-nextcloud.org", "Create share" : "Kreirajte deljenje", + "Direct link copied" : "Директни линк је копиран", "Please copy the link manually:" : "Молимо вас да ручно корпирате линк:", "Custom date range" : "Произвољни опсег датума", "Pick start date" : "Изаберите почетни датум", diff --git a/core/l10n/sr.json b/core/l10n/sr.json index 05c9fb8aa02..448e1d8bf1f 100644 --- a/core/l10n/sr.json +++ b/core/l10n/sr.json @@ -228,6 +228,7 @@ "Federated user" : "Федерисани корисник", "user@your-nextcloud.org" : "korisnik@vas-nextcloud.org", "Create share" : "Kreirajte deljenje", + "Direct link copied" : "Директни линк је копиран", "Please copy the link manually:" : "Молимо вас да ручно корпирате линк:", "Custom date range" : "Произвољни опсег датума", "Pick start date" : "Изаберите почетни датум", diff --git a/core/l10n/zh_CN.js b/core/l10n/zh_CN.js index 8c64035aeea..6cc3c910ebe 100644 --- a/core/l10n/zh_CN.js +++ b/core/l10n/zh_CN.js @@ -230,6 +230,7 @@ OC.L10N.register( "Federated user" : "联合云用户", "user@your-nextcloud.org" : "user@your-nextcloud.org", "Create share" : "创建共享", + "Direct link copied" : "直接链接已复制", "Please copy the link manually:" : "请手动复制链接:", "Custom date range" : "自定义日期范围", "Pick start date" : "选择起始日期", @@ -253,6 +254,7 @@ OC.L10N.register( "Search people" : "搜索用户", "People" : "用户", "Filter in current view" : "在当前视图中筛选", + "Search connected services" : "搜索已连接的服务", "Results" : "结果", "Load more results" : "加载更多结果", "Search in" : "搜索", diff --git a/core/l10n/zh_CN.json b/core/l10n/zh_CN.json index a2e453cad86..ac126df01c9 100644 --- a/core/l10n/zh_CN.json +++ b/core/l10n/zh_CN.json @@ -228,6 +228,7 @@ "Federated user" : "联合云用户", "user@your-nextcloud.org" : "user@your-nextcloud.org", "Create share" : "创建共享", + "Direct link copied" : "直接链接已复制", "Please copy the link manually:" : "请手动复制链接:", "Custom date range" : "自定义日期范围", "Pick start date" : "选择起始日期", @@ -251,6 +252,7 @@ "Search people" : "搜索用户", "People" : "用户", "Filter in current view" : "在当前视图中筛选", + "Search connected services" : "搜索已连接的服务", "Results" : "结果", "Load more results" : "加载更多结果", "Search in" : "搜索", diff --git a/core/l10n/zh_HK.js b/core/l10n/zh_HK.js index fc51c994b6f..7487b4db1f3 100644 --- a/core/l10n/zh_HK.js +++ b/core/l10n/zh_HK.js @@ -254,6 +254,7 @@ OC.L10N.register( "Search people" : "搜尋人仕", "People" : "人物", "Filter in current view" : "目前檢視裡的篩選條件", + "Search connected services" : "搜尋已連結的服務", "Results" : "結果", "Load more results" : "載入更多結果", "Search in" : "搜尋", diff --git a/core/l10n/zh_HK.json b/core/l10n/zh_HK.json index 0d1d227094c..07fd64fcc53 100644 --- a/core/l10n/zh_HK.json +++ b/core/l10n/zh_HK.json @@ -252,6 +252,7 @@ "Search people" : "搜尋人仕", "People" : "人物", "Filter in current view" : "目前檢視裡的篩選條件", + "Search connected services" : "搜尋已連結的服務", "Results" : "結果", "Load more results" : "載入更多結果", "Search in" : "搜尋", diff --git a/core/l10n/zh_TW.js b/core/l10n/zh_TW.js index 7b730c8b827..2ee4df6d897 100644 --- a/core/l10n/zh_TW.js +++ b/core/l10n/zh_TW.js @@ -230,6 +230,7 @@ OC.L10N.register( "Federated user" : "聯盟使用者", "user@your-nextcloud.org" : "user@your-nextcloud.org", "Create share" : "建立分享", + "Direct link copied" : "已複製直接連結", "Please copy the link manually:" : "請手動複製連結:", "Custom date range" : "自訂日期範圍", "Pick start date" : "挑選開始日期", @@ -253,6 +254,7 @@ OC.L10N.register( "Search people" : "搜尋人物", "People" : "人物", "Filter in current view" : "目前檢視中的篩選條件", + "Search connected services" : "搜尋已連結的服務", "Results" : "結果", "Load more results" : "載入更多結果", "Search in" : "搜尋條件", diff --git a/core/l10n/zh_TW.json b/core/l10n/zh_TW.json index c2c548b95f7..1ba4fe09f17 100644 --- a/core/l10n/zh_TW.json +++ b/core/l10n/zh_TW.json @@ -228,6 +228,7 @@ "Federated user" : "聯盟使用者", "user@your-nextcloud.org" : "user@your-nextcloud.org", "Create share" : "建立分享", + "Direct link copied" : "已複製直接連結", "Please copy the link manually:" : "請手動複製連結:", "Custom date range" : "自訂日期範圍", "Pick start date" : "挑選開始日期", @@ -251,6 +252,7 @@ "Search people" : "搜尋人物", "People" : "人物", "Filter in current view" : "目前檢視中的篩選條件", + "Search connected services" : "搜尋已連結的服務", "Results" : "結果", "Load more results" : "載入更多結果", "Search in" : "搜尋條件", diff --git a/core/openapi-ex_app.json b/core/openapi-ex_app.json index 4dad268c1b3..21e349ffbd0 100644 --- a/core/openapi-ex_app.json +++ b/core/openapi-ex_app.json @@ -145,7 +145,8 @@ "progress", "scheduledAt", "startedAt", - "endedAt" + "endedAt", + "allowCleanup" ], "properties": { "id": { @@ -216,6 +217,9 @@ "type": "integer", "format": "int64", "nullable": true + }, + "allowCleanup": { + "type": "boolean" } } } diff --git a/core/openapi-full.json b/core/openapi-full.json index 9b52d366f26..b95a88b191a 100644 --- a/core/openapi-full.json +++ b/core/openapi-full.json @@ -639,7 +639,8 @@ "progress", "scheduledAt", "startedAt", - "endedAt" + "endedAt", + "allowCleanup" ], "properties": { "id": { @@ -710,6 +711,9 @@ "type": "integer", "format": "int64", "nullable": true + }, + "allowCleanup": { + "type": "boolean" } } }, diff --git a/core/openapi.json b/core/openapi.json index dddd7cf6587..1c72bff9f82 100644 --- a/core/openapi.json +++ b/core/openapi.json @@ -639,7 +639,8 @@ "progress", "scheduledAt", "startedAt", - "endedAt" + "endedAt", + "allowCleanup" ], "properties": { "id": { @@ -710,6 +711,9 @@ "type": "integer", "format": "int64", "nullable": true + }, + "allowCleanup": { + "type": "boolean" } } }, diff --git a/core/register_command.php b/core/register_command.php index ed64983e762..9fd5b9b611e 100644 --- a/core/register_command.php +++ b/core/register_command.php @@ -253,6 +253,7 @@ if ($config->getSystemValueBool('installed', false)) { $application->add(Server::get(EnabledCommand::class)); $application->add(Server::get(Command\TaskProcessing\ListCommand::class)); $application->add(Server::get(Statistics::class)); + $application->add(Server::get(Command\TaskProcessing\Cleanup::class)); $application->add(Server::get(RedisCommand::class)); $application->add(Server::get(DistributedClear::class)); diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index dde92e40747..8d20f898dc3 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -1349,6 +1349,7 @@ return array( 'OC\\Core\\Command\\SystemTag\\Delete' => $baseDir . '/core/Command/SystemTag/Delete.php', 'OC\\Core\\Command\\SystemTag\\Edit' => $baseDir . '/core/Command/SystemTag/Edit.php', 'OC\\Core\\Command\\SystemTag\\ListCommand' => $baseDir . '/core/Command/SystemTag/ListCommand.php', + 'OC\\Core\\Command\\TaskProcessing\\Cleanup' => $baseDir . '/core/Command/TaskProcessing/Cleanup.php', 'OC\\Core\\Command\\TaskProcessing\\EnabledCommand' => $baseDir . '/core/Command/TaskProcessing/EnabledCommand.php', 'OC\\Core\\Command\\TaskProcessing\\GetCommand' => $baseDir . '/core/Command/TaskProcessing/GetCommand.php', 'OC\\Core\\Command\\TaskProcessing\\ListCommand' => $baseDir . '/core/Command/TaskProcessing/ListCommand.php', @@ -1515,6 +1516,7 @@ return array( 'OC\\Core\\Migrations\\Version31000Date20250213102442' => $baseDir . '/core/Migrations/Version31000Date20250213102442.php', 'OC\\Core\\Migrations\\Version32000Date20250620081925' => $baseDir . '/core/Migrations/Version32000Date20250620081925.php', 'OC\\Core\\Migrations\\Version32000Date20250731062008' => $baseDir . '/core/Migrations/Version32000Date20250731062008.php', + 'OC\\Core\\Migrations\\Version32000Date20250806110519' => $baseDir . '/core/Migrations/Version32000Date20250806110519.php', 'OC\\Core\\Notification\\CoreNotifier' => $baseDir . '/core/Notification/CoreNotifier.php', 'OC\\Core\\ResponseDefinitions' => $baseDir . '/core/ResponseDefinitions.php', 'OC\\Core\\Service\\LoginFlowV2Service' => $baseDir . '/core/Service/LoginFlowV2Service.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index d938b355f80..ef0e6d35740 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -1390,6 +1390,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Core\\Command\\SystemTag\\Delete' => __DIR__ . '/../../..' . '/core/Command/SystemTag/Delete.php', 'OC\\Core\\Command\\SystemTag\\Edit' => __DIR__ . '/../../..' . '/core/Command/SystemTag/Edit.php', 'OC\\Core\\Command\\SystemTag\\ListCommand' => __DIR__ . '/../../..' . '/core/Command/SystemTag/ListCommand.php', + 'OC\\Core\\Command\\TaskProcessing\\Cleanup' => __DIR__ . '/../../..' . '/core/Command/TaskProcessing/Cleanup.php', 'OC\\Core\\Command\\TaskProcessing\\EnabledCommand' => __DIR__ . '/../../..' . '/core/Command/TaskProcessing/EnabledCommand.php', 'OC\\Core\\Command\\TaskProcessing\\GetCommand' => __DIR__ . '/../../..' . '/core/Command/TaskProcessing/GetCommand.php', 'OC\\Core\\Command\\TaskProcessing\\ListCommand' => __DIR__ . '/../../..' . '/core/Command/TaskProcessing/ListCommand.php', @@ -1556,6 +1557,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Core\\Migrations\\Version31000Date20250213102442' => __DIR__ . '/../../..' . '/core/Migrations/Version31000Date20250213102442.php', 'OC\\Core\\Migrations\\Version32000Date20250620081925' => __DIR__ . '/../../..' . '/core/Migrations/Version32000Date20250620081925.php', 'OC\\Core\\Migrations\\Version32000Date20250731062008' => __DIR__ . '/../../..' . '/core/Migrations/Version32000Date20250731062008.php', + 'OC\\Core\\Migrations\\Version32000Date20250806110519' => __DIR__ . '/../../..' . '/core/Migrations/Version32000Date20250806110519.php', 'OC\\Core\\Notification\\CoreNotifier' => __DIR__ . '/../../..' . '/core/Notification/CoreNotifier.php', 'OC\\Core\\ResponseDefinitions' => __DIR__ . '/../../..' . '/core/ResponseDefinitions.php', 'OC\\Core\\Service\\LoginFlowV2Service' => __DIR__ . '/../../..' . '/core/Service/LoginFlowV2Service.php', diff --git a/lib/l10n/sr.js b/lib/l10n/sr.js index ac9d3bb0520..42f9cdf5fdb 100644 --- a/lib/l10n/sr.js +++ b/lib/l10n/sr.js @@ -129,6 +129,8 @@ OC.L10N.register( "Settings" : "Поставке", "Log out" : "Одјава", "Accounts" : "Налози", + "Bluesky" : "Bluesky", + "View %s on Bluesky" : "Прикажи %s на Bluesky", "Email" : "Е-пошта", "Mail %s" : "Пошта %s", "Fediverse" : "Fediverse", diff --git a/lib/l10n/sr.json b/lib/l10n/sr.json index 0cf5937a91d..8d5a255ef96 100644 --- a/lib/l10n/sr.json +++ b/lib/l10n/sr.json @@ -127,6 +127,8 @@ "Settings" : "Поставке", "Log out" : "Одјава", "Accounts" : "Налози", + "Bluesky" : "Bluesky", + "View %s on Bluesky" : "Прикажи %s на Bluesky", "Email" : "Е-пошта", "Mail %s" : "Пошта %s", "Fediverse" : "Fediverse", diff --git a/lib/l10n/sw.js b/lib/l10n/sw.js index aa914220945..293e9450d1d 100644 --- a/lib/l10n/sw.js +++ b/lib/l10n/sw.js @@ -2,9 +2,11 @@ OC.L10N.register( "lib", { "Other activities" : "Other activities", + "%1$s and %2$s" : "%1$s na %2$s", "Authentication" : "Uthibitisho", "Unknown filetype" : "Aina ya faili haijulikani", "Invalid image" : "Taswira si halisi", + "Avatar image is not square" : "Avatar image is not square", "Files" : "Mafaili", "View profile" : "Angalia wasifu", "Local time: %s" : "Muda wa kawaida: %s", diff --git a/lib/l10n/sw.json b/lib/l10n/sw.json index f0dc60848ce..708f4eb58cb 100644 --- a/lib/l10n/sw.json +++ b/lib/l10n/sw.json @@ -1,8 +1,10 @@ { "translations": { "Other activities" : "Other activities", + "%1$s and %2$s" : "%1$s na %2$s", "Authentication" : "Uthibitisho", "Unknown filetype" : "Aina ya faili haijulikani", "Invalid image" : "Taswira si halisi", + "Avatar image is not square" : "Avatar image is not square", "Files" : "Mafaili", "View profile" : "Angalia wasifu", "Local time: %s" : "Muda wa kawaida: %s", diff --git a/lib/private/TaskProcessing/Db/Task.php b/lib/private/TaskProcessing/Db/Task.php index 4d919deaf94..05c0ae9ac74 100644 --- a/lib/private/TaskProcessing/Db/Task.php +++ b/lib/private/TaskProcessing/Db/Task.php @@ -45,6 +45,8 @@ use OCP\TaskProcessing\Task as OCPTask; * @method int getStartedAt() * @method setEndedAt(int $endedAt) * @method int getEndedAt() + * @method setAllowCleanup(int $allowCleanup) + * @method int getAllowCleanup() */ class Task extends Entity { protected $lastUpdated; @@ -63,16 +65,17 @@ class Task extends Entity { protected $scheduledAt; protected $startedAt; protected $endedAt; + protected $allowCleanup; /** * @var string[] */ - public static array $columns = ['id', 'last_updated', 'type', 'input', 'output', 'status', 'user_id', 'app_id', 'custom_id', 'completion_expected_at', 'error_message', 'progress', 'webhook_uri', 'webhook_method', 'scheduled_at', 'started_at', 'ended_at']; + public static array $columns = ['id', 'last_updated', 'type', 'input', 'output', 'status', 'user_id', 'app_id', 'custom_id', 'completion_expected_at', 'error_message', 'progress', 'webhook_uri', 'webhook_method', 'scheduled_at', 'started_at', 'ended_at', 'allow_cleanup']; /** * @var string[] */ - public static array $fields = ['id', 'lastUpdated', 'type', 'input', 'output', 'status', 'userId', 'appId', 'customId', 'completionExpectedAt', 'errorMessage', 'progress', 'webhookUri', 'webhookMethod', 'scheduledAt', 'startedAt', 'endedAt']; + public static array $fields = ['id', 'lastUpdated', 'type', 'input', 'output', 'status', 'userId', 'appId', 'customId', 'completionExpectedAt', 'errorMessage', 'progress', 'webhookUri', 'webhookMethod', 'scheduledAt', 'startedAt', 'endedAt', 'allowCleanup']; public function __construct() { @@ -94,6 +97,7 @@ class Task extends Entity { $this->addType('scheduledAt', 'integer'); $this->addType('startedAt', 'integer'); $this->addType('endedAt', 'integer'); + $this->addType('allowCleanup', 'integer'); } public function toRow(): array { @@ -122,6 +126,7 @@ class Task extends Entity { 'scheduledAt' => $task->getScheduledAt(), 'startedAt' => $task->getStartedAt(), 'endedAt' => $task->getEndedAt(), + 'allowCleanup' => $task->getAllowCleanup() ? 1 : 0, ]); return $taskEntity; } @@ -144,6 +149,7 @@ class Task extends Entity { $task->setScheduledAt($this->getScheduledAt()); $task->setStartedAt($this->getStartedAt()); $task->setEndedAt($this->getEndedAt()); + $task->setAllowCleanup($this->getAllowCleanup() !== 0); return $task; } } diff --git a/lib/private/TaskProcessing/Db/TaskMapper.php b/lib/private/TaskProcessing/Db/TaskMapper.php index 91fd68820ae..fee96534633 100644 --- a/lib/private/TaskProcessing/Db/TaskMapper.php +++ b/lib/private/TaskProcessing/Db/TaskMapper.php @@ -183,16 +183,39 @@ class TaskMapper extends QBMapper { /** * @param int $timeout + * @param bool $force If true, ignore the allow_cleanup flag * @return int the number of deleted tasks * @throws Exception */ - public function deleteOlderThan(int $timeout): int { + public function deleteOlderThan(int $timeout, bool $force = false): int { $qb = $this->db->getQueryBuilder(); $qb->delete($this->tableName) ->where($qb->expr()->lt('last_updated', $qb->createPositionalParameter($this->timeFactory->getDateTime()->getTimestamp() - $timeout))); + if (!$force) { + $qb->andWhere($qb->expr()->eq('allow_cleanup', $qb->createPositionalParameter(1, IQueryBuilder::PARAM_INT))); + } return $qb->executeStatement(); } + /** + * @param int $timeout + * @param bool $force If true, ignore the allow_cleanup flag + * @return \Generator<Task> + * @throws Exception + */ + public function getTasksToCleanup(int $timeout, bool $force = false): \Generator { + $qb = $this->db->getQueryBuilder(); + $qb->select(Task::$columns) + ->from($this->tableName) + ->where($qb->expr()->lt('last_updated', $qb->createPositionalParameter($this->timeFactory->getDateTime()->getTimestamp() - $timeout))); + if (!$force) { + $qb->andWhere($qb->expr()->eq('allow_cleanup', $qb->createPositionalParameter(1, IQueryBuilder::PARAM_INT))); + } + foreach ($this->yieldEntities($qb) as $entity) { + yield $entity; + }; + } + public function update(Entity $entity): Entity { $entity->setLastUpdated($this->timeFactory->now()->getTimestamp()); return parent::update($entity); diff --git a/lib/private/TaskProcessing/Manager.php b/lib/private/TaskProcessing/Manager.php index 11fb2bed559..e288f2981a8 100644 --- a/lib/private/TaskProcessing/Manager.php +++ b/lib/private/TaskProcessing/Manager.php @@ -30,6 +30,7 @@ use OCP\Files\IRootFolder; use OCP\Files\Node; use OCP\Files\NotPermittedException; use OCP\Files\SimpleFS\ISimpleFile; +use OCP\Files\SimpleFS\ISimpleFolder; use OCP\Http\Client\IClientService; use OCP\IAppConfig; use OCP\ICache; @@ -78,6 +79,8 @@ class Manager implements IManager { 'ai.taskprocessing_provider_preferences', ]; + public const MAX_TASK_AGE_SECONDS = 60 * 60 * 24 * 30 * 4; // 4 months + /** @var list<IProvider>|null */ private ?array $providers = null; @@ -1449,6 +1452,97 @@ class Manager implements IManager { } /** + * @param Task $task + * @return list<int> + * @throws NotFoundException + */ + public function extractFileIdsFromTask(Task $task): array { + $ids = []; + $taskTypes = $this->getAvailableTaskTypes(); + if (!isset($taskTypes[$task->getTaskTypeId()])) { + throw new NotFoundException('Could not find task type'); + } + $taskType = $taskTypes[$task->getTaskTypeId()]; + foreach ($taskType['inputShape'] + $taskType['optionalInputShape'] as $key => $descriptor) { + if (in_array(EShapeType::getScalarType($descriptor->getShapeType()), [EShapeType::File, EShapeType::Image, EShapeType::Audio, EShapeType::Video], true)) { + /** @var int|list<int> $inputSlot */ + $inputSlot = $task->getInput()[$key]; + if (is_array($inputSlot)) { + $ids = array_merge($inputSlot, $ids); + } else { + $ids[] = $inputSlot; + } + } + } + if ($task->getOutput() !== null) { + foreach ($taskType['outputShape'] + $taskType['optionalOutputShape'] as $key => $descriptor) { + if (in_array(EShapeType::getScalarType($descriptor->getShapeType()), [EShapeType::File, EShapeType::Image, EShapeType::Audio, EShapeType::Video], true)) { + /** @var int|list<int> $outputSlot */ + $outputSlot = $task->getOutput()[$key]; + if (is_array($outputSlot)) { + $ids = array_merge($outputSlot, $ids); + } else { + $ids[] = $outputSlot; + } + } + } + } + return $ids; + } + + /** + * @param ISimpleFolder $folder + * @param int $ageInSeconds + * @return \Generator + */ + public function clearFilesOlderThan(ISimpleFolder $folder, int $ageInSeconds = self::MAX_TASK_AGE_SECONDS): \Generator { + foreach ($folder->getDirectoryListing() as $file) { + if ($file->getMTime() < time() - $ageInSeconds) { + try { + $fileName = $file->getName(); + $file->delete(); + yield $fileName; + } catch (NotPermittedException $e) { + $this->logger->warning('Failed to delete a stale task processing file', ['exception' => $e]); + } + } + } + } + + /** + * @param int $ageInSeconds + * @return \Generator + * @throws Exception + * @throws InvalidPathException + * @throws NotFoundException + * @throws \JsonException + * @throws \OCP\Files\NotFoundException + */ + public function cleanupTaskProcessingTaskFiles(int $ageInSeconds = self::MAX_TASK_AGE_SECONDS): \Generator { + $taskIdsToCleanup = []; + foreach ($this->taskMapper->getTasksToCleanup($ageInSeconds) as $task) { + $taskIdsToCleanup[] = $task->getId(); + $ocpTask = $task->toPublicTask(); + $fileIds = $this->extractFileIdsFromTask($ocpTask); + foreach ($fileIds as $fileId) { + // only look for output files stored in appData/TaskProcessing/ + $file = $this->rootFolder->getFirstNodeByIdInPath($fileId, '/' . $this->rootFolder->getAppDataDirectoryName() . '/core/TaskProcessing/'); + if ($file instanceof File) { + try { + $fileId = $file->getId(); + $fileName = $file->getName(); + $file->delete(); + yield ['task_id' => $task->getId(), 'file_id' => $fileId, 'file_name' => $fileName]; + } catch (NotPermittedException $e) { + $this->logger->warning('Failed to delete a stale task processing file', ['exception' => $e]); + } + } + } + } + return $taskIdsToCleanup; + } + + /** * Make a request to the task's webhookUri if necessary * * @param Task $task diff --git a/lib/private/TaskProcessing/RemoveOldTasksBackgroundJob.php b/lib/private/TaskProcessing/RemoveOldTasksBackgroundJob.php index 42d073a024d..52fc204b752 100644 --- a/lib/private/TaskProcessing/RemoveOldTasksBackgroundJob.php +++ b/lib/private/TaskProcessing/RemoveOldTasksBackgroundJob.php @@ -10,17 +10,14 @@ use OC\TaskProcessing\Db\TaskMapper; use OCP\AppFramework\Utility\ITimeFactory; use OCP\BackgroundJob\TimedJob; use OCP\Files\AppData\IAppDataFactory; -use OCP\Files\NotFoundException; -use OCP\Files\NotPermittedException; -use OCP\Files\SimpleFS\ISimpleFolder; use Psr\Log\LoggerInterface; class RemoveOldTasksBackgroundJob extends TimedJob { - public const MAX_TASK_AGE_SECONDS = 60 * 60 * 24 * 30 * 4; // 4 months private \OCP\Files\IAppData $appData; public function __construct( ITimeFactory $timeFactory, + private Manager $taskProcessingManager, private TaskMapper $taskMapper, private LoggerInterface $logger, IAppDataFactory $appDataFactory, @@ -32,48 +29,29 @@ class RemoveOldTasksBackgroundJob extends TimedJob { $this->appData = $appDataFactory->get('core'); } - /** * @inheritDoc */ protected function run($argument): void { try { - $this->taskMapper->deleteOlderThan(self::MAX_TASK_AGE_SECONDS); - } catch (\OCP\DB\Exception $e) { - $this->logger->warning('Failed to delete stale task processing tasks', ['exception' => $e]); + iterator_to_array($this->taskProcessingManager->cleanupTaskProcessingTaskFiles()); + } catch (\Exception $e) { + $this->logger->warning('Failed to delete stale task processing tasks files', ['exception' => $e]); } try { - $this->clearFilesOlderThan($this->appData->getFolder('text2image'), self::MAX_TASK_AGE_SECONDS); - } catch (NotFoundException $e) { - // noop + $this->taskMapper->deleteOlderThan(Manager::MAX_TASK_AGE_SECONDS); + } catch (\OCP\DB\Exception $e) { + $this->logger->warning('Failed to delete stale task processing tasks', ['exception' => $e]); } try { - $this->clearFilesOlderThan($this->appData->getFolder('audio2text'), self::MAX_TASK_AGE_SECONDS); - } catch (NotFoundException $e) { + iterator_to_array($this->taskProcessingManager->clearFilesOlderThan($this->appData->getFolder('text2image'))); + } catch (\OCP\Files\NotFoundException $e) { // noop } try { - $this->clearFilesOlderThan($this->appData->getFolder('TaskProcessing'), self::MAX_TASK_AGE_SECONDS); - } catch (NotFoundException $e) { + iterator_to_array($this->taskProcessingManager->clearFilesOlderThan($this->appData->getFolder('audio2text'))); + } catch (\OCP\Files\NotFoundException $e) { // noop } } - - /** - * @param ISimpleFolder $folder - * @param int $ageInSeconds - * @return void - */ - private function clearFilesOlderThan(ISimpleFolder $folder, int $ageInSeconds): void { - foreach ($folder->getDirectoryListing() as $file) { - if ($file->getMTime() < time() - $ageInSeconds) { - try { - $file->delete(); - } catch (NotPermittedException $e) { - $this->logger->warning('Failed to delete a stale task processing file', ['exception' => $e]); - } - } - } - } - } diff --git a/lib/public/TaskProcessing/IManager.php b/lib/public/TaskProcessing/IManager.php index 723eca8f615..731250d7aa1 100644 --- a/lib/public/TaskProcessing/IManager.php +++ b/lib/public/TaskProcessing/IManager.php @@ -234,4 +234,14 @@ interface IManager { * @since 30.0.0 */ public function setTaskStatus(Task $task, int $status): void; + + /** + * Extract all input and output file IDs from a task + * + * @param Task $task + * @return list<int> + * @throws NotFoundException + * @since 32.0.0 + */ + public function extractFileIdsFromTask(Task $task): array; } diff --git a/lib/public/TaskProcessing/Task.php b/lib/public/TaskProcessing/Task.php index 71c271cd43d..06dc84d59ff 100644 --- a/lib/public/TaskProcessing/Task.php +++ b/lib/public/TaskProcessing/Task.php @@ -66,6 +66,7 @@ final class Task implements \JsonSerializable { protected ?int $scheduledAt = null; protected ?int $startedAt = null; protected ?int $endedAt = null; + protected bool $allowCleanup = true; /** * @param string $taskTypeId @@ -253,7 +254,23 @@ final class Task implements \JsonSerializable { } /** - * @psalm-return array{id: int, lastUpdated: int, type: string, status: 'STATUS_CANCELLED'|'STATUS_FAILED'|'STATUS_SUCCESSFUL'|'STATUS_RUNNING'|'STATUS_SCHEDULED'|'STATUS_UNKNOWN', userId: ?string, appId: string, input: array<string, list<numeric|string>|numeric|string>, output: ?array<string, list<numeric|string>|numeric|string>, customId: ?string, completionExpectedAt: ?int, progress: ?float, scheduledAt: ?int, startedAt: ?int, endedAt: ?int} + * @return bool + * @since 32.0.0 + */ + final public function getAllowCleanup(): bool { + return $this->allowCleanup; + } + + /** + * @param bool $allowCleanup + * @since 32.0.0 + */ + final public function setAllowCleanup(bool $allowCleanup): void { + $this->allowCleanup = $allowCleanup; + } + + /** + * @psalm-return array{id: int, lastUpdated: int, type: string, status: 'STATUS_CANCELLED'|'STATUS_FAILED'|'STATUS_SUCCESSFUL'|'STATUS_RUNNING'|'STATUS_SCHEDULED'|'STATUS_UNKNOWN', userId: ?string, appId: string, input: array<string, list<numeric|string>|numeric|string>, output: ?array<string, list<numeric|string>|numeric|string>, customId: ?string, completionExpectedAt: ?int, progress: ?float, scheduledAt: ?int, startedAt: ?int, endedAt: ?int, allowCleanup: bool} * @since 30.0.0 */ final public function jsonSerialize(): array { @@ -272,6 +289,7 @@ final class Task implements \JsonSerializable { 'scheduledAt' => $this->getScheduledAt(), 'startedAt' => $this->getStartedAt(), 'endedAt' => $this->getEndedAt(), + 'allowCleanup' => $this->getAllowCleanup(), ]; } diff --git a/openapi.json b/openapi.json index 5e6e38d0d05..3e45158ecbb 100644 --- a/openapi.json +++ b/openapi.json @@ -677,7 +677,8 @@ "progress", "scheduledAt", "startedAt", - "endedAt" + "endedAt", + "allowCleanup" ], "properties": { "id": { @@ -748,6 +749,9 @@ "type": "integer", "format": "int64", "nullable": true + }, + "allowCleanup": { + "type": "boolean" } } }, diff --git a/package-lock.json b/package-lock.json index 9c21c751b26..88e89c19d73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -139,7 +139,7 @@ "karma-viewport": "^1.0.9", "mime": "^4.0.7", "msw": "^2.10.4", - "puppeteer": "^24.16.0", + "puppeteer": "^24.16.2", "raw-loader": "^4.0.2", "regextras": "^0.8.0", "sass": "^1.90.0", @@ -8805,9 +8805,9 @@ "optional": true }, "node_modules/bare-fs": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.6.tgz", - "integrity": "sha512-25RsLF33BqooOEFNdMcEhMpJy8EoR88zSMrnOQOaM3USnOK2VmaJ1uaQEwPA6AQjrv1lXChScosN6CzbwbO9OQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.2.0.tgz", + "integrity": "sha512-oRfrw7gwwBVAWx9S5zPMo2iiOjxyiZE12DmblmMQREgcogbNO0AFaZ+QBxxkEXiPspcpvO/Qtqn8LabUx4uYXg==", "dev": true, "optional": true, "dependencies": { @@ -8848,9 +8848,9 @@ } }, "node_modules/bare-stream": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz", - "integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz", + "integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==", "dev": true, "optional": true, "dependencies": { @@ -9689,9 +9689,9 @@ } }, "node_modules/chromium-bidi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-7.2.0.tgz", - "integrity": "sha512-gREyhyBstermK+0RbcJLbFhcQctg92AGgDe/h/taMJEOLRdtSswBAO9KmvltFSQWgM2LrwWu5SIuEUbdm3JsyQ==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-7.3.1.tgz", + "integrity": "sha512-i+BMGluhZZc4Jic9L1aHJBTfaopxmCqQxGklyMcqFx4fvF3nI4BJ3bCe1ad474nvYRIo/ZN/VrdA4eOaRZua4Q==", "dev": true, "dependencies": { "mitt": "^3.0.1", @@ -15808,14 +15808,10 @@ } }, "node_modules/ip-address": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", - "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", "dev": true, - "dependencies": { - "jsbn": "1.1.0", - "sprintf-js": "^1.1.3" - }, "engines": { "node": ">= 12" } @@ -16949,12 +16945,6 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true }, - "node_modules/jsbn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", - "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", - "dev": true - }, "node_modules/jsdoc-type-pratt-parser": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.0.0.tgz", @@ -20883,17 +20873,17 @@ "license": "MIT" }, "node_modules/puppeteer": { - "version": "24.16.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.16.0.tgz", - "integrity": "sha512-5qxFGOpdAzYexoPwKPEF4L/IYKYOFE1MxWsqcp7K33HySM8N8S/yZwSQCaV0rzmJsTLX5LxU4zt65+ceNiVDgQ==", + "version": "24.16.2", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.16.2.tgz", + "integrity": "sha512-eNjKzwjITM4Lvho6iHb+VQamadUBgc8TsjAApsKi5N8DXipxAaAZWssBOFsrIOLo4eYWYj0Qk5gmr4wBSqzJWw==", "dev": true, "hasInstallScript": true, "dependencies": { "@puppeteer/browsers": "2.10.6", - "chromium-bidi": "7.2.0", + "chromium-bidi": "7.3.1", "cosmiconfig": "^9.0.0", "devtools-protocol": "0.0.1475386", - "puppeteer-core": "24.16.0", + "puppeteer-core": "24.16.2", "typed-query-selector": "^2.12.0" }, "bin": { @@ -20904,13 +20894,13 @@ } }, "node_modules/puppeteer-core": { - "version": "24.16.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.16.0.tgz", - "integrity": "sha512-tZ0tJiOYaDGTRzzr2giDpf8O/55JsoqkrafS1Xu4H6S8oP4eeL6RbZzY9OzjShSf5EQvx/zAc55QKpDqzXos/Q==", + "version": "24.16.2", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.16.2.tgz", + "integrity": "sha512-areKSSQzpoHa5nCk3uD/o504yjrW5ws0N6jZfdFZ3a4H+Q7NBgvuDydjN5P87jN4Rj+eIpLcK3ELOThTtYuuxg==", "dev": true, "dependencies": { "@puppeteer/browsers": "2.10.6", - "chromium-bidi": "7.2.0", + "chromium-bidi": "7.3.1", "debug": "^4.4.1", "devtools-protocol": "0.0.1475386", "typed-query-selector": "^2.12.0", @@ -22942,12 +22932,12 @@ } }, "node_modules/socks": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.6.tgz", - "integrity": "sha512-pe4Y2yzru68lXCb38aAqRf5gvN8YdjP1lok5o0J7BOHljkyCGKVz7H3vpVIXKD27rj2giOJ7DwVyk/GWrPHDWA==", + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", "dev": true, "dependencies": { - "ip-address": "^9.0.5", + "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" }, "engines": { @@ -23204,12 +23194,6 @@ "integrity": "sha512-kpEo1WuMXuc6QfdQdO2V/fl/trONlkUKp+pputsLTiW9RMtwEvjb4/aYGm2m3+KAzjmb+zLwr4A4SYZu74+pgQ==", "license": "MIT" }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "dev": true - }, "node_modules/ssh2": { "version": "1.16.0", "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz", diff --git a/package.json b/package.json index 4bc3efb9fa1..cb27a4fae29 100644 --- a/package.json +++ b/package.json @@ -170,7 +170,7 @@ "karma-viewport": "^1.0.9", "mime": "^4.0.7", "msw": "^2.10.4", - "puppeteer": "^24.16.0", + "puppeteer": "^24.16.2", "raw-loader": "^4.0.2", "regextras": "^0.8.0", "sass": "^1.90.0", diff --git a/tests/lib/TaskProcessing/TaskProcessingTest.php b/tests/lib/TaskProcessing/TaskProcessingTest.php index fee4e9ba3ba..d2f619da349 100644 --- a/tests/lib/TaskProcessing/TaskProcessingTest.php +++ b/tests/lib/TaskProcessing/TaskProcessingTest.php @@ -972,6 +972,7 @@ class TaskProcessingTest extends \Test\TestCase { // run background job $bgJob = new RemoveOldTasksBackgroundJob( $timeFactory, + $this->manager, $this->taskMapper, Server::get(LoggerInterface::class), Server::get(IAppDataFactory::class), diff --git a/version.php b/version.php index c13cc6eebc2..97d570fc3db 100644 --- a/version.php +++ b/version.php @@ -9,7 +9,7 @@ // between betas, final and RCs. This is _not_ the public version number. Reset minor/patch level // when updating major/minor version number. -$OC_Version = [32, 0, 0, 2]; +$OC_Version = [32, 0, 0, 3]; // The human-readable string $OC_VersionString = '32.0.0 dev'; |