packages feed

hpath-io 0.10.1 → 0.11.0

raw patch · 9 files changed

+422/−19 lines, 9 filesdep +exceptionsdep +timedep ~hpath-filepathPVP ok

version bump matches the API change (PVP)

Dependencies added: exceptions, time

Dependency ranges changed: hpath-filepath

API changes (from Hackage documentation)

+ HPath.IO: createDirIfMissing :: FileMode -> Path b -> IO ()
+ HPath.IO: getDirsFiles' :: Path b -> IO [Path Fn]
+ HPath.IO: getModificationTime :: Path b -> IO UTCTime
+ HPath.IO: isExecutable :: Path b -> IO Bool
+ HPath.IO: isReadable :: Path b -> IO Bool
+ HPath.IO: setModificationTime :: Path b -> EpochTime -> IO ()
+ HPath.IO: setModificationTimeHiRes :: Path b -> POSIXTime -> IO ()
+ HPath.IO: withHandle :: ByteString -> OpenMode -> ((Handle, Path a) -> IO a) -> IO a
+ HPath.IO: withRawFilePath :: MonadThrow m => ByteString -> (Path a -> m b) -> m b
+ HPath.IO: writeFileL :: Path b -> Maybe FileMode -> ByteString -> IO ()
+ HPath.IO.Errors: hideError :: IOErrorType -> IO () -> IO ()
- HPath.IO: writeFile :: Path b -> ByteString -> IO ()
+ HPath.IO: writeFile :: Path b -> Maybe FileMode -> ByteString -> IO ()

Files

CHANGELOG.md view
@@ -1,5 +1,20 @@ # Revision history for hpath-io +## 0.11.0 -- 2020-01-18++* `writeFile` not allows to set file mode and create file if it does not exist (this broke API)+* added various new functions:+	* createDirIfMissing+	* writeFileL (for lazy bytestring)+	* isReadable+	* isExecutable+	* getModificationTime+	* setModificationTime+	* setModificationTimeHiRes+	* getDirsFiles' (returns filenames instead of paths)+	* withRawFilePath+	* withHandle+ ## 0.10.1 -- 2020-01-13  * Move file check functions to HPath.IO
hpath-io.cabal view
@@ -1,5 +1,5 @@ name:                hpath-io-version:             0.10.1+version:             0.11.0 synopsis:            High-level IO operations on files/directories description:         High-level IO operations on files/directories, utilizing type-safe Paths -- bug-reports:@@ -33,14 +33,17 @@   c-sources:         cbits/dirutils.c    other-modules:     Streamly.ByteString+                     Streamly.ByteString.Lazy   -- other-extensions:   build-depends:     base >= 4.8 && <5                    , IfElse                    , bytestring >= 0.10.0.0+                   , exceptions                    , hpath >= 0.10 && < 0.11-                   , hpath-filepath >= 0.10 && < 0.11+                   , hpath-filepath >= 0.10.2 && < 0.11                    , safe-exceptions >= 0.1                    , streamly >= 0.7+                   , time >= 1.8                    , unix >= 2.5                    , unix-bytestring                    , utf8-string@@ -65,6 +68,7 @@                         HPath.IO.CopyDirRecursiveSpec                         HPath.IO.CopyFileOverwriteSpec                         HPath.IO.CopyFileSpec+                        HPath.IO.CreateDirIfMissingSpec                         HPath.IO.CreateDirRecursiveSpec                         HPath.IO.CreateDirSpec                         HPath.IO.CreateRegularFileSpec@@ -81,6 +85,7 @@                         HPath.IO.RecreateSymlinkSpec                         HPath.IO.RenameFileSpec                         HPath.IO.ToAbsSpec+                        HPath.IO.WriteFileLSpec                         HPath.IO.WriteFileSpec                         Spec                         Utils@@ -93,6 +98,7 @@                       , hpath-io                       , hspec >= 1.3                       , process+                      , time >= 1.8                       , unix                       , unix-bytestring                       , utf8-string
src/HPath/IO.hs view
@@ -60,6 +60,7 @@   -- * File creation   , createRegularFile   , createDir+  , createDirIfMissing   , createDirRecursive   , createSymlink   -- * File renaming/moving@@ -70,6 +71,7 @@   , readFileStream   -- * File writing   , writeFile+  , writeFileL   , appendFile   -- * File permissions   , newFilePerms@@ -78,15 +80,24 @@   , doesExist   , doesFileExist   , doesDirectoryExist+  , isReadable   , isWritable+  , isExecutable   , canOpenDirectory+  -- * File times+  , getModificationTime+  , setModificationTime+  , setModificationTimeHiRes   -- * Directory reading   , getDirsFiles+  , getDirsFiles'   -- * Filetype operations   , getFileType   -- * Others   , canonicalizePath   , toAbs+  , withRawFilePath+  , withHandle   )   where @@ -99,7 +110,9 @@   (     IOException   , bracket+  , bracketOnError   , throwIO+  , finally   ) import Control.Monad   (@@ -107,6 +120,7 @@   , void   , when   )+import Control.Monad.Catch (MonadThrow(..)) import Control.Monad.IfElse   (     unlessM@@ -115,6 +129,8 @@   (     ByteString   )+import           Data.Traversable               ( for )+import           Data.Functor                   ( ($>) ) #if MIN_VERSION_bytestring(0,10,2) import Data.ByteString.Builder #else@@ -150,6 +166,8 @@     (<>)   , mempty   )+import Data.Time.Clock+import Data.Time.Clock.POSIX (getPOSIXTime, posixSecondsToUTCTime, POSIXTime) import Data.Word   (     Word8@@ -184,6 +202,7 @@ import Prelude hiding (appendFile, readFile, writeFile) import Streamly import Streamly.ByteString+import qualified Streamly.ByteString.Lazy as SL import qualified Streamly.Data.Fold as FL import Streamly.Memory.Array import qualified Streamly.FileSystem.Handle as FH@@ -234,6 +253,7 @@   , setFileMode   , unionFileModes   )+import qualified System.Posix.FilePath as FP import qualified System.Posix.Files.ByteString as PF import qualified "unix" System.Posix.IO.ByteString as SPI import qualified "unix-bytestring" System.Posix.IO.ByteString as SPB@@ -249,7 +269,9 @@     FileMode   , ProcessID   , Fd+  , EpochTime   )+import System.Posix.Time   @@ -716,7 +738,18 @@ createDir :: FileMode -> Path b -> IO () createDir fm (MkPath destBS) = createDirectory destBS fm +-- |Create an empty directory at the given directory with the given filename.+--+-- Throws:+--+--    - `PermissionDenied` if output directory cannot be written to+--    - `NoSuchThing` if any of the parent components of the path+--      do not exist+createDirIfMissing :: FileMode -> Path b -> IO ()+createDirIfMissing fm (MkPath destBS) =+  hideError AlreadyExists $ createDirectory destBS fm + -- |Create an empty directory at the given directory with the given filename. -- All parent directories are created with the same filemode. This -- basically behaves like:@@ -734,7 +767,7 @@ --    - `PermissionDenied` if any part of the path components do not --      exist and cannot be written to --    - `AlreadyExists` if destination already exists and---      is not a directory+--      is *not* a directory -- -- Note: calls `getcwd` if the input path is a relative path createDirRecursive :: FileMode -> Path b -> IO ()@@ -922,7 +955,7 @@   -- |Write a given ByteString to a file, truncating the file beforehand.--- The file must exist. Follows symlinks.+-- Follows symlinks. -- -- Throws: --@@ -930,12 +963,36 @@ --     - `PermissionDenied` if we cannot read the file or the directory --        containting it --     - `NoSuchThing` if the file does not exist-writeFile :: Path b -> ByteString -> IO ()-writeFile (MkPath fp) bs =-  bracket (openFd fp SPI.WriteOnly [SPDF.oTrunc] Nothing) (SPI.closeFd) $ \fd -> -    void $ SPB.fdWrite fd bs+writeFile :: Path b+          -> Maybe FileMode  -- ^ if Nothing, file must exist+          -> ByteString+          -> IO ()+writeFile (MkPath fp) fmode bs =+  bracket (openFd fp SPI.WriteOnly [SPDF.oTrunc] fmode) (SPI.closeFd) $ \fd -> void $ SPB.fdWrite fd bs  +-- |Write a given lazy ByteString to a file, truncating the file beforehand.+-- Follows symlinks.+--+-- Throws:+--+--     - `InappropriateType` if file is not a regular file or a symlink+--     - `PermissionDenied` if we cannot read the file or the directory+--        containting it+--     - `NoSuchThing` if the file does not exist+--+-- Note: uses streamly under the hood+writeFileL :: Path b+           -> Maybe FileMode  -- ^ if Nothing, file must exist+           -> L.ByteString+           -> IO ()+writeFileL (MkPath fp) fmode lbs = do+  handle <- bracketOnError (openFd fp SPI.WriteOnly [SPDF.oTrunc] fmode) (SPI.closeFd) $ SPI.fdToHandle+  finally (streamlyCopy handle) (SIO.hClose handle)+  where+    streamlyCopy tH = S.fold (FH.writeChunks tH) $ SL.fromByteString lbs++ -- |Append a given ByteString to a file. -- The file must exist. Follows symlinks. --@@ -1022,6 +1079,16 @@     $ return False  +-- |Checks whether a file or folder is readable.+--+-- Only eACCES, eROFS, eTXTBSY, ePERM are catched (and return False).+--+-- Throws:+--+--     - `NoSuchThing` if the file does not exist+isReadable :: Path b -> IO Bool+isReadable (MkPath bs) = fileAccess bs True False False+ -- |Checks whether a file or folder is writable. -- -- Only eACCES, eROFS, eTXTBSY, ePERM are catched (and return False).@@ -1033,6 +1100,18 @@ isWritable (MkPath bs) = fileAccess bs False True False  +-- |Checks whether a file or folder is executable.+--+-- Only eACCES, eROFS, eTXTBSY, ePERM are catched (and return False).+--+-- Throws:+--+--     - `NoSuchThing` if the file does not exist+isExecutable :: Path b -> IO Bool+isExecutable (MkPath bs) = fileAccess bs False False True+++ -- |Checks whether the directory at the given path exists and can be -- opened. This invokes `openDirStream` which follows symlinks. canOpenDirectory :: Path b -> IO Bool@@ -1046,6 +1125,30 @@   +    ------------------+    --[ File times ]--+    ------------------+++getModificationTime :: Path b -> IO UTCTime+getModificationTime (MkPath bs) = do+  fs <- PF.getFileStatus bs+  pure $ posixSecondsToUTCTime $ PF.modificationTimeHiRes fs++setModificationTime :: Path b -> EpochTime -> IO ()+setModificationTime (MkPath bs) t = do+  -- TODO: setFileTimes doesn't allow to pass NULL to utime+  ctime <- epochTime+  PF.setFileTimes bs ctime t++setModificationTimeHiRes :: Path b -> POSIXTime -> IO ()+setModificationTimeHiRes (MkPath bs) t = do+  -- TODO: setFileTimesHiRes doesn't allow to pass NULL to utimes+  ctime <- getPOSIXTime+  PF.setFileTimesHiRes bs ctime t+++     -------------------------     --[ Directory reading ]--     -------------------------@@ -1063,17 +1166,25 @@ --     - `InappropriateType` if file type is wrong (symlink to file) --     - `InappropriateType` if file type is wrong (symlink to dir) --     - `PermissionDenied` if directory cannot be opened+--     - `PathParseException` if a filename could not be parsed (should never happen) getDirsFiles :: Path b        -- ^ dir to read              -> IO [Path b] getDirsFiles p@(MkPath fp) = do+  contents <- getDirsFiles' p+  pure $ fmap (p </>) contents+++-- | Like 'getDirsFiles', but returns the filename only, instead+-- of prepending the base path.+getDirsFiles' :: Path b        -- ^ dir to read+             -> IO [Path Fn]+getDirsFiles' p@(MkPath fp) = do   fd <- openFd fp SPI.ReadOnly [SPDF.oNofollow] Nothing-  return-    . catMaybes-    .   fmap (\x -> (</>) p <$> (parseMaybe . snd $ x))-    =<< getDirectoryContents' fd-  where-    parseMaybe :: ByteString -> Maybe (Path Fn)-    parseMaybe = parseFn+  rawContents <- getDirectoryContents' fd+  fmap catMaybes $ for rawContents $ \(_, f) ->+      if FP.isSpecialDirectoryEntry f+      then pure Nothing+      else fmap Just $ parseFn f   @@ -1139,3 +1250,37 @@       cwd <- getWorkingDirectory >>= parseAbs       rel <- parseRel bs -- we know it must be relative now       return $ cwd </> rel+++-- | Helper function to use the Path library without+-- buying into the Path type too much. This uses 'parseAny'+-- under the hood and may throw `PathParseException`.+--+-- Throws:+--+--    - `PathParseException` if the bytestring could neither be parsed as+--      relative or absolute Path+withRawFilePath :: MonadThrow m => ByteString -> (Path a -> m b) -> m b+withRawFilePath bs action = do+  path <- parseAny bs+  action path+++-- | Convenience function to open the path as a handle.+--+-- If the file does not exist, it will be created with 'newFilePerms'.+--+-- Throws:+--+--    - `PathParseException` if the bytestring could neither be parsed as+--      relative or absolute Path+withHandle :: ByteString+           -> SPI.OpenMode+           -> ((SIO.Handle, Path a) -> IO a)+           -> IO a+withHandle bs mode action = do+  path   <- parseAny bs+  handle <-+    bracketOnError (openFd bs mode [] (Just newFilePerms)) (SPI.closeFd)+      $ SPI.fdToHandle+  finally (action (handle, path)) (SIO.hClose handle)
src/HPath/IO/Errors.hs view
@@ -38,6 +38,7 @@   , catchErrno   , rethrowErrnoAs   , handleIOError+  , hideError   , bracketeer   , reactOnError   )@@ -281,9 +282,13 @@ handleIOError = flip catchIOError  +hideError :: IOErrorType -> IO () -> IO ()+hideError err = handleIO (\e -> if err == ioeGetErrorType e then pure () else ioError e)++ -- |Like `bracket`, but allows to have different clean-up -- actions depending on whether the in-between computation--- has raised an exception or not. +-- has raised an exception or not. bracketeer :: IO a        -- ^ computation to run first            -> (a -> IO b) -- ^ computation to run last, when                           --   no exception was raised
+ src/Streamly/ByteString/Lazy.hs view
@@ -0,0 +1,45 @@+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Streamly.ByteString.Lazy where++import Control.Monad.IO.Class+import qualified Data.ByteString as BS+import Data.ByteString.Lazy (ByteString, foldlChunks, fromChunks)+import Data.ByteString.Unsafe+import Data.Word (Word8)+import Foreign.ForeignPtr+import Foreign.ForeignPtr.Unsafe+import Foreign.Ptr (castPtr, minusPtr, plusPtr)+import Prelude hiding (length)+import Streamly+import Streamly.ByteString (arrayToByteString, byteStringToArray)+import Streamly.Memory.Array+import qualified Streamly.Prelude as S++toByteString ::+     forall m. (MonadIO m, MonadAsync m)+  => SerialT m (Array Word8)+  -> m ByteString+toByteString stream = do+  ys :: [BS.ByteString] <- S.toList $ S.mapM arrayToByteString stream+  pure $ fromChunks ys++stepFunction ::+     forall m. (MonadIO m)+  => SerialT m (Array Word8)+  -> BS.ByteString+  -> SerialT m (Array Word8)+stepFunction stream1 bs = do+  arr <- liftIO $ byteStringToArray bs+  let stream2 = pure arr+  stream1 <> stream2++fromByteString ::+     forall m. (MonadIO m)+  => ByteString+  -> SerialT m (Array Word8)+fromByteString bs = foldlChunks stepFunction mempty bs+
+ test/HPath/IO/CreateDirIfMissingSpec.hs view
@@ -0,0 +1,69 @@+{-# LANGUAGE OverloadedStrings #-}++module HPath.IO.CreateDirIfMissingSpec where+++import Test.Hspec+import System.IO.Error+  (+    ioeGetErrorType+  )+import GHC.IO.Exception+  (+    IOErrorType(..)+  )+import Utils++++upTmpDir :: IO ()+upTmpDir = do+  setTmpDir "CreateDirIfMissingSpec"+  createTmpDir++setupFiles :: IO ()+setupFiles = do+  createDir' "alreadyExists"+  createDir' "noPerms"+  createDir' "noWritePerms"+  noPerms "noPerms"+  noWritableDirPerms "noWritePerms"++++cleanupFiles :: IO ()+cleanupFiles = do+  normalDirPerms "noPerms"+  normalDirPerms "noWritePerms"+  deleteDir' "alreadyExists"+  deleteDir' "noPerms"+  deleteDir' "noWritePerms"+++spec :: Spec+spec = beforeAll_ (upTmpDir >> setupFiles) $ afterAll_ cleanupFiles $+  describe "HPath.IO.CreateDirIfMissing" $ do++    -- successes --+    it "createDirIfMissing, all fine" $ do+      createDirIfMissing' "newDir"+      removeDirIfExists "newDir"++    it "createDirIfMissing, destination directory already exists" $+      createDirIfMissing' "alreadyExists"++    -- posix failures --+    it "createDirIfMissing, parent directories do not exist" $+      createDirIfMissing' "some/thing/dada"+        `shouldThrow`+        (\e -> ioeGetErrorType e == NoSuchThing)++    it "createDirIfMissing, can't write to output directory" $+      createDirIfMissing' "noWritePerms/newDir"+        `shouldThrow`+        (\e -> ioeGetErrorType e == PermissionDenied)++    it "createDirIfMissing, can't open output directory" $+      createDirIfMissing' "noPerms/newDir"+        `shouldThrow`+        (\e -> ioeGetErrorType e == PermissionDenied)
test/HPath/IO/GetDirsFilesSpec.hs view
@@ -8,7 +8,7 @@     sort   ) import qualified HPath as P-import HPath.IO+import HPath.IO hiding (getDirsFiles') import Test.Hspec import System.IO.Error   (
+ test/HPath/IO/WriteFileLSpec.hs view
@@ -0,0 +1,108 @@+{-# LANGUAGE OverloadedStrings #-}+++module HPath.IO.WriteFileLSpec where+++import Test.Hspec+import System.IO.Error+  (+    ioeGetErrorType+  )+import GHC.IO.Exception+  (+    IOErrorType(..)+  )+import Utils++++upTmpDir :: IO ()+upTmpDir = do+  setTmpDir "WriteFileLSpec"+  createTmpDir++setupFiles :: IO ()+setupFiles = do+  createRegularFile' "fileWithContent"+  createRegularFile' "fileWithoutContent"+  createSymlink' "inputFileSymL" "fileWithContent"+  createDir' "alreadyExistsD"+  createRegularFile' "noPerms"+  noPerms "noPerms"+  createDir' "noPermsD"+  createRegularFile' "noPermsD/inputFile"+  noPerms "noPermsD"+  writeFile' "fileWithContent" "BLKASL"+++cleanupFiles :: IO ()+cleanupFiles = do+  deleteFile' "fileWithContent"+  deleteFile' "fileWithoutContent"+  deleteFile' "inputFileSymL"+  deleteDir' "alreadyExistsD"+  normalFilePerms "noPerms"+  deleteFile' "noPerms"+  normalDirPerms "noPermsD"+  deleteFile' "noPermsD/inputFile"+  deleteDir' "noPermsD"+++spec :: Spec+spec = beforeAll_ (upTmpDir >> setupFiles) $ afterAll_ cleanupFiles $+  describe "HPath.IO.WriteFileL" $ do++    -- successes --+    it "WriteFileL file with content, everything clear" $ do+      writeFileL' "fileWithContent" "blahfaselllll"+      out <- readFile' "fileWithContent"+      out `shouldBe` "blahfaselllll"++    it "WriteFileL file with content, everything clear" $ do+      writeFileL' "fileWithContent" "gagagaga"+      out <- readFile' "fileWithContent"+      out `shouldBe` "gagagaga"++    it "WriteFileL file with content, everything clear" $ do+      writeFileL' "fileWithContent" ""+      out <- readFile' "fileWithContent"+      out `shouldBe` ""++    it "WriteFileL file without content, everything clear" $ do+      writeFileL' "fileWithoutContent" "blahfaselllll"+      out <- readFile' "fileWithoutContent"+      out `shouldBe` "blahfaselllll"++    it "WriteFileL, everything clear" $ do+      writeFileL' "fileWithoutContent" "gagagaga"+      out <- readFile' "fileWithoutContent"+      out `shouldBe` "gagagaga"++    it "WriteFileL symlink, everything clear" $ do+      writeFileL' "inputFileSymL" "blahfaselllll"+      out <- readFile' "inputFileSymL"+      out `shouldBe` "blahfaselllll"++    it "WriteFileL symlink, everything clear" $ do+      writeFileL' "inputFileSymL" "gagagaga"+      out <- readFile' "inputFileSymL"+      out `shouldBe` "gagagaga"+++    -- posix failures --+    it "WriteFileL to dir, inappropriate type" $ do+      writeFileL' "alreadyExistsD" ""+        `shouldThrow` (\e -> ioeGetErrorType e == InappropriateType)++    it "WriteFileL, no permissions to file" $ do+      writeFileL' "noPerms" ""+        `shouldThrow` (\e -> ioeGetErrorType e == PermissionDenied)++    it "WriteFileL, no permissions to file" $ do+      writeFileL' "noPermsD/inputFile" ""+        `shouldThrow` (\e -> ioeGetErrorType e == PermissionDenied)++    it "WriteFileL, file does not exist" $ do+      writeFileL' "gaga" ""+        `shouldThrow` (\e -> ioeGetErrorType e == NoSuchThing)
test/Utils.hs view
@@ -19,6 +19,7 @@     whenM   ) import qualified Data.ByteString as BS+import qualified Data.ByteString.Lazy as BSL import Data.IORef   (     newIORef@@ -163,6 +164,10 @@ {-# NOINLINE createDir' #-} createDir' dest = withTmpDir dest (createDir newDirPerms) +createDirIfMissing' :: ByteString -> IO ()+{-# NOINLINE createDirIfMissing' #-}+createDirIfMissing' dest = withTmpDir dest (createDirIfMissing newDirPerms)+ createDirRecursive' :: ByteString -> IO () {-# NOINLINE createDirRecursive' #-} createDirRecursive' dest = withTmpDir dest (createDirRecursive newDirPerms)@@ -262,8 +267,13 @@  writeFile' :: ByteString -> ByteString -> IO () {-# NOINLINE writeFile' #-}-writeFile' ip bs = -  withTmpDir ip $ \p -> writeFile p bs+writeFile' ip bs =+  withTmpDir ip $ \p -> writeFile p Nothing bs++writeFileL' :: ByteString -> BSL.ByteString -> IO ()+{-# NOINLINE writeFileL' #-}+writeFileL' ip bs =+  withTmpDir ip $ \p -> writeFileL p Nothing bs   appendFile' :: ByteString -> ByteString -> IO ()