hpath-io 0.10.0 → 0.10.1
raw patch · 9 files changed
+201/−182 lines, 9 filesdep +safe-exceptionsdep +transformersPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependencies added: safe-exceptions, transformers
API changes (from Hackage documentation)
- HPath.IO: readFileEOF :: Path b -> IO ByteString
- HPath.IO.Errors: canOpenDirectory :: Path b -> IO Bool
- HPath.IO.Errors: doesDirectoryExist :: Path b -> IO Bool
- HPath.IO.Errors: doesFileExist :: Path b -> IO Bool
- HPath.IO.Errors: isWritable :: Path b -> IO Bool
+ HPath.IO: canOpenDirectory :: Path b -> IO Bool
+ HPath.IO: doesDirectoryExist :: Path b -> IO Bool
+ HPath.IO: doesExist :: Path b -> IO Bool
+ HPath.IO: doesFileExist :: Path b -> IO Bool
+ HPath.IO: isWritable :: Path b -> IO Bool
+ HPath.IO: readFileStream :: Path b -> IO (SerialT IO ByteString)
Files
- CHANGELOG.md +14/−2
- README.md +2/−0
- hpath-io.cabal +5/−3
- src/HPath/IO.hs +109/−43
- src/HPath/IO.hs-boot +8/−0
- src/HPath/IO/Errors.hs +5/−42
- src/Streamly/ByteString.hs +57/−0
- test/HPath/IO/ReadFileEOFSpec.hs +0/−85
- test/Utils.hs +1/−7
CHANGELOG.md view
@@ -1,5 +1,17 @@ # Revision history for hpath-io -## 0.9.3 -- YYYY-mm-dd+## 0.10.1 -- 2020-01-13 -* First version. Released on an unsuspecting world.+* Move file check functions to HPath.IO+* Add 'doesExist'+* Exception handling of `doesExist`, `doesFileExist`, `doesDirectoryExist` has changed: only eNOENT is catched+* Exception handling of `isWritable` has changed: just a wrapper around `access` now+* switch exception handling to `safe-exceptions`+* Redo file reading API (readFileEOF dropped and now using streamly under the hood, added `readFileStream`)+++## 0.10.0 -- 2020-01-04++* First version. Split from 'hpath', contains only the IO parts.+* Now uses streamly for 'copyFile'+* Fixed tmpdir in hspec
README.md view
@@ -4,6 +4,8 @@ High-level IO operations on files/directories, utilizing type-safe Paths. +This package is part of the HPath suite, also check out [hpath](https://hackage.haskell.org/package/hpath) and [hpath-filepath](https://hackage.haskell.org/package/hpath-filepath).+ ## Motivation The motivation came during development of
hpath-io.cabal view
@@ -1,5 +1,5 @@ name: hpath-io-version: 0.10.0+version: 0.10.1 synopsis: High-level IO operations on files/directories description: High-level IO operations on files/directories, utilizing type-safe Paths -- bug-reports:@@ -32,17 +32,20 @@ System.Posix.FD c-sources: cbits/dirutils.c - -- other-modules:+ other-modules: Streamly.ByteString -- other-extensions: build-depends: base >= 4.8 && <5 , IfElse , bytestring >= 0.10.0.0 , hpath >= 0.10 && < 0.11 , hpath-filepath >= 0.10 && < 0.11+ , safe-exceptions >= 0.1 , streamly >= 0.7 , unix >= 2.5 , unix-bytestring , utf8-string+ if !impl(ghc>=7.11)+ build-depends: transformers hs-source-dirs: src default-language: Haskell2010 @@ -73,7 +76,6 @@ HPath.IO.GetFileTypeSpec HPath.IO.MoveFileOverwriteSpec HPath.IO.MoveFileSpec- HPath.IO.ReadFileEOFSpec HPath.IO.ReadFileSpec HPath.IO.RecreateSymlinkOverwriteSpec HPath.IO.RecreateSymlinkSpec
src/HPath/IO.hs view
@@ -36,6 +36,7 @@ {-# LANGUAGE CPP #-} {-# LANGUAGE PackageImports #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE FlexibleContexts #-} module HPath.IO (@@ -66,13 +67,19 @@ , moveFile -- * File reading , readFile- , readFileEOF+ , readFileStream -- * File writing , writeFile , appendFile -- * File permissions , newFilePerms , newDirPerms+ -- * File checks+ , doesExist+ , doesFileExist+ , doesDirectoryExist+ , isWritable+ , canOpenDirectory -- * Directory reading , getDirsFiles -- * Filetype operations@@ -88,7 +95,7 @@ ( (<$>) )-import Control.Exception+import Control.Exception.Safe ( IOException , bracket@@ -175,8 +182,14 @@ import HPath.Internal import HPath.IO.Errors import Prelude hiding (appendFile, readFile, writeFile)+import Streamly+import Streamly.ByteString+import qualified Streamly.Data.Fold as FL+import Streamly.Memory.Array import qualified Streamly.FileSystem.Handle as FH+import qualified Streamly.Internal.Data.Unfold as SU import qualified Streamly.Internal.FileSystem.Handle as IFH+import qualified Streamly.Internal.Memory.ArrayStream as AS import qualified Streamly.Prelude as S import qualified System.IO as SIO import System.IO.Error@@ -191,7 +204,9 @@ import System.Posix.Directory.ByteString ( createDirectory+ , closeDirStream , getWorkingDirectory+ , openDirStream , removeDirectory ) import System.Posix.Directory.Traversals@@ -201,6 +216,7 @@ import System.Posix.Files.ByteString ( createSymbolicLink+ , fileAccess , fileMode , getFdStatus , groupExecuteMode@@ -446,7 +462,9 @@ case cm of Strict -> return () Overwrite -> do- writable <- toAbs newsym >>= isWritable+ writable <- toAbs newsym >>= (\p -> do+ e <- doesExist p+ if e then isWritable p else pure False) isfile <- doesFileExist newsym isdir <- doesDirectoryExist newsym when (writable && isfile) (deleteFile newsym)@@ -830,7 +848,10 @@ easyDelete from Overwrite -> do ft <- getFileType from- writable <- toAbs to >>= isWritable+ writable <- toAbs to >>= (\p -> do+ e <- doesFileExist p+ if e then isWritable p else pure False)+ case ft of RegularFile -> do exists <- doesFileExist to@@ -853,19 +874,14 @@ -------------------- --- |Read the given file at once into memory as a strict ByteString.+-- |Read the given file *at once* into memory as a lazy ByteString. -- Symbolic links are followed, no sanity checks on file size--- or file type. File must exist.------ Note: the size of the file is determined in advance, as to only--- have one allocation.+-- or file type. File must exist. Uses Builders under the hood+-- (hence lazy ByteString). -- -- Safety/reliability concerns: ----- * since amount of bytes to read is determined in advance,--- the file might be read partially only if something else is--- appending to it while reading--- * the whole file is read into memory!+-- * the whole file is read into memory, this doesn't read lazily -- -- Throws: --@@ -873,21 +889,15 @@ -- - `PermissionDenied` if we cannot read the file or the directory -- containting it -- - `NoSuchThing` if the file does not exist-readFile :: Path b -> IO ByteString-readFile (MkPath fp) =- bracket (openFd fp SPI.ReadOnly [] Nothing) (SPI.closeFd) $ \fd -> do- stat <- PF.getFdStatus fd- let fsize = PF.fileSize stat- SPB.fdRead fd (fromIntegral fsize)+readFile :: Path b -> IO L.ByteString+readFile path = do+ stream <- readFileStream path+ toLazyByteString <$> S.fold FL.mconcat (fmap byteString stream) --- |Read the given file in chunks of size `8192` into memory until--- `fread` returns 0. Returns a lazy ByteString, because it uses--- Builders under the hood.------ Safety/reliability concerns:------ * the whole file is read into memory!++-- | Open the given file as a filestream. Once the filestream is+-- exits, the filehandle is cleaned up. -- -- Throws: --@@ -895,23 +905,13 @@ -- - `PermissionDenied` if we cannot read the file or the directory -- containting it -- - `NoSuchThing` if the file does not exist-readFileEOF :: Path b -> IO L.ByteString-readFileEOF (MkPath fp) =- bracket (openFd fp SPI.ReadOnly [] Nothing) (SPI.closeFd) $ \fd ->- allocaBytes (fromIntegral bufSize) $ \buf -> read' fd buf mempty- where- bufSize :: CSize- bufSize = 8192- read' :: Fd -> Ptr Word8 -> Builder -> IO L.ByteString- read' fd buf builder = do- size <- SPB.fdReadBuf fd buf bufSize- if size == 0- then return $ toLazyByteString builder- else do- readBS <- unsafePackCStringFinalizer buf- (fromIntegral size)- (return ())- read' fd buf (builder <> byteString readBS)+readFileStream :: Path b+ -> IO (SerialT IO ByteString)+readFileStream (MkPath fp) = do+ fd <- openFd fp SPI.ReadOnly [] Nothing+ handle <- SPI.fdToHandle fd+ let stream = (S.unfold (SU.finally SIO.hClose FH.readChunks) handle) >>= arrayToByteString+ pure stream @@ -977,6 +977,72 @@ `unionFileModes` groupReadMode `unionFileModes` otherExecuteMode `unionFileModes` otherReadMode+++++ -------------------+ --[ File checks ]--+ -------------------+++-- |Checks if the given file exists.+-- Does not follow symlinks.+--+-- Only eNOENT is catched (and returns False).+doesExist :: Path b -> IO Bool+doesExist (MkPath bs) =+ catchErrno [eNOENT] (do+ _ <- PF.getSymbolicLinkStatus bs+ return $ True)+ $ return False+++-- |Checks if the given file exists and is not a directory.+-- Does not follow symlinks.+--+-- Only eNOENT is catched (and returns False).+doesFileExist :: Path b -> IO Bool+doesFileExist (MkPath bs) =+ catchErrno [eNOENT] (do+ fs <- PF.getSymbolicLinkStatus bs+ return $ not . PF.isDirectory $ fs)+ $ return False+++-- |Checks if the given file exists and is a directory.+-- Does not follow symlinks.+--+-- Only eNOENT is catched (and returns False).+doesDirectoryExist :: Path b -> IO Bool+doesDirectoryExist (MkPath bs) =+ catchErrno [eNOENT] (do+ fs <- PF.getSymbolicLinkStatus bs+ return $ PF.isDirectory fs)+ $ return False+++-- |Checks whether a file or folder is writable.+--+-- Only eACCES, eROFS, eTXTBSY, ePERM are catched (and return False).+--+-- Throws:+--+-- - `NoSuchThing` if the file does not exist+isWritable :: Path b -> IO Bool+isWritable (MkPath bs) = fileAccess bs False True False+++-- |Checks whether the directory at the given path exists and can be+-- opened. This invokes `openDirStream` which follows symlinks.+canOpenDirectory :: Path b -> IO Bool+canOpenDirectory (MkPath bs) =+ handleIOError (\_ -> return False) $ do+ bracket (openDirStream bs)+ closeDirStream+ (\_ -> return ())+ return True+
src/HPath/IO.hs-boot view
@@ -6,3 +6,11 @@ canonicalizePath :: Path b -> IO (Path Abs) toAbs :: Path b -> IO (Path Abs)++doesFileExist :: Path b -> IO Bool++doesDirectoryExist :: Path b -> IO Bool++isWritable :: Path b -> IO Bool++canOpenDirectory :: Path b -> IO Bool
src/HPath/IO/Errors.hs view
@@ -33,10 +33,6 @@ , throwSameFile , sameFile , throwDestinationInSource- , doesFileExist- , doesDirectoryExist- , isWritable- , canOpenDirectory -- * Error handling functions , catchErrno@@ -52,7 +48,7 @@ ( (<$>) )-import Control.Exception+import Control.Exception.Safe hiding (handleIOError) import Control.Monad ( forM@@ -92,11 +88,14 @@ ( canonicalizePath , toAbs+ , doesFileExist+ , doesDirectoryExist+ , isWritable+ , canOpenDirectory ) import System.IO.Error ( alreadyExistsErrorType- , catchIOError , ioeGetErrorType , mkIOError )@@ -240,42 +239,6 @@ $ PF.getFileStatus sbs when (elem sid dids) (throwIO $ DestinationInSource dbs sbs)----- |Checks if the given file exists and is not a directory.--- Does not follow symlinks.-doesFileExist :: Path b -> IO Bool-doesFileExist (MkPath bs) =- handleIOError (\_ -> return False) $ do- fs <- PF.getSymbolicLinkStatus bs- return $ not . PF.isDirectory $ fs----- |Checks if the given file exists and is a directory.--- Does not follow symlinks.-doesDirectoryExist :: Path b -> IO Bool-doesDirectoryExist (MkPath bs) =- handleIOError (\_ -> return False) $ do- fs <- PF.getSymbolicLinkStatus bs- return $ PF.isDirectory fs----- |Checks whether a file or folder is writable.-isWritable :: Path b -> IO Bool-isWritable (MkPath bs) =- handleIOError (\_ -> return False) $- fileAccess bs False True False----- |Checks whether the directory at the given path exists and can be--- opened. This invokes `openDirStream` which follows symlinks.-canOpenDirectory :: Path b -> IO Bool-canOpenDirectory (MkPath bs) =- handleIOError (\_ -> return False) $ do- bracket (PFD.openDirStream bs)- PFD.closeDirStream- (\_ -> return ())- return True
+ src/Streamly/ByteString.hs view
@@ -0,0 +1,57 @@+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Streamly.ByteString where++import Control.Monad.IO.Class+import Data.ByteString hiding (length)+import qualified Data.ByteString as BS+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.Internal.Memory.Array.Types+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 =+ let xs = S.mapM arrayToByteString stream+ ys = S.foldlM' (\a b -> pure $ a <> b) mempty xs+ in ys++arrayToByteString :: (MonadIO m) => Array Word8 -> m ByteString+arrayToByteString arr+ | length arr == 0 = return mempty+arrayToByteString Array {..} =+ liftIO $+ withForeignPtr aStart $ \ptr ->+ unsafePackCStringFinalizer ptr aLen (return ())+ where+ aLen =+ let p = unsafeForeignPtrToPtr aStart+ in aEnd `minusPtr` p++byteStringToArray :: (MonadIO m) => ByteString -> m (Array Word8)+byteStringToArray bs =+ liftIO $+ unsafeUseAsCStringLen+ bs+ (\(ptr, _) -> do+ let endPtr pr = (castPtr pr `plusPtr` (BS.length bs))+ fptr <- newForeignPtr_ (castPtr ptr)+ return $ Array {aStart = fptr, aEnd = endPtr ptr, aBound = endPtr ptr})++fromByteString ::+ forall m. (MonadIO m)+ => ByteString+ -> m (Array Word8)+fromByteString bs = byteStringToArray bs
− test/HPath/IO/ReadFileEOFSpec.hs
@@ -1,85 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}---module HPath.IO.ReadFileEOFSpec where---import Test.Hspec-import System.IO.Error- (- ioeGetErrorType- )-import GHC.IO.Exception- (- IOErrorType(..)- )-import Utils----upTmpDir :: IO ()-upTmpDir = do- setTmpDir "ReadFileEOFSpec"- 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" "Blahfaselgagaga"---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.readFileEOF" $ do-- -- successes --- it "readFileEOF (Strict) file with content, everything clear" $ do- out <- readFileEOF' "fileWithContent"- out `shouldBe` "Blahfaselgagaga"-- it "readFileEOF (Strict) symlink, everything clear" $ do- out <- readFileEOF' "inputFileSymL"- out `shouldBe` "Blahfaselgagaga"-- it "readFileEOF (Strict) empty file, everything clear" $ do- out <- readFileEOF' "fileWithoutContent"- out `shouldBe` ""--- -- posix failures --- it "readFileEOF (Strict) directory, wrong file type" $ do- readFileEOF' "alreadyExistsD"- `shouldThrow` (\e -> ioeGetErrorType e == InappropriateType)-- it "readFileEOF (Strict) file, no permissions" $ do- readFileEOF' "noPerms"- `shouldThrow` (\e -> ioeGetErrorType e == PermissionDenied)-- it "readFileEOF (Strict) file, no permissions on dir" $ do- readFileEOF' "noPermsD/inputFile"- `shouldThrow` (\e -> ioeGetErrorType e == PermissionDenied)-- it "readFileEOF (Strict) file, no such file" $ do- readFileEOF' "lalala"- `shouldThrow` (\e -> ioeGetErrorType e == NoSuchThing)
test/Utils.hs view
@@ -27,7 +27,6 @@ , IORef ) import HPath.IO-import HPath.IO.Errors import Prelude hiding (appendFile, readFile, writeFile) import Data.Maybe (@@ -281,10 +280,5 @@ readFile' :: ByteString -> IO ByteString {-# NOINLINE readFile' #-}-readFile' p = withTmpDir p readFile---readFileEOF' :: ByteString -> IO L.ByteString-{-# NOINLINE readFileEOF' #-}-readFileEOF' p = withTmpDir p readFileEOF+readFile' p = withTmpDir p (fmap L.toStrict . readFile)