Skip to content
This repository has been archived by the owner on Sep 6, 2021. It is now read-only.

Enable opening of remote(http|https) files in Brackets #14153

Merged
merged 10 commits into from
May 4, 2018
5 changes: 3 additions & 2 deletions src/document/DocumentManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -323,10 +323,11 @@ define(function (require, exports, module) {
* If all you need is the Document's getText() value, use the faster getDocumentText() instead.
*
* @param {!string} fullPath
* @param {!object} fileObj actual File|RemoteFile or some other protocol adapter handle
* @return {$.Promise} A promise object that will be resolved with the Document, or rejected
* with a FileSystemError if the file is not yet open and can't be read from disk.
*/
function getDocumentForPath(fullPath) {
function getDocumentForPath(fullPath, fileObj) {
var doc = getOpenDocumentForPath(fullPath);

if (doc) {
Expand All @@ -342,7 +343,7 @@ define(function (require, exports, module) {
return promise;
}

var file = FileSystem.getFileForPath(fullPath),
var file = fileObj || FileSystem.getFileForPath(fullPath),
pendingPromise = getDocumentForPath._pendingDocumentPromises[file.id];

if (pendingPromise) {
Expand Down
209 changes: 209 additions & 0 deletions src/extensions/default/RemoteFileAdapter/RemoteFile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
/*
* Copyright (c) 2018 - present Adobe Systems Incorporated. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/

define(function (require, exports, module) {
"use strict";

var FileSystemError = brackets.getModule("filesystem/FileSystemError"),
FileSystemStats = brackets.getModule("filesystem/FileSystemStats");

var SESSION_START_TIME = new Date();

/*
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: /**

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in latest commit 👍

* Create a new file stat. See the FileSystemStats class for more details.
*
* @param {!string} fullPath The full path for this File.
* @return {FileSystemStats} stats.
*/
function _getStats(uri) {
return new FileSystemStats({
isFile: true,
mtime: SESSION_START_TIME.toISOString(),
size: 0,
realPath: uri,
hash: uri
});
}

/*
* Model for a RemoteFile.
*
* This class should *not* be instantiated directly. Use FileSystem.getFileForPath
*
* See the FileSystem class for more details.
*
* @constructor
* @param {!string} fullPath The full path for this File.
* @param {!FileSystem} fileSystem The file system associated with this File.
*/
function RemoteFile(fullPath, fileSystem) {
this._isFile = true;
this._isDirectory = false;
this._path = fullPath;
this._stat = _getStats(fullPath);
this._id = fullPath;
this._name = fullPath.split('/').pop();
this._fileSystem = fileSystem;
this.donotWatch = true;
}

// Add "fullPath", "name", "parent", "id", "isFile" and "isDirectory" getters
Object.defineProperties(RemoteFile.prototype, {
"fullPath": {
get: function () { return this._path; },
set: function () { throw new Error("Cannot set fullPath"); }
},
"name": {
get: function () { return this._name; },
set: function () { throw new Error("Cannot set name"); }
},
"parentPath": {
get: function () { return this._parentPath; },
set: function () { throw new Error("Cannot set parentPath"); }
},
"id": {
get: function () { return this._id; },
set: function () { throw new Error("Cannot set id"); }
},
"isFile": {
get: function () { return this._isFile; },
set: function () { throw new Error("Cannot set isFile"); }
},
"isDirectory": {
get: function () { return this._isDirectory; },
set: function () { throw new Error("Cannot set isDirectory"); }
},
"_impl": {
get: function () { return this._fileSystem._impl; },
set: function () { throw new Error("Cannot set _impl"); }
}
});

RemoteFile.prototype.exists = function (callback) {
callback(null, true);
};

/**
* Helpful toString for debugging and equality check purposes
*/
RemoteFile.prototype.toString = function () {
return "[" + (this._isDirectory ? "Directory " : "File ") + this._path + "]";
};

RemoteFile.prototype.constructor = RemoteFile;

/**
* Cached contents of this file. This value is nullable but should NOT be undefined.
* @private
* @type {?string}
*/
RemoteFile.prototype._contents = null;


/**
* @private
* @type {?string}
*/
RemoteFile.prototype._encoding = "utf8";

/**
* @private
* @type {?bool}
*/
RemoteFile.prototype._preserveBOM = false;


/**
* Clear any cached data for this file. Note that this explicitly does NOT
* clear the file's hash.
* @private
*/
RemoteFile.prototype._clearCachedData = function () {
// no-op
};

/**
* Reads a remote file.
*
* @param {Object=} options Currently unused.
* @param {function (?string, string=, FileSystemStats=)} callback Callback that is passed the
* FileSystemError string or the file's contents and its stats.
*/
RemoteFile.prototype.read = function (options, callback) {
if (typeof (options) === "function") {
callback = options;
}
this._encoding = "utf8";

if (this._contents !== null && this._stat) {
callback(null, this._contents, this._encoding, this._stat);
return;
}

var self = this;
$.ajax({
url: this.fullPath
})
.done(function (data) {
self._contents = data;
callback(null, data, self._encoding, self._stat);
})
.fail(function (e) {
callback(FileSystemError.NOT_FOUND);
});
};

/**
* Write a file.
*
* @param {string} data Data to write.
* @param {object=} options Currently unused.
* @param {function (?string, FileSystemStats=)=} callback Callback that is passed the
* FileSystemError string or the file's new stats.
*/
RemoteFile.prototype.write = function (data, encoding, callback) {
if (typeof (encoding) === "function") {
callback = encoding;
}
callback(FileSystemError.NOT_FOUND);
};

RemoteFile.prototype.exists = function (callback) {
callback(null, false);
};

RemoteFile.prototype.unlink = function (callback) {
callback(FileSystemError.NOT_FOUND);
};

RemoteFile.prototype.rename = function (newName, callback) {
callback(FileSystemError.NOT_FOUND);
};

RemoteFile.prototype.moveToTrash = function (callback) {
callback(FileSystemError.NOT_FOUND);
};

// Export this class
module.exports = RemoteFile;
});
70 changes: 70 additions & 0 deletions src/extensions/default/RemoteFileAdapter/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright (c) 2018 - present Adobe Systems Incorporated. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/

define(function (require, exports, module) {
"use strict";

var AppInit = brackets.getModule("utils/AppInit"),
FileSystem = brackets.getModule("filesystem/FileSystem"),
QuickOpen = brackets.getModule("search/QuickOpen"),
PathUtils = brackets.getModule("thirdparty/path-utils/path-utils"),
CommandManager = brackets.getModule("command/CommandManager"),
Commands = brackets.getModule("command/Commands"),
RemoteFile = require("RemoteFile");

var HTTP_PROTOCOL = "http:",
HTTPS_PROTOCOL = "https:";

AppInit.htmlReady(function () {
var protocolAdapter = {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is an extension shouldn't the definition of ProtocolAdapter be provided by Brackets-Core for consistency.

Copy link
Collaborator Author

@swmitra swmitra Apr 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vickramdhawal Sorry for the confusion. protocolAdapter is just a variable to hold the custom imprint of the adapter definition to be passed as protocol adapters for HTTP: and HTTPS:. Registration happens by invoking this part -

// Register the custom object as HTTP and HTTPS protocol adapter
FileSystem.registerProtocolAdapter(HTTP_PROTOCOL, protocolAdapter);
FileSystem.registerProtocolAdapter(HTTPS_PROTOCOL, protocolAdapter);

I can't really define an interface (which if was possible, would have been part of core) and Duck Typing would be a gimmick as type checking is not something what we are interested about here.

For better understanding I have added a JSDoc @typedef

/**
 * Typical signature of a file protocol adapter.
 * @typedef {Object} FileProtocol~Adapter
 * @property {Number} priority - Indicates the priority.
 * @property {Object} fileImpl - Handle for the custom file implementation prototype.
 * @property {function} canRead - To check if this impl can read a file for a given path.
 */

/**
 * FileSystem hook to register file protocol adapter
 * @param {string} protocol ex: "https:"|"http:"|"ftp:"|"file:"
 * @param {...FileProtocol~Adapter} adapter wrapper over file implementation
 */

priority: 0, // Default priority
fileImpl: RemoteFile,
canRead: function (filePath) {
return true; // Always claim true, we are the default adpaters
}
};

QuickOpen.addQuickOpenPlugin(
{
name: "Remote file URI input",
languageIds: [], // for all language modes
search: function () {
return $.Deferred().resolve([arguments[0]]);
},
match: function (query) {
var protocol = PathUtils.parseUrl(query).protocol;
return [HTTP_PROTOCOL, HTTPS_PROTOCOL].indexOf(protocol) !== -1;
},
itemFocus: function (query) {
}, // no op
itemSelect: function () {
CommandManager.execute(Commands.FILE_OPEN, {fullPath: arguments[0]});
}
}
);

FileSystem.registerProtocolAdapter(HTTP_PROTOCOL, protocolAdapter);
FileSystem.registerProtocolAdapter(HTTPS_PROTOCOL, protocolAdapter);
});

});
Loading