ING 3 FISA DATA | Mohammed Ayoub BERHILI
Github Repository Of This Project
Data Services Orchestration: Cloud Functions & GCP Bucket Automation
Creation of a file upload, search and processing service. Automating the creation and configuration of this service using terraform and docker
This service will be hosted on the cloud (Google Cloud) and will use the cloud functions and Buckets (Clound Storage) to perform the required tasks.
We are supposed to implement the following requirement
Ce qui manque à faire : étant donné que j'avais du refaire mon TP à cause des problèmes liés à mon compte GCP, j'étais contraint par le temps et je n'ai pas tout implémenté comme demandé. Il manque l'implémentation d'une interface web pour l'interaction avec les C-Functions implémentées. Dans la partie suivant j'expliquerai comment peut-on tester ces fonctions sans devoir passer par une interface web (avec Postman).
Le curl du requête
curl --location --request POST 'https://us-central1-orchestration-gcp-episen.cloudfunctions.net/orch-http-image-upload' --form 'image=@"PATH_TO_IMAGE"'
Résultat (Image non offensive) Sur le bucket-input
Résultat (Image offensive)
Sur le bucket-input

Le curl du requête
curl --location --request GET 'https://us-central1-orchestration-gcp-episen.cloudfunctions.net/orch-http-image-search'
Si l'image téléchargée dans l'étape précédente n'est pas offensive, on doit la voir dans la liste des images récupérées, sinon elle en sera présente. (L'exemple de l'image off_blood => offensive)
Comme vu avant, la détéction se fera automatiquement après le téléchargement de l'image, mais sinon on peut faire la détéction manuellement en envoyant une requête à l'url de la fonction Detect
Le curl du requête
curl --location --request POST 'https://us-central1-orchestration-gcp-episen.cloudfunctions.net/orch-http-image-detect' \
--header 'Content-Type: application/json' \
--data-raw '{
"imageId" : "NOM_IMAGE"
}'
Dans la partie suivante j'explique l'implémentation de chaque function ainsi que son code (toujours disponible sur github)
On créera 3 cloud functions qui prendront en charge pour chacune d'elles ce qui suit :
- La recherche et le téléchargement d'une image
- Le filtrage et la détection des images offensives
- Le téléchargement d'une image vers le cloud
Les propriétés de chaque function sont comme suivant (figurant dans la capture d'écran en dessous) :
| Propriété | Valeur |
|---|---|
| Environment | 1st Generation |
| Region | us-central1 |
| Nom | orc-http-image-{search, detect, upload} |
| Trigger | HTTP |
| Authentification | Sans (pour vous faciliter l'intéraction avec les c-functions) |
| HTTPS | Non (pour simplifier nos l'envoie des requêtes) |
| Langage Programmation | Nodejs (JS) |
Screenshot du GCP
| C-Function | Trigger URL |
|---|---|
| Search Function | https://us-central1-orchestration-gcp-episen.cloudfunctions.net/orch-http-image-search |
| Detection Function | https://us-central1-orchestration-gcp-episen.cloudfunctions.net/orch-http-image-detect |
| Upload Function | https://us-central1-orchestration-gcp-episen.cloudfunctions.net/orch-http-image-upload |
Comme mentionné avant, nos cloud functions seront écrites avec NodeJs. Ce choix est principalement influencé par la simplicité du langage (JS) et la grande adoption de l'environement NodeJs.
Initialement, on modifie le modèle fourni par GCP pour et on déploit les functions pour tester le bon fonctionnement des triggers :
Après le deploiment on enverra une requete sur l'url de déclenchement de notre c-function pour vérifier que cette dernièrre se déclenche correctement :
Après avoir envoyé des requêtes, on peut remarquer la mise à jour des dashboards sur GCP :
Les Cloud Functions sont dorénavent déployées est prête à etre utilisées :
Pour le code des C-Functions, vous pouvez le trouver sous le répertoir de chaque c-function repectivement sous le répertoir (GCP) de cette repository (Repo GitHub)
Pour cette partie, on assume la présence des Cloud Storage Buckets pour le stockage des images. On reviendra sur la partie des buckets ultérieurement sur ce rapport.
Cette fonction vise à implémenter un endpoint pour la recherche et le téléchargement des images "safe", cela veut dire les images classifiées comme non-offensives auparavant par la fonction de détection. Cette fonction intéragira uniquement avec le bucket "Search" qui contiendra bien entendu que les images autorisées à être consultées/téléchargées.
On va essayer de commencer avec l'exemple suivant pour tester le téléchargement des images avec nodejs vers notre bucket.
const projectId = 'orchestration-gcp-episen';
const BUCKET_INPUT='image-input'
const storage = new Storage({projectId});
const file = {
name : "nodejs"
}
async function uploadImageToBucketTest(file, blurredBucketName){
// upload image to bucket
const tempLocalPath = "nodejs.png"
const blurredBucket = storage.bucket(blurredBucketName);
const gcsPath = `gs://${blurredBucketName}/${file.name}`;
try {
await blurredBucket.upload(tempLocalPath, {destination: file.name});
console.log(`Uploaded image to: ${gcsPath}`);
} catch (err) {
throw new Error(`Unable to upload image to ${gcsPath}: ${err}`);
}
}
uploadImageToBucketTest(file, BUCKET_INPUT)
On testant en local on reçoit le message suivant indiquant le bon envoi de l'image vers notre bucket image-input :
On vérifiant sur Cloud Storage on voit que l'image a été bien envoyée :
Après avoir deployé notre fonction d'upload on peut la tester avec postman, on voit dans la liste suivante les images ayant déjà présentes sur le bucket-input, on essayer par la suite d'en envoyer une nouvelle image en se basant sur le trigger de la fonction (avec Postman)
Après avoir executer la requête, on peut remarquer que l'image a été bien téléchargée sur le bucket-input :
Le code de la fonction (disponible sur le repo github) :
const fs = require('fs');
const path = require('path');
const axios = require('axios')
const Busboy = require('busboy');
const {Storage} = require('@google-cloud/storage');
const projectId = 'orchestration-gcp-episen';
const BUCKET_INPUT='image-input'
const DETECT_FUNCTION_URL='https://us-central1-orchestration-gcp-episen.cloudfunctions.net/orc-http-image-detect'
const storage = new Storage({projectId});
// Use the express-fileupload middleware
/**
* Responds to any HTTP request.
*
* @param {!express:Request} req HTTP request context.
* @param {!express:Response} res HTTP response context.
*/
exports.upload = (req, res) => {
// Get the file that was set to our field named "image"
const busboy = Busboy({ headers: req.headers });
// Image uploads obj
const uploads = {};
const fileWrites = [];
// This code will process each file uploaded.
busboy.on('file', (fieldname, file, filename) => {
console.log(`Processed image ${filename.filename}`);
const filepath = path.join('/tmp', filename.filename);
uploads[fieldname] = filepath;
const writeStream = fs.createWriteStream(filepath);
file.pipe(writeStream);
// File was processed by Busboy; wait for it to be written to disk.
const promise = new Promise((resolve, reject) => {
file.on('end', () => {
writeStream.end();
});
writeStream.on('finish', resolve);
writeStream.on('error', reject);
});
fileWrites.push(promise);
});
busboy.on('finish', () => {
Promise.all(fileWrites)
.then(() => {
for (const name in uploads) {
const file = uploads[name];
async function upload2bucket() {
fileRes = await storage.bucket(BUCKET_INPUT).upload(file);
fs.unlinkSync(file);
console.log(fileRes[0].name)
const post_data = {
"imageId" : fileRes[0].name
}
await axios.post(DETECT_FUNCTION_URL, post_data)
res.send(fileRes);
}
upload2bucket()
}
});
});
busboy.end(req.rawBody);
};
// Old function
async function uploadImageToBucket(imageName, tempLocalPath){
// upload image to bucket
const blurredBucket = storage.bucket(BUCKET_INPUT);
const gcsPath = `gs://${BUCKET_INPUT}/${imageName}`;
try {
await blurredBucket.upload(tempLocalPath, {destination: imageName});
console.log(`Uploaded image to: ${gcsPath}`);
} catch (err) {
throw new Error(`Unable to upload image to ${gcsPath}: ${err}`);
}
}
Cette fonction a pour objectif de detecter les images offenssives et les envoyer ensuite dans un bucket s'appellant, bucket-blurred. Si l'image n'est pas jugée offenssive, elle sera envoyée vers le bucket-unblurred et sera exposée par la suite aux utilisateurs par la C-Function Search Fucntion.
Notre detect function prend le nom d'une images en corps de la POST request, puis elle récuperera cette image depuis le bucket-input. Après, et à travers l'api vision du GCP SDK, la fonction classifie l'image et puis fera l'envoie soit vers bucket-blurred soit vers bucket-unblurred.
Example : Sur GCP, l'onglet Testing de la Detect Function, on enverra le nom d'une image déjà sauvegradée sur le bucket-input (elephant.jpg) :
Après on peut vérifier sur le bucket-unblurred que l'iamge a été bien envoyée :
Le code de la Detect Function (Dispo sur le repo github également) :
const fs = require('fs');
const path = require('path');
const {Storage} = require('@google-cloud/storage');
const vision = require('@google-cloud/vision');
const gm = require('gm').subClass({imageMagick: true});
const projectId = 'orchestration-gcp-episen';
const BUCKET_INPUT='image-input'
const BUCKET_BLURRED='image-blurred'
const BUCKET_UNBLURRED='image-unblurred'
const storage = new Storage({projectId});
const client = new vision.ImageAnnotatorClient();
// Use the express-fileupload middleware
/**
* Responds to any HTTP request.
*
* @param {!express:Request} req HTTP request context.
* @param {!express:Response} res HTTP response context.
*/
exports.detect = async (req, res) => {
// Get the file that was set to our field named "image"
const IMAGE_ID = req.body.imageId
if(!IMAGE_ID) res.sendStatus(400)
const file = storage.bucket(BUCKET_INPUT).file(IMAGE_ID);
const filePath = `gs://${BUCKET_INPUT}/${IMAGE_ID}`;
// Detect Image w/ Vision
try {
const [result] = await client.safeSearchDetection(filePath);
const detections = result.safeSearchAnnotation || {};
if (detections.adult === 'VERY_LIKELY' || detections.violence === 'VERY_LIKELY') {
console.log(`Detected ${file.name} as offensive.`);
await blurImageAndSaveToBucket(file);
res.sendStatus(201)
} else {
console.log(`Detected ${file.name} as OK.`);
await saveToBucket(file)
res.sendStatus(201)
}
} catch (err) {
console.error(`Failed to analyze ${file.name}.`, err);
res.sendStatus(400)
throw err;
}
};
async function blurImageAndSaveToBucket(file) {
const tempLocalPath = `/tmp/${path.parse(file.name).base}`;
// Download file from bucket.
try {
await file.download({destination: tempLocalPath});
console.log(`Downloaded ${file.name} to ${tempLocalPath}.`);
} catch (err) {
throw new Error(`Image download failed: ${err}`);
}
await new Promise((resolve, reject) => {
gm(tempLocalPath)
.blur(0, 16)
.write(tempLocalPath, (err, stdout) => {
if (err) {
console.error('Failed to blur image.', err);
reject(err);
} else {
console.log(`Blurred image: ${file.name}`);
resolve(stdout);
}
});
});
const blurredBucket = storage.bucket(BUCKET_BLURRED);
// Upload the Blurred image back into the bucket.
const gcsPath = `gs://${BUCKET_BLURRED}/${file.name}`;
try {
await blurredBucket.upload(tempLocalPath, {destination: file.name});
console.log(`Uploaded blurred image to: ${gcsPath}`);
} catch (err) {
throw new Error(`Unable to upload blurred image to ${gcsPath}: ${err}`);
}
// Delete the temporary file.
return fs.unlink(tempLocalPath);
}
async function saveToBucket(file) {
const tempLocalPath = `/tmp/${path.parse(file.name).base}`;
// Download file from bucket.
try {
await file.download({destination: tempLocalPath});
console.log(`Downloaded ${file.name} to ${tempLocalPath}.`);
} catch (err) {
throw new Error(`Image download failed: ${err}`);
}
const unblurredBucket = storage.bucket(BUCKET_UNBLURRED);
// Upload the Blurred image back into the bucket.
const gcsPath = `gs://${BUCKET_UNBLURRED}/${file.name}`;
try {
await unblurredBucket.upload(tempLocalPath, {destination: file.name});
console.log(`Uploaded image to: ${gcsPath}`);
} catch (err) {
throw new Error(`Unable to upload image to ${gcsPath}: ${err}`);
}
// Delete the temporary file.
fs.unlinkSync(tempLocalPath);
return;
}
Cette fonction récupère tout simplement toutes les images dans le bucket-unblurred. Il fallait tout d'abord rendre les objets de ce bucket publiques pour accès depuis internet.
Exemple (réponse http) :
[
{
"image": "elephant.jpg",
"link": "https://storage.googleapis.com/download/storage/v1/b/image-unblurred/o/elephant.jpg?generation=1672757457733781&alt=media"
}
]
L'objet de réponse contient le nom de l'image ainsi que son url de type media (pour l'affichage et le téléchargemet)
Code (Dispo également sur github) :
const fs = require('fs');
const path = require('path');
const {Storage} = require('@google-cloud/storage');
const projectId = 'orchestration-gcp-episen';
const BUCKET_UNBLURRED='image-unblurred'
const storage = new Storage({projectId});
// Use the express-fileupload middleware
/**
* Responds to any HTTP request.
*
* @param {!express:Request} req HTTP request context.
* @param {!express:Response} res HTTP response context.
*/
exports.search = async (req, res) => {
const [files] = await storage.bucket(BUCKET_UNBLURRED).getFiles()
const resObj = []
files.forEach(file => resObj.push({'image' : file.name, 'link' : file.metadata.mediaLink}))
res.send(resObj)
};
On créera 3 buckets pour le stockage des :
- Toutes les Images initialement (Venant de la fonction upload)
- Les images Non-Offensives (è consommer par la fonction serach après classification par la fonction detect)
- Les images Offensives (après classification par la fonction detect)
Les buckets seront créées suivant la configucation suivante :
Voici la liste complète des buckets créées sur Cloud Storage :





















