-- 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 . module Files ( FileName, LocalPath, WebPath, Path(..) , (), (), (<.>) , fileName, subPaths, pathLength , localPath, webPath, fromWebPath , FSNode(..), AnchoredFSNode(..) , nodeName, isHidden, flattenDir, filterDir , readDirectory, copyTo , ensureParentDir, remove, isOutdated ) where import Data.List (isPrefixOf, length, sortOn) import Data.Function ((&)) import Data.Functor ((<&>)) import Data.Text (pack, unpack) import Data.Aeson (ToJSON, FromJSON) import qualified Data.Aeson as JSON import System.Directory ( doesDirectoryExist , doesPathExist , canonicalizePath , getModificationTime , listDirectory , createDirectoryIfMissing , removePathForcibly , copyFile ) import qualified System.FilePath import qualified System.FilePath.Posix type FileName = String type LocalPath = String type WebPath = String -- | Reversed path component list newtype Path = Path [FileName] deriving Show instance ToJSON Path where toJSON = JSON.String . pack . webPath instance FromJSON Path where parseJSON = JSON.withText "Path" (return . fromWebPath . unpack) instance Eq Path where left == right = webPath left == webPath right () :: Path -> Path -> Path (Path l) (Path r) = Path (r ++ l) ( FileName -> Path (Path path) ) :: FileName -> Path -> Path file /> (Path path) = Path (path ++ [file]) (<.>) :: Path -> String -> Path (Path (filename:pathto)) <.> ext = Path $ System.FilePath.addExtension filename ext : pathto (Path _) <.> ext = Path [ext] fileName :: Path -> Maybe FileName fileName (Path (name:_)) = Just name fileName _ = Nothing subPaths :: Path -> [Path] subPaths (Path path) = map Path $ subpaths path where subpaths [] = [] subpaths full@(_:r) = full : subpaths r pathLength :: Path -> Int pathLength (Path path) = Data.List.length path localPath :: Path -> LocalPath localPath (Path path) = System.FilePath.joinPath $ reverse path webPath :: Path -> WebPath webPath (Path path) = System.FilePath.Posix.joinPath $ reverse path fromWebPath :: WebPath -> Path fromWebPath = Path . reverse . System.FilePath.Posix.splitDirectories data FSNode = File { path :: Path , canonicalPath :: FilePath } | Dir { path :: Path , canonicalPath :: FilePath , items :: [FSNode] } deriving Show data AnchoredFSNode = AnchoredFSNode { anchor :: LocalPath , root :: FSNode } deriving Show nodeName :: FSNode -> Maybe FileName nodeName = fileName . path isHidden :: FSNode -> Bool isHidden = hiddenName . nodeName where hiddenName :: Maybe FileName -> Bool hiddenName Nothing = False hiddenName (Just filename) = "." `isPrefixOf` filename && length filename > 1 -- | DFS with intermediate dirs first. flattenDir :: FSNode -> [FSNode] flattenDir file@File{} = [file] flattenDir dir@Dir{items} = dir:concatMap flattenDir items -- | Filters a dir tree. The root is always returned. filterDir :: (FSNode -> Bool) -> AnchoredFSNode -> AnchoredFSNode filterDir cond (AnchoredFSNode anchor root) = AnchoredFSNode anchor (filterNode root) where filterNode :: FSNode -> FSNode filterNode file@File{} = file filterNode Dir{path, canonicalPath, items} = filter cond items & map filterNode & Dir path canonicalPath readDirectory :: LocalPath -> IO AnchoredFSNode readDirectory root = AnchoredFSNode root <$> mkNode (Path []) where mkNode :: Path -> IO FSNode mkNode path = do let relPath = localPath (root /> path) canonicalPath <- canonicalizePath relPath isDir <- doesDirectoryExist relPath if isDir then mkDirNode path canonicalPath else mkFileNode path canonicalPath mkFileNode :: Path -> FilePath -> IO FSNode mkFileNode path canonicalPath = return $ File path canonicalPath mkDirNode :: Path -> FilePath -> IO FSNode mkDirNode path canonicalPath = listDirectory (localPath (root /> path)) >>= mapM (mkNode . (path sortOn nodeName <&> Dir path canonicalPath copyTo :: FilePath -> AnchoredFSNode -> IO () copyTo target AnchoredFSNode{anchor, root} = copyNode root where copyNode :: FSNode -> IO () copyNode File{path} = copyFile (localPath $ anchor /> path) (localPath $ target /> path) copyNode Dir{path, items} = createDirectoryIfMissing True (localPath $ target /> path) >> mapM_ copyNode items ensureParentDir :: (FileName -> a -> IO b) -> FileName -> a -> IO b ensureParentDir writer filePath a = createDirectoryIfMissing True parentDir >> writer filePath a where parentDir = System.FilePath.dropFileName filePath remove :: FileName -> IO () remove path = do putStrLn $ "Removing:\t" ++ path removePathForcibly path isOutdated :: Bool -> FilePath -> FilePath -> IO Bool isOutdated onMissingTarget ref target = do refExists <- doesPathExist ref targetExists <- doesPathExist target if refExists && targetExists then do refTime <- getModificationTime ref targetTime <- getModificationTime target return (targetTime < refTime) else return onMissingTarget