diff --git a/effectful/CHANGELOG.md b/effectful/CHANGELOG.md index feefec40..33cb0038 100644 --- a/effectful/CHANGELOG.md +++ b/effectful/CHANGELOG.md @@ -1,4 +1,8 @@ # effectful-2.7.0.0 (2026-??-??) +* Add `OsPath` variants of the `FilePath` based APIs + (`Effectful.FileSystem.OsPath` for `System.Directory.OsPath` and + `Effectful.FileSystem.File.OsPath` for `System.File.OsPath` from the + `file-io` package). * `runInBoundThread` and `runInUnboundThread` from `Effectful.Concurrent` no longer run the computation in a cloned environment, so changes to thread-local effects made within are no longer discarded. diff --git a/effectful/effectful.cabal b/effectful/effectful.cabal index 4f2bef4c..9c5d79d0 100644 --- a/effectful/effectful.cabal +++ b/effectful/effectful.cabal @@ -59,8 +59,10 @@ library build-depends: base >= 4.18 && < 5 , async >= 2.2.5 , bytestring >= 0.10 - , directory >= 1.3.2 + , directory >= 1.3.8 , effectful-core >= 2.7.0.0 && < 2.7.1.0 + , file-io >= 0.1.4 + , filepath >= 1.4.100 , process >= 1.6.9 , strict-mutable-base >= 1.1.0.0 , time >= 1.9.2 @@ -83,11 +85,13 @@ library Effectful.Console.ByteString.Lazy Effectful.Environment Effectful.FileSystem + Effectful.FileSystem.File.OsPath Effectful.FileSystem.IO Effectful.FileSystem.IO.ByteString Effectful.FileSystem.IO.ByteString.Builder Effectful.FileSystem.IO.ByteString.Lazy Effectful.FileSystem.IO.File + Effectful.FileSystem.OsPath Effectful.Prim.IORef Effectful.Prim.IORef.Strict Effectful.Process @@ -143,10 +147,12 @@ test-suite test ghc-options: -threaded -rtsopts -with-rtsopts=-N4 build-depends: base + , bytestring , containers , effectful , effectful-core , exceptions + , filepath , lifted-base , primitive , random @@ -166,6 +172,7 @@ test-suite test EnvTests EnvironmentTests ErrorTests + FileSystemTests InputTests LabeledTests NonDetTests diff --git a/effectful/src/Effectful/FileSystem/File/OsPath.hs b/effectful/src/Effectful/FileSystem/File/OsPath.hs new file mode 100644 index 00000000..5866970d --- /dev/null +++ b/effectful/src/Effectful/FileSystem/File/OsPath.hs @@ -0,0 +1,150 @@ +-- | Lifted "System.File.OsPath". +module Effectful.FileSystem.File.OsPath + ( -- * Effect + FileSystem + + -- ** Handlers + , runFileSystem + + -- * Files + , IOMode (..) + , Handle + , openBinaryFile + , withFile + , withBinaryFile + , withFile' + , withBinaryFile' + , readFile + , readFile' + , writeFile + , writeFile' + , appendFile + , appendFile' + , openFile + , openExistingFile + , openTempFile + , openBinaryTempFile + , openTempFileWithDefaultPermissions + , openBinaryTempFileWithDefaultPermissions + ) where + +import Data.ByteString (ByteString) +import Data.ByteString.Lazy qualified as BSL +import Prelude hiding (appendFile, readFile, writeFile) +import System.File.OsPath qualified as F +import System.IO (Handle, IOMode (..)) +import System.OsPath (OsPath, OsString) + +import Effectful +import Effectful.Dispatch.Static +import Effectful.FileSystem.Effect + +-- | Lifted 'F.openBinaryFile'. +openBinaryFile :: FileSystem :> es => OsPath -> IOMode -> Eff es Handle +openBinaryFile path = unsafeEff_ . F.openBinaryFile path + +-- | Lifted 'F.withFile'. +withFile + :: FileSystem :> es + => OsPath + -> IOMode + -> (Handle -> Eff es a) + -> Eff es a +withFile path mode inner = unsafeSeqUnliftIO $ \unlift -> do + F.withFile path mode $ unlift . inner + +-- | Lifted 'F.withBinaryFile'. +withBinaryFile + :: FileSystem :> es + => OsPath + -> IOMode + -> (Handle -> Eff es a) + -> Eff es a +withBinaryFile path mode inner = unsafeSeqUnliftIO $ \unlift -> do + F.withBinaryFile path mode $ unlift . inner + +-- | Lifted 'F.withFile''. +withFile' + :: FileSystem :> es + => OsPath + -> IOMode + -> (Handle -> Eff es a) + -> Eff es a +withFile' path mode inner = unsafeSeqUnliftIO $ \unlift -> do + F.withFile' path mode $ unlift . inner + +-- | Lifted 'F.withBinaryFile''. +withBinaryFile' + :: FileSystem :> es + => OsPath + -> IOMode + -> (Handle -> Eff es a) + -> Eff es a +withBinaryFile' path mode inner = unsafeSeqUnliftIO $ \unlift -> do + F.withBinaryFile' path mode $ unlift . inner + +-- | Lifted 'F.readFile'. +readFile :: FileSystem :> es => OsPath -> Eff es BSL.ByteString +readFile = unsafeEff_ . F.readFile + +-- | Lifted 'F.readFile''. +readFile' :: FileSystem :> es => OsPath -> Eff es ByteString +readFile' = unsafeEff_ . F.readFile' + +-- | Lifted 'F.writeFile'. +writeFile :: FileSystem :> es => OsPath -> BSL.ByteString -> Eff es () +writeFile path = unsafeEff_ . F.writeFile path + +-- | Lifted 'F.writeFile''. +writeFile' :: FileSystem :> es => OsPath -> ByteString -> Eff es () +writeFile' path = unsafeEff_ . F.writeFile' path + +-- | Lifted 'F.appendFile'. +appendFile :: FileSystem :> es => OsPath -> BSL.ByteString -> Eff es () +appendFile path = unsafeEff_ . F.appendFile path + +-- | Lifted 'F.appendFile''. +appendFile' :: FileSystem :> es => OsPath -> ByteString -> Eff es () +appendFile' path = unsafeEff_ . F.appendFile' path + +-- | Lifted 'F.openFile'. +openFile :: FileSystem :> es => OsPath -> IOMode -> Eff es Handle +openFile path = unsafeEff_ . F.openFile path + +-- | Lifted 'F.openExistingFile'. +openExistingFile :: FileSystem :> es => OsPath -> IOMode -> Eff es Handle +openExistingFile path = unsafeEff_ . F.openExistingFile path + +-- | Lifted 'F.openTempFile'. +openTempFile + :: FileSystem :> es + => OsPath + -> OsString + -> Eff es (OsPath, Handle) +openTempFile dir = unsafeEff_ . F.openTempFile dir + +-- | Lifted 'F.openBinaryTempFile'. +openBinaryTempFile + :: FileSystem :> es + => OsPath + -> OsString + -> Eff es (OsPath, Handle) +openBinaryTempFile dir = unsafeEff_ . F.openBinaryTempFile dir + +-- | Lifted 'F.openTempFileWithDefaultPermissions'. +openTempFileWithDefaultPermissions + :: FileSystem :> es + => OsPath + -> OsString + -> Eff es (OsPath, Handle) +openTempFileWithDefaultPermissions dir = + unsafeEff_ . F.openTempFileWithDefaultPermissions dir + +-- | Lifted 'F.openBinaryTempFileWithDefaultPermissions'. +openBinaryTempFileWithDefaultPermissions + :: FileSystem :> es + => OsPath + -> OsString + -> Eff es (OsPath, Handle) +openBinaryTempFileWithDefaultPermissions dir = + unsafeEff_ . F.openBinaryTempFileWithDefaultPermissions dir diff --git a/effectful/src/Effectful/FileSystem/OsPath.hs b/effectful/src/Effectful/FileSystem/OsPath.hs new file mode 100644 index 00000000..4cf5aeb0 --- /dev/null +++ b/effectful/src/Effectful/FileSystem/OsPath.hs @@ -0,0 +1,345 @@ +-- | Lifted "System.Directory.OsPath". +module Effectful.FileSystem.OsPath + ( -- * Effect + FileSystem + + -- ** Handlers + , runFileSystem + + -- * Actions on directories + , createDirectory + , createDirectoryIfMissing + , removeDirectory + , removeDirectoryRecursive + , removePathForcibly + , renameDirectory + , listDirectory + , getDirectoryContents + + -- ** Current working directory + , getCurrentDirectory + , setCurrentDirectory + , withCurrentDirectory + + -- * Pre-defined directories + , getHomeDirectory + , getXdgDirectory + , getXdgDirectoryList + , getAppUserDataDirectory + , getUserDocumentsDirectory + , getTemporaryDirectory + + -- * Actions on files + , removeFile + , renameFile + , renamePath + , copyFile + , copyFileWithMetadata + , getFileSize + , canonicalizePath + , makeAbsolute + , makeRelativeToCurrentDirectory + + -- * Existence tests + , doesPathExist + , doesFileExist + , doesDirectoryExist + , findExecutable + , findExecutables + , findExecutablesInDirectories + , findFile + , findFiles + , findFileWith + , findFilesWith + + -- * Symbolic links + , createFileLink + , createDirectoryLink + , removeDirectoryLink + , pathIsSymbolicLink + , getSymbolicLinkTarget + + -- * Permissions + , getPermissions + , setPermissions + , copyPermissions + + -- * Timestamps + , getAccessTime + , getModificationTime + , setAccessTime + , setModificationTime + + -- * Re-exports + + -- ** Pre-defined directories + , D.XdgDirectory(..) + , D.XdgDirectoryList(..) + + -- ** Existence tests + , D.exeExtension + + -- ** Permissions + , D.Permissions + , D.emptyPermissions + , D.readable + , D.writable + , D.executable + , D.searchable + , D.setOwnerReadable + , D.setOwnerWritable + , D.setOwnerExecutable + , D.setOwnerSearchable + ) where + +import Data.Time (UTCTime) +import System.Directory.OsPath qualified as D +import System.OsPath (OsPath, OsString) + +import Effectful +import Effectful.Dispatch.Static +import Effectful.FileSystem.Effect + +---------------------------------------- +-- Actions on directories + +-- | Lifted 'D.createDirectory'. +createDirectory :: FileSystem :> es => OsPath -> Eff es () +createDirectory = unsafeEff_ . D.createDirectory + +-- | Lifted 'D.createDirectoryIfMissing'. +createDirectoryIfMissing :: FileSystem :> es => Bool -> OsPath -> Eff es () +createDirectoryIfMissing doCreateParents = + unsafeEff_ . D.createDirectoryIfMissing doCreateParents + +-- | Lifted 'D.removeDirectory'. +removeDirectory :: FileSystem :> es => OsPath -> Eff es () +removeDirectory = unsafeEff_ . D.removeDirectory + +-- | Lifted 'D.removeDirectoryRecursive'. +removeDirectoryRecursive :: FileSystem :> es => OsPath -> Eff es () +removeDirectoryRecursive = unsafeEff_ . D.removeDirectoryRecursive + +-- | Lifted 'D.removePathForcibly'. +removePathForcibly :: FileSystem :> es => OsPath -> Eff es () +removePathForcibly = unsafeEff_ . D.removePathForcibly + +-- | Lifted 'D.renameDirectory'. +renameDirectory :: FileSystem :> es => OsPath -> OsPath -> Eff es () +renameDirectory old = unsafeEff_ . D.renameDirectory old + +-- | Lifted 'D.listDirectory'. +listDirectory :: FileSystem :> es => OsPath -> Eff es [OsPath] +listDirectory = unsafeEff_ . D.listDirectory + +-- | Lifted 'D.getDirectoryContents'. +getDirectoryContents :: FileSystem :> es => OsPath -> Eff es [OsPath] +getDirectoryContents = unsafeEff_ . D.getDirectoryContents + +---------------------------------------- +-- Current working directory + +-- | Lifted 'D.getCurrentDirectory'. +getCurrentDirectory :: FileSystem :> es => Eff es OsPath +getCurrentDirectory = unsafeEff_ D.getCurrentDirectory + +-- | Lifted 'D.setCurrentDirectory'. +setCurrentDirectory :: FileSystem :> es => OsPath -> Eff es () +setCurrentDirectory = unsafeEff_ . D.setCurrentDirectory + +-- | Lifted 'D.withCurrentDirectory'. +withCurrentDirectory :: FileSystem :> es => OsPath -> Eff es a -> Eff es a +withCurrentDirectory path = unsafeLiftMapIO (D.withCurrentDirectory path) + +---------------------------------------- +-- Pre-defined directories + +-- | Lifted 'D.getHomeDirectory'. +getHomeDirectory :: FileSystem :> es => Eff es OsPath +getHomeDirectory = unsafeEff_ D.getHomeDirectory + +-- | Lifted 'D.getXdgDirectory'. +getXdgDirectory + :: FileSystem :> es + => D.XdgDirectory + -> OsPath + -> Eff es OsPath +getXdgDirectory xdgDir = unsafeEff_ . D.getXdgDirectory xdgDir + +-- | Lifted 'D.getXdgDirectoryList'. +getXdgDirectoryList + :: FileSystem :> es + => D.XdgDirectoryList + -> Eff es [OsPath] +getXdgDirectoryList = unsafeEff_ . D.getXdgDirectoryList + +-- | Lifted 'D.getAppUserDataDirectory'. +getAppUserDataDirectory :: FileSystem :> es => OsPath -> Eff es OsPath +getAppUserDataDirectory = unsafeEff_ . D.getAppUserDataDirectory + +-- | Lifted 'D.getUserDocumentsDirectory'. +getUserDocumentsDirectory :: FileSystem :> es => Eff es OsPath +getUserDocumentsDirectory = unsafeEff_ D.getUserDocumentsDirectory + +-- | Lifted 'D.getTemporaryDirectory'. +getTemporaryDirectory :: FileSystem :> es => Eff es OsPath +getTemporaryDirectory = unsafeEff_ D.getTemporaryDirectory + +---------------------------------------- +-- Actions on files + +-- | Lifted 'D.removeFile'. +removeFile :: FileSystem :> es => OsPath -> Eff es () +removeFile = unsafeEff_ . D.removeFile + +-- | Lifted 'D.renameFile'. +renameFile :: FileSystem :> es => OsPath -> OsPath -> Eff es () +renameFile old = unsafeEff_ . D.renameFile old + +-- | Lifted 'D.renamePath'. +renamePath :: FileSystem :> es => OsPath -> OsPath -> Eff es () +renamePath old = unsafeEff_ . D.renamePath old + +-- | Lifted 'D.copyFile'. +copyFile :: FileSystem :> es => OsPath -> OsPath -> Eff es () +copyFile src = unsafeEff_ . D.copyFile src + +-- | Lifted 'D.copyFileWithMetadata'. +copyFileWithMetadata :: FileSystem :> es => OsPath -> OsPath -> Eff es () +copyFileWithMetadata src = unsafeEff_ . D.copyFileWithMetadata src + +-- | Lifted 'D.getFileSize'. +getFileSize :: FileSystem :> es => OsPath -> Eff es Integer +getFileSize = unsafeEff_ . D.getFileSize + +-- | Lifted 'D.canonicalizePath'. +canonicalizePath :: FileSystem :> es => OsPath -> Eff es OsPath +canonicalizePath = unsafeEff_ . D.canonicalizePath + +-- | Lifted 'D.makeAbsolute'. +makeAbsolute :: FileSystem :> es => OsPath -> Eff es OsPath +makeAbsolute = unsafeEff_ . D.makeAbsolute + +-- | Lifted 'D.makeRelativeToCurrentDirectory'. +makeRelativeToCurrentDirectory + :: FileSystem :> es + => OsPath + -> Eff es OsPath +makeRelativeToCurrentDirectory = unsafeEff_ . D.makeRelativeToCurrentDirectory + +---------------------------------------- +-- Existence tests + +-- | Lifted 'D.doesPathExist'. +doesPathExist :: FileSystem :> es => OsPath -> Eff es Bool +doesPathExist = unsafeEff_ . D.doesPathExist + +-- | Lifted 'D.doesFileExist'. +doesFileExist :: FileSystem :> es => OsPath -> Eff es Bool +doesFileExist = unsafeEff_ . D.doesFileExist + +-- | Lifted 'D.doesDirectoryExist'. +doesDirectoryExist :: FileSystem :> es => OsPath -> Eff es Bool +doesDirectoryExist = unsafeEff_ . D.doesDirectoryExist + +-- | Lifted 'D.findExecutable'. +findExecutable :: FileSystem :> es => OsString -> Eff es (Maybe OsPath) +findExecutable = unsafeEff_ . D.findExecutable + +-- | Lifted 'D.findExecutables'. +findExecutables :: FileSystem :> es => OsString -> Eff es [OsPath] +findExecutables = unsafeEff_ . D.findExecutables + +-- | Lifted 'D.findExecutablesInDirectories'. +findExecutablesInDirectories + :: FileSystem :> es + => [OsPath] + -> OsString + -> Eff es [OsPath] +findExecutablesInDirectories dirs = + unsafeEff_ . D.findExecutablesInDirectories dirs + +-- | Lifted 'D.findFile'. +findFile :: FileSystem :> es => [OsPath] -> OsString -> Eff es (Maybe OsPath) +findFile dirs = unsafeEff_ . D.findFile dirs + +-- | Lifted 'D.findFiles'. +findFiles :: FileSystem :> es => [OsPath] -> OsString -> Eff es [OsPath] +findFiles dirs = unsafeEff_ . D.findFiles dirs + +-- | Lifted 'D.findFileWith'. +findFileWith + :: FileSystem :> es + => (OsPath -> Eff es Bool) + -> [OsPath] + -> OsString + -> Eff es (Maybe OsPath) +findFileWith p dirs n = unsafeSeqUnliftIO $ \unlift -> do + D.findFileWith (unlift . p) dirs n + +-- | Lifted 'D.findFilesWith'. +findFilesWith + :: FileSystem :> es + => (OsPath -> Eff es Bool) + -> [OsPath] + -> OsString + -> Eff es [OsPath] +findFilesWith p dirs ns = unsafeSeqUnliftIO $ \unlift -> do + D.findFilesWith (unlift . p) dirs ns + +---------------------------------------- +-- Symbolic links + +-- | Lifted 'D.createFileLink'. +createFileLink :: FileSystem :> es => OsPath -> OsPath -> Eff es () +createFileLink target = unsafeEff_ . D.createFileLink target + +-- | Lifted 'D.createDirectoryLink'. +createDirectoryLink :: FileSystem :> es => OsPath -> OsPath -> Eff es () +createDirectoryLink target = unsafeEff_ . D.createDirectoryLink target + +-- | Lifted 'D.removeDirectoryLink'. +removeDirectoryLink :: FileSystem :> es => OsPath -> Eff es () +removeDirectoryLink = unsafeEff_ . D.removeDirectoryLink + +-- | Lifted 'D.pathIsSymbolicLink'. +pathIsSymbolicLink :: FileSystem :> es => OsPath -> Eff es Bool +pathIsSymbolicLink = unsafeEff_ . D.pathIsSymbolicLink + +-- | Lifted 'D.getSymbolicLinkTarget'. +getSymbolicLinkTarget :: FileSystem :> es => OsPath -> Eff es OsPath +getSymbolicLinkTarget = unsafeEff_ . D.getSymbolicLinkTarget + +---------------------------------------- +-- Permissions + +-- | Lifted 'D.getPermissions'. +getPermissions :: FileSystem :> es => OsPath -> Eff es D.Permissions +getPermissions = unsafeEff_ . D.getPermissions + +-- | Lifted 'D.setPermissions'. +setPermissions :: FileSystem :> es => OsPath -> D.Permissions -> Eff es () +setPermissions path = unsafeEff_ . D.setPermissions path + +-- | Lifted 'D.copyPermissions'. +copyPermissions :: FileSystem :> es => OsPath -> OsPath -> Eff es () +copyPermissions src = unsafeEff_ . D.copyPermissions src + +---------------------------------------- +-- Timestamps + +-- | Lifted 'D.getAccessTime'. +getAccessTime :: FileSystem :> es => OsPath -> Eff es UTCTime +getAccessTime = unsafeEff_ . D.getAccessTime + +-- | Lifted 'D.getModificationTime'. +getModificationTime :: FileSystem :> es => OsPath -> Eff es UTCTime +getModificationTime = unsafeEff_ . D.getModificationTime + +-- | Lifted 'D.setAccessTime'. +setAccessTime :: FileSystem :> es => OsPath -> UTCTime -> Eff es () +setAccessTime path = unsafeEff_ . D.setAccessTime path + +-- | Lifted 'D.setModificationTime'. +setModificationTime :: FileSystem :> es => OsPath -> UTCTime -> Eff es () +setModificationTime path = unsafeEff_ . D.setModificationTime path diff --git a/effectful/tests/FileSystemTests.hs b/effectful/tests/FileSystemTests.hs new file mode 100644 index 00000000..14897a8f --- /dev/null +++ b/effectful/tests/FileSystemTests.hs @@ -0,0 +1,53 @@ +module FileSystemTests (fileSystemTests) where + +import Data.ByteString.Char8 qualified as BS8 +import System.OsPath (()) +import System.OsPath qualified as OsPath +import Test.Tasty +import Test.Tasty.HUnit + +import Effectful +import Effectful.FileSystem.File.OsPath qualified as F +import Effectful.FileSystem.OsPath +import Effectful.Temporary +import Utils qualified as U + +fileSystemTests :: TestTree +fileSystemTests = testGroup "FileSystem" + [ testCase "OsPath directory operations" test_directoryOperations + , testCase "OsPath file operations" test_fileOperations + ] + +test_directoryOperations :: Assertion +test_directoryOperations = runEff . runFileSystem . runTemporary $ do + withSystemTempDirectory "effectful" $ \tmpDirFp -> do + tmpDir <- OsPath.encodeUtf tmpDirFp + subDirName <- OsPath.encodeUtf "subdir" + let subDir = tmpDir subDirName + createDirectory subDir + subDirExists <- doesDirectoryExist subDir + U.assertBool "subdirectory was not created" subDirExists + newDirName <- OsPath.encodeUtf "newdir" + let newDir = tmpDir newDirName + renameDirectory subDir newDir + contents <- listDirectory tmpDir + U.assertEqual "unexpected directory contents" [newDirName] contents + removeDirectory newDir + newDirExists <- doesDirectoryExist newDir + U.assertBool "directory was not removed" (not newDirExists) + +test_fileOperations :: Assertion +test_fileOperations = runEff . runFileSystem . runTemporary $ do + withSystemTempDirectory "effectful" $ \tmpDirFp -> do + tmpDir <- OsPath.encodeUtf tmpDirFp + fileName <- OsPath.encodeUtf "test.txt" + let file = tmpDir fileName + F.writeFile' file (BS8.pack "hello") + F.appendFile' file (BS8.pack " world") + contents <- F.readFile' file + U.assertEqual "unexpected file contents" (BS8.pack "hello world") contents + size <- getFileSize file + U.assertEqual "unexpected file size" 11 size + removeFile file + fileExists <- doesFileExist file + U.assertBool "file was not removed" (not fileExists) diff --git a/effectful/tests/Main.hs b/effectful/tests/Main.hs index f6ab13f3..9145e807 100644 --- a/effectful/tests/Main.hs +++ b/effectful/tests/Main.hs @@ -7,6 +7,7 @@ import ConcurrencyTests import EnvTests import EnvironmentTests import ErrorTests +import FileSystemTests import InputTests import LabeledTests import NonDetTests @@ -25,6 +26,7 @@ main = defaultMain $ testGroup "effectful" , envTests , environmentTests , errorTests + , fileSystemTests , inputTests , labeledTests , nonDetTests