summaryrefslogtreecommitdiff
path: root/src/Propellor/Property/File.hs
diff options
context:
space:
mode:
Diffstat (limited to 'src/Propellor/Property/File.hs')
-rw-r--r--src/Propellor/Property/File.hs119
1 files changed, 107 insertions, 12 deletions
diff --git a/src/Propellor/Property/File.hs b/src/Propellor/Property/File.hs
index 95fc6f81..3293599a 100644
--- a/src/Propellor/Property/File.hs
+++ b/src/Propellor/Property/File.hs
@@ -1,4 +1,4 @@
-{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE FlexibleInstances, FlexibleContexts #-}
module Propellor.Property.File where
@@ -6,8 +6,10 @@ import Propellor.Base
import Utility.FileMode
import qualified Data.ByteString.Lazy as L
+import Data.List (isInfixOf, isPrefixOf)
import System.Posix.Files
import System.Exit
+import Data.Char
type Line = String
@@ -18,14 +20,42 @@ f `hasContent` newcontent = fileProperty
(\_oldcontent -> newcontent) f
-- | Ensures that a line is present in a file, adding it to the end if not.
+--
+-- For example:
+--
+-- > & "/etc/default/daemon.conf" `File.containsLine` ("cachesize = " ++ val 1024)
+--
+-- The above example uses `val` to serialize a `ConfigurableValue`
containsLine :: FilePath -> Line -> Property UnixLike
f `containsLine` l = f `containsLines` [l]
+-- | Ensures that a list of lines are present in a file, adding any that are not
+-- to the end of the file.
+--
+-- Note that this property does not guarantee that the lines will appear
+-- consecutively, nor in the order specified. If you need either of these, use
+-- 'File.containsBlock'.
containsLines :: FilePath -> [Line] -> Property UnixLike
f `containsLines` ls = fileProperty (f ++ " contains:" ++ show ls) go f
where
go content = content ++ filter (`notElem` content) ls
+-- | Ensures that a block of consecutive lines is present in a file, adding it
+-- to the end if not. Revert to ensure that the block is not present (though
+-- the lines it contains could be present, non-consecutively).
+containsBlock :: FilePath -> [Line] -> RevertableProperty UnixLike UnixLike
+f `containsBlock` ls =
+ fileProperty (f ++ " contains block:" ++ show ls) add f
+ <!> fileProperty (f ++ " lacks block:" ++ show ls) remove f
+ where
+ add content
+ | ls `isInfixOf` content = content
+ | otherwise = content ++ ls
+ remove [] = []
+ remove content@(x:xs)
+ | ls `isPrefixOf` content = remove (drop (length ls) content)
+ | otherwise = x : remove xs
+
-- | Ensures that a line is not present in a file.
-- Note that the file is ensured to exist, so if it doesn't, an empty
-- file will be written.
@@ -75,11 +105,11 @@ hasPrivContent' writemode source f context =
-- | Replaces the content of a file with the transformed content of another file
basedOn :: FilePath -> (FilePath, [Line] -> [Line]) -> Property UnixLike
-f `basedOn` (f', a) = property' desc $ \o -> do
- tmpl <- liftIO $ readFile f'
+f `basedOn` (src, a) = property' desc $ \o -> do
+ tmpl <- liftIO $ readFile src
ensureProperty o $ fileProperty desc (\_ -> a $ lines $ tmpl) f
where
- desc = f ++ " is based on " ++ f'
+ desc = f ++ " is based on " ++ src
-- | Removes a file. Does not remove symlinks or non-plain-files.
notPresent :: FilePath -> Property UnixLike
@@ -120,23 +150,26 @@ link `isSymlinkedTo` (LinkTarget target) = property desc $
-- | Ensures that a file is a copy of another (regular) file.
isCopyOf :: FilePath -> FilePath -> Property UnixLike
-f `isCopyOf` f' = property desc $ go =<< (liftIO $ tryIO $ getFileStatus f')
+f `isCopyOf` src = property desc $ go =<< (liftIO $ tryIO $ getFileStatus src)
where
- desc = f ++ " is copy of " ++ f'
+ desc = f ++ " is copy of " ++ src
go (Right stat) = if isRegularFile stat
- then gocmp =<< (liftIO $ cmp)
- else warningMessage (f' ++ " is not a regular file") >>
+ then ifM (liftIO $ doesFileExist f)
+ ( gocmp =<< (liftIO $ cmp)
+ , doit
+ )
+ else warningMessage (src ++ " is not a regular file") >>
return FailedChange
go (Left e) = warningMessage (show e) >> return FailedChange
- cmp = safeSystem "cmp" [Param "-s", Param "--", File f, File f']
+ cmp = safeSystem "cmp" [Param "-s", Param "--", File f, File src]
gocmp ExitSuccess = noChange
gocmp (ExitFailure 1) = doit
gocmp _ = warningMessage "cmp failed" >> return FailedChange
- doit = makeChange $ copy f' `viaStableTmp` f
- copy src dest = unlessM (runcp src dest) $ errorMessage "cp failed"
- runcp src dest = boolSystem "cp"
+ doit = makeChange $ copy `viaStableTmp` f
+ copy dest = unlessM (runcp dest) $ errorMessage "cp failed"
+ runcp dest = boolSystem "cp"
[Param "--preserve=all", Param "--", File src, File dest]
-- | Ensures that a file/dir has the specified owner and group.
@@ -147,6 +180,20 @@ ownerGroup f (User owner) (Group group) = p `describe` (f ++ " owner " ++ og)
`changesFile` f
og = owner ++ ":" ++ group
+-- | Given a base directory, and a relative path under that
+-- directory, applies a property to each component of the path in turn,
+-- starting with the base directory.
+--
+-- For example, to make a file owned by a user, making sure their home
+-- directory and the subdirectories to it are also owned by them:
+--
+-- > "/home/user/program/file" `hasContent` ["foo"]
+-- > `before` applyPath "/home/user" ".config/program/file"
+-- > (\f -> ownerGroup f (User "user") (Group "user"))
+applyPath :: Monoid (Property metatypes) => FilePath -> FilePath -> (FilePath -> Property metatypes) -> Property metatypes
+applyPath basedir relpath mkp = mconcat $
+ map mkp (scanl (</>) basedir (splitPath relpath))
+
-- | Ensures that a file/dir has the specfied mode.
mode :: FilePath -> FileMode -> Property UnixLike
mode f v = p `changesFile` f
@@ -221,3 +268,51 @@ viaStableTmp a f = bracketIO setup cleanup go
go tmpfile = do
a tmpfile
liftIO $ rename tmpfile f
+
+-- | Generates a base configuration file name from a String, which
+-- can be put in a configuration directory, such as
+-- </etc/apt/sources.list.d/>
+--
+-- The generated file name is limited to using ASCII alphanumerics,
+-- \'_\' and \'.\' , so that programs that only accept a limited set of
+-- characters will accept it. Any other characters will be encoded
+-- in escaped form.
+--
+-- Some file extensions, such as ".old" may be filtered out by
+-- programs that use configuration directories. To avoid such problems,
+-- it's a good idea to add an static prefix and extension to the
+-- result of this function. For example:
+--
+-- > aptConf foo = "/etc/apt/apt.conf.d" </> "propellor_" ++ configFileName foo <.> ".conf"
+configFileName :: String -> FilePath
+configFileName = concatMap escape
+ where
+ escape c
+ | isAscii c && isAlphaNum c = [c]
+ | c == '.' = [c]
+ | otherwise = '_' : show (ord c)
+
+-- | Applies configFileName to any value that can be shown.
+showConfigFileName :: Show v => v -> FilePath
+showConfigFileName = configFileName . show
+
+-- | Inverse of showConfigFileName.
+readConfigFileName :: Read v => FilePath -> Maybe v
+readConfigFileName = readish . unescape
+ where
+ unescape [] = []
+ unescape ('_':cs) = case break (not . isDigit) cs of
+ ([], _) -> '_' : unescape cs
+ (ns, cs') -> case readish ns of
+ Nothing -> '_' : ns ++ unescape cs'
+ Just n -> chr n : unescape cs'
+ unescape (c:cs) = c : unescape cs
+
+data Overwrite = OverwriteExisting | PreserveExisting
+
+-- | When passed PreserveExisting, only ensures the property when the file
+-- does not exist.
+checkOverwrite :: Overwrite -> FilePath -> (FilePath -> Property i) -> Property i
+checkOverwrite OverwriteExisting f mkp = mkp f
+checkOverwrite PreserveExisting f mkp =
+ check (not <$> doesFileExist f) (mkp f)