This article explains how to configure your Parse server so files uploaded through an application are placed directly in a DigitalOcean Space.
Among the features provided by Parse Server is file upload. This feature can be used, for example, to publish photos in a photo-sharing application.
Thanks to the provided JavaScript SDK, this is relatively simple:
var base64 = 'V29ya2luZyBhdCBQYXJzZSBpcyBncmVhdCE=';
// create the file
var parseFile = new Parse.File('picture001.jpg', { base64: base64 });
// save the file
parseFile.save().then(
function () {
// create a post
var Post = new Parse.Object('Post');
Post.set('user', 'Joe Smith');
// define the file attached to the Post
Post.set('picture', parseFile);
// save the Post object
Post.save();
},
function (error) {
console.log(error.message);
}
);But where are these images stored? Previously, when Parse.com still existed, these files were stored on Amazon S3.
Now with Parse Server, the default configuration stores these files in your MongoDB database, but this is not the recommended configuration for optimal usage.
Fortunately, Parse Server lets you change this behavior through FileAdapters. FileAdapters let you define where your files will be stored without having to worry about the rest. You only need to configure the FileAdapter of your choice, and it will handle your file when it is sent to your Parse server.
Here is a list of some FileAdapters:
- GridStoreAdapter: the default adapter, which stores files directly in the database.
- S3Adapter: stores files through Amazon's S3 service.
- GCSAdapter: stores files through Google Cloud Storage.
As we saw in the previous article, Spaces work the same way as S3. We can therefore use the S3Adapter so our files are stored on DigitalOcean.
To start, we will use the basic Parse Server configuration from this article.
The first thing to do is add the @parse/s3-files-adapter": "^1.2.1" dependency:
{
"name": "parse-server-sample",
"version": "1.0.0",
"dependencies": {
"express": "^4.16.4",
"parse-dashboard": "^1.2.0",
"parse-server": "^3.1.3",
"@parse/s3-files-adapter": "^1.2.1"
},
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-preset-env": "^1.7.0"
},
"scripts": {
"clean": "rm -rf build && mkdir build",
"build-server": "babel -d ./build ./server -s",
"build": "npm run clean && npm run build-server",
"start": "npm run build && node ./build/index.js",
"dev": "babel-node ./server/index.js"
}
}Then we will modify the docker-compose.yml file to add the environment variables required to configure the FileAdapter.
Add the following variables to the api service:
SPACE_ACCESS_KEY: 'access_key'
SPACE_ACCESS_SECRET: 'secret_key'
SPACE_REGION: 'region'
SPACE_BUCKET_PREFIX: 'prefix'
SPACE_BUCKET_NAME: 'name'
SPACE_BASE_URL: 'base_url'
SPACE_ENDPOINT: 'end_point'SPACE_ACCESS_KEY: your access_key.SPACE_ACCESS_SECRET: your access_secret.SPACE_REGION: the region of your Space, ams3 for Amsterdam.SPACE_BUCKET_PREFIX: the name of the folder where your files will be placed, appdata/.SPACE_BUCKET_NAME: the name of your Space, testparsearticle.SPACE_BASE_URL: the full address of your Space, https://testparsearticle.ams3.digitaloceanspaces.com.SPACE_ENDPOINT: the endpoint of your Space, ams3.digitaloceanspaces.com.
Now we can modify the index.js file that contains the configuration of our Parse server:
First, we add the required dependencies:
var S3Adapter = require('@parse/s3-files-adapter');
var AWS = require('aws-sdk');Then we will specify that we do not want to use Amazon S3, but DigitalOcean Spaces. To do this, we indicate that the endpoint used will be the Spaces endpoint:
const spacesEndpoint = new AWS.Endpoint(process.env.SPACE_ENDPOINT);Then we define the options for our adapter:
var s3Options = {
bucket: process.env.SPACE_BUCKET_NAME,
baseUrl: process.env.SPACE_BASE_URL,
region: process.env.SPACE_REGION,
directAccess: true,
globalCacheControl: 'public, max-age=31536000',
bucketPrefix: process.env.SPACE_BUCKET_PREFIX,
s3overrides: {
accessKeyId: process.env.SPACE_ACCESS_KEY,
secretAccessKey: process.env.SPACE_ACCESS_SECRET,
endpoint: spacesEndpoint,
},
};bucket: the name of your Space.baseUrl: the URL used to access the Space.region: the region where the Space is located.directAccess: defines whether files are read directly from Spaces or through your Parse Server. The value true creates the files and makes them publicly accessible.globalCacheControl: defines the cache value the files will have.bucketPrefix: your bucket prefix, if you want to add one.s3overrides: access to your Space.
Additional explanations about baseUrl and bucketPrefix
baseUrl will be used to define the access URL for your file. Let's return to our JavaScript example:
Post.set('picture', file);When we save our Post object, the picture field, which is a File field, will contain the following information:
- file name
- url
The file URL will be built as follows: baseUrl+fileName
The purpose of bucketPrefix is to "classify" objects in your Space. Imagine that you created a Space and store your application's assets in it. Later, you also want to store files uploaded by your application, but you do not want to mix them with your assets. You want to create an appdata folder and put the uploaded files inside it.
This is where bucketPrefix comes in: by naming it appdata, your files will appear in an appdata folder. In reality, this is not completely true because folders do not exist in Spaces or S3. That is why it is called a prefix: it adds the prefix to the file name, but in administration interfaces it is represented as a folder.
Now that our adapter options are defined, we only need to create the adapter and tell our server to use it as the fileAdapter.
var s3Adapter = new S3Adapter(s3Options);
var api = new ParseServer({
databaseURI: databaseURI,
cloud: cloudPath,
appId: appId,
masterKey: masterKey,
serverURL: serverURL,
logLevel: logLevel,
filesAdapter: s3Adapter,
});Here is the complete index.js file:
// 1. Import the required dependencies
var express = require('express');
var ParseServer = require('parse-server').ParseServer;
var ParseDashboard = require('parse-dashboard');
var S3Adapter = require('@parse/s3-files-adapter');
var AWS = require('aws-sdk');
// 2. Define our server options from environment variables
const mountPath = process.env.PARSE_MOUNT || '/parse';
const port = process.env.PORT || 1337;
const databaseURI = process.env.DATABASE_URI || 'mongodb://localhost:27017/dev';
const cloudPath = __dirname + '/cloud/main.js';
const appId = process.env.APP_ID || 'myAppId';
const masterKey = process.env.MASTER_KEY || '';
const serverURL = process.env.SERVER_URL || 'http://localhost:1337/parse';
const logLevel = process.env.LOG_LEVEL || 'info';
const allowInsecureHTTP = process.env.ALLOW_INSECURE_HTTP_DASHBOARD;
const appName = process.env.APP_NAME;
const dashboard_user = process.env.DASHBOARD_USER;
const dashboard_password = process.env.DASHBOARD_PASSWORD;
// 3. Define the FileAdapter options
const spacesEndpoint = new AWS.Endpoint(process.env.SPACE_ENDPOINT);
var s3Options = {
bucket: process.env.SPACE_BUCKET_NAME,
baseUrl: process.env.SPACE_BASE_URL,
region: process.env.SPACE_REGION,
directAccess: true,
globalCacheControl: 'public, max-age=31536000',
bucketPrefix: process.env.SPACE_BUCKET_PREFIX,
s3overrides: {
accessKeyId: process.env.SPACE_ACCESS_KEY,
secretAccessKey: process.env.SPACE_ACCESS_SECRET,
endpoint: spacesEndpoint,
},
};
var s3Adapter = new S3Adapter(s3Options);
// 4. Create a new Parse instance with the base configuration
var api = new ParseServer({
databaseURI: databaseURI,
cloud: cloudPath,
appId: appId,
masterKey: masterKey,
serverURL: serverURL,
logLevel: logLevel,
filesAdapter: s3Adapter,
});
// 5. Create the dashboard
var dashboard = new ParseDashboard(
{
apps: [
{
serverURL: serverURL,
appId: appId,
masterKey: masterKey,
appName: appName,
},
],
users: [
{
user: dashboard_user,
pass: dashboard_password,
apps: [{ appId: appId }],
},
],
},
//options
{ allowInsecureHTTP: allowInsecureHTTP }
);
// 6. Start the server
var app = express();
app.use(mountPath, api);
app.use('/dashboard', dashboard);
var httpServer = require('http').createServer(app);
httpServer.listen(port, function () {
console.log('parse-server running on port ' + port + '.');
});To quickly test that your configuration works, create an index.html file with the code below and open it in your browser:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Test Parse Server</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" type="text/css" media="screen" href="main.css" />
<script src="main.js"></script>
</head>
<body>
<script src="https://npmcdn.com/parse/dist/parse.min.js"></script>
<script>
Parse.initialize('APP_ID');
Parse.serverURL = 'http://localhost:1337/parse';
var base64 = 'V29ya2luZyBhdCBQYXJzZSBpcyBncmVhdCE=';
// create the file
var parseFile = new Parse.File('picture001.jpg', { base64: base64 });
// save the file
parseFile.save().then(
function () {
// create a post
var Post = new Parse.Object('Post');
Post.set('user', 'Joe Smith');
// define the file attached to the Post
Post.set('picture', parseFile);
// save the Post object
Post.save();
},
function (error) {
console.log(error.message);
}
);
</script>
</body>
</html>You should see a new file appear in your Space every time you refresh the index.html page.