Skip to content

Commit 18fd4ea

Browse files
committed
First
0 parents  commit 18fd4ea

File tree

7 files changed

+264
-0
lines changed

7 files changed

+264
-0
lines changed

LICENSE

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
Copyright (c) 2015 Justin Leitgeb
2+
3+
Permission is hereby granted, free of charge, to any person obtaining
4+
a copy of this software and associated documentation files (the
5+
"Software"), to deal in the Software without restriction, including
6+
without limitation the rights to use, copy, modify, merge, publish,
7+
distribute, sublicense, and/or sell copies of the Software, and to
8+
permit persons to whom the Software is furnished to do so, subject to
9+
the following conditions:
10+
11+
The above copyright notice and this permission notice shall be included
12+
in all copies or substantial portions of the Software.
13+
14+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
18+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
19+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
20+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

README.md

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Atomic Write
2+
3+
Atomic Write assists with atomic modification of files using
4+
Haskell. It is a wrapper for using the atomic mv(1) operation which
5+
correctly sets permissions based on the original file, or on system
6+
defaults if no file previously exists.
7+
8+
See the
9+
[Haddock documentation](http://hackage.haskell.org/package/atomic-write)
10+
for more information.
11+
12+
## Author
13+
14+
Justin Leitgeb
15+
16+
## License
17+
18+
MIT
19+
20+
## Copyright
21+
22+
(C) 2015 [Stack Builders Inc.](http://www.stackbuilders.com/)

Setup.hs

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import Distribution.Simple
2+
main = defaultMain

atomic-write.cabal

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
name: atomic-write
2+
version: 0.1.0.0
3+
synopsis: Atomically write to a file
4+
5+
description:
6+
.
7+
Atomically write to a file on POSIX-compliant systems while preserving
8+
permissions.
9+
.
10+
On most Unix systems, `mv` is an atomic operation. This makes it simple to write
11+
to a file atomically just by using the mv operation. However, this will
12+
destroy the permissions on the original file. This library does the following
13+
to preserve permissions while atomically writing to a file:
14+
.
15+
* If an original file exists, take those permissions and apply them to the
16+
temp file before `mv`ing the file into place.
17+
* If the original file does not exist, create a following with default
18+
permissions (based on the currently-active umask).
19+
.
20+
This way, when the file is `mv`'ed into place, the permissions will be the ones
21+
held by the original file.
22+
.
23+
This library is based on similar implementations found in common libraries in
24+
Ruby and Python:
25+
.
26+
* <http://apidock.com/rails/File/atomic_write/class Ruby on Rails includes a similar method called atomic_write>
27+
* <https://github.com/chef/chef/blob/c4631816132fcfefaba3d123a1d0dfe8bc2866bb/lib/chef/file_content_management/deploy/mv_unix.rb#L23:L71 Chef includes atomic update functionality>
28+
* <https://github.com/sashka/atomicfile There is a python library for atomically updating a file>
29+
.
30+
Note that at this time Windows is not supported, however we would appreciate
31+
contributions to the <http://github.com/stackbuilders/atomic-write github repository>.
32+
license: MIT
33+
license-file: LICENSE
34+
author: Justin Leitgeb
35+
maintainer: [email protected]
36+
copyright: 2015 Stack Builders Inc.
37+
category: System
38+
build-type: Simple
39+
-- extra-source-files:
40+
cabal-version: >=1.10
41+
42+
library
43+
exposed-modules: System.AtomicWrite
44+
-- other-modules:
45+
-- other-extensions:
46+
build-depends: base >=4.5 && <4.8
47+
, temporary
48+
, unix
49+
, directory
50+
, filepath
51+
52+
hs-source-dirs: src
53+
default-language: Haskell2010
54+
ghc-options: -Wall
55+
56+
test-suite atomic-write-test
57+
type: exitcode-stdio-1.0
58+
hs-source-dirs: spec, src
59+
main-is: Spec.hs
60+
61+
build-depends: base >=4.5 && <4.8
62+
, temporary
63+
, unix
64+
, directory
65+
, filepath
66+
67+
, hspec
68+
69+
default-language: Haskell2010
70+
ghc-options: -Wall
71+
72+
73+
source-repository head
74+
type: git
75+
location: https://github.com/stackbuilders/atomic-file

spec/Spec.hs

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{-# OPTIONS_GHC -F -pgmF hspec-discover #-}

spec/System/AtomicWriteSpec.hs

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
{-# LANGUAGE OverloadedStrings #-}
2+
3+
module System.AtomicWriteSpec (spec) where
4+
5+
import Test.Hspec (it, describe, shouldBe, Spec)
6+
7+
import System.AtomicWrite (atomicWriteFile)
8+
9+
import System.IO.Temp (withSystemTempDirectory)
10+
import System.FilePath.Posix (joinPath)
11+
import System.Posix.Files
12+
(setFileMode, setFileCreationMask, getFileStatus, fileMode)
13+
14+
15+
spec :: Spec
16+
spec = describe "atomicWriteFile" $ do
17+
it "writes contents to a file" $
18+
withSystemTempDirectory "atomicFileTest" $ \tmpDir -> do
19+
20+
let path = joinPath [ tmpDir, "writeTest.tmp" ]
21+
22+
atomicWriteFile path "just testing"
23+
24+
contents <- readFile path
25+
26+
contents `shouldBe` "just testing"
27+
28+
29+
it "preserves the permissions of original file, regardless of umask" $
30+
withSystemTempDirectory "atomicFileTest" $ \tmpDir -> do
31+
let
32+
filePath = joinPath [tmpDir, "testFile"]
33+
34+
writeFile filePath "initial contents"
35+
setFileMode filePath 0o100644
36+
37+
newStat <- getFileStatus filePath
38+
fileMode newStat `shouldBe` 0o100644
39+
40+
-- New files are created with 100600 perms.
41+
_ <- setFileCreationMask 0o100066
42+
43+
-- Create a new file once different mask is set and make sure that mask
44+
-- is applied.
45+
writeFile (joinPath [tmpDir, "sanityCheck"]) "with sanity check mask"
46+
sanityCheckStat <- getFileStatus $ joinPath [tmpDir, "sanityCheck"]
47+
fileMode sanityCheckStat `shouldBe` 0o100600
48+
49+
-- Since we move, this makes the new file assume the filemask of 0600
50+
atomicWriteFile filePath "new contents"
51+
52+
resultStat <- getFileStatus filePath
53+
54+
-- Fails when using atomic mv command unless apply perms on initial file
55+
fileMode resultStat `shouldBe` 0o100644
56+
57+
58+
it "creates a new file with permissions based on active umask" $
59+
withSystemTempDirectory "atomicFileTest" $ \tmpDir -> do
60+
let
61+
filePath = joinPath [tmpDir, "testFile"]
62+
sampleFilePath = joinPath [tmpDir, "sampleFile"]
63+
64+
-- Set somewhat distinctive defaults for test
65+
_ <- setFileCreationMask 0o100171
66+
67+
-- We don't know what the default file permissions are, so create a
68+
-- file to sample them.
69+
writeFile sampleFilePath "I'm being written to sample permissions"
70+
71+
newStat <- getFileStatus sampleFilePath
72+
fileMode newStat `shouldBe` 0o100606
73+
74+
atomicWriteFile filePath "new contents"
75+
76+
resultStat <- getFileStatus filePath
77+
78+
-- The default tempfile permissions are 0600, so this fails unless we
79+
-- make sure that the default umask is relied on for creation of the
80+
-- tempfile.
81+
fileMode resultStat `shouldBe` 0o100606

src/System/AtomicWrite.hs

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
-- |
2+
-- Module : System.AtomicWrite
3+
-- Copyright : (c) 2015 Stack Builders Inc.
4+
--
5+
-- License : MIT
6+
-- Maintainer : [email protected]
7+
-- Stability : experimental
8+
-- Portability : GHC
9+
--
10+
-- A library for atomically modifying files while preserving permissions
11+
--
12+
13+
module System.AtomicWrite (atomicWriteFile) where
14+
15+
import System.Directory (renameFile, doesFileExist)
16+
import System.FilePath.Posix (takeDirectory)
17+
import System.IO
18+
(Handle, openTempFile, openTempFileWithDefaultPermissions, hPutStr, hClose)
19+
20+
import System.Posix.Files (setFileMode, getFileStatus, fileMode)
21+
22+
23+
-- | Creates a file atomically on POSIX-compliant systems while preserving
24+
-- permissions.
25+
atomicWriteFile ::
26+
FilePath -- ^ The path where the file will be updated or created
27+
-> String -- ^ The content to write to the file
28+
-> IO ()
29+
atomicWriteFile f txt = do
30+
(temppath, h) <- tempFileFor f
31+
32+
hPutStr h txt
33+
hClose h
34+
35+
renameFile temppath f
36+
37+
38+
-- | Returns a temporary file with permissions correctly set. chooses
39+
-- either previously-set permissions if the file that we're writing
40+
-- to existed, or permissions following the current umask.
41+
tempFileFor :: FilePath -> IO (FilePath, Handle)
42+
tempFileFor originalFilePath = do
43+
let targetDirectory = takeDirectory originalFilePath
44+
45+
doesFileExist originalFilePath >>=
46+
tmpFile originalFilePath targetDirectory "atomic.write"
47+
48+
where
49+
50+
tmpFile :: FilePath -> FilePath -> String -> Bool -> IO (FilePath, Handle)
51+
tmpFile originalPath workingDirectory template previousExisted =
52+
53+
if previousExisted then do
54+
(temppath, handle) <- openTempFile workingDirectory template
55+
56+
oldStat <- getFileStatus originalPath
57+
58+
setFileMode temppath $ fileMode oldStat
59+
60+
return (temppath, handle)
61+
62+
else
63+
openTempFileWithDefaultPermissions workingDirectory template

0 commit comments

Comments
 (0)