Skip to content

Commit 51a2de8

Browse files
authored
Scheduled backups for Firestore (#19)
1 parent 616a70c commit 51a2de8

File tree

7 files changed

+202
-0
lines changed

7 files changed

+202
-0
lines changed

.eslintrc.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
},
99
"extends": "eslint:recommended",
1010
"parserOptions": {
11+
"ecmaVersion": 8,
1112
"sourceType": "module"
1213
},
1314
"rules": {
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# This file specifies files that are *not* uploaded to Google Cloud Platform
2+
# using gcloud. It follows the same syntax as .gitignore, with the addition of
3+
# "#!include" directives (which insert the entries of the given .gitignore-style
4+
# file at that point).
5+
#
6+
# For more information, run:
7+
# $ gcloud topic gcloudignore
8+
#
9+
.gcloudignore
10+
# If you would like to upload your .git directory, .gitignore file or files
11+
# from your .gitignore file, remove the corresponding line
12+
# below:
13+
.git
14+
.gitignore
15+
16+
# Node.js dependencies:
17+
node_modules/
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Cloud Firestore Scheduled Backups
2+
3+
This sample demonstrates using AppEngine cron jobs to run nightly backups
4+
of data in Cloud Firestore.
5+
6+
## Setup
7+
8+
### 1 - Create a Project
9+
If you haven't already, create a new Firebase project and create a Cloud
10+
Firestore database within the project.
11+
12+
### 2 - Configure IAM
13+
This sample will use the AppEngine default service account to perform
14+
backups of your Cloud Firestore data. To do this, you will need to give
15+
the service account permission to access your data and save it to
16+
google cloud storage.
17+
18+
Make the service account a Datastore Import/Export Admin:
19+
20+
```shell
21+
$ gcloud projects add-iam-policy-binding YOUR_PROJECT_ID \
22+
--member serviceAccount:[email protected] --role roles/datastore.importExportAdmin
23+
```
24+
25+
Give the service account permission to write to the GCS bucket you
26+
are going to use. Here we will use the default bucket:
27+
28+
```shell
29+
$ sutil iam ch \
30+
serviceAccount:[email protected]:objectCreator \
31+
gs://YOUR_PROJECT_ID.appspot.com
32+
```
33+
34+
### 3 - Configure Cron
35+
Open `cron.yaml` and edit this line:
36+
37+
```
38+
/cloud-firestore-export?outputUriPrefix=gs://BUCKET_NAME[/PATH]&collections=test1,test2
39+
```
40+
41+
You shoudl change `gs://BUCKET_NAME[/PATH]` to the Google Cloud Storage
42+
path where your data should be backed up. If you only want to back up certain
43+
collections, change `test1,test2` to a comma-separated list of those collections.
44+
Otherwise, delete the `&collections=test1,test2` param.
45+
46+
## Deploy
47+
48+
To deploy the project, run:
49+
50+
```
51+
$ gcloud app deploy app.yaml cron.yaml
52+
```
53+
54+
To make sure it deployed correctly, navigate to https://YOUR_PROJECT_ID.appspot.com/
55+
56+
## Test
57+
58+
To test the backup, navigate to the following URL:
59+
https://YOUR_PROJECT_ID.appspot.com/cloud-firestore-export?outputUriPrefix=gs://YOUR_PROJECT_ID.appspot.com
60+
61+
You should see some output like this, letting you know that a backup
62+
was started:
63+
64+
```js
65+
{
66+
name: "projects/YOUR_PROJECT_ID/databases/(default)/operations/ASA2NDIwNjI3ODQJGnRsdWFmZWQHEmxhcnRuZWNzdS1zYm9qLW5pbWRhFAosEg",
67+
metadata: {
68+
@type: "type.googleapis.com/google.firestore.admin.v1beta1.ExportDocumentsMetadata",
69+
startTime: "2018-10-11T22:47:15.473517Z",
70+
operationState: "PROCESSING",
71+
outputUriPrefix: "gs://YOUR_PROJECT_ID.appspot.com/2018-10-11-22-47-15"
72+
}
73+
}
74+
```
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
const axios = require('axios');
2+
const dateformat = require('dateformat');
3+
const express = require('express');
4+
const { google } = require('googleapis');
5+
6+
const app = express();
7+
8+
// Trigger a backup
9+
app.get('/cloud-firestore-export', async (req, res) => {
10+
const auth = await google.auth.getClient({
11+
scopes: ['https://www.googleapis.com/auth/datastore']
12+
});
13+
14+
const accessTokenResponse = await auth.getAccessToken();
15+
const accessToken = accessTokenResponse.token;
16+
17+
const headers = {
18+
'Content-Type': 'application/json',
19+
Authorization: 'Bearer ' + accessToken
20+
};
21+
22+
const outputUriPrefix = req.param('outputUriPrefix');
23+
if (!(outputUriPrefix && outputUriPrefix.indexOf('gs://') == 0)) {
24+
res.status(500).send(`Malformed outputUriPrefix: ${outputUriPrefix}`);
25+
}
26+
27+
// Construct a backup path folder based on the timestamp
28+
const timestamp = dateformat(Date.now(), 'yyyy-mm-dd-HH-MM-ss');
29+
let path = outputUriPrefix;
30+
if (path.endsWith('/')) {
31+
path += timestamp;
32+
} else {
33+
path += '/' + timestamp;
34+
}
35+
36+
const body = {
37+
outputUriPrefix: path
38+
};
39+
40+
// If specified, mark specific collections for backup
41+
const collectionParam = req.param('collections');
42+
if (collectionParam) {
43+
body.collectionIds = collectionParam.split(',');
44+
}
45+
46+
const projectId = process.env.GOOGLE_CLOUD_PROJECT;
47+
const url = `https://firestore.googleapis.com/v1beta1/projects/${projectId}/databases/(default):exportDocuments`;
48+
49+
try {
50+
const response = await axios.post(url, body, { headers: headers });
51+
res
52+
.status(200)
53+
.send(response.data)
54+
.end();
55+
} catch (e) {
56+
if (e.response) {
57+
console.warn(e.response.data);
58+
}
59+
60+
res
61+
.status(500)
62+
.send('Could not start backup: ' + e)
63+
.end();
64+
}
65+
});
66+
67+
// Index page, just to make it easy to see if the app is working.
68+
app.get('/', (req, res) => {
69+
res
70+
.status(200)
71+
.send('[scheduled-backups]: Hello, world!')
72+
.end();
73+
});
74+
75+
// Start the server
76+
const PORT = process.env.PORT || 6060;
77+
app.listen(PORT, () => {
78+
console.log(`App listening on port ${PORT}`);
79+
console.log('Press Ctrl+C to quit.');
80+
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
runtime: nodejs8
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
cron:
2+
- description: "Daily Cloud Firestore Export"
3+
url: /cloud-firestore-export?outputUriPrefix=gs://BUCKET_NAME[/PATH]&collections=test1,test2
4+
target: cloud-firestore-admin
5+
schedule: every 24 hours
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"name": "solution-scheduled-backups",
3+
"version": "1.0.0",
4+
"description": "Scheduled Cloud Firestore backups via AppEngine cron",
5+
"main": "app.js",
6+
"engines": {
7+
"node": "8.x.x"
8+
},
9+
"scripts": {
10+
"deploy": "gcloud app deploy --quiet app.yaml cron.yaml",
11+
"start": "node app.js"
12+
},
13+
"author": "Google, Inc.",
14+
"license": "Apache-2.0",
15+
"dependencies": {
16+
"axios": "^0.18.0",
17+
"dateformat": "^3.0.3",
18+
"express": "^4.16.4",
19+
"googleapis": "^34.0.0"
20+
},
21+
"devDependencies": {
22+
"prettier": "^1.14.3"
23+
}
24+
}

0 commit comments

Comments
 (0)