Skip to content

Commit ba68a79

Browse files
committed
Merge remote-tracking branch 'github/master'
2 parents f039b70 + 29d70a6 commit ba68a79

File tree

16 files changed

+369
-312
lines changed

16 files changed

+369
-312
lines changed

.npmignore

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Logs
2+
logs
3+
*.log
4+
5+
# Runtime data
6+
pids
7+
*.pid
8+
*.seed
9+
10+
# Directory for instrumented libs generated by jscoverage/JSCover
11+
lib-cov
12+
13+
# Coverage directory used by tools like istanbul
14+
coverage
15+
16+
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17+
.grunt
18+
19+
# node-waf configuration
20+
.lock-wscript
21+
22+
# Compiled binary addons (http://nodejs.org/api/addons.html)
23+
build/Release
24+
25+
# Dependency directory
26+
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
27+
node_modules
28+
29+
# Emacs
30+
*~
31+
32+
# WebStorm/IntelliJ
33+
.idea

bin/parse-server

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ if (process.env.PARSE_SERVER_OPTIONS) {
2121
options.restAPIKey = process.env.PARSE_SERVER_REST_API_KEY;
2222
options.dotNetKey = process.env.PARSE_SERVER_DOTNET_KEY;
2323
options.javascriptKey = process.env.PARSE_SERVER_JAVASCRIPT_KEY;
24-
options.dotNetKey = process.env.PARSE_SERVER_DOTNET_KEY;
2524
options.masterKey = process.env.PARSE_SERVER_MASTER_KEY;
2625
options.fileKey = process.env.PARSE_SERVER_FILE_KEY;
2726
// Comma separated list of facebook app ids

spec/ParseUser.spec.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,22 @@ describe('Parse.User testing', () => {
6464
});
6565
});
6666

67+
it("user login with files", (done) => {
68+
"use strict";
69+
70+
let file = new Parse.File("yolo.txt", [1,2,3], "text/plain");
71+
file.save().then((file) => {
72+
return Parse.User.signUp("asdf", "zxcv", { "file" : file });
73+
}).then(() => {
74+
return Parse.User.logIn("asdf", "zxcv");
75+
}).then((user) => {
76+
let fileAgain = user.get('file');
77+
ok(fileAgain.name());
78+
ok(fileAgain.url());
79+
done();
80+
});
81+
});
82+
6783
it("become", (done) => {
6884
var user = null;
6985
var sessionToken = null;

src/Adapters/Files/FilesAdapter.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Files Adapter
2+
//
3+
// Allows you to change the file storage mechanism.
4+
//
5+
// Adapter classes must implement the following functions:
6+
// * createFile(config, filename, data)
7+
// * getFileData(config, filename)
8+
// * getFileLocation(config, request, filename)
9+
//
10+
// Default is GridStoreAdapter, which requires mongo
11+
// and for the API server to be using the ExportAdapter
12+
// database adapter.
13+
14+
export class FilesAdapter {
15+
createFile(config, filename, data) { }
16+
17+
getFileData(config, filename) { }
18+
19+
getFileLocation(config, filename) { }
20+
}
21+
22+
export default FilesAdapter;
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// GridStoreAdapter
2+
//
3+
// Stores files in Mongo using GridStore
4+
// Requires the database adapter to be based on mongoclient
5+
6+
import { GridStore } from 'mongodb';
7+
import { FilesAdapter } from './FilesAdapter';
8+
9+
export class GridStoreAdapter extends FilesAdapter {
10+
// For a given config object, filename, and data, store a file
11+
// Returns a promise
12+
createFile(config, filename, data) {
13+
return config.database.connect().then(() => {
14+
let gridStore = new GridStore(config.database.db, filename, 'w');
15+
return gridStore.open();
16+
}).then((gridStore) => {
17+
return gridStore.write(data);
18+
}).then((gridStore) => {
19+
return gridStore.close();
20+
});
21+
}
22+
23+
getFileData(config, filename) {
24+
return config.database.connect().then(() => {
25+
return GridStore.exist(config.database.db, filename);
26+
}).then(() => {
27+
let gridStore = new GridStore(config.database.db, filename, 'r');
28+
return gridStore.open();
29+
}).then((gridStore) => {
30+
return gridStore.read();
31+
});
32+
}
33+
34+
getFileLocation(config, filename) {
35+
return (config.mount + '/files/' + config.applicationId + '/' + encodeURIComponent(filename));
36+
}
37+
}
38+
39+
export default GridStoreAdapter;

src/Adapters/Files/S3Adapter.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// S3Adapter
2+
//
3+
// Stores Parse files in AWS S3.
4+
5+
import * as AWS from 'aws-sdk';
6+
import { FilesAdapter } from './FilesAdapter';
7+
8+
const DEFAULT_S3_REGION = "us-east-1";
9+
const DEFAULT_S3_BUCKET = "parse-files";
10+
11+
export class S3Adapter extends FilesAdapter {
12+
// Creates an S3 session.
13+
// Providing AWS access and secret keys is mandatory
14+
// Region and bucket will use sane defaults if omitted
15+
constructor(
16+
accessKey,
17+
secretKey,
18+
{ region = DEFAULT_S3_REGION,
19+
bucket = DEFAULT_S3_BUCKET,
20+
bucketPrefix = '',
21+
directAccess = false } = {}
22+
) {
23+
super();
24+
25+
this._region = region;
26+
this._bucket = bucket;
27+
this._bucketPrefix = bucketPrefix;
28+
this._directAccess = directAccess;
29+
30+
let s3Options = {
31+
accessKeyId: accessKey,
32+
secretAccessKey: secretKey,
33+
params: { Bucket: this._bucket }
34+
};
35+
AWS.config._region = this._region;
36+
this._s3Client = new AWS.S3(s3Options);
37+
}
38+
39+
// For a given config object, filename, and data, store a file in S3
40+
// Returns a promise containing the S3 object creation response
41+
createFile(config, filename, data) {
42+
let params = {
43+
Key: this._bucketPrefix + filename,
44+
Body: data
45+
};
46+
if (this._directAccess) {
47+
params.ACL = "public-read"
48+
}
49+
return new Promise((resolve, reject) => {
50+
this._s3Client.upload(params, (err, data) => {
51+
if (err !== null) {
52+
return reject(err);
53+
}
54+
resolve(data);
55+
});
56+
});
57+
}
58+
59+
// Search for and return a file if found by filename
60+
// Returns a promise that succeeds with the buffer result from S3
61+
getFileData(config, filename) {
62+
let params = {Key: this._bucketPrefix + filename};
63+
return new Promise((resolve, reject) => {
64+
this._s3Client.getObject(params, (err, data) => {
65+
if (err !== null) {
66+
return reject(err);
67+
}
68+
resolve(data.Body);
69+
});
70+
});
71+
}
72+
73+
// Generates and returns the location of a file stored in S3 for the given request and filename
74+
// The location is the direct S3 link if the option is set, otherwise we serve the file through parse-server
75+
getFileLocation(config, filename) {
76+
if (this._directAccess) {
77+
return ('https://' + this.bucket + '._s3Client.amazonaws.com' + '/' + this._bucketPrefix + filename);
78+
}
79+
return (config.mount + '/files/' + config.applicationId + '/' + encodeURIComponent(filename));
80+
}
81+
}
82+
83+
export default S3Adapter;

src/Config.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,17 @@ function Config(applicationId, mount) {
1313

1414
this.applicationId = applicationId;
1515
this.collectionPrefix = cacheInfo.collectionPrefix || '';
16-
this.database = DatabaseAdapter.getDatabaseConnection(applicationId);
1716
this.masterKey = cacheInfo.masterKey;
1817
this.clientKey = cacheInfo.clientKey;
1918
this.javascriptKey = cacheInfo.javascriptKey;
2019
this.dotNetKey = cacheInfo.dotNetKey;
2120
this.restAPIKey = cacheInfo.restAPIKey;
2221
this.fileKey = cacheInfo.fileKey;
2322
this.facebookAppIds = cacheInfo.facebookAppIds;
23+
24+
this.database = DatabaseAdapter.getDatabaseConnection(applicationId);
25+
this.filesController = cacheInfo.filesController;
26+
2427
this.mount = mount;
2528
}
2629

src/Controllers/FilesController.js

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// FilesController.js
2+
3+
import express from 'express';
4+
import mime from 'mime';
5+
import { Parse } from 'parse/node';
6+
import BodyParser from 'body-parser';
7+
import hat from 'hat';
8+
import * as Middlewares from '../middlewares';
9+
import Config from '../Config';
10+
11+
const rack = hat.rack();
12+
13+
export class FilesController {
14+
constructor(filesAdapter) {
15+
this._filesAdapter = filesAdapter;
16+
}
17+
18+
getHandler() {
19+
return (req, res) => {
20+
let config = new Config(req.params.appId);
21+
let filename = req.params.filename;
22+
this._filesAdapter.getFileData(config, filename).then((data) => {
23+
res.status(200);
24+
var contentType = mime.lookup(filename);
25+
res.set('Content-type', contentType);
26+
res.end(data);
27+
}).catch((error) => {
28+
res.status(404);
29+
res.set('Content-type', 'text/plain');
30+
res.end('File not found.');
31+
});
32+
};
33+
}
34+
35+
createHandler() {
36+
return (req, res, next) => {
37+
if (!req.body || !req.body.length) {
38+
next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR,
39+
'Invalid file upload.'));
40+
return;
41+
}
42+
43+
if (req.params.filename.length > 128) {
44+
next(new Parse.Error(Parse.Error.INVALID_FILE_NAME,
45+
'Filename too long.'));
46+
return;
47+
}
48+
49+
if (!req.params.filename.match(/^[_a-zA-Z0-9][a-zA-Z0-9@\.\ ~_-]*$/)) {
50+
next(new Parse.Error(Parse.Error.INVALID_FILE_NAME,
51+
'Filename contains invalid characters.'));
52+
return;
53+
}
54+
55+
// If a content-type is included, we'll add an extension so we can
56+
// return the same content-type.
57+
let extension = '';
58+
let hasExtension = req.params.filename.indexOf('.') > 0;
59+
let contentType = req.get('Content-type');
60+
if (!hasExtension && contentType && mime.extension(contentType)) {
61+
extension = '.' + mime.extension(contentType);
62+
}
63+
64+
let filename = rack() + '_' + req.params.filename + extension;
65+
this._filesAdapter.createFile(req.config, filename, req.body).then(() => {
66+
res.status(201);
67+
var location = this._filesAdapter.getFileLocation(req.config, filename);
68+
res.set('Location', location);
69+
res.json({ url: location, name: filename });
70+
}).catch((error) => {
71+
next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR,
72+
'Could not store file.'));
73+
});
74+
};
75+
}
76+
77+
/**
78+
* Find file references in REST-format object and adds the url key
79+
* with the current mount point and app id.
80+
* Object may be a single object or list of REST-format objects.
81+
*/
82+
expandFilesInObject(config, object) {
83+
if (object instanceof Array) {
84+
object.map((obj) => this.expandFilesInObject(config, obj));
85+
return;
86+
}
87+
if (typeof object !== 'object') {
88+
return;
89+
}
90+
for (let key in object) {
91+
let fileObject = object[key];
92+
if (fileObject && fileObject['__type'] === 'File') {
93+
if (fileObject['url']) {
94+
continue;
95+
}
96+
let filename = fileObject['name'];
97+
if (filename.indexOf('tfss-') === 0) {
98+
fileObject['url'] = 'http://files.parsetfss.com/' + config.fileKey + '/' + encodeURIComponent(filename);
99+
} else {
100+
fileObject['url'] = this._filesAdapter.getFileLocation(config, filename);
101+
}
102+
}
103+
}
104+
}
105+
106+
getExpressRouter() {
107+
let router = express.Router();
108+
router.get('/files/:appId/:filename', this.getHandler());
109+
110+
router.post('/files', function(req, res, next) {
111+
next(new Parse.Error(Parse.Error.INVALID_FILE_NAME,
112+
'Filename not provided.'));
113+
});
114+
115+
router.post('/files/:filename',
116+
Middlewares.allowCrossDomain,
117+
BodyParser.raw({type: '*/*', limit: '20mb'}),
118+
Middlewares.handleParseHeaders,
119+
this.createHandler()
120+
);
121+
122+
return router;
123+
}
124+
}
125+
126+
export default FilesController;

0 commit comments

Comments
 (0)