keysafe-0.20160819: Storage/Local.hs
{- Copyright 2016 Joey Hess <id@joeyh.name>
-
- Licensed under the GNU AGPL version 3 or higher.
-}
module Storage.Local (localStorage, uploadQueue) where
import Types
import Types.Storage
import Storage.Network (Server(..))
import Serialization ()
import qualified Data.ByteString as B
import qualified Data.ByteString.UTF8 as U8
import Data.Monoid
import Data.List
import System.Posix.User
import System.IO
import System.Directory
import System.Posix
import System.FilePath
import Raaz.Core.Encode
import Control.DeepSeq
import Control.Exception
import Control.Monad
newtype Section = Section String
localStorage :: String -> Storage
localStorage n = Storage
{ storeShare = store section
, retrieveShare = retrieve section
, obscureShares = obscure section
, countShares = count section
, moveShares = move section
}
where
section = Section n
uploadQueue :: Server -> Storage
uploadQueue s = localStorage ("uploadqueue" </> serverName s)
store :: Section -> StorableObjectIdent -> Share -> IO StoreResult
store section i s = onError (StoreFailure . show) $ do
dir <- shareDir section
createDirectoryIfMissing True dir
let dest = dir </> shareFile i
exists <- doesFileExist dest
if exists
then return StoreAlreadyExists
else do
let tmp = dest ++ ".tmp"
fd <- openFd tmp WriteOnly (Just 0o400)
(defaultFileFlags { exclusive = True } )
h <- fdToHandle fd
B.hPut h (toByteString s)
hClose h
renameFile tmp dest
return StoreSuccess
retrieve :: Section -> ShareNum -> StorableObjectIdent -> IO RetrieveResult
retrieve section n i = onError (RetrieveFailure . show) $ do
dir <- shareDir section
fd <- openFd (dir </> shareFile i) ReadOnly Nothing defaultFileFlags
h <- fdToHandle fd
b <- B.hGetContents h
b `deepseq` hClose h
return $ RetrieveSuccess $ Share n (StorableObject b)
-- | Set atime and mtime to epoch, to obscure access and modification
-- patterns.
--
-- There is no way to set the ctime to the epoch, but setting the other
-- times does at least set it to the current time, which makes all
-- currently stored files look alike.
--
-- Note that the contents of shares is never changed, so it's ok to set the
-- mtime to the epoch; backup programs won't be confused.
obscure :: Section -> IO ObscureResult
obscure section = onError (ObscureFailure . show) $ do
dir <- shareDir section
fs <- filter isShareFile <$> getDirectoryContents dir
mapM_ (\f -> setFileTimes (dir </> f) 0 0) fs
return ObscureSuccess
count :: Section -> IO CountResult
count section = onError (CountFailure . show) $ do
dir <- shareDir section
CountResult . genericLength . filter isShareFile
<$> getDirectoryContents dir
move :: Section -> Storage -> IO ()
move section storage = do
dir <- shareDir section
fs <- getDirectoryContents dir
forM_ fs $ \f -> case fromShareFile f of
Nothing -> return ()
Just i -> do
-- Use a dummy share number of 0; it doesn't
-- matter because we're not going to be
-- recombining the share, just sending its contents
-- on the the server.
r <- retrieve section 0 i
case r of
RetrieveFailure _ -> return ()
RetrieveSuccess share -> do
s <- storeShare storage i share
case s of
StoreFailure _ -> return ()
_ -> removeFile f
onError :: (IOException -> a) -> IO a -> IO a
onError f a = do
v <- try a
return $ case v of
Left e -> f e
Right r -> r
shareDir :: Section -> IO FilePath
shareDir (Section section) = do
u <- getUserEntryForID =<< getEffectiveUserID
return $ homeDirectory u </> dotdir </> section
shareFile :: StorableObjectIdent -> FilePath
shareFile i = U8.toString (toByteString i) <> ext
fromShareFile :: FilePath -> Maybe StorableObjectIdent
fromShareFile f
| isShareFile f = fromByteString $ U8.fromString $ dropExtension f
| otherwise = Nothing
isShareFile :: FilePath -> Bool
isShareFile f = ext `isSuffixOf` f
ext :: String
ext = ".keysafe"
dotdir :: FilePath
dotdir = ".keysafe" </> "objects"