-- ldgallery - A static generator which turns a collection of tagged -- pictures into a searchable web gallery. -- -- Copyright (C) 2019-2020 Pacien TRAN-GIRARD -- -- This program is free software: you can redistribute it and/or modify -- it under the terms of the GNU Affero General Public License as -- published by the Free Software Foundation, either version 3 of the -- License, or (at your option) any later version. -- -- This program is distributed in the hope that it will be useful, -- but WITHOUT ANY WARRANTY; without even the implied warranty of -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -- GNU Affero General Public License for more details. -- -- You should have received a copy of the GNU Affero General Public License -- along with this program. If not, see <https://www.gnu.org/licenses/>. module Compiler ( compileGallery , writeJSON ) where import GHC.Generics (Generic) import Control.Monad (liftM2, when) import Data.Bool (bool) import Data.Maybe (fromMaybe) import System.FilePath ((</>)) import qualified System.FilePath.Glob as Glob import System.Directory (canonicalizePath, doesFileExist) import Data.Aeson (ToJSON, FromJSON) import qualified Data.Aeson as JSON import Config import Input (InputTree, readInputTree, filterInputTree, sidecar, tags) import Resource ( GalleryItem , GalleryItemProps , Thumbnail , buildGalleryTree , galleryCleanupResourceDir , properties , thumbnail) import Files ( FileName , FSNode(..) , readDirectory , isHidden , nodeName , filterDir , ensureParentDir ) import ItemProcessors (ItemProcessor, itemFileProcessor, thumbnailFileProcessor) import Caching (Cache, noCache, buildItemCache, useCached) defaultGalleryConf :: String defaultGalleryConf = "gallery.yaml" defaultIndexFile :: String defaultIndexFile = "index.json" itemsDir :: String itemsDir = "items" thumbnailsDir :: String thumbnailsDir = "thumbnails" data GalleryIndex = GalleryIndex { properties :: ViewerConfig , tree :: GalleryItem } deriving (Generic, Show, ToJSON, FromJSON) writeJSON :: ToJSON a => FileName -> a -> IO () writeJSON outputPath object = do putStrLn $ "Generating:\t" ++ outputPath ensureParentDir JSON.encodeFile outputPath object loadGalleryIndex :: FilePath -> IO (Maybe GalleryIndex) loadGalleryIndex path = doesFileExist path >>= bool (return Nothing) decodeIndex where decodeIndex = putStrLn ("Loading previous index:\t" ++ path) >> JSON.eitherDecodeFileStrict path >>= either (\err -> warn err >> return Nothing) (return . Just) warn = putStrLn . ("Warning:\tUnable to reuse existing index as cache: " ++) (&&&) :: (a -> Bool) -> (a -> Bool) -> a -> Bool (&&&) = liftM2 (&&) (|||) :: (a -> Bool) -> (a -> Bool) -> a -> Bool (|||) = liftM2 (||) anyPattern :: [String] -> String -> Bool anyPattern patterns string = any (flip Glob.match string . Glob.compile) patterns galleryDirFilter :: GalleryConfig -> [FilePath] -> FSNode -> Bool galleryDirFilter config excludedCanonicalDirs = (not . isHidden) &&& (not . isExcludedDir) &&& (matchesDir (anyPattern $ includedDirectories config) ||| matchesFile (anyPattern $ includedFiles config)) &&& (not . (matchesDir (anyPattern $ excludedDirectories config) ||| matchesFile (anyPattern $ excludedFiles config))) where matchesDir :: (FileName -> Bool) -> FSNode -> Bool matchesDir cond dir@Dir{} = maybe False cond $ nodeName dir matchesDir _ File{} = False matchesFile :: (FileName -> Bool) -> FSNode -> Bool matchesFile cond file@File{} = maybe False cond $ nodeName file matchesFile _ Dir{} = False isExcludedDir :: FSNode -> Bool isExcludedDir Dir{canonicalPath} = canonicalPath `elem` excludedCanonicalDirs isExcludedDir File{} = False inputTreeFilter :: GalleryConfig -> InputTree -> Bool inputTreeFilter GalleryConfig{includedTags, excludedTags} = hasTagMatching (anyPattern includedTags) &&& (not . hasTagMatching (anyPattern excludedTags)) where hasTagMatching :: (String -> Bool) -> InputTree -> Bool hasTagMatching cond = any cond . (fromMaybe [""] . tags) . sidecar compileGallery :: FilePath -> FilePath -> FilePath -> FilePath -> [FilePath] -> Bool -> Bool -> IO () compileGallery configPath inputDirPath outputDirPath outputIndexPath excludedDirs rebuildAll cleanOutput = do config <- readConfig $ inputGalleryConf configPath putStrLn "Inventorying input files" inputDir <- readDirectory inputDirPath excludedCanonicalDirs <- mapM canonicalizePath excludedDirs let sourceFilter = galleryDirFilter config excludedCanonicalDirs let sourceTree = filterDir sourceFilter inputDir putStrLn "Reading input metadata" inputTree <- readInputTree sourceTree let curatedInputTree = filterInputTree (inputTreeFilter config) inputTree let galleryIndexPath = outputGalleryIndex outputIndexPath cachedIndex <- loadCachedIndex galleryIndexPath let cache = mkCache cachedIndex let itemProc = itemProcessor config (cache $ return . Resource.properties) let thumbnailProc = thumbnailProcessor config (cache $ fmap return . Resource.thumbnail) let galleryBuilder = buildGalleryTree itemProc thumbnailProc (tagsFromDirectories config) resources <- galleryBuilder curatedInputTree when cleanOutput $ galleryCleanupResourceDir resources outputDirPath writeJSON galleryIndexPath $ GalleryIndex (viewerConfig config) resources where inputGalleryConf :: FilePath -> FilePath inputGalleryConf "" = inputDirPath </> defaultGalleryConf inputGalleryConf file = file outputGalleryIndex :: FilePath -> FilePath outputGalleryIndex "" = outputDirPath </> defaultIndexFile outputGalleryIndex file = file loadCachedIndex :: FilePath -> IO (Maybe GalleryIndex) loadCachedIndex galleryIndexPath = if rebuildAll then return Nothing else loadGalleryIndex galleryIndexPath mkCache :: Maybe GalleryIndex -> (GalleryItem -> Maybe a) -> Cache a mkCache refGalleryIndex = if rebuildAll then const noCache else useCached (buildItemCache $ fmap tree refGalleryIndex) itemProcessor :: GalleryConfig -> Cache GalleryItemProps -> ItemProcessor GalleryItemProps itemProcessor config cache = itemFileProcessor (pictureMaxResolution config) cache inputDirPath outputDirPath itemsDir thumbnailProcessor :: GalleryConfig -> Cache (Maybe Thumbnail) -> ItemProcessor (Maybe Thumbnail) thumbnailProcessor config cache = thumbnailFileProcessor (thumbnailMaxResolution config) cache inputDirPath outputDirPath thumbnailsDir