Skip to content

Commit e98ac2d

Browse files
authored
Merge pull request #45 from tus/hooks
Hooks
2 parents e2e7e80 + 773cc0a commit e98ac2d

File tree

14 files changed

+252
-15
lines changed

14 files changed

+252
-15
lines changed

README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,58 @@ app.all('/files/*', function(req, res) {
8181
app.listen(port, host);
8282
```
8383
84+
## Event Hooks
85+
86+
Execute code when lifecycle events happen by adding event handlers to your server.
87+
88+
```js
89+
const Server = require('tus-node-server').Server;
90+
const EVENTS = require('tus-node-server').EVENTS;
91+
92+
const server = new Server();
93+
server.on(EVENTS.EVENT_UPLOAD_COMPLETE, (event) => {
94+
console.log(`[${new Date().toLocaleTimeString()}] [EVENT HOOK] Upload complete for file ${event.file.id}`);
95+
console.log(event);
96+
});
97+
```
98+
99+
#### Events:
100+
101+
- `EVENT_FILE_CREATED`: Fired when a `POST` request successfully creates a new file
102+
103+
_Example payload:_
104+
```
105+
{
106+
file: {
107+
id: '7b26bf4d22cf7198d3b3706bf0379794',
108+
upload_length: '41767441',
109+
upload_metadata: 'filename NDFfbWIubXA0'
110+
}
111+
}
112+
```
113+
114+
- `EVENT_ENDPOINT_CREATED`: Fired when a `POST` request successfully creates a new upload endpoint
115+
116+
_Example payload:_
117+
```
118+
{
119+
url: 'http://localhost:8000/files/7b26bf4d22cf7198d3b3706bf0379794'
120+
}
121+
```
122+
123+
- `EVENT_UPLOAD_COMPLETE`: Fired when a `PATCH` request finishes writing the file.
124+
125+
_Example payload:_
126+
```
127+
{
128+
file: {
129+
id: '7b26bf4d22cf7198d3b3706bf0379794',
130+
upload_length: '41767441',
131+
upload_metadata: 'filename NDFfbWIubXA0'
132+
}
133+
}
134+
```
135+
84136
## Development
85137
86138
Start the demo server using Local File Storage

demo/server.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
'use strict';
22

3-
const tus = require('../index');
43
const path = require('path');
54
const fs = require('fs');
65

7-
const server = new tus.Server();
6+
const Server = require('../index').Server;
7+
const FileStore = require('../index').FileStore;
8+
const GCSDataStore = require('../index').GCSDataStore;
9+
const EVENTS = require('../index').EVENTS;
10+
11+
const server = new Server();
812

913
const data_store = process.env.DATA_STORE || 'FileStore';
1014

1115
switch (data_store) {
1216
case 'GCSDataStore':
13-
server.datastore = new tus.GCSDataStore({
17+
server.datastore = new GCSDataStore({
1418
path: '/files',
1519
projectId: 'vimeo-open-source',
1620
keyFilename: path.resolve(__dirname, '../test/keyfile.json'),
@@ -19,7 +23,7 @@ switch (data_store) {
1923
break;
2024

2125
default:
22-
server.datastore = new tus.FileStore({
26+
server.datastore = new FileStore({
2327
path: '/files',
2428
});
2529
}
@@ -52,6 +56,10 @@ server.get('/', writeFile);
5256
server.get('/demo/index.js', writeFile);
5357
server.get('/node_modules/tus-js-client/dist/tus.js', writeFile);
5458

59+
server.on(EVENTS.EVENT_UPLOAD_COMPLETE, (event) => {
60+
console.log(`[${new Date().toLocaleTimeString()}] [EVENT HOOK] Upload complete for file ${event.file.id}`);
61+
});
62+
5563
const host = '127.0.0.1';
5664
const port = 8000;
5765
server.listen({ host, port }, () => {

index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ const Server = require('./lib/Server');
44
const DataStore = require('./lib/stores/DataStore');
55
const FileStore = require('./lib/stores/FileStore');
66
const GCSDataStore = require('./lib/stores/GCSDataStore');
7+
const EVENTS = require('./lib/constants').EVENTS;
78

89
module.exports = {
910
Server,
1011
DataStore,
1112
FileStore,
1213
GCSDataStore,
14+
EVENTS,
1315
};

lib/Server.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* @author Ben Stahl <[email protected]>
88
*/
99
const http = require('http');
10+
const EventEmitter = require('events');
1011

1112
const DataStore = require('./stores/DataStore');
1213
const HeadHandler = require('./handlers/HeadHandler');
@@ -16,15 +17,37 @@ const PostHandler = require('./handlers/PostHandler');
1617
const RequestValidator = require('./validators/RequestValidator');
1718

1819
const EXPOSED_HEADERS = require('./constants').EXPOSED_HEADERS;
20+
const REQUEST_METHODS = require('./constants').REQUEST_METHODS;
1921
const TUS_RESUMABLE = require('./constants').TUS_RESUMABLE;
2022

21-
class TusServer {
23+
class TusServer extends EventEmitter {
2224

2325
constructor() {
26+
super();
27+
2428
// Any handlers assigned to this object with the method as the key
2529
// will be used to repond to those requests. They get set/re-set
2630
// when a datastore is assigned to the server.
2731
this.handlers = {};
32+
33+
// Remove any event listeners from each handler as they are removed
34+
// from the server. This must come before adding a 'newListener' listener,
35+
// to not add a 'removeListener' event listener to all request handlers.
36+
this.on('removeListener', (event, listener) => {
37+
this.datastore.removeListener(event, listener);
38+
REQUEST_METHODS.forEach((method) => {
39+
this.handlers[method].removeListener(event, listener);
40+
});
41+
});
42+
43+
// As event listeners are added to the server, make sure they are
44+
// bubbled up from request handlers to fire on the server level.
45+
this.on('newListener', (event, listener) => {
46+
this.datastore.on(event, listener);
47+
REQUEST_METHODS.forEach((method) => {
48+
this.handlers[method].on(event, listener);
49+
});
50+
});
2851
}
2952

3053
/**

lib/constants.js

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use strict';
22

3-
const METHODS = [
3+
const REQUEST_METHODS = [
44
'POST',
55
'HEAD',
66
'PATCH',
@@ -59,14 +59,29 @@ const ERRORS = {
5959
},
6060
};
6161

62+
const EVENT_ENDPOINT_CREATED = 'EVENT_ENDPOINT_CREATED';
63+
const EVENT_FILE_CREATED = 'EVENT_FILE_CREATED';
64+
const EVENT_UPLOAD_COMPLETE = 'EVENT_UPLOAD_COMPLETE';
65+
66+
const EVENTS = {
67+
EVENT_ENDPOINT_CREATED,
68+
EVENT_FILE_CREATED,
69+
EVENT_UPLOAD_COMPLETE,
70+
};
71+
6272
module.exports = {
63-
TUS_RESUMABLE: '1.0.0',
64-
TUS_VERSION: ['1.0.0'],
65-
HEADERS,
66-
HEADERS_LOWERCASE,
67-
ALLOWED_METHODS: METHODS.join(', '),
6873
ALLOWED_HEADERS: HEADERS.join(', '),
74+
ALLOWED_METHODS: REQUEST_METHODS.join(', '),
75+
ERRORS,
76+
EVENT_ENDPOINT_CREATED,
77+
EVENT_FILE_CREATED,
78+
EVENT_UPLOAD_COMPLETE,
79+
EVENTS,
6980
EXPOSED_HEADERS: HEADERS.join(', '),
81+
HEADERS,
82+
HEADERS_LOWERCASE,
7083
MAX_AGE: 86400,
71-
ERRORS,
84+
REQUEST_METHODS,
85+
TUS_RESUMABLE: '1.0.0',
86+
TUS_VERSION: ['1.0.0'],
7287
};

lib/handlers/BaseHandler.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
'use strict';
22

33
const DataStore = require('../stores/DataStore');
4+
const EventEmitter = require('events');
45

5-
class BaseHandler {
6+
7+
class BaseHandler extends EventEmitter {
68
constructor(store) {
9+
super();
710
if (!(store instanceof DataStore)) {
811
throw new Error(`${store} is not a DataStore`);
912
}

lib/handlers/PostHandler.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
const BaseHandler = require('./BaseHandler');
44
const ERRORS = require('../constants').ERRORS;
5+
const EVENT_ENDPOINT_CREATED = require('../constants').EVENT_ENDPOINT_CREATED;
56

67
class PostHandler extends BaseHandler {
78
/**
@@ -15,6 +16,7 @@ class PostHandler extends BaseHandler {
1516
return this.store.create(req)
1617
.then((File) => {
1718
const url = `http://${req.headers.host}${this.store.path}/${File.id}`;
19+
this.emit(EVENT_ENDPOINT_CREATED, { url });
1820
return super.send(res, 201, { Location: url });
1921
})
2022
.catch((error) => {

lib/stores/DataStore.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,13 @@
99

1010
const Uid = require('../models/Uid');
1111
const File = require('../models/File');
12+
const EventEmitter = require('events');
1213
const ERRORS = require('../constants').ERRORS;
14+
const EVENTS = require('../constants').EVENTS;
1315

14-
class DataStore {
16+
class DataStore extends EventEmitter {
1517
constructor(options) {
18+
super();
1619
if (!options || !options.path) {
1720
throw new Error('Store must have a path');
1821
}
@@ -59,6 +62,7 @@ class DataStore {
5962
const file_id = this.generateFileName(req);
6063
const file = new File(file_id, upload_length, upload_defer_length, upload_metadata);
6164

65+
this.emit(EVENTS.EVENT_FILE_CREATED, { file });
6266
return resolve(file);
6367
});
6468
}
@@ -78,6 +82,8 @@ class DataStore {
7882
return new Promise((resolve, reject) => {
7983
// Stub resolve for tests
8084
const offset = 0;
85+
86+
this.emit(EVENTS.EVENT_UPLOAD_COMPLETE, { file: null });
8187
return resolve(offset);
8288
});
8389
}
@@ -95,6 +101,7 @@ class DataStore {
95101
if (!id) {
96102
return reject(ERRORS.FILE_NOT_FOUND);
97103
}
104+
98105
return resolve({ size: 0, upload_length: 1 });
99106
});
100107
}

lib/stores/FileStore.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const MASK = '0777';
99
const IGNORED_MKDIR_ERROR = 'EEXIST';
1010
const FILE_DOESNT_EXIST = 'ENOENT';
1111
const ERRORS = require('../constants').ERRORS;
12+
const EVENTS = require('../constants').EVENTS;
1213

1314

1415
/**
@@ -82,6 +83,7 @@ class FileStore extends DataStore {
8283
return reject(exception);
8384
}
8485

86+
this.emit(EVENTS.EVENT_FILE_CREATED, { file });
8587
return resolve(file);
8688
});
8789
});
@@ -115,6 +117,11 @@ class FileStore extends DataStore {
115117
console.info(`[FileStore] write: ${new_offset} bytes written to ${path}`);
116118
offset += new_offset;
117119
console.info(`[FileStore] write: File is now ${offset} bytes`);
120+
121+
const config = this.configstore.get(file_id);
122+
if (config && parseInt(config.upload_length, 10) === new_offset) {
123+
this.emit(EVENTS.EVENT_UPLOAD_COMPLETE, { file: config });
124+
}
118125
resolve(offset);
119126
});
120127

lib/stores/GCSDataStore.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const gcloud = require('google-cloud');
66
const assign = require('object-assign');
77
const stream = require('stream');
88
const ERRORS = require('../constants').ERRORS;
9+
const EVENTS = require('../constants').EVENTS;
910
const TUS_RESUMABLE = require('../constants').TUS_RESUMABLE;
1011
const DEFAULT_CONFIG = {
1112
scopes: ['https://www.googleapis.com/auth/devstorage.full_control'],
@@ -107,6 +108,7 @@ class GCSDataStore extends DataStore {
107108
fake_stream.pipe(gcs_file.createWriteStream(options))
108109
.on('error', reject)
109110
.on('finish', () => {
111+
this.emit(EVENTS.EVENT_FILE_CREATED, { file });
110112
resolve(file);
111113
});
112114
});
@@ -153,6 +155,11 @@ class GCSDataStore extends DataStore {
153155

154156
req.on('end', () => {
155157
console.log(`${new_offset} bytes written`);
158+
159+
if (data.upload_length === new_offset) {
160+
this.emit(EVENTS.EVENT_UPLOAD_COMPLETE, { file });
161+
}
162+
156163
resolve(new_offset);
157164
});
158165

0 commit comments

Comments
 (0)