{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE TypeApplications #-}
module Clod.FileSystem.Checksums
(
calculateChecksum
, checksumFile
, initializeDatabase
, loadDatabase
, saveDatabase
, updateDatabase
, detectFileChanges
, findChangedFiles
, findRenamedFiles
, getFileStatus
, FileStatus(..)
, cleanupStagingDirectories
, flushMissingEntries
) where
import Control.Exception (try, IOException, SomeException)
import Control.Monad (when, forM)
import qualified Data.ByteString as BS
import qualified Data.Map.Strict as Map
import Data.Maybe (mapMaybe, catMaybes)
import Data.Time (UTCTime, getCurrentTime)
import System.Directory (doesFileExist, doesDirectoryExist, getModificationTime,
removeDirectoryRecursive, createDirectoryIfMissing, renameFile)
import System.FilePath ((</>), takeDirectory)
import GHC.Generics (Generic)
import Numeric (showHex)
import Clod.Types
import Clod.FileSystem.Detection (safeFileExists, safeIsTextFile)
import Clod.FileSystem.Operations (safeReadFile)
import qualified Data.Text.IO as TextIO
import qualified Dhall
import qualified Dhall.Core
import qualified Data.Digest.XXHash.FFI as XXH
import Data.Hashable (hash)
createError :: String -> IOError
createError :: FilePath -> IOException
createError = FilePath -> IOException
Prelude.userError
data FileStatus
= Unchanged
| New
| Modified
| Deleted
| Renamed FilePath
deriving (Int -> FileStatus -> ShowS
[FileStatus] -> ShowS
FileStatus -> FilePath
(Int -> FileStatus -> ShowS)
-> (FileStatus -> FilePath)
-> ([FileStatus] -> ShowS)
-> Show FileStatus
forall a.
(Int -> a -> ShowS) -> (a -> FilePath) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> FileStatus -> ShowS
showsPrec :: Int -> FileStatus -> ShowS
$cshow :: FileStatus -> FilePath
show :: FileStatus -> FilePath
$cshowList :: [FileStatus] -> ShowS
showList :: [FileStatus] -> ShowS
Show, FileStatus -> FileStatus -> Bool
(FileStatus -> FileStatus -> Bool)
-> (FileStatus -> FileStatus -> Bool) -> Eq FileStatus
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: FileStatus -> FileStatus -> Bool
== :: FileStatus -> FileStatus -> Bool
$c/= :: FileStatus -> FileStatus -> Bool
/= :: FileStatus -> FileStatus -> Bool
Eq, (forall x. FileStatus -> Rep FileStatus x)
-> (forall x. Rep FileStatus x -> FileStatus) -> Generic FileStatus
forall x. Rep FileStatus x -> FileStatus
forall x. FileStatus -> Rep FileStatus x
forall a.
(forall x. a -> Rep a x) -> (forall x. Rep a x -> a) -> Generic a
$cfrom :: forall x. FileStatus -> Rep FileStatus x
from :: forall x. FileStatus -> Rep FileStatus x
$cto :: forall x. Rep FileStatus x -> FileStatus
to :: forall x. Rep FileStatus x -> FileStatus
Generic)
calculateChecksum :: BS.ByteString -> Checksum
calculateChecksum :: ByteString -> Checksum
calculateChecksum ByteString
content =
let
hashVal :: Int
hashVal = XXH3 ByteString -> Int
forall a. Hashable a => a -> Int
hash (ByteString -> XXH3 ByteString
forall a. a -> XXH3 a
XXH.XXH3 ByteString
content)
absHash :: Int
absHash = Int -> Int
forall a. Num a => a -> a
abs Int
hashVal
hexStr :: FilePath
hexStr = Int -> ShowS
forall a. Integral a => a -> ShowS
showHex Int
absHash FilePath
""
in FilePath -> Checksum
Checksum FilePath
hexStr
checksumFile :: FileReadCap -> FilePath -> ClodM Checksum
checksumFile :: FileReadCap -> FilePath -> ClodM Checksum
checksumFile FileReadCap
readCap FilePath
path = do
fileExists <- FileReadCap -> FilePath -> ClodM Bool
safeFileExists FileReadCap
readCap FilePath
path
if not fileExists
then throwError $ FileSystemError path (createError "File does not exist")
else do
isText <- safeIsTextFile readCap path
if not isText
then throwError $ ChecksumError $ "Cannot checksum binary or ineligible file: " ++ path
else do
content <- safeReadFile readCap path
return $ calculateChecksum content
initializeDatabase :: ClodM ClodDatabase
initializeDatabase :: ClodM ClodDatabase
initializeDatabase = do
currentTime <- IO UTCTime -> ReaderT ClodConfig (ExceptT ClodError IO) UTCTime
forall a. IO a -> ReaderT ClodConfig (ExceptT ClodError IO) a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO IO UTCTime
getCurrentTime
return $ ClodDatabase
{ dbFiles = Map.empty
, dbChecksums = Map.empty
, dbLastStagingDir = Nothing
, dbLastRunTime = currentTime
}
loadDatabase :: FilePath -> ClodM ClodDatabase
loadDatabase :: FilePath -> ClodM ClodDatabase
loadDatabase FilePath
dbPath = do
fileExists <- IO Bool -> ClodM Bool
forall a. IO a -> ReaderT ClodConfig (ExceptT ClodError IO) a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO Bool -> ClodM Bool) -> IO Bool -> ClodM Bool
forall a b. (a -> b) -> a -> b
$ FilePath -> IO Bool
doesFileExist FilePath
dbPath
if not fileExists
then do
db <- initializeDatabase
liftIO $ createDirectoryIfMissing True (takeDirectory dbPath)
saveDatabase dbPath db
return db
else do
eitherResult <- liftIO $ try @SomeException $ do
sdb <- Dhall.inputFile Dhall.auto dbPath
return $ fromSerializable sdb
case eitherResult of
Right ClodDatabase
db -> ClodDatabase -> ClodM ClodDatabase
forall a. a -> ReaderT ClodConfig (ExceptT ClodError IO) a
forall (m :: * -> *) a. Monad m => a -> m a
return ClodDatabase
db
Left SomeException
err -> do
config <- ReaderT ClodConfig (ExceptT ClodError IO) ClodConfig
forall r (m :: * -> *). MonadReader r m => m r
ask
when (verbose config) $
liftIO $ putStrLn $ "Warning: Failed to parse database: " ++ show err
whenVerbose $ liftIO $ putStrLn "Creating a new empty database"
db <- initializeDatabase
saveDatabase dbPath db
return db
where
whenVerbose :: m () -> m ()
whenVerbose m ()
action = do
config <- m ClodConfig
forall r (m :: * -> *). MonadReader r m => m r
ask
when (verbose config) action
saveDatabase :: FilePath -> ClodDatabase -> ClodM ()
saveDatabase :: FilePath
-> ClodDatabase -> ReaderT ClodConfig (ExceptT ClodError IO) ()
saveDatabase FilePath
dbPath ClodDatabase
db = do
IO () -> ReaderT ClodConfig (ExceptT ClodError IO) ()
forall a. IO a -> ReaderT ClodConfig (ExceptT ClodError IO) a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO () -> ReaderT ClodConfig (ExceptT ClodError IO) ())
-> IO () -> ReaderT ClodConfig (ExceptT ClodError IO) ()
forall a b. (a -> b) -> a -> b
$ Bool -> FilePath -> IO ()
createDirectoryIfMissing Bool
True (ShowS
takeDirectory FilePath
dbPath)
let serializedDb :: SerializableClodDatabase
serializedDb = ClodDatabase -> SerializableClodDatabase
toSerializable ClodDatabase
db
let tempPath :: FilePath
tempPath = FilePath
dbPath FilePath -> ShowS
forall a. [a] -> [a] -> [a]
++ FilePath
".new"
eitherResult <- IO (Either IOException ())
-> ReaderT
ClodConfig (ExceptT ClodError IO) (Either IOException ())
forall a. IO a -> ReaderT ClodConfig (ExceptT ClodError IO) a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO (Either IOException ())
-> ReaderT
ClodConfig (ExceptT ClodError IO) (Either IOException ()))
-> IO (Either IOException ())
-> ReaderT
ClodConfig (ExceptT ClodError IO) (Either IOException ())
forall a b. (a -> b) -> a -> b
$ forall e a. Exception e => IO a -> IO (Either e a)
try @IOException (IO () -> IO (Either IOException ()))
-> IO () -> IO (Either IOException ())
forall a b. (a -> b) -> a -> b
$ do
let dhallExpr :: Expr Src Void
dhallExpr = Encoder SerializableClodDatabase
-> SerializableClodDatabase -> Expr Src Void
forall a. Encoder a -> a -> Expr Src Void
Dhall.embed Encoder SerializableClodDatabase
forall a. ToDhall a => Encoder a
Dhall.inject SerializableClodDatabase
serializedDb
let dhallText :: Text
dhallText = Expr Src Void -> Text
forall a. Pretty a => a -> Text
Dhall.Core.pretty Expr Src Void
dhallExpr
FilePath -> Text -> IO ()
TextIO.writeFile FilePath
tempPath Text
dhallText
FilePath -> FilePath -> IO ()
renameFile FilePath
tempPath FilePath
dbPath
case eitherResult of
Left IOException
err -> ClodError -> ReaderT ClodConfig (ExceptT ClodError IO) ()
forall a. ClodError -> ReaderT ClodConfig (ExceptT ClodError IO) a
forall e (m :: * -> *) a. MonadError e m => e -> m a
throwError (ClodError -> ReaderT ClodConfig (ExceptT ClodError IO) ())
-> ClodError -> ReaderT ClodConfig (ExceptT ClodError IO) ()
forall a b. (a -> b) -> a -> b
$ FilePath -> ClodError
DatabaseError (FilePath -> ClodError) -> FilePath -> ClodError
forall a b. (a -> b) -> a -> b
$ FilePath
"Failed to save database: " FilePath -> ShowS
forall a. [a] -> [a] -> [a]
++ IOException -> FilePath
forall a. Show a => a -> FilePath
show IOException
err
Right ()
_ -> ReaderT ClodConfig (ExceptT ClodError IO) ()
-> ReaderT ClodConfig (ExceptT ClodError IO) ()
forall {m :: * -> *}. MonadReader ClodConfig m => m () -> m ()
whenVerbose (ReaderT ClodConfig (ExceptT ClodError IO) ()
-> ReaderT ClodConfig (ExceptT ClodError IO) ())
-> ReaderT ClodConfig (ExceptT ClodError IO) ()
-> ReaderT ClodConfig (ExceptT ClodError IO) ()
forall a b. (a -> b) -> a -> b
$ IO () -> ReaderT ClodConfig (ExceptT ClodError IO) ()
forall a. IO a -> ReaderT ClodConfig (ExceptT ClodError IO) a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO () -> ReaderT ClodConfig (ExceptT ClodError IO) ())
-> IO () -> ReaderT ClodConfig (ExceptT ClodError IO) ()
forall a b. (a -> b) -> a -> b
$ FilePath -> IO ()
putStrLn (FilePath -> IO ()) -> FilePath -> IO ()
forall a b. (a -> b) -> a -> b
$ FilePath
"Successfully saved database to: " FilePath -> ShowS
forall a. [a] -> [a] -> [a]
++ FilePath
dbPath
where
whenVerbose :: m () -> m ()
whenVerbose m ()
action = do
config <- m ClodConfig
forall r (m :: * -> *). MonadReader r m => m r
ask
when (verbose config) action
updateDatabase :: ClodDatabase -> FilePath -> Checksum -> UTCTime -> OptimizedName -> ClodDatabase
updateDatabase :: ClodDatabase
-> FilePath -> Checksum -> UTCTime -> OptimizedName -> ClodDatabase
updateDatabase ClodDatabase
db FilePath
path Checksum
checksum UTCTime
modTime OptimizedName
optName =
let
newEntry :: FileEntry
newEntry = FileEntry
{ entryPath :: FilePath
entryPath = FilePath
path
, entryChecksum :: Checksum
entryChecksum = Checksum
checksum
, entryLastModified :: UTCTime
entryLastModified = UTCTime
modTime
, entryOptimizedName :: OptimizedName
entryOptimizedName = OptimizedName
optName
}
newFiles :: Map FilePath FileEntry
newFiles = FilePath
-> FileEntry -> Map FilePath FileEntry -> Map FilePath FileEntry
forall k a. Ord k => k -> a -> Map k a -> Map k a
Map.insert FilePath
path FileEntry
newEntry (ClodDatabase -> Map FilePath FileEntry
dbFiles ClodDatabase
db)
newChecksums :: Map FilePath FilePath
newChecksums = FilePath
-> FilePath -> Map FilePath FilePath -> Map FilePath FilePath
forall k a. Ord k => k -> a -> Map k a -> Map k a
Map.insert (Checksum -> FilePath
unChecksum Checksum
checksum) FilePath
path (ClodDatabase -> Map FilePath FilePath
dbChecksums ClodDatabase
db)
in
ClodDatabase
db { dbFiles = newFiles, dbChecksums = newChecksums }
getFileStatus :: ClodDatabase -> FilePath -> Checksum -> ClodM FileStatus
getFileStatus :: ClodDatabase -> FilePath -> Checksum -> ClodM FileStatus
getFileStatus ClodDatabase
db FilePath
path Checksum
checksum = do
let
files :: Map FilePath FileEntry
files = ClodDatabase -> Map FilePath FileEntry
dbFiles ClodDatabase
db
checksums :: Map FilePath FilePath
checksums = ClodDatabase -> Map FilePath FilePath
dbChecksums ClodDatabase
db
checksumStr :: FilePath
checksumStr = Checksum -> FilePath
unChecksum Checksum
checksum
case FilePath -> Map FilePath FileEntry -> Maybe FileEntry
forall k a. Ord k => k -> Map k a -> Maybe a
Map.lookup FilePath
path Map FilePath FileEntry
files of
Maybe FileEntry
Nothing ->
case FilePath -> Map FilePath FilePath -> Maybe FilePath
forall k a. Ord k => k -> Map k a -> Maybe a
Map.lookup FilePath
checksumStr Map FilePath FilePath
checksums of
Just FilePath
oldPath ->
if FilePath
oldPath FilePath -> FilePath -> Bool
forall a. Eq a => a -> a -> Bool
/= FilePath
path
then FileStatus -> ClodM FileStatus
forall a. a -> ReaderT ClodConfig (ExceptT ClodError IO) a
forall (m :: * -> *) a. Monad m => a -> m a
return (FileStatus -> ClodM FileStatus) -> FileStatus -> ClodM FileStatus
forall a b. (a -> b) -> a -> b
$ FilePath -> FileStatus
Renamed FilePath
oldPath
else FileStatus -> ClodM FileStatus
forall a. a -> ReaderT ClodConfig (ExceptT ClodError IO) a
forall (m :: * -> *) a. Monad m => a -> m a
return FileStatus
New
Maybe FilePath
Nothing -> FileStatus -> ClodM FileStatus
forall a. a -> ReaderT ClodConfig (ExceptT ClodError IO) a
forall (m :: * -> *) a. Monad m => a -> m a
return FileStatus
New
Just FileEntry
entry ->
if FileEntry -> Checksum
entryChecksum FileEntry
entry Checksum -> Checksum -> Bool
forall a. Eq a => a -> a -> Bool
== Checksum
checksum
then FileStatus -> ClodM FileStatus
forall a. a -> ReaderT ClodConfig (ExceptT ClodError IO) a
forall (m :: * -> *) a. Monad m => a -> m a
return FileStatus
Unchanged
else FileStatus -> ClodM FileStatus
forall a. a -> ReaderT ClodConfig (ExceptT ClodError IO) a
forall (m :: * -> *) a. Monad m => a -> m a
return FileStatus
Modified
findChangedFiles :: ClodDatabase -> [(FilePath, Checksum, UTCTime)] -> ClodM [(FilePath, FileStatus)]
findChangedFiles :: ClodDatabase
-> [(FilePath, Checksum, UTCTime)]
-> ClodM [(FilePath, FileStatus)]
findChangedFiles ClodDatabase
db [(FilePath, Checksum, UTCTime)]
fileInfos = do
ReaderT ClodConfig (ExceptT ClodError IO) ()
-> ReaderT ClodConfig (ExceptT ClodError IO) ()
forall {m :: * -> *}. MonadReader ClodConfig m => m () -> m ()
whenVerbose (ReaderT ClodConfig (ExceptT ClodError IO) ()
-> ReaderT ClodConfig (ExceptT ClodError IO) ())
-> ReaderT ClodConfig (ExceptT ClodError IO) ()
-> ReaderT ClodConfig (ExceptT ClodError IO) ()
forall a b. (a -> b) -> a -> b
$ IO () -> ReaderT ClodConfig (ExceptT ClodError IO) ()
forall a. IO a -> ReaderT ClodConfig (ExceptT ClodError IO) a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO () -> ReaderT ClodConfig (ExceptT ClodError IO) ())
-> IO () -> ReaderT ClodConfig (ExceptT ClodError IO) ()
forall a b. (a -> b) -> a -> b
$ FilePath -> IO ()
putStrLn (FilePath -> IO ()) -> FilePath -> IO ()
forall a b. (a -> b) -> a -> b
$ FilePath
"Processing " FilePath -> ShowS
forall a. [a] -> [a] -> [a]
++ Int -> FilePath
forall a. Show a => a -> FilePath
show ([(FilePath, Checksum, UTCTime)] -> Int
forall a. [a] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length [(FilePath, Checksum, UTCTime)]
fileInfos) FilePath -> ShowS
forall a. [a] -> [a] -> [a]
++ FilePath
" files for change detection"
[(FilePath, Checksum, UTCTime)]
-> ((FilePath, Checksum, UTCTime)
-> ReaderT
ClodConfig (ExceptT ClodError IO) (FilePath, FileStatus))
-> ClodM [(FilePath, FileStatus)]
forall (t :: * -> *) (m :: * -> *) a b.
(Traversable t, Monad m) =>
t a -> (a -> m b) -> m (t b)
forM [(FilePath, Checksum, UTCTime)]
fileInfos (((FilePath, Checksum, UTCTime)
-> ReaderT
ClodConfig (ExceptT ClodError IO) (FilePath, FileStatus))
-> ClodM [(FilePath, FileStatus)])
-> ((FilePath, Checksum, UTCTime)
-> ReaderT
ClodConfig (ExceptT ClodError IO) (FilePath, FileStatus))
-> ClodM [(FilePath, FileStatus)]
forall a b. (a -> b) -> a -> b
$ \(FilePath
path, Checksum
checksum, UTCTime
_) -> do
status <- ClodDatabase -> FilePath -> Checksum -> ClodM FileStatus
getFileStatus ClodDatabase
db FilePath
path Checksum
checksum
whenVerbose $ liftIO $ putStrLn $ "File status for " ++ path ++ ": " ++ show status
return (path, status)
where
whenVerbose :: m () -> m ()
whenVerbose m ()
action = do
config <- m ClodConfig
forall r (m :: * -> *). MonadReader r m => m r
ask
when (verbose config) action
findRenamedFiles :: ClodDatabase -> [(FilePath, FileStatus)] -> [(FilePath, FilePath)]
findRenamedFiles :: ClodDatabase -> [(FilePath, FileStatus)] -> [(FilePath, FilePath)]
findRenamedFiles ClodDatabase
_ [(FilePath, FileStatus)]
fileStatuses =
((FilePath, FileStatus) -> Maybe (FilePath, FilePath))
-> [(FilePath, FileStatus)] -> [(FilePath, FilePath)]
forall a b. (a -> Maybe b) -> [a] -> [b]
mapMaybe (FilePath, FileStatus) -> Maybe (FilePath, FilePath)
forall {a}. (a, FileStatus) -> Maybe (a, FilePath)
extractRename [(FilePath, FileStatus)]
fileStatuses
where
extractRename :: (a, FileStatus) -> Maybe (a, FilePath)
extractRename (a
newPath, Renamed FilePath
oldPath) = (a, FilePath) -> Maybe (a, FilePath)
forall a. a -> Maybe a
Just (a
newPath, FilePath
oldPath)
extractRename (a, FileStatus)
_ = Maybe (a, FilePath)
forall a. Maybe a
Nothing
detectFileChanges :: FileReadCap -> ClodDatabase -> [FilePath] -> FilePath -> ClodM ([(FilePath, FileStatus)], [(FilePath, FilePath)])
detectFileChanges :: FileReadCap
-> ClodDatabase
-> [FilePath]
-> FilePath
-> ClodM ([(FilePath, FileStatus)], [(FilePath, FilePath)])
detectFileChanges FileReadCap
readCap ClodDatabase
db [FilePath]
filePaths FilePath
projectRoot = do
ReaderT ClodConfig (ExceptT ClodError IO) ()
-> ReaderT ClodConfig (ExceptT ClodError IO) ()
forall {m :: * -> *}. MonadReader ClodConfig m => m () -> m ()
whenVerbose (ReaderT ClodConfig (ExceptT ClodError IO) ()
-> ReaderT ClodConfig (ExceptT ClodError IO) ())
-> ReaderT ClodConfig (ExceptT ClodError IO) ()
-> ReaderT ClodConfig (ExceptT ClodError IO) ()
forall a b. (a -> b) -> a -> b
$ IO () -> ReaderT ClodConfig (ExceptT ClodError IO) ()
forall a. IO a -> ReaderT ClodConfig (ExceptT ClodError IO) a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO () -> ReaderT ClodConfig (ExceptT ClodError IO) ())
-> IO () -> ReaderT ClodConfig (ExceptT ClodError IO) ()
forall a b. (a -> b) -> a -> b
$ FilePath -> IO ()
putStrLn (FilePath -> IO ()) -> FilePath -> IO ()
forall a b. (a -> b) -> a -> b
$ FilePath
"Detecting changes for " FilePath -> ShowS
forall a. [a] -> [a] -> [a]
++ Int -> FilePath
forall a. Show a => a -> FilePath
show ([FilePath] -> Int
forall a. [a] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length [FilePath]
filePaths) FilePath -> ShowS
forall a. [a] -> [a] -> [a]
++ FilePath
" files"
ReaderT ClodConfig (ExceptT ClodError IO) ()
-> ReaderT ClodConfig (ExceptT ClodError IO) ()
forall {m :: * -> *}. MonadReader ClodConfig m => m () -> m ()
whenVerbose (ReaderT ClodConfig (ExceptT ClodError IO) ()
-> ReaderT ClodConfig (ExceptT ClodError IO) ())
-> ReaderT ClodConfig (ExceptT ClodError IO) ()
-> ReaderT ClodConfig (ExceptT ClodError IO) ()
forall a b. (a -> b) -> a -> b
$ IO () -> ReaderT ClodConfig (ExceptT ClodError IO) ()
forall a. IO a -> ReaderT ClodConfig (ExceptT ClodError IO) a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO () -> ReaderT ClodConfig (ExceptT ClodError IO) ())
-> IO () -> ReaderT ClodConfig (ExceptT ClodError IO) ()
forall a b. (a -> b) -> a -> b
$ FilePath -> IO ()
putStrLn (FilePath -> IO ()) -> FilePath -> IO ()
forall a b. (a -> b) -> a -> b
$ FilePath
"Database has " FilePath -> ShowS
forall a. [a] -> [a] -> [a]
++ Int -> FilePath
forall a. Show a => a -> FilePath
show (Map FilePath FileEntry -> Int
forall k a. Map k a -> Int
Map.size (ClodDatabase -> Map FilePath FileEntry
dbFiles ClodDatabase
db)) FilePath -> ShowS
forall a. [a] -> [a] -> [a]
++ FilePath
" entries"
fileInfos <- [Maybe (FilePath, Checksum, UTCTime)]
-> [(FilePath, Checksum, UTCTime)]
forall a. [Maybe a] -> [a]
catMaybes ([Maybe (FilePath, Checksum, UTCTime)]
-> [(FilePath, Checksum, UTCTime)])
-> ReaderT
ClodConfig
(ExceptT ClodError IO)
[Maybe (FilePath, Checksum, UTCTime)]
-> ReaderT
ClodConfig (ExceptT ClodError IO) [(FilePath, Checksum, UTCTime)]
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> [FilePath]
-> (FilePath
-> ReaderT
ClodConfig
(ExceptT ClodError IO)
(Maybe (FilePath, Checksum, UTCTime)))
-> ReaderT
ClodConfig
(ExceptT ClodError IO)
[Maybe (FilePath, Checksum, UTCTime)]
forall (t :: * -> *) (m :: * -> *) a b.
(Traversable t, Monad m) =>
t a -> (a -> m b) -> m (t b)
forM [FilePath]
filePaths (\FilePath
path -> do
let fullPath :: FilePath
fullPath = FilePath
projectRoot FilePath -> ShowS
</> FilePath
path
fileExists <- FileReadCap -> FilePath -> ClodM Bool
safeFileExists FileReadCap
readCap FilePath
fullPath
if not fileExists
then do
whenVerbose $ liftIO $ putStrLn $ "File does not exist: " ++ fullPath
return Nothing
else do
isText <- safeIsTextFile readCap fullPath
if not isText
then do
whenVerbose $ liftIO $ putStrLn $ "Not a text file: " ++ fullPath
return Nothing
else do
checksum <- checksumFile readCap fullPath
modTime <- liftIO $ getModificationTime fullPath
whenVerbose $ liftIO $ putStrLn $ "Processed file: " ++ path ++ " with checksum: " ++ unChecksum checksum
return $ Just (path, checksum, modTime)
)
whenVerbose $ liftIO $ putStrLn $ "Got " ++ show (length fileInfos) ++ " files to check"
changedFiles <- findChangedFiles db fileInfos
let renamedFiles = ClodDatabase -> [(FilePath, FileStatus)] -> [(FilePath, FilePath)]
findRenamedFiles ClodDatabase
db [(FilePath, FileStatus)]
changedFiles
whenVerbose $ liftIO $ putStrLn $ "Found " ++ show (length renamedFiles) ++ " renamed files"
return (changedFiles, renamedFiles)
where
whenVerbose :: m () -> m ()
whenVerbose m ()
action = do
config <- m ClodConfig
forall r (m :: * -> *). MonadReader r m => m r
ask
when (verbose config) action
cleanupStagingDirectories :: ClodM ()
cleanupStagingDirectories :: ReaderT ClodConfig (ExceptT ClodError IO) ()
cleanupStagingDirectories = do
config <- ReaderT ClodConfig (ExceptT ClodError IO) ClodConfig
forall r (m :: * -> *). MonadReader r m => m r
ask
case previousStaging config of
Maybe FilePath
Nothing -> () -> ReaderT ClodConfig (ExceptT ClodError IO) ()
forall a. a -> ReaderT ClodConfig (ExceptT ClodError IO) a
forall (m :: * -> *) a. Monad m => a -> m a
return ()
Just FilePath
oldDir -> do
dirExists <- IO Bool -> ClodM Bool
forall a. IO a -> ReaderT ClodConfig (ExceptT ClodError IO) a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO Bool -> ClodM Bool) -> IO Bool -> ClodM Bool
forall a b. (a -> b) -> a -> b
$ FilePath -> IO Bool
doesDirectoryExist FilePath
oldDir
when dirExists $ do
whenVerbose $ liftIO $ putStrLn $ "Cleaning up previous staging directory: " ++ oldDir
result <- liftIO $ try $ removeDirectoryRecursive oldDir :: ClodM (Either IOException ())
case result of
Left IOException
err -> ReaderT ClodConfig (ExceptT ClodError IO) ()
-> ReaderT ClodConfig (ExceptT ClodError IO) ()
forall {m :: * -> *}. MonadReader ClodConfig m => m () -> m ()
whenVerbose (ReaderT ClodConfig (ExceptT ClodError IO) ()
-> ReaderT ClodConfig (ExceptT ClodError IO) ())
-> ReaderT ClodConfig (ExceptT ClodError IO) ()
-> ReaderT ClodConfig (ExceptT ClodError IO) ()
forall a b. (a -> b) -> a -> b
$ IO () -> ReaderT ClodConfig (ExceptT ClodError IO) ()
forall a. IO a -> ReaderT ClodConfig (ExceptT ClodError IO) a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO () -> ReaderT ClodConfig (ExceptT ClodError IO) ())
-> IO () -> ReaderT ClodConfig (ExceptT ClodError IO) ()
forall a b. (a -> b) -> a -> b
$ FilePath -> IO ()
putStrLn (FilePath -> IO ()) -> FilePath -> IO ()
forall a b. (a -> b) -> a -> b
$ FilePath
"Warning: Failed to remove old staging directory: " FilePath -> ShowS
forall a. [a] -> [a] -> [a]
++ IOException -> FilePath
forall a. Show a => a -> FilePath
show IOException
err
Right ()
_ -> () -> ReaderT ClodConfig (ExceptT ClodError IO) ()
forall a. a -> ReaderT ClodConfig (ExceptT ClodError IO) a
forall (m :: * -> *) a. Monad m => a -> m a
return ()
where
whenVerbose :: m () -> m ()
whenVerbose m ()
action = do
config <- m ClodConfig
forall r (m :: * -> *). MonadReader r m => m r
ask
when (verbose config) action
flushMissingEntries :: FileReadCap -> ClodDatabase -> FilePath -> ClodM ClodDatabase
flushMissingEntries :: FileReadCap -> ClodDatabase -> FilePath -> ClodM ClodDatabase
flushMissingEntries FileReadCap
readCap ClodDatabase
db FilePath
projectRoot = do
config <- ReaderT ClodConfig (ExceptT ClodError IO) ClodConfig
forall r (m :: * -> *). MonadReader r m => m r
ask
if not (flushMode config)
then return db
else do
whenVerbose $ liftIO $ putStrLn "Checking for missing files to flush from database..."
let files = Map FilePath FileEntry -> [(FilePath, FileEntry)]
forall k a. Map k a -> [(k, a)]
Map.toList (ClodDatabase -> Map FilePath FileEntry
dbFiles ClodDatabase
db)
existingEntries <- forM files $ \(FilePath
path, FileEntry
entry) -> do
let fullPath :: FilePath
fullPath = FilePath
projectRoot FilePath -> ShowS
</> FilePath
path
fileExists <- FileReadCap -> FilePath -> ClodM Bool
safeFileExists FileReadCap
readCap FilePath
fullPath
if fileExists
then return (path, Just entry)
else do
whenVerbose $ liftIO $ putStrLn $ "File no longer exists: " ++ path
return (path, Nothing)
let newFiles = [(FilePath, FileEntry)] -> Map FilePath FileEntry
forall k a. Ord k => [(k, a)] -> Map k a
Map.fromList [(FilePath
path, FileEntry
entry) | (FilePath
path, Just FileEntry
entry) <- [(FilePath, Maybe FileEntry)]
existingEntries]
missingCount = Map FilePath FileEntry -> Int
forall a. Map FilePath a -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length (ClodDatabase -> Map FilePath FileEntry
dbFiles ClodDatabase
db) Int -> Int -> Int
forall a. Num a => a -> a -> a
- Map FilePath FileEntry -> Int
forall k a. Map k a -> Int
Map.size Map FilePath FileEntry
newFiles
let newChecksums = [(FilePath, FilePath)] -> Map FilePath FilePath
forall k a. Ord k => [(k, a)] -> Map k a
Map.fromList
([(FilePath, FilePath)] -> Map FilePath FilePath)
-> [(FilePath, FilePath)] -> Map FilePath FilePath
forall a b. (a -> b) -> a -> b
$ (FileEntry -> (FilePath, FilePath))
-> [FileEntry] -> [(FilePath, FilePath)]
forall a b. (a -> b) -> [a] -> [b]
map (\FileEntry
entry -> (Checksum -> FilePath
unChecksum (FileEntry -> Checksum
entryChecksum FileEntry
entry), FileEntry -> FilePath
entryPath FileEntry
entry))
([FileEntry] -> [(FilePath, FilePath)])
-> [FileEntry] -> [(FilePath, FilePath)]
forall a b. (a -> b) -> a -> b
$ Map FilePath FileEntry -> [FileEntry]
forall k a. Map k a -> [a]
Map.elems Map FilePath FileEntry
newFiles
whenVerbose $ liftIO $ putStrLn $ "Removed " ++ show missingCount ++ " missing files from database"
return db { dbFiles = newFiles, dbChecksums = newChecksums }
where
whenVerbose :: m () -> m ()
whenVerbose m ()
action = do
config <- m ClodConfig
forall r (m :: * -> *). MonadReader r m => m r
ask
when (verbose config) action