module Darcs.Util.File ( -- * Files and directories getFileStatus , doesDirectoryReallyExist , removeFileMayNotExist , getRecursiveContents , getRecursiveContentsFullPath , copyTree -- * Fetching files , fetchFilePS , fetchFileLazyPS , gzFetchFilePS , speculateFileOrUrl , copyFileOrUrl , Cachable(..) -- * Backup , backupByRenaming , backupByCopying -- * Temporary files , withTemp , withOpenTemp ) where import Darcs.Prelude import Darcs.Util.ByteString ( gzReadFilePS ) import Darcs.Util.Exception ( catchall, ifDoesNotExistError ) import Darcs.Util.Global ( defaultRemoteDarcsCmd ) import Darcs.Util.HTTP ( Cachable(..) ) import qualified Darcs.Util.HTTP as HTTP import Darcs.Util.Path ( FilePathLike, toFilePath ) import Darcs.Util.Ssh ( copySSH ) import Darcs.Util.URL ( isHttpUrl, isSshUrl, isValidLocalPath, splitSshUrl ) import Control.Exception ( IOException, bracket, catch ) import Control.Monad ( forM, unless, when, zipWithM_ ) import qualified Data.ByteString as B ( ByteString, readFile ) import qualified Data.ByteString.Lazy as BL import Network.URI ( parseURI, uriScheme ) import System.Directory ( copyFile , createDirectory , doesDirectoryExist , doesFileExist , listDirectory , removeFile , renameDirectory , renameFile ) import System.FilePath.Posix ( normalise, () ) import System.IO ( Handle, hClose, openBinaryTempFile ) import System.IO.Error ( catchIOError, isDoesNotExistError ) import System.Posix.Files ( FileStatus , createLink , getSymbolicLinkStatus , isDirectory , isRegularFile ) -- | Badly named, since it is actually 'getSymbolicLinkStatus', with all -- 'IOError's turned into 'Nothing'. getFileStatus :: FilePath -> IO (Maybe FileStatus) getFileStatus f = Just `fmap` getSymbolicLinkStatus f `catchIOError` (\_-> return Nothing) -- | Whether a path is an existing directory, but not a symlink to one. doesDirectoryReallyExist :: FilePath -> IO Bool doesDirectoryReallyExist f = ifDoesNotExistError False (isDirectory `fmap` getSymbolicLinkStatus f) -- | Variant of 'removeFile' that doesn't throw exception when file does not exist. removeFileMayNotExist :: FilePathLike p => p -> IO () removeFileMayNotExist f = ifDoesNotExistError () (removeFile $ toFilePath f) -- | Return all files under given directory that aren't directories. getRecursiveContents :: FilePath -> IO [FilePath] getRecursiveContents topdir = do entries <- listDirectory topdir paths <- forM entries $ \name -> do let path = topdir name isDir <- doesDirectoryExist path if isDir then getRecursiveContents path else return [name] return (concat paths) -- | Return all files under given directory that aren't directories. -- Unlike 'getRecursiveContents' this function returns the full path. getRecursiveContentsFullPath :: FilePath -> IO [FilePath] getRecursiveContentsFullPath topdir = do entries <- listDirectory topdir paths <- forM entries $ \name -> do let path = topdir name isDir <- doesDirectoryExist path if isDir then getRecursiveContentsFullPath path else return [path] return (concat paths) -- | Very much darcs-specific copying procedure. For local files it tries -- to hard-link, falling back to normal copy if it fails. Remote URLs are -- downloaded using either HTTP or SSH. For SSH, this tries to use the -- given remote darcs command to invoke it's transfer-mode command. copyFileOrUrl :: String -- ^ remote darcs executable -> String -- ^ path representing the origin file or URL -> FilePath -- ^ destination path -> Cachable -- ^ tell whether file to copy is cachable -> IO () copyFileOrUrl _ fou out _ | isValidLocalPath fou = copyLocal fou out copyFileOrUrl _ fou out cache | isHttpUrl fou = HTTP.copyRemote fou out cache copyFileOrUrl rd fou out _ | isSshUrl fou = copySSH rd (splitSshUrl fou) out copyFileOrUrl _ fou _ _ = fail $ "unknown transport protocol: " ++ fou -- | Hard-link file, falling back to normal copying it that fails. copyLocal :: String -> FilePath -> IO () copyLocal fou out = createLink fou out `catchall` copyFile fou out -- | Recursively copy a directory, where the target directory is supposed to -- already exist. copyTree :: FilePath -> FilePath -> IO () copyTree source dest = do fs <- getSymbolicLinkStatus source if isDirectory fs then do fps <- listDirectory source zipWithM_ copySubTree (map (source ) fps) (map (dest ) fps) else fail ("copyTree: Bad source " ++ source) `catch` \(_ :: IOException) -> fail ("copyTree: Bad source " ++ source) -- | Recursively copy a directory, where the target directory does not yet -- exist but it's parent does. copySubTree :: FilePath -> FilePath -> IO () copySubTree source dest = do fs <- getSymbolicLinkStatus source if isDirectory fs then do createDirectory dest fps <- listDirectory source zipWithM_ copySubTree (map (source ) fps) (map (dest ) fps) else if isRegularFile fs then copyFile source dest else fail ("copySubTree: Bad source "++ source) `catch` (\e -> unless (isDoesNotExistError e) $ ioError e) backupByRenaming :: FilePath -> IO () backupByRenaming = backupBy rename where rename x y = do isD <- doesDirectoryExist x if isD then renameDirectory x y else renameFile x y backupByCopying :: FilePath -> IO () backupByCopying = backupBy copy where copy x y = do isD <- doesDirectoryExist x if isD then do createDirectory y copyTree (normalise x) (normalise y) else copyFile x y backupBy :: (FilePath -> FilePath -> IO ()) -> FilePath -> IO () backupBy backup f = do hasBF <- doesFileExist f hasBD <- doesDirectoryExist f when (hasBF || hasBD) $ helper 0 where helper :: Int -> IO () helper i = do existsF <- doesFileExist next existsD <- doesDirectoryExist next if existsF || existsD then helper (i + 1) else do putStrLn $ "Backing up " ++ f ++ "(" ++ suffix ++ ")" backup f next where next = f ++ suffix suffix = ".~" ++ show i ++ "~" -- | Generic file fetching support function that takes care of downloading -- remote files to a temporary location if necessary before invoking the actual -- reading procedure. copyAndReadFile :: (FilePath -> IO a) -> String -> Cachable -> IO a copyAndReadFile readfn fou _ | isValidLocalPath fou = readfn fou copyAndReadFile readfn fou cache = withTemp $ \t -> do copyFileOrUrl defaultRemoteDarcsCmd fou t cache readfn t -- | @fetchFilePS fileOrUrl cache@ returns the content of its argument (either a -- file or an URL). If it has to download an url, then it will use a cache as -- required by its second argument. -- -- We always use default remote darcs, since it is not fatal if the remote -- darcs does not exist or is too old -- anything that supports transfer-mode -- should do, and if not, we will fall back to SFTP or SCP. fetchFilePS :: String -> Cachable -> IO B.ByteString fetchFilePS = copyAndReadFile B.readFile -- | @fetchFileLazyPS fileOrUrl cache@ lazily reads the content of its argument -- (either a file or an URL). Warning: this function may constitute a fd leak; -- make sure to force consumption of file contents to avoid that. See -- "fetchFilePS" for details. fetchFileLazyPS :: String -> Cachable -> IO BL.ByteString fetchFileLazyPS x c = case parseURI x of Just x' | let s = uriScheme x' , s == "http:" || s == "https:" -> HTTP.copyRemoteLazy x c _ -> copyAndReadFile BL.readFile x c -- | Like 'fetchFilePS' but transparently handle gzip compressed files. gzFetchFilePS :: String -> Cachable -> IO B.ByteString gzFetchFilePS = copyAndReadFile gzReadFilePS -- | Initiate background file download for the given file path or URL -- to the given location. speculateFileOrUrl :: String -> FilePath -> IO () speculateFileOrUrl fou out | isHttpUrl fou = HTTP.speculateRemote fou out | otherwise = return () -- | Invoke the given action on a file that is temporarily created -- in the current directory, and removed afterwards. withTemp :: (FilePath -> IO a) -> IO a withTemp = bracket get_empty_file removeFileMayNotExist where get_empty_file = do (f, h) <- openBinaryTempFile "." "darcs" hClose h `catchall` return () return f -- | Invoke the given action on a file that is temporarily created and opened -- in the current directory, and closed and removed afterwards. withOpenTemp :: ((Handle, FilePath) -> IO a) -> IO a withOpenTemp = bracket get_empty_file cleanup where cleanup (h, f) = do hClose h `catchall` return () removeFileMayNotExist f get_empty_file = swap `fmap` openBinaryTempFile "." "darcs" swap (a, b) = (b, a)