@@ -203,25 +311,119 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/box/box.js b/box/box.js
index 74280442..72fe5370 100644
--- a/box/box.js
+++ b/box/box.js
@@ -14,538 +14,20 @@
* limitations under the License.
**/
-module.exports = function(RED) {
- "use strict";
- var crypto = require("crypto");
- var fs = require("fs");
- var request = require("request");
- var url = require("url");
- var minimatch = require("minimatch");
-
- function BoxNode(n) {
- RED.nodes.createNode(this,n);
- }
- RED.nodes.registerType("box-credentials", BoxNode, {
- credentials: {
- displayName: {type:"text"},
- clientId: {type:"text"},
- clientSecret: {type:"password"},
- accessToken: {type:"password"},
- refreshToken: {type:"password"},
- expireTime: {type:"password"}
- }
- });
-
- BoxNode.prototype.refreshToken = function(cb) {
- var credentials = this.credentials;
- var node = this;
- //console.log("refreshing token: " + credentials.refreshToken);
- if (!credentials.refreshToken) {
- // TODO: add a timeout to make sure we make a request
- // every so often (if no flows trigger one) to ensure the
- // refresh token does not expire
- node.error(RED._("box.error.no-refresh-token"));
- return cb(RED._("box.error.no-refresh-token"));
- }
- request.post({
- url: 'https://api.box.com/oauth2/token',
- json: true,
- form: {
- grant_type: 'refresh_token',
- client_id: credentials.clientId,
- client_secret: credentials.clientSecret,
- refresh_token: credentials.refreshToken,
- },
- }, function(err, result, data) {
- if (err) {
- node.error(RED._("box.error.token-request-error",{err:err}));
- return;
- }
- if (data.error) {
- node.error(RED._("box.error.refresh-token-error",{message:data.error.message}));
- return;
- }
- // console.log("refreshed: " + require('util').inspect(data));
- credentials.accessToken = data.access_token;
- if (data.refresh_token) {
- credentials.refreshToken = data.refresh_token;
- }
- credentials.expiresIn = data.expires_in;
- credentials.expireTime =
- data.expires_in + (new Date().getTime()/1000);
- credentials.tokenType = data.token_type;
- RED.nodes.addCredentials(node.id, credentials);
- if (typeof cb !== undefined) {
- cb();
- }
- });
- };
-
- BoxNode.prototype.request = function(req, retries, cb) {
- var node = this;
- if (typeof retries === 'function') {
- cb = retries;
- retries = 1;
- }
- if (typeof req !== 'object') {
- req = { url: req };
- }
- req.method = req.method || 'GET';
- if (!req.hasOwnProperty("json")) {
- req.json = true;
- }
- // always set access token to the latest ignoring any already present
- req.auth = { bearer: this.credentials.accessToken };
- if (!this.credentials.expireTime ||
- this.credentials.expireTime < (new Date().getTime()/1000)) {
- if (retries === 0) {
- node.error(RED._("box.error.too-many-refresh-attempts"));
- cb(RED._("box.error.too-many-refresh-attempts"));
- return;
- }
- node.warn(RED._("box.warn.refresh-token"));
- node.refreshToken(function (err) {
- if (err) {
- return;
- }
- node.request(req, 0, cb);
- });
- return;
- }
- return request(req, function(err, result, data) {
- if (err) {
- // handled in callback
- return cb(err, data);
- }
- if (result.statusCode === 401 && retries > 0) {
- retries--;
- node.warn(RED._("box.warn.refresh-401"));
- node.refreshToken(function (err) {
- if (err) {
- return cb(err, null);
- }
- return node.request(req, retries, cb);
- });
- }
- if (result.statusCode >= 400) {
- return cb(result.statusCode + ": " + data.message, data);
- }
- return cb(err, data);
- });
- };
-
- BoxNode.prototype.folderInfo = function(parent_id, cb) {
- this.request('https://api.box.com/2.0/folders/'+parent_id, cb);
- };
-
- BoxNode.prototype.resolvePath = function(path, parent_id, cb) {
- var node = this;
- if (typeof parent_id === 'function') {
- cb = parent_id;
- parent_id = 0;
- }
- if (typeof path === "string") {
- // split path and remove empty string components
- path = path.split("/").filter(function(e) { return e !== ""; });
- // TODO: could also handle '/blah/../' and '/./' perhaps
- } else {
- path = path.filter(function(e) { return e !== ""; });
- }
- if (path.length === 0) {
- return cb(null, parent_id);
- }
- var folder = path.shift();
- node.folderInfo(parent_id, function(err, data) {
- if (err) {
- return cb(err, -1);
- }
- var entries = data.item_collection.entries;
- for (var i = 0; i < entries.length; i++) {
- if (entries[i].type === 'folder' &&
- entries[i].name === folder) {
- // found
- return node.resolvePath(path, entries[i].id, cb);
- }
- }
- return cb(RED._("box.error.not-found"), -1);
- });
- };
-
- BoxNode.prototype.resolveFile = function(path, parent_id, cb) {
- var node = this;
- if (typeof parent_id === 'function') {
- cb = parent_id;
- parent_id = 0;
- }
- if (typeof path === "string") {
- // split path and remove empty string components
- path = path.split("/").filter(function(e) { return e !== ""; });
- // TODO: could also handle '/blah/../' and '/./' perhaps
- } else {
- path = path.filter(function(e) { return e !== ""; });
- }
- if (path.length === 0) {
- return cb(RED._("box.error.missing-filename"), -1);
- }
- var file = path.pop();
- node.resolvePath(path, function(err, parent_id) {
- if (err) {
- return cb(err, parent_id);
- }
- node.folderInfo(parent_id, function(err, data) {
- if (err) {
- return cb(err, -1);
- }
- var entries = data.item_collection.entries;
- for (var i = 0; i < entries.length; i++) {
- if (entries[i].type === 'file' &&
- entries[i].name === file) {
- // found
- return cb(null, entries[i].id);
- }
- }
- return cb(RED._("box.error.not-found"), -1);
- });
- });
- };
-
- function constructFullPath(entry) {
- var parentPath = entry.path_collection.entries
- .filter(function (e) { return e.id !== "0"; })
- .map(function (e) { return e.name; })
- .join('/');
- return (parentPath !== "" ? parentPath+'/' : "") + entry.name;
- }
-
- RED.httpAdmin.get('/box-credentials/auth', function(req, res) {
- if (!req.query.clientId || !req.query.clientSecret ||
- !req.query.id || !req.query.callback) {
- res.send(400);
- return;
- }
- var node_id = req.query.id;
- var callback = req.query.callback;
- var credentials = {
- clientId: req.query.clientId,
- clientSecret: req.query.clientSecret
- };
-
- var csrfToken = crypto.randomBytes(18).toString('base64').replace(/\//g, '-').replace(/\+/g, '_');
- credentials.csrfToken = csrfToken;
- credentials.callback = callback;
- res.cookie('csrf', csrfToken);
- res.redirect(url.format({
- protocol: 'https',
- hostname: 'app.box.com',
- pathname: '/api/oauth2/authorize',
- query: {
- response_type: 'code',
- client_id: credentials.clientId,
- state: node_id + ":" + csrfToken,
- redirect_uri: callback
- }
- }));
- RED.nodes.addCredentials(node_id, credentials);
- });
-
- RED.httpAdmin.get('/box-credentials/auth/callback', function(req, res) {
- if (req.query.error) {
- return res.send('ERROR: '+ req.query.error + ': ' + req.query.error_description);
- }
- var state = req.query.state.split(':');
- var node_id = state[0];
- var credentials = RED.nodes.getCredentials(node_id);
- if (!credentials || !credentials.clientId || !credentials.clientSecret) {
- return res.send(RED._("box.error.no-credentials"));
- }
- if (state[1] !== credentials.csrfToken) {
- return res.status(401).send(
- RED._("box.error.token-mismatch")
- );
- }
-
- request.post({
- url: 'https://app.box.com/api/oauth2/token',
- json: true,
- form: {
- grant_type: 'authorization_code',
- code: req.query.code,
- client_id: credentials.clientId,
- client_secret: credentials.clientSecret,
- redirect_uri: credentials.callback,
- },
- }, function(err, result, data) {
- if (err) {
- console.log("request error:" + err);
- return res.send(RED._("box.error.something-broke"));
- }
- if (data.error) {
- console.log("oauth error: " + data.error);
- return res.send(RED._("box.error.something-broke"));
- }
- //console.log("data: " + require('util').inspect(data));
- credentials.accessToken = data.access_token;
- credentials.refreshToken = data.refresh_token;
- credentials.expiresIn = data.expires_in;
- credentials.expireTime = data.expires_in + (new Date().getTime()/1000);
- credentials.tokenType = data.token_type;
- delete credentials.csrfToken;
- delete credentials.callback;
- RED.nodes.addCredentials(node_id, credentials);
- request.get({
- url: 'https://api.box.com/2.0/users/me',
- json: true,
- auth: { bearer: credentials.accessToken },
- }, function(err, result, data) {
- if (err) {
- console.log('fetching box profile failed: ' + err);
- return res.send(RED._("box.error.profile-fetch-failed"));
- }
- if (result.statusCode >= 400) {
- console.log('fetching box profile failed: ' +
- result.statusCode + ": " + data.message);
- return res.send(RED._("box.error.profile-fetch-failed"));
- }
- if (!data.name) {
- console.log('fetching box profile failed: no name found');
- return res.send(RED._("box.error.profile-fetch-failed"));
- }
- credentials.displayName = data.name;
- RED.nodes.addCredentials(node_id, credentials);
- res.send(RED._("box.error.authorized"));
- });
- });
- });
-
- function BoxInNode(n) {
- RED.nodes.createNode(this,n);
- this.filepattern = n.filepattern || "";
- this.box = RED.nodes.getNode(n.box);
- var node = this;
- if (!this.box || !this.box.credentials.accessToken) {
- this.warn(RED._("box.warn.missing-credentials"));
- return;
- }
- node.status({fill:"blue",shape:"dot",text:"box.status.initializing"});
- this.box.request({
- url: 'https://api.box.com/2.0/events?stream_position=now&stream_type=changes',
- }, function (err, data) {
- if (err) {
- node.error(RED._("box.error.event-stream-initialize-failed",{err:err.toString()}));
- node.status({fill:"red",shape:"ring",text:"box.status.failed"});
- return;
- }
- node.state = data.next_stream_position;
- node.status({});
- node.on("input", function(msg) {
- node.status({fill:"blue",shape:"dot",text:"box.status.checking-for-events"});
- node.box.request({
- url: 'https://api.box.com/2.0/events?stream_position='+node.state+'&stream_type=changes',
- }, function(err, data) {
- if (err) {
- node.error(RED._("box.error.events-fetch-failed",{err:err.toString()}),msg);
- node.status({});
- return;
- }
- node.status({});
- node.state = data.next_stream_position;
- for (var i = 0; i < data.entries.length; i++) {
- // TODO: support other event types
- // TODO: suppress duplicate events
- // for both of the above see:
- // https://developers.box.com/docs/#events
- var event;
- if (data.entries[i].event_type === 'ITEM_CREATE') {
- event = 'add';
- } else if (data.entries[i].event_type === 'ITEM_UPLOAD') {
- event = 'add';
- } else if (data.entries[i].event_type === 'ITEM_RENAME') {
- event = 'add';
- // TODO: emit delete event?
- } else if (data.entries[i].event_type === 'ITEM_TRASH') {
- // need to find old path
- node.lookupOldPath({}, data.entries[i], 'delete');
- /* strictly speaking the {} argument above should
- * be clone(msg) but:
- * - it must be {}
- * - if there was any possibility of a different
- * msg then it should be cloned using the
- * node-red/red/nodes/Node.js cloning function
- */
- continue;
- } else {
- event = 'unknown';
- }
- //console.log(JSON.stringify(data.entries[i], null, 2));
- node.sendEvent(msg, data.entries[i], event);
- }
- });
- });
- var interval = setInterval(function() {
- node.emit("input", {});
- }, 600000); // 10 minutes
- node.on("close", function() {
- if (interval !== null) { clearInterval(interval); }
- });
- });
- }
- RED.nodes.registerType("box in", BoxInNode);
-
- BoxInNode.prototype.sendEvent = function(msg, entry, event, path) {
- var source = entry.source;
- if (typeof path === "undefined") {
- path = constructFullPath(source);
- }
- if (this.filepattern && !minimatch(path, this.filepattern)) {
- return;
- }
- msg.file = source.name;
- msg.payload = path;
- msg.event = event;
- msg.data = entry;
- this.send(msg);
- };
-
- BoxInNode.prototype.lookupOldPath = function (msg, entry, event) {
- var source = entry.source;
- this.status({fill:"blue",shape:"dot",text:"box.status.resolving-path"});
- var node = this;
- node.box.folderInfo(source.parent.id, function(err, folder) {
- if (err) {
- node.warn(RED._("box.warn.old-path-failed",{err:err.toString()}));
- node.status({fill:"red",shape:"ring",text:"box.status.failed"});
- return;
- }
- node.status({});
- // TODO: add folder path_collection to entry.parent?
- var parentPath = constructFullPath(folder);
- node.sendEvent(msg, entry, event,
- (parentPath !== "" ? parentPath + '/' : '') + source.name);
- });
- };
-
- function BoxQueryNode(n) {
- RED.nodes.createNode(this,n);
- this.filename = n.filename || "";
- this.box = RED.nodes.getNode(n.box);
- var node = this;
- if (!this.box || !this.box.credentials.accessToken) {
- this.warn(RED._("box.warn.missing-credentials"));
- return;
- }
-
- node.on("input", function(msg) {
- var filename = node.filename || msg.filename;
- if (filename === "") {
- node.error(RED._("box.error.no-filename-specified"));
- return;
- }
- msg.filename = filename;
- node.status({fill:"blue",shape:"dot",text:"box.status.resolving-path"});
- node.box.resolveFile(filename, function(err, file_id) {
- if (err) {
- node.error(RED._("box.error.path-resolve-failed",{err:err.toString()}),msg);
- node.status({fill:"red",shape:"ring",text:"box.status.failed"});
- return;
- }
- node.status({fill:"blue",shape:"dot",text:"box.status.downloading"});
- node.box.request({
- url: 'https://api.box.com/2.0/files/'+file_id+'/content',
- json: false,
- followRedirect: true,
- maxRedirects: 1,
- encoding: null,
- }, function(err, data) {
- if (err) {
- node.error(RED._("box.error.download-failed",{err:err.toString()}),msg);
- node.status({fill:"red",shape:"ring",text:"box.status.failed"});
- } else {
- msg.payload = data;
- delete msg.error;
- node.status({});
- node.send(msg);
- }
- });
- });
- });
- }
- RED.nodes.registerType("box", BoxQueryNode);
-
- function BoxOutNode(n) {
- RED.nodes.createNode(this,n);
- this.filename = n.filename || "";
- this.localFilename = n.localFilename || "";
- this.box = RED.nodes.getNode(n.box);
- var node = this;
- if (!this.box || !this.box.credentials.accessToken) {
- this.warn(RED._("box.warn.missing-credentials"));
- return;
- }
-
- node.on("input", function(msg) {
- var filename = node.filename || msg.filename;
- if (filename === "") {
- node.error(RED._("box.error.no-filename-specified"));
- return;
- }
- var path = filename.split("/");
- var basename = path.pop();
- node.status({fill:"blue",shape:"dot",text:"box.status.resolving-path"});
- var localFilename = node.localFilename || msg.localFilename;
- if (!localFilename && typeof msg.payload === "undefined") {
- return;
- }
- node.box.resolvePath(path, function(err, parent_id) {
- if (err) {
- node.error(RED._("box.error.path-resolve-failed",{err:err.toString()}),msg);
- node.status({fill:"red",shape:"ring",text:"box.status.failed"});
- return;
- }
- node.status({fill:"blue",shape:"dot",text:"box.status.uploading"});
- var r = node.box.request({
- method: 'POST',
- url: 'https://upload.box.com/api/2.0/files/content',
- }, function(err, data) {
- if (err) {
- if (data && data.status === 409 &&
- data.context_info && data.context_info.conflicts) {
- // existing file, attempt to overwrite it
- node.status({fill:"blue",shape:"dot",text:"box.status.overwriting"});
- var r = node.box.request({
- method: 'POST',
- url: 'https://upload.box.com/api/2.0/files/'+
- data.context_info.conflicts.id+'/content',
- }, function(err, data) {
- if (err) {
- node.error(RED._("box.error.upload-failed",{err:err.toString()}),msg);
- node.status({fill:"red",shape:"ring",text:"box.status.failed"});
- return;
- }
- node.status({});
- });
- var form = r.form();
- if (localFilename) {
- form.append('filename', fs.createReadStream(localFilename), { filename: basename });
- } else {
- form.append('filename', RED.util.ensureBuffer(msg.payload), { filename: basename });
- }
- } else {
- node.error(RED._("box.error.upload-failed",{err:err.toString()}),msg);
- node.status({fill:"red",shape:"ring",text:"box.status.failed"});
- }
- return;
- }
- node.status({});
- });
- var form = r.form();
- if (localFilename) {
- form.append('filename', fs.createReadStream(localFilename), { filename: basename });
- } else {
- form.append('filename', RED.util.ensureBuffer(msg.payload), { filename: basename });
- }
- form.append('parent_id', parent_id);
- });
- });
- }
- RED.nodes.registerType("box out",BoxOutNode);
+'use strict';
+
+const initAPINode = require('./lib/box-api');
+const initDownloadNode = require('./lib/box-download');
+const initEventNode = require('./lib/box-event');
+const initUploadNode = require('./lib/box-upload');
+const initItemsNode = require('./lib/box-items');
+const initFileInfoNode = require('./lib/box-set-file-info');
+
+module.exports = RED => {
+ initAPINode(RED);
+ initDownloadNode(RED);
+ initEventNode(RED);
+ initUploadNode(RED);
+ initItemsNode(RED);
+ initFileInfoNode(RED);
};
diff --git a/box/lib/box-api.js b/box/lib/box-api.js
new file mode 100644
index 00000000..579fb9e3
--- /dev/null
+++ b/box/lib/box-api.js
@@ -0,0 +1,666 @@
+/**
+ * Copyright 2014 IBM Corp.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ **/
+
+'use strict';
+
+// note the confusing naming
+const BoxSDKNode = require('box-node-sdk');
+const crypto = require('crypto');
+const url = require('url');
+const fs = require('fs');
+const path = require('path');
+
+module.exports = RED => {
+
+ /**
+ * Consumes a readable stream as a UTF-8 string;
+ * resolves Promise when stream ends.
+ * @param {ReadableStream} stream
+ * @returns Promise
Stream data
+ */
+ const streamToPromise = stream => {
+ let content = '';
+ return new Promise((resolve, reject) => {
+ stream.on('data', chunk => {
+ content += chunk;
+ })
+ .on('end', () => {
+ resolve(content);
+ })
+ .on('error', err => {
+ reject(err);
+ });
+ });
+ };
+
+ const normalizeFilepath = filepath =>
+ (typeof filepath === "string" ? filepath.split("/").filter(Boolean) : filepath.filter(Boolean));
+
+ /**
+ * Provides an adapter for a Box SDK persistent client to store its tokens
+ * in Node-RED.
+ * For our purposes, this is the entirety of the Node's `credentials` object.
+ * This class is *not* used directly by the Nodes.
+ */
+ class TokenStore {
+ /**
+ * Assigns this TokenStore a Node ID
+ * @param {string} id Node ID to associate TokenStore with
+ */
+ constructor(id) {
+ this.id = id;
+ }
+
+ /**
+ * Reads the token store
+ * @param {Function} cb Nodeback
+ */
+ read(cb) {
+ const id = this.id;
+ // this is only here to force the callback to be called async
+ process.nextTick(() => {
+ cb(null, RED.nodes.getCredentials(id));
+ });
+ }
+
+ /**
+ * Writes "Token Info" to the token store
+ * @param {Object} tokenInfo "Token Info" object
+ * @param {Function} cb Nodeback
+ */
+ write(tokenInfo, cb) {
+ const id = this.id;
+ const credentials = RED.nodes.getCredentials(id);
+ Object.assign(credentials, tokenInfo);
+ RED.nodes.addCredentials(id, credentials)
+ .then(() => cb(), cb);
+ }
+
+ /**
+ * Annihilates the contents of the token store
+ * @param {Function} cb Nodeback
+ */
+ clear(cb) {
+ this.write(null, cb);
+ }
+ }
+
+ /**
+ * Collection of mixins for difft behavior of difft auth strategies.
+ */
+ const AuthModeMixins = {
+ OAUTH2: {
+ /**
+ * `true` if the credentials are in place
+ * @this BoxAPINode
+ * @returns {boolean} Credentials OK
+ */
+ _hasCredentials() {
+ const c = this.credentials;
+ return c.clientSecret && c.clientId && c.accessToken &&
+ c.refreshToken && c.accessTokenTTLMS && c.acquiredAtMS;
+ },
+
+ /**
+ * A Box SDK client, created as per the auth mode.
+ * @returns {BoxClient}
+ */
+ _client() {
+ if (this.__client) {
+ return this.__client;
+ }
+ return this.sdk.getPersistentClient({
+ accessToken: this.credentials.accessToken,
+ refreshToken: this.credentials.refreshToken,
+ acquiredAtMS: this.credentials.acquiredAtMS,
+ accessTokenTTLMS: this.credentials.accessTokenTTLMS
+ }, this.tokenStore);
+ },
+
+ /**
+ * A Box SDK instance, created as per the auth mode.
+ * @returns {BoxSDKNode}
+ */
+ _sdk() {
+ if (this.__sdk) {
+ return this.__sdk;
+ }
+ this.__sdk = new BoxSDKNode({
+ clientID: this.credentials.clientId,
+ clientSecret: this.credentials.clientSecret
+ });
+ return this.__sdk;
+ },
+
+ /**
+ * Gets an event stream
+ * @param {Object} [options={}] Options
+ * @param {number} [options.interval=0] Fetch interval, in seconds
+ * @returns {Promise} Readable stream
+ */
+ eventStream(options) {
+ return this.client.events.getEventStream({
+ // this is milliseconds. handy!
+ fetchInterval: (options.interval || 0) * 1000
+ });
+ }
+ },
+ APP: {
+ /**
+ * `true` if the credentials are in place
+ * @this BoxAPINode
+ * @returns {boolean} Credentials OK
+ */
+ _hasCredentials() {
+ const c = this.credentials;
+ return c.clientId && c.clientSecret && c.publicKeyId && c.privateKey &&
+ c.passphrase && c.appEnterpriseId;
+ },
+ /**
+ * A Box SDK client, created as per the app mode.
+ * If `appUserId` is present, return an "app user" client; otherwise
+ * an enterprise one.
+ * @returns {BoxClient}
+ */
+ _client() {
+ if (this.__client) {
+ return this.__client;
+ }
+
+ if (this.credentials.appUserId) {
+ this.__client = this.sdk.getAppAuthClient('user', this.credentials.appUserId);
+ this.debug(`Authenticating as app user ${this.credentials.appUserId}`);
+ } else {
+ this.__client = this.sdk.getAppAuthClient('enterprise', this.credentials.appEnterpriseId);
+ this.debug('Authenticating as service user');
+ }
+
+ return this.__client;
+ },
+
+ /**
+ * A Box SDK instance, created as per the app mode
+ * @returns {BoxSDKNode}
+ */
+ _sdk() {
+ if (this.__sdk) {
+ return this.__sdk;
+ }
+ this.__sdk = new BoxSDKNode({
+ clientID: this.credentials.clientId,
+ clientSecret: this.credentials.clientSecret,
+ appAuth: {
+ keyID: this.credentials.publicKeyId,
+ privateKey: this.credentials.privateKey,
+ passphrase: this.credentials.passphrase
+ }
+ });
+ return this.__sdk;
+ },
+
+ /**
+ * Gets an enterprise event stream
+ * @param {Object} [options={}] Options
+ * @param {number} [options.interval=0] Polling interval, in seconds
+ * @returns {Promise} Readable stream
+ */
+ eventStream(options) {
+ return this.credentials.appUserId ? AuthModeMixins.OAUTH2.eventStream.call(this, options) :
+ this.client.events.getEnterpriseEventStream({
+ // this is seconds
+ pollingInterval: options.interval
+ });
+ }
+ },
+ DEV: {
+ /**
+ * `true` if the credentials are in place
+ * @this BoxAPINode
+ * @returns {boolean} Credentials OK
+ */
+ _hasCredentials() {
+ const c = this.credentials;
+ return c.clientId && c.clientSecret && c.devToken;
+ },
+
+ /**
+ * @returns {BoxClient}
+ */
+ _client() {
+ return this.sdk.getBasicClient(this.credentials.devToken);
+ },
+
+ /**
+ * @returns {BoxSDKNode}
+ */
+ _sdk() {
+ return AuthModeMixins.OAUTH2._sdk.call(this);
+ },
+
+ eventStream() {
+ throw new Error('not implemented');
+ }
+ }
+ };
+
+ /**
+ * Represents an interface into the Box API; contains credentials.
+ * Requires a mixin applied from `AuthModeMixins` to work properly;
+ * the `authMode` property determines this.
+ * @class BoxAPINode
+ */
+ class BoxAPINode {
+ /**
+ * Creates an instance of BoxAPINode.
+ * @param {*} n
+ * @memberof BoxAPINode
+ */
+ constructor(n) {
+ RED.nodes.createNode(this, n);
+
+ this.authMode = n.authMode || 'OAUTH2';
+
+ if (!AuthModeMixins[this.authMode]) {
+ this.error('Invalid auth mode');
+ return;
+ }
+
+ Object.assign(this, AuthModeMixins[this.authMode]);
+ }
+
+ /**
+ * The TokenStore associated with this Node
+ * @type {TokenStore}
+ * @memberof BoxAPINode
+ */
+ get tokenStore() {
+ const tokenStore = this._tokenStore;
+ if (tokenStore) {
+ return tokenStore;
+ }
+ this._tokenStore = new TokenStore(this.id);
+ return this.tokenStore;
+ }
+
+ /**
+ * @type {BoxSDKNode}
+ * @memberof BoxAPINode
+ */
+ get sdk() {
+ return this._sdk();
+ }
+
+ /**
+ * @type {BoxClient}
+ * @memberof BoxAPINode
+ */
+ get client() {
+ return this._client();
+ }
+
+ /**
+ * @type {boolean}
+ * @memberof BoxAPINode
+ */
+ get hasCredentials() {
+ return this._hasCredentials();
+ }
+
+ /**
+ * Returns the ID of a folder in Box
+ * @param {string} folderpath A filepath
+ * @param {string} [folderId=0] Parent folder ID; defaults to root
+ * @returns {Promise} A folder ID
+ */
+ resolveFolder(folderpath, folderId) {
+ return Promise.resolve()
+ .then(() => {
+ folderId = folderId || '0';
+ folderpath = normalizeFilepath(folderpath);
+ if (!folderpath.length) {
+ return folderId;
+ }
+ const folder = folderpath.shift();
+ return this.client.folders.getItems(folderId)
+ .then(data => {
+ const entries = data.entries;
+ for (let i = 0; i < entries.length; i++) {
+ if (entries[i].type === 'folder' &&
+ entries[i].name === folder) {
+ // found
+ return this.resolveFolder(folderpath, entries[i].id);
+ }
+ }
+ return Promise.reject(RED._("box.error.not-found"));
+ });
+ });
+ }
+
+ /**
+ * Attaches a listener function to an event stream
+ * @param {Function} listener Listener function; receives event object
+ * @param {Object} [options={}] Options
+ * @param {number} [options.interval=0] Polling or fetch interval, in seconds
+ * @returns {Promise} An "unsubscribe" function
+ */
+ subscribe(listener, options) {
+ options = options || {};
+ const errListener = err => {
+ this.error(RED._('box.error.event-fetch-failed', {
+ err: err.toString()
+ }));
+ };
+ return this.eventStream(options)
+ .then(stream => {
+ stream.on('error', errListener)
+ .on('data', listener);
+
+ this.on('close', () => {
+ stream.removeAllListeners('data')
+ stream.removeAllListeners('error');
+ stream.destroy();
+ });
+
+ return () => {
+ stream.removeListener('data', listener);
+ stream.removeListener('error', errListener);
+ };
+ });
+ }
+
+ /**
+ * Finds a file's ID by filename
+ * @param {string} filename A filename
+ * @returns {Promise} File ID, if found
+ */
+ resolveFile(filename) {
+ return Promise.resolve()
+ .then(() => {
+ const filepath = normalizeFilepath(filename);
+ if (!filepath.length) {
+ return Promise.reject(RED._("box.error.missing-filename"));
+ }
+ const file = filepath.pop();
+ return this.resolveFolder(path.dirname(filename))
+ .then(id => this.client.folders.getItems(id))
+ .then(data => {
+ const entries = data.entries;
+ for (var i = 0; i < entries.length; i++) {
+ if (entries[i].type === 'file' &&
+ entries[i].name === file) {
+ // found
+ return entries[i].id;
+ }
+ }
+ return Promise.reject(RED._("box.error.not-found"));
+ });
+ });
+ }
+
+ /**
+ * Downloads a file, optionally coerced to a representation type.
+ * @param {string} filepath A filepath
+ * @param {FileRepresentationType} [representation] File representation type
+ * @returns {Promise} A Buffer or string of the file contents (or representation thereof)
+ */
+ download(filepath, representation) {
+ return Promise.resolve()
+ .then(() => {
+ if (representation && this.client.files.representation[representation]) {
+ return this.client.files.getRepresentationContent(
+ filepath, this.client.files.representation[representation]
+ );
+ }
+ // "raw"
+ return this.client.files.getReadStream(filepath);
+ })
+ .then(streamToPromise);
+ }
+
+ /**
+ * Uploads a new version of a file, performing a preflight check to ensure uploading will *likely* work.
+ * One of `opts.localFilename` or `opts.content` is required.
+ * @param {Object} opts Options
+ * @param {string} opts.fileId Box File ID
+ * @param {number} opts.size File size (bytes)
+ * @param {string} [opts.localFilename] Path to local (server) filename
+ * @param {string|Buffer} [opts.content] File content
+ * @private
+ * @returns Promise