-rw-r--r-- src/Env.hs
1 {-# LANGUAGE DataKinds #-} 2 {-# LANGUAGE DeriveGeneric #-} 3 {-# LANGUAGE DerivingVia #-} 4 {-# LANGUAGE OverloadedStrings #-} 5 {-# LANGUAGE TypeOperators #-} 6 7 module Env ( 8 Config (..), 9 getConfig, 10 Env (..), 11 loadEnv, 12 ) where 13 14 import Control.Monad (filterM, join, when, (<=<)) 15 import Control.Monad.Extra (findM) 16 import Data.Maybe (catMaybes, fromMaybe, isNothing) 17 import qualified Data.Text as T 18 import Dhall 19 import Dhall.Deriving 20 import Path (Abs, Dir, File, Path, dirname, filename, parseAbsDir, toFilePath, (</>)) 21 import Path.IO ( 22 copyDirRecur, 23 copyFile, 24 doesDirExist, 25 ensureDir, 26 forgivingAbsence, 27 isSymlink, 28 listDir, 29 ) 30 import System.Directory ( 31 canonicalizePath, 32 createDirectoryLink, 33 createFileLink, 34 getSymbolicLinkTarget, 35 makeAbsolute, 36 pathIsSymbolicLink, 37 removeFile, 38 ) 39 import System.Exit (die) 40 import qualified System.FilePath as FP 41 42 import Templates (Template (..), loadTemplate) 43 44 {- 45 The Config data type represents the configuration options available in the config file. 46 Each record of the type, without the 'conf' prefix, is an option. 47 -} 48 data Config = Config 49 { confRepos :: [FilePath] 50 , confScan :: Bool 51 , confTemplate :: FilePath 52 , confOutput :: FilePath 53 , confHost :: Text 54 } 55 deriving stock (Generic) 56 deriving 57 (FromDhall) 58 via Codec (Field (CamelCase <<< DropPrefix "conf")) Config 59 60 getConfig :: String -> IO Config 61 getConfig = input auto . T.pack <=< makeAbsolute 62 63 {- 64 The Env data type represents all of the program's state, including user configuration 65 and loaded template data. This can be accessed as immutable global state at any point. 66 -} 67 data Env = Env 68 { envConfig :: Config 69 , envIndexTemplates :: [Template] 70 , envCommitTemplate :: Maybe Template 71 , envBlobTemplate :: Maybe Template 72 , envTreeTemplate :: Maybe Template 73 , envRepoTemplates :: [Template] 74 , envOutput :: Path Abs Dir 75 , envRepos :: [Path Abs Dir] 76 , envHost :: T.Text 77 , envQuiet :: Bool 78 , envForce :: Bool 79 , envRepoCopyStatics :: Path Abs Dir -> IO () 80 } 81 82 {- 83 This creates the runtime environment, collecting the config and loading template data 84 from the template directory. 85 -} 86 loadEnv :: Bool -> Bool -> Config -> IO Env 87 loadEnv quiet force config = do 88 -- First ensure that the output directory exists 89 output <- parseAbsDir <=< canonicalizePath . confOutput $ config 90 ensureDir output 91 92 -- Parse repos for env 93 repos <- 94 if confScan config 95 then do 96 ps <- fmap concat . mapM (fmap fst . ls) . confRepos $ config 97 return . filter ((/=) ".git" . toFilePath . dirname) $ ps 98 else mapM (parseAbsDir <=< canonicalizePath) . confRepos $ config 99 100 -- Find template files, copying the static files as is 101 (dirs, files) <- ls . confTemplate $ config 102 (dirsRepo, filesRepo) <- ls $ confTemplate config FP.</> "repo" 103 copyStaticDirs dirs output 104 copyStaticFiles files output 105 106 -- Load files from template directory 107 indexT <- collectTemplates files 108 commitT <- findTemplate "foreach.commit.html" filesRepo 109 blobT <- findTemplate "foreach.blob.html" filesRepo 110 treeT <- findTemplate "foreach.tree.html" filesRepo 111 repoT <- 112 collectTemplates 113 . filter (flip notElem ["foreach.commit.html", "foreach.blob.html", "foreach.tree.html"] . toFilePath . filename) 114 $ filesRepo 115 116 -- Exit early if we didn't find any templates 117 when 118 ( all null [indexT, repoT] && all isNothing [commitT, blobT, treeT] 119 ) 120 $ die "No templates were found." 121 122 -- App environment 123 return 124 Env 125 { envConfig = config 126 , envIndexTemplates = indexT 127 , envCommitTemplate = commitT 128 , envBlobTemplate = blobT 129 , envTreeTemplate = treeT 130 , envRepoTemplates = repoT 131 , envOutput = output 132 , envRepos = repos 133 , envHost = confHost config 134 , envQuiet = quiet 135 , envForce = force 136 , envRepoCopyStatics = \p -> copyStaticDirs dirsRepo p >> copyStaticFiles filesRepo p 137 } 138 where 139 ls :: FilePath -> IO ([Path Abs Dir], [Path Abs File]) 140 ls dir = do 141 canon <- parseAbsDir =<< canonicalizePath dir 142 exists <- doesDirExist canon 143 if exists 144 then listDir canon 145 else return ([], []) 146 147 collectTemplates :: [Path Abs File] -> IO [Template] 148 collectTemplates = fmap catMaybes . mapM loadTemplate <=< filterM isMatch 149 where 150 isMatch p = do 151 ((FP.takeExtension . toFilePath $ p) == ".html" &&) 152 <$> (fmap not . pathIsSymbolicLink . toFilePath $ p) 153 154 findTemplate :: FilePath -> [Path Abs File] -> IO (Maybe Template) 155 findTemplate name = fmap join . mapM loadTemplate <=< findM isMatch 156 where 157 isMatch p = 158 ((toFilePath . filename $ p) == name &&) 159 <$> (fmap not . pathIsSymbolicLink . toFilePath $ p) 160 161 {- 162 The logic for copying static files and folders. Any file or folder in the 163 ``confTemplate`` is considered static if: 164 165 - it is a symbolic link, or 166 - it does not end in ".html" or ".include". 167 168 Symbolic links are not followed and are copied as is. This means that a symbolic link 169 from `confTemplate/link.html` to `gitja/index.html` will be copied, keeping the link 170 intact, resulting in a symbolic link at `output/link.html` essentially pointing to 171 `output/gitja/index.html`. 172 -} 173 copyStaticDirs :: [Path Abs Dir] -> Path Abs Dir -> IO () 174 copyStaticDirs dirs output = mapM_ copy dirs 175 where 176 copy :: Path Abs Dir -> IO () 177 copy p = do 178 let isRepo = (toFilePath . dirname $ p) == "repo/" 179 isLink <- pathIsSymbolicLink . toFilePath $ p 180 when (not isRepo || isLink) $ do 181 let output' = output </> dirname p 182 let fp = FP.dropTrailingPathSeparator . toFilePath $ p 183 if isLink 184 then do 185 target <- getSymbolicLinkTarget fp 186 createDirectoryLink target . FP.dropTrailingPathSeparator . toFilePath $ output' 187 else copyDirRecur p output' 188 189 copyStaticFiles :: [Path Abs File] -> Path Abs Dir -> IO () 190 copyStaticFiles files output = mapM_ copy files 191 where 192 copy :: Path Abs File -> IO () 193 copy p = do 194 let fp = toFilePath p 195 let isTemplate = FP.takeExtension fp `elem` [".html", ".include"] 196 isLink <- pathIsSymbolicLink fp 197 when (not isTemplate || isLink) $ do 198 let output' = output </> filename p 199 if isLink 200 then do 201 target <- getSymbolicLinkTarget fp 202 maybeExists <- forgivingAbsence . isSymlink $ output' 203 let exists = fromMaybe False maybeExists 204 when exists . removeFile . toFilePath $ output' 205 createFileLink target . toFilePath $ output' 206 else copyFile p output' 207