diff options
59 files changed, 1204 insertions, 181 deletions
diff --git a/apps/dav/lib/Connector/Sabre/File.php b/apps/dav/lib/Connector/Sabre/File.php index 9e927ff85e5..57c072fda47 100644 --- a/apps/dav/lib/Connector/Sabre/File.php +++ b/apps/dav/lib/Connector/Sabre/File.php @@ -164,14 +164,19 @@ class File extends Node implements IFile { $this->changeLock(ILockingProvider::LOCK_EXCLUSIVE); } - $target = $partStorage->fopen($internalPartPath, 'wb'); - if ($target === false) { - \OC::$server->getLogger()->error('\OC\Files\Filesystem::fopen() failed', ['app' => 'webdav']); - // because we have no clue about the cause we can only throw back a 500/Internal Server Error - throw new Exception('Could not write file contents'); + if ($partStorage->instanceOfStorage(Storage\IWriteStreamStorage::class)) { + $count = $partStorage->writeStream($internalPartPath, $data); + $result = $count > 0; + } else { + $target = $partStorage->fopen($internalPartPath, 'wb'); + if ($target === false) { + \OC::$server->getLogger()->error('\OC\Files\Filesystem::fopen() failed', ['app' => 'webdav']); + // because we have no clue about the cause we can only throw back a 500/Internal Server Error + throw new Exception('Could not write file contents'); + } + list($count, $result) = \OC_Helper::streamCopy($data, $target); + fclose($target); } - list($count, $result) = \OC_Helper::streamCopy($data, $target); - fclose($target); if ($result === false) { $expected = -1; @@ -185,7 +190,7 @@ class File extends Node implements IFile { // double check if the file was fully received // compare expected and actual size if (isset($_SERVER['CONTENT_LENGTH']) && $_SERVER['REQUEST_METHOD'] === 'PUT') { - $expected = (int) $_SERVER['CONTENT_LENGTH']; + $expected = (int)$_SERVER['CONTENT_LENGTH']; if ($count !== $expected) { throw new BadRequest('expected filesize ' . $expected . ' got ' . $count); } @@ -219,7 +224,7 @@ class File extends Node implements IFile { $renameOkay = $storage->moveFromStorage($partStorage, $internalPartPath, $internalPath); $fileExists = $storage->file_exists($internalPath); if ($renameOkay === false || $fileExists === false) { - \OC::$server->getLogger()->error('renaming part file to final file failed ($run: ' . ( $run ? 'true' : 'false' ) . ', $renameOkay: ' . ( $renameOkay ? 'true' : 'false' ) . ', $fileExists: ' . ( $fileExists ? 'true' : 'false' ) . ')', ['app' => 'webdav']); + \OC::$server->getLogger()->error('renaming part file to final file failed $renameOkay: ' . ($renameOkay ? 'true' : 'false') . ', $fileExists: ' . ($fileExists ? 'true' : 'false') . ')', ['app' => 'webdav']); throw new Exception('Could not rename part file to final file'); } } catch (ForbiddenException $ex) { @@ -246,7 +251,7 @@ class File extends Node implements IFile { $this->header('X-OC-MTime: accepted'); } } - + if ($view) { $this->emitPostHooks($exists); } @@ -443,7 +448,7 @@ class File extends Node implements IFile { //detect aborted upload if (isset ($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'PUT') { if (isset($_SERVER['CONTENT_LENGTH'])) { - $expected = (int) $_SERVER['CONTENT_LENGTH']; + $expected = (int)$_SERVER['CONTENT_LENGTH']; if ($bytesWritten !== $expected) { $chunk_handler->remove($info['index']); throw new BadRequest( diff --git a/apps/dav/tests/unit/Connector/Sabre/FileTest.php b/apps/dav/tests/unit/Connector/Sabre/FileTest.php index 5e7a6374206..edb61edc6ed 100644 --- a/apps/dav/tests/unit/Connector/Sabre/FileTest.php +++ b/apps/dav/tests/unit/Connector/Sabre/FileTest.php @@ -164,7 +164,7 @@ class FileTest extends \Test\TestCase { public function testSimplePutFails($thrownException, $expectedException, $checkPreviousClass = true) { // setup $storage = $this->getMockBuilder(Local::class) - ->setMethods(['fopen']) + ->setMethods(['writeStream']) ->setConstructorArgs([['datadir' => \OC::$server->getTempManager()->getTemporaryFolder()]]) ->getMock(); \OC\Files\Filesystem::mount($storage, [], $this->user . '/'); @@ -182,11 +182,11 @@ class FileTest extends \Test\TestCase { if ($thrownException !== null) { $storage->expects($this->once()) - ->method('fopen') + ->method('writeStream') ->will($this->throwException($thrownException)); } else { $storage->expects($this->once()) - ->method('fopen') + ->method('writeStream') ->will($this->returnValue(false)); } diff --git a/apps/files/css/files.scss b/apps/files/css/files.scss index 580bab32d6d..d6f9bd6131e 100644 --- a/apps/files/css/files.scss +++ b/apps/files/css/files.scss @@ -8,7 +8,12 @@ } /* FILE MENU */ -.actions { padding:5px; height:32px; display: inline-block; float: left; } +.actions { + padding: 5px; + height: 100%; + display: inline-block; + float: left; +} .actions input, .actions button, .actions .button { margin:0; float:left; } .actions .button a { color: #555; } .actions .button a:hover, @@ -659,8 +664,14 @@ table.dragshadow td.size { top: 100%; margin-top: 4px; min-width: 100px; - margin-left: 7px; + margin-left: 22px; /* Align left edge below center of + button … */ + transform: translateX(-50%); /* … then center it below button */ z-index: 1001; + + /* Center triangle */ + &::after { + left: calc(50% - 8px) !important; + } } #filestable .filename .action .icon, diff --git a/apps/files/js/detailsview.js b/apps/files/js/detailsview.js index cd602961c0a..bac2a5ebd21 100644 --- a/apps/files/js/detailsview.js +++ b/apps/files/js/detailsview.js @@ -174,6 +174,9 @@ // hide other tabs $tabsContainer.find('.tab').addClass('hidden'); + $tabsContainer.attr('class', 'tabsContainer'); + $tabsContainer.addClass(tabView.getTabsContainerExtraClasses()); + // tab already rendered ? if (!$tabEl.length) { // render tab diff --git a/apps/files/js/detailtabview.js b/apps/files/js/detailtabview.js index a66cedbc15d..1e046f30246 100644 --- a/apps/files/js/detailtabview.js +++ b/apps/files/js/detailtabview.js @@ -41,6 +41,21 @@ }, /** + * Returns the extra CSS classes used by the tabs container when this + * tab is the selected one. + * + * In general you should not extend this method, as tabs should not + * modify the classes of its container; this is reserved as a last + * resort for very specific cases in which there is no other way to get + * the proper style or behaviour. + * + * @return {String} space-separated CSS classes + */ + getTabsContainerExtraClasses: function() { + return ''; + }, + + /** * Returns the tab label * * @return {String} label diff --git a/apps/files/l10n/cs.js b/apps/files/l10n/cs.js index cbd6c72d572..96b93d361e7 100644 --- a/apps/files/l10n/cs.js +++ b/apps/files/l10n/cs.js @@ -142,6 +142,7 @@ OC.L10N.register( "WebDAV" : "WebDAV", "Use this address to <a href=\"%s\" target=\"_blank\" rel=\"noreferrer noopener\">access your Files via WebDAV</a>" : "Použijte tuto adresu pro <a href=\"%s\" target=\"_blank\" rel=\"noreferrer noopener\">přístup k vašim souborům přes WebDAV</a>", "Cancel upload" : "Zrušit nahrávání", + "Toggle grid view" : "Přepnout zobrazení mřížky", "No files in here" : "Žádné soubory", "Upload some content or sync with your devices!" : "Nahrajte nějaký obsah nebo synchronizujte se svými přístroji!", "No entries found in this folder" : "V této složce nebylo nic nalezeno", diff --git a/apps/files/l10n/cs.json b/apps/files/l10n/cs.json index a8f82c5c5d7..cb01165503d 100644 --- a/apps/files/l10n/cs.json +++ b/apps/files/l10n/cs.json @@ -140,6 +140,7 @@ "WebDAV" : "WebDAV", "Use this address to <a href=\"%s\" target=\"_blank\" rel=\"noreferrer noopener\">access your Files via WebDAV</a>" : "Použijte tuto adresu pro <a href=\"%s\" target=\"_blank\" rel=\"noreferrer noopener\">přístup k vašim souborům přes WebDAV</a>", "Cancel upload" : "Zrušit nahrávání", + "Toggle grid view" : "Přepnout zobrazení mřížky", "No files in here" : "Žádné soubory", "Upload some content or sync with your devices!" : "Nahrajte nějaký obsah nebo synchronizujte se svými přístroji!", "No entries found in this folder" : "V této složce nebylo nic nalezeno", diff --git a/apps/files_versions/appinfo/info.xml b/apps/files_versions/appinfo/info.xml index d2f873edb07..6d1b3085f80 100644 --- a/apps/files_versions/appinfo/info.xml +++ b/apps/files_versions/appinfo/info.xml @@ -41,4 +41,8 @@ <collection>OCA\Files_Versions\Sabre\RootCollection</collection> </collections> </sabre> + + <versions> + <backend for="OCP\Files\Storage\IStorage">OCA\Files_Versions\Versions\LegacyVersionsBackend</backend> + </versions> </info> diff --git a/apps/files_versions/composer/composer/autoload_classmap.php b/apps/files_versions/composer/composer/autoload_classmap.php index 4bb112b4f11..1283e533914 100644 --- a/apps/files_versions/composer/composer/autoload_classmap.php +++ b/apps/files_versions/composer/composer/autoload_classmap.php @@ -23,4 +23,11 @@ return array( 'OCA\\Files_Versions\\Sabre\\VersionHome' => $baseDir . '/../lib/Sabre/VersionHome.php', 'OCA\\Files_Versions\\Sabre\\VersionRoot' => $baseDir . '/../lib/Sabre/VersionRoot.php', 'OCA\\Files_Versions\\Storage' => $baseDir . '/../lib/Storage.php', + 'OCA\\Files_Versions\\Versions\\BackendNotFoundException' => $baseDir . '/../lib/Versions/BackendNotFoundException.php', + 'OCA\\Files_Versions\\Versions\\IVersion' => $baseDir . '/../lib/Versions/IVersion.php', + 'OCA\\Files_Versions\\Versions\\IVersionBackend' => $baseDir . '/../lib/Versions/IVersionBackend.php', + 'OCA\\Files_Versions\\Versions\\IVersionManager' => $baseDir . '/../lib/Versions/IVersionManager.php', + 'OCA\\Files_Versions\\Versions\\LegacyVersionsBackend' => $baseDir . '/../lib/Versions/LegacyVersionsBackend.php', + 'OCA\\Files_Versions\\Versions\\Version' => $baseDir . '/../lib/Versions/Version.php', + 'OCA\\Files_Versions\\Versions\\VersionManager' => $baseDir . '/../lib/Versions/VersionManager.php', ); diff --git a/apps/files_versions/composer/composer/autoload_static.php b/apps/files_versions/composer/composer/autoload_static.php index 29bc592b41c..6a6b753c2e5 100644 --- a/apps/files_versions/composer/composer/autoload_static.php +++ b/apps/files_versions/composer/composer/autoload_static.php @@ -38,6 +38,13 @@ class ComposerStaticInitFiles_Versions 'OCA\\Files_Versions\\Sabre\\VersionHome' => __DIR__ . '/..' . '/../lib/Sabre/VersionHome.php', 'OCA\\Files_Versions\\Sabre\\VersionRoot' => __DIR__ . '/..' . '/../lib/Sabre/VersionRoot.php', 'OCA\\Files_Versions\\Storage' => __DIR__ . '/..' . '/../lib/Storage.php', + 'OCA\\Files_Versions\\Versions\\BackendNotFoundException' => __DIR__ . '/..' . '/../lib/Versions/BackendNotFoundException.php', + 'OCA\\Files_Versions\\Versions\\IVersion' => __DIR__ . '/..' . '/../lib/Versions/IVersion.php', + 'OCA\\Files_Versions\\Versions\\IVersionBackend' => __DIR__ . '/..' . '/../lib/Versions/IVersionBackend.php', + 'OCA\\Files_Versions\\Versions\\IVersionManager' => __DIR__ . '/..' . '/../lib/Versions/IVersionManager.php', + 'OCA\\Files_Versions\\Versions\\LegacyVersionsBackend' => __DIR__ . '/..' . '/../lib/Versions/LegacyVersionsBackend.php', + 'OCA\\Files_Versions\\Versions\\Version' => __DIR__ . '/..' . '/../lib/Versions/Version.php', + 'OCA\\Files_Versions\\Versions\\VersionManager' => __DIR__ . '/..' . '/../lib/Versions/VersionManager.php', ); public static function getInitializer(ClassLoader $loader) diff --git a/apps/files_versions/lib/AppInfo/Application.php b/apps/files_versions/lib/AppInfo/Application.php index 340b5ab5cbd..935556221fa 100644 --- a/apps/files_versions/lib/AppInfo/Application.php +++ b/apps/files_versions/lib/AppInfo/Application.php @@ -24,9 +24,10 @@ namespace OCA\Files_Versions\AppInfo; use OCA\DAV\Connector\Sabre\Principal; +use OCA\Files_Versions\Versions\IVersionManager; +use OCA\Files_Versions\Versions\VersionManager; use OCP\AppFramework\App; -use OCA\Files_Versions\Expiration; -use OCP\AppFramework\Utility\ITimeFactory; +use OCP\AppFramework\IAppContainer; use OCA\Files_Versions\Capabilities; class Application extends App { @@ -43,14 +44,45 @@ class Application extends App { /* * Register $principalBackend for the DAV collection */ - $container->registerService('principalBackend', function () { + $container->registerService('principalBackend', function (IAppContainer $c) { + $server = $c->getServer(); return new Principal( - \OC::$server->getUserManager(), - \OC::$server->getGroupManager(), - \OC::$server->getShareManager(), - \OC::$server->getUserSession(), - \OC::$server->getConfig() + $server->getUserManager(), + $server->getGroupManager(), + $server->getShareManager(), + $server->getUserSession(), + $server->getConfig() ); }); + + $container->registerService(IVersionManager::class, function(IAppContainer $c) { + return new VersionManager(); + }); + + $this->registerVersionBackends(); + } + + public function registerVersionBackends() { + $server = $this->getContainer()->getServer(); + $logger = $server->getLogger(); + $appManager = $server->getAppManager(); + /** @var IVersionManager $versionManager */ + $versionManager = $this->getContainer()->getServer()->query(IVersionManager::class); + foreach($appManager->getInstalledApps() as $app) { + $appInfo = $appManager->getAppInfo($app); + if (isset($appInfo['versions'])) { + $backends = $appInfo['versions']; + foreach($backends as $backend) { + $class = $backend['@value']; + $for = $backend['@attributes']['for']; + try { + $backendObject = $server->query($class); + $versionManager->registerBackend($for, $backendObject); + } catch (\Exception $e) { + $logger->logException($e); + } + } + } + } } } diff --git a/apps/files_versions/lib/Controller/PreviewController.php b/apps/files_versions/lib/Controller/PreviewController.php index b8bf464fb3f..f41250a8971 100644 --- a/apps/files_versions/lib/Controller/PreviewController.php +++ b/apps/files_versions/lib/Controller/PreviewController.php @@ -21,45 +21,53 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. * */ + namespace OCA\Files_Versions\Controller; +use OCA\Files_Versions\Versions\IVersionManager; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\FileDisplayResponse; -use OCP\Files\File; -use OCP\Files\Folder; use OCP\Files\IMimeTypeDetector; use OCP\Files\IRootFolder; use OCP\Files\NotFoundException; use OCP\IPreview; use OCP\IRequest; +use OCP\IUserSession; class PreviewController extends Controller { /** @var IRootFolder */ private $rootFolder; - /** @var string */ - private $userId; + /** @var IUserSession */ + private $userSession; /** @var IMimeTypeDetector */ private $mimeTypeDetector; + /** @var IVersionManager */ + private $versionManager; + /** @var IPreview */ private $previewManager; - public function __construct($appName, - IRequest $request, - IRootFolder $rootFolder, - $userId, - IMimeTypeDetector $mimeTypeDetector, - IPreview $previewManager) { + public function __construct( + $appName, + IRequest $request, + IRootFolder $rootFolder, + IUserSession $userSession, + IMimeTypeDetector $mimeTypeDetector, + IVersionManager $versionManager, + IPreview $previewManager + ) { parent::__construct($appName, $request); $this->rootFolder = $rootFolder; - $this->userId = $userId; + $this->userSession = $userSession; $this->mimeTypeDetector = $mimeTypeDetector; + $this->versionManager = $versionManager; $this->previewManager = $previewManager; } @@ -79,20 +87,17 @@ class PreviewController extends Controller { $y = 44, $version = '' ) { - if($file === '' || $version === '' || $x === 0 || $y === 0) { + if ($file === '' || $version === '' || $x === 0 || $y === 0) { return new DataResponse([], Http::STATUS_BAD_REQUEST); } try { - $userFolder = $this->rootFolder->getUserFolder($this->userId); - /** @var Folder $versionFolder */ - $versionFolder = $userFolder->getParent()->get('files_versions'); - $mimeType = $this->mimeTypeDetector->detectPath($file); - $file = $versionFolder->get($file.'.v'.$version); - - /** @var File $file */ - $f = $this->previewManager->getPreview($file, $x, $y, true, IPreview::MODE_FILL, $mimeType); - return new FileDisplayResponse($f, Http::STATUS_OK, ['Content-Type' => $f->getMimeType()]); + $user = $this->userSession->getUser(); + $userFolder = $this->rootFolder->getUserFolder($user->getUID()); + $file = $userFolder->get($file); + $versionFile = $this->versionManager->getVersionFile($user, $file, (int)$version); + $preview = $this->previewManager->getPreview($versionFile, $x, $y, true, IPreview::MODE_FILL, $versionFile->getMimetype()); + return new FileDisplayResponse($preview, Http::STATUS_OK, ['Content-Type' => $preview->getMimeType()]); } catch (NotFoundException $e) { return new DataResponse([], Http::STATUS_NOT_FOUND); } catch (\InvalidArgumentException $e) { diff --git a/apps/files_versions/lib/Sabre/RestoreFolder.php b/apps/files_versions/lib/Sabre/RestoreFolder.php index c398d02692b..c8504646bad 100644 --- a/apps/files_versions/lib/Sabre/RestoreFolder.php +++ b/apps/files_versions/lib/Sabre/RestoreFolder.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace OCA\Files_Versions\Sabre; +use OCP\IUser; use Sabre\DAV\Exception\Forbidden; use Sabre\DAV\ICollection; use Sabre\DAV\IMoveTarget; @@ -31,14 +32,6 @@ use Sabre\DAV\INode; class RestoreFolder implements ICollection, IMoveTarget { - - /** @var string */ - protected $userId; - - public function __construct(string $userId) { - $this->userId = $userId; - } - public function createFile($name, $data = null) { throw new Forbidden(); } @@ -80,7 +73,8 @@ class RestoreFolder implements ICollection, IMoveTarget { return false; } - return $sourceNode->rollBack(); + $sourceNode->rollBack(); + return true; } } diff --git a/apps/files_versions/lib/Sabre/RootCollection.php b/apps/files_versions/lib/Sabre/RootCollection.php index ca5979573b5..504c3362505 100644 --- a/apps/files_versions/lib/Sabre/RootCollection.php +++ b/apps/files_versions/lib/Sabre/RootCollection.php @@ -20,10 +20,13 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. * */ + namespace OCA\Files_Versions\Sabre; +use OCA\Files_Versions\Versions\IVersionManager; use OCP\Files\IRootFolder; use OCP\IConfig; +use OCP\IUserManager; use Sabre\DAV\INode; use Sabre\DAVACL\AbstractPrincipalCollection; use Sabre\DAVACL\PrincipalBackend; @@ -33,12 +36,24 @@ class RootCollection extends AbstractPrincipalCollection { /** @var IRootFolder */ private $rootFolder; - public function __construct(PrincipalBackend\BackendInterface $principalBackend, - IRootFolder $rootFolder, - IConfig $config) { + /** @var IUserManager */ + private $userManager; + + /** @var IVersionManager */ + private $versionManager; + + public function __construct( + PrincipalBackend\BackendInterface $principalBackend, + IRootFolder $rootFolder, + IConfig $config, + IUserManager $userManager, + IVersionManager $versionManager + ) { parent::__construct($principalBackend, 'principals/users'); $this->rootFolder = $rootFolder; + $this->userManager = $userManager; + $this->versionManager = $versionManager; $this->disableListing = !$config->getSystemValue('debug', false); } @@ -54,12 +69,12 @@ class RootCollection extends AbstractPrincipalCollection { * @return INode */ public function getChildForPrincipal(array $principalInfo) { - list(,$name) = \Sabre\Uri\split($principalInfo['uri']); + list(, $name) = \Sabre\Uri\split($principalInfo['uri']); $user = \OC::$server->getUserSession()->getUser(); if (is_null($user) || $name !== $user->getUID()) { throw new \Sabre\DAV\Exception\Forbidden(); } - return new VersionHome($principalInfo, $this->rootFolder); + return new VersionHome($principalInfo, $this->rootFolder, $this->userManager, $this->versionManager); } public function getName() { diff --git a/apps/files_versions/lib/Sabre/VersionCollection.php b/apps/files_versions/lib/Sabre/VersionCollection.php index 481a5f491c3..9a3a6a365f0 100644 --- a/apps/files_versions/lib/Sabre/VersionCollection.php +++ b/apps/files_versions/lib/Sabre/VersionCollection.php @@ -21,11 +21,15 @@ declare(strict_types=1); * along with this program. If not, see <http://www.gnu.org/licenses/>. * */ + namespace OCA\Files_Versions\Sabre; use OCA\Files_Versions\Storage; +use OCA\Files_Versions\Versions\IVersion; +use OCA\Files_Versions\Versions\IVersionManager; use OCP\Files\File; use OCP\Files\Folder; +use OCP\IUser; use Sabre\DAV\Exception\Forbidden; use Sabre\DAV\Exception\NotFound; use Sabre\DAV\ICollection; @@ -37,13 +41,17 @@ class VersionCollection implements ICollection { /** @var File */ private $file; - /** @var string */ - private $userId; + /** @var IUser */ + private $user; + + /** @var IVersionManager */ + private $versionManager; - public function __construct(Folder $userFolder, File $file, string $userId) { + public function __construct(Folder $userFolder, File $file, IUser $user, IVersionManager $versionManager) { $this->userFolder = $userFolder; $this->file = $file; - $this->userId = $userId; + $this->user = $user; + $this->versionManager = $versionManager; } public function createFile($name, $data = null) { @@ -68,10 +76,10 @@ class VersionCollection implements ICollection { } public function getChildren(): array { - $versions = Storage::getVersions($this->userId, $this->userFolder->getRelativePath($this->file->getPath())); + $versions = $this->versionManager->getVersionsForFile($this->user, $this->file); - return array_map(function (array $data) { - return new VersionFile($data, $this->userFolder->getParent()); + return array_map(function (IVersion $version) { + return new VersionFile($version, $this->versionManager); }, $versions); } diff --git a/apps/files_versions/lib/Sabre/VersionFile.php b/apps/files_versions/lib/Sabre/VersionFile.php index 347058448fc..2d630008d2a 100644 --- a/apps/files_versions/lib/Sabre/VersionFile.php +++ b/apps/files_versions/lib/Sabre/VersionFile.php @@ -21,26 +21,26 @@ declare(strict_types=1); * along with this program. If not, see <http://www.gnu.org/licenses/>. * */ + namespace OCA\Files_Versions\Sabre; -use OCA\Files_Versions\Storage; -use OCP\Files\File; -use OCP\Files\Folder; +use OCA\Files_Versions\Versions\IVersion; +use OCA\Files_Versions\Versions\IVersionManager; use OCP\Files\NotFoundException; use Sabre\DAV\Exception\Forbidden; use Sabre\DAV\Exception\NotFound; use Sabre\DAV\IFile; class VersionFile implements IFile { - /** @var array */ - private $data; + /** @var IVersion */ + private $version; - /** @var Folder */ - private $userRoot; + /** @var IVersionManager */ + private $versionManager; - public function __construct(array $data, Folder $userRoot) { - $this->data = $data; - $this->userRoot = $userRoot; + public function __construct(IVersion $version, IVersionManager $versionManager) { + $this->version = $version; + $this->versionManager = $versionManager; } public function put($data) { @@ -49,27 +49,22 @@ class VersionFile implements IFile { public function get() { try { - /** @var Folder $versions */ - $versions = $this->userRoot->get('files_versions'); - /** @var File $version */ - $version = $versions->get($this->data['path'].'.v'.$this->data['version']); + return $this->versionManager->read($this->version); } catch (NotFoundException $e) { throw new NotFound(); } - - return $version->fopen('rb'); } public function getContentType(): string { - return $this->data['mimetype']; + return $this->version->getMimeType(); } public function getETag(): string { - return $this->data['version']; + return (string)$this->version->getRevisionId(); } public function getSize(): int { - return $this->data['size']; + return $this->version->getSize(); } public function delete() { @@ -77,7 +72,7 @@ class VersionFile implements IFile { } public function getName(): string { - return $this->data['version']; + return (string)$this->version->getRevisionId(); } public function setName($name) { @@ -85,10 +80,10 @@ class VersionFile implements IFile { } public function getLastModified(): int { - return (int)$this->data['version']; + return $this->version->getTimestamp(); } - public function rollBack(): bool { - return Storage::rollback($this->data['path'], $this->data['version']); + public function rollBack() { + $this->versionManager->rollback($this->version); } } diff --git a/apps/files_versions/lib/Sabre/VersionHome.php b/apps/files_versions/lib/Sabre/VersionHome.php index 7a99d2376d4..7be5974bbbe 100644 --- a/apps/files_versions/lib/Sabre/VersionHome.php +++ b/apps/files_versions/lib/Sabre/VersionHome.php @@ -20,9 +20,13 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. * */ + namespace OCA\Files_Versions\Sabre; +use OC\User\NoUserException; +use OCA\Files_Versions\Versions\IVersionManager; use OCP\Files\IRootFolder; +use OCP\IUserManager; use Sabre\DAV\Exception\Forbidden; use Sabre\DAV\ICollection; @@ -34,9 +38,25 @@ class VersionHome implements ICollection { /** @var IRootFolder */ private $rootFolder; - public function __construct(array $principalInfo, IRootFolder $rootFolder) { + /** @var IUserManager */ + private $userManager; + + /** @var IVersionManager */ + private $versionManager; + + public function __construct(array $principalInfo, IRootFolder $rootFolder, IUserManager $userManager, IVersionManager $versionManager) { $this->principalInfo = $principalInfo; $this->rootFolder = $rootFolder; + $this->userManager = $userManager; + $this->versionManager = $versionManager; + } + + private function getUser() { + list(, $name) = \Sabre\Uri\split($this->principalInfo['uri']); + $user = $this->userManager->get($name); + if (!$user) { + throw new NoUserException(); + } } public function delete() { @@ -44,8 +64,7 @@ class VersionHome implements ICollection { } public function getName(): string { - list(,$name) = \Sabre\Uri\split($this->principalInfo['uri']); - return $name; + return $this->getUser()->getUID(); } public function setName($name) { @@ -61,22 +80,22 @@ class VersionHome implements ICollection { } public function getChild($name) { - list(,$userId) = \Sabre\Uri\split($this->principalInfo['uri']); + $user = $this->getUser(); if ($name === 'versions') { - return new VersionRoot($userId, $this->rootFolder); + return new VersionRoot($user, $this->rootFolder, $this->versionManager); } if ($name === 'restore') { - return new RestoreFolder($userId); + return new RestoreFolder(); } } public function getChildren() { - list(,$userId) = \Sabre\Uri\split($this->principalInfo['uri']); + $user = $this->getUser(); return [ - new VersionRoot($userId, $this->rootFolder), - new RestoreFolder($userId), + new VersionRoot($user, $this->rootFolder, $this->versionManager), + new RestoreFolder(), ]; } diff --git a/apps/files_versions/lib/Sabre/VersionRoot.php b/apps/files_versions/lib/Sabre/VersionRoot.php index 743b1c6ef1b..1c689a4d87b 100644 --- a/apps/files_versions/lib/Sabre/VersionRoot.php +++ b/apps/files_versions/lib/Sabre/VersionRoot.php @@ -23,23 +23,29 @@ declare(strict_types=1); */ namespace OCA\Files_Versions\Sabre; +use OCA\Files_Versions\Versions\IVersionManager; use OCP\Files\File; use OCP\Files\IRootFolder; +use OCP\IUser; use Sabre\DAV\Exception\Forbidden; use Sabre\DAV\Exception\NotFound; use Sabre\DAV\ICollection; class VersionRoot implements ICollection { - /** @var string */ - private $userId; + /** @var IUser */ + private $user; /** @var IRootFolder */ private $rootFolder; - public function __construct(string $userId, IRootFolder $rootFolder) { - $this->userId = $userId; + /** @var IVersionManager */ + private $versionManager; + + public function __construct(IUser $user, IRootFolder $rootFolder, IVersionManager $versionManager) { + $this->user = $user; $this->rootFolder = $rootFolder; + $this->versionManager = $versionManager; } public function delete() { @@ -63,7 +69,7 @@ class VersionRoot implements ICollection { } public function getChild($name) { - $userFolder = $this->rootFolder->getUserFolder($this->userId); + $userFolder = $this->rootFolder->getUserFolder($this->user->getUID()); $fileId = (int)$name; $nodes = $userFolder->getById($fileId); @@ -78,7 +84,7 @@ class VersionRoot implements ICollection { throw new NotFound(); } - return new VersionCollection($userFolder, $node, $this->userId); + return new VersionCollection($userFolder, $node, $this->user, $this->versionManager); } public function getChildren(): array { diff --git a/apps/files_versions/lib/Storage.php b/apps/files_versions/lib/Storage.php index 401544cc5d7..e2e4888cbce 100644 --- a/apps/files_versions/lib/Storage.php +++ b/apps/files_versions/lib/Storage.php @@ -48,6 +48,7 @@ use OC\Files\View; use OCA\Files_Versions\AppInfo\Application; use OCA\Files_Versions\Command\Expire; use OCA\Files_Versions\Events\CreateVersionEvent; +use OCA\Files_Versions\Versions\IVersionManager; use OCP\Files\NotFoundException; use OCP\Lock\ILockingProvider; use OCP\User; @@ -178,10 +179,10 @@ class Storage { list($uid, $filename) = self::getUidAndFilename($filename); $files_view = new View('/'.$uid .'/files'); - $users_view = new View('/'.$uid); $eventDispatcher = \OC::$server->getEventDispatcher(); - $id = $files_view->getFileInfo($filename)->getId(); + $fileInfo = $files_view->getFileInfo($filename); + $id = $fileInfo->getId(); $nodes = \OC::$server->getRootFolder()->getById($id); foreach ($nodes as $node) { $event = new CreateVersionEvent($node); @@ -192,20 +193,16 @@ class Storage { } // no use making versions for empty files - if ($files_view->filesize($filename) === 0) { + if ($fileInfo->getSize() === 0) { return false; } - // create all parent folders - self::createMissingDirectories($filename, $users_view); - - self::scheduleExpire($uid, $filename); + /** @var IVersionManager $versionManager */ + $versionManager = \OC::$server->query(IVersionManager::class); + $userManager = \OC::$server->getUserManager(); + $user = $userManager->get($uid); - // store a new version of a file - $mtime = $users_view->filemtime('files/' . $filename); - $users_view->copy('files/' . $filename, 'files_versions/' . $filename . '.v' . $mtime); - // call getFileInfo to enforce a file cache entry for the new version - $users_view->getFileInfo('files_versions/' . $filename . '.v' . $mtime); + $versionManager->createVersion($user, $fileInfo); } @@ -695,7 +692,7 @@ class Storage { * @param string $uid owner of the file * @param string $fileName file/folder for which to schedule expiration */ - private static function scheduleExpire($uid, $fileName) { + public static function scheduleExpire($uid, $fileName) { // let the admin disable auto expire $expiration = self::getExpiration(); if ($expiration->isEnabled()) { @@ -833,7 +830,7 @@ class Storage { * "files" folder * @param View $view view on data/user/ */ - private static function createMissingDirectories($filename, $view) { + public static function createMissingDirectories($filename, $view) { $dirname = Filesystem::normalizePath(dirname($filename)); $dirParts = explode('/', $dirname); $dir = "/files_versions"; diff --git a/apps/files_versions/lib/Versions/BackendNotFoundException.php b/apps/files_versions/lib/Versions/BackendNotFoundException.php new file mode 100644 index 00000000000..09985a716b9 --- /dev/null +++ b/apps/files_versions/lib/Versions/BackendNotFoundException.php @@ -0,0 +1,26 @@ +<?php +/** + * @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\Files_Versions\Versions; + +class BackendNotFoundException extends \Exception { + +} diff --git a/apps/files_versions/lib/Versions/IVersion.php b/apps/files_versions/lib/Versions/IVersion.php new file mode 100644 index 00000000000..b6fc95814d8 --- /dev/null +++ b/apps/files_versions/lib/Versions/IVersion.php @@ -0,0 +1,99 @@ +<?php +declare(strict_types=1); +/** + * @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\Files_Versions\Versions; + +use OCP\Files\FileInfo; +use OCP\IUser; + +/** + * @since 15.0.0 + */ +interface IVersion { + /** + * @return IVersionBackend + * @since 15.0.0 + */ + public function getBackend(): IVersionBackend; + + /** + * Get the file info of the source file + * + * @return FileInfo + * @since 15.0.0 + */ + public function getSourceFile(): FileInfo; + + /** + * Get the id of the revision for the file + * + * @return int + * @since 15.0.0 + */ + public function getRevisionId(): int; + + /** + * Get the timestamp this version was created + * + * @return int + * @since 15.0.0 + */ + public function getTimestamp(): int; + + /** + * Get the size of this version + * + * @return int + * @since 15.0.0 + */ + public function getSize(): int; + + /** + * Get the name of the source file at the time of making this version + * + * @return string + * @since 15.0.0 + */ + public function getSourceFileName(): string; + + /** + * Get the mimetype of this version + * + * @return string + * @since 15.0.0 + */ + public function getMimeType(): string; + + /** + * Get the path of this version + * + * @return string + * @since 15.0.0 + */ + public function getVersionPath(): string; + + /** + * @return IUser + * @since 15.0.0 + */ + public function getUser(): IUser; +} diff --git a/apps/files_versions/lib/Versions/IVersionBackend.php b/apps/files_versions/lib/Versions/IVersionBackend.php new file mode 100644 index 00000000000..616d535f7fd --- /dev/null +++ b/apps/files_versions/lib/Versions/IVersionBackend.php @@ -0,0 +1,81 @@ +<?php declare(strict_types=1); +/** + * @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\Files_Versions\Versions; + +use OCP\Files\File; +use OCP\Files\FileInfo; +use OCP\Files\NotFoundException; +use OCP\Files\SimpleFS\ISimpleFile; +use OCP\IUser; + +/** + * @since 15.0.0 + */ +interface IVersionBackend { + /** + * Get all versions for a file + * + * @param IUser $user + * @param FileInfo $file + * @return IVersion[] + * @since 15.0.0 + */ + public function getVersionsForFile(IUser $user, FileInfo $file): array; + + /** + * Create a new version for a file + * + * @param IUser $user + * @param FileInfo $file + * @since 15.0.0 + */ + public function createVersion(IUser $user, FileInfo $file); + + /** + * Restore this version + * + * @param IVersion $version + * @since 15.0.0 + */ + public function rollback(IVersion $version); + + /** + * Open the file for reading + * + * @param IVersion $version + * @return resource + * @throws NotFoundException + * @since 15.0.0 + */ + public function read(IVersion $version); + + /** + * Get the preview for a specific version of a file + * + * @param IUser $user + * @param FileInfo $sourceFile + * @param int $revision + * @return ISimpleFile + * @since 15.0.0 + */ + public function getVersionFile(IUser $user, FileInfo $sourceFile, int $revision): File; +} diff --git a/apps/files_versions/lib/Versions/IVersionManager.php b/apps/files_versions/lib/Versions/IVersionManager.php new file mode 100644 index 00000000000..748b649b1a2 --- /dev/null +++ b/apps/files_versions/lib/Versions/IVersionManager.php @@ -0,0 +1,36 @@ +<?php declare(strict_types=1); +/** + * @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\Files_Versions\Versions; + +/** + * @since 15.0.0 + */ +interface IVersionManager extends IVersionBackend { + /** + * Register a new backend + * + * @param string $storageType + * @param IVersionBackend $backend + * @since 15.0.0 + */ + public function registerBackend(string $storageType, IVersionBackend $backend); +} diff --git a/apps/files_versions/lib/Versions/LegacyVersionsBackend.php b/apps/files_versions/lib/Versions/LegacyVersionsBackend.php new file mode 100644 index 00000000000..7293aca641e --- /dev/null +++ b/apps/files_versions/lib/Versions/LegacyVersionsBackend.php @@ -0,0 +1,105 @@ +<?php declare(strict_types=1); +/** + * @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\Files_Versions\Versions; + +use OC\Files\View; +use OCA\Files_Versions\Storage; +use OCP\Files\File; +use OCP\Files\FileInfo; +use OCP\Files\Folder; +use OCP\Files\IRootFolder; +use OCP\Files\NotFoundException; +use OCP\IUser; + +class LegacyVersionsBackend implements IVersionBackend { + /** @var IRootFolder */ + private $rootFolder; + + public function __construct(IRootFolder $rootFolder) { + $this->rootFolder = $rootFolder; + } + + public function getVersionsForFile(IUser $user, FileInfo $file): array { + $userFolder = $this->rootFolder->getUserFolder($user->getUID()); + $versions = Storage::getVersions($user->getUID(), $userFolder->getRelativePath($file->getPath())); + + return array_map(function (array $data) use ($file, $user) { + return new Version( + (int)$data['version'], + (int)$data['version'], + $data['name'], + (int)$data['size'], + $data['mimetype'], + $data['path'], + $file, + $this, + $user + ); + }, $versions); + } + + public function createVersion(IUser $user, FileInfo $file) { + $userFolder = $this->rootFolder->getUserFolder($user->getUID()); + $relativePath = $userFolder->getRelativePath($file->getPath()); + $userView = new View('/' . $user->getUID()); + // create all parent folders + Storage::createMissingDirectories($relativePath, $userView); + + Storage::scheduleExpire($user->getUID(), $relativePath); + + // store a new version of a file + $userView->copy('files/' . $relativePath, 'files_versions/' . $relativePath . '.v' . $file->getMtime()); + // ensure the file is scanned + $userView->getFileInfo('files_versions/' . $relativePath . '.v' . $file->getMtime()); + } + + public function rollback(IVersion $version) { + return Storage::rollback($version->getVersionPath(), $version->getRevisionId()); + } + + private function getVersionFolder(IUser $user): Folder { + $userRoot = $this->rootFolder->getUserFolder($user->getUID()) + ->getParent(); + try { + /** @var Folder $folder */ + $folder = $userRoot->get('files_versions'); + return $folder; + } catch (NotFoundException $e) { + return $userRoot->newFolder('files_versions'); + } + } + + public function read(IVersion $version) { + $versions = $this->getVersionFolder($version->getUser()); + /** @var File $file */ + $file = $versions->get($version->getVersionPath() . '.v' . $version->getRevisionId()); + return $file->fopen('r'); + } + + public function getVersionFile(IUser $user, FileInfo $sourceFile, int $revision): File { + $userFolder = $this->rootFolder->getUserFolder($user->getUID()); + $versionFolder = $this->getVersionFolder($user); + /** @var File $file */ + $file = $versionFolder->get($userFolder->getRelativePath($sourceFile->getPath()) . '.v' . $revision); + return $file; + } +} diff --git a/apps/files_versions/lib/Versions/Version.php b/apps/files_versions/lib/Versions/Version.php new file mode 100644 index 00000000000..5988234db61 --- /dev/null +++ b/apps/files_versions/lib/Versions/Version.php @@ -0,0 +1,113 @@ +<?php +declare(strict_types=1); +/** + * @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\Files_Versions\Versions; + +use OCP\Files\FileInfo; +use OCP\IUser; + +class Version implements IVersion { + /** @var int */ + private $timestamp; + + /** @var int */ + private $revisionId; + + /** @var string */ + private $name; + + /** @var int */ + private $size; + + /** @var string */ + private $mimetype; + + /** @var string */ + private $path; + + /** @var FileInfo */ + private $sourceFileInfo; + + /** @var IVersionBackend */ + private $backend; + + /** @var IUser */ + private $user; + + public function __construct( + int $timestamp, + int $revisionId, + string $name, + int $size, + string $mimetype, + string $path, + FileInfo $sourceFileInfo, + IVersionBackend $backend, + IUser $user + ) { + $this->timestamp = $timestamp; + $this->revisionId = $revisionId; + $this->name = $name; + $this->size = $size; + $this->mimetype = $mimetype; + $this->path = $path; + $this->sourceFileInfo = $sourceFileInfo; + $this->backend = $backend; + $this->user = $user; + } + + public function getBackend(): IVersionBackend { + return $this->backend; + } + + public function getSourceFile(): FileInfo { + return $this->sourceFileInfo; + } + + public function getRevisionId(): int { + return $this->revisionId; + } + + public function getTimestamp(): int { + return $this->timestamp; + } + + public function getSize(): int { + return $this->size; + } + + public function getSourceFileName(): string { + return $this->name; + } + + public function getMimeType(): string { + return $this->mimetype; + } + + public function getVersionPath(): string { + return $this->path; + } + + public function getUser(): IUser { + return $this->user; + } +} diff --git a/apps/files_versions/lib/Versions/VersionManager.php b/apps/files_versions/lib/Versions/VersionManager.php new file mode 100644 index 00000000000..757b6002710 --- /dev/null +++ b/apps/files_versions/lib/Versions/VersionManager.php @@ -0,0 +1,93 @@ +<?php declare(strict_types=1); +/** + * @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\Files_Versions\Versions; + +use OCP\Files\File; +use OCP\Files\FileInfo; +use OCP\Files\Storage\IStorage; +use OCP\IUser; + +class VersionManager implements IVersionManager { + /** @var IVersionBackend[] */ + private $backends = []; + + public function registerBackend(string $storageType, IVersionBackend $backend) { + $this->backends[$storageType] = $backend; + } + + /** + * @return IVersionBackend[] + */ + private function getBackends(): array { + return $this->backends; + } + + /** + * @param IStorage $storage + * @return IVersionBackend + * @throws BackendNotFoundException + */ + public function getBackendForStorage(IStorage $storage): IVersionBackend { + $fullType = get_class($storage); + $backends = $this->getBackends(); + $foundType = array_reduce(array_keys($backends), function ($type, $registeredType) use ($storage) { + if ( + $storage->instanceOfStorage($registeredType) && + ($type === '' || is_subclass_of($registeredType, $type)) + ) { + return $registeredType; + } else { + return $type; + } + }, ''); + if ($foundType === '') { + throw new BackendNotFoundException("Version backend for $fullType not found"); + } else { + return $backends[$foundType]; + } + } + + public function getVersionsForFile(IUser $user, FileInfo $file): array { + $backend = $this->getBackendForStorage($file->getStorage()); + return $backend->getVersionsForFile($user, $file); + } + + public function createVersion(IUser $user, FileInfo $file) { + $backend = $this->getBackendForStorage($file->getStorage()); + $backend->createVersion($user, $file); + } + + public function rollback(IVersion $version) { + $backend = $version->getBackend(); + return $backend->rollback($version); + } + + public function read(IVersion $version) { + $backend = $version->getBackend(); + return $backend->read($version); + } + + public function getVersionFile(IUser $user, FileInfo $sourceFile, int $revision): File { + $backend = $this->getBackendForStorage($sourceFile->getStorage()); + return $backend->getVersionFile($user, $sourceFile, $revision); + } +} diff --git a/apps/files_versions/tests/Controller/PreviewControllerTest.php b/apps/files_versions/tests/Controller/PreviewControllerTest.php index 384f43cf495..7c248b36349 100644 --- a/apps/files_versions/tests/Controller/PreviewControllerTest.php +++ b/apps/files_versions/tests/Controller/PreviewControllerTest.php @@ -20,9 +20,12 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. * */ + namespace OCA\Files_Versions\Tests\Controller; +use OC\User\User; use OCA\Files_Versions\Controller\PreviewController; +use OCA\Files_Versions\Versions\IVersionManager; use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\FileDisplayResponse; @@ -34,6 +37,8 @@ use OCP\Files\NotFoundException; use OCP\Files\SimpleFS\ISimpleFile; use OCP\IPreview; use OCP\IRequest; +use OCP\IUser; +use OCP\IUserSession; use Test\TestCase; class PreviewControllerTest extends TestCase { @@ -50,23 +55,39 @@ class PreviewControllerTest extends TestCase { /** @var IPreview|\PHPUnit_Framework_MockObject_MockObject */ private $previewManager; - /** @var PreviewController */ + /** @var PreviewController|\PHPUnit_Framework_MockObject_MockObject */ private $controller; + /** @var IUserSession|\PHPUnit_Framework_MockObject_MockObject */ + private $userSession; + + /** @var IVersionManager|\PHPUnit_Framework_MockObject_MockObject */ + private $versionManager; + public function setUp() { parent::setUp(); $this->rootFolder = $this->createMock(IRootFolder::class); $this->userId = 'user'; + $user = $this->createMock(IUser::class); + $user->expects($this->any()) + ->method('getUID') + ->willReturn($this->userId); $this->mimeTypeDetector = $this->createMock(IMimeTypeDetector::class); $this->previewManager = $this->createMock(IPreview::class); + $this->userSession = $this->createMock(IUserSession::class); + $this->userSession->expects($this->any()) + ->method('getUser') + ->willReturn($user); + $this->versionManager = $this->createMock(IVersionManager::class); $this->controller = new PreviewController( 'files_versions', $this->createMock(IRequest::class), $this->rootFolder, - $this->userId, + $this->userSession, $this->mimeTypeDetector, + $this->versionManager, $this->previewManager ); } @@ -102,24 +123,23 @@ class PreviewControllerTest extends TestCase { public function testValidPreview() { $userFolder = $this->createMock(Folder::class); $userRoot = $this->createMock(Folder::class); - $versions = $this->createMock(Folder::class); $this->rootFolder->method('getUserFolder') ->with($this->userId) ->willReturn($userFolder); $userFolder->method('getParent') ->willReturn($userRoot); - $userRoot->method('get') - ->with('files_versions') - ->willReturn($versions); - $this->mimeTypeDetector->method('detectPath') - ->with($this->equalTo('file')) - ->willReturn('myMime'); + $sourceFile = $this->createMock(File::class); + $userFolder->method('get') + ->with('file') + ->willReturn($sourceFile); $file = $this->createMock(File::class); - $versions->method('get') - ->with($this->equalTo('file.v42')) + $file->method('getMimetype') + ->willReturn('myMime'); + + $this->versionManager->method('getVersionFile') ->willReturn($file); $preview = $this->createMock(ISimpleFile::class); @@ -138,24 +158,23 @@ class PreviewControllerTest extends TestCase { public function testVersionNotFound() { $userFolder = $this->createMock(Folder::class); $userRoot = $this->createMock(Folder::class); - $versions = $this->createMock(Folder::class); $this->rootFolder->method('getUserFolder') ->with($this->userId) ->willReturn($userFolder); $userFolder->method('getParent') ->willReturn($userRoot); - $userRoot->method('get') - ->with('files_versions') - ->willReturn($versions); + + $sourceFile = $this->createMock(File::class); + $userFolder->method('get') + ->with('file') + ->willReturn($sourceFile); $this->mimeTypeDetector->method('detectPath') ->with($this->equalTo('file')) ->willReturn('myMime'); - $file = $this->createMock(File::class); - $versions->method('get') - ->with($this->equalTo('file.v42')) + $this->versionManager->method('getVersionFile') ->willThrowException(new NotFoundException()); $res = $this->controller->getPreview('file', 10, 10, '42'); diff --git a/core/css/global.scss b/core/css/global.scss index 9511d4324fa..06dc3688a2b 100644 --- a/core/css/global.scss +++ b/core/css/global.scss @@ -25,12 +25,12 @@ } .hidden { - display: none; + display: none !important; /* Hiding should take precedence */ } .hidden-visually { position: absolute; - left:-10000px; + left: -10000px; top: auto; width: 1px; height: 1px; @@ -47,4 +47,4 @@ .inlineblock { display: inline-block; -}
\ No newline at end of file +} diff --git a/core/css/header.scss b/core/css/header.scss index e790f719a1f..879734097ae 100644 --- a/core/css/header.scss +++ b/core/css/header.scss @@ -561,6 +561,7 @@ nav[role='navigation'] { span { opacity: .6; bottom: 2px; + z-index: -1; /* fix clickability issue - otherwise we need to move the span into the link */ } /* Prominent app title for current and hovered/focused app */ diff --git a/core/l10n/cs.js b/core/l10n/cs.js index a846eb1517a..37ab7ea022f 100644 --- a/core/l10n/cs.js +++ b/core/l10n/cs.js @@ -352,6 +352,7 @@ OC.L10N.register( "For help, see the <a target=\"_blank\" rel=\"noreferrer noopener\" href=\"%s\">documentation</a>." : "Pro pomoc, nahlédněte do <a target=\"_blank\" rel=\"noreferrer noopener\" href=\"%s\">dokumentace</a>.", "I know that if I continue doing the update via web UI has the risk, that the request runs into a timeout and could cause data loss, but I have a backup and know how to restore my instance in case of a failure." : "Beru na vědomí, že při aktualizaci skrze webové rozhraní hrozí nebezpečí vypršení požadavku, který může vyústit ve ztrátu dat. Mám pro takový případ zálohu a vím, jak ji v případě selhání obnovit.", "Upgrade via web on my own risk" : "Na vlastní nebezpečí aktualizovat skrze web", + "Maintenance mode" : "Režim údržby", "This %s instance is currently in maintenance mode, which may take a while." : "Tato instalace %s je právě ve stavu údržby a ta může chvíli trvat.", "Contact your system administrator if this message persists or appeared unexpectedly." : "Pokud se tato zpráva objevuje opakovaně nebo nečekaně, obraťte se správce systému.", "Updated \"%s\" to %s" : "Aktualizováno z „%s“ na %s", diff --git a/core/l10n/cs.json b/core/l10n/cs.json index 7d4184b5de1..edcc2917e54 100644 --- a/core/l10n/cs.json +++ b/core/l10n/cs.json @@ -350,6 +350,7 @@ "For help, see the <a target=\"_blank\" rel=\"noreferrer noopener\" href=\"%s\">documentation</a>." : "Pro pomoc, nahlédněte do <a target=\"_blank\" rel=\"noreferrer noopener\" href=\"%s\">dokumentace</a>.", "I know that if I continue doing the update via web UI has the risk, that the request runs into a timeout and could cause data loss, but I have a backup and know how to restore my instance in case of a failure." : "Beru na vědomí, že při aktualizaci skrze webové rozhraní hrozí nebezpečí vypršení požadavku, který může vyústit ve ztrátu dat. Mám pro takový případ zálohu a vím, jak ji v případě selhání obnovit.", "Upgrade via web on my own risk" : "Na vlastní nebezpečí aktualizovat skrze web", + "Maintenance mode" : "Režim údržby", "This %s instance is currently in maintenance mode, which may take a while." : "Tato instalace %s je právě ve stavu údržby a ta může chvíli trvat.", "Contact your system administrator if this message persists or appeared unexpectedly." : "Pokud se tato zpráva objevuje opakovaně nebo nečekaně, obraťte se správce systému.", "Updated \"%s\" to %s" : "Aktualizováno z „%s“ na %s", diff --git a/core/l10n/de.js b/core/l10n/de.js index 2a14d6115a9..1be2a21a5c6 100644 --- a/core/l10n/de.js +++ b/core/l10n/de.js @@ -167,6 +167,7 @@ OC.L10N.register( "Share to {name}" : "Mit {name} teilen", "Copy link" : "Link kopieren", "Link" : "Link", + "Hide download" : "Download verbergen", "Password protect" : "Passwortschutz", "Allow editing" : "Bearbeitung erlauben", "Email link to person" : "Link per E-Mail verschicken", @@ -212,6 +213,8 @@ OC.L10N.register( "An error occurred. Please try again" : "Es ist ein Fehler aufgetreten. Bitte versuche es noch einmal", "{sharee} (remote group)" : "{sharee} (externe Gruppe)", "{sharee} ({type}, {owner})" : "{sharee} ({type}, {owner})", + "Home" : "Start", + "Other" : "Andere", "Share" : "Teilen", "Name or email address..." : "Name oder E-Mail-Adresse…", "Name or federated cloud ID..." : "Name oder Federated-Cloud-ID…", diff --git a/core/l10n/de.json b/core/l10n/de.json index 569176bee77..f2063db8b24 100644 --- a/core/l10n/de.json +++ b/core/l10n/de.json @@ -165,6 +165,7 @@ "Share to {name}" : "Mit {name} teilen", "Copy link" : "Link kopieren", "Link" : "Link", + "Hide download" : "Download verbergen", "Password protect" : "Passwortschutz", "Allow editing" : "Bearbeitung erlauben", "Email link to person" : "Link per E-Mail verschicken", @@ -210,6 +211,8 @@ "An error occurred. Please try again" : "Es ist ein Fehler aufgetreten. Bitte versuche es noch einmal", "{sharee} (remote group)" : "{sharee} (externe Gruppe)", "{sharee} ({type}, {owner})" : "{sharee} ({type}, {owner})", + "Home" : "Start", + "Other" : "Andere", "Share" : "Teilen", "Name or email address..." : "Name oder E-Mail-Adresse…", "Name or federated cloud ID..." : "Name oder Federated-Cloud-ID…", diff --git a/core/l10n/de_DE.js b/core/l10n/de_DE.js index 99ad786719f..bdcea5e0bb5 100644 --- a/core/l10n/de_DE.js +++ b/core/l10n/de_DE.js @@ -167,6 +167,7 @@ OC.L10N.register( "Share to {name}" : "Mit {name} teilen", "Copy link" : "Link kopieren", "Link" : "Link", + "Hide download" : "Download verbergen", "Password protect" : "Passwortschutz", "Allow editing" : "Bearbeitung erlauben", "Email link to person" : "Link per E-Mail verschicken", @@ -212,6 +213,8 @@ OC.L10N.register( "An error occurred. Please try again" : "Es ist ein Fehler aufgetreten. Bitte versuchen Sie es noch einmal", "{sharee} (remote group)" : "{sharee} (Externe Gruppe)", "{sharee} ({type}, {owner})" : "{sharee} ({type}, {owner})", + "Home" : "Start", + "Other" : "Andere", "Share" : "Teilen", "Name or email address..." : "Name oder E-Mail-Adresse…", "Name or federated cloud ID..." : "Name oder Federated-Cloud-ID…", diff --git a/core/l10n/de_DE.json b/core/l10n/de_DE.json index 06dd9d5285e..7f844020d63 100644 --- a/core/l10n/de_DE.json +++ b/core/l10n/de_DE.json @@ -165,6 +165,7 @@ "Share to {name}" : "Mit {name} teilen", "Copy link" : "Link kopieren", "Link" : "Link", + "Hide download" : "Download verbergen", "Password protect" : "Passwortschutz", "Allow editing" : "Bearbeitung erlauben", "Email link to person" : "Link per E-Mail verschicken", @@ -210,6 +211,8 @@ "An error occurred. Please try again" : "Es ist ein Fehler aufgetreten. Bitte versuchen Sie es noch einmal", "{sharee} (remote group)" : "{sharee} (Externe Gruppe)", "{sharee} ({type}, {owner})" : "{sharee} ({type}, {owner})", + "Home" : "Start", + "Other" : "Andere", "Share" : "Teilen", "Name or email address..." : "Name oder E-Mail-Adresse…", "Name or federated cloud ID..." : "Name oder Federated-Cloud-ID…", diff --git a/core/l10n/it.js b/core/l10n/it.js index 21bfccaf8db..0c502a16885 100644 --- a/core/l10n/it.js +++ b/core/l10n/it.js @@ -167,6 +167,7 @@ OC.L10N.register( "Share to {name}" : "Condividi con {name}", "Copy link" : "Copia collegamento", "Link" : "Collegamento", + "Hide download" : "Nascondi scaricamento", "Password protect" : "Proteggi con password", "Allow editing" : "Consenti la modifica", "Email link to person" : "Invia collegamento via email", @@ -212,6 +213,8 @@ OC.L10N.register( "An error occurred. Please try again" : "Si è verificato un errore. Prova ancora", "{sharee} (remote group)" : "{sharee} (remote group)", "{sharee} ({type}, {owner})" : "{sharee} ({type}, {owner})", + "Home" : "Home", + "Other" : "Altro", "Share" : "Condividi", "Name or email address..." : "Nome o indirizzo email...", "Name or federated cloud ID..." : "Nome o ID di cloud federata...", diff --git a/core/l10n/it.json b/core/l10n/it.json index 0c4c03a3345..e93e3137276 100644 --- a/core/l10n/it.json +++ b/core/l10n/it.json @@ -165,6 +165,7 @@ "Share to {name}" : "Condividi con {name}", "Copy link" : "Copia collegamento", "Link" : "Collegamento", + "Hide download" : "Nascondi scaricamento", "Password protect" : "Proteggi con password", "Allow editing" : "Consenti la modifica", "Email link to person" : "Invia collegamento via email", @@ -210,6 +211,8 @@ "An error occurred. Please try again" : "Si è verificato un errore. Prova ancora", "{sharee} (remote group)" : "{sharee} (remote group)", "{sharee} ({type}, {owner})" : "{sharee} ({type}, {owner})", + "Home" : "Home", + "Other" : "Altro", "Share" : "Condividi", "Name or email address..." : "Nome o indirizzo email...", "Name or federated cloud ID..." : "Nome o ID di cloud federata...", diff --git a/core/l10n/pt_BR.js b/core/l10n/pt_BR.js index 93294b787c8..db7a373647c 100644 --- a/core/l10n/pt_BR.js +++ b/core/l10n/pt_BR.js @@ -167,6 +167,7 @@ OC.L10N.register( "Share to {name}" : "Compartilhar com {name}", "Copy link" : "Copiar link", "Link" : "Link", + "Hide download" : "Ocultar download", "Password protect" : "Proteger com senha", "Allow editing" : "Permitir edição", "Email link to person" : "Enviar link por e-mail", @@ -212,6 +213,8 @@ OC.L10N.register( "An error occurred. Please try again" : "Ocorreu um erro. Tente novamente", "{sharee} (remote group)" : "{sharee} (grupo remoto)", "{sharee} ({type}, {owner})" : "{sharee} ({type}, {owner})", + "Home" : "Home", + "Other" : "Outro", "Share" : "Compartilhar", "Name or email address..." : "Nome ou endereço de e-mail...", "Name or federated cloud ID..." : "Nome ou ID de cloud federada...", diff --git a/core/l10n/pt_BR.json b/core/l10n/pt_BR.json index 91467fe3081..99330296c3b 100644 --- a/core/l10n/pt_BR.json +++ b/core/l10n/pt_BR.json @@ -165,6 +165,7 @@ "Share to {name}" : "Compartilhar com {name}", "Copy link" : "Copiar link", "Link" : "Link", + "Hide download" : "Ocultar download", "Password protect" : "Proteger com senha", "Allow editing" : "Permitir edição", "Email link to person" : "Enviar link por e-mail", @@ -210,6 +211,8 @@ "An error occurred. Please try again" : "Ocorreu um erro. Tente novamente", "{sharee} (remote group)" : "{sharee} (grupo remoto)", "{sharee} ({type}, {owner})" : "{sharee} ({type}, {owner})", + "Home" : "Home", + "Other" : "Outro", "Share" : "Compartilhar", "Name or email address..." : "Nome ou endereço de e-mail...", "Name or federated cloud ID..." : "Nome ou ID de cloud federada...", diff --git a/core/l10n/tr.js b/core/l10n/tr.js index 4c4642705a2..080a6bc46f8 100644 --- a/core/l10n/tr.js +++ b/core/l10n/tr.js @@ -167,6 +167,7 @@ OC.L10N.register( "Share to {name}" : "{name} ile paylaş", "Copy link" : "Bağlantıyı kopyala", "Link" : "Bağlantı", + "Hide download" : "İndirmeyi gizle", "Password protect" : "Parola koruması", "Allow editing" : "Düzenlenebilsin", "Email link to person" : "Bağlantıyı e-posta ile gönder", @@ -212,6 +213,8 @@ OC.L10N.register( "An error occurred. Please try again" : "Bir sorun çıktı. Lütfen yeniden deneyin", "{sharee} (remote group)" : "{sharee} (uzak grup)", "{sharee} ({type}, {owner})" : "{sharee} ({type}, {owner})", + "Home" : "Giriş", + "Other" : "Diğer", "Share" : "Paylaş", "Name or email address..." : "Ad ya da e-posta adresi...", "Name or federated cloud ID..." : "Ad ya da birleşmiş bulut kodu...", diff --git a/core/l10n/tr.json b/core/l10n/tr.json index 786e961725e..fb66979b76f 100644 --- a/core/l10n/tr.json +++ b/core/l10n/tr.json @@ -165,6 +165,7 @@ "Share to {name}" : "{name} ile paylaş", "Copy link" : "Bağlantıyı kopyala", "Link" : "Bağlantı", + "Hide download" : "İndirmeyi gizle", "Password protect" : "Parola koruması", "Allow editing" : "Düzenlenebilsin", "Email link to person" : "Bağlantıyı e-posta ile gönder", @@ -210,6 +211,8 @@ "An error occurred. Please try again" : "Bir sorun çıktı. Lütfen yeniden deneyin", "{sharee} (remote group)" : "{sharee} (uzak grup)", "{sharee} ({type}, {owner})" : "{sharee} ({type}, {owner})", + "Home" : "Giriş", + "Other" : "Diğer", "Share" : "Paylaş", "Name or email address..." : "Ad ya da e-posta adresi...", "Name or federated cloud ID..." : "Ad ya da birleşmiş bulut kodu...", diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index c94837ed15c..8c6dc502487 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -231,6 +231,7 @@ return array( 'OCP\\Files\\Storage\\INotifyStorage' => $baseDir . '/lib/public/Files/Storage/INotifyStorage.php', 'OCP\\Files\\Storage\\IStorage' => $baseDir . '/lib/public/Files/Storage/IStorage.php', 'OCP\\Files\\Storage\\IStorageFactory' => $baseDir . '/lib/public/Files/Storage/IStorageFactory.php', + 'OCP\\Files\\Storage\\IWriteStreamStorage' => $baseDir . '/lib/public/Files/Storage/IWriteStreamStorage.php', 'OCP\\Files\\UnseekableException' => $baseDir . '/lib/public/Files/UnseekableException.php', 'OCP\\Files_FullTextSearch\\Model\\AFilesDocument' => $baseDir . '/lib/public/Files_FullTextSearch/Model/AFilesDocument.php', 'OCP\\FullTextSearch\\Exceptions\\FullTextSearchAppNotAvailableException' => $baseDir . '/lib/public/FullTextSearch/Exceptions/FullTextSearchAppNotAvailableException.php', @@ -821,6 +822,7 @@ return array( 'OC\\Files\\Storage\\Wrapper\\PermissionsMask' => $baseDir . '/lib/private/Files/Storage/Wrapper/PermissionsMask.php', 'OC\\Files\\Storage\\Wrapper\\Quota' => $baseDir . '/lib/private/Files/Storage/Wrapper/Quota.php', 'OC\\Files\\Storage\\Wrapper\\Wrapper' => $baseDir . '/lib/private/Files/Storage/Wrapper/Wrapper.php', + 'OC\\Files\\Stream\\CountReadStream' => $baseDir . '/lib/private/Files/Stream/CountReadStream.php', 'OC\\Files\\Stream\\Encryption' => $baseDir . '/lib/private/Files/Stream/Encryption.php', 'OC\\Files\\Stream\\Quota' => $baseDir . '/lib/private/Files/Stream/Quota.php', 'OC\\Files\\Type\\Detection' => $baseDir . '/lib/private/Files/Type/Detection.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 80aabae2389..2a46e99e020 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -261,6 +261,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OCP\\Files\\Storage\\INotifyStorage' => __DIR__ . '/../../..' . '/lib/public/Files/Storage/INotifyStorage.php', 'OCP\\Files\\Storage\\IStorage' => __DIR__ . '/../../..' . '/lib/public/Files/Storage/IStorage.php', 'OCP\\Files\\Storage\\IStorageFactory' => __DIR__ . '/../../..' . '/lib/public/Files/Storage/IStorageFactory.php', + 'OCP\\Files\\Storage\\IWriteStreamStorage' => __DIR__ . '/../../..' . '/lib/public/Files/Storage/IWriteStreamStorage.php', 'OCP\\Files\\UnseekableException' => __DIR__ . '/../../..' . '/lib/public/Files/UnseekableException.php', 'OCP\\Files_FullTextSearch\\Model\\AFilesDocument' => __DIR__ . '/../../..' . '/lib/public/Files_FullTextSearch/Model/AFilesDocument.php', 'OCP\\FullTextSearch\\Exceptions\\FullTextSearchAppNotAvailableException' => __DIR__ . '/../../..' . '/lib/public/FullTextSearch/Exceptions/FullTextSearchAppNotAvailableException.php', @@ -851,6 +852,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Files\\Storage\\Wrapper\\PermissionsMask' => __DIR__ . '/../../..' . '/lib/private/Files/Storage/Wrapper/PermissionsMask.php', 'OC\\Files\\Storage\\Wrapper\\Quota' => __DIR__ . '/../../..' . '/lib/private/Files/Storage/Wrapper/Quota.php', 'OC\\Files\\Storage\\Wrapper\\Wrapper' => __DIR__ . '/../../..' . '/lib/private/Files/Storage/Wrapper/Wrapper.php', + 'OC\\Files\\Stream\\CountReadStream' => __DIR__ . '/../../..' . '/lib/private/Files/Stream/CountReadStream.php', 'OC\\Files\\Stream\\Encryption' => __DIR__ . '/../../..' . '/lib/private/Files/Stream/Encryption.php', 'OC\\Files\\Stream\\Quota' => __DIR__ . '/../../..' . '/lib/private/Files/Stream/Quota.php', 'OC\\Files\\Type\\Detection' => __DIR__ . '/../../..' . '/lib/private/Files/Type/Detection.php', diff --git a/lib/private/Files/ObjectStore/ObjectStoreStorage.php b/lib/private/Files/ObjectStore/ObjectStoreStorage.php index 3ce919a4cbe..71acd27783c 100644 --- a/lib/private/Files/ObjectStore/ObjectStoreStorage.php +++ b/lib/private/Files/ObjectStore/ObjectStoreStorage.php @@ -28,6 +28,7 @@ namespace OC\Files\ObjectStore; use Icewind\Streams\CallbackWrapper; use Icewind\Streams\IteratorDirectory; use OC\Files\Cache\CacheEntry; +use OC\Files\Stream\CountReadStream; use OCP\Files\ObjectStore\IObjectStore; class ObjectStoreStorage extends \OC\Files\Storage\Common { @@ -382,25 +383,48 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common { } public function writeBack($tmpFile, $path) { + $size = filesize($tmpFile); + $this->writeStream($path, fopen($tmpFile, 'r'), $size); + } + + /** + * external changes are not supported, exclusive access to the object storage is assumed + * + * @param string $path + * @param int $time + * @return false + */ + public function hasUpdated($path, $time) { + return false; + } + + public function needsPartFile() { + return false; + } + + public function file_put_contents($path, $data) { + $stream = fopen('php://temp', 'r+'); + fwrite($stream, $data); + rewind($stream); + return $this->writeStream($path, $stream, strlen($data)) > 0; + } + + public function writeStream(string $path, $stream, int $size = null): int { $stat = $this->stat($path); if (empty($stat)) { // create new file - $stat = array( + $stat = [ 'permissions' => \OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_CREATE, - ); + ]; } // update stat with new data $mTime = time(); - $stat['size'] = filesize($tmpFile); + $stat['size'] = (int)$size; $stat['mtime'] = $mTime; $stat['storage_mtime'] = $mTime; - // run path based detection first, to use file extension because $tmpFile is only a random string $mimetypeDetector = \OC::$server->getMimeTypeDetector(); $mimetype = $mimetypeDetector->detectPath($path); - if ($mimetype === 'application/octet-stream') { - $mimetype = $mimetypeDetector->detect($tmpFile); - } $stat['mimetype'] = $mimetype; $stat['etag'] = $this->getETag($path); @@ -408,7 +432,20 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common { $fileId = $this->getCache()->put($path, $stat); try { //upload to object storage - $this->objectStore->writeObject($this->getURN($fileId), fopen($tmpFile, 'r')); + if ($size === null) { + $countStream = CountReadStream::wrap($stream, function ($writtenSize) use ($fileId, &$size) { + $this->getCache()->update($fileId, [ + 'size' => $writtenSize + ]); + $size = $writtenSize; + }); + $this->objectStore->writeObject($this->getURN($fileId), $countStream); + if (is_resource($countStream)) { + fclose($countStream); + } + } else { + $this->objectStore->writeObject($this->getURN($fileId), $stream); + } } catch (\Exception $ex) { $this->getCache()->remove($path); $this->logger->logException($ex, [ @@ -417,20 +454,7 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common { ]); throw $ex; // make this bubble up } - } - /** - * external changes are not supported, exclusive access to the object storage is assumed - * - * @param string $path - * @param int $time - * @return false - */ - public function hasUpdated($path, $time) { - return false; - } - - public function needsPartFile() { - return false; + return $size; } } diff --git a/lib/private/Files/Storage/Common.php b/lib/private/Files/Storage/Common.php index b6c82f3a1df..72fe3a79792 100644 --- a/lib/private/Files/Storage/Common.php +++ b/lib/private/Files/Storage/Common.php @@ -54,6 +54,7 @@ use OCP\Files\InvalidPathException; use OCP\Files\ReservedWordException; use OCP\Files\Storage\ILockingStorage; use OCP\Files\Storage\IStorage; +use OCP\Files\Storage\IWriteStreamStorage; use OCP\ILogger; use OCP\Lock\ILockingProvider; use OCP\Lock\LockedException; @@ -69,7 +70,7 @@ use OCP\Lock\LockedException; * Some \OC\Files\Storage\Common methods call functions which are first defined * in classes which extend it, e.g. $this->stat() . */ -abstract class Common implements Storage, ILockingStorage { +abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage { use LocalTempFileTrait; @@ -809,4 +810,23 @@ abstract class Common implements Storage, ILockingStorage { public function needsPartFile() { return true; } + + /** + * fallback implementation + * + * @param string $path + * @param resource $stream + * @param int $size + * @return int + */ + public function writeStream(string $path, $stream, int $size = null): int { + $target = $this->fopen($path, 'w'); + if (!$target) { + return 0; + } + list($count, $result) = \OC_Helper::streamCopy($stream, $target); + fclose($stream); + fclose($target); + return $count; + } } diff --git a/lib/private/Files/Storage/Local.php b/lib/private/Files/Storage/Local.php index 46b53dcf95c..5f7232e64b3 100644 --- a/lib/private/Files/Storage/Local.php +++ b/lib/private/Files/Storage/Local.php @@ -462,4 +462,8 @@ class Local extends \OC\Files\Storage\Common { return parent::moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath); } } + + public function writeStream(string $path, $stream, int $size = null): int { + return (int)file_put_contents($this->getSourcePath($path), $stream); + } } diff --git a/lib/private/Files/Storage/Wrapper/Encryption.php b/lib/private/Files/Storage/Wrapper/Encryption.php index 42653b2d4a6..e1c1225e0cc 100644 --- a/lib/private/Files/Storage/Wrapper/Encryption.php +++ b/lib/private/Files/Storage/Wrapper/Encryption.php @@ -1029,4 +1029,13 @@ class Encryption extends Wrapper { } + public function writeStream(string $path, $stream, int $size = null): int { + // always fall back to fopen + $target = $this->fopen($path, 'w'); + list($count, $result) = \OC_Helper::streamCopy($stream, $target); + fclose($stream); + fclose($target); + return $count; + } + } diff --git a/lib/private/Files/Storage/Wrapper/Jail.php b/lib/private/Files/Storage/Wrapper/Jail.php index 56514af6d80..f21b5716467 100644 --- a/lib/private/Files/Storage/Wrapper/Jail.php +++ b/lib/private/Files/Storage/Wrapper/Jail.php @@ -29,6 +29,7 @@ use OC\Files\Cache\Wrapper\CacheJail; use OC\Files\Cache\Wrapper\JailPropagator; use OC\Files\Filesystem; use OCP\Files\Storage\IStorage; +use OCP\Files\Storage\IWriteStreamStorage; use OCP\Lock\ILockingProvider; /** @@ -515,4 +516,18 @@ class Jail extends Wrapper { $this->propagator = new JailPropagator($storage, \OC::$server->getDatabaseConnection()); return $this->propagator; } + + public function writeStream(string $path, $stream, int $size = null): int { + $storage = $this->getWrapperStorage(); + if ($storage->instanceOfStorage(IWriteStreamStorage::class)) { + /** @var IWriteStreamStorage $storage */ + return $storage->writeStream($this->getUnjailedPath($path), $stream, $size); + } else { + $target = $this->fopen($path, 'w'); + list($count, $result) = \OC_Helper::streamCopy($stream, $target); + fclose($stream); + fclose($target); + return $count; + } + } } diff --git a/lib/private/Files/Storage/Wrapper/Wrapper.php b/lib/private/Files/Storage/Wrapper/Wrapper.php index 060c596ad65..f9c84b89fe5 100644 --- a/lib/private/Files/Storage/Wrapper/Wrapper.php +++ b/lib/private/Files/Storage/Wrapper/Wrapper.php @@ -32,9 +32,10 @@ namespace OC\Files\Storage\Wrapper; use OCP\Files\InvalidPathException; use OCP\Files\Storage\ILockingStorage; use OCP\Files\Storage\IStorage; +use OCP\Files\Storage\IWriteStreamStorage; use OCP\Lock\ILockingProvider; -class Wrapper implements \OC\Files\Storage\Storage, ILockingStorage { +class Wrapper implements \OC\Files\Storage\Storage, ILockingStorage, IWriteStreamStorage { /** * @var \OC\Files\Storage\Storage $storage */ @@ -621,4 +622,18 @@ class Wrapper implements \OC\Files\Storage\Storage, ILockingStorage { public function needsPartFile() { return $this->getWrapperStorage()->needsPartFile(); } + + public function writeStream(string $path, $stream, int $size = null): int { + $storage = $this->getWrapperStorage(); + if ($storage->instanceOfStorage(IWriteStreamStorage::class)) { + /** @var IWriteStreamStorage $storage */ + return $storage->writeStream($path, $stream, $size); + } else { + $target = $this->fopen($path, 'w'); + list($count, $result) = \OC_Helper::streamCopy($stream, $target); + fclose($stream); + fclose($target); + return $count; + } + } } diff --git a/lib/private/Files/Stream/CountReadStream.php b/lib/private/Files/Stream/CountReadStream.php new file mode 100644 index 00000000000..93cadf8f214 --- /dev/null +++ b/lib/private/Files/Stream/CountReadStream.php @@ -0,0 +1,65 @@ +<?php declare(strict_types=1); +/** + * @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Files\Stream; + +use Icewind\Streams\Wrapper; + +class CountReadStream extends Wrapper { + /** @var int */ + private $count; + + /** @var callback */ + private $callback; + + public static function wrap($source, $callback) { + $context = stream_context_create(array( + 'count' => array( + 'source' => $source, + 'callback' => $callback, + ) + )); + return Wrapper::wrapSource($source, $context, 'count', self::class); + } + + public function dir_opendir($path, $options) { + return false; + } + + public function stream_open($path, $mode, $options, &$opened_path) { + $context = $this->loadContext('count'); + + $this->callback = $context['callback']; + return true; + } + + public function stream_read($count) { + $result = parent::stream_read($count); + $this->count += strlen($result); + return $result; + } + + public function stream_close() { + $result = parent::stream_close(); + call_user_func($this->callback, $this->count); + return $result; + } +} diff --git a/lib/private/FullTextSearch/FullTextSearchManager.php b/lib/private/FullTextSearch/FullTextSearchManager.php index 6529ef2506a..9a9b077cf23 100644 --- a/lib/private/FullTextSearch/FullTextSearchManager.php +++ b/lib/private/FullTextSearch/FullTextSearchManager.php @@ -195,7 +195,7 @@ class FullTextSearchManager implements IFullTextSearchManager { * @throws FullTextSearchAppNotAvailableException */ public function updateIndexesStatus(string $providerId, array $documentIds, int $status, bool $reset = false) { - $this->getIndexService()->updateIndexStatus($providerId, $documentIds, $status, $reset); + $this->getIndexService()->updateIndexesStatus($providerId, $documentIds, $status, $reset); } diff --git a/lib/private/Preview/Generator.php b/lib/private/Preview/Generator.php index 86579e3480b..1f7decf2b79 100644 --- a/lib/private/Preview/Generator.php +++ b/lib/private/Preview/Generator.php @@ -298,19 +298,23 @@ class Generator { if ($height !== $maxHeight && $width !== $maxWidth) { /* - * Scale to the nearest power of two + * Scale to the nearest power of four */ - $pow2height = 2 ** ceil(log($height) / log(2)); - $pow2width = 2 ** ceil(log($width) / log(2)); + $pow4height = 4 ** ceil(log($height) / log(4)); + $pow4width = 4 ** ceil(log($width) / log(4)); - $ratioH = $height / $pow2height; - $ratioW = $width / $pow2width; + // Minimum size is 64 + $pow4height = max($pow4height, 64); + $pow4width = max($pow4width, 64); + + $ratioH = $height / $pow4height; + $ratioW = $width / $pow4width; if ($ratioH < $ratioW) { - $width = $pow2width; + $width = $pow4width; $height /= $ratioW; } else { - $height = $pow2height; + $height = $pow4height; $width /= $ratioH; } } diff --git a/lib/public/Files/Storage/IWriteStreamStorage.php b/lib/public/Files/Storage/IWriteStreamStorage.php new file mode 100644 index 00000000000..39a28dd037b --- /dev/null +++ b/lib/public/Files/Storage/IWriteStreamStorage.php @@ -0,0 +1,40 @@ +<?php declare(strict_types=1); +/** + * @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCP\Files\Storage; + +/** + * Interface that adds the ability to write a stream directly to file + * + * @since 15.0.0 + */ +interface IWriteStreamStorage extends IStorage { + /** + * Write the data from a stream to a file + * + * @param string $path + * @param resource $stream + * @param int|null $size the size of the stream if known in advance + * @return int the number of bytes written + * @since 15.0.0 + */ + public function writeStream(string $path, $stream, int $size = null): int; +} diff --git a/lib/public/FullTextSearch/Service/IIndexService.php b/lib/public/FullTextSearch/Service/IIndexService.php index 376c07a4490..c5d1b9b3bcf 100644 --- a/lib/public/FullTextSearch/Service/IIndexService.php +++ b/lib/public/FullTextSearch/Service/IIndexService.php @@ -65,11 +65,11 @@ interface IIndexService { * @since 15.0.0 * * @param string $providerId - * @param $documentId + * @param string $documentId * @param int $status * @param bool $reset */ - public function updateIndexStatus(string $providerId, $documentId, int $status, bool $reset = false); + public function updateIndexStatus(string $providerId, string $documentId, int $status, bool $reset = false); /** diff --git a/resources/app-info.xsd b/resources/app-info.xsd index fa06752c01d..287ed6b9913 100644 --- a/resources/app-info.xsd +++ b/resources/app-info.xsd @@ -63,6 +63,8 @@ maxOccurs="1" /> <xs:element name="trash" type="trash" minOccurs="0" maxOccurs="1" /> + <xs:element name="versions" type="versions" minOccurs="0" + maxOccurs="1" /> </xs:sequence> </xs:complexType> <xs:unique name="uniqueNameL10n"> @@ -670,6 +672,21 @@ </xs:simpleContent> </xs:complexType> + <xs:complexType name="versions"> + <xs:sequence> + <xs:element name="backend" type="versions-backend" minOccurs="1" + maxOccurs="unbounded"/> + </xs:sequence> + </xs:complexType> + + <xs:complexType name="versions-backend"> + <xs:simpleContent> + <xs:extension base="php-class"> + <xs:attribute name="for" type="php-class" use="required"/> + </xs:extension> + </xs:simpleContent> + </xs:complexType> + <xs:simpleType name="php-class"> <xs:restriction base="xs:string"> <xs:pattern diff --git a/settings/BackgroundJobs/VerifyUserData.php b/settings/BackgroundJobs/VerifyUserData.php index b4a60ec8405..56ebadff9c3 100644 --- a/settings/BackgroundJobs/VerifyUserData.php +++ b/settings/BackgroundJobs/VerifyUserData.php @@ -63,6 +63,9 @@ class VerifyUserData extends Job { /** @var string */ private $lookupServerUrl; + /** @var IConfig */ + private $config; + /** * VerifyUserData constructor. * @@ -85,6 +88,7 @@ class VerifyUserData extends Job { $lookupServerUrl = $config->getSystemValue('lookup_server', 'https://lookup.nextcloud.com'); $this->lookupServerUrl = rtrim($lookupServerUrl, '/'); + $this->config = $config; } /** diff --git a/tests/lib/Files/Storage/Storage.php b/tests/lib/Files/Storage/Storage.php index 04aafece2e3..a25a3f74f9e 100644 --- a/tests/lib/Files/Storage/Storage.php +++ b/tests/lib/Files/Storage/Storage.php @@ -23,6 +23,7 @@ namespace Test\Files\Storage; use OC\Files\Cache\Watcher; +use OCP\Files\Storage\IWriteStreamStorage; abstract class Storage extends \Test\TestCase { /** @@ -628,4 +629,20 @@ abstract class Storage extends \Test\TestCase { $this->instance->rename('bar.txt.part', 'bar.txt'); $this->assertEquals('bar', $this->instance->file_get_contents('bar.txt')); } + + public function testWriteStream() { + $textFile = \OC::$SERVERROOT . '/tests/data/lorem.txt'; + + if (!$this->instance->instanceOfStorage(IWriteStreamStorage::class)) { + $this->markTestSkipped('Not a WriteSteamStorage'); + } + /** @var IWriteStreamStorage $storage */ + $storage = $this->instance; + + $source = fopen($textFile, 'r'); + + $storage->writeStream('test.txt', $source); + $this->assertTrue($storage->file_exists('test.txt')); + $this->assertEquals(file_get_contents($textFile), $storage->file_get_contents('test.txt')); + } } diff --git a/tests/lib/Files/Stream/CountReadStreamTest.php b/tests/lib/Files/Stream/CountReadStreamTest.php new file mode 100644 index 00000000000..99291d1644f --- /dev/null +++ b/tests/lib/Files/Stream/CountReadStreamTest.php @@ -0,0 +1,49 @@ +<?php declare(strict_types=1); +/** + * @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace Test\Files\Stream; + +use OC\Files\Stream\CountReadStream; +use Test\TestCase; + +class CountReadStreamTest extends TestCase { + private function getStream($data) { + $handle = fopen('php://temp', 'w+'); + fwrite($handle, $data); + rewind($handle); + return $handle; + } + + public function testBasicCount() { + $source = $this->getStream('foo'); + $stream = CountReadStream::wrap($source, function ($size) { + $this->assertEquals(3, $size); + }); + stream_get_contents($stream); + } + + public function testLarger() { + $stream = CountReadStream::wrap(fopen(__DIR__ . '/../../../data/testimage.mp4', 'r'), function ($size) { + $this->assertEquals(383631, $size); + }); + stream_get_contents($stream); + } +} diff --git a/tests/lib/Preview/GeneratorTest.php b/tests/lib/Preview/GeneratorTest.php index 64786fa9fe8..565b526b659 100644 --- a/tests/lib/Preview/GeneratorTest.php +++ b/tests/lib/Preview/GeneratorTest.php @@ -104,7 +104,7 @@ class GeneratorTest extends \Test\TestCase { $previewFile = $this->createMock(ISimpleFile::class); $previewFolder->method('getFile') - ->with($this->equalTo('128-128.png')) + ->with($this->equalTo('256-256.png')) ->willReturn($previewFile); $this->eventDispatcher->expects($this->once()) @@ -212,7 +212,7 @@ class GeneratorTest extends \Test\TestCase { ->will($this->returnCallback(function($filename) use ($maxPreview, $previewFile) { if ($filename === '2048-2048-max.png') { return $maxPreview; - } else if ($filename === '128-128.png') { + } else if ($filename === '256-256.png') { return $previewFile; } $this->fail('Unexpected file'); @@ -223,7 +223,7 @@ class GeneratorTest extends \Test\TestCase { ->with($this->equalTo('my data')); $previewFolder->method('getFile') - ->with($this->equalTo('128-128.png')) + ->with($this->equalTo('256-256.png')) ->willThrowException(new NotFoundException()); $image = $this->createMock(IImage::class); @@ -233,7 +233,7 @@ class GeneratorTest extends \Test\TestCase { $image->expects($this->once()) ->method('resize') - ->with(128); + ->with(256); $image->method('data') ->willReturn('my resized data'); $image->method('valid')->willReturn(true); @@ -328,8 +328,8 @@ class GeneratorTest extends \Test\TestCase { return [ [1024, 2048, 512, 512, false, IPreview::MODE_FILL, 256, 512], [1024, 2048, 512, 512, false, IPreview::MODE_COVER, 512, 1024], - [1024, 2048, 512, 512, true, IPreview::MODE_FILL, 512, 512], - [1024, 2048, 512, 512, true, IPreview::MODE_COVER, 512, 512], + [1024, 2048, 512, 512, true, IPreview::MODE_FILL, 1024, 1024], + [1024, 2048, 512, 512, true, IPreview::MODE_COVER, 1024, 1024], [1024, 2048, -1, 512, false, IPreview::MODE_COVER, 256, 512], [1024, 2048, 512, -1, false, IPreview::MODE_FILL, 512, 1024], @@ -343,14 +343,20 @@ class GeneratorTest extends \Test\TestCase { [2048, 1024, 512, 512, false, IPreview::MODE_FILL, 512, 256], [2048, 1024, 512, 512, false, IPreview::MODE_COVER, 1024, 512], - [2048, 1024, 512, 512, true, IPreview::MODE_FILL, 512, 512], - [2048, 1024, 512, 512, true, IPreview::MODE_COVER, 512, 512], + [2048, 1024, 512, 512, true, IPreview::MODE_FILL, 1024, 1024], + [2048, 1024, 512, 512, true, IPreview::MODE_COVER, 1024, 1024], [2048, 1024, -1, 512, false, IPreview::MODE_FILL, 1024, 512], [2048, 1024, 512, -1, false, IPreview::MODE_COVER, 512, 256], [2048, 1024, 4096, 1024, true, IPreview::MODE_FILL, 2048, 512], [2048, 1024, 4096, 1024, true, IPreview::MODE_COVER, 2048, 512], + + //Test minimum size + [2048, 1024, 32, 32, false, IPreview::MODE_FILL, 64, 32], + [2048, 1024, 32, 32, false, IPreview::MODE_COVER, 64, 32], + [2048, 1024, 32, 32, true, IPreview::MODE_FILL, 64, 64], + [2048, 1024, 32, 32, true, IPreview::MODE_COVER, 64, 64], ]; } |