diff --git a/System/Directory.hs b/System/Directory.hs index 052f5716..0690324d 100644 --- a/System/Directory.hs +++ b/System/Directory.hs @@ -50,6 +50,7 @@ module System.Directory , copyFile , copyFileWithMetadata , getFileSize + , replaceFile , canonicalizePath , makeAbsolute @@ -567,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/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..03aaddd4 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 @@ -663,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. @@ -702,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 () @@ -709,6 +741,88 @@ renamePath opath npath = (`ioeAddLocation` "renamePath") `modifyIOError` do renamePathInternal 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. +replaceFile :: OsPath -- ^ File to be replaced + -> OsPath -- ^ Replacement file + -> 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..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.1.0 && < 2.15 + build-depends: Win32 >= 2.14.2.1 && < 2.15 else build-depends: unix >= 2.8.0 && < 2.9 @@ -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/ReplaceFile001.hs b/tests/ReplaceFile001.hs new file mode 100644 index 00000000..6f422738 --- /dev/null +++ b/tests/ReplaceFile001.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"