Node.jsJavaScriptGatsby

Gatsby Webhook Service (5) - Gatsby

In diesem Artikel geht es darum das Gatsby CLI (Command Line Interface) als Kind - Prozess beim Anfragen der entsprechenden Routen ausführen zulassen.

Gatsby programmatisch steuern

image Außer den Routen in der Grafik kommen noch eine zum Löschen des WordPress Caches (hauptsächlich Bilder) hinzu. Das Starten des Gatsby Prozess wurde in zwei Funktionen ausgelagert. Die Gatsby Kommandozeilenausgabe wird HTML formatiert in Redis gespeichert. Von dort wird sie dann per Polling in einem Wordpress Plugin abgerufen.

./api/routes/gatsby.js
/*
  Module laden
*/
require('dotenv').config();

const path = require('path');
const { promises: fs } = require('fs');
const { spawn } = require('child_process');

module.exports = async (fastify) => {
  /*
    Ohne Bearer Token im Request Header geht es hier nicht weiter.
  */
  fastify.requireAuthentication();
  /*
    Diese Funktion prüft ob das Flag isBuilding gesetzt ist 
    und startet je nach dem Gatsby mit übergebenen Kommando.
    Es gibt direkt ein Objekt zurück, welches zum Client 
    gesendet wird.
  */
  const handleRequest = async gatsbyCmd => {
    const { redis } = fastify;
    const isBuilding = await redis.get('isBuilding');    

    if (isBuilding === 'true') {
      if (gatsbyCmd === 'clean') {
        redis.set('isBuilding', 'false');
      }
      
      return '{ "result": 0 }';
    }

    await redis.set('isBuilding', 'true');
    gatsby(gatsbyCmd);   

    return '{ "result": 1 }';
  }
  /*
    Beim Data Event wird dem message[] Array der Lesebuffer angefügt. 
    Vorab werden die Konsolensteuerzeichen für die farbliche Anpassung
    durch HTML Markup ersetzt. Nach dem Schließen des Prozesses und wenn
    das Kommando *build* war, wird das Bash Script cp.sh ausgeführt.
    Es kopiert den public Ordner in ein Verzeichniss, das den aktuellen 
    Timestamp als Namen hat, löscht ein symbolic Link und erstellt einen
    zum kopierten Verzeichniss. Dadurch gibt es praktisch keine Downtime 
    und man hat die Möglichkeit eines schnellen Rollbacks im Fehlerfall.
  */
  const gatsby = async cmd => {    
    const messages = [];
    const proc = spawn(`gatsby`, [cmd === 'rebuild' ? 'clean' : cmd], {cwd: path.join(`../`, `gatsby`), shell: true});
    
    fastify.redis.set(`gatsby-${cmd}`, JSON.stringify({ code: -1, message: messages }));

    proc.stdout.on('data', data => {
      const msg = data.toString()
                  .replace('\u001b[2K\u001b[1A\u001b[2K\u001b[G', '')
                  .replace('success', '<span style="color:darkgreen; font-weight: bold;">success</span>')
                  .replace('info', '<span style="color:navy; font-weight: bold;">info</span>')
                  .replace('warning', '<span style="color:orange; font-weight: bold;">warning</span>')
                  .replace('ERROR', '<span style="color:red; font-weight: bold;">ERROR</span>')
                  .replaceAll('  ', ' ')
                  .replace('\n\n','\n');      

      if (msg === '\n')
        return;

      messages.push(msg);
      fastify.redis.set(`gatsby-${cmd}`, JSON.stringify({ code: -1, message: messages }))
      fastify.log.info(msg);
    });
    proc.on('error', error => { 
      fastify.redis.set(`gatsby-${cmd}`, JSON.stringify({ code: 1, message: error.message }));
      fastify.redis.set('isBuilding', 'false');
      fastify.log.error(error.message);
    });
    proc.on('close', async code => { 
      fastify.redis.set(`gatsby-${cmd}`, JSON.stringify({ code, message: messages }));
      fastify.redis.set('isBuilding', 'false');
      fastify.log.info(`Gatsby exited (${code})`);

      if (cmd === 'rebuild') {
        gatsby('build');
      } else if (cmd === 'build' && code === 0) {
        const cp = spawn('/copy.sh', { shell: true })
        cp.on('data', data => fastify.log.info(data.toString()));
      }					
    });
  }   
  /*
    Es werden je Route und Methode der Funktion handleRequest() schlicht 
    das entsprechende Gatsby Kommando übergeben.
  */
  fastify.post(
    '/api/gatsby', 
    async (req, res) =>  res
      .header('Content-Type', 'application/json; charset=utf-8')
      .send((await handleRequest('build')))
  );

  fastify.delete(
    '/api/gatsby', 
    async (req, res) => res
      .header('Content-Type', 'application/json; charset=utf-8')
      .send((await handleRequest('clean')))
  );

	fastify.post(
    '/api/gatsby/rebuild', 
    async (req, res) => res
      .header('Content-Type', 'application/json; charset=utf-8')
      .send((await handleRequest('rebuild')))
  );
  /*
    Beim WordPress Cache handelt es sich um eine Besonderheit vom
    gatsby-source-wordpress Plugin. Es lässt sich einstellen die 
    Mediendateien in ein gesondertes Verzeichniss zu speichern 
    damit sie nicht unnötig gelöscht werden. Hierdurch soll es 
    möglich sein diese zu löschen, sollte es doch mal nötig sein.
  */
  fastify.delete('/api/wordpress-cache', async (req, res) => {
    try {
      await fs.rmdir('/usr/lib/gatsby/.wordpress-cache', { recursive: true });
      res.send({ result: 1 });
    } catch(error) {
      fastify.log.error(error.message);
      res.send({ result: 0, message: error.message });
    }
  })
}

Die "privaten" Routen

In den anderen Dateien fehlen die Routen mit den DELETE Methoden. Diese sollen auch nicht für Jedermann verfügbar sein. Diese habe ich in einer extra Datei geschrieben.

./api/routes/private.js
const path = require('path');
const { spawn } = require('child_process');
const { promises: fs } = require('fs');

module.exports = async function(fastify) {  
  /*
    Der Client muss authentifiziert sein.
  */
  fastify.requireAuthentication();
  /*
    Cookies ...
  */
  fastify.get('/api/cookies', async (req, res) => {
    const { redis } = fastify;

    const cookies = await redis.get('cookies');

    res.send(cookies.split(';').map(cookie => cookie.split('=')));
  });
  /*
    ... speichern
  */
  fastify.post('/api/cookies', async (req, res) => {
    const { redis } = fastify;
    const { cookies } = req.body;
    const cookieString = cookies.map(cookie => cookie.join('=')).join(';');
   
    fs.writeFile(path.join(__dirname, '../data/cookie'), cookieString);
    const result = await redis.set('cookies', cookieString);

    res.send({ result });
  });  
  /*
    Instagram Meta Cache löschen
  */
  fastify.delete('/api/instagram/:user', async (req, res) => {      
    const { redis } = fastify;
    const user = req.params.user;  
    const result = await redis.del(user);        

    res.header('Content-Type', 'application/json; charset=utf-8');
    res.send({ result });    
  });
  /*
    Bild löschen
  */
  fastify.delete('/api/static/:image', async (req, res) => {
    const image = req.params.image;
    const [filename] = image.split('?');
    const file = path.join(__dirname, 'static', filename);

    try {
      await fs.rm(file);

    } catch(error) {
      if (error.code === 'ENOENT') {  
        fastify.log.warn(error.message);
        res
          .status(404)
          .send({ result: `${filename} not found`});
      } else {
        fastify.log.error(error.message);            
        throw error;
      }
    }
  });
  /*
    Die Gatsby Ausgabe senden (Zum Polling im Plugin)
  */
  fastify.get('/api/status/:cmd', async (req, res) => {
    const { cmd } = req.params;
    const { redis } = fastify;
    const status = await redis.get(cmd);

    if (status) {
      res.send(status);
      return;
    }   

    res.status(204).send();
  });
}

Letzte Änderungen

Abschließend müssen noch in zwei Dateien aus kommentierter Code... "dekommentiert" werden.

./api/routes/index.js
module.exports = {
  access: require('./access-token'),
  gatsby: require('./gatsby'),
  instagram: require('./instagram'),
  private: require('./private'),
};
./api/app.js
/* ... */

fastify.register(routes.access);
fastify.register(routes.gatsby);
fastify.register(routes.instagram, { ...external, expire });
fastify.register(routes.private);

/* ... */
Der ganze Aufwand nur um Gatsby "malen" zulassen
Blog - xmalanderssein.de
Ich bin Anne und dies hier ist mein persönlicher Blog. Im Fluss des Lebens zeigt sich hier ein Mensch als x-mal anders fühlend und fragend.
image

Weitere Blog Posts aus dieser Serie

  1. Gatsby Webhook Service
  2. Gatsby Webhook Service (2) - Das große Containern
  3. Gatsby Webhook Service (3) - JWT Authentifizierung
  4. Gatsby Webhook Service (4) - Instagram
  5. Gatsby Webhook Service (5) - Gatsby