diff --git a/System/File/OsPath.hs b/System/File/OsPath.hs index 6d8057f..11c8aa7 100644 --- a/System/File/OsPath.hs +++ b/System/File/OsPath.hs @@ -27,6 +27,7 @@ module System.File.OsPath ( , openBinaryTempFile , openTempFileWithDefaultPermissions , openBinaryTempFileWithDefaultPermissions +, createTempDirectory ) where diff --git a/System/File/OsPath/Internal.hs b/System/File/OsPath/Internal.hs index a18086f..9bfe943 100644 --- a/System/File/OsPath/Internal.hs +++ b/System/File/OsPath/Internal.hs @@ -162,6 +162,18 @@ openTempFile :: OsPath -- ^ Directory in which to create the file -> IO (OsPath, Handle) openTempFile tmp_dir template = openTempFile' "openTempFile" tmp_dir template False 0o600 +-- | The function creates a temporary directory. +-- +-- @since 0.1.6 +createTempDirectory :: OsPath -- ^ Directory in which to create the file + -> OsString -- ^ Directory name template. If the template is \"foo\" then + -- the created directory will be \"fooXXX\" where XXX is some + -- random number. Note that this should not contain any path + -- separator characters. On Windows, the template may + -- be truncated to 3 chars. + -> IO OsPath +createTempDirectory tmp_dir template = createTempDirectory' "createTempDirectory" tmp_dir template 0o700 + -- | Like 'openTempFile', but opens the file in binary mode. See 'openBinaryFile' for more comments. -- -- @since 0.1.3 @@ -247,6 +259,15 @@ openTempFile' loc (OsString tmp_dir) template@(OsString tmpl) binary mode -- below filepath in the hierarchy here. (OsString prefix, OsString suffix) = OSP.splitExtension template +createTempDirectory' :: String -> OsPath -> OsString -> CMode + -> IO OsPath +createTempDirectory' loc (OsString tmp_dir) template@(OsString tmpl) mode + | any_ (== OSP.pathSeparator) template + = throwIO $ userError $ "createTempDirectory': Template string must not contain path separator characters: " ++ P.lenientDecode tmpl + | otherwise = do + fp <- P.findTempDName tmpl loc tmp_dir mode + pure (OsString fp) + #if MIN_VERSION_filepath(1, 5, 0) any_ :: (OsChar -> Bool) -> OsString -> Bool any_ = OSS.any diff --git a/file-io.cabal b/file-io.cabal index f09a911..b29346c 100644 --- a/file-io.cabal +++ b/file-io.cabal @@ -61,56 +61,71 @@ library ghc-options: -Wall + +common test-utils + other-modules: TestUtils + build-depends: os-string + if os(windows) + build-depends: Win32 >=2.13.3.0 + else + build-depends: unix >=2.8.0.0 && <3 + test-suite T15 + import: test-utils hs-source-dirs: tests main-is: T15.hs type: exitcode-stdio-1.0 default-language: Haskell2010 - build-depends: base >=4.13.0.0 && <5, tasty, tasty-hunit, file-io, filepath, temporary + build-depends: base >=4.13.0.0 && <5, tasty, tasty-hunit, file-io, filepath ghc-options: -Wall -threaded -rtsopts "-with-rtsopts=-N10" if os(windows) build-depends: Win32 >=2.13.3.0 test-suite T15Win + import: test-utils hs-source-dirs: tests main-is: T15Win.hs type: exitcode-stdio-1.0 default-language: Haskell2010 if os(windows) - build-depends: base >=4.13.0.0 && <5, tasty, tasty-hunit, file-io, filepath, temporary, Win32 >=2.13.3.0 + build-depends: base >=4.13.0.0 && <5, tasty, tasty-hunit, file-io, filepath, Win32 >=2.13.3.0 else - build-depends: base >=4.13.0.0 && <5 + build-depends: base >=4.13.0.0 && <5, filepath, file-io ghc-options: -Wall -threaded -rtsopts "-with-rtsopts=-N10" test-suite T14 + import: test-utils hs-source-dirs: tests main-is: T14.hs type: exitcode-stdio-1.0 default-language: Haskell2010 - build-depends: base >=4.13.0.0 && <5, file-io, filepath, temporary + build-depends: base >=4.13.0.0 && <5, file-io, filepath ghc-options: -Wall test-suite T8 + import: test-utils hs-source-dirs: tests main-is: T8.hs type: exitcode-stdio-1.0 default-language: Haskell2010 - build-depends: base >=4.13.0.0 && <5, bytestring, file-io, filepath, temporary + build-depends: base >=4.13.0.0 && <5, bytestring, file-io, filepath ghc-options: -Wall -threaded test-suite CLC237 + import: test-utils hs-source-dirs: tests main-is: CLC237.hs type: exitcode-stdio-1.0 default-language: Haskell2010 - build-depends: base >=4.13.0.0 && <5, file-io, filepath, temporary + build-depends: base >=4.13.0.0 && <5, file-io, filepath ghc-options: -Wall test-suite Properties + import: test-utils hs-source-dirs: tests main-is: Properties.hs type: exitcode-stdio-1.0 default-language: Haskell2010 - build-depends: base >=4.13.0.0 && <5, bytestring, tasty, tasty-hunit, file-io, filepath, temporary + build-depends: base >=4.13.0.0 && <5, bytestring, tasty, tasty-hunit, file-io, filepath ghc-options: -Wall -threaded -rtsopts "-with-rtsopts=-N10" diff --git a/posix/System/File/Platform.hs b/posix/System/File/Platform.hs index 1260686..175e773 100644 --- a/posix/System/File/Platform.hs +++ b/posix/System/File/Platform.hs @@ -20,7 +20,9 @@ import System.OsPath.Posix ( PosixPath, PosixString, () ) import qualified System.OsPath.Posix as PS import Data.IORef (IORef, newIORef) import System.Posix (CMode) +import System.Posix.Directory.PosixPath (createDirectory) import System.IO (utf8, latin1) +import System.IO.Error (isAlreadyExistsError) import System.IO.Unsafe (unsafePerformIO) import System.Posix.Internals (c_getpid) import GHC.IORef (atomicModifyIORef'_) @@ -102,6 +104,23 @@ findTempName (prefix, suffix) loc tmp_dir mode = go openTempFile_ :: PosixPath -> CMode -> IO Fd openTempFile_ fp cmode = openFd fp ReadWrite defaultFileFlags' { creat = Just cmode, nonBlock = True, noctty = True, exclusive = True } +findTempDName :: PosixString + -> String + -> PosixPath + -> CMode + -> IO PosixPath +findTempDName template _loc tmp_dir mode = go + where + go = do + rs <- rand_string + let dirname = template <> rs + dirpath = tmp_dir dirname + r <- try $ createDirectory dirpath mode + case r of + Right _ -> return dirpath + Left e | isAlreadyExistsError e -> go + | otherwise -> ioError e + tempCounter :: IORef Int tempCounter = unsafePerformIO $ newIORef 0 {-# NOINLINE tempCounter #-} diff --git a/tests/CLC237.hs b/tests/CLC237.hs index 9475f49..9be4a57 100644 --- a/tests/CLC237.hs +++ b/tests/CLC237.hs @@ -10,13 +10,12 @@ import qualified System.OsPath as OSP import qualified System.File.OsPath as OSP import GHC.IO.Exception (IOErrorType(..), IOException(..)) import System.IO -import System.IO.Temp +import TestUtils -- Test that the action in 'withFile' does not inherit the filepath annotation -- See https://github.com/haskell/core-libraries-committee/issues/237 main :: IO () -main = withSystemTempDirectory "tar-test" $ \baseDir' -> do - baseDir <- OSP.encodeFS baseDir' +main = withFileIOTestDir $ \baseDir-> do res <- try @IOException $ OSP.withFile (baseDir [osp|foo|]) WriteMode $ \_ -> fail "test" case res of Left (IOError Nothing UserError "" "test" Nothing Nothing) -> pure () diff --git a/tests/LongPaths.hs b/tests/LongPaths.hs new file mode 100644 index 0000000..e69de29 diff --git a/tests/Properties.hs b/tests/Properties.hs index e719b3d..6ea6bb9 100644 --- a/tests/Properties.hs +++ b/tests/Properties.hs @@ -8,7 +8,6 @@ module Main where import Control.Exception -import qualified System.FilePath as FP import Test.Tasty import Test.Tasty.HUnit import System.OsPath ((), osp, OsPath, OsString) @@ -16,8 +15,8 @@ import qualified System.OsPath as OSP import qualified System.File.OsPath as OSP import GHC.IO.Exception (IOErrorType(..), IOException(..)) import System.IO -import System.IO.Temp import qualified Data.ByteString as BS +import TestUtils main :: IO () @@ -58,16 +57,14 @@ main = defaultMain $ testGroup "All" writeFileReadFile :: Assertion writeFileReadFile = do - withSystemTempDirectory "test" $ \baseDir' -> do - baseDir <- OSP.encodeFS baseDir' + withFileIOTestDir $ \baseDir -> do OSP.writeFile (baseDir [osp|foo|]) "test" contents <- OSP.readFile (baseDir [osp|foo|]) "test" @=? contents writeWriteFileReadFile :: Assertion writeWriteFileReadFile = do - withSystemTempDirectory "test" $ \baseDir' -> do - baseDir <- OSP.encodeFS baseDir' + withFileIOTestDir $ \baseDir -> do OSP.writeFile (baseDir [osp|foo|]) "lol" OSP.writeFile (baseDir [osp|foo|]) "test" contents <- OSP.readFile (baseDir [osp|foo|]) @@ -75,8 +72,7 @@ writeWriteFileReadFile = do appendFileReadFile :: Assertion appendFileReadFile = do - withSystemTempDirectory "test" $ \baseDir' -> do - baseDir <- OSP.encodeFS baseDir' + withFileIOTestDir $ \baseDir -> do OSP.writeFile (baseDir [osp|foo|]) "test" OSP.appendFile (baseDir [osp|foo|]) "test" contents <- OSP.readFile (baseDir [osp|foo|]) @@ -84,35 +80,31 @@ appendFileReadFile = do iomodeReadFile :: Assertion iomodeReadFile = do - withSystemTempDirectory "test" $ \baseDir' -> do - baseDir <- OSP.encodeFS baseDir' + withFileIOTestDir $ \baseDir -> do OSP.writeFile (baseDir [osp|foo|]) "" r <- try @IOException $ OSP.withFile (baseDir [osp|foo|]) ReadMode $ \h -> BS.hPut h "test" - IOError Nothing IllegalOperation "hPutBuf" "handle is not open for writing" Nothing (Just $ baseDir' FP. "foo") + IOError Nothing IllegalOperation "hPutBuf" "handle is not open for writing" Nothing (Just $ so $ baseDir [osp|foo|]) @==? r iomodeWriteFile :: Assertion iomodeWriteFile = do - withSystemTempDirectory "test" $ \baseDir' -> do - baseDir <- OSP.encodeFS baseDir' + withFileIOTestDir $ \baseDir -> do OSP.writeFile (baseDir [osp|foo|]) "" r <- try @IOException $ OSP.withFile (baseDir [osp|foo|]) WriteMode $ \h -> BS.hGetContents h - IOError Nothing IllegalOperation "hGetBuf" "handle is not open for reading" Nothing (Just $ baseDir' FP. "foo") + IOError Nothing IllegalOperation "hGetBuf" "handle is not open for reading" Nothing (Just $ so $ baseDir [osp|foo|]) @==? r iomodeAppendFile :: Assertion iomodeAppendFile = do - withSystemTempDirectory "test" $ \baseDir' -> do - baseDir <- OSP.encodeFS baseDir' + withFileIOTestDir $ \baseDir -> do OSP.writeFile (baseDir [osp|foo|]) "" r <- try @IOException $ OSP.withFile (baseDir [osp|foo|]) AppendMode $ \h -> BS.hGetContents h - IOError Nothing IllegalOperation "hGetBuf" "handle is not open for reading" Nothing (Just $ baseDir' FP. "foo") + IOError Nothing IllegalOperation "hGetBuf" "handle is not open for reading" Nothing (Just $ so $ baseDir [osp|foo|]) @==? r iomodeReadWriteFile :: Assertion iomodeReadWriteFile = do - withSystemTempDirectory "test" $ \baseDir' -> do - baseDir <- OSP.encodeFS baseDir' + withFileIOTestDir $ \baseDir -> do OSP.writeFile (baseDir [osp|foo|]) "" r <- try @IOException $ OSP.withFile (baseDir [osp|foo|]) ReadWriteMode $ \h -> do BS.hPut h "test" @@ -121,19 +113,17 @@ iomodeReadWriteFile = do concFile :: Assertion concFile = do - withSystemTempDirectory "test" $ \baseDir' -> do - baseDir <- OSP.encodeFS baseDir' + withFileIOTestDir $ \baseDir -> do let fp = baseDir [osp|foo|] OSP.writeFile fp "" !h <- OSP.openFile fp ReadMode r <- try @IOException $ OSP.withFile fp WriteMode $ \h' -> do BS.hPut h' "test" _ <- try @IOException $ BS.hPut h "" - IOError Nothing fileLockedType "withFile" fileLockedMsg Nothing (Just $ baseDir' FP. "foo") @==? r + IOError Nothing fileLockedType "withFile" fileLockedMsg Nothing (Just $ so $ baseDir [osp|foo|]) @==? r concFile2 :: Assertion concFile2 = do - withSystemTempDirectory "test" $ \baseDir' -> do - baseDir <- OSP.encodeFS baseDir' + withFileIOTestDir $ \baseDir -> do let fp = baseDir [osp|foo|] OSP.writeFile fp "h" !h <- OSP.openFile fp ReadMode @@ -143,51 +133,45 @@ concFile2 = do concFile3 :: Assertion concFile3 = do - withSystemTempDirectory "test" $ \baseDir' -> do - baseDir <- OSP.encodeFS baseDir' + withFileIOTestDir $ \baseDir -> do let fp = baseDir [osp|foo|] OSP.writeFile fp "" !h <- OSP.openFile fp ReadMode r <- try @IOException $ OSP.withFile fp WriteMode (flip BS.hPut "test") _ <- try @IOException $ BS.hPut h "" - IOError Nothing fileLockedType "withFile" fileLockedMsg Nothing (Just $ baseDir' FP. "foo") @==? r + IOError Nothing fileLockedType "withFile" fileLockedMsg Nothing (Just $ so $ baseDir [osp|foo|]) @==? r existingFile :: Assertion existingFile = do - withSystemTempDirectory "test" $ \baseDir' -> do - baseDir <- OSP.encodeFS baseDir' + withFileIOTestDir $ \baseDir -> do let fp = baseDir [osp|foo|] r <- try @IOException $ OSP.openExistingFile fp ReadMode - IOError Nothing NoSuchThing "openExistingFile" noSuchFileMsg Nothing (Just $ baseDir' FP. "foo") @==? r + IOError Nothing NoSuchThing "openExistingFile" noSuchFileMsg Nothing (Just $ so $ baseDir [osp|foo|]) @==? r existingFile2 :: Assertion existingFile2 = do - withSystemTempDirectory "test" $ \baseDir' -> do - baseDir <- OSP.encodeFS baseDir' + withFileIOTestDir $ \baseDir -> do let fp = baseDir [osp|foo|] r <- try @IOException $ OSP.openExistingFile fp WriteMode - IOError Nothing NoSuchThing "openExistingFile" noSuchFileMsg Nothing (Just $ baseDir' FP. "foo") @==? r + IOError Nothing NoSuchThing "openExistingFile" noSuchFileMsg Nothing (Just $ so $ baseDir [osp|foo|]) @==? r existingFile3 :: Assertion existingFile3 = do - withSystemTempDirectory "test" $ \baseDir' -> do - baseDir <- OSP.encodeFS baseDir' + withFileIOTestDir $ \baseDir -> do let fp = baseDir [osp|foo|] r <- try @IOException $ OSP.openExistingFile fp AppendMode - IOError Nothing NoSuchThing "openExistingFile" noSuchFileMsg Nothing (Just $ baseDir' FP. "foo") @==? r + IOError Nothing NoSuchThing "openExistingFile" noSuchFileMsg Nothing (Just $ so $ baseDir [osp|foo|]) @==? r existingFile4 :: Assertion existingFile4 = do - withSystemTempDirectory "test" $ \baseDir' -> do - baseDir <- OSP.encodeFS baseDir' + withFileIOTestDir $ \baseDir -> do let fp = baseDir [osp|foo|] r <- try @IOException $ OSP.openExistingFile fp AppendMode - IOError Nothing NoSuchThing "openExistingFile" noSuchFileMsg Nothing (Just $ baseDir' FP. "foo") @==? r + IOError Nothing NoSuchThing "openExistingFile" noSuchFileMsg Nothing (Just $ so $ baseDir [osp|foo|]) @==? r existingFile' :: Assertion existingFile' = do - withSystemTempDirectory "test" $ \baseDir' -> do - baseDir <- OSP.encodeFS baseDir' + withFileIOTestDir $ \baseDir -> do let fp = baseDir [osp|foo|] OSP.writeFile fp "test" r <- try @IOException $ (OSP.openExistingFile fp ReadMode >>= BS.hGetContents) @@ -195,8 +179,7 @@ existingFile' = do existingFile2' :: Assertion existingFile2' = do - withSystemTempDirectory "test" $ \baseDir' -> do - baseDir <- OSP.encodeFS baseDir' + withFileIOTestDir $ \baseDir -> do let fp = baseDir [osp|foo|] OSP.writeFile fp "test" r <- try @IOException $ do @@ -206,8 +189,7 @@ existingFile2' = do existingFile3' :: Assertion existingFile3' = do - withSystemTempDirectory "test" $ \baseDir' -> do - baseDir <- OSP.encodeFS baseDir' + withFileIOTestDir $ \baseDir -> do let fp = baseDir [osp|foo|] OSP.writeFile fp "test" r <- try @IOException $ do @@ -217,8 +199,7 @@ existingFile3' = do existingFile4' :: Assertion existingFile4' = do - withSystemTempDirectory "test" $ \baseDir' -> do - baseDir <- OSP.encodeFS baseDir' + withFileIOTestDir $ \baseDir -> do let fp = baseDir [osp|foo|] OSP.writeFile fp "testx" r <- try @IOException $ @@ -234,8 +215,7 @@ existingFile4' = do openTempFile1 :: (OsPath -> OsString -> IO (OsPath, Handle)) -> Assertion openTempFile1 open = do - withSystemTempDirectory "test" $ \baseDir' -> do - baseDir <- OSP.encodeFS baseDir' + withFileIOTestDir $ \baseDir -> do let file = [osp|foo.ext|] (!fp, h') <- open baseDir file hClose h' @@ -246,8 +226,7 @@ openTempFile1 open = do openTempFile2 :: (OsPath -> OsString -> IO (OsPath, Handle)) -> Assertion openTempFile2 open = do - withSystemTempDirectory "test" $ \baseDir' -> do - baseDir <- OSP.encodeFS baseDir' + withFileIOTestDir $ \baseDir -> do let file = [osp|foo.ext|] (fp, h) <- open baseDir file r <- try @IOException $ do @@ -257,8 +236,7 @@ openTempFile2 open = do openTempFile3 :: (OsPath -> OsString -> IO (OsPath, Handle)) -> Assertion openTempFile3 open = do - withSystemTempDirectory "test" $ \baseDir' -> do - baseDir <- OSP.encodeFS baseDir' + withFileIOTestDir $ \baseDir -> do let file = [osp|foo.ext|] (!fp, h) <- open baseDir file (!fp', h') <- open baseDir file diff --git a/tests/T14.hs b/tests/T14.hs index c1dd1c9..0fcdbe1 100644 --- a/tests/T14.hs +++ b/tests/T14.hs @@ -8,13 +8,12 @@ import Control.Exception import System.OsPath ((), osp) import qualified System.OsPath as OSP import qualified System.File.OsPath as OSP -import System.IO.Temp +import TestUtils -- Test that 'readFile' does not create a file -- https://github.com/hasufell/file-io/issues/14 main :: IO () -main = withSystemTempDirectory "tar-test" $ \baseDir' -> do - baseDir <- OSP.encodeFS baseDir' +main = withFileIOTestDir $ \baseDir -> do res <- try @SomeException $ OSP.readFile (baseDir [osp|foo|]) case res of Left e -> print e >> return () diff --git a/tests/T15.hs b/tests/T15.hs index 4f9634e..5f26ee6 100644 --- a/tests/T15.hs +++ b/tests/T15.hs @@ -9,13 +9,12 @@ import System.OsPath ((), osp) import qualified System.OsPath as OSP import qualified System.File.OsPath as OSP import System.IO -import System.IO.Temp +import TestUtils -- Test that we can read concurrently without file lock -- https://github.com/hasufell/file-io/issues/15 main :: IO () -main = withSystemTempDirectory "tar-test" $ \baseDir' -> do - baseDir <- OSP.encodeFS baseDir' +main = withFileIOTestDir $ \baseDir -> do OSP.writeFile (baseDir [osp|foo|]) "" defaultMain $ testGroup "All" [ testGroup "System.File.OsPath" diff --git a/tests/T15Win.hs b/tests/T15Win.hs index 5b993d1..82ff02f 100644 --- a/tests/T15Win.hs +++ b/tests/T15Win.hs @@ -10,7 +10,7 @@ import Test.Tasty import Test.Tasty.HUnit import qualified System.File.PlatformPath as PFP import System.IO -import System.IO.Temp +import TestUtils import Control.Exception (bracketOnError) import Data.Bits @@ -27,8 +27,7 @@ import GHC.IO.SubSystem -- Test that we can read concurrently without file lock -- https://github.com/hasufell/file-io/issues/15 main :: IO () -main = withSystemTempDirectory "tar-test" $ \baseDir' -> do - baseDir <- WS.encodeFS baseDir' +main = withFileIOTestDir $ \baseDir -> do PFP.writeFile (baseDir WS. [pstr|foo|]) "" defaultMain $ testGroup "All" [ testGroup "System.File.OsPath (Windows)" $ diff --git a/tests/T8.hs b/tests/T8.hs index ed2917a..d0935f4 100644 --- a/tests/T8.hs +++ b/tests/T8.hs @@ -7,16 +7,15 @@ import Control.Concurrent import Control.Monad import System.File.OsPath import System.OsPath -import System.IO.Temp +import TestUtils import qualified Data.ByteString.Lazy as BL import qualified System.OsPath as OSP import qualified System.File.OsPath as OSP main :: IO () -main = withSystemTempDirectory "test" $ \baseDir' -> do +main = withFileIOTestDir $ \baseDir -> do let fn = [osp|test.txt|] - baseDir <- OSP.encodeFS baseDir' let fp = baseDir OSP. fn OSP.writeFile fp "" diff --git a/tests/TestUtils.hsc b/tests/TestUtils.hsc new file mode 100644 index 0000000..6e59f95 --- /dev/null +++ b/tests/TestUtils.hsc @@ -0,0 +1,413 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE CApiFFI #-} +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE PackageImports #-} + +module TestUtils where + +#if !defined(mingw32_HOST_OS) +#include +#include +#endif + +import Control.Monad (when) +import Control.Exception (bracket, displayException, Exception) +import Data.Maybe +import Data.Foldable (sequenceA_) +import System.IO.Error +import GHC.IO.Exception +import Foreign.Ptr +import Foreign.C.String +import Foreign.C.Types +import Foreign.ForeignPtr +import Data.Bits + +import System.OsPath hiding (OsString) +#if MIN_VERSION_filepath(1, 5, 0) +import "os-string" System.OsString.Internal.Types (OsString(OsString), getOsString) +#else +import "filepath" System.OsString.Internal.Types (OsString(OsString), getOsString) +#endif +import System.File.OsPath + +#if defined(mingw32_HOST_OS) || defined(__MINGW32__) +import qualified System.Win32.WindowsString.File as Win32 +import qualified System.Win32.WindowsString.Info as Win32 +import qualified System.Win32.WindowsString.Shell as Win32 +import qualified System.Win32.WindowsString.Time as Win32 +import qualified System.Win32.WindowsString.Types as Win32 +import qualified System.Win32.WindowsString.Console as Win32 +#else +import qualified System.Posix.Env.PosixString as Posix +import qualified System.Posix.PosixPath.FilePath as Posix +import qualified System.Posix.Files as Posix (FileStatus(..)) +import qualified System.Posix.Files.PosixString as Posix +import qualified System.Posix.Types as Posix +import qualified System.Posix.Internals as Posix (CStat) +import qualified System.Posix.Directory.PosixPath as Posix +import qualified System.Posix.IO.PosixString as Posix +import qualified System.Posix.Directory.Fd as Posix +#endif + + + + +withSystemTempDirectory :: OsPath -> (OsPath -> IO a) -> IO a +withSystemTempDirectory template action = do + tmp <- getTemporaryDirectory + bracket + (createTempDirectory tmp template) + (removeDirectoryRecursive) + action + +withFileIOTestDir :: (OsPath -> IO a) -> IO a +withFileIOTestDir action = withSystemTempDirectory [osp|file-io-test|] action + + +-- inlined from directory +-- + +#if defined(mingw32_HOST_OS) || defined(__MINGW32__) +type RawHandle = OsPath +type Metadata = Win32.BY_HANDLE_FILE_INFORMATION +type Mode = Win32.FileAttributeOrFlag + +getTemporaryInternal :: IO OsPath +getTemporaryInternal = OsString <$> Win32.getTemporaryDirectory + +fileTypeFromMetadata :: Metadata -> FileType +fileTypeFromMetadata info + | isLink = if isDir then DirectoryLink else SymbolicLink + | isDir = Directory + | otherwise = File + where + isLink = attrs .&. Win32.fILE_ATTRIBUTE_REPARSE_POINT /= 0 + isDir = attrs .&. Win32.fILE_ATTRIBUTE_DIRECTORY /= 0 + attrs = Win32.bhfiFileAttributes info + +filesAlwaysRemovable :: Bool +filesAlwaysRemovable = False + +setModeAt :: Maybe RawHandle -> OsPath -> Mode -> IO () +setModeAt dir path = setFileMode (pathAt dir path) + +setForceRemoveMode :: Mode -> Mode +setForceRemoveMode m = m .&. complement Win32.fILE_ATTRIBUTE_READONLY + +modeFromMetadata :: Metadata -> Mode +modeFromMetadata = Win32.bhfiFileAttributes + +getMetadataAt :: WhetherFollow -> Maybe RawHandle -> OsPath -> IO Metadata +getMetadataAt NoFollow dir path = getSymbolicLinkMetadata (pathAt dir path) +getMetadataAt FollowLinks dir path = getFileMetadata (pathAt dir path) + +readDirToEnd :: RawHandle -> IO [OsPath] +readDirToEnd = getDirectoryContentsInternal + +openRaw :: WhetherFollow -> Maybe RawHandle -> OsPath -> IO RawHandle +openRaw _ dir path = pure (pathAt dir path) + +closeRaw :: RawHandle -> IO () +closeRaw _ = pure () + +getDirectoryContentsInternal :: OsPath -> IO [OsPath] +getDirectoryContentsInternal path = do + query <- furnishPath (path os "*") + bracket + (Win32.findFirstFile query) + (\ (h, _) -> Win32.findClose h) + (\ (h, fdat) -> loop h fdat []) + where + -- we needn't worry about empty directories: a directory always + -- has at least "." and ".." entries + loop :: Win32.HANDLE -> Win32.FindData -> [OsPath] -> IO [OsPath] + loop h fdat acc = do + filename <- Win32.getFindDataFileName fdat + more <- Win32.findNextFile h fdat + if more + then loop h fdat (OsString filename : acc) + else pure (OsString filename : acc) + -- no need to reverse, ordering is undefined + -- +removePathAt :: FileType -> Maybe RawHandle -> OsPath -> IO () +removePathAt ty dir path = removePathInternal isDir (pathAt dir path) + where isDir = fileTypeIsDirectory ty + +removePathInternal :: Bool -> OsPath -> IO () +removePathInternal isDir path = + (`ioeSetOsPath` path) `modifyIOError` do + furnishPath path + >>= if isDir then Win32.removeDirectory else Win32.deleteFile + +furnishPath :: OsPath -> IO WindowsPath +furnishPath path = + (toExtendedLengthPath <$> rawPrependCurrentDirectory path) + `catchIOError` \ _ -> + pure (getOsString path) + +toExtendedLengthPath :: OsPath -> WindowsPath +toExtendedLengthPath path = + getOsString $ + if isRelative path + then simplifiedPath + else + case toChar <$> simplifiedPath' of + '\\' : '?' : '?' : '\\' : _ -> simplifiedPath + '\\' : '\\' : _ -> simplifiedPath + _ -> os "\\\\?\\" <> simplifiedPath + where simplifiedPath = simplify path + simplifiedPath' = unpack simplifiedPath + +rawPrependCurrentDirectory :: OsPath -> IO OsPath +rawPrependCurrentDirectory path + | isRelative path = + ((`ioeAddLocation` "prependCurrentDirectory") . + (`ioeSetOsPath` path)) `modifyIOError` do + getFullPathName path + | otherwise = pure path + +getFullPathName :: OsPath -> IO OsPath +getFullPathName path = + fromExtendedLengthPath <$> Win32.getFullPathName (toExtendedLengthPath path) + +fromExtendedLengthPath :: WindowsPath -> OsPath +fromExtendedLengthPath ePath' = + case unpack ePath of + c1 : c2 : c3 : c4 : path + | (toChar <$> [c1, c2, c3, c4]) == "\\\\?\\" -> + case path of + c5 : c6 : c7 : subpath@(c8 : _) + | (toChar <$> [c5, c6, c7, c8]) == "UNC\\" -> + pack (c8 : subpath) + drive : col : subpath + -- if the path is not "regular", then the prefix is necessary + -- to ensure the path is interpreted literally + | toChar col == ':', isDriveChar drive, isPathRegular subpath -> + pack path + _ -> ePath + _ -> ePath + where + ePath = OsString ePath' + isDriveChar drive = isAlpha (toChar drive) && isAscii (toChar drive) + isPathRegular path = + not ('/' `elem` (toChar <$> path) || + os "." `elem` splitDirectories (pack path) || + os ".." `elem` splitDirectories (pack path)) + +getSymbolicLinkMetadata :: OsPath -> IO Metadata +getSymbolicLinkMetadata path = + (`ioeSetOsPath` path) `modifyIOError` do + path' <- furnishPath path + let open = Win32.createFile path' 0 maxShareMode Nothing Win32.oPEN_EXISTING + (Win32.fILE_FLAG_BACKUP_SEMANTICS .|. + win32_fILE_FLAG_OPEN_REPARSE_POINT) Nothing + bracket open Win32.closeHandle $ \ h -> do + Win32.getFileInformationByHandle h +#else + +type RawHandle = Posix.Fd +type Metadata = Posix.FileStatus +type Mode = Posix.FileMode + +getTemporaryDirectory :: IO OsPath +getTemporaryDirectory = fromMaybe (os "/tmp") <$> lookupEnvOs (os "TMPDIR") + +lookupEnvOs :: OsString -> IO (Maybe OsString) +lookupEnvOs (OsString name) = (OsString <$>) <$> Posix.getEnv name + +fileTypeFromMetadata :: Metadata -> FileType +fileTypeFromMetadata stat + | isLink = SymbolicLink + | isDir = Directory + | otherwise = File + where + isLink = Posix.isSymbolicLink stat + isDir = Posix.isDirectory stat + +filesAlwaysRemovable :: Bool +filesAlwaysRemovable = True + +foreign import capi "sys/stat.h fchmodat" c_fchmodat + :: Posix.Fd -> CString -> Posix.FileMode -> CInt -> IO CInt + +c_AT_FDCWD :: Posix.Fd +c_AT_FDCWD = Posix.Fd (#const AT_FDCWD) + +setModeAt :: Maybe RawHandle -> OsPath -> Mode -> IO () +setModeAt dir (OsString path) mode = do + Posix.withFilePath path $ \ pPath -> + Posix.throwErrnoPathIfMinus1_ "fchmodat" path $ do + c_fchmodat (fromMaybe c_AT_FDCWD dir) pPath mode 0 + +setForceRemoveMode :: Mode -> Mode +setForceRemoveMode m = m .|. Posix.ownerModes + +modeFromMetadata :: Metadata -> Mode +modeFromMetadata = Posix.fileMode + +foreign import capi "sys/stat.h fstatat" c_fstatat + :: Posix.Fd -> CString -> Ptr Posix.CStat -> CInt -> IO CInt + +getMetadataAt :: WhetherFollow -> Maybe RawHandle -> OsPath -> IO Metadata +getMetadataAt whetherFollow dir (OsString path) = + Posix.withFilePath path $ \ pPath -> do + stat <- mallocForeignPtrBytes (#const sizeof(struct stat)) + withForeignPtr stat $ \ pStat -> do + Posix.throwErrnoPathIfMinus1_ "fstatat" path $ do + c_fstatat (fromMaybe c_AT_FDCWD dir) pPath pStat flags + pure (Posix.FileStatus stat) + where + flags = atWhetherFollow whetherFollow + +c_AT_SYMLINK_NOFOLLOW :: CInt +c_AT_SYMLINK_NOFOLLOW = (#const AT_SYMLINK_NOFOLLOW) + +atWhetherFollow :: WhetherFollow -> CInt +atWhetherFollow NoFollow = c_AT_SYMLINK_NOFOLLOW +atWhetherFollow FollowLinks = 0 + +defaultOpenFlags :: Posix.OpenFileFlags +defaultOpenFlags = + Posix.defaultFileFlags + { Posix.noctty = True + , Posix.nonBlock = True + , Posix.cloexec = True + } + +openRaw :: WhetherFollow -> Maybe RawHandle -> OsPath -> IO RawHandle +openRaw whetherFollow dir (OsString path) = + Posix.openFdAt dir path Posix.ReadOnly flags + where + flags = defaultOpenFlags { Posix.nofollow = isNoFollow whetherFollow } + +closeRaw :: RawHandle -> IO () +closeRaw = Posix.closeFd + +readDirStreamToEnd :: Posix.DirStream -> IO [OsPath] +readDirStreamToEnd stream = loop id + where + loop acc = do + e <- Posix.readDirStream stream + if e == mempty + then pure (acc []) + else loop (acc . (OsString e :)) + +readDirToEnd :: RawHandle -> IO [OsPath] +readDirToEnd fd = + bracket (openDirFromFd fd) Posix.closeDirStream readDirStreamToEnd + +openDirFromFd :: Posix.Fd -> IO Posix.DirStream +openDirFromFd fd = Posix.unsafeOpenDirStreamFd =<< Posix.dup fd + +removePathAt :: FileType -> Maybe RawHandle -> OsPath -> IO () +removePathAt ty dir (OsString path) = + Posix.withFilePath path $ \ pPath -> do + Posix.throwErrnoPathIfMinus1_ "unlinkat" path + (c_unlinkat (fromMaybe c_AT_FDCWD dir) pPath flag) + pure () + where + flag | fileTypeIsDirectory ty = (#const AT_REMOVEDIR) + | otherwise = 0 + +getSymbolicLinkMetadata :: OsPath -> IO Metadata +getSymbolicLinkMetadata = Posix.getSymbolicLinkStatus . getOsString + +foreign import ccall "unistd.h unlinkat" c_unlinkat + :: Posix.Fd -> CString -> CInt -> IO CInt +#endif + + +rightOrError :: Exception e => Either e a -> a +rightOrError (Left e) = error (displayException e) +rightOrError (Right a) = a + +-- | Fallibly converts String to OsString. Only intended to be used on literals. +os :: String -> OsString +os = rightOrError . encodeUtf + +data FileType = File + | SymbolicLink -- ^ POSIX: either file or directory link; Windows: file link + | Directory + | DirectoryLink -- ^ Windows only: directory link + deriving (Bounded, Enum, Eq, Ord, Read, Show) + +fileTypeIsDirectory :: FileType -> Bool +fileTypeIsDirectory Directory = True +fileTypeIsDirectory DirectoryLink = True +fileTypeIsDirectory _ = False + +-- | Return whether the given 'FileType' is a link. +fileTypeIsLink :: FileType -> Bool +fileTypeIsLink SymbolicLink = True +fileTypeIsLink DirectoryLink = True +fileTypeIsLink _ = False + +type Preremover = Maybe RawHandle -> OsPath -> Metadata -> IO () + +noPreremover :: Preremover +noPreremover _ _ _ = pure () + +forcePreremover :: Preremover +forcePreremover dir path metadata = do + when (fileTypeIsDirectory (fileTypeFromMetadata metadata) + || not filesAlwaysRemovable) $ do + setModeAt dir path mode + `catchIOError` \ _ -> pure () + where + mode = setForceRemoveMode (modeFromMetadata metadata) + +removeRecursivelyAt + :: (IO () -> IO ()) + -> ([IO ()] -> IO ()) + -> Preremover + -> Maybe RawHandle + -> OsPath + -> IO () +removeRecursivelyAt catcher sequencer preremover dir name = catcher $ do + metadata <- getMetadataAt NoFollow dir name + preremover dir name metadata + let + fileType = fileTypeFromMetadata metadata + subremovals = do + when (fileType == Directory) $ do + bracket (openRaw NoFollow dir name) closeRaw $ \ handle -> do + -- dropSpecialDotDirs is extremely important! Otherwise it will + -- recurse into the parent directory and wreak havoc. + names <- dropSpecialDotDirs <$> readDirToEnd handle + sequencer (recurse (Just handle) <$> names) + sequencer [subremovals, removePathAt fileType dir name] + where recurse = removeRecursivelyAt catcher sequencer preremover + +-- | @'removeDirectoryRecursive' dir@ removes an existing directory /dir/ +-- together with its contents and subdirectories. Within this directory, +-- symbolic links are removed without affecting their targets. +-- +-- On Windows, the operation fails if /dir/ is a directory symbolic link. +-- +-- This operation is reported to be flaky on Windows so retry logic may +-- be advisable. See: https://github.com/haskell/directory/pull/108 +removeDirectoryRecursive :: OsPath -> IO () +removeDirectoryRecursive path = do + m <- getSymbolicLinkMetadata path + case fileTypeFromMetadata m of + Directory -> + removeRecursivelyAt id sequenceA_ noPreremover Nothing path + DirectoryLink -> + ioError (err `ioeSetErrorString` "is a directory symbolic link") + _ -> + ioError (err `ioeSetErrorString` "not a directory") + where err = mkIOError InappropriateType "" Nothing Nothing + +data WhetherFollow = NoFollow | FollowLinks deriving (Show) + +isNoFollow :: WhetherFollow -> Bool +isNoFollow NoFollow = True +isNoFollow FollowLinks = False + +dropSpecialDotDirs :: [OsPath] -> [OsPath] +dropSpecialDotDirs = filter f + where f filename = filename /= os "." && filename /= os ".." + +-- | Fallibly converts OsString to String. Only intended to be used on literals. +so :: OsString -> String +so = rightOrError . decodeUtf