packages feed

keysafe-0.20160831: Storage/Local.hs

{- Copyright 2016 Joey Hess <id@joeyh.name>
 -
 - Licensed under the GNU AGPL version 3 or higher.
 -}

module Storage.Local (localStorage, storageDir, testStorageDir, 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

type GetShareDir = Section -> IO FilePath

newtype Section = Section String

localStorage :: GetShareDir -> String -> Storage
localStorage getsharedir n = Storage
	{ storeShare = store section getsharedir
	, retrieveShare = retrieve section getsharedir
	, obscureShares = obscure section getsharedir
	, countShares = count section getsharedir
	, moveShares = move section getsharedir
	}
  where
	section = Section n

uploadQueue :: Maybe LocalStorageDirectory -> Server -> Storage
uploadQueue d s = localStorage (storageDir d) ("uploadqueue" </> serverName s)

store :: Section -> GetShareDir -> StorableObjectIdent -> Share -> IO StoreResult
store section getsharedir i s = onError (StoreFailure . show) $ do
	dir <- getsharedir 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 -> GetShareDir -> ShareNum -> StorableObjectIdent -> IO RetrieveResult
retrieve section getsharedir n i = onError (RetrieveFailure . show) $ do
	dir <- getsharedir 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 -> GetShareDir -> IO ObscureResult
obscure section getsharedir = onError (ObscureFailure . show) $ do
	dir <- getsharedir section
	fs <- filter isShareFile <$> getDirectoryContents dir
	mapM_ (\f -> setFileTimes (dir </> f) 0 0) fs
	return ObscureSuccess

count :: Section -> GetShareDir -> IO CountResult
count section getsharedir = onError (CountFailure . show) $ do
	dir <- getsharedir section
	CountResult . genericLength . filter isShareFile
		<$> getDirectoryContents dir

move :: Section -> GetShareDir -> Storage -> IO ()
move section getsharedir storage = do
	dir <- getsharedir 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 getsharedir 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

storageDir :: Maybe LocalStorageDirectory -> GetShareDir
storageDir Nothing (Section section) = do
	u <- getUserEntryForID =<< getEffectiveUserID
	return $ homeDirectory u </> dotdir </> section
storageDir (Just (LocalStorageDirectory d)) (Section section) =
	pure $ d </> section

testStorageDir :: FilePath -> GetShareDir
testStorageDir tmpdir = storageDir (Just (LocalStorageDirectory tmpdir))

-- | The takeFileName ensures that, if the StorableObjectIdent somehow
-- contains a path (eg starts with "../" or "/"), it is not allowed
-- to point off outside the shareDir.
shareFile :: StorableObjectIdent -> FilePath
shareFile i = takeFileName (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"