Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions System/Directory.hs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ module System.Directory
, copyFile
, copyFileWithMetadata
, getFileSize
, replaceFile

, canonicalizePath
, makeAbsolute
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions System/Directory/Internal/Posix.hsc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replaceFileInternal = renamePathInternal


-- On POSIX, the removability of a file is only affected by the attributes of
-- the containing directory.
filesAlwaysRemovable :: Bool
Expand Down
8 changes: 8 additions & 0 deletions System/Directory/Internal/Windows.hsc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
124 changes: 119 additions & 5 deletions System/Directory/OsPath.hs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ module System.Directory.OsPath
, copyFile
, copyFileWithMetadata
, getFileSize
, replaceFile
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the convention is to provide the same set of functions both from System.Directory and from System.Directory.OsPath.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


, canonicalizePath
, makeAbsolute
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -702,13 +705,124 @@ 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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To my understanding, the Haskell Win32 library does not expose the underlying Win32 API error code in the IOException object, so the information here isn't really usable to users. It should really be rephrased as IOErrorTypes instead, and preferably merged with the Unix section if possible to reduce the platform-specific content for users. It's probably not worth documenting every single error case either, just the most commonly encountered ones.

-- 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 ()
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
Expand Down
3 changes: 2 additions & 1 deletion directory.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -117,6 +117,7 @@ test-suite test
RemovePathForcibly
RenameDirectory
RenameFile001
ReplaceFile001
RenamePath
Simplify
T8482
Expand Down
2 changes: 2 additions & 0 deletions tests/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions tests/ReplaceFile001.hs
Original file line number Diff line number Diff line change
@@ -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"
Loading