Skip to content

Commit

Permalink
Add File API to SDK (1.2.0)
Browse files Browse the repository at this point in the history
* Add method for downloading and storing files
  • Loading branch information
cbetta authored Jan 31, 2017
2 parents 2737c0c + f78319f commit c5c57ca
Show file tree
Hide file tree
Showing 8 changed files with 218 additions and 11 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,23 @@ nexmo.calls.dtmf.send(callId, params, callback);

For more information see https://docs.nexmo.com/voice/voice-api/api-reference#dtmf_put


## Files

For detailed information please see the documentation at https://docs.nexmo.com/voice/voice-api/recordings

### Get a file (recording)

```js
nexmo.files.get(fileIdOrUrl, callback);
```

### Save a file (recording)

```js
nexmo.files.save(fileIdOrUrl, file, callback);
```

## Verify

### Submit a Verification Request
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "nexmo",
"author": "nexmo",
"version": "1.1.2",
"version": "1.2.0",
"main": "lib/Nexmo",
"keywords": [
"sms",
Expand Down
81 changes: 81 additions & 0 deletions src/FilesResource.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"use strict";

var fs = require('fs');

class FilesResource {

/**
* The path to the `calls` resource.
*/
static get PATH() {
return '/v1/files';
}

/**
* Creates a new FilesResource.
*
* @param {Credentials} creds - Credentials used when interacting with the Nexmo API.
* @param {Object} options - additional options for the class.
*/
constructor(creds, options) {
this.creds = creds;
this.options = options;
}

/**
* Get stream for a remote File
*
* @param {string} [fileIdOrUrl] - The unique identifier or URL for the file
* @param {function} callback - function to be called when the request completes.
*/
get(fileIdOrUrl, callback) {

if(!fileIdOrUrl) {
throw new Error('"fileIdOrUrl" is a required parameter');
}

fileIdOrUrl = fileIdOrUrl.split("/").pop(-1);

var config = {
host:'api.nexmo.com',
path:`${FilesResource.PATH}/${fileIdOrUrl}`,
method: 'GET',
headers: {
'Content-Type': 'application/octet-stream',
'Authorization': `Bearer ${this.creds.generateJwt()}`
}
};

this.options.httpClient.request(config, callback);
}

/**
* Save remote File locally
*
* @param {string} [fileIdOrUrl] - The unique identifier or URL for the file
* @param {string} [file] - Filename or file descriptor
* @param {function} callback - function to be called when the request completes.
*/
save(fileIdOrUrl, file, callback) {
this.get(fileIdOrUrl, (error, data) => {
if (error) {
callback(error, null);
} else {
this.__storeFile(data, file, callback);
}
})
}

__storeFile(data, file, callback) {
fs.writeFile(file, data, (error) => {
if (error) {
callback(error, null);
} else {
callback(null, file);
}
});
}

}

export default FilesResource;
26 changes: 21 additions & 5 deletions src/HttpClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,16 +59,30 @@ class HttpClient {

request.end(endpoint.body);

var responseData = '';
// Keep an array of String or Buffers,
// depending on content type (binary or JSON) of response
var responseData = [];

request.on('response', (response) => {
response.setEncoding('utf8');
var isBinary = response.headers['content-type'] === 'application/octet-stream';
if (!isBinary) { response.setEncoding('utf8'); }

response.on('data', (chunk) => {
responseData += chunk;
responseData.push(chunk);
});

response.on('end', () => {
this.logger.info('response ended:', response.statusCode);
if (callback) {
this.__parseReponse(response.statusCode, responseData, method, callback)
if (isBinary) { responseData = Buffer.concat(responseData); }

this.__parseReponse(
response.status,
response.headers['content-type'],
responseData,
method,
callback
);
}
})
response.on('close', (e) => {
Expand All @@ -85,7 +99,7 @@ class HttpClient {

}

__parseReponse(status, data, method, callback) {
__parseReponse(status, contentType, data, method, callback) {
var response = null;
var error = null;

Expand All @@ -94,6 +108,8 @@ class HttpClient {
error = { message: 'Server Error: '+status };
} else if (status >= 400 || status < 200) {
error = JSON.parse(data);
} else if (contentType === 'application/octet-stream') {
response = data;
} else if (method !== 'DELETE') {
response = JSON.parse(data);
} else {
Expand Down
2 changes: 2 additions & 0 deletions src/Nexmo.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import NumberInsight from './NumberInsight';
import App from './App';
import Account from './Account';
import CallsResource from './CallsResource';
import FilesResource from './FilesResource';
import HttpClient from './HttpClient';
import NullLogger from './NullLogger';
import ConsoleLogger from './ConsoleLogger';
Expand Down Expand Up @@ -64,6 +65,7 @@ class Nexmo {
this.applications = new App(this.credentials, this.options);
this.account = new Account(this.credentials, this.options);
this.calls = new CallsResource(this.credentials, this.options);
this.files = new FilesResource(this.credentials, this.options);

/**
* @deprecated Please use nexmo.applications
Expand Down
76 changes: 76 additions & 0 deletions test/FilesResource-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import chai, {
expect
} from 'chai';
import sinon from 'sinon';
import sinonChai from 'sinon-chai';

chai.use(sinonChai);

import ResourceTestHelper from './ResourceTestHelper';

import FilesResource from '../lib/FilesResource';
import HttpClient from '../lib/HttpClient';
import Credentials from '../lib/Credentials';

var creds = Credentials.parse({
applicationId: 'some-id',
privateKey: __dirname + '/private-test.key'
});
var emptyCallback = () => {};

describe('FileResource', () => {

var httpClientStub = null;
var files = null;

beforeEach(() => {
httpClientStub = sinon.createStubInstance(HttpClient);
var options = {
httpClient: httpClientStub
};
files = new FilesResource(creds, options);
});

it('should get a single file using a file ID', () => {
const fileId = '2342342-lkjhlkjh-32423';
files.get(fileId, emptyCallback);

var expectedRequestArgs = ResourceTestHelper.requestArgsMatch(null, {
method: 'GET',
body: undefined,
path: `${FilesResource.PATH}/${fileId}`,
headers: {
'Content-Type': 'application/octet-stream',
'Authorization': 'Bearer '
}
});

expect(httpClientStub.request)
.to.have.been.calledWith(
sinon.match(expectedRequestArgs),
emptyCallback
);
});

it('should get a single file using a file URL', () => {
const fileId = '2342342-lkjhlkjh-32423';
const fileUrl = `https://rest.nexmo.com/api/v1/files/${fileId}`;
files.get(fileUrl, emptyCallback);

var expectedRequestArgs = ResourceTestHelper.requestArgsMatch(null, {
method: 'GET',
body: undefined,
path: `${FilesResource.PATH}/${fileId}`,
headers: {
'Content-Type': 'application/octet-stream',
'Authorization': 'Bearer '
}
});

expect(httpClientStub.request)
.to.have.been.calledWith(
sinon.match(expectedRequestArgs),
emptyCallback
);
});
});
17 changes: 12 additions & 5 deletions test/HttpClient-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -257,33 +257,40 @@ describe('parseResponse', function() {

it ('should parse a 500+ status code as an error', function() {
var callback = sinon.spy();
client.__parseReponse(504, '', 'GET', callback);
client.__parseReponse(504, 'application/json', '', 'GET', callback);
expect(callback).was.calledWith({ message: 'Server Error: 504' }, null);
});

it ('should parse a 400-499 status code as a JSON error', function() {
var callback = sinon.spy();
client.__parseReponse(404, '{ "error" : "error" }', 'GET', callback);
client.__parseReponse(404, 'application/json', '{ "error" : "error" }', 'GET', callback);
expect(callback).was.calledWith({ 'error' : 'error' }, null);
});

it ('should parse a 200-299 status code as a JSON object', function() {
var callback = sinon.spy();
client.__parseReponse(201, '{ "data" : "data" }', 'GET', callback);
client.__parseReponse(201, 'application/json', '{ "data" : "data" }', 'GET', callback);
expect(callback).was.calledWith(null, { 'data' : 'data' });
});

it ('should not try and parse successful DELETE request to JSON', function() {
var callback = sinon.spy();
client.__parseReponse(201, '', 'DELETE', callback);
client.__parseReponse(201, 'application/json', '', 'DELETE', callback);
expect(callback).was.calledWith(null, '');
});

it ('should catch invalid json', function() {
var callback = sinon.spy();
client.__parseReponse(201, 'not_json', 'GET', callback);
client.__parseReponse(201, 'application/json', 'not_json', 'GET', callback);
expect(callback).was.calledWith(sinon.match({
message: 'The API response could not be parsed.'
}), null);
});

it ('should parse binary data', function() {
var callback = sinon.spy();
var data = new Buffer('data');
client.__parseReponse(200, 'application/octet-stream', data, 'GET', callback);
expect(callback).was.calledWith(null, data);
});
});
8 changes: 8 additions & 0 deletions test/Nexmo-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,14 @@ describe('Nexmo Object instance', function() {
expect(nexmo.calls).to.be.an.instanceOf(CallsResource);
});

it('should expose a files object', function() {
var nexmo = new Nexmo({
apiKey: 'test',
apiSecret: 'test'
});
expect(nexmo.files).to.be.a('object');
});

it('should allow options to be passed', function() {
var initializedSpy = sinon.spy();
var options = {
Expand Down

0 comments on commit c5c57ca

Please sign in to comment.