Skip to content

Commit 6f4cca6

Browse files
mitjapMurderlonAcconut
authoredSep 26, 2022
Redesign stores for better separation of concerns (#186)
Co-authored-by: Merlijn Vos <merlijn@soverin.net> Co-authored-by: Marius <marius.kleidl@gmail.com>
1 parent e7669c7 commit 6f4cca6

30 files changed

+897
-911
lines changed
 

‎README.md

+24-31
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,14 @@ $ npm install tus-node-server
1515
- **Local File Storage**
1616
```js
1717
server.datastore = new tus.FileStore({
18-
path: '/files'
18+
directory: './files'
1919
});
2020
```
2121

2222
- **Google Cloud Storage**
2323
```js
2424
2525
server.datastore = new tus.GCSDataStore({
26-
path: '/files',
2726
projectId: 'project-id',
2827
keyFilename: 'path/to/your/keyfile.json',
2928
bucket: 'bucket-name',
@@ -34,13 +33,11 @@ $ npm install tus-node-server
3433
```js
3534
3635
server.datastore = new tus.S3Store({
37-
path: '/files',
3836
bucket: 'bucket-name',
3937
accessKeyId: 'access-key-id',
4038
secretAccessKey: 'secret-access-key',
4139
region: 'eu-west-1',
4240
partSize: 8 * 1024 * 1024, // each uploaded part will have ~8MB,
43-
tmpDirPrefix: 'tus-s3-store',
4441
});
4542
```
4643

@@ -56,10 +53,8 @@ $ docker run -p 1080:8080 -d bhstahl/tus-node-deploy
5653
```js
5754
const tus = require('tus-node-server');
5855
59-
const server = new tus.Server();
60-
server.datastore = new tus.FileStore({
61-
path: '/files'
62-
});
56+
const server = new tus.Server({ path: '/files' });
57+
server.datastore = new tus.FileStore({ directory: './files' });
6358
6459
const host = '127.0.0.1';
6560
const port = 1080;
@@ -72,10 +67,8 @@ server.listen({ host, port }, () => {
7267

7368
```js
7469
const tus = require('tus-node-server');
75-
const server = new tus.Server();
76-
server.datastore = new tus.FileStore({
77-
path: '/files'
78-
});
70+
const server = new tus.Server({ path: '/files' });
71+
server.datastore = new tus.FileStore({ directory: './files' });
7972

8073
const express = require('express');
8174
const app = express();
@@ -95,15 +88,14 @@ const http = require('http');
9588
const url = require('url');
9689
const Koa = require('koa')
9790
const tus = require('tus-node-server');
98-
const tusServer = new tus.Server();
91+
92+
const tusServer = new tus.Server({ path: '/files' });
93+
tusServer.datastore = new tus.FileStore({ directory: './files' });
9994

10095
const app = new Koa();
10196
const appCallback = app.callback();
10297
const port = 1080;
10398

104-
tusServer.datastore = new tus.FileStore({
105-
path: '/files',
106-
});
10799

108100
const server = http.createServer((req, res) => {
109101
const urlPath = url.parse(req.url).pathname;
@@ -123,15 +115,13 @@ server.listen(port)
123115

124116
```js
125117
const tus = require('tus-node-server');
126-
const tusServer = new tus.Server();
127-
tusServer.datastore = new tus.FileStore({
128-
path: '/files',
129-
});
118+
const tusServer = new tus.Server({ path: '/files' });
119+
tusServer.datastore = new tus.FileStore({ directory: './files' });
130120

131121
const fastify = require('fastify')({ logger: true });
132122

133123
/**
134-
* add new content-type to fastify forewards request
124+
* add new content-type to fastify forewards request
135125
* without any parser to leave body untouched
136126
* @see https://www.fastify.io/docs/latest/Reference/ContentTypeParser/
137127
*/
@@ -140,7 +130,7 @@ fastify.addContentTypeParser(
140130
);
141131

142132
/**
143-
* let tus handle preparation and filehandling requests
133+
* let tus handle preparation and filehandling requests
144134
* fastify exposes raw nodejs http req/res via .raw property
145135
* @see https://www.fastify.io/docs/latest/Reference/Request/
146136
* @see https://www.fastify.io/docs/latest/Reference/Reply/#raw
@@ -166,10 +156,12 @@ fastify.listen(3000, (err) => {
166156
Execute code when lifecycle events happen by adding event handlers to your server.
167157

168158
```js
169-
const Server = require('tus-node-server').Server;
159+
const tus = require('tus-node-server');
170160
const EVENTS = require('tus-node-server').EVENTS;
171161

172-
const server = new Server();
162+
const server = new tus.Server({ path: '/files' });
163+
server.datastore = new tus.FileStore({ directory: './files' });
164+
173165
server.on(EVENTS.EVENT_UPLOAD_COMPLETE, (event) => {
174166
console.log(`Upload complete for file ${event.file.id}`);
175167
});
@@ -209,24 +201,26 @@ server.on(EVENTS.EVENT_UPLOAD_COMPLETE, (event) => {
209201
}
210202
}
211203
```
212-
204+
213205
- `EVENT_FILE_DELETED`: Fired when a `DELETE` request finishes deleting the file
214206
215207
_Example payload:_
216208
```
217209
{
218210
file_id: '7b26bf4d22cf7198d3b3706bf0379794'
219-
211+
220212
}
221213
```
222214
223215
#### Custom `GET` handlers:
224216
Add custom `GET` handlers to suit your needs, similar to [Express routing](https://expressjs.com/en/guide/routing.html).
225217
```js
226-
const server = new Server();
218+
const server = new tus.Server({ path: '/files' });
219+
server.datastore = new tus.FileStore({ directory: './files' });
220+
227221
server.get('/uploads', (req, res) => {
228222
// Read from your DataStore
229-
fs.readdir(server.datastore.path, (err, files) => {
223+
fs.readdir(server.datastore.directory, (err, files) => {
230224
// Format the JSON response and send it
231225
}
232226
});
@@ -235,7 +229,6 @@ server.get('/uploads', (req, res) => {
235229
#### Custom file names:
236230

237231
The default naming of files is a random crypto hex string. When using your own `namingFunction`, make sure to create URL friendly names such as removing spaces.
238-
239232
```js
240233
const crypto = require('crypto');
241234

@@ -245,9 +238,9 @@ const randomString = (req) => {
245238
return crypto.randomBytes(16).toString('hex');
246239
}
247240

248-
server.datastore = new tus.FileStore({
241+
const server = new tus.Server({
249242
path: '/files',
250-
namingFunction: randomString
243+
namingFunction: randomString,
251244
});
252245
```
253246

‎demo/server.js

+4-6
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@ const GCSDataStore = require('../index').GCSDataStore;
1010
const S3Store = require('../index').S3Store;
1111
const EVENTS = require('../index').EVENTS;
1212

13-
const server = new Server();
13+
const options = { path: '/files' };
14+
15+
const server = new Server(options);
1416

1517
const data_store = process.env.DATA_STORE || 'FileStore';
1618

1719
switch (data_store) {
1820
case 'GCSDataStore':
1921
server.datastore = new GCSDataStore({
20-
path: '/files',
2122
projectId: 'vimeo-open-source',
2223
keyFilename: path.resolve(__dirname, '../keyfile.json'),
2324
bucket: 'tus-node-server',
@@ -31,7 +32,6 @@ switch (data_store) {
3132
assert.ok(process.env.AWS_REGION, 'environment variable `AWS_REGION` must be set');
3233

3334
server.datastore = new S3Store({
34-
path: '/files',
3535
bucket: process.env.AWS_BUCKET,
3636
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
3737
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
@@ -41,9 +41,7 @@ switch (data_store) {
4141
break;
4242

4343
default:
44-
server.datastore = new FileStore({
45-
path: '/files',
46-
});
44+
server.datastore = new FileStore({ directory: './files' });
4745
}
4846

4947
/**

‎index.d.ts

+41-24
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,27 @@ import { EventEmitter } from "events";
22
import * as fs from "fs";
33
import * as http from "http";
44

5+
declare interface Configstore {
6+
get(key: string) : Promise<IFile | undefined> | IFile | undefined;
7+
set(key: string, value: IFile) : Promise<void> | void;
8+
delete(key: string) : Promise<boolean> | boolean;
9+
}
10+
11+
declare interface ServerOptions {
12+
path: string;
13+
relativeLocation?: boolean;
14+
namingFunction?: () => string;
15+
}
16+
517
/**
618
* arguments of constructor which in class extend DataStore
719
*/
820
declare interface DataStoreOptions {
9-
path: string;
10-
namingFunction?: (req: http.IncomingMessage) => string;
11-
relativeLocation?: boolean;
1221
}
1322

1423
declare interface FileStoreOptions extends DataStoreOptions {
15-
directory?: string;
24+
directory: string;
25+
configstore?: Configstore;
1626
}
1727

1828
declare interface GCStoreOptions extends DataStoreOptions {
@@ -26,20 +36,22 @@ declare interface S3StoreOptions extends DataStoreOptions {
2636
secretAccessKey: string;
2737
bucket: string;
2838
region?: string;
29-
tmpDirPrefix: string;
3039
partSize: number;
3140
}
3241

33-
declare class File {
42+
declare interface IFile {
3443
id: string;
35-
upload_length: any;
36-
upload_defer_length: any;
37-
upload_metadata: any;
44+
upload_length: string;
45+
upload_defer_length: string;
46+
upload_metadata: string;
47+
}
48+
49+
declare class File implements IFile {
3850
constructor(
3951
file_id: string,
40-
upload_length: any,
41-
upload_defer_length: any,
42-
upload_metadata: any
52+
upload_length: string,
53+
upload_defer_length: string,
54+
upload_metadata: string
4355
);
4456
}
4557

@@ -48,24 +60,26 @@ declare class File {
4860
*/
4961
export declare class DataStore extends EventEmitter {
5062
constructor(options: DataStoreOptions);
51-
get extensions(): any;
52-
set extensions(extensions_array: any);
53-
create(req: Partial<http.IncomingMessage>): Promise<any>;
63+
get extensions(): string;
64+
set extensions(extensions_array: string[]);
65+
hasExtension(extension: string): boolean;
66+
create(file: File): Promise<IFile>;
67+
remove(file_id: string) : Promise<any>;
5468
write(
55-
req: http.IncomingMessage,
56-
file_id?: string,
57-
offset?: number
58-
): Promise<any>;
59-
getOffset(file_id: string): Promise<any>;
69+
stream: stream.Readable,
70+
file_id: string,
71+
offset: number
72+
): Promise<number>;
73+
getOffset(file_id: string): Promise<IFile>;
6074
}
6175

6276
/**
6377
* file store in local storage
6478
*/
6579
export declare class FileStore extends DataStore {
6680
constructor(options: FileStoreOptions);
67-
read(file_id: string): fs.ReadStream;
68-
getOffset(file_id: string): Promise<fs.Stats & File>;
81+
read(file_id: string): stream.Readable;
82+
getOffset(file_id: string): Promise<fs.Stats & IFile>;
6983
}
7084

7185
/**
@@ -80,14 +94,13 @@ export declare class GCSDataStore extends DataStore {
8094
*/
8195
export declare class S3Store extends DataStore {
8296
constructor(options: S3StoreOptions);
83-
getOffset(file_id: string, with_parts?: boolean): Promise<any>;
8497
}
8598

8699
/**
87100
* Tus protocol server implements
88101
*/
89102
export declare class Server extends EventEmitter {
90-
constructor();
103+
constructor(options: ServerOptions);
91104
get datastore(): DataStore;
92105
set datastore(store: DataStore);
93106
get(path: string, callback: (...args: any[]) => any): any;
@@ -117,6 +130,10 @@ export declare const ERRORS: {
117130
status_code: number;
118131
body: string;
119132
};
133+
INVALID_PATH: {
134+
status_code: number;
135+
body: string;
136+
},
120137
INVALID_OFFSET: {
121138
status_code: number;
122139
body: string;

‎lib/Server.js

+35-15
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,34 @@
99
const http = require('http');
1010
const EventEmitter = require('events');
1111

12-
const DataStore = require('./stores/DataStore');
1312
const GetHandler = require('./handlers/GetHandler');
1413
const HeadHandler = require('./handlers/HeadHandler');
1514
const OptionsHandler = require('./handlers/OptionsHandler');
1615
const PatchHandler = require('./handlers/PatchHandler');
1716
const PostHandler = require('./handlers/PostHandler');
1817
const DeleteHandler = require('./handlers/DeleteHandler');
1918
const RequestValidator = require('./validators/RequestValidator');
19+
const ERRORS = require('./constants').ERRORS;
2020
const EXPOSED_HEADERS = require('./constants').EXPOSED_HEADERS;
2121
const REQUEST_METHODS = require('./constants').REQUEST_METHODS;
2222
const TUS_RESUMABLE = require('./constants').TUS_RESUMABLE;
2323
const debug = require('debug');
2424
const log = debug('tus-node-server');
2525
class TusServer extends EventEmitter {
2626

27-
constructor() {
27+
constructor(options) {
2828
super();
2929

30+
if (!options) {
31+
throw new Error('\'options\' must be defined');
32+
}
33+
if (!options.path) {
34+
throw new Error('\'path\' is not defined; must have a path');
35+
}
36+
37+
this.options = { ...options };
38+
39+
3040
// Any handlers assigned to this object with the method as the key
3141
// will be used to repond to those requests. They get set/re-set
3242
// when a datastore is assigned to the server.
@@ -67,25 +77,21 @@ class TusServer extends EventEmitter {
6777
* @param {DataStore} store Store for uploaded files
6878
*/
6979
set datastore(store) {
70-
if (!(store instanceof DataStore)) {
71-
throw new Error(`${store} is not a DataStore`);
72-
}
73-
7480
this._datastore = store;
7581

7682
this.handlers = {
7783
// GET handlers should be written in the implementations
7884
// eg.
7985
// const server = new tus.Server();
8086
// server.get('/', (req, res) => { ... });
81-
GET: new GetHandler(store),
87+
GET: new GetHandler(store, this.options),
8288

8389
// These methods are handled under the tus protocol
84-
HEAD: new HeadHandler(store),
85-
OPTIONS: new OptionsHandler(store),
86-
PATCH: new PatchHandler(store),
87-
POST: new PostHandler(store),
88-
DELETE: new DeleteHandler(store),
90+
HEAD: new HeadHandler(store, this.options),
91+
OPTIONS: new OptionsHandler(store, this.options),
92+
PATCH: new PatchHandler(store, this.options),
93+
POST: new PostHandler(store, this.options),
94+
DELETE: new DeleteHandler(store, this.options),
8995
};
9096
}
9197

@@ -122,7 +128,14 @@ class TusServer extends EventEmitter {
122128

123129

124130
if (req.method === 'GET') {
125-
return this.handlers.GET.send(req, res);
131+
const handler = this.handlers.GET;
132+
return handler.send(req, res)
133+
.catch((error) => {
134+
log(`[${handler.constructor.name}]`, error);
135+
const status_code = error.status_code || ERRORS.UNKNOWN_ERROR.status_code;
136+
const body = error.body || `${ERRORS.UNKNOWN_ERROR.body}${error.message || ''}\n`;
137+
return handler.write(res, status_code, {}, body);
138+
});
126139
}
127140

128141
// The Tus-Resumable header MUST be included in every request and
@@ -168,8 +181,15 @@ class TusServer extends EventEmitter {
168181
}
169182

170183
// Invoke the handler for the method requested
171-
if (this.handlers[req.method]) {
172-
return this.handlers[req.method].send(req, res);
184+
const handler = this.handlers[req.method];
185+
if (handler) {
186+
return handler.send(req, res)
187+
.catch((error) => {
188+
log(`[${handler.constructor.name}]`, error);
189+
const status_code = error.status_code || ERRORS.UNKNOWN_ERROR.status_code;
190+
const body = error.body || `${ERRORS.UNKNOWN_ERROR.body}${error.message || ''}\n`;
191+
return handler.write(res, status_code, {}, body);
192+
});
173193
}
174194

175195
// 404 Anything else

‎lib/configstores/MemoryConfigstore.js

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
'use strict';
2+
3+
/**
4+
* @fileOverview
5+
* Memory based configstore.
6+
* Used mostly for unit tests.
7+
*
8+
* @author Mitja Puzigaća <mitjap@gmail.com>
9+
*/
10+
11+
class MemoryConfigstore {
12+
constructor() {
13+
this.data = new Map();
14+
}
15+
16+
async get(key) {
17+
let value = this.data.get(key);
18+
if (value !== undefined) {
19+
value = JSON.parse(value);
20+
}
21+
return value;
22+
}
23+
24+
async set(key, value) {
25+
this.data.set(key, JSON.stringify(value));
26+
}
27+
28+
async delete(key) {
29+
return this.data.delete(key);
30+
}
31+
}
32+
33+
module.exports = MemoryConfigstore;

‎lib/constants.js

+5
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const REQUEST_METHODS = [
55
'HEAD',
66
'PATCH',
77
'OPTIONS',
8+
'DELETE',
89
];
910

1011
const HEADERS = [
@@ -59,6 +60,10 @@ const ERRORS = {
5960
status_code: 500,
6061
body: 'Something went wrong receiving the file\n',
6162
},
63+
UNSUPPORTED_CONCATENATION_EXTENSION: {
64+
status_code: 501,
65+
body: 'Concatenation extension is not (yet) supported. Disable parallel uploads in the tus client.\n',
66+
},
6267
};
6368

6469
const EVENT_ENDPOINT_CREATED = 'EVENT_ENDPOINT_CREATED';

‎lib/handlers/BaseHandler.js

+12-6
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
'use strict';
22

3-
const DataStore = require('../stores/DataStore');
43
const EventEmitter = require('events');
54

65

76
class BaseHandler extends EventEmitter {
8-
constructor(store) {
7+
constructor(store, options) {
98
super();
10-
if (!(store instanceof DataStore)) {
11-
throw new Error(`${store} is not a DataStore`);
9+
10+
if (!store) {
11+
throw new Error('Store must be defined');
1212
}
13+
1314
this.store = store;
15+
this.options = options;
1416
}
1517

1618
/**
@@ -22,7 +24,7 @@ class BaseHandler extends EventEmitter {
2224
* @param {string} body
2325
* @return {ServerResponse}
2426
*/
25-
send(res, status, headers = {}, body) {
27+
write(res, status, headers = {}, body) {
2628
body = body ? body : '';
2729
headers = status === 204 ? headers : { ...headers, 'Content-Length': body.length };
2830

@@ -31,14 +33,18 @@ class BaseHandler extends EventEmitter {
3133
return res.end();
3234
}
3335

36+
generateUrl(req, file_id) {
37+
return this.options.relativeLocation ? `${req.baseUrl || ''}${this.options.path}/${file_id}` : `//${req.headers.host}${req.baseUrl || ''}${this.options.path}/${file_id}`;
38+
}
39+
3440
/**
3541
* Extract the file id from the request
3642
*
3743
* @param {object} req http.incomingMessage
3844
* @return {bool|string}
3945
*/
4046
getFileIdFromRequest(req) {
41-
const re = new RegExp(`${req.baseUrl || ''}${this.store.path}\\/(\\S+)\\/?`); // eslint-disable-line prefer-template
47+
const re = new RegExp(`${req.baseUrl || ''}${this.options.path}\\/(\\S+)\\/?`); // eslint-disable-line prefer-template
4248
const match = (req.originalUrl || req.url).match(re);
4349
if (!match) {
4450
return false;

‎lib/handlers/DeleteHandler.js

+9-15
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use strict';
22

33
const BaseHandler = require('./BaseHandler');
4-
const ERRORS = require('../constants').ERRORS;
4+
const { ERRORS, EVENTS } = require('../constants');
55

66
class DeleteHandler extends BaseHandler {
77
/**
@@ -11,22 +11,16 @@ class DeleteHandler extends BaseHandler {
1111
* @param {object} res http.ServerResponse
1212
* @return {function}
1313
*/
14-
send(req, res) {
14+
async send(req, res) {
1515
const file_id = this.getFileIdFromRequest(req);
16-
if (!file_id) {
17-
console.warn('[DeleteHandler]: not a valid path');
18-
return Promise.resolve(super.send(res, 404, {}, 'Invalid path name\n'));
16+
if (file_id === false) {
17+
throw ERRORS.FILE_NOT_FOUND;
1918
}
20-
req.file_id = file_id;
21-
return this.store.remove(req)
22-
.then(() => {
23-
return super.send(res, 204, {});
24-
})
25-
.catch((error) => {
26-
const status_code = error.status_code || ERRORS.UNKNOWN_ERROR.status_code;
27-
const body = error.body || `${ERRORS.UNKNOWN_ERROR.body}${error.message || ''}\n`;
28-
return super.send(res, status_code, {}, body);
29-
});
19+
20+
await this.store.remove(file_id);
21+
this.emit(EVENTS.EVENT_FILE_DELETED, { file_id });
22+
23+
return this.write(res, 204, {});
3024
}
3125
}
3226

‎lib/handlers/GetHandler.js

+24-35
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ const debug = require('debug');
88
const log = debug('tus-node-server:handlers:get');
99

1010
class GetHandler extends BaseHandler {
11-
constructor(store) {
12-
super(store);
11+
constructor(store, options) {
12+
super(store, options);
1313

1414
this.paths = new Map();
1515
}
@@ -25,49 +25,38 @@ class GetHandler extends BaseHandler {
2525
* @param {object} res http.ServerResponse
2626
* @return {function}
2727
*/
28-
send(req, res) {
28+
async send(req, res) {
2929
// Check if this url has been added to allow GET requests, with an
3030
// appropriate callback to handle the request
3131
if (this.paths.has(req.url)) {
3232
// invoke the callback
3333
return this.paths.get(req.url)(req, res);
3434
}
3535

36-
return Promise.resolve()
37-
.then(() => {
38-
if (!('read' in this.store)) {
39-
return Promise.reject(ERRORS.FILE_NOT_FOUND);
40-
}
36+
if (!('read' in this.store)) {
37+
throw ERRORS.FILE_NOT_FOUND;
38+
}
4139

42-
const file_id = this.getFileIdFromRequest(req);
43-
if (file_id === false) {
44-
return Promise.reject(ERRORS.FILE_NOT_FOUND);
45-
}
40+
const file_id = this.getFileIdFromRequest(req);
41+
if (file_id === false) {
42+
throw ERRORS.FILE_NOT_FOUND;
43+
}
4644

47-
return this.store.getOffset(file_id)
48-
.then((stats) => {
49-
const upload_length = parseInt(stats.upload_length, 10);
50-
if (stats.size !== upload_length) {
51-
log(`[GetHandler] send: File is not yet fully uploaded (${stats.size}/${upload_length})`);
52-
return Promise.reject(ERRORS.FILE_NOT_FOUND);
53-
}
45+
const stats = await this.store.getOffset(file_id);
46+
const upload_length = parseInt(stats.upload_length, 10);
47+
if (stats.size !== upload_length) {
48+
log(`[GetHandler] send: File is not yet fully uploaded (${stats.size}/${upload_length})`);
49+
throw ERRORS.FILE_NOT_FOUND;
50+
}
5451

55-
const file_stream = this.store.read(file_id);
56-
const headers = {
57-
'Content-Length': stats.size,
58-
};
59-
res.writeHead(200, headers);
60-
return stream.pipeline(file_stream, res, (err) => {
61-
// we have no need to handle streaming errors
62-
});
63-
});
64-
})
65-
.catch((error) => {
66-
log('[GetHandler]', error);
67-
const status_code = error.status_code || ERRORS.UNKNOWN_ERROR.status_code;
68-
const body = error.body || `${ERRORS.UNKNOWN_ERROR.body}${error.message || ''}\n`;
69-
return super.send(res, status_code, {}, body);
70-
});
52+
const file_stream = this.store.read(file_id);
53+
const headers = {
54+
'Content-Length': stats.size,
55+
};
56+
res.writeHead(200, headers);
57+
return stream.pipeline(file_stream, res, (err) => {
58+
// we have no need to handle streaming errors
59+
});
7160
}
7261
}
7362

‎lib/handlers/HeadHandler.js

+32-41
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
const BaseHandler = require('./BaseHandler');
44
const ERRORS = require('../constants').ERRORS;
5-
const debug = require('debug');
6-
const log = debug('tus-node-server:handlers:head');
75
class HeadHandler extends BaseHandler {
86
/**
97
* Send the bytes received for a given file.
@@ -12,49 +10,42 @@ class HeadHandler extends BaseHandler {
1210
* @param {object} res http.ServerResponse
1311
* @return {function}
1412
*/
15-
send(req, res) {
13+
async send(req, res) {
1614
const file_id = this.getFileIdFromRequest(req);
1715
if (file_id === false) {
18-
return super.send(res, ERRORS.FILE_NOT_FOUND.status_code, {}, ERRORS.FILE_NOT_FOUND.body);
16+
throw ERRORS.FILE_NOT_FOUND;
1917
}
2018

21-
return this.store.getOffset(file_id)
22-
.then((file) => {
23-
// The Server MUST prevent the client and/or proxies from
24-
// caching the response by adding the Cache-Control: no-store
25-
// header to the response.
26-
res.setHeader('Cache-Control', 'no-store');
27-
28-
// The Server MUST always include the Upload-Offset header in
29-
// the response for a HEAD request, even if the offset is 0
30-
res.setHeader('Upload-Offset', file.size);
31-
32-
if (file.upload_length !== undefined) {
33-
// If the size of the upload is known, the Server MUST include
34-
// the Upload-Length header in the response.
35-
res.setHeader('Upload-Length', file.upload_length);
36-
}
37-
38-
if (!('upload_length' in file) && file.upload_defer_length !== undefined) {
39-
// As long as the length of the upload is not known, the Server
40-
// MUST set Upload-Defer-Length: 1 in all responses to HEAD requests.
41-
res.setHeader('Upload-Defer-Length', file.upload_defer_length);
42-
}
43-
44-
if (file.upload_metadata !== undefined) {
45-
// If the size of the upload is known, the Server MUST include
46-
// the Upload-Length header in the response.
47-
res.setHeader('Upload-Metadata', file.upload_metadata);
48-
}
49-
50-
return res.end();
51-
})
52-
.catch((error) => {
53-
log('[HeadHandler]', error);
54-
const status_code = error.status_code || ERRORS.UNKNOWN_ERROR.status_code;
55-
const body = error.body || `${ERRORS.UNKNOWN_ERROR.body}${error.message || ''}\n`;
56-
return super.send(res, status_code, {}, body);
57-
});
19+
const file = await this.store.getOffset(file_id);
20+
21+
// The Server MUST prevent the client and/or proxies from
22+
// caching the response by adding the Cache-Control: no-store
23+
// header to the response.
24+
res.setHeader('Cache-Control', 'no-store');
25+
26+
// The Server MUST always include the Upload-Offset header in
27+
// the response for a HEAD request, even if the offset is 0
28+
res.setHeader('Upload-Offset', file.size);
29+
30+
if (file.upload_length !== undefined) {
31+
// If the size of the upload is known, the Server MUST include
32+
// the Upload-Length header in the response.
33+
res.setHeader('Upload-Length', file.upload_length);
34+
}
35+
36+
if (!('upload_length' in file) && file.upload_defer_length !== undefined) {
37+
// As long as the length of the upload is not known, the Server
38+
// MUST set Upload-Defer-Length: 1 in all responses to HEAD requests.
39+
res.setHeader('Upload-Defer-Length', file.upload_defer_length);
40+
}
41+
42+
if (file.upload_metadata !== undefined) {
43+
// If the size of the upload is known, the Server MUST include
44+
// the Upload-Length header in the response.
45+
res.setHeader('Upload-Metadata', file.upload_metadata);
46+
}
47+
48+
return res.end();
5849
}
5950
}
6051

‎lib/handlers/OptionsHandler.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class OptionsHandler extends BaseHandler {
1616
* @param {object} res http.ServerResponse
1717
* @return {function}
1818
*/
19-
send(req, res) {
19+
async send(req, res) {
2020
// Preflight request
2121
res.setHeader('Access-Control-Allow-Methods', ALLOWED_METHODS);
2222
res.setHeader('Access-Control-Allow-Headers', ALLOWED_HEADERS);
@@ -27,7 +27,7 @@ class OptionsHandler extends BaseHandler {
2727
res.setHeader('Tus-Extension', this.store.extensions);
2828
}
2929

30-
return super.send(res, 204);
30+
return this.write(res, 204);
3131
}
3232
}
3333

‎lib/handlers/PatchHandler.js

+25-29
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
'use strict';
22

33
const BaseHandler = require('./BaseHandler');
4-
const ERRORS = require('../constants').ERRORS;
4+
const File = require('../models/File');
5+
const { ERRORS, EVENTS } = require('../constants');
56
const debug = require('debug');
67
const log = debug('tus-node-server:handlers:patch');
78
class PatchHandler extends BaseHandler {
@@ -12,50 +13,45 @@ class PatchHandler extends BaseHandler {
1213
* @param {object} res http.ServerResponse
1314
* @return {function}
1415
*/
15-
send(req, res) {
16+
async send(req, res) {
1617
const file_id = this.getFileIdFromRequest(req);
1718
if (file_id === false) {
18-
return super.send(res, ERRORS.FILE_NOT_FOUND.status_code, {}, ERRORS.FILE_NOT_FOUND.body);
19+
throw ERRORS.FILE_NOT_FOUND;
1920
}
2021

2122
// The request MUST include a Upload-Offset header
2223
let offset = req.headers['upload-offset'];
2324
if (offset === undefined) {
24-
return super.send(res, ERRORS.MISSING_OFFSET.status_code, {}, ERRORS.MISSING_OFFSET.body);
25+
throw ERRORS.MISSING_OFFSET;
2526
}
2627

2728
// The request MUST include a Content-Type header
2829
const content_type = req.headers['content-type'];
2930
if (content_type === undefined) {
30-
return super.send(res, ERRORS.INVALID_CONTENT_TYPE.status_code, {}, ERRORS.INVALID_CONTENT_TYPE.body);
31+
throw ERRORS.INVALID_CONTENT_TYPE;
3132
}
3233

3334
offset = parseInt(offset, 10);
3435

35-
return this.store.getOffset(file_id)
36-
.then((stats) => {
37-
if (stats.size !== offset) {
38-
// If the offsets do not match, the Server MUST respond with the 409 Conflict status without modifying the upload resource.
39-
log(`[PatchHandler] send: Incorrect offset - ${offset} sent but file is ${stats.size}`);
40-
return Promise.reject(ERRORS.INVALID_OFFSET);
41-
}
42-
43-
return this.store.write(req, file_id, offset);
44-
})
45-
.then((new_offset) => {
46-
// It MUST include the Upload-Offset header containing the new offset.
47-
const headers = {
48-
'Upload-Offset': new_offset,
49-
};
50-
// The Server MUST acknowledge successful PATCH requests with the 204
51-
return super.send(res, 204, headers);
52-
})
53-
.catch((error) => {
54-
log('[PatchHandler]', error);
55-
const status_code = error.status_code || ERRORS.UNKNOWN_ERROR.status_code;
56-
const body = error.body || `${ERRORS.UNKNOWN_ERROR.body}${error.message || ''}\n`;
57-
return super.send(res, status_code, {}, body);
58-
});
36+
const stats = await this.store.getOffset(file_id);
37+
if (stats.size !== offset) {
38+
// If the offsets do not match, the Server MUST respond with the 409 Conflict status without modifying the upload resource.
39+
log(`[PatchHandler] send: Incorrect offset - ${offset} sent but file is ${stats.size}`);
40+
throw ERRORS.INVALID_OFFSET;
41+
}
42+
43+
const new_offset = await this.store.write(req, file_id, offset);
44+
if (`${new_offset}` === stats.upload_length) {
45+
this.emit(EVENTS.EVENT_UPLOAD_COMPLETE, { file: new File(file_id, stats.upload_length, stats.upload_defer_length, stats.upload_metadata) });
46+
}
47+
48+
// It MUST include the Upload-Offset header containing the new offset.
49+
const headers = {
50+
'Upload-Offset': new_offset,
51+
};
52+
53+
// The Server MUST acknowledge successful PATCH requests with the 204
54+
return this.write(res, 204, headers);
5955
}
6056
}
6157

‎lib/handlers/PostHandler.js

+49-22
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,73 @@
11
'use strict';
22

33
const BaseHandler = require('./BaseHandler');
4+
const File = require('../models/File');
5+
const Uid = require('../models/Uid');
46
const RequestValidator = require('../validators/RequestValidator');
5-
const ERRORS = require('../constants').ERRORS;
6-
const EVENT_ENDPOINT_CREATED = require('../constants').EVENT_ENDPOINT_CREATED;
7+
const { EVENTS, ERRORS } = require('../constants');
78
const debug = require('debug');
89
const log = debug('tus-node-server:handlers:post');
910
class PostHandler extends BaseHandler {
11+
12+
constructor(store, options) {
13+
if (options.namingFunction && typeof options.namingFunction !== 'function') {
14+
throw new Error('\'namingFunction\' must be a function');
15+
}
16+
17+
if (!options.namingFunction) {
18+
options.namingFunction = Uid.rand;
19+
}
20+
21+
super(store, options);
22+
}
23+
1024
/**
1125
* Create a file in the DataStore.
1226
*
1327
* @param {object} req http.incomingMessage
1428
* @param {object} res http.ServerResponse
1529
* @return {function}
1630
*/
17-
send(req, res) {
31+
async send(req, res) {
1832
if ('upload-concat' in req.headers && !this.store.hasExtension('concatentation')) {
19-
return Promise.resolve(super.send(res, 501, {}, 'Concatenation extension is not (yet) supported. Disable parallel uploads in the tus client. '));
33+
throw ERRORS.UNSUPPORTED_CONCATENATION_EXTENSION;
34+
}
35+
36+
const upload_length = req.headers['upload-length'];
37+
const upload_defer_length = req.headers['upload-defer-length'];
38+
const upload_metadata = req.headers['upload-metadata'];
39+
40+
if ((upload_length === undefined) === (upload_defer_length === undefined)) {
41+
throw ERRORS.INVALID_LENGTH;
42+
}
43+
44+
let file_id;
45+
46+
try {
47+
file_id = this.options.namingFunction(req);
48+
}
49+
catch (err) {
50+
log('create: check your `namingFunction`. Error', err);
51+
throw ERRORS.FILE_WRITE_ERROR;
2052
}
2153

22-
return this.store.create(req)
23-
.then(async(File) => {
24-
const url = this.store.relativeLocation ? `${req.baseUrl || ''}${this.store.path}/${File.id}` : `//${req.headers.host}${req.baseUrl || ''}${this.store.path}/${File.id}`;
54+
const file = new File(file_id, upload_length, upload_defer_length, upload_metadata);
2555

26-
this.emit(EVENT_ENDPOINT_CREATED, { url });
56+
const obj = await this.store.create(file);
57+
this.emit(EVENTS.EVENT_FILE_CREATED, { file: obj });
2758

28-
const optional_headers = {};
59+
const url = this.generateUrl(req, file.id);
60+
this.emit(EVENTS.EVENT_ENDPOINT_CREATED, { url });
2961

30-
// The request MIGHT include a Content-Type header when using creation-with-upload extension
31-
if (!RequestValidator.isInvalidHeader('content-type', req.headers['content-type'])) {
32-
const new_offset = await this.store.write(req, File.id, 0);
33-
optional_headers['Upload-Offset'] = new_offset;
34-
}
62+
const optional_headers = {};
63+
64+
// The request MIGHT include a Content-Type header when using creation-with-upload extension
65+
if (!RequestValidator.isInvalidHeader('content-type', req.headers['content-type'])) {
66+
const new_offset = await this.store.write(req, file.id, 0);
67+
optional_headers['Upload-Offset'] = new_offset;
68+
}
3569

36-
return super.send(res, 201, { Location: url, ...optional_headers });
37-
})
38-
.catch((error) => {
39-
log('[PostHandler]', error);
40-
const status_code = error.status_code || ERRORS.UNKNOWN_ERROR.status_code;
41-
const body = error.body || `${ERRORS.UNKNOWN_ERROR.body}${error.message || ''}\n`;
42-
return super.send(res, status_code, {}, body);
43-
});
70+
return this.write(res, 201, { Location: url, ...optional_headers });
4471
}
4572
}
4673

‎lib/stores/DataStore.js

+16-63
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,8 @@
77
* @author Ben Stahl <bhstahl@gmail.com>
88
*/
99

10-
const Uid = require('../models/Uid');
11-
const File = require('../models/File');
1210
const EventEmitter = require('events');
13-
const ERRORS = require('../constants').ERRORS;
14-
const EVENTS = require('../constants').EVENTS;
15-
const debug = require('debug');
16-
const log = debug('tus-node-server:stores');
1711
class DataStore extends EventEmitter {
18-
constructor(options) {
19-
super();
20-
if (!options || !options.path) {
21-
throw new Error('Store must have a path');
22-
}
23-
if (options.namingFunction && typeof options.namingFunction !== 'function') {
24-
throw new Error('namingFunction must be a function');
25-
}
26-
this.path = options.path;
27-
this.generateFileName = options.namingFunction || Uid.rand;
28-
this.relativeLocation = options.relativeLocation || false;
29-
}
3012

3113
get extensions() {
3214
if (!this._extensions) {
@@ -52,40 +34,22 @@ class DataStore extends EventEmitter {
5234
*
5335
* http://tus.io/protocols/resumable-upload.html#creation
5436
*
55-
* @param {object} req http.incomingMessage
56-
* @return {Promise}
37+
* @param {File} file
38+
* @return {Promise} offset
5739
*/
58-
create(req) {
59-
return new Promise((resolve, reject) => {
60-
const upload_length = req.headers['upload-length'];
61-
const upload_defer_length = req.headers['upload-defer-length'];
62-
const upload_metadata = req.headers['upload-metadata'];
63-
64-
if (upload_length === undefined && upload_defer_length === undefined) {
65-
return reject(ERRORS.INVALID_LENGTH);
66-
}
67-
68-
const file_id = this.generateFileName(req);
69-
const file = new File(file_id, upload_length, upload_defer_length, upload_metadata);
70-
71-
this.emit(EVENTS.EVENT_FILE_CREATED, { file });
72-
return resolve(file);
73-
});
40+
create(file) {
41+
return Promise.resolve(file);
7442
}
7543

7644
/**
7745
* Called in DELETE requests. This method just deletes the file from the store.
7846
* http://tus.io/protocols/resumable-upload.html#termination
7947
*
80-
* @param {object} req http.incomingMessage
48+
* @param {string} file_id Name of the file
8149
* @return {Promise}
8250
*/
83-
remove(req) {
84-
return new Promise((resolve, reject) => {
85-
const file_id = req.file_id;
86-
this.emit(EVENTS.EVENT_FILE_DELETED, { file_id });
87-
return resolve();
88-
});
51+
remove(file_id) {
52+
return Promise.resolve();
8953
}
9054

9155

@@ -96,36 +60,25 @@ class DataStore extends EventEmitter {
9660
*
9761
* http://tus.io/protocols/resumable-upload.html#concatenation
9862
*
99-
* @param {object} req http.incomingMessage
63+
* @param {object} stream stream.Readable
64+
* @param {string} file_id Name of file
65+
* @param {integer} offset starting offset
10066
* @return {Promise}
10167
*/
102-
write(req) {
103-
log('[DataStore] write');
104-
return new Promise((resolve, reject) => {
105-
// Stub resolve for tests
106-
const offset = 0;
107-
108-
this.emit(EVENTS.EVENT_UPLOAD_COMPLETE, { file: null });
109-
return resolve(offset);
110-
});
68+
write(stream, file_id, offset) {
69+
return Promise.resolve(0);
11170
}
11271

11372
/**
11473
* Called in HEAD requests. This method should return the bytes
11574
* writen to the DataStore, for the client to know where to resume
11675
* the upload.
11776
*
118-
* @param {string} id filename
119-
* @return {Promise} bytes written
77+
* @param {string} file_id Name of file
78+
* @return {Promise} bytes written
12079
*/
121-
getOffset(id) {
122-
return new Promise((resolve, reject) => {
123-
if (!id) {
124-
return reject(ERRORS.FILE_NOT_FOUND);
125-
}
126-
127-
return resolve({ size: 0, upload_length: 1 });
128-
});
80+
getOffset(file_id) {
81+
return Promise.resolve({ size: 0, upload_length: 0 });
12982
}
13083
}
13184

‎lib/stores/FileStore.js

+47-68
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
'use strict';
22

33
const DataStore = require('./DataStore');
4-
const File = require('../models/File');
54
const fs = require('fs');
5+
const path = require('path');
66
const stream = require('stream');
77
const Configstore = require('configstore');
88
const pkg = require('../../package.json');
99
const MASK = '0777';
1010
const IGNORED_MKDIR_ERROR = 'EEXIST';
1111
const FILE_DOESNT_EXIST = 'ENOENT';
12-
const ERRORS = require('../constants').ERRORS;
13-
const EVENTS = require('../constants').EVENTS;
12+
const { ERRORS } = require('../constants');
1413
const debug = require('debug');
1514
const log = debug('tus-node-server:stores:filestore');
1615

@@ -24,10 +23,14 @@ const log = debug('tus-node-server:stores:filestore');
2423
class FileStore extends DataStore {
2524
constructor(options) {
2625
super(options);
27-
this.directory = options.directory || options.path.replace(/^\//, '');
26+
this.directory = options.directory;
27+
this.configstore = options.configstore;
28+
29+
if (!this.configstore) {
30+
this.configstore = new Configstore(`${pkg.name}-${pkg.version}`);
31+
}
2832

2933
this.extensions = ['creation', 'creation-with-upload', 'creation-defer-length', 'termination'];
30-
this.configstore = new Configstore(`${pkg.name}-${pkg.version}`);
3134
this._checkOrCreateDirectory();
3235
}
3336

@@ -45,45 +48,25 @@ class FileStore extends DataStore {
4548
/**
4649
* Create an empty file.
4750
*
48-
* @param {object} req http.incomingMessage
4951
* @param {File} file
5052
* @return {Promise}
5153
*/
52-
create(req) {
54+
create(file) {
5355
return new Promise((resolve, reject) => {
54-
const upload_length = req.headers['upload-length'];
55-
const upload_defer_length = req.headers['upload-defer-length'];
56-
const upload_metadata = req.headers['upload-metadata'];
57-
58-
if (upload_length === undefined && upload_defer_length === undefined) {
59-
return reject(ERRORS.INVALID_LENGTH);
60-
}
61-
62-
let file_id;
63-
try {
64-
file_id = this.generateFileName(req);
65-
}
66-
catch (generateError) {
67-
log('[FileStore] create: check your namingFunction. Error', generateError);
68-
return reject(ERRORS.FILE_WRITE_ERROR);
69-
}
70-
71-
const file = new File(file_id, upload_length, upload_defer_length, upload_metadata);
72-
return fs.open(`${this.directory}/${file.id}`, 'w', (err, fd) => {
56+
return fs.open(path.join(this.directory, file.id), 'w', async(err, fd) => {
7357
if (err) {
7458
log('[FileStore] create: Error', err);
7559
return reject(err);
7660
}
7761

78-
this.configstore.set(file.id, file);
62+
await this.configstore.set(file.id, file);
7963

8064
return fs.close(fd, (exception) => {
8165
if (exception) {
8266
log('[FileStore] create: Error', exception);
8367
return reject(exception);
8468
}
8569

86-
this.emit(EVENTS.EVENT_FILE_CREATED, { file });
8770
return resolve(file);
8871
});
8972
});
@@ -92,76 +75,72 @@ class FileStore extends DataStore {
9275

9376
/** Get file from filesystem
9477
*
95-
* @param {string} file_id Name of the file
78+
* @param {string} file_id Name of the file
9679
*
9780
* @return {stream.Readable}
9881
*/
9982
read(file_id) {
100-
const path = `${this.directory}/${file_id}`;
101-
return fs.createReadStream(path);
83+
return fs.createReadStream(path.join(this.directory, file_id));
10284
}
10385

10486
/**
10587
* Deletes a file.
10688
*
107-
* @param {object} req http.incomingMessage
89+
* @param {string} file_id Name of the file
10890
* @return {Promise}
10991
*/
110-
remove(req) {
92+
remove(file_id) {
11193
return new Promise((resolve, reject) => {
112-
const file_id = req.file_id;
113-
return fs.unlink(`${this.directory}/${file_id}`, (err, fd) => {
94+
return fs.unlink(`${this.directory}/${file_id}`, async(err, fd) => {
11495
if (err) {
115-
console.warn('[FileStore] delete: Error', err);
116-
return reject(ERRORS.FILE_NOT_FOUND);
96+
log('[FileStore] delete: Error', err);
97+
reject(ERRORS.FILE_NOT_FOUND);
98+
return;
99+
}
100+
101+
try {
102+
resolve(await this.configstore.delete(file_id));
103+
}
104+
catch (error) {
105+
reject(error);
117106
}
118-
this.emit(EVENTS.EVENT_FILE_DELETED, { file_id });
119-
this.configstore.delete(file_id);
120-
return resolve();
121107
});
122108
});
123109
}
124110

125111
/**
126112
* Write to the file, starting at the provided offset
127113
*
128-
* @param {object} req http.incomingMessage
129-
* @param {string} file_id Name of file
130-
* @param {integer} offset starting offset
114+
* @param {object} readable stream.Readable
115+
* @param {string} file_id Name of file
116+
* @param {integer} offset starting offset
131117
* @return {Promise}
132118
*/
133-
write(req, file_id, offset) {
134-
return new Promise((resolve, reject) => {
135-
const path = `${this.directory}/${file_id}`;
136-
const options = {
137-
flags: 'r+',
138-
start: offset,
139-
};
140-
141-
const write_stream = fs.createWriteStream(path, options);
142-
if (!write_stream || req.destroyed) {
143-
reject(ERRORS.FILE_WRITE_ERROR);
144-
return;
145-
}
119+
write(readable, file_id, offset) {
120+
const writeable = fs.createWriteStream(path.join(this.directory, file_id), {
121+
flags: 'r+',
122+
start: offset,
123+
});
146124

147-
let new_offset = 0;
148-
req.on('data', (buffer) => {
149-
new_offset += buffer.length;
150-
});
125+
let bytes_received = 0;
126+
const transform = new stream.Transform({
127+
transform(chunk, encoding, callback) {
128+
bytes_received += chunk.length;
129+
callback(null, chunk);
130+
}
131+
});
151132

152-
stream.pipeline(req, write_stream, (err) => {
133+
return new Promise((resolve, reject) => {
134+
stream.pipeline(readable, transform, writeable, (err) => {
153135
if (err) {
154136
log('[FileStore] write: Error', err);
155137
return reject(ERRORS.FILE_WRITE_ERROR);
156138
}
157139

158-
offset += new_offset;
140+
log(`[FileStore] write: ${bytes_received} bytes written to ${path}`);
141+
offset += bytes_received;
159142
log(`[FileStore] write: File is now ${offset} bytes`);
160143

161-
const config = this.configstore.get(file_id);
162-
if (config && parseInt(config.upload_length, 10) === offset) {
163-
this.emit(EVENTS.EVENT_UPLOAD_COMPLETE, { file: config });
164-
}
165144
return resolve(offset);
166145
});
167146
});
@@ -173,8 +152,8 @@ class FileStore extends DataStore {
173152
* @param {string} file_id name of the file
174153
* @return {object} fs stats
175154
*/
176-
getOffset(file_id) {
177-
const config = this.configstore.get(file_id);
155+
async getOffset(file_id) {
156+
const config = await this.configstore.get(file_id);
178157
return new Promise((resolve, reject) => {
179158
const file_path = `${this.directory}/${file_id}`;
180159
fs.stat(file_path, (error, stats) => {

‎lib/stores/GCSDataStore.js

+20-44
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
'use strict';
22

33
const DataStore = require('./DataStore');
4-
const File = require('../models/File');
54
const { Storage } = require('@google-cloud/storage');
65
const stream = require('stream');
7-
const ERRORS = require('../constants').ERRORS;
8-
const EVENTS = require('../constants').EVENTS;
9-
const TUS_RESUMABLE = require('../constants').TUS_RESUMABLE;
6+
const { ERRORS, TUS_RESUMABLE } = require('../constants');
107
const DEFAULT_CONFIG = {
118
scopes: ['https://www.googleapis.com/auth/devstorage.full_control'],
129
};
@@ -69,40 +66,24 @@ class GCSDataStore extends DataStore {
6966
/**
7067
* Create an empty file in GCS to store the metatdata.
7168
*
72-
* @param {object} req http.incomingMessage
7369
* @param {File} file
7470
* @return {Promise}
7571
*/
76-
create(req) {
72+
create(file) {
7773
return new Promise((resolve, reject) => {
78-
const upload_length = req.headers['upload-length'];
79-
const upload_defer_length = req.headers['upload-defer-length'];
80-
const upload_metadata = req.headers['upload-metadata'];
81-
82-
if (upload_length === undefined && upload_defer_length === undefined) {
83-
reject(ERRORS.INVALID_LENGTH);
84-
return;
85-
}
86-
87-
let file_id;
88-
try {
89-
file_id = this.generateFileName(req);
90-
}
91-
catch (generateError) {
92-
log('[FileStore] create: check your namingFunction. Error', generateError);
93-
reject(ERRORS.FILE_WRITE_ERROR);
74+
if (!file.id) {
75+
reject(ERRORS.FILE_NOT_FOUND);
9476
return;
9577
}
9678

97-
const file = new File(file_id, upload_length, upload_defer_length, upload_metadata);
9879
const gcs_file = this.bucket.file(file.id);
9980
const options = {
10081
metadata: {
10182
metadata: {
102-
upload_length: file.upload_length,
10383
tus_version: TUS_RESUMABLE,
104-
upload_metadata,
105-
upload_defer_length,
84+
upload_length: file.upload_length,
85+
upload_metadata: file.upload_metadata,
86+
upload_defer_length: file.upload_defer_length,
10687
},
10788
},
10889
};
@@ -112,7 +93,6 @@ class GCSDataStore extends DataStore {
11293
fake_stream.pipe(gcs_file.createWriteStream(options))
11394
.on('error', reject)
11495
.on('finish', () => {
115-
this.emit(EVENTS.EVENT_FILE_CREATED, { file });
11696
resolve(file);
11797
});
11898
});
@@ -132,12 +112,12 @@ class GCSDataStore extends DataStore {
132112
* Get the file metatata from the object in GCS, then upload a new version
133113
* passing through the metadata to the new version.
134114
*
135-
* @param {object} req http.incomingMessage
136-
* @param {string} file_id Name of file
137-
* @param {integer} offset starting offset
115+
* @param {object} readable stream.Readable
116+
* @param {string} file_id Name of file
117+
* @param {integer} offset starting offset
138118
* @return {Promise}
139119
*/
140-
write(req, file_id, offset) {
120+
write(readable, file_id, offset) {
141121
// GCS Doesn't persist metadata within versions,
142122
// get that metadata first
143123
return this.getOffset(file_id)
@@ -160,17 +140,17 @@ class GCSDataStore extends DataStore {
160140
};
161141

162142
const write_stream = destination.createWriteStream(options);
163-
if (!write_stream || req.destroyed) {
143+
if (!write_stream || readable.destroyed) {
164144
reject(ERRORS.FILE_WRITE_ERROR);
165145
return;
166146
}
167147

168-
let new_offset = data.size;
169-
req.on('data', (buffer) => {
170-
new_offset += buffer.length;
148+
let bytes_received = data.size;
149+
readable.on('data', (buffer) => {
150+
bytes_received += buffer.length;
171151
});
172152

173-
stream.pipeline(req, write_stream, async(e) => {
153+
stream.pipeline(readable, write_stream, async(e) => {
174154
if (e) {
175155
log(e);
176156
try {
@@ -181,19 +161,15 @@ class GCSDataStore extends DataStore {
181161
}
182162
}
183163
else {
184-
log(`${new_offset} bytes written`);
164+
log(`${bytes_received} bytes written`);
185165

186166
try {
187167
if (file !== destination) {
188168
await this.bucket.combine([file, destination], file);
189169
await Promise.all([file.setMetadata(options.metadata), destination.delete({ ignoreNotFound: true })]);
190170
}
191171

192-
if (data.upload_length === new_offset) {
193-
this.emit(EVENTS.EVENT_UPLOAD_COMPLETE, { file });
194-
}
195-
196-
resolve(new_offset);
172+
resolve(bytes_received);
197173
}
198174
catch (err) {
199175
log(err);
@@ -238,11 +214,11 @@ class GCSDataStore extends DataStore {
238214
}
239215

240216
if (metadata.metadata.upload_length) {
241-
data.upload_length = parseInt(metadata.metadata.upload_length, 10);
217+
data.upload_length = metadata.metadata.upload_length;
242218
}
243219

244220
if (metadata.metadata.upload_defer_length) {
245-
data.upload_defer_length = parseInt(metadata.metadata.upload_defer_length, 10);
221+
data.upload_defer_length = metadata.metadata.upload_defer_length;
246222
}
247223

248224
if (metadata.metadata.upload_metadata) {

‎lib/stores/S3Store.js

+23-47
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,11 @@
22

33
const assert = require('assert');
44
const os = require('os');
5-
const File = require('../models/File');
5+
66
const DataStore = require('./DataStore');
77
const { FileStreamSplitter } = require('../models/StreamSplitter');
88
const aws = require('aws-sdk');
9-
const ERRORS = require('../constants').ERRORS;
10-
const EVENTS = require('../constants').EVENTS;
11-
const TUS_RESUMABLE = require('../constants').TUS_RESUMABLE;
9+
const { ERRORS, TUS_RESUMABLE } = require('../constants');
1210

1311
const debug = require('debug');
1412
const fs = require('fs');
@@ -59,7 +57,6 @@ class S3Store extends DataStore {
5957
assert.ok(options.secretAccessKey, '[S3Store] `secretAccessKey` must be set');
6058
assert.ok(options.bucket, '[S3Store] `bucket` must be set');
6159

62-
this.tmp_dir_prefix = options.tmpDirPrefix || 'tus-s3-store';
6360
this.bucket_name = options.bucket;
6461
this.part_size = options.partSize || 8 * 1024 * 1024;
6562

@@ -160,7 +157,9 @@ class S3Store extends DataStore {
160157

161158
return data.UploadId;
162159
})
163-
.then((upload_id) => this._saveMetadata(file, upload_id))
160+
.then((upload_id) => {
161+
return this._saveMetadata(file, upload_id);
162+
})
164163
.catch((err) => {
165164
throw err;
166165
});
@@ -253,15 +252,14 @@ class S3Store extends DataStore {
253252
if (!metadata_string) {
254253
return {};
255254
}
256-
257255
const kv_pair_list = metadata_string.split(',');
258256

259257
return kv_pair_list.reduce((metadata, kv_pair) => {
260258
const [key, base64_value] = kv_pair.split(' ');
261259

262260
metadata[key] = {
263261
encoded: base64_value,
264-
decoded: base64_value ? Buffer.from(base64_value, 'base64').toString('ascii') : undefined,
262+
decoded: base64_value === undefined ? undefined : Buffer.from(base64_value, 'base64').toString('ascii'),
265263
};
266264

267265
return metadata;
@@ -458,41 +456,27 @@ class S3Store extends DataStore {
458456
delete this.cache[file_id];
459457
}
460458

461-
async create(req) {
462-
const upload_length = req.headers['upload-length'];
463-
const upload_defer_length = req.headers['upload-defer-length'];
464-
const upload_metadata = req.headers['upload-metadata'];
465-
466-
if (upload_length === undefined && upload_defer_length === undefined) {
467-
throw ERRORS.INVALID_LENGTH;
468-
}
469-
470-
let file_id;
471-
472-
try {
473-
file_id = this.generateFileName(req);
474-
}
475-
catch (err) {
476-
log('create: check your `namingFunction`. Error', err);
477-
throw ERRORS.FILE_WRITE_ERROR;
478-
}
479-
480-
const file = new File(file_id, upload_length, upload_defer_length, upload_metadata);
481-
459+
create(file) {
482460
return this._bucketExists()
483-
.then(() => this._initMultipartUpload(file))
484-
.then((data) => {
485-
this.emit(EVENTS.EVENT_FILE_CREATED, data);
486-
487-
return data.file;
461+
.then(() => {
462+
return this._initMultipartUpload(file);
488463
})
464+
.then(() => file)
489465
.catch((err) => {
490-
this._clearCache(file_id);
466+
this._clearCache(file.id);
491467
throw err;
492468
});
493469
}
494470

495-
write(req, file_id) {
471+
/**
472+
* Write to the file, starting at the provided offset
473+
*
474+
* @param {object} readable stream.Readable
475+
* @param {string} file_id Name of file
476+
* @param {integer} offset starting offset
477+
* @return {Promise}
478+
*/
479+
write(readable, file_id) {
496480
return this._getMetadata(file_id)
497481
.then((metadata) => {
498482
return Promise.all([metadata, this._countParts(file_id), this.getOffset(file_id)]);
@@ -501,22 +485,14 @@ class S3Store extends DataStore {
501485
const [metadata, part_number, initial_offset] = results;
502486
const next_part_number = part_number + 1;
503487

504-
return Promise.allSettled(
505-
await this._processUpload(metadata, req, next_part_number, initial_offset.size)
488+
return Promise.all(
489+
await this._processUpload(metadata, readable, next_part_number, initial_offset.size)
506490
)
507491
.then(() => this.getOffset(file_id))
508492
.then((current_offset) => {
509493
if (parseInt(metadata.file.upload_length, 10) === current_offset.size) {
510494
return this._finishMultipartUpload(metadata, current_offset.parts)
511-
.then((location) => {
512-
log(`[${metadata.file.id}] finished uploading: ${location}`);
513-
514-
this.emit(EVENTS.EVENT_UPLOAD_COMPLETE, {
515-
file: Object.assign({}, metadata.file, {
516-
location,
517-
}),
518-
});
519-
495+
.then(() => {
520496
this._clearCache(file_id);
521497

522498
return current_offset.size;

‎test/Test-BaseHandler.js

+8-8
Original file line numberDiff line numberDiff line change
@@ -28,33 +28,33 @@ describe('BaseHandler', () => {
2828
done();
2929
});
3030

31-
it('send() should end the response', (done) => {
32-
handler.send(res, 200, {});
31+
it('write() should end the response', (done) => {
32+
handler.write(res, 200, {});
3333
assert.equal(res.finished, true)
3434
done();
3535
});
3636

37-
it('send() should set a response code', (done) => {
38-
handler.send(res, 201, {});
37+
it('write() should set a response code', (done) => {
38+
handler.write(res, 201, {});
3939
assert.equal(res.statusCode, 201)
4040
done();
4141
});
4242

43-
it('send() should set headers', (done) => {
43+
it('write() should set headers', (done) => {
4444
let headers = {
4545
'Access-Control-Allow-Methods': 'GET, OPTIONS',
4646
};
47-
handler.send(res, 200, headers);
47+
handler.write(res, 200, headers);
4848
for (let header of Object.keys(headers)) {
4949
assert.equal(res.getHeader(header), headers[header]);
5050
}
5151
done();
5252
});
5353

5454

55-
it('send() should write the body', (done) => {
55+
it('write() should write the body', (done) => {
5656
const body = 'Hello tus!'
57-
handler.send(res, 200, {}, body);
57+
handler.write(res, 200, {}, body);
5858
let output = res._getData();
5959
assert.equal(output.match(/Hello tus!$/).index, output.length - body.length)
6060
done();

‎test/Test-DataStore.js

-15
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,6 @@ const File = require('../lib/models/File');
1010
describe('DataStore', () => {
1111
const datastore = new DataStore({path: '/test/output'});
1212

13-
it('constructor must require a path', (done) => {
14-
assert.throws(() => { new DataStore() }, Error);
15-
done();
16-
});
17-
18-
it('constructor must require the namingFunction to be a function, if it is provided', (done) => {
19-
assert.throws(() => { new DataStore({ path: '/test/output', namingFunction: {} }) }, Error);
20-
done();
21-
});
22-
23-
it('relativeLocation option must be boolean', (done) => {
24-
assert.equal(typeof datastore.relativeLocation, 'boolean');
25-
done();
26-
});
27-
2813
it('should provide extensions', (done) => {
2914
datastore.should.have.property('extensions');
3015
assert.equal(datastore.extensions, null);

‎test/Test-DeleteHandler.js

+42-43
Original file line numberDiff line numberDiff line change
@@ -3,54 +3,53 @@
33

44
const assert = require('assert');
55
const http = require('http');
6-
const fs = require('fs');
7-
const FileStore = require('../lib/stores/FileStore');
6+
7+
const sinon = require('sinon');
8+
const should = require('should');
9+
10+
const DataStore = require('../lib/stores/DataStore');
811
const DeleteHandler = require('../lib/handlers/DeleteHandler');
12+
const { ERRORS, EVENTS } = require('../lib/constants');
913

1014
describe('DeleteHandler', () => {
11-
let res = null;
1215
const path = '/test/output';
13-
const pathClean = path.replace(/^\//, '');
14-
const namingFunction = (req) => req.url.replace(/\//g, '-');
15-
const store = new FileStore({ path, namingFunction});
16-
const handler = new DeleteHandler(store);
17-
const filePath = "/1234";
18-
const req = { headers: {}, url: path+filePath};
19-
20-
beforeEach((done) => {
21-
res = new http.ServerResponse({ method: 'DELETE' });
22-
done();
16+
const fake_store = sinon.createStubInstance(DataStore);
17+
let handler;
18+
19+
let req = null;
20+
let res = null;
21+
22+
beforeEach(() => {
23+
fake_store.remove.resetHistory();
24+
handler = new DeleteHandler(fake_store, { relativeLocation: true, path });
25+
26+
req = { headers: {}, url: handler.generateUrl({}, '1234') };
27+
res = new http.ServerResponse({ method: 'HEAD' });
28+
});
29+
30+
it('should 404 if no file id match', () => {
31+
fake_store.remove.rejects(ERRORS.FILE_NOT_FOUND);
32+
return assert.rejects(() => handler.send(req, res), { status_code: 404 });
33+
});
34+
35+
it('should 404 if no file ID', async () => {
36+
sinon.stub(handler, "getFileIdFromRequest").returns(false);
37+
await assert.rejects(() => handler.send(req, res), { status_code: 404 });
38+
assert.equal(fake_store.remove.callCount, 0);
39+
});
40+
41+
it('must acknowledge successful DELETE requests with the 204', async () => {
42+
fake_store.remove.resolves();
43+
await handler.send(req, res);
44+
assert.equal(res.statusCode, 204);
2345
});
2446

25-
describe('send()', () => {
26-
it('must be 404 if no file found', (done) => {
27-
handler.send(req, res)
28-
.then(() => {
29-
assert.equal(res.statusCode, 404);
30-
return done();
31-
})
32-
.catch(done);
33-
});
34-
35-
it('must be 404 if invalid path', (done) => {
36-
let new_req = Object.assign({}, req);
37-
new_req.url = '/test/should/not/work/1234';
38-
handler.send(new_req, res)
39-
.then(() => {
40-
assert.equal(res.statusCode, 404);
41-
return done();
42-
})
43-
.catch(done);
44-
});
45-
46-
it('must acknowledge successful DELETE requests with the 204', (done) => {
47-
fs.closeSync(fs.openSync(pathClean+filePath, 'w'));
48-
handler.send(req, res)
49-
.then(() => {
50-
assert.equal(res.statusCode, 204);
51-
return done();
52-
})
53-
.catch(done);
54-
});
47+
it(`must fire the ${EVENTS.EVENT_FILE_DELETED} event`, (done) => {
48+
fake_store.remove.resolves();
49+
handler.on(EVENTS.EVENT_FILE_DELETED, (event) => {
50+
assert.equal(event.file_id, '1234');
51+
done();
52+
})
53+
handler.send(req, res);
5554
});
5655
});

‎test/Test-EndToEnd.js

+11-6
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,11 @@ describe('EndToEnd', () => {
4646
let file_id;
4747
let deferred_file_id;
4848
before(() => {
49-
server = new Server();
49+
server = new Server({
50+
path: STORE_PATH
51+
});
5052
server.datastore = new FileStore({
51-
path: STORE_PATH,
53+
directory: `./${STORE_PATH}`
5254
});
5355
listener = server.listen();
5456
agent = request.agent(listener);
@@ -251,12 +253,14 @@ describe('EndToEnd', () => {
251253

252254
describe('FileStore with relativeLocation', () => {
253255
before(() => {
254-
server = new Server();
255-
server.datastore = new FileStore({
256+
server = new Server({
256257
path: STORE_PATH,
257258
// configure the store to return relative path in Location Header
258259
relativeLocation: true
259260
});
261+
server.datastore = new FileStore({
262+
directory: `./${STORE_PATH}`
263+
});
260264
listener = server.listen();
261265
agent = request.agent(listener);
262266
});
@@ -292,9 +296,10 @@ describe('EndToEnd', () => {
292296
let deferred_file_id;
293297
const files_created = [];
294298
before(() => {
295-
server = new Server();
299+
server = new Server({
300+
path: STORE_PATH
301+
});
296302
server.datastore = new GCSDataStore({
297-
path: STORE_PATH,
298303
projectId: PROJECT_ID,
299304
keyFilename: KEYFILE,
300305
bucket: BUCKET,

‎test/Test-FileStore.js

+59-11
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,20 @@
22
const assert = require('assert')
33
const fs = require('fs')
44
const path = require('path')
5-
const Server = require('../lib/Server')
5+
6+
const sinon = require('sinon')
7+
const should = require('should')
8+
69
const FileStore = require('../lib/stores/FileStore')
10+
const MemoryConfigstore = require('../lib/configstores/MemoryConfigstore')
11+
const File = require('../lib/models/File')
12+
const { ERRORS, EVENTS } = require('../lib/constants')
13+
714

815
const shared = require('./Test-Stores.shared')
916

1017
describe('FileStore', function () {
11-
before(function() {
18+
before(function () {
1219
this.testFileSize = 960244
1320
this.testFileName = 'test.mp4'
1421
this.storePath = '/test/output'
@@ -17,22 +24,63 @@ describe('FileStore', function () {
1724
})
1825

1926
beforeEach(function () {
20-
this.server = new Server()
21-
this.server.datastore = new FileStore({ path: this.storePath })
27+
sinon.spy(fs, "mkdir")
28+
29+
this.datastore = new FileStore({
30+
directory: `${this.storePath.substring(1)}`,
31+
configstore: new MemoryConfigstore(),
32+
})
2233
})
2334

24-
it('should create a directory for the files', function (done) {
25-
const stats = fs.lstatSync(this.filesDirectory)
26-
assert.equal(stats.isDirectory(), true)
27-
done()
35+
this.afterEach(function () {
36+
fs.mkdir.restore()
2837
})
2938

30-
it('should reject when the directory doesnt exist', function (done) {
31-
this.server.datastore.directory = 'some_new_path'
32-
assert.throws(() => this.server.datastore.create(req))
39+
it('should create a directory for the files', function (done) {
40+
assert(fs.mkdir.calledOnce);
41+
assert.equal(this.datastore.directory, fs.mkdir.getCall(0).args[0]);
3342
done()
3443
})
3544

45+
describe('create', function () {
46+
const file = new File('1234', '1000');
47+
48+
it('should reject when the directory doesnt exist', function () {
49+
this.datastore.directory = 'some_new_path';
50+
return this.datastore.create(file).should.be.rejected()
51+
});
52+
53+
it('should resolve when the directory exists', function () {
54+
return this.datastore.create(file).should.be.fulfilled();
55+
});
56+
57+
it('should create an empty file', async function () {
58+
// TODO: this test would pass even if `datastore.create` would not create any file
59+
// as the file probably already exists from other tests
60+
await this.datastore.create(file);
61+
const stats = fs.statSync(path.join(this.datastore.directory, file.id));
62+
assert.equal(stats.size, 0);
63+
});
64+
});
65+
66+
describe('write', function () {
67+
const file = new File('1234', `${this.testFileSize}`, undefined, 'filename d29ybGRfZG9taW5hdGlvbl9wbGFuLnBkZg==,is_confidential');
68+
69+
it('created file\'s size should match \'upload_length\'', async function () {
70+
await this.datastore.create(file);
71+
await this.datastore.write(fs.createReadStream(this.testFilePath), file.id, 0);
72+
73+
const stats = fs.statSync(this.testFilePath);
74+
assert.equal(stats.size, this.testFileSize);
75+
});
76+
});
77+
78+
describe('getOffset', function () {
79+
it('should reject directories', function () {
80+
return this.datastore.getOffset('').should.be.rejected();
81+
});
82+
});
83+
3684
shared.shouldHaveStoreMethods()
3785
shared.shouldCreateUploads()
3886
shared.shouldRemoveUploads() // termination extension

‎test/Test-GCSDataStore.js

+1-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
'use strict'
22
const path = require('path')
33

4-
const Server = require('../lib/Server')
54
const GCSDataStore = require('../lib/stores/GCSDataStore')
65

76
const shared = require('./Test-Stores.shared')
@@ -15,9 +14,7 @@ describe('GCSDataStore', () => {
1514
})
1615

1716
beforeEach(function () {
18-
this.server = new Server()
19-
this.server.datastore = new GCSDataStore({
20-
path: this.storePath,
17+
this.datastore = new GCSDataStore({
2118
projectId: 'tus-node-server',
2219
keyFilename: path.resolve(__dirname, '../keyfile.json'),
2320
bucket: 'tus-node-server-ci',

‎test/Test-GetHandler.js

+59-82
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const should = require('should');
99
const sinon = require('sinon');
1010
const GetHandler = require('../lib/handlers/GetHandler');
1111
const DataStore = require('../lib/stores/DataStore');
12+
const FileStore = require('../lib/stores/FileStore');
1213
const { spy } = require('sinon');
1314

1415
const hasHeader = (res, header) => {
@@ -18,7 +19,7 @@ const hasHeader = (res, header) => {
1819

1920
describe('GetHandler', () => {
2021
const path = '/test/output';
21-
let req = null;
22+
let req = null;
2223
let res = null;
2324

2425
beforeEach((done) => {
@@ -30,135 +31,113 @@ describe('GetHandler', () => {
3031
describe('test error responses', () => {
3132

3233
it('should 404 when file does not exist', async() => {
33-
const store = new DataStore({ path });
34-
store.read = () => {};
34+
const store = sinon.createStubInstance(FileStore)
35+
store.getOffset.rejects({ status_code: 404 });
3536

36-
const fakeStore = sinon.stub(store)
37-
fakeStore.getOffset.rejects({ status_code: 404 });
38-
39-
const handler = new GetHandler(fakeStore);
37+
const handler = new GetHandler(store, { path });
4038
const spy_getFileIdFromRequest = sinon.spy(handler, 'getFileIdFromRequest');
41-
39+
4240
req.url = `${path}/1234`;
43-
await handler.send(req, res)
44-
assert.equal(res.statusCode, 404);
41+
await assert.rejects(() => handler.send(req, res), { status_code: 404 });
4542
assert.equal(spy_getFileIdFromRequest.calledOnceWith(req), true);
4643
});
47-
44+
4845
it('should 404 for non registered path', async() => {
49-
const store = new DataStore({ path });
50-
store.read = () => {};
46+
const store = sinon.createStubInstance(FileStore)
5147

52-
const fakeStore = sinon.stub(store)
53-
54-
const handler = new GetHandler(fakeStore);
48+
const handler = new GetHandler(store, { path });
5549
const spy_getFileIdFromRequest = sinon.spy(handler, 'getFileIdFromRequest');
56-
57-
req.url = `/not_a_valid_file_path`;
58-
await handler.send(req, res)
59-
assert.equal(res.statusCode, 404);
6050

51+
req.url = `/not_a_valid_file_path`;
52+
await assert.rejects(() => handler.send(req, res), { status_code: 404 });
6153
assert.equal(spy_getFileIdFromRequest.callCount, 1);
6254
});
63-
55+
6456
it('should 404 when file is not complete', async() => {
65-
const store = new DataStore({ path });
66-
store.read = () => {};
57+
const store = sinon.createStubInstance(FileStore)
58+
store.getOffset.resolves({ size: 512, upload_length: '1024' });
59+
60+
const handler = new GetHandler(store, { path });
6761

68-
const fakeStore = sinon.stub(store)
69-
fakeStore.getOffset.resolves({ size: 512, upload_length: '1024' });
70-
71-
const handler = new GetHandler(fakeStore);
72-
7362
const fileId = '1234';
7463
req.url = `${path}/${fileId}`;
75-
await handler.send(req, res)
76-
assert.equal(res.statusCode, 404);
77-
assert.equal(fakeStore.getOffset.calledWith(fileId), true);
64+
await assert.rejects(() => handler.send(req, res), { status_code: 404 });
65+
assert.equal(store.getOffset.calledWith(fileId), true);
7866
});
79-
80-
it('should 500 on error store.getOffset error', async() => {
67+
68+
it('should 500 on error store.getOffset error', () => {
8169
const store = new DataStore({ path });
8270
store.read = () => {};
8371

8472
const fakeStore = sinon.stub(store)
85-
fakeStore.getOffset.rejects({});
86-
73+
fakeStore.getOffset.rejects();
74+
8775
const handler = new GetHandler(fakeStore);
88-
76+
8977
req.url = `${path}/1234`;
90-
await handler.send(req, res);
91-
assert.equal(res.statusCode, 500);
78+
return assert.rejects(() => handler.send(req, res));
9279
});
9380

9481
it('test invalid stream', async() => {
95-
const store = new DataStore({ path });
96-
store.read = () => {};
82+
const store = sinon.createStubInstance(FileStore)
9783

98-
const fakeStore = sinon.stub(store)
99-
10084
const size = 512;
101-
fakeStore.getOffset.resolves({ size, upload_length: size.toString() });
102-
fakeStore.read.returns(stream.Readable.from(fs.createReadStream('invalid_path')));
103-
104-
const handler = new GetHandler(fakeStore);
105-
85+
store.getOffset.resolves({ size, upload_length: size.toString() });
86+
store.read.returns(stream.Readable.from(fs.createReadStream('invalid_path')));
87+
88+
const handler = new GetHandler(store, { path });
89+
10690
const fileId = '1234';
10791
req.url = `${path}/${fileId}`;
10892
await handler.send(req, res);
10993

11094
assert.equal(res.statusCode, 200);
111-
assert.equal(fakeStore.getOffset.calledWith(fileId), true);
112-
assert.equal(fakeStore.read.calledWith(fileId), true);
95+
assert.equal(store.getOffset.calledWith(fileId), true);
96+
assert.equal(store.read.calledWith(fileId), true);
11397
});
11498
})
11599

116100
describe('send()', () => {
117101

118102
it('test if `file_id` is properly passed to store', async() => {
119-
const store = new DataStore({ path });
120-
store.read = () => {};
103+
const store = sinon.createStubInstance(FileStore);
104+
105+
store.getOffset.resolves({ size: 512, upload_length: '512' });
106+
store.read.returns(stream.Readable.from(Buffer.alloc(512)));
107+
108+
const handler = new GetHandler(store, { path });
121109

122-
const fakeStore = sinon.stub(store)
123-
fakeStore.getOffset.resolves({ size: 512, upload_length: '512' });
124-
fakeStore.read.returns(stream.Readable.from(Buffer.alloc(512)));
125-
126-
const handler = new GetHandler(fakeStore);
127-
128110
const fileId = '1234'
129111
req.url = `${path}/${fileId}`;
130112
await handler.send(req, res);
131-
132-
assert.equal(fakeStore.getOffset.calledWith(fileId), true);
133-
assert.equal(fakeStore.read.calledWith(fileId), true);
113+
114+
assert.equal(store.getOffset.calledWith(fileId), true);
115+
assert.equal(store.read.calledWith(fileId), true);
134116
});
135117

136118
it('test successful response', async() => {
137-
const store = new DataStore({ path });
138-
store.read = () => {};
119+
const store = sinon.createStubInstance(FileStore);
139120

140-
const fakeStore = sinon.stub(store)
141-
142121
const size = 512;
143-
fakeStore.getOffset.resolves({ size, upload_length: size.toString() });
144-
fakeStore.read.returns(stream.Readable.from(Buffer.alloc(size), { objectMode: false }));
145-
146-
const handler = new GetHandler(fakeStore);
147-
122+
store.getOffset.resolves({ size, upload_length: size.toString() });
123+
store.read.returns(stream.Readable.from(Buffer.alloc(size), { objectMode: false }));
124+
125+
const handler = new GetHandler(store, { path });
126+
148127
const fileId = '1234';
149128
req.url = `${path}/${fileId}`;
150129
await handler.send(req, res);
151-
130+
152131
assert(res.statusCode, 200);
153132
assert(hasHeader(res, { 'Content-Length': size }), true);
154-
assert(fakeStore.getOffset.calledOnceWith(fileId), true);
155-
assert(fakeStore.read.calledOnceWith(fileId), true);
133+
assert(store.getOffset.calledOnceWith(fileId), true);
134+
assert(store.read.calledOnceWith(fileId), true);
156135
});
157136
});
158137

159138
describe('registerPath()', () => {
160139
it('should call registered path handler', async() => {
161-
const fakeStore = sinon.stub(new DataStore({ path }))
140+
const fakeStore = sinon.stub(new DataStore())
162141
const handler = new GetHandler(fakeStore);
163142

164143
const customPath1 = `/path1`;
@@ -168,7 +147,7 @@ describe('GetHandler', () => {
168147
const customPath2 = `/path2`;
169148
const pathHandler2 = sinon.spy();
170149
handler.registerPath(customPath2, pathHandler2);
171-
150+
172151
req.url = `${customPath1}`;
173152
await handler.send(req, res)
174153
assert.equal(pathHandler1.calledOnceWith(req, res), true);
@@ -181,14 +160,14 @@ describe('GetHandler', () => {
181160
});
182161

183162
it('should not call DataStore when path matches registered path', async() => {
184-
const fakeStore = sinon.stub(new DataStore({ path }))
163+
const fakeStore = sinon.stub(new DataStore())
185164
const handler = new GetHandler(fakeStore);
186-
165+
187166
const spy_getFileIdFromRequest = sinon.spy(handler, 'getFileIdFromRequest');
188167

189168
const customPath = `/path`;
190169
handler.registerPath(customPath, () => {});
191-
170+
192171
req.url = `${customPath}`;
193172
await handler.send(req, res)
194173
assert.equal(spy_getFileIdFromRequest.callCount, 0);
@@ -199,15 +178,13 @@ describe('GetHandler', () => {
199178
describe('DataStore without `read` method', () => {
200179

201180
it('should 404 if not implemented', async() => {
202-
const fakeStore = sinon.stub(new DataStore({ path }))
181+
const fakeStore = sinon.stub(new DataStore())
203182
fakeStore.getOffset.resolves({ size: 512, upload_length: '512' });
204183

205184
const handler = new GetHandler(fakeStore);
206-
207-
req.url = `/${path}/1234`;
208-
await handler.send(req, res);
209185

210-
assert.equal(res.statusCode, 404);
186+
req.url = `/${path}/1234`;
187+
await assert.rejects(() => handler.send(req, res), { status_code: 404 });
211188
});
212189
})
213190

‎test/Test-HeadHandler.js

+59-31
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@
33

44
const assert = require('assert');
55
const http = require('http');
6-
const HeadHandler = require('../lib/handlers/HeadHandler');
6+
7+
const sinon = require('sinon');
8+
79
const DataStore = require('../lib/stores/DataStore');
10+
const HeadHandler = require('../lib/handlers/HeadHandler');
11+
const { ERRORS } = require('../lib/constants');
812

913
const hasHeader = (res, header) => {
1014
const key = Object.keys(header)[0];
@@ -13,43 +17,67 @@ const hasHeader = (res, header) => {
1317

1418
describe('HeadHandler', () => {
1519
const path = '/test/output';
20+
const fake_store = sinon.createStubInstance(DataStore);
21+
const handler = new HeadHandler(fake_store, { relativeLocation: true, path });
22+
23+
let req = null;
1624
let res = null;
17-
const store = new DataStore({ path });
18-
const handler = new HeadHandler(store);
19-
const req = { headers: {} };
2025

21-
beforeEach((done) => {
26+
beforeEach(() => {
27+
req = { headers: {}, url: handler.generateUrl({}, '1234') };
2228
res = new http.ServerResponse({ method: 'HEAD' });
23-
done();
2429
});
2530

26-
it('should 404 if no file id match', (done) => {
27-
req.headers = {};
28-
req.url = '/null';
29-
handler.send(req, res);
30-
assert.equal(res.statusCode, 404);
31-
done();
31+
it('should 404 if no file id match', () => {
32+
fake_store.getOffset.rejects(ERRORS.FILE_NOT_FOUND);
33+
return assert.rejects(() => handler.send(req, res), { status_code: 404 });
3234
});
3335

34-
it('should 404 if no file ID ', (done) => {
35-
req.headers = {};
36+
it('should 404 if no file ID', () => {
3637
req.url = `${path}/`;
37-
handler.send(req, res);
38-
assert.equal(res.statusCode, 404);
39-
done();
40-
});
41-
42-
it('should resolve with the offset', (done) => {
43-
req.headers = { 'upload-offset': 0 };
44-
req.url = `${path}/1234`;
45-
handler.send(req, res)
46-
.then(() => {
47-
assert.equal(hasHeader(res, { 'Upload-Offset': 0 }), true);
48-
assert.equal(hasHeader(res, { 'Upload-Length': 1 }), true);
49-
assert.equal(res.statusCode, 200);
50-
return;
51-
})
52-
.then(done)
53-
.catch(done);
38+
return assert.rejects(() => handler.send(req, res), { status_code: 404 });
39+
});
40+
41+
it('should resolve with the offset and cache-control', async () => {
42+
fake_store.getOffset.resolves({ id: '1234', size: 0, upload_length: 1 });
43+
await handler.send(req, res);
44+
45+
assert.equal(hasHeader(res, { 'Upload-Offset': 0 }), true);
46+
assert.equal(hasHeader(res, { 'Cache-Control': 'no-store' }), true);
47+
assert.equal(res.statusCode, 200);
48+
});
49+
50+
it('should resolve with upload-length', async () => {
51+
const file = { id: '1234', size: 0, upload_length: '1', upload_metadata: 'filename d29ybGRfZG9taW5hdGlvbl9wbGFuLnBkZg==,is_confidential' };
52+
fake_store.getOffset.resolves(file);
53+
await handler.send(req, res);
54+
55+
assert.equal(hasHeader(res, { 'Upload-Length': file.upload_length }), true);
56+
assert.equal(res.hasHeader('Upload-Defer-Length'), false);
57+
});
58+
59+
it('should resolve with upload-defer-length', async () => {
60+
const file = { id: '1234', size: 0, upload_defer_length: '1', upload_metadata: 'filename d29ybGRfZG9taW5hdGlvbl9wbGFuLnBkZg==,is_confidential' };
61+
fake_store.getOffset.resolves(file);
62+
await handler.send(req, res);
63+
64+
assert.equal(hasHeader(res, { 'Upload-Defer-Length': file.upload_defer_length }), true);
65+
assert.equal(res.hasHeader('Upload-Length'), false);
66+
});
67+
68+
it('should resolve with metadata', async () => {
69+
const file = { id: '1234', size: 0, upload_length: '1', upload_metadata: 'filename d29ybGRfZG9taW5hdGlvbl9wbGFuLnBkZg==,is_confidential' };
70+
fake_store.getOffset.resolves(file);
71+
await handler.send(req, res);
72+
73+
assert.equal(hasHeader(res, { 'Upload-Metadata': file.upload_metadata }), true);
74+
});
75+
76+
it('should resolve without metadata', async () => {
77+
const file = { id: '1234', size: 0, upload_length: '1' };
78+
fake_store.getOffset.resolves(file);
79+
await handler.send(req, res);
80+
81+
assert.equal(res.hasHeader('Upload-Metadata'), false);
5482
});
5583
});

‎test/Test-PatchHandler.js

+16-28
Original file line numberDiff line numberDiff line change
@@ -18,55 +18,48 @@ const hasHeader = (res, header) => {
1818
describe('PatchHandler', () => {
1919
const path = '/test/output';
2020
let res = null;
21-
const store = new DataStore({ path });
22-
const handler = new PatchHandler(store);
21+
const store = new DataStore();
22+
const handler = new PatchHandler(store, { path });
2323
const req = { headers: {} };
2424

2525
beforeEach((done) => {
2626
res = new http.ServerResponse({ method: 'PATCH' });
2727
done();
2828
});
2929

30-
it('should 403 if no Content-Type header', (done) => {
30+
it('should 403 if no Content-Type header', () => {
3131
req.headers = {};
3232
req.url = `${path}/1234`;
33-
handler.send(req, res);
34-
assert.equal(res.statusCode, 403);
35-
done();
33+
return assert.rejects(() => handler.send(req, res), { status_code: 403 });
3634
});
3735

38-
it('should 403 if no Upload-Offset header', (done) => {
36+
it('should 403 if no Upload-Offset header', () => {
3937
req.headers = { 'content-type': 'application/offset+octet-stream' };
4038
req.url = `${path}/1234`;
41-
handler.send(req, res);
42-
assert.equal(res.statusCode, 403);
43-
done();
39+
return assert.rejects(() => handler.send(req, res), { status_code: 403 });
4440
});
4541

4642
describe('send()', () => {
4743

4844
it('should 404 urls without a path', () => {
4945
req.url = `${path}/`;
50-
handler.send(req, res);
51-
assert.equal(res.statusCode, 404);
46+
return assert.rejects(() => handler.send(req, res), { status_code: 404 });
5247
});
5348

5449
it('should 403 if the offset is omitted', () => {
5550
req.headers = {
5651
'content-type': 'application/offset+octet-stream',
5752
};
5853
req.url = `${path}/file`;
59-
handler.send(req, res);
60-
assert.equal(res.statusCode, 403);
54+
return assert.rejects(() => handler.send(req, res), { status_code: 403 });
6155
});
6256

6357
it('should 403 the content-type is omitted', () => {
6458
req.headers = {
6559
'upload-offset': 0,
6660
};
6761
req.url = `${path}/file`;
68-
handler.send(req, res);
69-
assert.equal(res.statusCode, 403);
62+
return assert.rejects(() => handler.send(req, res), { status_code: 403 });
7063
});
7164

7265
it('must return a promise if the headers validate', () => {
@@ -85,26 +78,21 @@ describe('PatchHandler', () => {
8578
};
8679
req.url = `${path}/1234`;
8780

88-
return handler.send(req, res)
89-
.then(() => {
90-
assert.equal(res.statusCode, 409);
91-
});
81+
return assert.rejects(() => handler.send(req, res), { status_code: 409 });
9282
});
9383

94-
it('must acknowledge successful PATCH requests with the 204', () => {
84+
it('must acknowledge successful PATCH requests with the 204', async () => {
9585
req.headers = {
9686
'upload-offset': 0,
9787
'content-type': 'application/offset+octet-stream',
9888
};
9989
req.url = `${path}/1234`;
10090

101-
return handler.send(req, res)
102-
.then(() => {
103-
assert.equal(hasHeader(res, { 'Upload-Offset': 0 }), true);
104-
assert.equal(hasHeader(res, 'Content-Length'), false);
105-
assert.equal(res.statusCode, 204);
106-
});
91+
await handler.send(req, res)
92+
93+
assert.equal(hasHeader(res, { 'Upload-Offset': 0 }), true);
94+
assert.equal(hasHeader(res, 'Content-Length'), false);
95+
assert.equal(res.statusCode, 204);
10796
});
10897
});
109-
11098
});

‎test/Test-PostHandler.js

+120-32
Original file line numberDiff line numberDiff line change
@@ -2,60 +2,148 @@
22
'use strict';
33

44
const assert = require('assert');
5-
const should = require('should');
65
const http = require('http');
6+
7+
const should = require('should');
8+
const sinon = require('sinon');
9+
710
const DataStore = require('../lib/stores/DataStore');
811
const PostHandler = require('../lib/handlers/PostHandler');
12+
const { EVENTS } = require('../lib/constants');
913

1014
const hasHeader = (res, header) => {
1115
const key = Object.keys(header)[0];
1216
return res._header.indexOf(`${key}: ${header[key]}`) > -1;
1317
};
1418

1519
describe('PostHandler', () => {
16-
const path = '/test/output';
20+
let req = null;
1721
let res = null;
18-
const namingFunction = (req) => req.url.replace(/\//g, '-');
19-
const store = new DataStore({ path, namingFunction });
20-
const handler = new PostHandler(store);
21-
const req = { headers: {}, url: '/test/output' };
22+
23+
const fake_store = sinon.createStubInstance(DataStore);
2224

2325
beforeEach((done) => {
26+
req = { headers: {}, url: '/files', host: 'localhost:3000' };
2427
res = new http.ServerResponse({ method: 'POST' });
2528
done();
2629
});
2730

31+
describe('constructor()', () => {
32+
it('must check for naming function', () => {
33+
assert.throws(() => { new PostHandler(fake_store); }, Error);
34+
assert.doesNotThrow(() => { new PostHandler(fake_store, { namingFunction: () => '1234' }); })
35+
})
36+
});
37+
2838
describe('send()', () => {
29-
it('must 412 if the Upload-Length and Upload-Defer-Length headers are both missing', (done) => {
30-
req.headers = {};
31-
handler.send(req, res).then(() => {
32-
assert.equal(res.statusCode, 412);
33-
return done();
39+
40+
describe('test errors', () => {
41+
it('must 412 if the Upload-Length and Upload-Defer-Length headers are both missing', async() => {
42+
const handler = new PostHandler(fake_store, { namingFunction: () => '1234' });
43+
44+
req.headers = {};
45+
return assert.rejects(() => handler.send(req, res), { status_code: 412 });
46+
});
47+
48+
it('must 412 if the Upload-Length and Upload-Defer-Length headers are both present', async() => {
49+
const handler = new PostHandler(fake_store, { namingFunction: () => '1234' });
50+
req.headers = { 'upload-length': '512', 'upload-defer-length': '1'};
51+
return assert.rejects(() => handler.send(req, res), { status_code: 412 });
52+
});
53+
54+
it('must 501 if the \'concatenation\' extension is not supported', async() => {
55+
const handler = new PostHandler(fake_store, { namingFunction: () => '1234' });
56+
req.headers = { 'upload-concat': 'partial' };
57+
return assert.rejects(() => handler.send(req, res), { status_code: 501 });
58+
});
59+
60+
it('should send error when naming function throws', async() => {
61+
const fake_store = sinon.createStubInstance(DataStore);
62+
const handler = new PostHandler(fake_store, { namingFunction: sinon.stub().throws() });
63+
64+
req.headers = { 'upload-length': 1000 };
65+
return assert.rejects(() => handler.send(req, res), { status_code: 500 });
66+
});
67+
68+
it('should call custom namingFunction', async() => {
69+
const fake_store = sinon.createStubInstance(DataStore);
70+
const namingFunction = sinon.stub().returns('1234');
71+
const handler = new PostHandler(fake_store, { namingFunction });
72+
73+
req.headers = { 'upload-length': 1000 };
74+
await handler.send(req, res);
75+
assert.equal(namingFunction.calledOnce, true);
76+
});
77+
78+
it('should send error when store rejects', () => {
79+
const fake_store = sinon.createStubInstance(DataStore);
80+
fake_store.create.rejects({ status_code: 500 });
81+
82+
const handler = new PostHandler(fake_store, { namingFunction: () => '1234' });
83+
84+
req.headers = { 'upload-length': 1000 };
85+
return assert.rejects(() => handler.send(req, res), { status_code: 500 });
86+
});
87+
})
88+
89+
describe('test successful scenarios', () => {
90+
it('must acknowledge successful POST requests with the 201', async () => {
91+
const handler = new PostHandler(fake_store, { path: '/test/output', namingFunction: () => '1234' });
92+
req.headers = { 'upload-length': 1000, host: 'localhost:3000' };
93+
await handler.send(req, res)
94+
assert.equal(hasHeader(res, { 'Location': '//localhost:3000/test/output/1234' }), true);
95+
assert.equal(res.statusCode, 201);
96+
});
97+
})
98+
99+
describe('events', () => {
100+
it(`must fire the ${EVENTS.EVENT_FILE_CREATED} event`, (done) => {
101+
const fake_store = sinon.createStubInstance(DataStore);
102+
103+
const file = {};
104+
fake_store.create.resolves(file);
105+
106+
const handler = new PostHandler(fake_store, { namingFunction: () => '1234' });
107+
handler.on(EVENTS.EVENT_FILE_CREATED, (obj) => {
108+
assert.strictEqual(obj.file, file);
109+
done();
110+
});
111+
112+
req.headers = { 'upload-length': 1000 };
113+
handler.send(req, res);
34114
})
35-
.catch(done);
36-
});
37115

38-
it('must 501 if the \'concatenation\' extension is not supported', (done) => {
39-
req.headers = { 'upload-concat': 'partial' };
40-
handler.send(req, res).then(() => {
41-
assert.equal(res.statusCode, 501);
42-
return done();
116+
it(`must fire the ${EVENTS.EVENT_ENDPOINT_CREATED} event with absolute URL`, (done) => {
117+
const fake_store = sinon.createStubInstance(DataStore);
118+
119+
const file = {};
120+
fake_store.create.resolves(file);
121+
122+
const handler = new PostHandler(fake_store, { path: '/test/output', namingFunction: () => '1234' });
123+
handler.on(EVENTS.EVENT_ENDPOINT_CREATED, (obj) => {
124+
assert.strictEqual(obj.url, '//localhost:3000/test/output/1234');
125+
done();
126+
});
127+
128+
req.headers = { 'upload-length': 1000, host: 'localhost:3000' };
129+
handler.send(req, res);
43130
})
44-
.catch(done);
45-
});
46131

47-
it('must acknowledge successful POST requests with the 201', (done) => {
48-
req.headers = { 'upload-length': 1000, host: 'localhost:3000' };
132+
it(`must fire the ${EVENTS.EVENT_ENDPOINT_CREATED} event with relative URL`, (done) => {
133+
const fake_store = sinon.createStubInstance(DataStore);
49134

50-
handler.send(req, res)
51-
.then(() => {
52-
assert.equal(hasHeader(res, { 'Location': '//localhost:3000/test/output/-test-output' }), true);
53-
assert.equal(res.statusCode, 201);
54-
return done();
55-
})
56-
.catch(done);
57-
});
135+
const file = {};
136+
fake_store.create.resolves(file);
58137

59-
});
138+
const handler = new PostHandler(fake_store, { path: '/test/output', relativeLocation: true, namingFunction: () => '1234' });
139+
handler.on(EVENTS.EVENT_ENDPOINT_CREATED, (obj) => {
140+
assert.strictEqual(obj.url, '/test/output/1234');
141+
done();
142+
});
60143

61-
});
144+
req.headers = { 'upload-length': 1000, host: 'localhost:3000' };
145+
handler.send(req, res);
146+
})
147+
});
148+
});
149+
});

‎test/Test-S3DataStore.js

+1-3
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,7 @@ describe('S3DataStore', function () {
1515
})
1616

1717
beforeEach(function() {
18-
this.server = new Server()
19-
this.server.datastore = new S3Store({
20-
path: this.storePath,
18+
this.datastore = new S3Store({
2119
bucket: process.env.AWS_BUCKET,
2220
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
2321
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,

‎test/Test-Server.js

+41-35
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
const request = require('supertest');
55
const assert = require('assert');
66
const http = require('http');
7-
const fs = require('fs');
7+
8+
const should = require('should');
9+
810
const Server = require('../lib/Server');
911
const FileStore = require('../lib/stores/FileStore');
1012
const DataStore = require('../lib/stores/DataStore');
@@ -18,20 +20,27 @@ const hasHeader = (res, header) => {
1820

1921
describe('Server', () => {
2022
describe('instantiation', () => {
21-
it('datastore setter must require a DataStore subclass', (done) => {
23+
it('constructor must require options', () => {
24+
assert.throws(() => { new Server(); }, Error);
25+
assert.throws(() => { new Server({}); }, Error);
26+
});
27+
28+
it('should accept valid options', () => {
29+
assert.doesNotThrow(() => { new Server({ path: '/files' }); });
30+
assert.doesNotThrow(() => { new Server({ path: '/files', namingFunction: () => { return "1234"; } }); });
31+
});
32+
33+
it('should throw on invalid namingFunction', () => {
2234
assert.throws(() => {
23-
const server = new Server();
24-
server.datastore = {};
35+
const server = new Server({ path: '/files', namingFunction: '1234' });
36+
server.datastore = new DataStore();
2537
}, Error);
26-
done();
2738
});
2839

2940
it('setting the DataStore should attach handlers', (done) => {
30-
const server = new Server();
41+
const server = new Server({ path: '/files' });
3142
server.handlers.should.be.empty();
32-
server.datastore = new DataStore({
33-
path: '/test/output',
34-
});
43+
server.datastore = new DataStore();
3544
server.handlers.should.have.property('HEAD');
3645
server.handlers.should.have.property('OPTIONS');
3746
server.handlers.should.have.property('POST');
@@ -40,13 +49,12 @@ describe('Server', () => {
4049
done();
4150
});
4251
});
52+
4353
describe('listen', () => {
4454
let server;
4555
before(() => {
46-
server = new Server();
47-
server.datastore = new DataStore({
48-
path: '/test/output',
49-
});
56+
server = new Server({ path: '/test/output' });
57+
server.datastore = new DataStore();
5058
});
5159

5260
it('should create an instance of http.Server', (done) => {
@@ -61,10 +69,8 @@ describe('Server', () => {
6169
let server;
6270
let listener;
6371
before(() => {
64-
server = new Server();
65-
server.datastore = new DataStore({
66-
path: '/test/output',
67-
});
72+
server = new Server({ path: '/test/output' });
73+
server.datastore = new DataStore();
6874

6975
server.get('/some_url', (req, res) => {
7076
res.writeHead(200);
@@ -96,9 +102,9 @@ describe('Server', () => {
96102
let server;
97103
let listener;
98104
before(() => {
99-
server = new Server();
105+
server = new Server({ path: '/test/output' });
100106
server.datastore = new FileStore({
101-
path: '/test/output',
107+
directory: './test/output',
102108
});
103109

104110
listener = server.listen();
@@ -136,30 +142,30 @@ describe('Server', () => {
136142

137143
it('POST should require Upload-Length header', (done) => {
138144
request(listener)
139-
.post(server.datastore.path)
145+
.post(server.options.path)
140146
.set('Tus-Resumable', TUS_RESUMABLE)
141147
.expect(412, {}, done);
142148
});
143149

144150
it('POST should require non negative Upload-Length number', (done) => {
145151
request(listener)
146-
.post(server.datastore.path)
152+
.post(server.options.path)
147153
.set('Tus-Resumable', TUS_RESUMABLE)
148154
.set('Upload-Length', -3)
149155
.expect(412, 'Invalid upload-length\n', done);
150156
});
151157

152158
it('POST should validate the metadata header', (done) => {
153159
request(listener)
154-
.post(server.datastore.path)
160+
.post(server.options.path)
155161
.set('Tus-Resumable', TUS_RESUMABLE)
156162
.set('Upload-Metadata', '')
157163
.expect(412, 'Invalid upload-metadata\n', done);
158164
});
159165

160166
it('DELETE should return 404 when file does not exist', (done) => {
161167
request(server.listen())
162-
.delete(server.datastore.path + "/123")
168+
.delete(server.options.path + "/123")
163169
.set('Tus-Resumable', TUS_RESUMABLE)
164170
.expect(404, 'The file for this url was not found\n', done);
165171
});
@@ -168,12 +174,12 @@ describe('Server', () => {
168174
request(server.listen())
169175
.delete("/this/is/wrong/123")
170176
.set('Tus-Resumable', TUS_RESUMABLE)
171-
.expect(404, 'Invalid path name\n', done);
177+
.expect(404, 'The file for this url was not found\n', done);
172178
});
173179

174180
it('DELETE should return 204 on proper deletion', (done) => {
175181
request(server.listen())
176-
.post(server.datastore.path)
182+
.post(server.options.path)
177183
.set('Tus-Resumable', TUS_RESUMABLE)
178184
.set('Upload-Length', 12345678)
179185
.then((res)=>{
@@ -185,7 +191,7 @@ describe('Server', () => {
185191

186192
it('POST should ignore invalid Content-Type header', (done) => {
187193
request(listener)
188-
.post(server.datastore.path)
194+
.post(server.options.path)
189195
.set('Tus-Resumable', TUS_RESUMABLE)
190196
.set('Upload-Length', 300)
191197
.set('Upload-Metadata', 'foo aGVsbG8=, bar d29ynGQ=')
@@ -227,9 +233,9 @@ describe('Server', () => {
227233
let server;
228234
let listener;
229235
beforeEach(() => {
230-
server = new Server();
236+
server = new Server({ path: '/test/output' });
231237
server.datastore = new FileStore({
232-
path: '/test/output',
238+
directory: './test/output',
233239
});
234240

235241
listener = server.listen();
@@ -246,7 +252,7 @@ describe('Server', () => {
246252
});
247253

248254
request(listener)
249-
.post(server.datastore.path)
255+
.post(server.options.path)
250256
.set('Tus-Resumable', TUS_RESUMABLE)
251257
.set('Upload-Length', 12345678)
252258
.end((err) => { if(err) done(err) });
@@ -259,10 +265,10 @@ describe('Server', () => {
259265
});
260266

261267
request(listener)
262-
.post(server.datastore.path)
268+
.post(server.options.path)
263269
.set('Tus-Resumable', TUS_RESUMABLE)
264270
.set('Upload-Length', 12345678)
265-
.end((err) => { if(err) done(err) });
271+
.end((err) => { if (err) done(err) });
266272
});
267273

268274
it('should fire when a file is deleted', (done) => {
@@ -272,14 +278,14 @@ describe('Server', () => {
272278
});
273279

274280
request(server.listen())
275-
.post(server.datastore.path)
281+
.post(server.options.path)
276282
.set('Tus-Resumable', TUS_RESUMABLE)
277283
.set('Upload-Length', 12345678)
278284
.then((res)=>{
279285
request(server.listen())
280286
.delete(res.headers.location)
281287
.set('Tus-Resumable', TUS_RESUMABLE)
282-
.end((err,res)=>{});
288+
.end((err) => { if (err) done(err) });
283289
});
284290
});
285291

@@ -290,7 +296,7 @@ describe('Server', () => {
290296
});
291297

292298
request(server.listen())
293-
.post(server.datastore.path)
299+
.post(server.options.path)
294300
.set('Tus-Resumable', TUS_RESUMABLE)
295301
.set('Upload-Length', Buffer.byteLength('test', 'utf8'))
296302
.then((res) => {
@@ -300,7 +306,7 @@ describe('Server', () => {
300306
.set('Tus-Resumable', TUS_RESUMABLE)
301307
.set('Upload-Offset', 0)
302308
.set('Content-Type', 'application/offset+octet-stream')
303-
.end((err,res)=>{});
309+
.end((err) => { if (err) done(err) });
304310
})
305311
});
306312
})

0 commit comments

Comments
 (0)
Please sign in to comment.