view mgtool.php @ 262:3c9ce1ad3c45

Fix gmagick/imagick checking.
author Matti Hamalainen <ccr@tnsp.org>
date Sun, 16 Dec 2018 16:53:48 +0200
parents 32c13f594e39
children 745af791367d
line wrap: on
line source

#!/usr/bin/php
<?php
//
// Yet Another Image Gallery
// -- Commandline tool for creating / updating gallery
// Programmed and designed by Matti 'ccr' Hamalainen <ccr@tnsp.org>
// (C) Copyright 2015-2018 Tecnic Software productions (TNSP)
//
require_once "mgallery.inc.php";

//
// Array for specifying what will be copied and converted
// from the image file's EXIF information tag(s).
//
$galExifConversions =
[
  [ MG_STR,   "caption"     , "ImageDescription" ],
  [ MG_STR,   "copyright"   , "Copyright" ],
  [ MG_STR,   "model"       , "Model" ],
  [ MG_INT,   "width"       , ["COMPUTED", "Width"] ],
  [ MG_INT,   "height"      , ["COMPUTED", "Height"] ],
  [ MG_DVA,   "fnumber"     , "FNumber" ],
  [ MG_DVA,   "exposure"    , "ExposureTime" ],
  [ MG_INT,   "iso"         , "ISOSpeedRatings" ],
  [ MG_STR,   "lensmodel"   , "UndefinedTag:0xA434" ],
  [ MG_DVA,   "focallength" , "FocalLength" ],
  [ MG_DATE,  "datetime"    , "DateTimeOriginal" ],
  [ MG_DATE,  "datetime"    , "DateTimeDigitized" ],
  [ MG_INT,   "filesize"    , "FileSize" ],
];

define("GCMD_UPDATE"      , 1);
define("GCMD_RESCAN"      , 2);
define("GCMD_CLEAN"       , 3);

define("GCLEAN_CACHES"    , 0x01);
define("GCLEAN_IMAGES"    , 0x02);
define("GCLEAN_TRASH"     , 0x04);
define("GCLEAN_ALL"       , 0x0f);

define("GUPD_MED_IMAGE"   , 0x01);
define("GUPD_TN_IMAGE"    , 0x02);
define("GUPD_IMAGES"      , 0x0f);
define("GUPD_EXIF_INFO"   , 0x10);
define("GUPD_CAPTION"     , 0x20);


//
// Convert and scale image file function, for generating
// the intermediate size images and thumbnails. Uses the
// PHP ImageMagick or GraphicsMagick bindings.
//
function mgConvertImage($inFilename, $outFilename, $outDim, $outFormat, $outQuality, $thumb)
{
  if (extension_loaded("imagick") && extension_loaded("gmagick"))
  {
    mgError("FATAL ERROR! Both ImageMagick AND GraphicsMagick modules enabled in PHP! This will cause problems! Refusing to work.\n");
    exit(1);
  }

  if (extension_loaded("imagick"))
  {
    // Create conversion entity
    try
    {
      $img = new Imagick($inFilename);
    }
    catch (Exception $e)
    {
      return mgError("ImageMagick exception for file '".$inFilename."':\n".$e->getMessage()."\n");
    }

    if ($img === FALSE)
      return mgError("ImageMagick could not digest '".$inFilename."'.\n");

    $profiles = $img->getImageProfiles("icc", true);
    $img->setImageDepth(16);
    $img->transformImageColorspace(Imagick::COLORSPACE_SRGB);
    $img->setImageColorspace(Imagick::COLORSPACE_SRGB);

    if ($outDim !== FALSE)
    {
      // Get dimensions, setup background
      $dim = $img->getImageGeometry();
      $img->setGravity(imagick::GRAVITY_CENTER);

      if ($dim["width"] < $dim["height"])
      {
        $stmp = $outDim[0];
        $outDim[0] = $outDim[1];
        $outDim[1] = $stmp;
      }

      if ($dim["width"] != $outDim[0] || $dim["height"] != $outDim[1])
      {
        $outDim[1] = ($dim["height"] * $outDim[0]) / $dim["width"];
      }

      // Act based on image size vs. desired size and $thumb mode
      if ($thumb || $dim["width"] != $outDim[0] || $dim["height"] != $outDim[1])
      {
        $img->resizeImage($outDim[0], $outDim[1], Imagick::FILTER_LANCZOS, 1);
        $img->setImageExtent($outDim[0], $outDim[1]);
        $img->unsharpMaskImage(0, 0.5, 1, 0.05);
      }
    }

    $img->setImageDepth(8);
    $tfmt = strtolower($outFormat);
    switch ($tfmt)
    {
      case "jpeg":
        $img->setFormat("JPEG");
        $img->setImageCompression(Imagick::COMPRESSION_JPEG);
        break;

      case "webp":
        $img->setFormat("WEBP");
        $img->setOption('webp:method', '6');
        break;

       default:
        return mgError("Unsupported MGallery med/tn format '".$tfmt."'.\n");
    }

    $img->setImageCompressionQuality($outQuality);

    $img->stripImage();
    if (!empty($profiles))
      $img->profileImage("icc", $profiles["icc"]);

    $img->writeImage($outFilename);
    $img->removeImage();
  }
  else
  if (extension_loaded("gmagick"))
  {
    // Create conversion entity
    try
    {
      $img = new Gmagick($inFilename);
    }
    catch (Exception $e)
    {
      return mgError("GraphicsMagick exception for file '".$inFilename."':\n".$e->getMessage()."\n");
    }

    if ($img === FALSE)
      return mgError("GraphicsMagick could not digest '".$inFilename."'.\n");

//    $profiles = $img->getImageProfile("icc");
//    $img->setImageDepth(16);
//    $img->setImageColorspace(Gmagick::COLORSPACE_SRGB);

    if ($outDim !== FALSE)
    {
      // Get dimensions, setup background
      $dim = $img->getImageGeometry();
      //$img->setGravity(Gmagick::GRAVITY_CENTER);

      if ($dim["width"] < $dim["height"])
      {
        $stmp = $outDim[0];
        $outDim[0] = $outDim[1];
        $outDim[1] = $stmp;
      }

      if ($dim["width"] != $outDim[0] || $dim["height"] != $outDim[1])
      {
        $outDim[1] = ($dim["height"] * $outDim[0]) / $dim["width"];
      }

      // Act based on image size vs. desired size and $thumb mode
      if ($thumb || $dim["width"] != $outDim[0] || $dim["height"] != $outDim[1])
      {
        $img->resizeImage($outDim[0], $outDim[1], Gmagick::FILTER_LANCZOS, 1);
        //$img->setImageExtent($outDim[0], $outDim[1]);
        //$img->unsharpMaskImage(0, 0.5, 1, 0.05);
      }
    }

    $img->setImageDepth(8);
    $tfmt = strtolower($outFormat);
    switch ($tfmt)
    {
      case "jpeg":
        $img->setFormat("JPEG");
        $img->setImageCompression(Gmagick::COMPRESSION_JPEG);
        break;

      case "webp":
        $img->setFormat("WEBP");
//        $img->setOption('webp:method', '6');
        break;

       default:
        return mgError("Unsupported MGallery med/tn format '".$tfmt."'.\n");
    }

    $img->setCompressionQuality($outQuality);

    $img->stripImage();
//    if (!empty($profiles))
//      $img->profileImage("icc", $profiles);

    $img->writeImage($outFilename);
    $img->removeImage();
  }
  else
  {
    return mgError("No ImageMagick OR GraphicsMagick module in PHP!\n");
  }

  return TRUE;
}


//
// Converts one value (mainly from EXIF tag information)
// by doing explicit type casting and special conversions.
//
function mgConvertExifData($val, $vtype)
{
  switch ($vtype)
  {
    case MG_STR: return is_array($val) ? $val : (string) $val;
    case MG_INT: return intval($val);
    case MG_BOOL: return intval($val);
    case MG_DVA:
      if (sscanf($val, "%d/%d", $v1, $v2) == 2 && $v2 != 0 && $v1 != 0)
      {
        if ($v1 < $v2)
          return sprintf("1/%1.1f", $v2 / $v1);
        else
          return sprintf("%1.1f", $v1 / $v2);
      }
      else
        return $val;

    case MG_DATE:
      return date_timestamp_get(date_create_from_format("Y:m:d H:i:s", $val));

    default:
      return $val;
  }
}


//
// Conditionally copies one "field" from an associated array/hash to another.
// If destination is already SET, nothing will be done. If source does
// not exist (e.g. one or more of the keys do not exist in source array),
// a default value will be used, if provided.
// Source may have multi-depth keys, destination has one key.
//
function mgCopyEntryData(&$dst, $src, $vtype, $dkey, $skeys, $default = NULL)
{
  // Is destination already set?
  if (isset($dst[$dkey]))
    return FALSE;

  // If input key is not array, change it into one
  if (!is_array($skeys))
    $skeys = array($skeys);

  // Traverse input array by using consequent keys
  $tmp = &$src;
  foreach ($skeys as $skey)
  {
    if (!array_key_exists($skey, $tmp))
    {
      // Key didn't exist, try for default
      if ($default !== NULL)
        $dst[$dkey] = $default;
      return FALSE;
    }
    else
      $tmp = &$tmp[$skey];
  }

  // Optionally convert the input value
  $dst[$dkey] = mgConvertExifData($tmp, $vtype);
  return TRUE;
}


//
// Attempt to get gallery album data from various sources.
//
function mgGetAlbumData($galBasePath, $galPath)
{
  // Check path permissions
  $galData = [];
  if (is_readable($galPath))
  {
    // First, try to read gallery/album info file
    $filename = mgGetPath($galPath, "info_file");
    if ($filename !== FALSE && file_exists($filename))
    {
      mgDebug("Reading INFOFILE: ".$filename."\n");
      if (($galData = parse_ini_file($filename, FALSE)) === FALSE)
        $galData = [];
    }

    // Read header file, if any, and we don't have "header" field set yet
    $filename = mgGetPath($galPath, "header_file");
    if ($filename !== FALSE && file_exists($filename) &&
        !isset($galData["header"]))
    {
      mgDebug("Reading HEADERFILE: ".$filename."\n");
      $galData["header"] = file_get_contents($filename);
    }

    // Check for alternate key/values for album title/caption
    if (isset($galData["title"]) && !isset($galData["caption"]))
    {
      $galData["caption"] = $galData["title"];
      unset($galData["title"]);
    }
  }
  else
    $galData["hide"] = TRUE;

  // If caption is not set, use last path component for
  // a fallback value in case we don't discover proper title
  // from other sources we can't check here yet.
  $path = explode("/", $galPath);
  $galData["fallback_caption"] = ucfirst(str_replace("_", " ", end($path)));

  // Last, store the current gallery path
  $len = strlen($galBasePath);
  if ($len < strlen($galPath) && substr($galPath, 0, $len) == $galBasePath)
    $galData["path"] = substr($galPath, $len);
  else
    $galData["path"] = "";

  return $galData;
}


//
// Parse a simple image captions file, with format of one entry per line.
// Lines starting with "# " are comments (note the whitespace), empty lines ignored.
// First continuous non-whitespace text is file/dirname, followed
// by caption text, which is separate with whitespace from the filename.
// Filenames starting with # will be made hidden entries.
// Filenames starging with % will hide contents.
//
function mgReadCaptionsFile($galBasePath, $galPath)
{
  $captions = [];
  $filename = mgGetPath($galPath, "captions_file");
  if ($filename === FALSE || ($fp = @fopen($filename, "rb")) === FALSE)
    return $captions;

  mgDebug("Reading CAPTIONS: ".$filename."\n");

  // Read and parse data
  while (!feof($fp))
  {
    $str = trim(fgets($fp));
    // Ignore comments and empty lines
    if ($str != "#" && $str != "")
    {
      if (preg_match("/^([#%]?)\s*(\S+?)\s+(.+)$/", $str, $m))
        $captions[$m[2]] = ["caption" => $m[3], "hide" => ($m[1] == "#"), "hide_contents" => ($m[1] == "%"), "used" => FALSE];
      else
      if (preg_match("/^([#%]?)\s*(\S+?)$/", $str, $m))
        $captions[$m[2]] = ["hide" => ($m[1] == "#"), "hide_contents" => ($m[1] == "%"), "used" => FALSE];
    }
  }

  fclose($fp);
  return $captions;
}


//
// Create directory with specified permissions
//
function mgMakeDir($path, $perm)
{
  if (!file_exists($path))
  {
    if (mkdir($path, $perm, TRUE) === false)
      return mgError("Could not create directory '".$path."'\n");
  }
  return TRUE;
}


//
// Print a simple yes/no prompt with given message
// and default value.
//
function mgYesNoPrompt($msg, $default = FALSE)
{
  echo $msg." [".($default ? "Y/n" : "y/N")."]? ";
  $sprompt = strtolower(trim(fgets(STDIN)));

  if ($sprompt == "")
    return $default;
  else
    return $sprompt[0] == 'y';
}


//
// Delete given directory OR file, recursively
//
function mgDelete($path, $showFiles)
{
  global $flagDoDelete, $countDoDelete;
  if (is_dir($path))
  {
    if (($dirHandle = @opendir($path)) === FALSE)
      return mgError("Could not read directory '".$path."'.\n");

    while (($dirFile = @readdir($dirHandle)) !== FALSE)
    {
      if ($dirFile != "." && $dirFile != "..")
        mgDelete($path."/".$dirFile, $showFiles);
    }

    closedir($dirHandle);

    echo " - ".$path." [DIR]\n";
    $countDoDelete++;
    if ($flagDoDelete)
      rmdir($path);
  }
  else
  if (file_exists($path))
  {
    if ($showFiles)
      echo " - ".$path."\n";

    $countDoDelete++;
    if ($flagDoDelete)
      unlink($path);
  }
}


function mgDeleteConditional($path, &$noDelete)
{
  global $flagDoDelete, $countDoDelete;
  if (is_dir($path))
  {
    if (($dirHandle = @opendir($path)) === FALSE)
      return mgError("Could not read directory '".$path."'.\n");

    while (($dirFile = @readdir($dirHandle)) !== FALSE)
    {
      if ($dirFile != "." && $dirFile != "..")
        mgDeleteConditional($path."/".$dirFile, $noDelete);
    }

    closedir($dirHandle);

    if (!array_key_exists($path, $noDelete))
    {
      $countDoDelete++;
      if ($flagDoDelete)
        rmdir($path);
      else
        echo "DEL DIR '".$path."'\n";
    }
  }
  else
  if (file_exists($path) && !array_key_exists($path, $noDelete))
  {
    $countDoDelete++;
    if ($flagDoDelete)
      unlink($path);
    else
      echo "DEL FILE '".$path."'\n";
  }
}


//
// Check if we have received the quit signal
// and if yes, quit cleanly now.
//
function mgCheckQuit($now = FALSE)
{
  global $flagQuit;

  // Dispatch pending signals
  pcntl_signal_dispatch();

  // Check result
  if ($now && $flagQuit)
    mgFatal("Quitting.\n");

  return $flagQuit;
}


function mgNeedUpdate($entry, $field, $cvalue)
{
  if (!array_key_exists($field, $entry))
    return TRUE;

  return ($entry[$field] < $cvalue);
}


function mgSortFunc($a, $b)
{
  if (isset($a["datetime"]) && isset($b["datetime"]))
    return $b["datetime"] - $a["datetime"];
  else
  if (isset($a["base"]) && isset($b["base"]))
    return strcmp($b["base"], $a["base"]);
  else
    return 0;
}


function mgWriteGalleryCache($cacheFilename, &$gallery, &$entries, &$parentEntry)
{
  // Store gallery cache for this directory
  $images = [];
  $albums = [];
  $output = [];

  // If we are hiding contents of an album, generate no data
  if (!$parentEntry["hide_contents"])
  {
    foreach ($entries as $ename => &$edata)
    {
      if ($edata["hide"])
        continue;

      unset($edata["hide"]);
      if ($edata["type"] == 0)
        $images[$ename] = &$edata;
      else
        $albums[$ename] = &$edata;

      $output[$ename] = &$edata;
    }

    uasort($images, "mgSortFunc");
    krsort($albums);

    // Choose gallery album image
    if (count($images) > 0)
    {
      if (isset($gallery["albumpic"]) &&
          isset($images[$gallery["albumpic"]]))
      {
        $parentEntry["image"] = $gallery["albumpic"];
      }
      else
      {
        end($images);
        $parentEntry["image"] = key($images);
      }
    }
    else
    {
      foreach ($albums as $aid => &$adata)
      if (isset($adata["image"]))
      {
        $parentEntry["image"] = &$adata;
        break;
      }
    }
  }

  $str =
    "<?\n".
    "\$galData = ".var_export($gallery, TRUE).";\n".
    "\$galAlbumsIndex = ".var_export(array_keys($albums), TRUE).";\n".
    "\$galImagesIndex = ".var_export(array_keys($images), TRUE).";\n".
    "\$galEntries = ".var_export($output, TRUE).";\n".
    "?>";

  if (@file_put_contents($cacheFilename, $str, LOCK_EX) === FALSE)
    return mgError("Error writing '".$cacheFilename."'\n");

  return TRUE;
}


function mgHandleDirectory($mode, $basepath, $path, &$parentData, &$parentEntry, $writeMode, $startAt)
{
  global $galExifConversions, $galTNPath, $galMedPath, $galCleanFlags;

  // Get cache file path
  if (($cacheFilename = mgGetPath($path, "cache_file")) === FALSE)
    return mgError("Cache filename / path not set.\n");

  mgCheckQuit(TRUE);

  // Read directory contents
  $entries = [];
  if (($dirHandle = @opendir($path)) === FALSE)
    return mgError("Could not read directory '".$path."'.\n");

  while (($dirFile = @readdir($dirHandle)) !== FALSE)
  {
    $realFile = $path."/".$dirFile;
    if (is_dir($realFile))
    {
      if ($dirFile[0] != "." && $dirFile != $galTNPath && $dirFile != $galMedPath)
        $entries[$dirFile] = ["type" => 1, "base" => $dirFile, "ext" => "", "mtime" => filemtime($realFile)];
    }
    else
    if (preg_match("/^([^\/]+)(".mgGetSetting("format_exts").")$/i", $dirFile, $dirMatch))
      $entries[$dirFile] = ["type" => 0, "base" => $dirMatch[1], "ext" => $dirMatch[2], "mtime" => filemtime($realFile), "hide" => false];
  }
  closedir($dirHandle);

  mgCheckQuit();

  $tnPath = $path."/".$galTNPath;
  $medPath = $path."/".$galMedPath;
  $generatedFiles = [];
  $generatedFiles[$tnPath] = 1;
  $generatedFiles[$medPath] = 1;

  // Cleanup mode
  if ($mode == GCMD_CLEAN)
  {
    $gallery = [];

    if ($writeMode)
    {
      if ($galCleanFlags & GCLEAN_CACHES)
        mgDelete($cacheFilename, TRUE);

      if ($galCleanFlags & GCLEAN_IMAGES)
      {
        mgDelete($path."/".$galTNPath, FALSE);
        mgDelete($path."/".$galMedPath, FALSE);
      }

      if ($galCleanFlags & GCLEAN_TRASH)
      {
        foreach ($entries as $ename => &$edata)
        {
          $medFilename = $medPath."/".$ename.".".mgGetSetting("med_format");
          $tnFilename = $tnPath."/".$ename.".".mgGetSetting("tn_format");
          $generatedFiles[$medFilename] = 1;
          $generatedFiles[$tnFilename] = 1;
        }

        // Delete any "trash" files from medium/thumbnail dirs
        mgDeleteConditional($tnPath, $generatedFiles);
        mgDeleteConditional($medPath, $generatedFiles);
      }
    }
  }
  else
  // Update modes
  if ($mode == GCMD_UPDATE || $mode == GCMD_RESCAN)
  {
    // Load current cache file, if it exists
    $galEntries = [];
    $cacheTime = -1;
    if ($mode == GCMD_UPDATE && file_exists($cacheFilename))
    {
      $cacheTime = filemtime($cacheFilename);
      @include $cacheFilename;
    }

    // Read caption data
    $captions = mgReadCaptionsFile($basepath, $path);
    $gallery = mgGetAlbumData($basepath, $path);
    if ($parentData !== NULL && $parentEntry !== NULL)
    {
      $gallery["parent"] = &$parentData;
      mgCopyEntryData($gallery, $parentEntry, MG_STR, "caption", "caption");
    }

    // Start actual processing
    $nentries = count($entries);
    $nentry = 0;
    echo $path." .. ";
    foreach ($entries as $ename => &$edata)
    {
      printf("\r%s (%1.1f%%) ..", $path, ($nentry * 100.0) / $nentries);

      $nentry++;
      $efilename = $path."/".$ename;
      $medFilename = $medPath."/".$ename.".".mgGetSetting("med_format");
      $tnFilename = $tnPath."/".$ename.".".mgGetSetting("tn_format");
      $capFilename = $path."/".$edata["base"].".txt";

      if (array_key_exists($ename, $galEntries))
        $galEntry = &$galEntries[$ename];
      else
        $galEntry = [];

      mgCheckQuit(TRUE);

      // Update with captions file data, if any
      if (array_key_exists($ename, $captions))
      {
        foreach ($captions[$ename] as $ckey => $cval)
          $edata[$ckey] = $cval;
      }

      // Handle entry based on type
      if ($edata["type"] == 0)
      {
        $updFlags = 0;

        // Check what we need to update ..
        if (!file_exists($medFilename) || filemtime($medFilename) < $edata["mtime"])
          $updFlags |= GUPD_MED_IMAGE;

        if (!file_exists($tnFilename) || filemtime($tnFilename) < $edata["mtime"])
          $updFlags |= GUPD_TN_IMAGE;

        if (mgNeedUpdate($galEntry, "mtime", $edata["mtime"]))
          $updFlags |= GUPD_EXIF_INFO;

        if (file_exists($capFilename) &&
          mgNeedUpdate($galEntry, "mtime", filemtime($capFilename)))
          $updFlags |= GUPD_CAPTION;

        // Check for EXIF info
        if (($updFlags & GUPD_EXIF_INFO) &&
            ($exif = @exif_read_data($efilename)) !== FALSE)
        {
          echo "%";
          foreach ($galExifConversions as $conv)
            mgCopyEntryData($edata, $exif, $conv[0], $conv[1], $conv[2]);
        }
        else
        {
          // Copy old data that is not yet in new
          echo "*";
          foreach ($galEntry as $okey => $odata)
          {
            if (!array_key_exists($okey, $edata))
              $edata[$okey] = $odata;
          }
        }

        // Generate thumbnails, etc.
        if ($updFlags & GUPD_IMAGES)
        {
          mgMakeDir($tnPath, 0755);
          mgMakeDir($medPath, 0755);

          if ($updFlags & GUPD_MED_IMAGE)
          {
            echo "1";
            mgConvertImage($efilename, $medFilename,
              [mgGetSetting("med_width"), mgGetSetting("med_height")],
              mgGetSetting("med_format"), mgGetSetting("med_quality"), TRUE);
          }

          if ($updFlags & GUPD_TN_IMAGE)
          {
            echo "2";
            mgConvertImage($efilename, $tnFilename,
              [mgGetSetting("tn_width"), mgGetSetting("tn_height")],
              mgGetSetting("tn_format"), mgGetSetting("tn_quality"), TRUE);
          }
        }

        // Check for .txt caption file
        if ($updFlags & GUPD_CAPTION)
        {
          echo "?";
          if (($tmpData = @file_get_contents($capFilename)) !== FALSE)
            $edata["caption"] = $tmpData;
        }
      }
      else
      if ($edata["type"] == 1)
      {
        // Set some of the album data here
        $tmp = mgGetAlbumData($basepath, $efilename);
        mgCopyEntryData($edata, $tmp, MG_STR, "caption", "caption");
        mgCopyEntryData($edata, $tmp, MG_STR, "caption", "fallback_caption");
        mgCopyEntryData($edata, $tmp, MG_STR, "caption", "title");
        mgCopyEntryData($edata, $tmp, MG_BOOL, "hide", "hide", FALSE);
        mgCopyEntryData($edata, $tmp, MG_BOOL, "hide_contents", "hide_contents", FALSE);
      }
    }

    echo "\r".$path." ..... DONE\n";

    mgCheckQuit(TRUE);
  }
  else
    mgFatal("Invalid work mode '".$mode."'.\n");

  mgCheckQuit(TRUE);

  // Recurse to subdirectories
  foreach ($entries as $ename => &$edata)
  if ($edata["type"] == 1)
  {
    $epath = $path."/".$ename."/";
    $newWriteMode = ($writeMode === FALSE && $epath == $startAt) || $writeMode;

    if (!mgHandleDirectory($mode, $basepath, $epath, $gallery, $edata, $newWriteMode, $startAt))
      return FALSE;
  }

  // Finish update modes
  if ($mode == GCMD_UPDATE || $mode == GCMD_RESCAN)
  {
    // Store gallery cache for this directory
    if ($writeMode && !mgWriteGalleryCache($cacheFilename, $gallery, $entries, $parentEntry))
      return FALSE;
  }

  mgCheckQuit(TRUE);

  return TRUE;
}


function mgSigHandler($signo)
{
  global $flagQuit;
  switch ($signo)
  {
    case SIGTERM:
      mgFatal("Received SIGTERM.\n");
      break;

    case SIGQUIT:
    case SIGINT:
      $flagQuit = TRUE;
      break;

  }
}


function mgCheckMPath($path, $id)
{
  if ($path."/" != mgGetSetting($id))
    mgError("Invalid ".$id." '".mgGetSetting($id)."', using '".$path."'.\n");

  if (strpos($path, "/") !== FALSE || $path == "")
    mgFatal("Invalid ".$id." '".$path."'.\n");
}


function mgProcessGalleries($cmd, $path)
{
  global $galTNPath, $galMedPath;

  // Check validity of some settings
  $galPath = mgGetSetting("base_path");
  $galTNPath = mgCleanPath(TRUE, mgGetSetting("tn_path"));
  $galMedPath = mgCleanPath(TRUE, mgGetSetting("med_path"));

  mgCheckMPath($galTNPath, "tn_path");
  mgCheckMPath($galMedPath, "med_path");

  $parentData = $parentEntry = NULL;
  $writeMode = TRUE;
  $startAt = NULL;

  // Check for path argument
  if ($path !== FALSE)
  {
    // Check the given path, needs to be "under" the gallery path
    $cmp = mgCleanPath(TRUE, mgRealPath($galPath))."/";
    $tmp = mgCleanPath(TRUE, mgRealPath($path))."/";
    if (substr($tmp, 0, strlen($cmp)) != $cmp)
      mgFatal("Path '".$path."' ('".$tmp."') does not reside under '".$galPath."' ('".$cmp."')!\n");

    // Check if we need to bootstrap
    if ($cmp != $tmp)
    {
      $bpath = mgCleanPath(TRUE, mgRealPath($tmp."../"));

      if ($cmd != GCMD_CLEAN)
      {
        $cacheFile = mgGetPath($bpath, "cache_file");
        if (!file_exists($cacheFile))
          mgFatal("Can't start working from '".$path."', parent '".$cacheFile."' does not exist!\n");

        @include($cacheFile);
        if (!isset($galEntries) || !isset($galData))
          mgFatal("Cache file '".$cacheFile."' is broken or stale.\n");
      }

      $writeMode = FALSE;
      $startAt = $tmp;
      $path = $bpath;

      echo "Starting: '".$startAt."' inside '".$path."'.\n";
    }
    else
      $path = $tmp;
  }
  else
    $path = $galPath;

  // Start working
  echo "Gallery path: '".$galPath."', starting at '".$path."' ...\n";
  mgHandleDirectory($cmd, $galPath, $path, $parentData, $parentEntry, $writeMode, $startAt);
}


function mgShowCopyright()
{
  global $mgProgVersion, $mgProgCopyright,
    $mgProgInfo, $mgProgEmail;

  echo
    "MGTool ".$mgProgVersion." - MGallery management tool\n".
    $mgProgInfo." ".$mgProgEmail."\n".
    "(C) Copyright ".$mgProgCopyright."\n";
}


function mgShowHelp()
{
  global $argv;
  echo
    "Usage: ".basename($argv[0])." <command> [arguments]\n".
    "\n".
    "  --help      - Show this help.\n".
    "  --version   - Show version information.\n".
    "\n".
    "  update [path]\n".
    "    Update directories under <path> or all gallery dirs.\n".
    "    Conditionally scans dirs for new or changed images and\n".
    "    updates cache files as needed.\n".
    "\n".
    "  rescan [path]\n".
    "    Like 'update', but forces all cache files to be regenerated\n".
    "    and EXIF/caption/etc information to be re-scanned even\n".
    "    if the file timestamps do not justify re-scanning.\n".
    "\n".
    "  clean <option> [path]\n".
    "    all    - Delete all generated files\n".
    "    images - Delete generated images (thumbnails/mediums)\n".
    "    caches - Delete mgallery data cache files\n".
    "    trash  - Delete any 'unmanaged' files inside thumbnail/medium dirs\n".
    "\n".
    "  config|dump\n".
    "    Display configuration values (with extra information) or\n".
    "    \"dump\" current configuration as is.\n".
    "\n";
}


//
// Main code starts
//
if (php_sapi_name() != "cli" || !empty($_SERVER["REMOTE_ADDR"]))
{
  header("Status: 404 Not Found");
  die();
}

pcntl_signal(SIGTERM, "mgSigHandler");
pcntl_signal(SIGHUP,  "mgSigHandler");
pcntl_signal(SIGQUIT, "mgSigHandler");
pcntl_signal(SIGINT,  "mgSigHandler");

if (mgReadSettings() === FALSE)
  die("MGallery is not configured, failed to find a configuration file.\n");

if (($pageTimeZone = mgGetSetting("timezone")) !== NULL)
  date_default_timezone_set($pageTimeZone);

$cmd = mgCArgLC(1);
switch ($cmd)
{
  case "--version":
  case "version":
  case "ver":
    mgShowCopyright();
    break;

  case FALSE:
    // No arguments
    mgError("Nothing to do. Showing help:\n");

  case "--help":
  case "help":
    if ($cmd !== FALSE)
      mgShowCopyright();
    mgShowHelp();
    break;

  case "update": case "up": case "upd": case "upda":
    mgProcessGalleries(GCMD_UPDATE, mgCArg(2));
    break;

  case "rescan": case "re": case "res":
    mgProcessGalleries(GCMD_RESCAN, mgCArg(2));
    break;

  case "clean": case "cl": case "cle":
    $cmode = mgCArgLC(2, 2);
    switch ($cmode)
    {
      case "al": $galCleanFlags = GCLEAN_ALL; break;
      case "ca": $galCleanFlags = GCLEAN_CACHES; break;
      case "im": $galCleanFlags = GCLEAN_IMAGES; break;
      case "tr": $galCleanFlags = GCLEAN_TRASH; break;
      case FALSE:
        mgFatal("Cleaning requires a mode argument.\n");

      default:
        mgFatal("Invalid clean mode '".mgCArg(2)."'.\n");
    }
    $countDoDelete = 0;
    $flagDoDelete = FALSE;
    mgProcessGalleries(GCMD_CLEAN, mgCArg(3));
    echo "--\n";
    if ($countDoDelete == 0)
    {
      echo "Nothing to clean!\n";
    }
    else
    if (mgYesNoPrompt("Really delete the above files and directories?"))
    {
      echo "Deleting ...\n";
      $flagDoDelete = TRUE;
      mgProcessGalleries(GCMD_CLEAN, mgCArg(3));
    }
    else
    {
      echo "Okay, canceling operation.\n";
    }
    break;

  case "config":
  case "dump":

    foreach ($mgDefaults as $key => $dval)
    {
      $sval = mgGetSetting($key);
      if ($cmd == "dump")
      {
        printf("%-20s = %s\n",
          $key,
          mgGetDValStr($dval, $sval));
      }
      else
      {
        printf("%-20s = %s%s\n",
          $key,
          mgGetDValStr($dval, $sval),
          ($dval[1] !== NULL && $sval !== $dval[1]) ? " (default: ".mgGetDValStr($dval, $dval[1]).")" : "");
      }
    }
    break;

  default:
    mgError("Unknown option/command '".$cmd."'.\n");
    break;
}

?>