aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLukas Reschke <lukas@owncloud.com>2014-12-15 19:55:18 +0100
committerLukas Reschke <lukas@owncloud.com>2014-12-15 19:55:18 +0100
commitbe3d4fd303569a99554dbc6c62ce8992a45c51ad (patch)
tree6ef59f62bee77bf49bdd313810e43eaaa67cc21e
parent76357af2d50b7f040109311286cc57ee53c44cf1 (diff)
parent207d77e5cdf6386dd22d87a5851adae38ad9f77f (diff)
downloadnextcloud-server-be3d4fd303569a99554dbc6c62ce8992a45c51ad.tar.gz
nextcloud-server-be3d4fd303569a99554dbc6c62ce8992a45c51ad.zip
Merge pull request #12360 from owncloud/files-tags
Add favorites to files app
-rw-r--r--apps/files/appinfo/application.php40
-rw-r--r--apps/files/appinfo/routes.php29
-rw-r--r--apps/files/controller/apicontroller.php56
-rw-r--r--apps/files/css/files.css26
-rw-r--r--apps/files/index.php19
-rw-r--r--apps/files/js/app.js2
-rw-r--r--apps/files/js/favoritesfilelist.js99
-rw-r--r--apps/files/js/favoritesplugin.js116
-rw-r--r--apps/files/js/tagsplugin.js172
-rw-r--r--apps/files/lib/helper.php25
-rw-r--r--apps/files/service/tagservice.php94
-rw-r--r--apps/files/simplelist.php29
-rw-r--r--apps/files/templates/simplelist.php36
-rw-r--r--apps/files/tests/js/favoritesfilelistspec.js109
-rw-r--r--apps/files/tests/js/favoritespluginspec.js130
-rw-r--r--apps/files/tests/js/tagspluginspec.js87
-rw-r--r--apps/files/tests/service/tagservice.php121
-rw-r--r--core/js/core.json3
-rw-r--r--core/js/js.js19
-rw-r--r--lib/private/files/node/node.php7
20 files changed, 1209 insertions, 10 deletions
diff --git a/apps/files/appinfo/application.php b/apps/files/appinfo/application.php
index 7ca48bab474..13ff60daf89 100644
--- a/apps/files/appinfo/application.php
+++ b/apps/files/appinfo/application.php
@@ -11,6 +11,8 @@ namespace OCA\Files\Appinfo;
use OC\AppFramework\Utility\SimpleContainer;
use OCA\Files\Controller\ApiController;
use OCP\AppFramework\App;
+use \OCA\Files\Service\TagService;
+use \OCP\IContainer;
class Application extends App {
public function __construct(array $urlParams=array()) {
@@ -21,10 +23,44 @@ class Application extends App {
/**
* Controllers
*/
- $container->registerService('APIController', function (SimpleContainer $c) {
+ $container->registerService('APIController', function (IContainer $c) {
return new ApiController(
$c->query('AppName'),
- $c->query('Request')
+ $c->query('Request'),
+ $c->query('TagService')
+ );
+ });
+
+ /**
+ * Core
+ */
+ $container->registerService('L10N', function(IContainer $c) {
+ return $c->query('ServerContainer')->getL10N($c->query('AppName'));
+ });
+
+ /**
+ * Services
+ */
+ $container->registerService('Tagger', function(IContainer $c) {
+ return $c->query('ServerContainer')->getTagManager()->load('files');
+ });
+ $container->registerService('TagService', function(IContainer $c) {
+ $homeFolder = $c->query('ServerContainer')->getUserFolder();
+ return new TagService(
+ $c->query('ServerContainer')->getUserSession(),
+ $c->query('Tagger'),
+ $homeFolder
+ );
+ });
+
+ /**
+ * Controllers
+ */
+ $container->registerService('APIController', function (IContainer $c) {
+ return new ApiController(
+ $c->query('AppName'),
+ $c->query('Request'),
+ $c->query('TagService')
);
});
}
diff --git a/apps/files/appinfo/routes.php b/apps/files/appinfo/routes.php
index 96790a04855..349284ec52d 100644
--- a/apps/files/appinfo/routes.php
+++ b/apps/files/appinfo/routes.php
@@ -9,10 +9,31 @@
namespace OCA\Files\Appinfo;
$application = new Application();
-$application->registerRoutes($this, array('routes' => array(
- array('name' => 'API#getThumbnail', 'url' => '/api/v1/thumbnail/{x}/{y}/{file}', 'verb' => 'GET', 'requirements' => array('file' => '.+')),
-)));
-
+$application->registerRoutes(
+ $this,
+ array(
+ 'routes' => array(
+ array(
+ 'name' => 'API#getThumbnail',
+ 'url' => '/api/v1/thumbnail/{x}/{y}/{file}',
+ 'verb' => 'GET',
+ 'requirements' => array('file' => '.+')
+ ),
+ array(
+ 'name' => 'API#updateFileTags',
+ 'url' => '/api/v1/files/{path}',
+ 'verb' => 'POST',
+ 'requirements' => array('path' => '.+'),
+ ),
+ array(
+ 'name' => 'API#getFilesByTag',
+ 'url' => '/api/v1/tags/{tagName}/files',
+ 'verb' => 'GET',
+ 'requirements' => array('tagName' => '.+'),
+ ),
+ )
+ )
+);
/** @var $this \OC\Route\Router */
diff --git a/apps/files/controller/apicontroller.php b/apps/files/controller/apicontroller.php
index 89d24a5c47f..902731a0492 100644
--- a/apps/files/controller/apicontroller.php
+++ b/apps/files/controller/apicontroller.php
@@ -12,13 +12,21 @@ use OCP\AppFramework\Http;
use OCP\AppFramework\Controller;
use OCP\IRequest;
use OCP\AppFramework\Http\JSONResponse;
+use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\DownloadResponse;
use OC\Preview;
+use OCA\Files\Service\TagService;
class ApiController extends Controller {
- public function __construct($appName, IRequest $request){
+ /**
+ * @var TagService $tagService
+ */
+ private $tagService;
+
+ public function __construct($appName, IRequest $request, TagService $tagService){
parent::__construct($appName, $request);
+ $this->tagService = $tagService;
}
@@ -49,4 +57,50 @@ class ApiController extends Controller {
}
}
+ /**
+ * Updates the info of the specified file path
+ * The passed tags are absolute, which means they will
+ * replace the actual tag selection.
+ *
+ * @NoAdminRequired
+ * @CORS
+ *
+ * @param string $path path
+ * @param array $tags array of tags
+ */
+ public function updateFileTags($path, $tags = null) {
+ $result = array();
+ // if tags specified or empty array, update tags
+ if (!is_null($tags)) {
+ try {
+ $this->tagService->updateFileTags($path, $tags);
+ } catch (\OCP\Files\NotFoundException $e) {
+ return new DataResponse($e->getMessage(), Http::STATUS_NOT_FOUND);
+ }
+ $result['tags'] = $tags;
+ }
+ return new DataResponse($result, Http::STATUS_OK);
+ }
+
+ /**
+ * Returns a list of all files tagged with the given tag.
+ *
+ * @NoAdminRequired
+ * @CORS
+ *
+ * @param array $tagName tag name to filter by
+ */
+ public function getFilesByTag($tagName) {
+ $files = array();
+ $fileInfos = $this->tagService->getFilesByTag($tagName);
+ foreach ($fileInfos as &$fileInfo) {
+ $file = \OCA\Files\Helper::formatFileInfo($fileInfo);
+ $parts = explode('/', dirname($fileInfo->getPath()), 4);
+ $file['path'] = '/' . $parts[3];
+ $file['tags'] = array($tagName);
+ $files[] = $file;
+ }
+ return new DataResponse(array('files' => $files), Http::STATUS_OK);
+ }
+
}
diff --git a/apps/files/css/files.css b/apps/files/css/files.css
index d8336847e00..3829759a14e 100644
--- a/apps/files/css/files.css
+++ b/apps/files/css/files.css
@@ -191,10 +191,15 @@ table th#headerName {
width: 9999px; /* not really sure why this works better than 100% … table styling */
padding: 0;
}
+
#headerName-container {
position: relative;
height: 50px;
}
+.has-favorites #headerName-container {
+ padding-left: 50px;
+}
+
table th#headerSize, table td.filesize {
text-align: right;
}
@@ -286,6 +291,10 @@ table td.filename .nametext {
max-width: 800px;
height: 100%;
}
+.has-favorites #fileList td.filename a.name {
+ left: 50px;
+ margin-right: 50px;
+}
table td.filename .nametext .innernametext {
text-overflow: ellipsis;
@@ -403,6 +412,9 @@ table td.filename .uploadtext {
left: 18px;
z-index: 10;
}
+.has-favorites .select-all {
+ left: 68px;
+}
#fileList tr td.filename {
position: relative;
@@ -417,6 +429,18 @@ table td.filename .uploadtext {
height: 50px;
}
+#fileList tr td.filename .favorite {
+ display: inline-block;
+ float: left;
+}
+#fileList tr td.filename .action-favorite {
+ display: block;
+ float: left;
+ width: 30px;
+ line-height: 100%;
+ text-align: center;
+}
+
#uploadsize-message,#delete-confirm { display:none; }
/* File actions */
@@ -442,7 +466,7 @@ table td.filename .uploadtext {
padding: 17px 14px;
}
-#fileList .action.action-share-notification span, #fileList a {
+#fileList .action.action-share-notification span, #fileList a.name {
cursor: default !important;
}
diff --git a/apps/files/index.php b/apps/files/index.php
index 929bc5e79da..02076226c1a 100644
--- a/apps/files/index.php
+++ b/apps/files/index.php
@@ -20,6 +20,7 @@
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/
+use OCA\Files\Appinfo\Application;
// Check if we are a user
OCP\User::checkLoggedIn();
@@ -38,8 +39,16 @@ OCP\Util::addscript('files', 'filesummary');
OCP\Util::addscript('files', 'breadcrumb');
OCP\Util::addscript('files', 'filelist');
+\OCP\Util::addScript('files', 'favoritesfilelist');
+\OCP\Util::addScript('files', 'tagsplugin');
+\OCP\Util::addScript('files', 'favoritesplugin');
+
+\OC_Util::addVendorScript('core', 'handlebars/handlebars');
+
OCP\App::setActiveNavigationEntry('files_index');
+$l = \OC::$server->getL10N('files');
+
$isIE8 = false;
preg_match('/MSIE (.*?);/', $_SERVER['HTTP_USER_AGENT'], $matches);
if (count($matches) > 0 && $matches[1] <= 9) {
@@ -79,6 +88,16 @@ function sortNavigationItems($item1, $item2) {
return $item1['order'] - $item2['order'];
}
+\OCA\Files\App::getNavigationManager()->add(
+ array(
+ 'id' => 'favorites',
+ 'appname' => 'files',
+ 'script' => 'simplelist.php',
+ 'order' => 50,
+ 'name' => $l->t('Favorites')
+ )
+);
+
$navItems = \OCA\Files\App::getNavigationManager()->getAll();
usort($navItems, 'sortNavigationItems');
$nav->assign('navigationItems', $navItems);
diff --git a/apps/files/js/app.js b/apps/files/js/app.js
index ee5330485e7..adb1893bb0e 100644
--- a/apps/files/js/app.js
+++ b/apps/files/js/app.js
@@ -80,6 +80,8 @@
// refer to the one of the "files" view
window.FileList = this.fileList;
+ OC.Plugins.attach('OCA.Files.App', this);
+
this._setupEvents();
// trigger URL change event handlers
this._onPopState(urlParams);
diff --git a/apps/files/js/favoritesfilelist.js b/apps/files/js/favoritesfilelist.js
new file mode 100644
index 00000000000..0d555ce609d
--- /dev/null
+++ b/apps/files/js/favoritesfilelist.js
@@ -0,0 +1,99 @@
+/*
+ * 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.FavoritesFileList
+ * @augments OCA.Files.FavoritesFileList
+ *
+ * @classdesc Favorites file list.
+ * Displays the list of files marked as favorites
+ *
+ * @param $el container element with existing markup for the #controls
+ * and a table
+ * @param [options] map of options, see other parameters
+ */
+ var FavoritesFileList = function($el, options) {
+ this.initialize($el, options);
+ };
+ FavoritesFileList.prototype = _.extend({}, OCA.Files.FileList.prototype,
+ /** @lends OCA.Files.FavoritesFileList.prototype */ {
+ id: 'favorites',
+ appName: 'Favorites',
+
+ _clientSideSort: true,
+ _allowSelection: false,
+
+ /**
+ * @private
+ */
+ initialize: function($el, options) {
+ OCA.Files.FileList.prototype.initialize.apply(this, arguments);
+ if (this.initialized) {
+ return;
+ }
+ OC.Plugins.attach('OCA.Files.FavoritesFileList', 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() {
+ var tagName = OC.TAG_FAVORITE;
+ this.showMask();
+ if (this._reloadCall) {
+ this._reloadCall.abort();
+ }
+ this._reloadCall = $.ajax({
+ url: OC.generateUrl('/apps/files/api/v1/tags/{tagName}/files', {tagName: tagName}),
+ 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));
+ }
+ else {
+ // TODO: error handling
+ }
+ }
+ });
+
+ OCA.Files.FavoritesFileList = FavoritesFileList;
+ })(OCA);
+});
+
diff --git a/apps/files/js/favoritesplugin.js b/apps/files/js/favoritesplugin.js
new file mode 100644
index 00000000000..417a32ef804
--- /dev/null
+++ b/apps/files/js/favoritesplugin.js
@@ -0,0 +1,116 @@
+/*
+ * 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.FavoritesPlugin
+ *
+ * Registers the favorites file list from the files app sidebar.
+ */
+ OCA.Files.FavoritesPlugin = {
+ name: 'Favorites',
+
+ /**
+ * @type OCA.Files.FavoritesFileList
+ */
+ favoritesFileList: null,
+
+ attach: function() {
+ var self = this;
+ $('#app-content-favorites').on('show.plugin-favorites', function(e) {
+ self.showFileList($(e.target));
+ });
+ $('#app-content-favorites').on('hide.plugin-favorites', function() {
+ self.hideFileList();
+ });
+ },
+
+ detach: function() {
+ if (this.favoritesFileList) {
+ this.favoritesFileList.destroy();
+ OCA.Files.fileActions.off('setDefault.plugin-favorites', this._onActionsUpdated);
+ OCA.Files.fileActions.off('registerAction.plugin-favorites', this._onActionsUpdated);
+ $('#app-content-favorites').off('.plugin-favorites');
+ this.favoritesFileList = null;
+ }
+ },
+
+ showFileList: function($el) {
+ if (!this.favoritesFileList) {
+ this.favoritesFileList = this._createFavoritesFileList($el);
+ }
+ return this.favoritesFileList;
+ },
+
+ hideFileList: function() {
+ if (this.favoritesFileList) {
+ this.favoritesFileList.$fileList.empty();
+ }
+ },
+
+ /**
+ * Creates the favorites file list.
+ *
+ * @param $el container for the file list
+ * @return {OCA.Files.FavoritesFileList} file list
+ */
+ _createFavoritesFileList: function($el) {
+ var fileActions = this._createFileActions();
+ // register favorite list for sidebar section
+ return new OCA.Files.FavoritesFileList(
+ $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-favorites', this._onActionsUpdated);
+ OCA.Files.fileActions.on('registerAction.plugin-favorites', 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});
+ OCA.Files.App.fileList.changeDirectory(context.$file.attr('data-path') + '/' + filename, true, true);
+ });
+ fileActions.setDefault('dir', 'Open');
+ return fileActions;
+ },
+
+ _onActionsUpdated: function(ev) {
+ if (ev.action) {
+ this.favoritesFileList.fileActions.registerAction(ev.action);
+ } else if (ev.defaultAction) {
+ this.favoritesFileList.fileActions.setDefault(
+ ev.defaultAction.mime,
+ ev.defaultAction.name
+ );
+ }
+ }
+ };
+
+})(OCA);
+
+OC.Plugins.register('OCA.Files.App', OCA.Files.FavoritesPlugin);
+
diff --git a/apps/files/js/tagsplugin.js b/apps/files/js/tagsplugin.js
new file mode 100644
index 00000000000..a6757431ffa
--- /dev/null
+++ b/apps/files/js/tagsplugin.js
@@ -0,0 +1,172 @@
+/*
+ * 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.
+ *
+ */
+
+/* global Handlebars */
+
+(function(OCA) {
+
+ var TEMPLATE_FAVORITE_ACTION =
+ '<a href="#" ' +
+ 'class="action action-favorite {{#isFavorite}}permanent{{/isFavorite}}">' +
+ '<img class="svg" alt="{{altText}}" src="{{imgFile}}" />' +
+ '</a>';
+
+ /**
+ * Returns the path to the star image
+ *
+ * @param {boolean} state true if starred, false otherwise
+ * @return {string} path to star image
+ */
+ function getStarImage(state) {
+ return OC.imagePath('core', state ? 'actions/starred' : 'actions/star');
+ }
+
+ /**
+ * Render the star icon with the given state
+ *
+ * @param {boolean} state true if starred, false otherwise
+ * @return {Object} jQuery object
+ */
+ function renderStar(state) {
+ if (!this._template) {
+ this._template = Handlebars.compile(TEMPLATE_FAVORITE_ACTION);
+ }
+ return this._template({
+ isFavorite: state,
+ altText: state ? t('core', 'Favorited') : t('core', 'Favorite'),
+ imgFile: getStarImage(state)
+ });
+ }
+
+ /**
+ * Toggle star icon on action element
+ *
+ * @param {Object} action element
+ * @param {boolean} state true if starred, false otherwise
+ */
+ function toggleStar($actionEl, state) {
+ $actionEl.find('img').attr('src', getStarImage(state));
+ $actionEl.toggleClass('permanent', state);
+ }
+
+ OCA.Files = OCA.Files || {};
+
+ /**
+ * @namespace OCA.Files.TagsPlugin
+ *
+ * Extends the file actions and file list to include a favorite action icon
+ * and addition "data-tags" and "data-favorite" attributes.
+ */
+ OCA.Files.TagsPlugin = {
+ name: 'Tags',
+
+ allowedLists: [
+ 'files',
+ 'favorites'
+ ],
+
+ _extendFileActions: function(fileActions) {
+ var self = this;
+ // register "star" action
+ fileActions.registerAction({
+ name: 'favorite',
+ displayName: 'Favorite',
+ mime: 'all',
+ permissions: OC.PERMISSION_READ,
+ render: function(actionSpec, isDefault, context) {
+ var $file = context.$file;
+ var isFavorite = $file.data('favorite') === true;
+ var $icon = $(renderStar(isFavorite));
+ $file.find('td:first>.favorite').replaceWith($icon);
+ return $icon;
+ },
+ actionHandler: function(fileName, context) {
+ var $actionEl = context.$file.find('.action-favorite');
+ var $file = context.$file;
+ var dir = context.dir || context.fileList.getCurrentDirectory();
+ var tags = $file.attr('data-tags');
+ if (_.isUndefined(tags)) {
+ tags = '';
+ }
+ tags = tags.split('|');
+ tags = _.without(tags, '');
+ var isFavorite = tags.indexOf(OC.TAG_FAVORITE) >= 0;
+ if (isFavorite) {
+ // remove tag from list
+ tags = _.without(tags, OC.TAG_FAVORITE);
+ } else {
+ tags.push(OC.TAG_FAVORITE);
+ }
+ toggleStar($actionEl, !isFavorite);
+
+ self.applyFileTags(
+ dir + '/' + fileName,
+ tags
+ ).then(function(result) {
+ // read latest state from result
+ toggleStar($actionEl, (result.tags.indexOf(OC.TAG_FAVORITE) >= 0));
+ $file.attr('data-tags', tags.join('|'));
+ $file.attr('data-favorite', !isFavorite);
+ });
+ }
+ });
+ },
+
+ _extendFileList: function(fileList) {
+ // extend row prototype
+ fileList.$el.addClass('has-favorites');
+ var oldCreateRow = fileList._createRow;
+ fileList._createRow = function(fileData) {
+ var $tr = oldCreateRow.apply(this, arguments);
+ if (fileData.tags) {
+ $tr.attr('data-tags', fileData.tags.join('|'));
+ if (fileData.tags.indexOf(OC.TAG_FAVORITE) >= 0) {
+ $tr.attr('data-favorite', true);
+ }
+ }
+ $tr.find('td:first').prepend('<div class="favorite"></div>');
+ return $tr;
+ };
+ },
+
+ attach: function(fileList) {
+ if (this.allowedLists.indexOf(fileList.id) < 0) {
+ return;
+ }
+ this._extendFileActions(fileList.fileActions);
+ this._extendFileList(fileList);
+ },
+
+ /**
+ * Replaces the given files' tags with the specified ones.
+ *
+ * @param {String} fileName path to the file or folder to tag
+ * @param {Array.<String>} tagNames array of tag names
+ */
+ applyFileTags: function(fileName, tagNames) {
+ var encodedPath = OC.encodePath(fileName);
+ while (encodedPath[0] === '/') {
+ encodedPath = encodedPath.substr(1);
+ }
+ return $.ajax({
+ url: OC.generateUrl('/apps/files/api/v1/files/') + encodedPath,
+ contentType: 'application/json',
+ data: JSON.stringify({
+ tags: tagNames || []
+ }),
+ dataType: 'json',
+ type: 'POST'
+ });
+ }
+ };
+})(OCA);
+
+OC.Plugins.register('OCA.Files.FileList', OCA.Files.TagsPlugin);
+
diff --git a/apps/files/lib/helper.php b/apps/files/lib/helper.php
index 97b9d8e7044..7adca3ffa6d 100644
--- a/apps/files/lib/helper.php
+++ b/apps/files/lib/helper.php
@@ -122,6 +122,9 @@ class Helper
$entry['size'] = $i['size'];
$entry['type'] = $i['type'];
$entry['etag'] = $i['etag'];
+ if (isset($i['tags'])) {
+ $entry['tags'] = $i['tags'];
+ }
if (isset($i['displayname_owner'])) {
$entry['shareOwner'] = $i['displayname_owner'];
}
@@ -171,11 +174,33 @@ class Helper
*/
public static function getFiles($dir, $sortAttribute = 'name', $sortDescending = false) {
$content = \OC\Files\Filesystem::getDirectoryContent($dir);
+ $content = self::populateTags($content);
return self::sortFiles($content, $sortAttribute, $sortDescending);
}
/**
+ * Populate the result set with file tags
+ *
+ * @param array file list
+ * @return file list populated with tags
+ */
+ public static function populateTags($fileList) {
+ $filesById = array();
+ foreach ($fileList as $fileData) {
+ $filesById[$fileData['fileid']] = $fileData;
+ }
+ $tagger = \OC::$server->getTagManager()->load('files');
+ $tags = $tagger->getTagsForObjects(array_keys($filesById));
+ if ($tags) {
+ foreach ($tags as $fileId => $fileTags) {
+ $filesById[$fileId]['tags'] = $fileTags;
+ }
+ }
+ return $fileList;
+ }
+
+ /**
* Sort the given file info array
*
* @param \OCP\Files\FileInfo[] $files files to sort
diff --git a/apps/files/service/tagservice.php b/apps/files/service/tagservice.php
new file mode 100644
index 00000000000..86885e38ddd
--- /dev/null
+++ b/apps/files/service/tagservice.php
@@ -0,0 +1,94 @@
+<?php
+/**
+ * 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.
+ */
+
+namespace OCA\Files\Service;
+
+/**
+ * Service class to manage tags on files.
+ */
+class TagService {
+
+ /**
+ * @var \OCP\IUserSession
+ */
+ private $userSession;
+
+ /**
+ * @var \OCP\ITags
+ */
+ private $tagger;
+
+ /**
+ * @var \OCP\Files\Folder
+ */
+ private $homeFolder;
+
+ public function __construct(
+ \OCP\IUserSession $userSession,
+ \OCP\ITags $tagger,
+ \OCP\Files\Folder $homeFolder
+ ) {
+ $this->userSession = $userSession;
+ $this->tagger = $tagger;
+ $this->homeFolder = $homeFolder;
+ }
+
+ /**
+ * Updates the tags of the specified file path.
+ * The passed tags are absolute, which means they will
+ * replace the actual tag selection.
+ *
+ * @param string $path path
+ * @param array $tags array of tags
+ * @return array list of tags
+ * @throws \OCP\NotFoundException if the file does not exist
+ */
+ public function updateFileTags($path, $tags) {
+ $fileId = $this->homeFolder->get($path)->getId();
+
+ $currentTags = $this->tagger->getTagsForObjects(array($fileId));
+
+ if (!empty($currentTags)) {
+ $currentTags = current($currentTags);
+ }
+
+ $newTags = array_diff($tags, $currentTags);
+ foreach ($newTags as $tag) {
+ $this->tagger->tagAs($fileId, $tag);
+ }
+ $deletedTags = array_diff($currentTags, $tags);
+ foreach ($deletedTags as $tag) {
+ $this->tagger->unTag($fileId, $tag);
+ }
+
+ // TODO: re-read from tagger to make sure the
+ // list is up to date, in case of concurrent changes ?
+ return $tags;
+ }
+
+ /**
+ * Updates the tags of the specified file path.
+ * The passed tags are absolute, which means they will
+ * replace the actual tag selection.
+ *
+ * @param array $tagName tag name to filter by
+ * @return FileInfo[] list of matching files
+ * @throws \Exception if the tag does not exist
+ */
+ public function getFilesByTag($tagName) {
+ $nodes = $this->homeFolder->searchByTag(
+ $tagName, $this->userSession->getUser()->getUId()
+ );
+ foreach ($nodes as &$node) {
+ $node = $node->getFileInfo();
+ }
+
+ return $nodes;
+ }
+}
+
diff --git a/apps/files/simplelist.php b/apps/files/simplelist.php
new file mode 100644
index 00000000000..53e56b4ed32
--- /dev/null
+++ b/apps/files/simplelist.php
@@ -0,0 +1,29 @@
+<?php
+
+/**
+ * ownCloud - Simple files list
+ *
+ * @author Vincent Petry
+ * @copyright 2014 Vincent Petry <pvince81@owncloud.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or any later version.
+ *
+ * This library 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 library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// TODO: move to handlebars
+
+// renders the controls and table headers template
+$tmpl = new OCP\Template('files', 'simplelist', '');
+$tmpl->printPage();
+
diff --git a/apps/files/templates/simplelist.php b/apps/files/templates/simplelist.php
new file mode 100644
index 00000000000..c00febce653
--- /dev/null
+++ b/apps/files/templates/simplelist.php
@@ -0,0 +1,36 @@
+<div id="controls">
+ <div id="file_action_panel"></div>
+</div>
+<div id='notification'></div>
+
+<div id="emptycontent" class="hidden"></div>
+
+<input type="hidden" name="dir" value="" id="dir">
+
+<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><span class="sort-indicator"></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><span class="sort-indicator"></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">
+ <?php p($l->t('Delete'))?>
+ <img class="svg" alt="<?php p($l->t('Delete'))?>"
+ src="<?php print_unescaped(OCP\image_path("core", "actions/delete.svg")); ?>" />
+ </a></span>
+ </th>
+ </tr>
+ </thead>
+ <tbody id="fileList">
+ </tbody>
+ <tfoot>
+ </tfoot>
+</table>
+
diff --git a/apps/files/tests/js/favoritesfilelistspec.js b/apps/files/tests/js/favoritesfilelistspec.js
new file mode 100644
index 00000000000..608ddaca18b
--- /dev/null
+++ b/apps/files/tests/js/favoritesfilelistspec.js
@@ -0,0 +1,109 @@
+/*
+ * 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.
+ *
+ */
+
+describe('OCA.Files.FavoritesFileList tests', function() {
+ var fileList;
+
+ beforeEach(function() {
+ // init parameters and test table elements
+ $('#testArea').append(
+ '<div id="app-content-container">' +
+ // init horrible parameters
+ '<input type="hidden" id="dir" value="/"></input>' +
+ '<input type="hidden" id="permissions" value="31"></input>' +
+ // dummy controls
+ '<div id="controls">' +
+ ' <div class="actions creatable"></div>' +
+ ' <div class="notCreatable"></div>' +
+ '</div>' +
+ // dummy table
+ // TODO: at some point this will be rendered by the fileList class itself!
+ '<table id="filestable">' +
+ '<thead><tr>' +
+ '<th id="headerName" class="hidden column-name">' +
+ '<a class="name columntitle" data-sort="name"><span>Name</span><span class="sort-indicator"></span></a>' +
+ '</th>' +
+ '<th class="hidden column-mtime">' +
+ '<a class="columntitle" data-sort="mtime"><span class="sort-indicator"></span></a>' +
+ '</th>' +
+ '</tr></thead>' +
+ '<tbody id="fileList"></tbody>' +
+ '<tfoot></tfoot>' +
+ '</table>' +
+ '<div id="emptycontent">Empty content message</div>' +
+ '</div>'
+ );
+ });
+ afterEach(function() {
+ fileList.destroy();
+ fileList = undefined;
+ });
+
+ describe('loading file list', function() {
+ var response;
+
+ beforeEach(function() {
+ fileList = new OCA.Files.FavoritesFileList(
+ $('#app-content-container')
+ );
+ OCA.Files.FavoritesPlugin.attach(fileList);
+
+ fileList.reload();
+
+ /* jshint camelcase: false */
+ response = {
+ files: [{
+ id: 7,
+ name: 'test.txt',
+ path: '/somedir',
+ size: 123,
+ mtime: 11111000,
+ tags: [OC.TAG_FAVORITE],
+ permissions: OC.PERMISSION_ALL,
+ mimetype: 'text/plain'
+ }]
+ };
+ });
+ it('render files', function() {
+ var request;
+
+ expect(fakeServer.requests.length).toEqual(1);
+ request = fakeServer.requests[0];
+ expect(request.url).toEqual(
+ OC.generateUrl('apps/files/api/v1/tags/{tagName}/files', {tagName: OC.TAG_FAVORITE})
+ );
+
+ fakeServer.requests[0].respond(
+ 200,
+ { 'Content-Type': 'application/json' },
+ JSON.stringify(response)
+ );
+
+ var $rows = fileList.$el.find('tbody tr');
+ var $tr = $rows.eq(0);
+ expect($rows.length).toEqual(1);
+ expect($tr.attr('data-id')).toEqual('7');
+ expect($tr.attr('data-type')).toEqual('file');
+ expect($tr.attr('data-file')).toEqual('test.txt');
+ expect($tr.attr('data-path')).toEqual('/somedir');
+ expect($tr.attr('data-size')).toEqual('123');
+ expect(parseInt($tr.attr('data-permissions'), 10))
+ .toEqual(OC.PERMISSION_ALL);
+ expect($tr.attr('data-mime')).toEqual('text/plain');
+ expect($tr.attr('data-mtime')).toEqual('11111000');
+ expect($tr.find('a.name').attr('href')).toEqual(
+ OC.webroot +
+ '/index.php/apps/files/ajax/download.php' +
+ '?dir=%2Fsomedir&files=test.txt'
+ );
+ expect($tr.find('.nametext').text().trim()).toEqual('test.txt');
+ });
+ });
+});
diff --git a/apps/files/tests/js/favoritespluginspec.js b/apps/files/tests/js/favoritespluginspec.js
new file mode 100644
index 00000000000..90b40ede74b
--- /dev/null
+++ b/apps/files/tests/js/favoritespluginspec.js
@@ -0,0 +1,130 @@
+/*
+ * 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.
+ *
+ */
+
+describe('OCA.Files.FavoritesPlugin tests', function() {
+ var Plugin = OCA.Files.FavoritesPlugin;
+ var fileList;
+
+ beforeEach(function() {
+ $('#testArea').append(
+ '<div id="app-navigation">' +
+ '<ul><li data-id="files"><a>Files</a></li>' +
+ '<li data-id="sharingin"><a></a></li>' +
+ '<li data-id="sharingout"><a></a></li>' +
+ '</ul></div>' +
+ '<div id="app-content">' +
+ '<div id="app-content-files" class="hidden">' +
+ '</div>' +
+ '<div id="app-content-favorites" class="hidden">' +
+ '</div>' +
+ '</div>' +
+ '</div>'
+ );
+ OC.Plugins.attach('OCA.Files.App', Plugin);
+ fileList = Plugin.showFileList($('#app-content-favorites'));
+ });
+ afterEach(function() {
+ OC.Plugins.detach('OCA.Files.App', Plugin);
+ });
+
+ describe('initialization', function() {
+ it('inits favorites list on show', function() {
+ expect(fileList).toBeDefined();
+ });
+ });
+ describe('file actions', function() {
+ var oldLegacyFileActions;
+
+ beforeEach(function() {
+ oldLegacyFileActions = window.FileActions;
+ window.FileActions = new OCA.Files.FileActions();
+ });
+
+ afterEach(function() {
+ window.FileActions = oldLegacyFileActions;
+ });
+ it('provides default file actions', function() {
+ var fileActions = fileList.fileActions;
+
+ expect(fileActions.actions.all).toBeDefined();
+ expect(fileActions.actions.all.Delete).toBeDefined();
+ expect(fileActions.actions.all.Rename).toBeDefined();
+ expect(fileActions.actions.all.Download).toBeDefined();
+
+ expect(fileActions.defaults.dir).toEqual('Open');
+ });
+ it('provides custom file actions', function() {
+ var actionStub = sinon.stub();
+ // regular file action
+ OCA.Files.fileActions.register(
+ 'all',
+ 'RegularTest',
+ OC.PERMISSION_READ,
+ OC.imagePath('core', 'actions/shared'),
+ actionStub
+ );
+
+ Plugin.favoritesFileList = null;
+ fileList = Plugin.showFileList($('#app-content-favorites'));
+
+ expect(fileList.fileActions.actions.all.RegularTest).toBeDefined();
+ });
+ it('does not provide legacy file actions', function() {
+ var actionStub = sinon.stub();
+ // legacy file action
+ window.FileActions.register(
+ 'all',
+ 'LegacyTest',
+ OC.PERMISSION_READ,
+ OC.imagePath('core', 'actions/shared'),
+ actionStub
+ );
+
+ Plugin.favoritesFileList = null;
+ fileList = Plugin.showFileList($('#app-content-favorites'));
+
+ expect(fileList.fileActions.actions.all.LegacyTest).not.toBeDefined();
+ });
+ it('redirects to files app when opening a directory', function() {
+ var oldList = OCA.Files.App.fileList;
+ // dummy new list to make sure it exists
+ OCA.Files.App.fileList = new OCA.Files.FileList($('<table><thead></thead><tbody></tbody></table>'));
+
+ var setActiveViewStub = sinon.stub(OCA.Files.App, 'setActiveView');
+ // create dummy table so we can click the dom
+ var $table = '<table><thead></thead><tbody id="fileList"></tbody></table>';
+ $('#app-content-favorites').append($table);
+
+ Plugin.favoritesFileList = null;
+ fileList = Plugin.showFileList($('#app-content-favorites'));
+
+ fileList.setFiles([{
+ name: 'testdir',
+ type: 'dir',
+ path: '/somewhere/inside/subdir',
+ counterParts: ['user2'],
+ shareOwner: 'user2'
+ }]);
+
+ fileList.findFileEl('testdir').find('td a.name').click();
+
+ expect(OCA.Files.App.fileList.getCurrentDirectory()).toEqual('/somewhere/inside/subdir/testdir');
+
+ expect(setActiveViewStub.calledOnce).toEqual(true);
+ expect(setActiveViewStub.calledWith('files')).toEqual(true);
+
+ setActiveViewStub.restore();
+
+ // restore old list
+ OCA.Files.App.fileList = oldList;
+ });
+ });
+});
+
diff --git a/apps/files/tests/js/tagspluginspec.js b/apps/files/tests/js/tagspluginspec.js
new file mode 100644
index 00000000000..66240575a5c
--- /dev/null
+++ b/apps/files/tests/js/tagspluginspec.js
@@ -0,0 +1,87 @@
+/*
+ * 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.
+ *
+ */
+
+describe('OCA.Files.TagsPlugin tests', function() {
+ var fileList;
+ var testFiles;
+
+ beforeEach(function() {
+ var $content = $('<div id="content"></div>');
+ $('#testArea').append($content);
+ // dummy file list
+ var $div = $(
+ '<div>' +
+ '<table id="filestable">' +
+ '<thead></thead>' +
+ '<tbody id="fileList"></tbody>' +
+ '</table>' +
+ '</div>');
+ $('#content').append($div);
+
+ fileList = new OCA.Files.FileList($div);
+ OCA.Files.TagsPlugin.attach(fileList);
+
+ testFiles = [{
+ id: 1,
+ type: 'file',
+ name: 'One.txt',
+ path: '/subdir',
+ mimetype: 'text/plain',
+ size: 12,
+ permissions: OC.PERMISSION_ALL,
+ etag: 'abc',
+ shareOwner: 'User One',
+ isShareMountPoint: false,
+ tags: ['tag1', 'tag2']
+ }];
+ });
+ afterEach(function() {
+ fileList.destroy();
+ fileList = null;
+ });
+
+ describe('Favorites icon', function() {
+ it('renders favorite icon and extra data', function() {
+ var $action, $tr;
+ fileList.setFiles(testFiles);
+ $tr = fileList.$el.find('tbody tr:first');
+ $action = $tr.find('.action-favorite');
+ expect($action.length).toEqual(1);
+ expect($action.hasClass('permanent')).toEqual(false);
+
+ expect($tr.attr('data-tags').split('|')).toEqual(['tag1', 'tag2']);
+ expect($tr.attr('data-favorite')).not.toBeDefined();
+ });
+ it('renders permanent favorite icon and extra data', function() {
+ var $action, $tr;
+ testFiles[0].tags.push(OC.TAG_FAVORITE);
+ fileList.setFiles(testFiles);
+ $tr = fileList.$el.find('tbody tr:first');
+ $action = $tr.find('.action-favorite');
+ expect($action.length).toEqual(1);
+ expect($action.hasClass('permanent')).toEqual(true);
+
+ expect($tr.attr('data-tags').split('|')).toEqual(['tag1', 'tag2', OC.TAG_FAVORITE]);
+ expect($tr.attr('data-favorite')).toEqual('true');
+ });
+ it('adds has-favorites class on table', function() {
+ expect(fileList.$el.hasClass('has-favorites')).toEqual(true);
+ });
+ });
+ describe('Applying tags', function() {
+ it('sends request to server and updates icon', function() {
+ // TODO
+ fileList.setFiles(testFiles);
+ });
+ it('sends all tags to server when applyFileTags() is called ', function() {
+ // TODO
+ });
+ });
+});
diff --git a/apps/files/tests/service/tagservice.php b/apps/files/tests/service/tagservice.php
new file mode 100644
index 00000000000..158dd77e858
--- /dev/null
+++ b/apps/files/tests/service/tagservice.php
@@ -0,0 +1,121 @@
+<?php
+
+/**
+ * ownCloud
+ *
+ * @author Vincent Petry
+ * @copyright 2014 Vincent Petry <pvince81@owncloud.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or any later version.
+ *
+ * This library 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 library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+namespace OCA\Files;
+
+use \OCA\Files\Service\TagService;
+
+class TagServiceTest extends \Test\TestCase {
+
+ /**
+ * @var string
+ */
+ private $user;
+
+ /**
+ * @var \OCP\Files\Folder
+ */
+ private $root;
+
+ /**
+ * @var \OCA\Files\Service\TagService
+ */
+ private $tagService;
+
+ /**
+ * @var \OCP\ITags
+ */
+ private $tagger;
+
+ protected function setUp() {
+ parent::setUp();
+ $this->user = $this->getUniqueId('user');
+ \OC_User::createUser($this->user, 'test');
+ \OC_User::setUserId($this->user);
+ \OC_Util::setupFS($this->user);
+ /**
+ * @var \OCP\IUser
+ */
+ $user = new \OC\User\User($this->user, null);
+ /**
+ * @var \OCP\IUserSession
+ */
+ $userSession = $this->getMock('\OCP\IUserSession');
+ $userSession->expects($this->any())
+ ->method('getUser')
+ ->withAnyParameters()
+ ->will($this->returnValue($user));
+
+ $this->root = \OC::$server->getUserFolder();
+
+ $this->tagger = \OC::$server->getTagManager()->load('files');
+ $this->tagService = new TagService(
+ $userSession,
+ $this->tagger,
+ $this->root
+ );
+ }
+
+ protected function tearDown() {
+ \OC_User::setUserId('');
+ \OC_User::deleteUser($this->user);
+ }
+
+ public function testUpdateFileTags() {
+ $tag1 = 'tag1';
+ $tag2 = 'tag2';
+
+ $subdir = $this->root->newFolder('subdir');
+ $testFile = $subdir->newFile('test.txt');
+ $testFile->putContent('test contents');
+
+ $fileId = $testFile->getId();
+
+ // set tags
+ $this->tagService->updateFileTags('subdir/test.txt', array($tag1, $tag2));
+
+ $this->assertEquals(array($fileId), $this->tagger->getIdsForTag($tag1));
+ $this->assertEquals(array($fileId), $this->tagger->getIdsForTag($tag2));
+
+ // remove tag
+ $result = $this->tagService->updateFileTags('subdir/test.txt', array($tag2));
+ $this->assertEquals(array(), $this->tagger->getIdsForTag($tag1));
+ $this->assertEquals(array($fileId), $this->tagger->getIdsForTag($tag2));
+
+ // clear tags
+ $result = $this->tagService->updateFileTags('subdir/test.txt', array());
+ $this->assertEquals(array(), $this->tagger->getIdsForTag($tag1));
+ $this->assertEquals(array(), $this->tagger->getIdsForTag($tag2));
+
+ // non-existing file
+ $caught = false;
+ try {
+ $this->tagService->updateFileTags('subdir/unexist.txt', array($tag1));
+ } catch (\OCP\Files\NotFoundException $e) {
+ $caught = true;
+ }
+ $this->assertTrue($caught);
+
+ $subdir->delete();
+ }
+}
+
diff --git a/core/js/core.json b/core/js/core.json
index e529315fdb1..d3a9e2404e8 100644
--- a/core/js/core.json
+++ b/core/js/core.json
@@ -4,7 +4,8 @@
"jquery/jquery-migrate.min.js",
"jquery-ui/ui/jquery-ui.custom.js",
"underscore/underscore.js",
- "moment/min/moment-with-locales.js"
+ "moment/min/moment-with-locales.js",
+ "handlebars/handlebars.js"
],
"libraries": [
"jquery-showpassword.js",
diff --git a/core/js/js.js b/core/js/js.js
index a4f66ac7d90..f01c0eb77c1 100644
--- a/core/js/js.js
+++ b/core/js/js.js
@@ -66,6 +66,7 @@ var OC={
PERMISSION_DELETE:8,
PERMISSION_SHARE:16,
PERMISSION_ALL:31,
+ TAG_FAVORITE: '_$!<Favorite>!$_',
/* jshint camelcase: false */
webroot:oc_webroot,
appswebroots:(typeof oc_appswebroots !== 'undefined') ? oc_appswebroots:false,
@@ -212,6 +213,24 @@ var OC={
},
/**
+ * URI-Encodes a file path but keep the path slashes.
+ *
+ * @param path path
+ * @return encoded path
+ */
+ encodePath: function(path) {
+ if (!path) {
+ return path;
+ }
+ var parts = path.split('/');
+ var result = [];
+ for (var i = 0; i < parts.length; i++) {
+ result.push(encodeURIComponent(parts[i]));
+ }
+ return result.join('/');
+ },
+
+ /**
* Load a script for the server and load it. If the script is already loaded,
* the event handler will be called directly
* @param {string} app the app id to which the script belongs
diff --git a/lib/private/files/node/node.php b/lib/private/files/node/node.php
index b80db28e8ec..87d4a4b9156 100644
--- a/lib/private/files/node/node.php
+++ b/lib/private/files/node/node.php
@@ -43,7 +43,12 @@ class Node implements \OCP\Files\Node, FileInfo {
$this->path = $path;
}
- private function getFileInfo() {
+ /**
+ * Returns the matching file info
+ *
+ * @return \OCP\Files\FileInfo
+ */
+ public function getFileInfo() {
if (!$this->fileInfo) {
$this->fileInfo = $this->view->getFileInfo($this->path);
}