git-annex/Utility/Tmp.hs
Joey Hess af3b9cbd36
simplify replaceFile using relatedTemplate
Now that truncateFilePath and relatedTemplate have both been optimised,
may as well use them in replaceFile, rather than the custom hack it
used.

Removed the windows-specific ifdef as well, because on Windows long
filepaths no longer really a problem, since ghc and git-annex use UNC
converted paths.

replaceFile no longer checks fileNameLengthLimit. That took a syscall,
and since we have an existing file, we know filenames of its length are
supported by the filesystem. Assuming that the withOtherTmp directory is
on the same filesystem as the file replaceFile is being called on, which
I believe it is.

Sponsored-by: Leon Schuermann
2025-01-22 13:25:06 -04:00

134 lines
4.3 KiB
Haskell

{- Temporary files.
-
- Copyright 2010-2025 Joey Hess <id@joeyh.name>
-
- License: BSD-2-clause
-}
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -fno-warn-tabs #-}
module Utility.Tmp (
Template,
viaTmp,
withTmpFile,
withTmpFileIn,
openTmpFileIn,
relatedTemplate,
relatedTemplate',
) where
import System.IO
import System.Directory
import Control.Monad.IO.Class
import System.IO.Error
import Data.Char
import qualified Data.ByteString as B
import qualified System.FilePath.ByteString as P
import Utility.Exception
import Utility.FileSystemEncoding
import Utility.FileMode
import qualified Utility.RawFilePath as R
import qualified Utility.FileIO as F
import Utility.OsPath
type Template = OsString
{- This is the same as openTempFile, except when there is an
- error, it displays the template as well as the directory,
- to help identify what call was responsible.
-}
openTmpFileIn :: OsPath -> Template -> IO (OsPath, Handle)
openTmpFileIn dir template = F.openTempFile dir template
`catchIO` decoraterrror
where
decoraterrror e = throwM $
let loc = ioeGetLocation e ++ " template " ++ decodeBS (fromOsPath template)
in annotateIOError e loc Nothing Nothing
{- Runs an action like writeFile, writing to a temp file first and
- then moving it into place. The temp file is stored in the same
- directory as the final file to avoid cross-device renames.
-
- While this uses a temp file, the file will end up with the same
- mode as it would when using writeFile, unless the writer action changes
- it.
-}
viaTmp :: (MonadMask m, MonadIO m) => (OsPath -> v -> m ()) -> OsPath -> v -> m ()
viaTmp a file content = bracketIO setup cleanup use
where
(dir, base) = P.splitFileName (fromOsPath file)
template = relatedTemplate (base <> ".tmp")
setup = do
createDirectoryIfMissing True (fromRawFilePath dir)
openTmpFileIn (toOsPath dir) template
cleanup (tmpfile, h) = do
_ <- tryIO $ hClose h
tryIO $ R.removeLink (fromOsPath tmpfile)
use (tmpfile, h) = do
let tmpfile' = fromOsPath tmpfile
-- Make mode the same as if the file were created usually,
-- not as a temp file. (This may fail on some filesystems
-- that don't support file modes well, so ignore
-- exceptions.)
_ <- liftIO $ tryIO $
R.setFileMode (fromOsPath tmpfile)
=<< defaultFileMode
liftIO $ hClose h
a tmpfile content
liftIO $ R.rename tmpfile' (fromOsPath file)
{- Runs an action with a tmp file located in the system's tmp directory
- (or in "." if there is none) then removes the file. -}
withTmpFile :: (MonadIO m, MonadMask m) => Template -> (OsPath -> Handle -> m a) -> m a
withTmpFile template a = do
tmpdir <- liftIO $ catchDefaultIO "." getTemporaryDirectory
withTmpFileIn (toOsPath (toRawFilePath tmpdir)) template a
{- Runs an action with a tmp file located in the specified directory,
- then removes the file.
-
- Note that the tmp file will have a file mode that only allows the
- current user to access it.
-}
withTmpFileIn :: (MonadIO m, MonadMask m) => OsPath -> Template -> (OsPath -> Handle -> m a) -> m a
withTmpFileIn tmpdir template a = bracket create remove use
where
create = liftIO $ openTmpFileIn tmpdir template
remove (name, h) = liftIO $ do
hClose h
tryIO $ R.removeLink (fromOsPath name)
use (name, h) = a name h
{- It's not safe to use a FilePath of an existing file as the template
- for openTempFile, because if the FilePath is really long, the tmpfile
- will be longer, and may exceed the maximum filename length.
-
- This generates a template that is never too long.
-}
relatedTemplate :: RawFilePath -> Template
relatedTemplate = toOsPath . relatedTemplate'
relatedTemplate' :: RawFilePath -> RawFilePath
relatedTemplate' f
| len > templateAddedLength =
{- Some filesystems like FAT have issues with filenames
- ending in ".", so avoid truncating a filename to end
- that way. -}
B.dropWhileEnd (== dot) $
truncateFilePath (len - templateAddedLength) f
| otherwise = f
where
len = B.length f
dot = fromIntegral (ord '.')
{- When a Template is used to create a temporary file, some random bytes
- are appended to it. This is how many such bytes can be added, maximum.
-
- This needs to be as long or longer than the current implementation
- of openTempFile, and some extra has been added to make it longer
- than any likely implementation.
-}
templateAddedLength :: Int
templateAddedLength = 20