From 3a77b83ce6e4752896acf991e5691ecf6dfde573 Mon Sep 17 00:00:00 2001 From: zlonast Date: Sat, 17 May 2025 21:02:13 +0300 Subject: [PATCH 1/5] Add replaceFile --- System/Directory.hs | 1 + System/Directory/Internal/Posix.hsc | 3 ++ System/Directory/Internal/Windows.hsc | 8 ++++ System/Directory/OsPath.hs | 64 +++++++++++++++++++++++++++ directory.cabal | 2 +- 5 files changed, 77 insertions(+), 1 deletion(-) diff --git a/System/Directory.hs b/System/Directory.hs index 052f5716..8a77b790 100644 --- a/System/Directory.hs +++ b/System/Directory.hs @@ -50,6 +50,7 @@ module System.Directory , copyFile , copyFileWithMetadata , getFileSize + , replaceFile , canonicalizePath , makeAbsolute diff --git a/System/Directory/Internal/Posix.hsc b/System/Directory/Internal/Posix.hsc index e4f006db..ac1a69b1 100644 --- a/System/Directory/Internal/Posix.hsc +++ b/System/Directory/Internal/Posix.hsc @@ -85,6 +85,9 @@ removePathInternal False = Posix.removeLink . getOsString renamePathInternal :: OsPath -> OsPath -> IO () renamePathInternal (OsString p1) (OsString p2) = Posix.rename p1 p2 +replaceFileInternal :: OsPath -> OsPath -> Maybe OsPath -> IO () +replaceFileInternal (OsString p1) (OsString p2) _ = Posix.rename p1 p2 + -- On POSIX, the removability of a file is only affected by the attributes of -- the containing directory. filesAlwaysRemovable :: Bool diff --git a/System/Directory/Internal/Windows.hsc b/System/Directory/Internal/Windows.hsc index 3e923f4b..d9f672a0 100644 --- a/System/Directory/Internal/Windows.hsc +++ b/System/Directory/Internal/Windows.hsc @@ -98,6 +98,14 @@ renamePathInternal opath npath = npath' <- furnishPath npath Win32.moveFileEx opath' (Just npath') Win32.mOVEFILE_REPLACE_EXISTING +replaceFileInternal :: OsPath -> OsPath -> Maybe OsPath -> IO () +replaceFileInternal replacedFile replacementFile mBackupFile = + (`ioeSetOsPath` replacedFile) `modifyIOError` do + replacedFile' <- furnishPath replacedFile + replacementFile' <- furnishPath replacementFile + mBackupFile' <- fmap furnishPath mBackupFile + Win32.replaceFile replacedFile' replacementFile' mBackupFile' Win32.rEPLACEFILE_IGNORE_MERGE_ERRORS + -- On Windows, the removability of a file may be affected by the attributes of -- the file itself. filesAlwaysRemovable :: Bool diff --git a/System/Directory/OsPath.hs b/System/Directory/OsPath.hs index 01a9e709..28195cfa 100644 --- a/System/Directory/OsPath.hs +++ b/System/Directory/OsPath.hs @@ -52,6 +52,7 @@ module System.Directory.OsPath , copyFile , copyFileWithMetadata , getFileSize + , replaceFile , canonicalizePath , makeAbsolute @@ -709,6 +710,69 @@ renamePath opath npath = (`ioeAddLocation` "renamePath") `modifyIOError` do renamePathInternal opath npath +-- | 'replaceFile' replaces one file with another file. The replacement file +-- assumes the name of the replaced file and its identity. +-- +-- This operation is atomic. +-- +-- On the unix same as renamePath, on the Windows platform this is ReplaceFileW. +-- +-- The operation on unix may fail with: +-- +-- * @HardwareFault@ +-- A physical I\/O error has occurred. +-- @[EIO]@ +-- +-- * @InvalidArgument@ +-- Either operand is not a valid file name. +-- @[ENAMETOOLONG, ELOOP]@ +-- +-- * 'isDoesNotExistError' +-- The original file does not exist, or there is no path to the target. +-- @[ENOENT, ENOTDIR]@ +-- +-- * 'isPermissionError' +-- The process has insufficient privileges to perform the operation. +-- @[EROFS, EACCES, EPERM]@ +-- +-- * 'System.IO.isFullError' +-- Insufficient resources are available to perform the operation. +-- @[EDQUOT, ENOSPC, ENOMEM, EMLINK]@ +-- +-- * @UnsatisfiedConstraints@ +-- Implementation-dependent constraints are not satisfied. +-- @[EBUSY]@ +-- +-- * @UnsupportedOperation@ +-- The implementation does not support renaming in this situation. +-- @[EXDEV]@ +-- +-- * @InappropriateType@ +-- Either the destination path refers to an existing directory, or one of the +-- parent segments in the destination path is not a directory. +-- @[ENOTDIR, EISDIR, EINVAL, EEXIST, ENOTEMPTY]@ +-- +-- The operation on Windows may fail with: +-- +-- ERROR_UNABLE_TO_MOVE_REPLACEMENT 1176 (0x498) +-- The replacement file could not be renamed. The replaced file no longer exists +-- and the replacement file exists under its original name. +-- +-- ERROR_UNABLE_TO_MOVE_REPLACEMENT_2 1177 (0x499) +-- +-- The replacement file could not be moved. The replacement file still exists +-- under its original name; however, it has inherited the file streams and +-- attributes from the file it is replacing. The file to be replaced still +-- exists with a different name. +-- +-- ERROR_UNABLE_TO_REMOVE_REPLACED 1175 (0x497) +-- The replaced file could not be deleted. The replaced and replacement files +-- retain their original file names. +replaceFile :: OsPath -> OsPath -> IO () +replaceFile opath npath = + (`ioeAddLocation` "replaceFile") `modifyIOError` do + replaceFileInternal opath npath Nothing + -- | Copy a file with its permissions. If the destination file already exists, -- it is replaced atomically. Neither path may refer to an existing -- directory. No exceptions are thrown if the permissions could not be diff --git a/directory.cabal b/directory.cabal index c438c64c..676716eb 100644 --- a/directory.cabal +++ b/directory.cabal @@ -63,7 +63,7 @@ Library file-io >= 0.1.4 && < 0.2, time >= 1.8.0 && < 1.15, if os(windows) - build-depends: Win32 >= 2.14.1.0 && < 2.15 + build-depends: Win32 >= 2.14.2.0 && < 2.15 else build-depends: unix >= 2.8.0 && < 2.9 From 23d4d4eeab90dda2fc6f558026957768ae643cae Mon Sep 17 00:00:00 2001 From: Ilia Baryshnikov Date: Wed, 6 Aug 2025 15:38:17 +0300 Subject: [PATCH 2/5] Fix docs --- System/Directory/OsPath.hs | 90 +++++++++++++++++++++++++++++--------- 1 file changed, 70 insertions(+), 20 deletions(-) diff --git a/System/Directory/OsPath.hs b/System/Directory/OsPath.hs index 28195cfa..03aaddd4 100644 --- a/System/Directory/OsPath.hs +++ b/System/Directory/OsPath.hs @@ -664,12 +664,14 @@ renameFile opath npath = _ -> pure () -- | Rename a file or directory. If the destination path already exists, it --- is replaced atomically. The destination path must not point to an existing --- directory. A conformant implementation need not support renaming files in --- all situations (e.g. renaming across different physical devices), but the --- constraints must be documented. +-- is replaced atomically on unix. If the destination path already exists and +-- destination on the same volume, it is replaced atomically on Windows. +-- The destination path must not point to an existing directory. A conformant +-- implementation need not support renaming files in all situations +-- (e.g. renaming across different physical devices), but the constraints must +-- be documented. -- --- The operation may fail with: +-- The operation on unix may fail with: -- -- * @HardwareFault@ -- A physical I\/O error has occurred. @@ -703,6 +705,35 @@ renameFile opath npath = -- Either the destination path refers to an existing directory, or one of the -- parent segments in the destination path is not a directory. -- @[ENOTDIR, EISDIR, EINVAL, EEXIST, ENOTEMPTY]@ +-- +-- The operation on Windows may fail with: +-- +-- ERROR_FILE_NOT_FOUND 2 (0x2) +-- The system cannot find the specified file. +-- +-- ERROR_PATH_NOT_FOUND 3 (0x3) +-- The system cannot find the specified path. +-- +-- ERROR_ACCESS_DENIED 5 (0x5) +-- Access to the file or resource is denied. +-- +-- ERROR_ALREADY_EXISTS 183 (0xB7) +-- The file already exists and cannot be overwritten or recreated. +-- +-- ERROR_SHARING_VIOLATION 32 (0x20) +-- The file is in use by another process and cannot be accessed. +-- +-- ERROR_NOT_SAME_DEVICE 17 (0x11) +-- The operation cannot be performed across different storage devices. +-- +-- ERROR_INVALID_PARAMETER 87 (0x57) +-- An invalid parameter was passed to the function. +-- +-- ERROR_WRITE_PROTECT 19 (0x13) +-- The storage media is write-protected and cannot be modified. +-- +-- ERROR_LOCK_VIOLATION 33 (0x21) +-- The file is locked by another process and cannot be accessed. renamePath :: OsPath -- ^ Old path -> OsPath -- ^ New path -> IO () @@ -710,10 +741,14 @@ renamePath opath npath = (`ioeAddLocation` "renamePath") `modifyIOError` do renamePathInternal opath npath --- | 'replaceFile' replaces one file with another file. The replacement file --- assumes the name of the replaced file and its identity. --- --- This operation is atomic. +-- | Replaces one file with another file. The replacement file assumes the name +-- of the replaced file and its identity. +-- +-- Note on Windows atomicity: +-- File replacement is typically atomic when both files are on the same volume and +-- no special file system features interfere. If the files are on different volumes, +-- or if a system crash or power failure occurs during the operation, atomicity is +-- not guaranteed and the destination file may be left in an inconsistent state. -- -- On the unix same as renamePath, on the Windows platform this is ReplaceFileW. -- @@ -754,21 +789,36 @@ renamePath opath npath = -- -- The operation on Windows may fail with: -- --- ERROR_UNABLE_TO_MOVE_REPLACEMENT 1176 (0x498) --- The replacement file could not be renamed. The replaced file no longer exists --- and the replacement file exists under its original name. --- --- ERROR_UNABLE_TO_MOVE_REPLACEMENT_2 1177 (0x499) --- --- The replacement file could not be moved. The replacement file still exists --- under its original name; however, it has inherited the file streams and --- attributes from the file it is replacing. The file to be replaced still --- exists with a different name. +-- ERROR_FILE_NOT_FOUND 2 (0x2) +-- The system cannot find the specified file. +-- +-- ERROR_PATH_NOT_FOUND 3 (0x3) +-- The system cannot find the specified path. +-- +-- ERROR_ACCESS_DENIED 5 (0x5) +-- Access to the file or resource is denied. +-- +-- ERROR_SHARING_VIOLATION 32 (0x20) +-- The file is in use by another process and cannot be accessed. +-- +-- ERROR_INVALID_PARAMETER 87 (0x57) +-- An invalid parameter was passed to the function. -- -- ERROR_UNABLE_TO_REMOVE_REPLACED 1175 (0x497) -- The replaced file could not be deleted. The replaced and replacement files -- retain their original file names. -replaceFile :: OsPath -> OsPath -> IO () +-- +-- ERROR_UNABLE_TO_MOVE_REPLACEMENT 1176 (0x498) +-- The replacement file could not be renamed. The replaced file no longer exists +-- and the replacement file remains under its original name. +-- +-- ERROR_UNABLE_TO_MOVE_REPLACEMENT_2 1177 (0x499) +-- The replacement file could not be moved. It still exists under its original name +-- but has inherited attributes from the target file. The original target file +-- persists under a different name. +replaceFile :: OsPath -- ^ File to be replaced + -> OsPath -- ^ Replacement file + -> IO () replaceFile opath npath = (`ioeAddLocation` "replaceFile") `modifyIOError` do replaceFileInternal opath npath Nothing From bc4fd7abd4ccb8fcd9d9df0d5c0bd394e258a9d9 Mon Sep 17 00:00:00 2001 From: Ilia Baryshnikov Date: Wed, 6 Aug 2025 15:49:22 +0300 Subject: [PATCH 3/5] simple unit test --- directory.cabal | 1 + tests/Main.hs | 2 ++ tests/ReplaceFile.hs | 18 ++++++++++++++++++ 3 files changed, 21 insertions(+) create mode 100644 tests/ReplaceFile.hs diff --git a/directory.cabal b/directory.cabal index 676716eb..09ce4a45 100644 --- a/directory.cabal +++ b/directory.cabal @@ -117,6 +117,7 @@ test-suite test RemovePathForcibly RenameDirectory RenameFile001 + ReplaceFile001 RenamePath Simplify T8482 diff --git a/tests/Main.hs b/tests/Main.hs index 227e5727..c24e7527 100644 --- a/tests/Main.hs +++ b/tests/Main.hs @@ -26,6 +26,7 @@ import qualified RemoveDirectoryRecursive001 import qualified RemovePathForcibly import qualified RenameDirectory import qualified RenameFile001 +import qualified ReplaceFile001 import qualified RenamePath import qualified Simplify import qualified T8482 @@ -60,6 +61,7 @@ main = T.testMain $ \ _t -> do T.isolatedRun _t "RemovePathForcibly" RemovePathForcibly.main T.isolatedRun _t "RenameDirectory" RenameDirectory.main T.isolatedRun _t "RenameFile001" RenameFile001.main + T.isolatedRun _t "ReplaceFile001" ReplaceFile001.main T.isolatedRun _t "RenamePath" RenamePath.main T.isolatedRun _t "Simplify" Simplify.main T.isolatedRun _t "T8482" T8482.main diff --git a/tests/ReplaceFile.hs b/tests/ReplaceFile.hs new file mode 100644 index 00000000..6f422738 --- /dev/null +++ b/tests/ReplaceFile.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE CPP #-} +module ReplaceFile001 where +#include "util.inl" +import System.Directory.Internal + +main :: TestEnv -> IO () +main _t = do + writeFile tmp1 contents1 + replaceFile (os tmp1) (os tmp2) + T(expectEq) () contents1 =<< readFile tmp2 + writeFile tmp1 contents2 + replaceFile (os tmp2) (os tmp1) + T(expectEq) () contents1 =<< readFile tmp1 + where + tmp1 = "tmp1" + tmp2 = "tmp2" + contents1 = "test" + contents2 = "test2" From c51a802c742719577519ec01b0dceb395a3616ba Mon Sep 17 00:00:00 2001 From: Ilia Baryshnikov Date: Thu, 7 Aug 2025 22:52:31 +0300 Subject: [PATCH 4/5] try fix ci --- System/Directory.hs | 85 +++++++++++++++++++++++++++++++++++++++++++++ directory.cabal | 2 +- 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/System/Directory.hs b/System/Directory.hs index 8a77b790..0690324d 100644 --- a/System/Directory.hs +++ b/System/Directory.hs @@ -568,6 +568,91 @@ renamePath opath npath = do npath' <- encodeFS npath D.renamePath opath' npath' +-- | Replaces one file with another file. The replacement file assumes the name +-- of the replaced file and its identity. +-- +-- Note on Windows atomicity: +-- File replacement is typically atomic when both files are on the same volume and +-- no special file system features interfere. If the files are on different volumes, +-- or if a system crash or power failure occurs during the operation, atomicity is +-- not guaranteed and the destination file may be left in an inconsistent state. +-- +-- On the unix same as renamePath, on the Windows platform this is ReplaceFileW. +-- +-- The operation on unix may fail with: +-- +-- * @HardwareFault@ +-- A physical I\/O error has occurred. +-- @[EIO]@ +-- +-- * @InvalidArgument@ +-- Either operand is not a valid file name. +-- @[ENAMETOOLONG, ELOOP]@ +-- +-- * 'isDoesNotExistError' +-- The original file does not exist, or there is no path to the target. +-- @[ENOENT, ENOTDIR]@ +-- +-- * 'isPermissionError' +-- The process has insufficient privileges to perform the operation. +-- @[EROFS, EACCES, EPERM]@ +-- +-- * 'System.IO.isFullError' +-- Insufficient resources are available to perform the operation. +-- @[EDQUOT, ENOSPC, ENOMEM, EMLINK]@ +-- +-- * @UnsatisfiedConstraints@ +-- Implementation-dependent constraints are not satisfied. +-- @[EBUSY]@ +-- +-- * @UnsupportedOperation@ +-- The implementation does not support renaming in this situation. +-- @[EXDEV]@ +-- +-- * @InappropriateType@ +-- Either the destination path refers to an existing directory, or one of the +-- parent segments in the destination path is not a directory. +-- @[ENOTDIR, EISDIR, EINVAL, EEXIST, ENOTEMPTY]@ +-- +-- The operation on Windows may fail with: +-- +-- ERROR_FILE_NOT_FOUND 2 (0x2) +-- The system cannot find the specified file. +-- +-- ERROR_PATH_NOT_FOUND 3 (0x3) +-- The system cannot find the specified path. +-- +-- ERROR_ACCESS_DENIED 5 (0x5) +-- Access to the file or resource is denied. +-- +-- ERROR_SHARING_VIOLATION 32 (0x20) +-- The file is in use by another process and cannot be accessed. +-- +-- ERROR_INVALID_PARAMETER 87 (0x57) +-- An invalid parameter was passed to the function. +-- +-- ERROR_UNABLE_TO_REMOVE_REPLACED 1175 (0x497) +-- The replaced file could not be deleted. The replaced and replacement files +-- retain their original file names. +-- +-- ERROR_UNABLE_TO_MOVE_REPLACEMENT 1176 (0x498) +-- The replacement file could not be renamed. The replaced file no longer exists +-- and the replacement file remains under its original name. +-- +-- ERROR_UNABLE_TO_MOVE_REPLACEMENT_2 1177 (0x499) +-- The replacement file could not be moved. It still exists under its original name +-- but has inherited attributes from the target file. The original target file +-- persists under a different name. +-- +-- @since 1.3.10.0 +replaceFile :: FilePath -- ^ File to be replaced + -> FilePath -- ^ Replacement file + -> IO () +replaceFile opath npath = do + opath' <- encodeFS opath + npath' <- encodeFS npath + D.replaceFile opath' npath' + -- | Copy a file with its permissions. If the destination file already exists, -- it is replaced atomically. Neither path may refer to an existing -- directory. No exceptions are thrown if the permissions could not be diff --git a/directory.cabal b/directory.cabal index 09ce4a45..643d5514 100644 --- a/directory.cabal +++ b/directory.cabal @@ -63,7 +63,7 @@ Library file-io >= 0.1.4 && < 0.2, time >= 1.8.0 && < 1.15, if os(windows) - build-depends: Win32 >= 2.14.2.0 && < 2.15 + build-depends: Win32 >= 2.14.2.1 && < 2.15 else build-depends: unix >= 2.8.0 && < 2.9 From e1438af0cb2bd8d7f3bd57db31f3fc1b8bea754c Mon Sep 17 00:00:00 2001 From: Ilia Baryshnikov Date: Thu, 7 Aug 2025 23:11:33 +0300 Subject: [PATCH 5/5] fix test --- tests/{ReplaceFile.hs => ReplaceFile001.hs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{ReplaceFile.hs => ReplaceFile001.hs} (100%) diff --git a/tests/ReplaceFile.hs b/tests/ReplaceFile001.hs similarity index 100% rename from tests/ReplaceFile.hs rename to tests/ReplaceFile001.hs