tahoe-chk-0.1.0.2: src/Tahoe/Server.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Tahoe.Server (
nullStorageServer,
memoryStorageServer,
directoryStorageServer,
directoryStorageServer',
) where
import qualified Data.Map.Strict as M
import Data.Maybe (fromMaybe)
import qualified Data.Set as Set
import Control.Exception (
Exception,
catch,
throwIO,
)
import Tahoe.CHK.Server (
StorageServer (..),
)
import Tahoe.CHK.Types (
Offset,
ShareNum,
StorageIndex,
)
import Data.IORef (
IORef,
modifyIORef',
newIORef,
readIORef,
)
import System.Directory (
createDirectoryIfMissing,
listDirectory,
)
import System.FilePath (
(</>),
)
import qualified Data.ByteString as BS
import Data.ByteString.Base32 (encodeBase32Unpadded)
import qualified Data.Text as T
import System.IO (
IOMode (..),
SeekMode (..),
hSeek,
withBinaryFile,
)
import System.IO.Error (
isDoesNotExistError,
)
{- | Create a storage server backed by a certain directory which already
exists.
-}
directoryStorageServer :: FilePath -> StorageServer
directoryStorageServer serverRoot =
StorageServer
{ storageServerID = T.pack serverRoot
, storageServerWrite = writeShareDataAt serverRoot
, storageServerRead = \index sharenum ->
withBinaryFile (sharePath serverRoot index sharenum) ReadMode BS.hGetContents
, storageServerGetBuckets = getBuckets
}
where
writeShareDataAt :: FilePath -> StorageIndex -> ShareNum -> Offset -> BS.ByteString -> IO ()
writeShareDataAt shareRoot' storageIndex shareNum offset xs = do
createDirectoryIfMissing True (bucketPath shareRoot' storageIndex)
withBinaryFile (sharePath shareRoot' storageIndex shareNum) ReadWriteMode $ \f ->
hSeek f AbsoluteSeek offset >> BS.hPut f xs
-- Get the path to the directory where shares for the given storage
-- index should be written.
bucketPath :: FilePath -> StorageIndex -> FilePath
bucketPath root storageIndex = root </> bucketName
where
bucketName = "shares" </> shortPiece </> fullName
fullName = T.unpack . T.toLower . encodeBase32Unpadded $ storageIndex
shortPiece = take 2 fullName
-- Get the path to the file where data for the given share of the given
-- storage index should be written.
sharePath :: FilePath -> StorageIndex -> ShareNum -> FilePath
sharePath root storageIndex shareNum =
bucketPath root storageIndex </> show shareNum
getBuckets :: StorageIndex -> IO (Set.Set ShareNum)
getBuckets storageIndex =
readShareFilenames `catch` doesNotExist
where
readShareFilenames =
Set.fromList . map read <$> listDirectory (bucketPath serverRoot storageIndex)
doesNotExist e =
if isDoesNotExistError e
then return Set.empty
else ioError e
{- | Create a storage server backed by a certain directory which may or may
not already exist.
-}
directoryStorageServer' :: FilePath -> IO StorageServer
directoryStorageServer' shareRoot = do
createDirectoryIfMissing True shareRoot
pure $ directoryStorageServer shareRoot
-- | Create a storage server backed only by in-memory data.
memoryStorageServer :: IO StorageServer
memoryStorageServer = do
shares :: IORef (M.Map (StorageIndex, ShareNum) BS.ByteString) <- newIORef mempty
let storageServerID = "memory"
storageServerWrite index sharenum offset sharedata =
modifyIORef' shares $ M.alter (appendBytes offset sharedata) (index, sharenum)
appendBytes :: Offset -> BS.ByteString -> Maybe BS.ByteString -> Maybe BS.ByteString
appendBytes 0 sharedata Nothing = Just sharedata
appendBytes n _sharedata Nothing =
error $
"memoryStorageServer appendBytes requires append-only usage; 0 bytes written but offset is "
<> show n
appendBytes n sharedata (Just existing)
| fromIntegral (BS.length existing) /= n =
error $
"memoryStorageServer appendBytes requires append-only usage; "
<> show (BS.length existing)
<> " bytes written but offset is "
<> show n
| otherwise = Just (existing <> sharedata)
storageServerRead :: StorageIndex -> ShareNum -> IO BS.ByteString
storageServerRead index sharenum =
fromMaybe "" . M.lookup (index, sharenum) <$> readIORef shares
storageServerGetBuckets :: StorageIndex -> IO (Set.Set ShareNum)
storageServerGetBuckets index =
Set.fromList . map snd . filter ((== index) . fst) . M.keys <$> readIORef shares
pure $ StorageServer{..}
{- | Create a StorageServer that discards writes to it and throws errors on
reads.
-}
nullStorageServer :: StorageServer
nullStorageServer =
StorageServer
{ storageServerID = "null-server"
, storageServerWrite = \_index _sharenum _offset _data -> return ()
, storageServerRead = \_index _sharenum -> throwIO IThrewYourDataAway
, storageServerGetBuckets = \_index -> return mempty
}
data ReadError = IThrewYourDataAway deriving (Show)
instance Exception ReadError