Skip to content

Commit bce8f6f

Browse files
committed
Merge pull request #35 from tus/gcs-datastore
Add GCSDataStore
2 parents b6f212a + 5d27391 commit bce8f6f

File tree

11 files changed

+645
-22
lines changed

11 files changed

+645
-22
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,6 @@ files/
3535

3636
*.sublime-workspace
3737

38+
# Keyfile will be decrypted from keyfile.json.enc by travis
39+
# https://docs.travis-ci.com/user/encrypting-files/
40+
test/keyfile.json

.travis.yml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
language: node_js
22
node_js:
3-
- "4.2.4"
3+
- 4.2.4
44
script:
5-
- npm run lint
6-
- npm run coveralls
5+
- npm run lint
6+
- npm run coveralls
7+
before_install:
8+
- openssl aes-256-cbc -K $encrypted_d9f08a58d46a_key -iv $encrypted_d9f08a58d46a_iv
9+
-in keyfile.json.enc -out test/keyfile.json -d

README.md

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,20 @@ $ npm install tus-node-server
1414

1515
## Flexible Data Stores
1616

17-
- Local File Storage
18-
```javascript
17+
- **Local File Storage**
18+
```js
1919
server.datastore = new tus.FileStore({
2020
path: '/files'
2121
});
2222
```
2323

24-
- Google Cloud Storage ([_coming soon_](https://github.com/tus/tus-node-server/issues/20))
25-
```javascript
24+
Try it:
25+
```sh
26+
$ npm run example
27+
```
28+
29+
- **Google Cloud Storage**
30+
```js
2631
2732
server.datastore = new tus.GCSDataStore({
2833
path: '/files',
@@ -31,9 +36,13 @@ $ npm install tus-node-server
3136
bucket: 'bucket-name',
3237
});
3338
```
39+
Try it:
40+
```sh
41+
$ npm run gcs_example
42+
```
3443

35-
- Amazon S3 ([_coming soon_](https://github.com/tus/tus-node-server/issues/12))
36-
```javascript
44+
- **Amazon S3** ([_coming soon_](https://github.com/tus/tus-node-server/issues/12))
45+
```js
3746
3847
server.datastore = new tus.S3Store({
3948
path: '/files',
@@ -44,7 +53,7 @@ $ npm install tus-node-server
4453
## Quick Start
4554

4655
#### Build a standalone server
47-
```javascript
56+
```js
4857
const tus = require('tus-node-server');
4958
5059
const server = new tus.Server();
@@ -61,7 +70,7 @@ server.listen({ host, port }, () => {
6170
6271
#### Alternatively, you could deploy tus-node-server as [Express Middleware](http://expressjs.com/en/guide/using-middleware.html)
6372
64-
```javascript
73+
```js
6574
const tus = require('tus-node-server');
6675
const server = new tus.Server();
6776
server.datastore = new tus.FileStore({

example/server.js

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,30 @@
11
'use strict';
22

33
const tus = require('../index');
4+
const path = require('path');
45

56
const server = new tus.Server();
67

7-
server.datastore = new tus.FileStore({
8-
path: '/files',
9-
});
8+
const data_store = process.env.DATA_STORE || 'FileStore';
9+
10+
switch (data_store) {
11+
case 'GCSDataStore':
12+
server.datastore = new tus.GCSDataStore({
13+
path: '/files',
14+
projectId: 'vimeo-open-source',
15+
keyFilename: path.resolve(__dirname, '../test/keyfile.json'),
16+
bucket: 'tus-node-server',
17+
});
18+
break;
19+
20+
default:
21+
server.datastore = new tus.FileStore({
22+
path: '/files',
23+
});
24+
}
1025

1126
const host = '127.0.0.1';
1227
const port = 8000;
1328
server.listen({ host, port }, () => {
14-
console.log(`[${new Date().toLocaleTimeString()}] tus server listening at http://${host}:${port}`);
29+
console.log(`[${new Date().toLocaleTimeString()}] tus server listening at http://${host}:${port} using ${data_store}`);
1530
});

index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
const Server = require('./lib/Server');
44
const DataStore = require('./lib/stores/DataStore');
55
const FileStore = require('./lib/stores/FileStore');
6+
const GCSDataStore = require('./lib/stores/GCSDataStore');
67

78
module.exports = {
89
Server,
910
DataStore,
1011
FileStore,
12+
GCSDataStore,
1113
};

keyfile.json.enc

2.3 KB
Binary file not shown.

lib/handlers/HeadHandler.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,19 @@ class HeadHandler extends BaseHandler {
3030
// the response for a HEAD request, even if the offset is 0
3131
res.setHeader('Upload-Offset', file.size);
3232

33-
if ('upload_length' in file) {
33+
if (file.upload_length !== undefined) {
3434
// If the size of the upload is known, the Server MUST include
3535
// the Upload-Length header in the response.
3636
res.setHeader('Upload-Length', file.upload_length);
3737
}
3838

39-
if (!('upload_length' in file) && 'upload_defer_length' in file) {
39+
if (!('upload_length' in file) && file.upload_defer_length !== undefined) {
4040
// As long as the length of the upload is not known, the Server
4141
// MUST set Upload-Defer-Length: 1 in all responses to HEAD requests.
4242
res.setHeader('Upload-Defer-Length', file.upload_defer_length);
4343
}
4444

45-
if ('upload_metadata' in file) {
45+
if (file.upload_metadata !== undefined) {
4646
// If the size of the upload is known, the Server MUST include
4747
// the Upload-Length header in the response.
4848
res.setHeader('Upload-Metadata', file.upload_metadata);

lib/stores/GCSDataStore.js

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
'use strict';
2+
3+
const DataStore = require('./DataStore');
4+
const File = require('../models/File');
5+
const gcloud = require('gcloud');
6+
const assign = require('object-assign');
7+
const stream = require('stream');
8+
const ERRORS = require('../constants').ERRORS;
9+
const TUS_RESUMABLE = require('../constants').TUS_RESUMABLE;
10+
const DEFAULT_CONFIG = {
11+
scopes: ['https://www.googleapis.com/auth/devstorage.full_control'],
12+
};
13+
14+
15+
/**
16+
* @fileOverview
17+
* Store using local filesystem.
18+
*
19+
* @author Ben Stahl <[email protected]>
20+
*/
21+
22+
class GCSDataStore extends DataStore {
23+
constructor(options) {
24+
super(options);
25+
this.extensions = ['creation', 'creation-defer-length'];
26+
27+
if (!options.bucket) {
28+
throw new Error('GCSDataStore must have a bucket');
29+
}
30+
this.bucket_name = options.bucket;
31+
this.gcs = gcloud.storage({
32+
projectId: options.projectId,
33+
keyFilename: options.keyFilename,
34+
});
35+
this.bucket = this._getBucket();
36+
37+
this.authConfig = assign(DEFAULT_CONFIG, {
38+
keyFilename: options.keyFilename,
39+
});
40+
}
41+
42+
/**
43+
* Check the bucket exists in GCS.
44+
*
45+
* @return {[type]} [description]
46+
*/
47+
_getBucket() {
48+
const bucket = this.gcs.bucket(this.bucket_name);
49+
bucket.exists((error, exists) => {
50+
if (error) {
51+
console.warn(error);
52+
throw new Error(`[GCSDataStore] _getBucket: ${error.message}`);
53+
}
54+
55+
if (!exists) {
56+
throw new Error(`[GCSDataStore] _getBucket: ${this.bucket_name} bucket does not exist`);
57+
}
58+
59+
});
60+
61+
return bucket;
62+
}
63+
64+
/**
65+
* Create an empty file in GCS to store the metatdata.
66+
*
67+
* @param {object} req http.incomingMessage
68+
* @param {File} file
69+
* @return {Promise}
70+
*/
71+
create(req) {
72+
return new Promise((resolve, reject) => {
73+
const upload_length = req.headers['upload-length'];
74+
const upload_defer_length = req.headers['upload-defer-length'];
75+
const upload_metadata = req.headers['upload-metadata'];
76+
77+
if (upload_length === undefined && upload_defer_length === undefined) {
78+
reject(ERRORS.INVALID_LENGTH);
79+
return;
80+
}
81+
82+
let file_id;
83+
try {
84+
file_id = this.generateFileName(req);
85+
}
86+
catch (generateError) {
87+
console.warn('[FileStore] create: check your namingFunction. Error', generateError);
88+
reject(ERRORS.FILE_WRITE_ERROR);
89+
return;
90+
}
91+
92+
const file = new File(file_id, upload_length, upload_defer_length, upload_metadata);
93+
const gcs_file = this.bucket.file(file.id);
94+
const options = {
95+
metadata: {
96+
metadata: {
97+
upload_length: file.upload_length,
98+
tus_version: TUS_RESUMABLE,
99+
upload_metadata,
100+
upload_defer_length,
101+
},
102+
},
103+
};
104+
105+
const fake_stream = new stream.PassThrough();
106+
fake_stream.end();
107+
fake_stream.pipe(gcs_file.createWriteStream(options))
108+
.on('error', reject)
109+
.on('finish', () => {
110+
resolve(file);
111+
});
112+
});
113+
}
114+
115+
/**
116+
* Get the file metatata from the object in GCS, then upload a new version
117+
* passing through the metadata to the new version.
118+
*
119+
* @param {object} req http.incomingMessage
120+
* @param {string} file_id Name of file
121+
* @param {integer} offset starting offset
122+
* @return {Promise}
123+
*/
124+
write(req, file_id, offset) {
125+
// GCS Doesn't persist metadata within versions,
126+
// get that metadata first
127+
return this.getOffset(file_id)
128+
.then((data) => {
129+
return new Promise((resolve, reject) => {
130+
const file = this.bucket.file(file_id);
131+
132+
const options = {
133+
offset,
134+
metadata: {
135+
metadata: {
136+
upload_length: data.upload_length,
137+
tus_version: TUS_RESUMABLE,
138+
upload_metadata: data.upload_metadata,
139+
upload_defer_length: data.upload_defer_length,
140+
},
141+
},
142+
};
143+
144+
const write_stream = file.createWriteStream(options);
145+
if (!write_stream) {
146+
return reject(ERRORS.FILE_WRITE_ERROR);
147+
}
148+
149+
let new_offset = 0;
150+
req.on('data', (buffer) => {
151+
new_offset += buffer.length;
152+
});
153+
154+
req.on('end', () => {
155+
console.log(`${new_offset} bytes written`);
156+
resolve(new_offset);
157+
});
158+
159+
write_stream.on('error', (e) => {
160+
console.log(e);
161+
reject(ERRORS.FILE_WRITE_ERROR);
162+
});
163+
164+
return req.pipe(write_stream);
165+
});
166+
});
167+
}
168+
169+
/**
170+
* Get file metadata from the GCS Object.
171+
*
172+
* @param {string} file_id name of the file
173+
* @return {object}
174+
*/
175+
getOffset(file_id) {
176+
return new Promise((resolve, reject) => {
177+
const file = this.bucket.file(file_id);
178+
file.getMetadata((error, metadata, apiResponse) => {
179+
if (error && error.message === 'Not Found') {
180+
return reject(ERRORS.FILE_NOT_FOUND);
181+
}
182+
183+
if (error) {
184+
console.warn('[GCSDataStore] getFileMetadata', error);
185+
return reject(error);
186+
}
187+
188+
const data = {
189+
size: parseInt(metadata.size, 10),
190+
};
191+
192+
if (!('metadata' in metadata)) {
193+
return resolve(data);
194+
}
195+
196+
if (metadata.metadata.upload_length) {
197+
data.upload_length = parseInt(metadata.metadata.upload_length, 10);
198+
}
199+
200+
if (metadata.metadata.upload_defer_length) {
201+
data.upload_defer_length = parseInt(metadata.metadata.upload_defer_length, 10);
202+
}
203+
204+
if (metadata.metadata.upload_metadata) {
205+
data.upload_metadata = metadata.metadata.upload_metadata;
206+
}
207+
208+
return resolve(data);
209+
});
210+
});
211+
}
212+
}
213+
214+
module.exports = GCSDataStore;

package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"test": "NODE_ENV=test mocha",
3737
"coveralls": "NODE_ENV=test istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage",
3838
"example": "nodemon example/server.js",
39+
"gcs_example": "DATA_STORE=GCSDataStore nodemon example/server.js",
3940
"lint": "eslint ."
4041
},
4142
"devDependencies": {
@@ -52,7 +53,11 @@
5253
"supertest": "^1.1.0"
5354
},
5455
"dependencies": {
56+
"configstore": "^2.0.0",
5557
"crypto-rand": "0.0.2",
56-
"configstore": "^2.0.0"
58+
"gcloud": "^0.34.0",
59+
"google-auto-auth": "^0.2.4",
60+
"object-assign": "^4.1.0",
61+
"request": "^2.72.0"
5762
}
5863
}

0 commit comments

Comments
 (0)