summaryrefslogtreecommitdiffstats
path: root/core/js/files
diff options
context:
space:
mode:
authorVincent Petry <pvince81@owncloud.com>2015-07-13 17:31:47 +0200
committerLukas Reschke <lukas@owncloud.com>2015-11-22 16:05:49 +0100
commitf120846e291bf83244831770c5f25b730fa8ba90 (patch)
treed15856a288b5f443f4f7838d99529efacfd0e802 /core/js/files
parentfb3d5c7856b9fbed926091e91ec54de023859df8 (diff)
downloadnextcloud-server-f120846e291bf83244831770c5f25b730fa8ba90.tar.gz
nextcloud-server-f120846e291bf83244831770c5f25b730fa8ba90.zip
Added OC.Files.Client Webdav-based files client
Diffstat (limited to 'core/js/files')
-rw-r--r--core/js/files/client.js673
-rw-r--r--core/js/files/fileinfo.js143
2 files changed, 816 insertions, 0 deletions
diff --git a/core/js/files/client.js b/core/js/files/client.js
new file mode 100644
index 00000000000..9bb7bb999fd
--- /dev/null
+++ b/core/js/files/client.js
@@ -0,0 +1,673 @@
+/*
+ * Copyright (c) 2015
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+/* global dav */
+
+(function(OC, FileInfo) {
+ /**
+ * @class OC.Files.Client
+ * @classdesc Client to access files on the server
+ *
+ * @param {Object} options
+ * @param {String} options.host host name
+ * @param {int} [options.port] port
+ * @param {boolean} [options.useHTTPS] whether to use https
+ * @param {String} [options.root] root path
+ * @param {String} [options.userName] user name
+ * @param {String} [options.password] password
+ *
+ * @since 8.2
+ */
+ var Client = function(options) {
+ this._root = options.root;
+ if (this._root.charAt(this._root.length - 1) === '/') {
+ this._root = this._root.substr(0, this._root.length - 1);
+ }
+
+ if (!options.port) {
+ // workaround in case port is null or empty
+ options.port = undefined;
+ }
+ var url = '';
+ var port = '';
+ if (options.useHTTPS) {
+ url += 'https://';
+ if (options.port && options.port !== 443) {
+ port = ':' + options.port;
+ }
+ } else {
+ url += 'http://';
+ if (options.port && options.port !== 80) {
+ port = ':' + options.port;
+ }
+ }
+ var credentials = '';
+ if (options.userName) {
+ credentials += encodeURIComponent(options.userName);
+ }
+ if (options.password) {
+ credentials += ':' + encodeURIComponent(options.password);
+ }
+ if (credentials.length > 0) {
+ url += credentials + '@';
+ }
+
+ url += options.host + port + this._root;
+ this._defaultHeaders = options.defaultHeaders || {'X-Requested-With': 'XMLHttpRequest'};
+ this._baseUrl = url;
+ this._client = new dav.Client({
+ baseUrl: this._baseUrl,
+ xmlNamespaces: {
+ 'DAV:': 'd',
+ 'http://owncloud.org/ns': 'oc'
+ }
+ });
+ this._client.xhrProvider = _.bind(this._xhrProvider, this);
+ };
+
+ Client.NS_OWNCLOUD = 'http://owncloud.org/ns';
+ Client.NS_DAV = 'DAV:';
+ Client._PROPFIND_PROPERTIES = [
+ /**
+ * Modified time
+ */
+ [Client.NS_DAV, 'getlastmodified'],
+ /**
+ * Etag
+ */
+ [Client.NS_DAV, 'getetag'],
+ /**
+ * Mime type
+ */
+ [Client.NS_DAV, 'getcontenttype'],
+ /**
+ * Resource type "collection" for folders, empty otherwise
+ */
+ [Client.NS_DAV, 'resourcetype'],
+ /**
+ * Compound file id, contains fileid + server instance id
+ */
+ [Client.NS_OWNCLOUD, 'id'],
+ /**
+ * Letter-coded permissions
+ */
+ [Client.NS_OWNCLOUD, 'permissions'],
+ //[Client.NS_OWNCLOUD, 'downloadURL'],
+ /**
+ * Folder sizes
+ */
+ [Client.NS_OWNCLOUD, 'size'],
+ /**
+ * File sizes
+ */
+ [Client.NS_DAV, 'getcontentlength']
+ ];
+
+ /**
+ * @memberof OC.Files
+ */
+ Client.prototype = {
+
+ /**
+ * Root path of the Webdav endpoint
+ *
+ * @type string
+ */
+ _root: null,
+
+ /**
+ * Client from the library
+ *
+ * @type nl.sara.webdav.Client
+ */
+ _client: null,
+
+ /**
+ * Returns the configured XHR provider for davclient
+ * @return {XMLHttpRequest}
+ */
+ _xhrProvider: function() {
+ var headers = this._defaultHeaders;
+ var xhr = new XMLHttpRequest();
+ var oldOpen = xhr.open;
+ // override open() method to add headers
+ xhr.open = function() {
+ var result = oldOpen.apply(this, arguments);
+ _.each(headers, function(value, key) {
+ xhr.setRequestHeader(key, value);
+ });
+ return result;
+ };
+ return xhr;
+ },
+
+ /**
+ * Prepends the base url to the given path sections
+ *
+ * @param {...String} path sections
+ *
+ * @return {String} base url + joined path, any leading or trailing slash
+ * will be kept
+ */
+ _buildUrl: function() {
+ var path = this._buildPath.apply(this, arguments);
+ if (path.charAt([path.length - 1]) === '/') {
+ path = path.substr(0, path.length - 1);
+ }
+ if (path.charAt(0) === '/') {
+ path = path.substr(1);
+ }
+ return this._baseUrl + '/' + path;
+ },
+
+ /**
+ * Append the path to the root and also encode path
+ * sections
+ *
+ * @param {...String} path sections
+ *
+ * @return {String} joined path, any leading or trailing slash
+ * will be kept
+ */
+ _buildPath: function() {
+ var path = OC.joinPaths.apply(this, arguments);
+ var sections = path.split('/');
+ var i;
+ for (i = 0; i < sections.length; i++) {
+ sections[i] = encodeURIComponent(sections[i]);
+ }
+ path = sections.join('/');
+ return path;
+ },
+
+ /**
+ * Parse headers string into a map
+ *
+ * @param {string} headersString headers list as string
+ *
+ * @return {Object.<String,Array>} map of header name to header contents
+ */
+ _parseHeaders: function(headersString) {
+ var headerRows = headersString.split('\n');
+ var headers = {};
+ for (var i = 0; i < headerRows.length; i++) {
+ var sepPos = headerRows[i].indexOf(':');
+ if (sepPos < 0) {
+ continue;
+ }
+
+ var headerName = headerRows[i].substr(0, sepPos);
+ var headerValue = headerRows[i].substr(sepPos + 2);
+
+ if (!headers[headerName]) {
+ // make it an array
+ headers[headerName] = [];
+ }
+
+ headers[headerName].push(headerValue);
+ }
+ return headers;
+ },
+
+ /**
+ * Parses the compound file id
+ *
+ * @param {string} compoundFileId compound file id as returned by the server
+ *
+ * @return {int} local file id, stripped of the instance id
+ */
+ _parseFileId: function(compoundFileId) {
+ if (!compoundFileId || compoundFileId.length < 8) {
+ return null;
+ }
+ return parseInt(compoundFileId.substr(0, 8), 10);
+ },
+
+ /**
+ * Parses the etag response which is in double quotes.
+ *
+ * @param {string} etag etag value in double quotes
+ *
+ * @return {string} etag without double quotes
+ */
+ _parseEtag: function(etag) {
+ if (etag.charAt(0) === '"') {
+ return etag.split('"')[1];
+ }
+ return etag;
+ },
+
+ /**
+ * Parse Webdav result
+ *
+ * @param {Object} response XML object
+ *
+ * @return {Array.<FileInfo>} array of file info
+ */
+ _parseFileInfo: function(response) {
+ var path = response.href;
+ if (path.substr(0, this._root.length) === this._root) {
+ path = path.substr(this._root.length);
+ }
+
+ if (path.charAt(path.length - 1) === '/') {
+ path = path.substr(0, path.length - 1);
+ }
+
+ path = '/' + decodeURIComponent(path);
+
+ if (response.propStat.length === 1 && response.propStat[0].status !== 200) {
+ return null;
+ }
+
+ var props = response.propStat[0].properties;
+
+ var data = {
+ id: this._parseFileId(props['{' + Client.NS_OWNCLOUD + '}id']),
+ path: OC.dirname(path) || '/',
+ name: OC.basename(path),
+ mtime: new Date(props['{' + Client.NS_DAV + '}getlastmodified']),
+ _props: props
+ };
+
+ var etagProp = props['{' + Client.NS_DAV + '}getetag'];
+ if (!_.isUndefined(etagProp)) {
+ data.etag = this._parseEtag(etagProp);
+ }
+
+ var sizeProp = props['{' + Client.NS_DAV + '}getcontentlength'];
+ if (!_.isUndefined(sizeProp)) {
+ data.size = parseInt(sizeProp, 10);
+ }
+
+ sizeProp = props['{' + Client.NS_OWNCLOUD + '}size'];
+ if (!_.isUndefined(sizeProp)) {
+ data.size = parseInt(sizeProp, 10);
+ }
+
+ var contentType = props['{' + Client.NS_DAV + '}getcontenttype'];
+ if (!_.isUndefined(contentType)) {
+ data.mimetype = contentType;
+ }
+
+ var resType = props['{' + Client.NS_DAV + '}resourcetype'];
+ var isFile = true;
+ if (!data.mimetype && resType) {
+ var xmlvalue = resType[0];
+ if (xmlvalue.namespaceURI === Client.NS_DAV && xmlvalue.nodeName.split(':')[1] === 'collection') {
+ data.mimetype = 'httpd/unix-directory';
+ isFile = false;
+ }
+ }
+
+ data.permissions = OC.PERMISSION_READ;
+ var permissionProp = props['{' + Client.NS_OWNCLOUD + '}permissions'];
+ if (!_.isUndefined(permissionProp)) {
+ var permString = permissionProp || '';
+ data.mountType = null;
+ for (var i = 0; i < permString.length; i++) {
+ var c = permString.charAt(i);
+ switch (c) {
+ // FIXME: twisted permissions
+ case 'C':
+ case 'K':
+ data.permissions |= OC.PERMISSION_CREATE;
+ if (!isFile) {
+ data.permissions |= OC.PERMISSION_UPDATE;
+ }
+ break;
+ case 'W':
+ if (isFile) {
+ // also add create permissions
+ data.permissions |= OC.PERMISSION_CREATE;
+ }
+ data.permissions |= OC.PERMISSION_UPDATE;
+ break;
+ case 'D':
+ data.permissions |= OC.PERMISSION_DELETE;
+ break;
+ case 'R':
+ data.permissions |= OC.PERMISSION_SHARE;
+ break;
+ case 'M':
+ if (!data.mountType) {
+ // TODO: how to identify external-root ?
+ data.mountType = 'external';
+ }
+ break;
+ case 'S':
+ // TODO: how to identify shared-root ?
+ data.mountType = 'shared';
+ break;
+ }
+ }
+ }
+
+ return new FileInfo(data);
+ },
+
+ /**
+ * Parse Webdav multistatus
+ *
+ * @param {Array} responses
+ */
+ _parseResult: function(responses) {
+ var self = this;
+ return _.map(responses, function(response) {
+ return self._parseFileInfo(response);
+ });
+ },
+
+ /**
+ * Returns whether the given status code means success
+ *
+ * @param {int} status status code
+ *
+ * @return true if status code is between 200 and 299 included
+ */
+ _isSuccessStatus: function(status) {
+ return status >= 200 && status <= 299;
+ },
+
+ /**
+ * Returns the default PROPFIND properties to use during a call.
+ *
+ * @return {Array.<Object>} array of properties
+ */
+ _getPropfindProperties: function() {
+ if (!this._propfindProperties) {
+ this._propfindProperties = _.map(Client._PROPFIND_PROPERTIES, function(propDef) {
+ return '{' + propDef[0] + '}' + propDef[1];
+ });
+ }
+ return this._propfindProperties;
+ },
+
+ /**
+ * Lists the contents of a directory
+ *
+ * @param {String} path path to retrieve
+ * @param {Object} [options] options
+ * @param {boolean} [options.includeParent=false] set to true to keep
+ * the parent folder in the result list
+ *
+ * @return {Promise} promise
+ */
+ getFolderContents: function(path, options) {
+ if (!path) {
+ path = '';
+ }
+ var self = this;
+ var deferred = $.Deferred();
+ var promise = deferred.promise();
+
+ // TODO: headers
+ this._client.propFind(
+ this._buildUrl(path),
+ this._getPropfindProperties(),
+ 1
+ ).then(function(result) {
+ if (self._isSuccessStatus(result.status)) {
+ var results = self._parseResult(result.body);
+ if (!options || !options.includeParent) {
+ // remove root dir, the first entry
+ results.shift();
+ }
+ deferred.resolve(result.status, results);
+ } else {
+ deferred.reject(result.status);
+ }
+ });
+ return promise;
+ },
+
+ /**
+ * Returns the file info of a given path.
+ *
+ * @param {String} path path
+ * @param {Array} [properties] list of webdav properties to
+ * retrieve
+ *
+ * @return {Promise} promise
+ */
+ getFileInfo: function(path) {
+ if (!path) {
+ path = '';
+ }
+ var self = this;
+ var deferred = $.Deferred();
+ var promise = deferred.promise();
+
+ // TODO: headers
+ this._client.propFind(
+ this._buildUrl(path),
+ this._getPropfindProperties(),
+ 0
+ ).then(
+ function(result) {
+ if (self._isSuccessStatus(result.status)) {
+ deferred.resolve(result.status, self._parseResult([result.body])[0]);
+ } else {
+ deferred.reject(result.status);
+ }
+ }
+ );
+ return promise;
+ },
+
+ /**
+ * Returns the contents of the given file.
+ *
+ * @param {String} path path to file
+ *
+ * @return {Promise}
+ */
+ getFileContents: function(path) {
+ if (!path) {
+ throw 'Missing argument "path"';
+ }
+ var self = this;
+ var deferred = $.Deferred();
+ var promise = deferred.promise();
+
+ this._client.request(
+ 'GET',
+ this._buildUrl(path),
+ this._defaultHeaders
+ ).then(
+ function(result) {
+ if (self._isSuccessStatus(result.status)) {
+ deferred.resolve(result.status, result.body);
+ } else {
+ deferred.reject(result.status);
+ }
+ }
+ );
+ return promise;
+ },
+
+ /**
+ * Puts the given data into the given file.
+ *
+ * @param {String} path path to file
+ * @param {String} body file body
+ * @param {Object} [options]
+ * @param {String} [options.contentType='text/plain'] content type
+ * @param {bool} [options.overwrite=true] whether to overwrite an existing file
+ *
+ * @return {Promise}
+ */
+ putFileContents: function(path, body, options) {
+ if (!path) {
+ throw 'Missing argument "path"';
+ }
+ var self = this;
+ var deferred = $.Deferred();
+ var promise = deferred.promise();
+ options = options || {};
+ var headers = _.extend({}, this._defaultHeaders);
+ var contentType = 'text/plain';
+ if (options.contentType) {
+ contentType = options.contentType;
+ }
+
+ headers['Content-Type'] = contentType;
+
+ if (_.isUndefined(options.overwrite) || options.overwrite) {
+ // will trigger 412 precondition failed if a file already exists
+ headers['If-None-Match'] = '*';
+ }
+
+ this._client.request(
+ 'PUT',
+ this._buildUrl(path),
+ headers,
+ body || ''
+ ).then(
+ function(result) {
+ if (self._isSuccessStatus(result.status)) {
+ deferred.resolve(result.status);
+ } else {
+ deferred.reject(result.status);
+ }
+ }
+ );
+ return promise;
+ },
+
+ _simpleCall: function(method, path) {
+ if (!path) {
+ throw 'Missing argument "path"';
+ }
+
+ var self = this;
+ var deferred = $.Deferred();
+ var promise = deferred.promise();
+
+ this._client.request(
+ method,
+ this._buildUrl(path),
+ this._defaultHeaders
+ ).then(
+ function(result) {
+ if (self._isSuccessStatus(result.status)) {
+ deferred.resolve(result.status);
+ } else {
+ deferred.reject(result.status);
+ }
+ }
+ );
+ return promise;
+ },
+
+ /**
+ * Creates a directory
+ *
+ * @param {String} path path to create
+ *
+ * @return {Promise}
+ */
+ createDirectory: function(path) {
+ return this._simpleCall('MKCOL', path);
+ },
+
+ /**
+ * Deletes a file or directory
+ *
+ * @param {String} path path to delete
+ *
+ * @return {Promise}
+ */
+ remove: function(path) {
+ return this._simpleCall('DELETE', path);
+ },
+
+ /**
+ * Moves path to another path
+ *
+ * @param {String} path path to move
+ * @param {String} destinationPath destination path
+ * @param {boolean} [allowOverwrite=false] true to allow overwriting,
+ * false otherwise
+ *
+ * @return {Promise} promise
+ */
+ move: function(path, destinationPath, allowOverwrite) {
+ if (!path) {
+ throw 'Missing argument "path"';
+ }
+ if (!destinationPath) {
+ throw 'Missing argument "destinationPath"';
+ }
+
+ var self = this;
+ var deferred = $.Deferred();
+ var promise = deferred.promise();
+ var headers =
+ _.extend({
+ 'Destination' : this._buildUrl(destinationPath)
+ }, this._defaultHeaders);
+
+ if (!allowOverwrite) {
+ headers['Overwrite'] = 'F';
+ }
+
+ this._client.request(
+ 'MOVE',
+ this._buildUrl(path),
+ headers
+ ).then(
+ function(response) {
+ if (self._isSuccessStatus(response.status)) {
+ deferred.resolve(response.status);
+ } else {
+ deferred.reject(response.status);
+ }
+ }
+ );
+ return promise;
+ }
+
+ };
+
+ if (!OC.Files) {
+ /**
+ * @namespace OC.Files
+ *
+ * @since 8.2
+ */
+ OC.Files = {};
+ }
+
+ /**
+ * Returns the default instance of the files client
+ *
+ * @return {OC.Files.Client} default client
+ *
+ * @since 8.2
+ */
+ OC.Files.getClient = function() {
+ if (OC.Files._defaultClient) {
+ return OC.Files._defaultClient;
+ }
+
+ var client = new OC.Files.Client({
+ host: OC.getHost(),
+ port: OC.getPort(),
+ root: OC.linkToRemoteBase('webdav'),
+ useHTTPS: OC.getProtocol() === 'https'
+ });
+ OC.Files._defaultClient = client;
+ return client;
+ };
+
+ OC.Files.Client = Client;
+})(OC, OC.Files.FileInfo);
+
diff --git a/core/js/files/fileinfo.js b/core/js/files/fileinfo.js
new file mode 100644
index 00000000000..95af07b7992
--- /dev/null
+++ b/core/js/files/fileinfo.js
@@ -0,0 +1,143 @@
+/*
+ * Copyright (c) 2015
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function(OC) {
+
+ /**
+ * @class OC.Files.FileInfo
+ * @classdesc File information
+ *
+ * @param {Object} data file data, see attributes for details
+ *
+ * @since 8.2
+ */
+ var FileInfo = function(data) {
+ if (!_.isUndefined(data.id)) {
+ this.id = parseInt(data.id, 10);
+ }
+
+ // TODO: normalize path
+ this.path = data.path || '';
+ this.name = data.name;
+
+ this.mtime = data.mtime;
+ this.etag = data.etag;
+ this.permissions = data.permissions;
+ this.size = data.size;
+ this.mimetype = data.mimetype || 'application/octet-stream';
+ this.mountType = data.mountType;
+ this.icon = data.icon;
+ this._props = data._props;
+
+ if (data.type) {
+ this.type = data.type;
+ } else if (this.mimetype === 'httpd/unix-directory') {
+ this.type = 'dir';
+ } else {
+ this.type = 'file';
+ }
+
+ if (!this.mimetype) {
+ if (this.type === 'dir') {
+ this.mimetype = 'httpd/unix-directory';
+ } else {
+ this.mimetype = 'application/octet-stream';
+ }
+ }
+ };
+
+ /**
+ * @memberof OC.Files
+ */
+ FileInfo.prototype = {
+ /**
+ * File id
+ *
+ * @type int
+ */
+ id: null,
+
+ /**
+ * File name
+ *
+ * @type String
+ */
+ name: null,
+
+ /**
+ * Path leading to the file, without the file name,
+ * and with a leading slash.
+ *
+ * @type String
+ */
+ path: null,
+
+ /**
+ * Mime type
+ *
+ * @type String
+ */
+ mimetype: null,
+
+ /**
+ * Icon URL.
+ *
+ * Can be used to override the mime type icon.
+ *
+ * @type String
+ */
+ icon: null,
+
+ /**
+ * File type. 'file' for files, 'dir' for directories.
+ *
+ * @type String
+ * @deprecated rely on mimetype instead
+ */
+ type: 'file',
+
+ /**
+ * Permissions.
+ *
+ * @see OC#PERMISSION_ALL for permissions
+ * @type int
+ */
+ permissions: null,
+
+ /**
+ * Modification time
+ *
+ * @type int
+ */
+ mtime: null,
+
+ /**
+ * Etag
+ *
+ * @type String
+ */
+ etag: null,
+
+ /**
+ * Mount type.
+ *
+ * One of null, "external-root", "shared" or "shared-root"
+ *
+ * @type string
+ */
+ mountType: null
+ };
+
+ if (!OC.Files) {
+ OC.Files = {};
+ }
+ OC.Files.FileInfo = FileInfo;
+})(OC);
+