Source: services/maintenance.js

'use strict'

const fs = require('fs')
const log = require('an-log')('$maintenance')

module.exports = function ($settings) {
  const maintenanceConfig = $settings.get('application.maintenance', {})
  const lockFile = $settings.get('application.maintenance.lockFile', 'maintenance.lock')

  const maintenanceMiddleware = require('../maintenance/middleware')(maintenanceConfig)

  // 10s entre le set en cli et son entrée en matière effective sur l'appli
  // (ou un set par l'appli et son application par les autres childs du cluster node)
  const DELAY_BETWEEN_LOCK_FILE_CHECKS = 10000
  const defaultResult = {
    mode: 'off',
    reason: ''
  }
  let lastLockFileCheck
  let lastLockResult = defaultResult
  // ce truc n'est plus très utile si on peut fixer la maintenance via le fichier de lock ou cli
  // mais ça prend pas de place… (et ça permet d'être sûr de démarrer tous les childs en mode maintenance,
  // mais faudra le désactiver et faire un reload manuel pour repasser en mode normal)
  if (maintenanceConfig.active) {
    lastLockResult = {
      mode: 'on',
      reason: 'settings'
    }
  }

  /**
   * @callback getMaintenanceModeCallback
   * @param {Object} result
   * @param {string} result.mode on|off
   * @param {string} result.reason 'manuel' ou 'update' ou 'settings' (ou n'importe quelle string mise dans le fichier de lock)
   */
  /**
   * Retourne le mode de maintenance actif ou inactif via un callback
   * @param {getMaintenanceModeCallback} cb
   */
  function getMaintenanceMode (cb) {
    // On vérifie la présence du fichier lock-maintenance au maximum toutes les 5 secondes
    if (lastLockFileCheck && Date.now() < lastLockFileCheck + DELAY_BETWEEN_LOCK_FILE_CHECKS) {
      return cb(null, lastLockResult) // On utilise la dernière valeur connue
    }
    lastLockFileCheck = Date.now()
    fs.readFile(lockFile, 'utf8', function (err, reason) {
      if (err) {
        // Une erreur veut dire que le fichier n'est pas accessible
        lastLockResult = defaultResult
      } else {
        lastLockResult = {
          mode: 'on',
          reason
        }
      }
      cb(null, lastLockResult)
    })
  }

  /**
   * Active/désactive le mode maintenance
   * @param {string} mode 'on' ou 'off', String pour activer ou non le mode maintenance
   * @param {string} reason 'manual' si activé via cli ou 'update' si activé par une update
   * @param {errorCallback} cb
   */
  function setMaintenance (mode, reason, cb) {
    mode = mode.toLowerCase()
    if (!['on', 'off'].includes(mode)) throw new Error('valeur invalide pour paramètre mode')
    if (!reason || typeof reason !== 'string') throw new Error('reason invalide')

    const done = (error) => {
      if (error) return cb(error)
      const etat = mode === 'on' ? 'activée' : 'désactivée'
      const sDelay = Math.round(DELAY_BETWEEN_LOCK_FILE_CHECKS / 1000)
      log(`Page de maintenance ${etat} (effectif dans ${sDelay}s)`)
      // Si cette instance déclenche elle même ce changement il sera instantané,
      // mais si c'est via un cli faudra attendre le délai
      lastLockFileCheck = undefined
      cb()
    }

    // On crée le lockFile
    if (mode === 'on') return fs.writeFile(lockFile, reason, 'utf8', done)
    // On supprime le lockFile
    if (mode === 'off') return fs.unlink(lockFile, done)
  }

  /**
   * Retourne le middleware de maintenance (qui est monté en premier dans la chaîne des middleware de lassi)
   * @return {expressMiddleware}
   */
  function middleware () {
    /**
     * @typedef expressMiddleware
     * @param req express request
     * @param res express response
     * @param next callback pour passer au middleware suivant
     */
    return function (req, res, next) {
      getMaintenanceMode(function (err, { mode }) {
        if (err) return next(err)

        if (mode === 'on') {
          // On passe la main au middleware de maintenance
          // (pas de next puisqu'il envoie la réponse)
          maintenanceMiddleware(req, res)
        } else {
          // On passe la main au middleware suivant
          next()
        }
      })
    }
  }

  return {
    setMaintenance,
    getMaintenanceMode,
    middleware
  }
}