{- Copyright 2016 Joey Hess - - Licensed under the GNU AGPL version 3 or higher. -} module Storage.Local ( localStorage , localStorageOverride , storageDir , storageTopDir , testStorageDir , localDiskUsage ) where import Types import Types.Storage import Output import Serialization () import Utility.UserInfo import Utility.Exception import qualified Data.ByteString as B import qualified Data.ByteString.UTF8 as U8 import Data.List import Data.Maybe import System.IO import System.Directory import System.Posix import System.FilePath import Raaz.Core.Encode import Control.DeepSeq import Control.Monad import System.DiskSpace import Control.Exception (IOException) type GetShareDir = Section -> IO FilePath newtype Section = Section String localStorage :: StorageLevel -> GetShareDir -> String -> Storage localStorage storagelevel getsharedir n = Storage { storeShare = store section getsharedir , retrieveShare = retrieve section getsharedir , obscureShares = obscure section getsharedir , countShares = count section getsharedir , moveShares = move section getsharedir , storageLevel = storagelevel , uploadQueue = Nothing , getServer = Nothing } where section = Section n localStorageOverride :: FilePath -> IO (Maybe Storage) localStorageOverride d = onStorageError' accesserror $ do -- Check that the directory can be written to. createDirectoryIfMissing True d -- Use a filename as long as used for keysafe share files. let f = d "testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttest.keysafe" writeFile f "test" _ <- readFile f removeFile f return $ Just $ localStorage LocallyPreferred (\_ -> pure d) "" where accesserror e = do warn $ "Unable to access local storage directory " ++ d ++ " (" ++ show e ++ ")" return Nothing store :: Section -> GetShareDir -> StorableObjectIdent -> Share -> IO StoreResult store section getsharedir i s = onStorageError (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 = onStorageError (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 = onStorageError (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 = onStorageError (CountFailure . show) $ do dir <- getsharedir section exists <- doesDirectoryExist dir if exists then CountResult . genericLength . filter isShareFile <$> getDirectoryContents dir else return (CountResult 0) move :: Section -> GetShareDir -> Storage -> IO [StoreResult] move section getsharedir storage = do dir <- getsharedir section fs <- map (dir ) <$> catchDefaultIO [] (getDirectoryContents dir) rs <- forM fs $ \f -> case fromShareFile f of Nothing -> return Nothing Just i -> Just <$> go f i return (catMaybes rs) where -- Use a dummy share number of 0; it doesn't -- matter because we're not going to be -- recombining the share here. sharenum = 0 go f i = do r <- retrieve section getsharedir sharenum i case r of RetrieveFailure e -> return (StoreFailure e) RetrieveSuccess share -> do s <- storeShare storage i share case s of StoreSuccess -> movesuccess f StoreAlreadyExists -> alreadyexists share i f StoreFailure e -> return (StoreFailure e) movesuccess f = do removeFile f return StoreSuccess -- Detect case where the same data is already -- stored on the other storage. alreadyexists share i f = do check <- retrieveShare storage sharenum i case check of RetrieveSuccess share' | share' == share -> movesuccess f _ -> return StoreAlreadyExists onStorageError :: (IOException -> a) -> IO a -> IO a onStorageError f = onStorageError' (pure . f) onStorageError' :: (IOException -> IO a) -> IO a -> IO a onStorageError' f a = do v <- try a case v of Left e -> f e Right r -> return r storageDir :: Maybe LocalStorageDirectory -> GetShareDir storageDir Nothing (Section section) = do home <- myHomeDir return $ home dotdir section storageDir (Just (LocalStorageDirectory d)) (Section section) = pure $ d section storageTopDir :: Maybe LocalStorageDirectory -> IO FilePath storageTopDir lsd = storageDir lsd (Section ".") testStorageDir :: FilePath -> GetShareDir testStorageDir tmpdir = storageDir (Just (LocalStorageDirectory tmpdir)) localDiskUsage :: Maybe LocalStorageDirectory -> IO DiskUsage localDiskUsage lsd = getDiskUsage =<< storageTopDir lsd -- | 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 $ takeFileName $ dropExtension f | otherwise = Nothing isShareFile :: FilePath -> Bool isShareFile f = ext `isSuffixOf` f ext :: String ext = ".keysafe" dotdir :: FilePath dotdir = ".keysafe" "objects"