hurricane/security_camera/resources/cloud_server/functions/addImage.js

// The Cloud Functions for Firebase SDK to create Cloud Functions and setup triggers.
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const uuid = require('uuid');
const config = require('./config/firebase-config');
const db = admin.firestore()
const storage = admin.storage()
const DEFAULT_CAMERA_SETTINGS = {
contrast: 0,
saturation: 0,
brightness: 0,
resolution: 6,
quality: 5,
mirror: false,
flip: false,
filter: 0,
interval: 3*60*1000 // 3min
}
/**************************************************************************************************
* Add image to database
* The call sequence is:
* 1. Device writes image message to DMS
* 2. DMS forwards message to Firebase
* 3. Firebase executes this cloud function
* 4. This cloud function retrieve or creates the device in the database
* 5. This cloud function saves the image file to storage
* 6. This cloud functions writes image metadata to the database
* 7. Prune any old images
*/
module.exports = functions.https.onRequest((req, res) => {
// console.log('Received request: ' + JSON.stringify(req.body));
const authToken = req.get('authorization');
// Ensure this request is coming from the DMS webhook
if(authToken !== config.DMS_WEBHOOK_AUTH_TOKEN) {
return res.status(403).send('Unauthorized');
}
const data = req.body.data
if(!data) {
return res.status(400).send('Missing data field');
}
// Validate the message arguments
const uuid = data.uuid
const code = data.code
const timestamp = data.timestamp
const meta = data.meta
const imageData = data.img
if(!uuid) {
return res.status(400).send('Missing uuid field');
}
if(!code) {
return res.status(400).send('Missing code field');
}
if(!timestamp) {
return res.status(400).send('Missing timestamp field');
}
if(!imageData || !imageData.data) {
return res.status(400).send('Missing img field');
}
let context = {
uuid: uuid.toUpperCase(),
code: code.toUpperCase(),
timestamp: timestamp,
meta: meta,
data: imageData.data,
imageCount: 0
}
return addNewImage(context).then(() => {
return res.status(200).send(context.settings);
}).catch((err) => {
console.log(`Failed to add image to db, err: ${err.message ||err.description || err}`)
return res.status(400).send(`Error: ${err.message || err}`);
})
});
/*************************************************************************************************/
const addNewImage = (context) => {
return retrieveOrCreateDevice(context).then(() => {
return addImageToStorage( context)
}).then(() => {
return addImageToDatabase(context)
})
}
/*************************************************************************************************/
const retrieveOrCreateDevice = (context) => {
const query = db.collection('devices').where('uuid', '==', context.uuid)
return query.get().then((results) => {
if(results.size > 0){
console.log(`Device already exists: ${context.uuid}`)
context.deviceRef = results.docs[0].ref
context.settings = results.docs[0].get('settings')
context.imageCount = results.docs[0].get('imageCount')
return Promise.resolve()
} else {
return createDevice(context)
}
})
}
/*************************************************************************************************/
const createDevice = (context) => {
console.log(`Creating new device: ${context.uuid}`)
return db.collection('devices').add({
uuid: context.uuid,
code: context.code,
settings: DEFAULT_CAMERA_SETTINGS,
imageCount: 0
}).then((deviceRef) => {
context.deviceRef = deviceRef
context.settings = DEFAULT_CAMERA_SETTINGS
context.imageCount = 0
return Promise.resolve()
})
}
/*************************************************************************************************/
const addImageToStorage = (context) => {
const bucket = storage.bucket();
const imgBuffer = Buffer.from(context.data);
console.log(`Current image count: ${context.imageCount}`)
pruneOldImages(context);
console.log('Adding image to storage ...');
context.filePath = `images/${context.uuid}/${context.timestamp}.jpg`;
context.imgFile = bucket.file(context.filePath);
return context.imgFile.save(imgBuffer, {resumable: false, public:true}).then(() => {
return setImageMetaData(context)
})
}
/*************************************************************************************************/
const setImageMetaData = (context) => {
context.token = uuid.v4();
const metadata = {
contentType: 'image/jpeg',
firebaseStorageDownloadTokens: context.token
};
// https://firebasestorage.googleapis.com/v0/b/[BUCKET_NAME]/o/[FILE_PATH]?alt=media&token=[THE_TOKEN_YOU_CREATED]
return context.imgFile.setMetadata(metadata);
}
/*************************************************************************************************/
const addImageToDatabase = (context) => {
console.log('Adding image to database ...');
return context.deviceRef.collection('images').add({
timestamp: context.timestamp,
path: context.filePath,
token: context.token,
meta: context.meta
}).then(() => {
return context.deviceRef.update({imageCount: context.imageCount + 1})
})
}
/*************************************************************************************************/
const pruneOldImages = (context) => {
const imageOverflowCount = context.imageCount - config.MAX_STORED_IMAGES
// To limit accesses to the DB, we only prune old images
// when the count exceeds 50% of the max
if(imageOverflowCount > config.MAX_STORED_IMAGES * 0.5) {
context.imageCount = config.MAX_STORED_IMAGES
// Return a list of the images for the given device ordered by timestamp in ascending order
// We remove imageOverflowCount of the oldest images
const query = context.deviceRef.collection('images').orderBy('timestamp', 'asc').limit(imageOverflowCount);
query.get().then((results) => {
console.log(`Pruning ${results.size} images`)
for (let i = 0; i < results.size; i++) {
deleteOldImage(results.docs[i])
}
return null;
}).catch((err) => {
console.log(`Error while pruning images, err:${err.message || err}`)
})
}
}
/*************************************************************************************************/
const deleteOldImage = (doc) => {
const bucket = storage.bucket();
const filepath = doc.get('path')
const file = bucket.file(filepath)
// Delete the file from storage, ignoring errors
file.delete().catch((err) => {
console.log(`Failed to delete ${filepath} from storage, err:${err.message || err}`)
})
// Delete the record from the database, ignoring errors
console.log(`Pruning image: ${doc.id}`)
doc.ref.delete().catch((err) => {
console.log(`Failed to delete image, err: ${err.message || err.description || err}`)
})
}