module Check.Internal where import Check.Types import Compiler.Types import Constants import CoreTypes import qualified Data.ByteString as SB import qualified Data.ByteString.Char8 as SBC import qualified Data.ByteString.Lazy as LB import qualified Data.Digest.Pure.MD5 as H (md5) import Data.Maybe (catMaybes) import System.Directory (getDirectoryContents) import System.Exit (ExitCode (..)) import System.FilePath (()) import System.Posix.Files (fileExist, getSymbolicLinkStatus, isBlockDevice, isCharacterDevice, isDirectory, isNamedPipe, isRegularFile, isSocket, isSymbolicLink, readSymbolicLink) import System.Process (readProcess, system) checkDeployment :: DiagnosedDeployment -> DeploymentCheckResult checkDeployment (Diagnosed [] (D dst _ _) _) = ImpossibleDeployment [unwords ["No source for deployment with destination", dst]] checkDeployment (Diagnosed srcs dst kind) = bestResult $ map (\src -> checkSingle src dst kind) srcs bestResult :: [CheckResult] -> DeploymentCheckResult bestResult cs | all impossible cs = ImpossibleDeployment $ map (\(Impossible s) -> s) cs | otherwise -- Will not be empty as per line above = case head $ dropWhile impossible cs of AlreadyDone -> DeploymentDone Ready i -> ReadyToDeploy i Dirty s i c -> DirtySituation s i c Impossible _ -> error "Cannot be the case" impossible :: CheckResult -> Bool impossible (Impossible _) = True impossible _ = False impossibleDeployment :: DeploymentCheckResult -> Bool impossibleDeployment (ImpossibleDeployment _) = True impossibleDeployment _ = False dirtyDeployment :: DeploymentCheckResult -> Bool dirtyDeployment (DirtySituation _ _ _) = True dirtyDeployment _ = False deploymentReadyToDeploy :: DeploymentCheckResult -> Bool deploymentReadyToDeploy (ReadyToDeploy _) = True deploymentReadyToDeploy _ = False deploymentIsDone :: DeploymentCheckResult -> Bool deploymentIsDone DeploymentDone = True deploymentIsDone _ = False -- | Check a single (@source@, @destination@, @kind@) triple. checkSingle :: DiagnosedFp -> DiagnosedFp -> DeploymentKind -> CheckResult checkSingle (D src srcd srch) (D dst dstd dsth) kind = case (srcd, dstd, kind) of (IsFile , Nonexistent , _ ) -> ready (IsFile , IsFile , LinkDeployment) -> e ["Both the source:", src, "and the destination:", dst, "are files for a link deployment."] (IsFile , IsFile , CopyDeployment) -> if srch == dsth then AlreadyDone else e ["Both the source:", src, "and the destination:", dst, "are files for a copy deployment, but they are not equal."] (IsFile , IsDirectory , _) -> e ["The source: ", src, "is a file but the destination:", dst, "is a directory."] (IsFile , IsLinkTo l , LinkDeployment) -> if l == src then AlreadyDone else e ["The source:", src, "is a file and the destination:", dst, "is a link for a link deployment but the destination does not point to the source. Instead it points to:", l ++ "."] (IsFile , IsLinkTo _ , CopyDeployment) -> e ["The source:", src, "is a file and the destination:", dst, "is a link for a copy deployment."] (IsDirectory, Nonexistent , _ ) -> ready (IsDirectory, IsFile , _ ) -> e ["The source:", src, "is a directory and the destination:", dst, "is a file."] (IsDirectory, IsDirectory , CopyDeployment) -> if srch == dsth then AlreadyDone else e ["The source:", src, "and destination:", dst, "are directories for a copy deployment, but they are not equal."] (IsDirectory, IsDirectory , LinkDeployment) -> e ["The source:", src, "and the destination:", dst, "are directories for a link deployment."] (IsDirectory, IsLinkTo l , LinkDeployment) -> if l == src then AlreadyDone else e ["The source:", src, "is a directory and the destination:", dst, "is a link for a link deployment but the destination does not point to the source. Instead it points to:", l ++ "."] (IsDirectory, IsLinkTo _ , CopyDeployment) -> e ["The source:", src, "is a directory and the destination:", dst, "is a link for a copy deployment."] (Nonexistent, _ , _ ) -> i ["The source:", src, "does not exist."] (IsLinkTo _ , _ , _ ) -> i ["The source:", src, "is a link."] (IsWeird , IsWeird , _ ) -> i ["Both the source:", src, "and the destination:", dst, "are weird."] (IsWeird , _ , _ ) -> i ["The source:", src, "is weird."] (_ , IsWeird , _ ) -> i ["The destination:", dst, "is weird."] where ins = Instruction src dst kind ready = Ready ins i = Impossible . unlines e s = Dirty (unlines s) ins cins cins = case dstd of IsFile -> CleanFile dst IsLinkTo _ -> CleanLink dst IsDirectory -> CleanDirectory dst _ -> error "should not occur" diagnoseDeployment :: Deployment -> IO DiagnosedDeployment diagnoseDeployment (Put srcs dst kind) = do dsrcs <- mapM diagnose srcs ddst <- diagnose dst return $ Diagnosed dsrcs ddst kind diagnose :: FilePath -> IO DiagnosedFp diagnose fp = do d <- diagnoseFp fp hash <- hashFilePath fp return $ D fp d hash diagnoseFp :: FilePath -> IO Diagnostics diagnoseFp fp = do e <- fileExist fp if e then do s <- getSymbolicLinkStatus fp if isBlockDevice s || isCharacterDevice s || isSocket s || isNamedPipe s then return IsWeird else do if isSymbolicLink s then do point <- readSymbolicLink fp return $ IsLinkTo point else if isDirectory s then return IsDirectory else if isRegularFile s then return IsFile else error $ "File " ++ fp ++ " was neither a block device, a character device, a socket, a named pipe, a symbolic link, a directory or a regular file" else do -- If a link exists, but it points to something that doesn't exist, it is considered as non-existent by `fileExist` es <- system $ unwords ["test", "-L", fp] case es of ExitSuccess -> do -- Need to do a manual call because readSymbolicLink fails for nonexistent destinations point <- readProcess "readlink" [fp] "" return $ IsLinkTo $ init point -- remove newline ExitFailure _ -> return Nonexistent md5 :: SB.ByteString -> HashDigest md5 bs = H.md5 $ LB.fromStrict bs -- | Hash a filepath so that two filepaths with the same contents have the same hash hashFilePath :: FilePath -> IO HashDigest hashFilePath fp = do d <- diagnoseFp fp case d of IsFile -> hashFile fp IsDirectory -> hashDirectory fp IsLinkTo _ -> return $ md5 SB.empty IsWeird -> return $ md5 SB.empty Nonexistent -> return $ md5 SB.empty hashFile :: FilePath -> IO HashDigest hashFile fp = md5 <$> SB.readFile fp hashDirectory :: FilePath -> IO HashDigest hashDirectory fp = do rawContents <- getDirectoryContents fp let contents = map (fp ) . filter (\f -> not $ f == "." || f == "..") $ rawContents hashes <- mapM hashFilePath contents let hashbs = map (SBC.pack . show) hashes return $ md5 $ SB.concat hashbs formatDeploymentChecks :: [(Deployment, DeploymentCheckResult)] -> String formatDeploymentChecks dss = if null output then "Deployment is done already." else unlines output ++ if all (impossibleDeployment . snd) dss then "Deployment is impossible." else "Deployment is possible." where output = catMaybes $ map formatDeploymentCheck dss formatDeploymentCheck :: (Deployment, DeploymentCheckResult) -> Maybe String formatDeploymentCheck (_, (ReadyToDeploy is)) = Just $ "READY: " ++ formatInstruction is formatDeploymentCheck (_, DeploymentDone) = Nothing formatDeploymentCheck (d, ImpossibleDeployment ds) = Just $ "IMPOSSIBLE: " ++ deployment_dst d ++ " cannot be deployed:\n" ++ unlines ds ++ "\n" formatDeploymentCheck (d, (DirtySituation str is c)) = Just $ "DIRTY: " ++ deployment_dst d ++ "\n" ++ str ++ "planned: " ++ formatInstruction is ++ "\n" ++ "cleanup needed:\n" ++ formatCleanupInstruction c ++ "\n" formatInstruction :: Instruction -> String formatInstruction (Instruction src dst k) = unwords $ [ src , kindSymbol k , dst ] where kindSymbol LinkDeployment = linkKindSymbol kindSymbol CopyDeployment = copyKindSymbol formatCleanupInstruction :: CleanupInstruction -> String formatCleanupInstruction (CleanFile fp) = "remove file " ++ fp formatCleanupInstruction (CleanDirectory dir) = "remove directory " ++ dir formatCleanupInstruction (CleanLink link) = "remove link " ++ link