From d499f68fd72bb6839023aa91fb2028587a62c496 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Fri, 22 Jul 2016 14:34:59 +0200 Subject: [PATCH 1/8] Fix storage id with storage jail --- lib/private/Files/Storage/Wrapper/Jail.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/private/Files/Storage/Wrapper/Jail.php b/lib/private/Files/Storage/Wrapper/Jail.php index 80c75523748f0..5b9085799b932 100644 --- a/lib/private/Files/Storage/Wrapper/Jail.php +++ b/lib/private/Files/Storage/Wrapper/Jail.php @@ -377,7 +377,7 @@ public function hasUpdated($path, $time) { */ public function getCache($path = '', $storage = null) { if (!$storage) { - $storage = $this; + $storage = $this->storage; } $sourceCache = $this->storage->getCache($this->getSourcePath($path), $storage); return new CacheJail($sourceCache, $this->rootPath); From e321ecd592fef123772c6e61e175b6a34a3c5044 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Fri, 22 Jul 2016 14:37:37 +0200 Subject: [PATCH 2/8] add recent files to node api --- lib/private/Files/Node/Folder.php | 113 ++++++++++++++++++ lib/private/Files/Node/LazyRoot.php | 7 +- lib/public/Files/FileInfo.php | 5 + lib/public/Files/Folder.php | 7 ++ tests/lib/Files/Node/FolderTest.php | 171 +++++++++++++++++++++++++++- 5 files changed, 298 insertions(+), 5 deletions(-) diff --git a/lib/private/Files/Node/Folder.php b/lib/private/Files/Node/Folder.php index 8813b6c0775cf..7746757c2a565 100644 --- a/lib/private/Files/Node/Folder.php +++ b/lib/private/Files/Node/Folder.php @@ -26,7 +26,10 @@ namespace OC\Files\Node; +use OC\DB\QueryBuilder\Literal; +use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\Files\FileInfo; +use OCP\Files\Mount\IMountPoint; use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; @@ -358,4 +361,114 @@ public function getNonExistingName($name) { $uniqueName = \OC_Helper::buildNotExistingFileNameForView($this->getPath(), $name, $this->view); return trim($this->getRelativePath($uniqueName), '/'); } + + /** + * @param int $since + * @return \OCP\Files\Node[] + */ + public function getRecent($since) { + $mimetypeLoader = \OC::$server->getMimeTypeLoader(); + $mounts = $this->root->getMountsIn($this->path); + $mounts[] = $this->getMountPoint(); + + $mounts = array_filter($mounts, function (IMountPoint $mount) { + return $mount->getStorage(); + }); + $storageIds = array_map(function (IMountPoint $mount) { + return $mount->getStorage()->getCache()->getNumericStorageId(); + }, $mounts); + /** @var IMountPoint[] $mountMap */ + $mountMap = array_combine($storageIds, $mounts); + $folderMimetype = $mimetypeLoader->getId(FileInfo::MIMETYPE_FOLDER); + + //todo look into options of filtering path based on storage id (only search in files/ for home storage, filter by share root for shared, etc) + + $builder = \OC::$server->getDatabaseConnection()->getQueryBuilder(); + $query = $builder + ->select('f.*') + ->from('filecache', 'f') + ->where($builder->expr()->gt('f.storage_mtime', $builder->createNamedParameter($since, IQueryBuilder::PARAM_INT))) + ->andWhere($builder->expr()->in('f.storage', $builder->createNamedParameter($storageIds, IQueryBuilder::PARAM_INT_ARRAY))) + ->andWhere($builder->expr()->orX( + // handle non empty folders separate + $builder->expr()->neq('f.mimetype', $builder->createNamedParameter($folderMimetype, IQueryBuilder::PARAM_INT)), + $builder->expr()->eq('f.size', new Literal(0)) + )) + ->orderBy('f.mtime', 'DESC'); + + $result = $query->execute()->fetchAll(); + + // select folders with their mtime being the mtime of the oldest file in the folder + // this way we still show new folders but dont bumb the folder every time a file in it is changed + $builder = \OC::$server->getDatabaseConnection()->getQueryBuilder(); + $query = $builder + ->select('p.fileid', 'p.storage', 'p.mimetype', 'p.mimepart', 'p.size', 'p.path', 'p.etag', 'f1.storage_mtime', 'f1.mtime', 'p.permissions') + ->from('filecache', 'f1') + ->leftJoin('f1', 'filecache', 'f2', $builder->expr()->andX( // find the f1 with lowest mtime in the folder + $builder->expr()->eq('f1.parent', 'f2.parent'), + $builder->expr()->gt('f1.storage_mtime', 'f2.storage_mtime') + )) + ->innerJoin('f1', 'filecache', 'p', $builder->expr()->eq('f1.parent', 'p.fileid')) + ->where($builder->expr()->isNull('f2.fileid')) + ->andWhere($builder->expr()->gt('f1.storage_mtime', $builder->createNamedParameter($since, IQueryBuilder::PARAM_INT))) + ->andWhere($builder->expr()->in('f1.storage', $builder->createNamedParameter($storageIds, IQueryBuilder::PARAM_INT_ARRAY))) + ->andWhere($builder->expr()->neq('f1.size', new Literal(0))) + ->orderBy('f1.storage_mtime', 'DESC'); + + $folderResults = $query->execute()->fetchAll(); + + $found = []; // we sometimes get duplicate folders + $folderResults = array_filter($folderResults, function ($item) use (&$found) { + $isFound = isset($found[$item['fileid']]); + $found[$item['fileid']] = true; + return !$isFound; + }); + + $result = array_merge($folderResults, $result); + + usort($result, function ($a, $b) use ($folderMimetype) { + $diff = $b['mtime'] - $a['mtime']; + if ($diff === 0) { + return $a['mimetype'] === $folderMimetype ? -1 : 1; + } else { + return $diff; + } + }); + + $files = array_filter(array_map(function (array $entry) use ($mountMap, $mimetypeLoader) { + $mount = $mountMap[$entry['storage']]; + $entry['internalPath'] = $entry['path']; + $entry['mimetype'] = $mimetypeLoader->getMimetypeById($entry['mimetype']); + $entry['mimepart'] = $mimetypeLoader->getMimetypeById($entry['mimepart']); + $path = $this->getAbsolutePath($mount, $entry['path']); + if (is_null($path)) { + return null; + } + $fileInfo = new \OC\Files\FileInfo($path, $mount->getStorage(), $entry['internalPath'], $entry, $mount); + return $this->root->createNode($fileInfo->getPath(), $fileInfo); + }, $result)); + + return array_values(array_filter($files, function (Node $node) { + $relative = $this->getRelativePath($node->getPath()); + return $relative !== null && $relative !== '/'; + })); + } + + private function getAbsolutePath(IMountPoint $mount, $path) { + $storage = $mount->getStorage(); + if ($storage->instanceOfStorage('\OC\Files\Storage\Wrapper\Jail')) { + /** @var \OC\Files\Storage\Wrapper\Jail $storage */ + $jailRoot = $storage->getSourcePath(''); + $rootLength = strlen($jailRoot) + 1; + if ($path === $jailRoot) { + return $mount->getMountPoint(); + } else if (substr($path, 0, $rootLength) === $jailRoot . '/') { + return $mount->getMountPoint() . substr($path, $rootLength); + } else { + return null; + } + } else { + return $mount->getMountPoint() . $path; + } + } } diff --git a/lib/private/Files/Node/LazyRoot.php b/lib/private/Files/Node/LazyRoot.php index 1203fc4d16276..adc41153313a9 100644 --- a/lib/private/Files/Node/LazyRoot.php +++ b/lib/private/Files/Node/LazyRoot.php @@ -471,5 +471,10 @@ public function unlock($type) { return $this->__call(__FUNCTION__, func_get_args()); } - + /** + * @inheritDoc + */ + public function getRecent($type) { + return $this->__call(__FUNCTION__, func_get_args()); + } } diff --git a/lib/public/Files/FileInfo.php b/lib/public/Files/FileInfo.php index d61726c4217aa..04790d4155669 100644 --- a/lib/public/Files/FileInfo.php +++ b/lib/public/Files/FileInfo.php @@ -58,6 +58,11 @@ interface FileInfo { */ const SPACE_UNLIMITED = -3; + /** + * @since 9.1.0 + */ + const MIMETYPE_FOLDER = 'httpd/unix-directory'; + /** * Get the Etag of the file or folder * diff --git a/lib/public/Files/Folder.php b/lib/public/Files/Folder.php index 9a2a338b559fd..b6686732f42d2 100644 --- a/lib/public/Files/Folder.php +++ b/lib/public/Files/Folder.php @@ -175,4 +175,11 @@ public function isCreatable(); * @since 8.1.0 */ public function getNonExistingName($name); + + /** + * @param int $since + * @return \OCP\Files\Node[] + * @since 9.1.0 + */ + public function getRecent($since); } diff --git a/tests/lib/Files/Node/FolderTest.php b/tests/lib/Files/Node/FolderTest.php index 7ce9fff141948..cae6b4a80c041 100644 --- a/tests/lib/Files/Node/FolderTest.php +++ b/tests/lib/Files/Node/FolderTest.php @@ -12,6 +12,8 @@ use OC\Files\FileInfo; use OC\Files\Mount\MountPoint; use OC\Files\Node\Node; +use OC\Files\Storage\Temporary; +use OC\Files\Storage\Wrapper\Jail; use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; use OC\Files\View; @@ -760,9 +762,9 @@ public function testGetByIdMultipleStorages() { public function uniqueNameProvider() { return [ // input, existing, expected - ['foo', [] , 'foo'], - ['foo', ['foo'] , 'foo (2)'], - ['foo', ['foo', 'foo (2)'] , 'foo (3)'] + ['foo', [], 'foo'], + ['foo', ['foo'], 'foo (2)'], + ['foo', ['foo', 'foo (2)'], 'foo (3)'] ]; } @@ -782,7 +784,7 @@ public function testGetUniqueName($name, $existingFiles, $expected) { ->method('file_exists') ->will($this->returnCallback(function ($path) use ($existingFiles, $folderPath) { foreach ($existingFiles as $existing) { - if ($folderPath . '/' . $existing === $path){ + if ($folderPath . '/' . $existing === $path) { return true; } } @@ -792,4 +794,165 @@ public function testGetUniqueName($name, $existingFiles, $expected) { $node = new \OC\Files\Node\Folder($root, $view, $folderPath); $this->assertEquals($expected, $node->getNonExistingName($name)); } + + public function testRecent() { + $manager = $this->getMock('\OC\Files\Mount\Manager'); + $folderPath = '/bar/foo'; + /** + * @var \OC\Files\View | \PHPUnit_Framework_MockObject_MockObject $view + */ + $view = $this->getMock('\OC\Files\View'); + /** @var \PHPUnit_Framework_MockObject_MockObject|\OC\Files\Node\Root $root */ + $root = $this->getMock('\OC\Files\Node\Root', array('getUser', 'getMountsIn', 'getMount'), array($manager, $view, $this->user)); + /** @var \PHPUnit_Framework_MockObject_MockObject|\OC\Files\FileInfo $folderInfo */ + $folderInfo = $this->getMockBuilder('\OC\Files\FileInfo') + ->disableOriginalConstructor()->getMock(); + + $baseTime = 1000; + $storage = new Temporary(); + $mount = new MountPoint($storage, ''); + + $folderInfo->expects($this->any()) + ->method('getMountPoint') + ->will($this->returnValue($mount)); + + $cache = $storage->getCache(); + + $id1 = $cache->put('bar/foo/inside.txt', [ + 'storage_mtime' => $baseTime, + 'mtime' => $baseTime, + 'mimetype' => 'text/plain', + 'size' => 3 + ]); + $id2 = $cache->put('bar/foo/old.txt', [ + 'storage_mtime' => $baseTime - 100, + 'mtime' => $baseTime - 100, + 'mimetype' => 'text/plain', + 'size' => 3 + ]); + $cache->put('bar/asd/outside.txt', [ + 'storage_mtime' => $baseTime, + 'mtime' => $baseTime, + 'mimetype' => 'text/plain', + 'size' => 3 + ]); + $cache->put('bar/foo/toold.txt', [ + 'storage_mtime' => $baseTime - 600, + 'mtime' => $baseTime - 600, + 'mimetype' => 'text/plain', + 'size' => 3 + ]); + + $node = new \OC\Files\Node\Folder($root, $view, $folderPath, $folderInfo); + + + $nodes = $node->getRecent($baseTime - 500); + $ids = array_map(function (Node $node) { + return (int)$node->getId(); + }, $nodes); + $this->assertEquals([$id1, $id2], $ids); + } + + public function testRecentFolder() { + $manager = $this->getMock('\OC\Files\Mount\Manager'); + $folderPath = '/bar/foo'; + /** + * @var \OC\Files\View | \PHPUnit_Framework_MockObject_MockObject $view + */ + $view = $this->getMock('\OC\Files\View'); + /** @var \PHPUnit_Framework_MockObject_MockObject|\OC\Files\Node\Root $root */ + $root = $this->getMock('\OC\Files\Node\Root', array('getUser', 'getMountsIn', 'getMount'), array($manager, $view, $this->user)); + /** @var \PHPUnit_Framework_MockObject_MockObject|\OC\Files\FileInfo $folderInfo */ + $folderInfo = $this->getMockBuilder('\OC\Files\FileInfo') + ->disableOriginalConstructor()->getMock(); + + $baseTime = 1000; + $storage = new Temporary(); + $mount = new MountPoint($storage, ''); + + $folderInfo->expects($this->any()) + ->method('getMountPoint') + ->will($this->returnValue($mount)); + + $cache = $storage->getCache(); + + $id1 = $cache->put('bar/foo/folder', [ + 'storage_mtime' => $baseTime, + 'mtime' => $baseTime, + 'mimetype' => \OCP\Files\FileInfo::MIMETYPE_FOLDER, + 'size' => 3 + ]); + $id2 = $cache->put('bar/foo/folder/bar.txt', [ + 'storage_mtime' => $baseTime, + 'mtime' => $baseTime, + 'mimetype' => 'text/plain', + 'size' => 3, + 'parent' => $id1 + ]); + $id3 = $cache->put('bar/foo/folder/asd.txt', [ + 'storage_mtime' => $baseTime, + 'mtime' => $baseTime - 100, + 'mimetype' => 'text/plain', + 'size' => 3, + 'parent' => $id1 + ]); + + $node = new \OC\Files\Node\Folder($root, $view, $folderPath, $folderInfo); + + + $nodes = $node->getRecent($baseTime - 500); + $ids = array_map(function (Node $node) { + return (int)$node->getId(); + }, $nodes); + $this->assertEquals([$id2, $id1, $id3], $ids);// sort folders before files with the same mtime, folders get the lowest child mtime + } + + public function testRecentJail() { + $manager = $this->getMock('\OC\Files\Mount\Manager'); + $folderPath = '/bar/foo'; + /** + * @var \OC\Files\View | \PHPUnit_Framework_MockObject_MockObject $view + */ + $view = $this->getMock('\OC\Files\View'); + /** @var \PHPUnit_Framework_MockObject_MockObject|\OC\Files\Node\Root $root */ + $root = $this->getMock('\OC\Files\Node\Root', array('getUser', 'getMountsIn', 'getMount'), array($manager, $view, $this->user)); + /** @var \PHPUnit_Framework_MockObject_MockObject|\OC\Files\FileInfo $folderInfo */ + $folderInfo = $this->getMockBuilder('\OC\Files\FileInfo') + ->disableOriginalConstructor()->getMock(); + + $baseTime = 1000; + $storage = new Temporary(); + $jail = new Jail([ + 'storage' => $storage, + 'root' => 'folder' + ]); + $mount = new MountPoint($jail, '/bar/foo'); + + $folderInfo->expects($this->any()) + ->method('getMountPoint') + ->will($this->returnValue($mount)); + + $cache = $storage->getCache(); + + $id1 = $cache->put('folder/inside.txt', [ + 'storage_mtime' => $baseTime, + 'mtime' => $baseTime, + 'mimetype' => 'text/plain', + 'size' => 3 + ]); + $cache->put('outside.txt', [ + 'storage_mtime' => $baseTime - 100, + 'mtime' => $baseTime - 100, + 'mimetype' => 'text/plain', + 'size' => 3 + ]); + + $node = new \OC\Files\Node\Folder($root, $view, $folderPath, $folderInfo); + + $nodes = $node->getRecent($baseTime - 500); + $ids = array_map(function (Node $node) { + return (int)$node->getId(); + }, $nodes); + $this->assertEquals([$id1], $ids); + } } From b94ff97a77be3947e62373cd4c6b75d7dfe5e5cc Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Fri, 22 Jul 2016 14:48:51 +0200 Subject: [PATCH 3/8] add recent files api endpoint --- apps/files/appinfo/routes.php | 5 ++ apps/files/lib/AppInfo/Application.php | 3 +- apps/files/lib/Controller/ApiController.php | 66 +++++++++++++++------ 3 files changed, 56 insertions(+), 18 deletions(-) diff --git a/apps/files/appinfo/routes.php b/apps/files/appinfo/routes.php index 34bb10117733d..7b5ac63a1603f 100644 --- a/apps/files/appinfo/routes.php +++ b/apps/files/appinfo/routes.php @@ -50,6 +50,11 @@ 'verb' => 'GET', 'requirements' => array('tagName' => '.+'), ), + array( + 'name' => 'API#getRecentFiles', + 'url' => '/api/v1/recent/', + 'verb' => 'GET' + ), array( 'name' => 'API#updateFileSorting', 'url' => '/api/v1/sorting', diff --git a/apps/files/lib/AppInfo/Application.php b/apps/files/lib/AppInfo/Application.php index fac8e3a3a4bea..fc91e05ba7e66 100644 --- a/apps/files/lib/AppInfo/Application.php +++ b/apps/files/lib/AppInfo/Application.php @@ -47,7 +47,8 @@ public function __construct(array $urlParams=array()) { $c->query('TagService'), $server->getPreviewManager(), $server->getShareManager(), - $server->getConfig() + $server->getConfig(), + $server->getUserFolder() ); }); diff --git a/apps/files/lib/Controller/ApiController.php b/apps/files/lib/Controller/ApiController.php index 57eb43bbe9cff..8adc73a0a4511 100644 --- a/apps/files/lib/Controller/ApiController.php +++ b/apps/files/lib/Controller/ApiController.php @@ -31,6 +31,7 @@ use OCP\AppFramework\Http; use OCP\AppFramework\Controller; +use OCP\Files\Folder; use OCP\IConfig; use OCP\IRequest; use OCP\AppFramework\Http\DataResponse; @@ -39,7 +40,7 @@ use OCA\Files\Service\TagService; use OCP\IPreview; use OCP\Share\IManager; -use OCP\Files\Node; +use OC\Files\Node\Node; use OCP\IUserSession; /** @@ -58,12 +59,18 @@ class ApiController extends Controller { private $userSession; /** IConfig */ private $config; + /** @var Folder */ + private $userFolder; /** * @param string $appName * @param IRequest $request + * @param IUserSession $userSession * @param TagService $tagService * @param IPreview $previewManager + * @param IManager $shareManager + * @param IConfig $config + * @param Folder $userFolder */ public function __construct($appName, IRequest $request, @@ -71,13 +78,15 @@ public function __construct($appName, TagService $tagService, IPreview $previewManager, IManager $shareManager, - IConfig $config) { + IConfig $config, + Folder $userFolder) { parent::__construct($appName, $request); $this->userSession = $userSession; $this->tagService = $tagService; $this->previewManager = $previewManager; $this->shareManager = $shareManager; $this->config = $config; + $this->userFolder = $userFolder; } /** @@ -142,6 +151,28 @@ public function updateFileTags($path, $tags = null) { return new DataResponse($result); } + /** + * @param \OCP\Files\Node[] $nodes + * @return array + */ + private function formatNodes(array $nodes) { + return array_values(array_map(function (Node $node) { + /** @var \OC\Files\Node\Node $shareTypes */ + $shareTypes = $this->getShareTypes($node); + $file = \OCA\Files\Helper::formatFileInfo($node->getFileInfo()); + $parts = explode('/', dirname($node->getPath()), 4); + if (isset($parts[3])) { + $file['path'] = '/' . $parts[3]; + } else { + $file['path'] = '/'; + } + if (!empty($shareTypes)) { + $file['shareTypes'] = $shareTypes; + } + return $file; + }, $nodes)); + } + /** * Returns a list of all files tagged with the given tag. * @@ -151,27 +182,28 @@ public function updateFileTags($path, $tags = null) { * @return DataResponse */ public function getFilesByTag($tagName) { - $files = array(); $nodes = $this->tagService->getFilesByTag($tagName); - foreach ($nodes as &$node) { - $shareTypes = $this->getShareTypes($node); - $fileInfo = $node->getFileInfo(); - $file = \OCA\Files\Helper::formatFileInfo($fileInfo); - $parts = explode('/', dirname($fileInfo->getPath()), 4); - if(isset($parts[3])) { - $file['path'] = '/' . $parts[3]; - } else { - $file['path'] = '/'; - } + $files = $this->formatNodes($nodes); + foreach ($files as &$file) { $file['tags'] = [$tagName]; - if (!empty($shareTypes)) { - $file['shareTypes'] = $shareTypes; - } - $files[] = $file; } return new DataResponse(['files' => $files]); } + /** + * Returns a list of recently modifed files. + * + * @NoAdminRequired + * + * @return DataResponse + */ + public function getRecentFiles() { + $since = time() - (60 * 60 * 24 * 7);//1 week + $nodes = $this->userFolder->getRecent($since); + $files = $this->formatNodes($nodes); + return new DataResponse(['files' => $files]); + } + /** * Return a list of share types for outgoing shares * From 2e3114cc28b44468d449838c3b75ed7e4e5decea Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Fri, 22 Jul 2016 15:20:51 +0200 Subject: [PATCH 4/8] Add recent file listing --- apps/files/appinfo/app.php | 14 ++- apps/files/js/recentfilelist.js | 105 +++++++++++++++++ apps/files/js/recentplugin.js | 117 +++++++++++++++++++ apps/files/lib/Controller/ViewController.php | 2 + apps/files/recentlist.php | 7 ++ apps/files/templates/recentlist.php | 42 +++++++ 6 files changed, 285 insertions(+), 2 deletions(-) create mode 100644 apps/files/js/recentfilelist.js create mode 100644 apps/files/js/recentplugin.js create mode 100644 apps/files/recentlist.php create mode 100644 apps/files/templates/recentlist.php diff --git a/apps/files/appinfo/app.php b/apps/files/appinfo/app.php index cc86e9bf27096..850c335c27d06 100644 --- a/apps/files/appinfo/app.php +++ b/apps/files/appinfo/app.php @@ -28,6 +28,7 @@ */ \OCP\App::registerAdmin('files', 'admin'); +$l = \OC::$server->getL10N('files'); \OC::$server->getNavigationManager()->add(function () { $urlGenerator = \OC::$server->getURLGenerator(); @@ -49,8 +50,7 @@ $templateManager->registerTemplate('application/vnd.oasis.opendocument.text', 'core/templates/filetemplates/template.odt'); $templateManager->registerTemplate('application/vnd.oasis.opendocument.spreadsheet', 'core/templates/filetemplates/template.ods'); -\OCA\Files\App::getNavigationManager()->add(function () { - $l = \OC::$server->getL10N('files'); +\OCA\Files\App::getNavigationManager()->add(function () use ($l) { return [ 'id' => 'files', 'appname' => 'files', @@ -60,6 +60,16 @@ ]; }); +\OCA\Files\App::getNavigationManager()->add(function () use ($l) { + return [ + 'id' => 'recent', + 'appname' => 'files', + 'script' => 'recentlist.php', + 'order' => 2, + 'name' => $l->t('Recent'), + ]; +}); + \OC::$server->getActivityManager()->registerExtension(function() { return new \OCA\Files\Activity( \OC::$server->query('L10NFactory'), diff --git a/apps/files/js/recentfilelist.js b/apps/files/js/recentfilelist.js new file mode 100644 index 0000000000000..e63a71f8549fa --- /dev/null +++ b/apps/files/js/recentfilelist.js @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2014 Vincent Petry + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +// HACK: this piece needs to be loaded AFTER the files app (for unit tests) +$(document).ready(function () { + (function (OCA) { + /** + * @class OCA.Files.RecentFileList + * @augments OCA.Files.RecentFileList + * + * @classdesc Recent file list. + * Displays the list of recently modified files + * + * @param $el container element with existing markup for the #controls + * and a table + * @param [options] map of options, see other parameters + */ + var RecentFileList = function ($el, options) { + options.sorting = { + mode: 'mtime', + direction: 'desc' + }; + this.initialize($el, options); + }; + RecentFileList.prototype = _.extend({}, OCA.Files.FileList.prototype, + /** @lends OCA.Files.RecentFileList.prototype */ { + id: 'recent', + appName: t('files', 'Recent'), + + _clientSideSort: true, + _allowSelection: false, + + /** + * @private + */ + initialize: function () { + OCA.Files.FileList.prototype.initialize.apply(this, arguments); + if (this.initialized) { + return; + } + OC.Plugins.attach('OCA.Files.RecentFileList', this); + }, + + updateEmptyContent: function () { + var dir = this.getCurrentDirectory(); + if (dir === '/') { + // root has special permissions + this.$el.find('#emptycontent').toggleClass('hidden', !this.isEmpty); + this.$el.find('#filestable thead th').toggleClass('hidden', this.isEmpty); + } + else { + OCA.Files.FileList.prototype.updateEmptyContent.apply(this, arguments); + } + }, + + getDirectoryPermissions: function () { + return OC.PERMISSION_READ | OC.PERMISSION_DELETE; + }, + + updateStorageStatistics: function () { + // no op because it doesn't have + // storage info like free space / used space + }, + + reload: function () { + this.showMask(); + if (this._reloadCall) { + this._reloadCall.abort(); + } + + // there is only root + this._setCurrentDir('/', false); + + this._reloadCall = $.ajax({ + url: OC.generateUrl('/apps/files/api/v1/recent'), + type: 'GET', + dataType: 'json' + }); + var callBack = this.reloadCallback.bind(this); + return this._reloadCall.then(callBack, callBack); + }, + + reloadCallback: function (result) { + delete this._reloadCall; + this.hideMask(); + + if (result.files) { + this.setFiles(result.files.sort(this._sortComparator)); + return true; + } + return false; + } + }); + + OCA.Files.RecentFileList = RecentFileList; + })(OCA); +}); + diff --git a/apps/files/js/recentplugin.js b/apps/files/js/recentplugin.js new file mode 100644 index 0000000000000..fcd427b18a2c2 --- /dev/null +++ b/apps/files/js/recentplugin.js @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2014 Vincent Petry + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +(function (OCA) { + /** + * @namespace OCA.Files.RecentPlugin + * + * Registers the recent file list from the files app sidebar. + */ + OCA.Files.RecentPlugin = { + name: 'Recent', + + /** + * @type OCA.Files.RecentFileList + */ + recentFileList: null, + + attach: function () { + var self = this; + $('#app-content-recent').on('show.plugin-recent', function (e) { + self.showFileList($(e.target)); + }); + $('#app-content-recent').on('hide.plugin-recent', function () { + self.hideFileList(); + }); + }, + + detach: function () { + if (this.recentFileList) { + this.recentFileList.destroy(); + OCA.Files.fileActions.off('setDefault.plugin-recent', this._onActionsUpdated); + OCA.Files.fileActions.off('registerAction.plugin-recent', this._onActionsUpdated); + $('#app-content-recent').off('.plugin-recent'); + this.recentFileList = null; + } + }, + + showFileList: function ($el) { + if (!this.recentFileList) { + this.recentFileList = this._createRecentFileList($el); + } + return this.recentFileList; + }, + + hideFileList: function () { + if (this.recentFileList) { + this.recentFileList.$fileList.empty(); + } + }, + + /** + * Creates the recent file list. + * + * @param $el container for the file list + * @return {OCA.Files.RecentFileList} file list + */ + _createRecentFileList: function ($el) { + var fileActions = this._createFileActions(); + // register recent list for sidebar section + return new OCA.Files.RecentFileList( + $el, { + fileActions: fileActions, + scrollContainer: $('#app-content') + } + ); + }, + + _createFileActions: function () { + // inherit file actions from the files app + var fileActions = new OCA.Files.FileActions(); + // note: not merging the legacy actions because legacy apps are not + // compatible with the sharing overview and need to be adapted first + fileActions.registerDefaultActions(); + fileActions.merge(OCA.Files.fileActions); + + if (!this._globalActionsInitialized) { + // in case actions are registered later + this._onActionsUpdated = _.bind(this._onActionsUpdated, this); + OCA.Files.fileActions.on('setDefault.plugin-recent', this._onActionsUpdated); + OCA.Files.fileActions.on('registerAction.plugin-recent', this._onActionsUpdated); + this._globalActionsInitialized = true; + } + + // when the user clicks on a folder, redirect to the corresponding + // folder in the files app instead of opening it directly + fileActions.register('dir', 'Open', OC.PERMISSION_READ, '', function (filename, context) { + OCA.Files.App.setActiveView('files', {silent: true}); + var path = OC.joinPaths(context.$file.attr('data-path'), filename); + OCA.Files.App.fileList.changeDirectory(path, true, true); + }); + fileActions.setDefault('dir', 'Open'); + return fileActions; + }, + + _onActionsUpdated: function (ev) { + if (ev.action) { + this.recentFileList.fileActions.registerAction(ev.action); + } else if (ev.defaultAction) { + this.recentFileList.fileActions.setDefault( + ev.defaultAction.mime, + ev.defaultAction.name + ); + } + } + }; + +})(OCA); + +OC.Plugins.register('OCA.Files.App', OCA.Files.RecentPlugin); + diff --git a/apps/files/lib/Controller/ViewController.php b/apps/files/lib/Controller/ViewController.php index 5e342e6589bc4..9dbe06ff7898f 100644 --- a/apps/files/lib/Controller/ViewController.php +++ b/apps/files/lib/Controller/ViewController.php @@ -173,9 +173,11 @@ public function index($dir = '', $view = '', $fileid = null) { \OCP\Util::addscript('files', 'search'); \OCP\Util::addScript('files', 'favoritesfilelist'); + \OCP\Util::addScript('files', 'recentfilelist'); \OCP\Util::addScript('files', 'tagsplugin'); \OCP\Util::addScript('files', 'gotoplugin'); \OCP\Util::addScript('files', 'favoritesplugin'); + \OCP\Util::addScript('files', 'recentplugin'); \OCP\Util::addScript('files', 'detailfileinfoview'); \OCP\Util::addScript('files', 'sidebarpreviewmanager'); diff --git a/apps/files/recentlist.php b/apps/files/recentlist.php new file mode 100644 index 0000000000000..1976be4894ad5 --- /dev/null +++ b/apps/files/recentlist.php @@ -0,0 +1,7 @@ +printPage(); diff --git a/apps/files/templates/recentlist.php b/apps/files/templates/recentlist.php new file mode 100644 index 0000000000000..1667eb4cc8d9a --- /dev/null +++ b/apps/files/templates/recentlist.php @@ -0,0 +1,42 @@ + +
+ + + + + + + + + + + + + + + + + + + +
From a4ba3eadd0d1e04dc4ef5d11dae59c7dc98c4466 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Mon, 11 Jul 2016 12:58:43 +0200 Subject: [PATCH 5/8] fix test --- .../tests/Controller/ApiControllerTest.php | 8 +++++- .../tests/Controller/ViewControllerTest.php | 25 ++++++++++++++----- tests/lib/Files/Node/FolderTest.php | 5 +++- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/apps/files/tests/Controller/ApiControllerTest.php b/apps/files/tests/Controller/ApiControllerTest.php index 1d39c88021bea..348150e0e0835 100644 --- a/apps/files/tests/Controller/ApiControllerTest.php +++ b/apps/files/tests/Controller/ApiControllerTest.php @@ -59,6 +59,8 @@ class ApiControllerTest extends TestCase { private $shareManager; /** @var \OCP\IConfig */ private $config; + /** @var \OC\Files\Node\Folder */ + private $userFolder; public function setUp() { $this->request = $this->getMockBuilder('\OCP\IRequest') @@ -82,6 +84,9 @@ public function setUp() { ->disableOriginalConstructor() ->getMock(); $this->config = $this->getMock('\OCP\IConfig'); + $this->userFolder = $this->getMockBuilder('\OC\Files\Node\Folder') + ->disableOriginalConstructor() + ->getMock(); $this->apiController = new ApiController( $this->appName, @@ -90,7 +95,8 @@ public function setUp() { $this->tagService, $this->preview, $this->shareManager, - $this->config + $this->config, + $this->userFolder ); } diff --git a/apps/files/tests/Controller/ViewControllerTest.php b/apps/files/tests/Controller/ViewControllerTest.php index ceb48a2241f3f..25b5b6e04e007 100644 --- a/apps/files/tests/Controller/ViewControllerTest.php +++ b/apps/files/tests/Controller/ViewControllerTest.php @@ -191,7 +191,16 @@ public function testIndexWithRegularBrowser() { 'appname' => 'files', 'script' => 'list.php', 'order' => 0, - 'name' => new \OC_L10N_String(new \OC_L10N('files'), 'All files', []), + 'name' => (string)new \OC_L10N_String(new \OC_L10N('files'), 'All files', []), + 'active' => false, + 'icon' => '', + ], + [ + 'id' => 'recent', + 'appname' => 'files', + 'script' => 'list.php', + 'order' => 2, + 'name' => (string)new \OC_L10N_String(new \OC_L10N('files'), 'Recent', []), 'active' => false, 'icon' => '', ], @@ -209,7 +218,7 @@ public function testIndexWithRegularBrowser() { 'appname' => 'files_sharing', 'script' => 'list.php', 'order' => 10, - 'name' => new \OC_L10N_String(new \OC_L10N('files_sharing'), 'Shared with you', []), + 'name' => (string)new \OC_L10N_String(new \OC_L10N('files_sharing'), 'Shared with you', []), 'active' => false, 'icon' => '', ], @@ -218,7 +227,7 @@ public function testIndexWithRegularBrowser() { 'appname' => 'files_sharing', 'script' => 'list.php', 'order' => 15, - 'name' => new \OC_L10N_String(new \OC_L10N('files_sharing'), 'Shared with others', []), + 'name' => (string)new \OC_L10N_String(new \OC_L10N('files_sharing'), 'Shared with others', []), 'active' => false, 'icon' => '', ], @@ -227,7 +236,7 @@ public function testIndexWithRegularBrowser() { 'appname' => 'files_sharing', 'script' => 'list.php', 'order' => 20, - 'name' => new \OC_L10N_String(new \OC_L10N('files_sharing'), 'Shared by link', []), + 'name' => (string)new \OC_L10N_String(new \OC_L10N('files_sharing'), 'Shared by link', []), 'active' => false, 'icon' => '', ], @@ -236,7 +245,7 @@ public function testIndexWithRegularBrowser() { 'appname' => 'systemtags', 'script' => 'list.php', 'order' => 25, - 'name' => new \OC_L10N_String(new \OC_L10N('systemtags'), 'Tags', []), + 'name' => (string)new \OC_L10N_String(new \OC_L10N('systemtags'), 'Tags', []), 'active' => false, 'icon' => '', ], @@ -245,7 +254,7 @@ public function testIndexWithRegularBrowser() { 'appname' => 'files_trashbin', 'script' => 'list.php', 'order' => 50, - 'name' => new \OC_L10N_String(new \OC_L10N('files_trashbin'), 'Deleted files', []), + 'name' => (string)new \OC_L10N_String(new \OC_L10N('files_trashbin'), 'Deleted files', []), 'active' => false, 'icon' => '', ], @@ -272,6 +281,10 @@ public function testIndexWithRegularBrowser() { 'id' => 'files', 'content' => null, ], + [ + 'id' => 'recent', + 'content' => null, + ], [ 'id' => 'favorites', 'content' => null, diff --git a/tests/lib/Files/Node/FolderTest.php b/tests/lib/Files/Node/FolderTest.php index cae6b4a80c041..eef78e7d428c1 100644 --- a/tests/lib/Files/Node/FolderTest.php +++ b/tests/lib/Files/Node/FolderTest.php @@ -890,7 +890,7 @@ public function testRecentFolder() { 'parent' => $id1 ]); $id3 = $cache->put('bar/foo/folder/asd.txt', [ - 'storage_mtime' => $baseTime, + 'storage_mtime' => $baseTime - 100, 'mtime' => $baseTime - 100, 'mimetype' => 'text/plain', 'size' => 3, @@ -905,6 +905,9 @@ public function testRecentFolder() { return (int)$node->getId(); }, $nodes); $this->assertEquals([$id2, $id1, $id3], $ids);// sort folders before files with the same mtime, folders get the lowest child mtime + $this->assertEquals($baseTime, $nodes[0]->getMTime()); + $this->assertEquals($baseTime - 100, $nodes[1]->getMTime()); + $this->assertEquals($baseTime - 100, $nodes[2]->getMTime()); } public function testRecentJail() { From 81e103074ea6ce9e7035734bc527ab582dbca89f Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Fri, 22 Jul 2016 13:58:53 +0200 Subject: [PATCH 6/8] use limit instead of since when listing recent files --- apps/files/lib/Controller/ApiController.php | 3 +- lib/private/Files/Node/Folder.php | 49 +++------------------ lib/private/Files/Node/LazyRoot.php | 2 +- lib/public/Files/Folder.php | 5 ++- tests/lib/Files/Node/FolderTest.php | 13 +++--- 5 files changed, 18 insertions(+), 54 deletions(-) diff --git a/apps/files/lib/Controller/ApiController.php b/apps/files/lib/Controller/ApiController.php index 8adc73a0a4511..7ce83bfca15e8 100644 --- a/apps/files/lib/Controller/ApiController.php +++ b/apps/files/lib/Controller/ApiController.php @@ -198,8 +198,7 @@ public function getFilesByTag($tagName) { * @return DataResponse */ public function getRecentFiles() { - $since = time() - (60 * 60 * 24 * 7);//1 week - $nodes = $this->userFolder->getRecent($since); + $nodes = $this->userFolder->getRecent(100); $files = $this->formatNodes($nodes); return new DataResponse(['files' => $files]); } diff --git a/lib/private/Files/Node/Folder.php b/lib/private/Files/Node/Folder.php index 7746757c2a565..e67e4817e2aaf 100644 --- a/lib/private/Files/Node/Folder.php +++ b/lib/private/Files/Node/Folder.php @@ -363,10 +363,11 @@ public function getNonExistingName($name) { } /** - * @param int $since + * @param int $limit + * @param int $offset * @return \OCP\Files\Node[] */ - public function getRecent($since) { + public function getRecent($limit, $offset = 0) { $mimetypeLoader = \OC::$server->getMimeTypeLoader(); $mounts = $this->root->getMountsIn($this->path); $mounts[] = $this->getMountPoint(); @@ -387,55 +388,19 @@ public function getRecent($since) { $query = $builder ->select('f.*') ->from('filecache', 'f') - ->where($builder->expr()->gt('f.storage_mtime', $builder->createNamedParameter($since, IQueryBuilder::PARAM_INT))) ->andWhere($builder->expr()->in('f.storage', $builder->createNamedParameter($storageIds, IQueryBuilder::PARAM_INT_ARRAY))) ->andWhere($builder->expr()->orX( // handle non empty folders separate $builder->expr()->neq('f.mimetype', $builder->createNamedParameter($folderMimetype, IQueryBuilder::PARAM_INT)), $builder->expr()->eq('f.size', new Literal(0)) )) - ->orderBy('f.mtime', 'DESC'); + ->orderBy('f.mtime', 'DESC') + ->setMaxResults($limit) + ->setFirstResult($offset); $result = $query->execute()->fetchAll(); - // select folders with their mtime being the mtime of the oldest file in the folder - // this way we still show new folders but dont bumb the folder every time a file in it is changed - $builder = \OC::$server->getDatabaseConnection()->getQueryBuilder(); - $query = $builder - ->select('p.fileid', 'p.storage', 'p.mimetype', 'p.mimepart', 'p.size', 'p.path', 'p.etag', 'f1.storage_mtime', 'f1.mtime', 'p.permissions') - ->from('filecache', 'f1') - ->leftJoin('f1', 'filecache', 'f2', $builder->expr()->andX( // find the f1 with lowest mtime in the folder - $builder->expr()->eq('f1.parent', 'f2.parent'), - $builder->expr()->gt('f1.storage_mtime', 'f2.storage_mtime') - )) - ->innerJoin('f1', 'filecache', 'p', $builder->expr()->eq('f1.parent', 'p.fileid')) - ->where($builder->expr()->isNull('f2.fileid')) - ->andWhere($builder->expr()->gt('f1.storage_mtime', $builder->createNamedParameter($since, IQueryBuilder::PARAM_INT))) - ->andWhere($builder->expr()->in('f1.storage', $builder->createNamedParameter($storageIds, IQueryBuilder::PARAM_INT_ARRAY))) - ->andWhere($builder->expr()->neq('f1.size', new Literal(0))) - ->orderBy('f1.storage_mtime', 'DESC'); - - $folderResults = $query->execute()->fetchAll(); - - $found = []; // we sometimes get duplicate folders - $folderResults = array_filter($folderResults, function ($item) use (&$found) { - $isFound = isset($found[$item['fileid']]); - $found[$item['fileid']] = true; - return !$isFound; - }); - - $result = array_merge($folderResults, $result); - - usort($result, function ($a, $b) use ($folderMimetype) { - $diff = $b['mtime'] - $a['mtime']; - if ($diff === 0) { - return $a['mimetype'] === $folderMimetype ? -1 : 1; - } else { - return $diff; - } - }); - - $files = array_filter(array_map(function (array $entry) use ($mountMap, $mimetypeLoader) { + $files = array_filter(array_map(function (array $entry) use ($mountMap, $mimetypeLoader) { $mount = $mountMap[$entry['storage']]; $entry['internalPath'] = $entry['path']; $entry['mimetype'] = $mimetypeLoader->getMimetypeById($entry['mimetype']); diff --git a/lib/private/Files/Node/LazyRoot.php b/lib/private/Files/Node/LazyRoot.php index adc41153313a9..317b8144653a8 100644 --- a/lib/private/Files/Node/LazyRoot.php +++ b/lib/private/Files/Node/LazyRoot.php @@ -474,7 +474,7 @@ public function unlock($type) { /** * @inheritDoc */ - public function getRecent($type) { + public function getRecent($limit, $offset = 0) { return $this->__call(__FUNCTION__, func_get_args()); } } diff --git a/lib/public/Files/Folder.php b/lib/public/Files/Folder.php index b6686732f42d2..8f8576d8503f2 100644 --- a/lib/public/Files/Folder.php +++ b/lib/public/Files/Folder.php @@ -177,9 +177,10 @@ public function isCreatable(); public function getNonExistingName($name); /** - * @param int $since + * @param int $limit + * @param int $offset * @return \OCP\Files\Node[] * @since 9.1.0 */ - public function getRecent($since); + public function getRecent($limit, $offset = 0); } diff --git a/tests/lib/Files/Node/FolderTest.php b/tests/lib/Files/Node/FolderTest.php index eef78e7d428c1..18acfcae1fabb 100644 --- a/tests/lib/Files/Node/FolderTest.php +++ b/tests/lib/Files/Node/FolderTest.php @@ -836,7 +836,7 @@ public function testRecent() { 'mimetype' => 'text/plain', 'size' => 3 ]); - $cache->put('bar/foo/toold.txt', [ + $id3 = $cache->put('bar/foo/older.txt', [ 'storage_mtime' => $baseTime - 600, 'mtime' => $baseTime - 600, 'mimetype' => 'text/plain', @@ -846,11 +846,11 @@ public function testRecent() { $node = new \OC\Files\Node\Folder($root, $view, $folderPath, $folderInfo); - $nodes = $node->getRecent($baseTime - 500); + $nodes = $node->getRecent(5); $ids = array_map(function (Node $node) { return (int)$node->getId(); }, $nodes); - $this->assertEquals([$id1, $id2], $ids); + $this->assertEquals([$id1, $id2, $id3], $ids); } public function testRecentFolder() { @@ -900,14 +900,13 @@ public function testRecentFolder() { $node = new \OC\Files\Node\Folder($root, $view, $folderPath, $folderInfo); - $nodes = $node->getRecent($baseTime - 500); + $nodes = $node->getRecent(5); $ids = array_map(function (Node $node) { return (int)$node->getId(); }, $nodes); - $this->assertEquals([$id2, $id1, $id3], $ids);// sort folders before files with the same mtime, folders get the lowest child mtime + $this->assertEquals([$id2, $id3], $ids); $this->assertEquals($baseTime, $nodes[0]->getMTime()); $this->assertEquals($baseTime - 100, $nodes[1]->getMTime()); - $this->assertEquals($baseTime - 100, $nodes[2]->getMTime()); } public function testRecentJail() { @@ -952,7 +951,7 @@ public function testRecentJail() { $node = new \OC\Files\Node\Folder($root, $view, $folderPath, $folderInfo); - $nodes = $node->getRecent($baseTime - 500); + $nodes = $node->getRecent(5); $ids = array_map(function (Node $node) { return (int)$node->getId(); }, $nodes); From 2139a031e706e256eee1e4f8cfc6cea06cf8cb9f Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Fri, 22 Jul 2016 14:19:04 +0200 Subject: [PATCH 7/8] block user sorting in recent files --- apps/files/js/filelist.js | 7 ++++++- apps/files/js/recentfilelist.js | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index f0b16a5788680..24cccb3a5c8f3 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -174,6 +174,11 @@ */ _clientSideSort: true, + /** + * Whether or not users can change the sort attribute or direction + */ + _allowSorting: true, + /** * Current directory * @type String @@ -718,7 +723,7 @@ $target = $target.closest('a'); } sort = $target.attr('data-sort'); - if (sort) { + if (sort && this._allowSorting) { if (this._sort === sort) { this.setSort(sort, (this._sortDirection === 'desc')?'asc':'desc', true, true); } diff --git a/apps/files/js/recentfilelist.js b/apps/files/js/recentfilelist.js index e63a71f8549fa..e8c61cbfe2df7 100644 --- a/apps/files/js/recentfilelist.js +++ b/apps/files/js/recentfilelist.js @@ -28,6 +28,7 @@ $(document).ready(function () { direction: 'desc' }; this.initialize($el, options); + this._allowSorting = false; }; RecentFileList.prototype = _.extend({}, OCA.Files.FileList.prototype, /** @lends OCA.Files.RecentFileList.prototype */ { From f18338d93284f475d7f78dc2762ff0fdc8137dea Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Mon, 25 Jul 2016 10:41:53 +0200 Subject: [PATCH 8/8] fix test --- apps/files/tests/Controller/ViewControllerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/files/tests/Controller/ViewControllerTest.php b/apps/files/tests/Controller/ViewControllerTest.php index 25b5b6e04e007..373f8c2515222 100644 --- a/apps/files/tests/Controller/ViewControllerTest.php +++ b/apps/files/tests/Controller/ViewControllerTest.php @@ -198,7 +198,7 @@ public function testIndexWithRegularBrowser() { [ 'id' => 'recent', 'appname' => 'files', - 'script' => 'list.php', + 'script' => 'recentlist.php', 'order' => 2, 'name' => (string)new \OC_L10N_String(new \OC_L10N('files'), 'Recent', []), 'active' => false,