{- Copyright 2016 Joey Hess - - 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"