summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorWilliam Bargent <personal@williambargent.co.uk>2016-07-25 15:25:02 +0100
committerGitHub <noreply@github.com>2016-07-25 15:25:02 +0100
commit352e24e703aae74c9486810e8c3550ae581c93cb (patch)
tree144f60ca01ed26ef1ea52cb66637b1220bfbe003
parente51afa1684a0d555082e3115a75dc357a14a9cca (diff)
parentf18338d93284f475d7f78dc2762ff0fdc8137dea (diff)
downloadnextcloud-server-352e24e703aae74c9486810e8c3550ae581c93cb.tar.gz
nextcloud-server-352e24e703aae74c9486810e8c3550ae581c93cb.zip
Merge pull request #292 from nextcloud/recent-files
Add "Recent" file listing
-rw-r--r--apps/files/appinfo/app.php14
-rw-r--r--apps/files/appinfo/routes.php5
-rw-r--r--apps/files/js/filelist.js7
-rw-r--r--apps/files/js/recentfilelist.js106
-rw-r--r--apps/files/js/recentplugin.js117
-rw-r--r--apps/files/lib/AppInfo/Application.php3
-rw-r--r--apps/files/lib/Controller/ApiController.php65
-rw-r--r--apps/files/lib/Controller/ViewController.php2
-rw-r--r--apps/files/recentlist.php7
-rw-r--r--apps/files/templates/recentlist.php42
-rw-r--r--apps/files/tests/Controller/ApiControllerTest.php8
-rw-r--r--apps/files/tests/Controller/ViewControllerTest.php25
-rw-r--r--lib/private/Files/Node/Folder.php78
-rw-r--r--lib/private/Files/Node/LazyRoot.php7
-rw-r--r--lib/private/Files/Storage/Wrapper/Jail.php2
-rw-r--r--lib/public/Files/FileInfo.php5
-rw-r--r--lib/public/Files/Folder.php8
-rw-r--r--tests/lib/Files/Node/FolderTest.php173
18 files changed, 640 insertions, 34 deletions
diff --git a/apps/files/appinfo/app.php b/apps/files/appinfo/app.php
index cc86e9bf270..850c335c27d 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.presentat
$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 @@ $templateManager->registerTemplate('application/vnd.oasis.opendocument.spreadshe
];
});
+\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/appinfo/routes.php b/apps/files/appinfo/routes.php
index 34bb1011773..7b5ac63a160 100644
--- a/apps/files/appinfo/routes.php
+++ b/apps/files/appinfo/routes.php
@@ -51,6 +51,11 @@ $application->registerRoutes(
'requirements' => array('tagName' => '.+'),
),
array(
+ 'name' => 'API#getRecentFiles',
+ 'url' => '/api/v1/recent/',
+ 'verb' => 'GET'
+ ),
+ array(
'name' => 'API#updateFileSorting',
'url' => '/api/v1/sorting',
'verb' => 'POST'
diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js
index f0b16a57886..24cccb3a5c8 100644
--- a/apps/files/js/filelist.js
+++ b/apps/files/js/filelist.js
@@ -175,6 +175,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
new file mode 100644
index 00000000000..e8c61cbfe2d
--- /dev/null
+++ b/apps/files/js/recentfilelist.js
@@ -0,0 +1,106 @@
+/*
+ * Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com>
+ *
+ * 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);
+ this._allowSorting = false;
+ };
+ 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 00000000000..fcd427b18a2
--- /dev/null
+++ b/apps/files/js/recentplugin.js
@@ -0,0 +1,117 @@
+/*
+ * Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com>
+ *
+ * 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/AppInfo/Application.php b/apps/files/lib/AppInfo/Application.php
index fac8e3a3a4b..fc91e05ba7e 100644
--- a/apps/files/lib/AppInfo/Application.php
+++ b/apps/files/lib/AppInfo/Application.php
@@ -47,7 +47,8 @@ class Application extends App {
$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 57eb43bbe9c..7ce83bfca15 100644
--- a/apps/files/lib/Controller/ApiController.php
+++ b/apps/files/lib/Controller/ApiController.php
@@ -31,6 +31,7 @@ namespace OCA\Files\Controller;
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 OCP\AppFramework\Http\Response;
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 @@ class ApiController extends Controller {
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;
}
/**
@@ -143,6 +152,28 @@ class ApiController extends Controller {
}
/**
+ * @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.
*
* @NoAdminRequired
@@ -151,28 +182,28 @@ class ApiController extends Controller {
* @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() {
+ $nodes = $this->userFolder->getRecent(100);
+ $files = $this->formatNodes($nodes);
+ return new DataResponse(['files' => $files]);
+ }
+
+ /**
* Return a list of share types for outgoing shares
*
* @param Node $node file node
diff --git a/apps/files/lib/Controller/ViewController.php b/apps/files/lib/Controller/ViewController.php
index 5e342e6589b..9dbe06ff789 100644
--- a/apps/files/lib/Controller/ViewController.php
+++ b/apps/files/lib/Controller/ViewController.php
@@ -173,9 +173,11 @@ class ViewController extends Controller {
\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 00000000000..1976be4894a
--- /dev/null
+++ b/apps/files/recentlist.php
@@ -0,0 +1,7 @@
+<?php
+// Check if we are a user
+OCP\User::checkLoggedIn();
+
+$tmpl = new OCP\Template('files', 'recentlist', '');
+
+$tmpl->printPage();
diff --git a/apps/files/templates/recentlist.php b/apps/files/templates/recentlist.php
new file mode 100644
index 00000000000..1667eb4cc8d
--- /dev/null
+++ b/apps/files/templates/recentlist.php
@@ -0,0 +1,42 @@
+<?php /** @var $l OC_L10N */ ?>
+<div id='notification'></div>
+
+<div id="emptycontent" class="hidden"></div>
+
+<input type="hidden" name="dir" value="" id="dir">
+
+<div class="nofilterresults hidden">
+ <div class="icon-search"></div>
+ <h2><?php p($l->t('No entries found in this folder')); ?></h2>
+ <p></p>
+</div>
+
+<table id="filestable">
+ <thead>
+ <tr>
+ <th id='headerName' class="hidden column-name">
+ <div id="headerName-container">
+ <a class="name sort columntitle"
+ data-sort="name"><span><?php p($l->t('Name')); ?></span></a>
+ </div>
+ </th>
+ <th id="headerSize" class="hidden column-size">
+ <a class="size sort columntitle"
+ data-sort="size"><span><?php p($l->t('Size')); ?></span></a>
+ </th>
+ <th id="headerDate" class="hidden column-mtime">
+ <a id="modified" class="columntitle"
+ data-sort="mtime"><span><?php p($l->t('Modified')); ?></span><span
+ class="sort-indicator"></span></a>
+ <span class="selectedActions"><a href="" class="delete-selected">
+ <span><?php p($l->t('Delete')) ?></span>
+ <span class="icon icon-delete"></span>
+ </a></span>
+ </th>
+ </tr>
+ </thead>
+ <tbody id="fileList">
+ </tbody>
+ <tfoot>
+ </tfoot>
+</table>
diff --git a/apps/files/tests/Controller/ApiControllerTest.php b/apps/files/tests/Controller/ApiControllerTest.php
index 1d39c88021b..348150e0e08 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 @@ class ApiControllerTest extends TestCase {
->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 @@ class ApiControllerTest extends TestCase {
$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 ceb48a2241f..373f8c25152 100644
--- a/apps/files/tests/Controller/ViewControllerTest.php
+++ b/apps/files/tests/Controller/ViewControllerTest.php
@@ -191,7 +191,16 @@ class ViewControllerTest extends TestCase {
'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' => 'recentlist.php',
+ 'order' => 2,
+ 'name' => (string)new \OC_L10N_String(new \OC_L10N('files'), 'Recent', []),
'active' => false,
'icon' => '',
],
@@ -209,7 +218,7 @@ class ViewControllerTest extends TestCase {
'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 @@ class ViewControllerTest extends TestCase {
'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 @@ class ViewControllerTest extends TestCase {
'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 @@ class ViewControllerTest extends TestCase {
'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 @@ class ViewControllerTest extends TestCase {
'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' => '',
],
@@ -273,6 +282,10 @@ class ViewControllerTest extends TestCase {
'content' => null,
],
[
+ 'id' => 'recent',
+ 'content' => null,
+ ],
+ [
'id' => 'favorites',
'content' => null,
],
diff --git a/lib/private/Files/Node/Folder.php b/lib/private/Files/Node/Folder.php
index 8813b6c0775..e67e4817e2a 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,79 @@ class Folder extends Node implements \OCP\Files\Folder {
$uniqueName = \OC_Helper::buildNotExistingFileNameForView($this->getPath(), $name, $this->view);
return trim($this->getRelativePath($uniqueName), '/');
}
+
+ /**
+ * @param int $limit
+ * @param int $offset
+ * @return \OCP\Files\Node[]
+ */
+ public function getRecent($limit, $offset = 0) {
+ $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')
+ ->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')
+ ->setMaxResults($limit)
+ ->setFirstResult($offset);
+
+ $result = $query->execute()->fetchAll();
+
+ $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 1203fc4d162..317b8144653 100644
--- a/lib/private/Files/Node/LazyRoot.php
+++ b/lib/private/Files/Node/LazyRoot.php
@@ -471,5 +471,10 @@ class LazyRoot implements IRootFolder {
return $this->__call(__FUNCTION__, func_get_args());
}
-
+ /**
+ * @inheritDoc
+ */
+ public function getRecent($limit, $offset = 0) {
+ return $this->__call(__FUNCTION__, func_get_args());
+ }
}
diff --git a/lib/private/Files/Storage/Wrapper/Jail.php b/lib/private/Files/Storage/Wrapper/Jail.php
index 80c75523748..5b9085799b9 100644
--- a/lib/private/Files/Storage/Wrapper/Jail.php
+++ b/lib/private/Files/Storage/Wrapper/Jail.php
@@ -377,7 +377,7 @@ class Jail extends Wrapper {
*/
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);
diff --git a/lib/public/Files/FileInfo.php b/lib/public/Files/FileInfo.php
index d61726c4217..04790d41556 100644
--- a/lib/public/Files/FileInfo.php
+++ b/lib/public/Files/FileInfo.php
@@ -59,6 +59,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
*
* @return string
diff --git a/lib/public/Files/Folder.php b/lib/public/Files/Folder.php
index 9a2a338b559..8f8576d8503 100644
--- a/lib/public/Files/Folder.php
+++ b/lib/public/Files/Folder.php
@@ -175,4 +175,12 @@ interface Folder extends Node {
* @since 8.1.0
*/
public function getNonExistingName($name);
+
+ /**
+ * @param int $limit
+ * @param int $offset
+ * @return \OCP\Files\Node[]
+ * @since 9.1.0
+ */
+ public function getRecent($limit, $offset = 0);
}
diff --git a/tests/lib/Files/Node/FolderTest.php b/tests/lib/Files/Node/FolderTest.php
index 7ce9fff1419..18acfcae1fa 100644
--- a/tests/lib/Files/Node/FolderTest.php
+++ b/tests/lib/Files/Node/FolderTest.php
@@ -12,6 +12,8 @@ use OC\Files\Cache\Cache;
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 @@ class FolderTest extends \Test\TestCase {
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 @@ class FolderTest extends \Test\TestCase {
->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,167 @@ class FolderTest extends \Test\TestCase {
$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
+ ]);
+ $id3 = $cache->put('bar/foo/older.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(5);
+ $ids = array_map(function (Node $node) {
+ return (int)$node->getId();
+ }, $nodes);
+ $this->assertEquals([$id1, $id2, $id3], $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 - 100,
+ 'mtime' => $baseTime - 100,
+ 'mimetype' => 'text/plain',
+ 'size' => 3,
+ 'parent' => $id1
+ ]);
+
+ $node = new \OC\Files\Node\Folder($root, $view, $folderPath, $folderInfo);
+
+
+ $nodes = $node->getRecent(5);
+ $ids = array_map(function (Node $node) {
+ return (int)$node->getId();
+ }, $nodes);
+ $this->assertEquals([$id2, $id3], $ids);
+ $this->assertEquals($baseTime, $nodes[0]->getMTime());
+ $this->assertEquals($baseTime - 100, $nodes[1]->getMTime());
+ }
+
+ 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(5);
+ $ids = array_map(function (Node $node) {
+ return (int)$node->getId();
+ }, $nodes);
+ $this->assertEquals([$id1], $ids);
+ }
}