diff --git a/CHANGELOG.md b/CHANGELOG.md index 70ee44bba3095f597016b6ca4f7a6ddf8be2a5a0..89f37bd8727fde3c24415a250174fcc2d52af226 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ * The download APIs now only send requests to a storage server after that storage server is authenticated using information from the NURL. +* ``Tahoe.Download.download`` and ``Tahoe.Download.downloadDirectory`` now return ``ExceptT``. + ## 0.1.0.0 -- 2023-08-17 * First version. Released on an unsuspecting world. diff --git a/app/Main.hs b/app/Main.hs index 157e0186750e0a957a19e66e419fb7cc993c332d..95edf3fc786f0447f00f74ec3fa092977dfd7542 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -1,5 +1,6 @@ module Main where +import Control.Monad.Except (runExceptT) import qualified Data.ByteString as B import qualified Data.ByteString.Char8 as C8 import qualified Data.ByteString.Lazy as BL @@ -22,7 +23,7 @@ main = do let Right (CHKReader cap) = parse pCapability "<argv>" (T.pack readCap) -- Download & decode the shares - result <- download announcements cap announcementToImmutableStorageServer + result <- runExceptT $ download announcements cap announcementToImmutableStorageServer -- Show the result putStrLn "Your result:" diff --git a/download-sdmf/Main.hs b/download-sdmf/Main.hs index 6078709a3bf36d0f5d149cc80228b2e152ecd48b..1f38473e57a6346cc3a4414703747545deb74550 100644 --- a/download-sdmf/Main.hs +++ b/download-sdmf/Main.hs @@ -1,36 +1,43 @@ {-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE LambdaCase #-} module Main where -import qualified Data.ByteString as B -import qualified Data.ByteString.Char8 as C8 -import qualified Data.ByteString.Lazy as BL +import Control.Monad.Except (ExceptT (ExceptT), MonadTrans (lift), runExceptT, withExceptT) +import qualified Data.ByteString.Lazy.Char8 as BL import qualified Data.Text as T -import Data.Yaml (decodeEither') +import Data.Yaml (decodeFileEither) import System.Environment (getArgs) import Tahoe.Announcement (Announcements (..)) import Tahoe.Download (announcementToMutableStorageServer, download) -import Tahoe.SDMF (SDMF (..), pCapability, writerReader) +import Tahoe.SDMF (Reader, SDMF (..), pCapability, writerReader) import Text.Megaparsec (parse) +newtype Failed = Failed String + +instance Show Failed where + show (Failed s) = s + main :: IO () -main = do - [announcementPath, readCap] <- getArgs - -- Load server announcements - announcementsBytes <- B.readFile announcementPath - let Right (Announcements announcements) = decodeEither' announcementsBytes - - -- Accept & parse read capability - case parse pCapability "<argv>" (T.pack readCap) of - Left e -> print $ "Failed to parse cap: " <> show e - Right (SDMFVerifier _) -> C8.putStrLn "Nothing currently implemented for verifier caps." - Right (SDMFWriter rwcap) -> go announcements (writerReader rwcap) - Right (SDMFReader rocap) -> go announcements rocap +main = + either (putStrLn . ("Failed: " <>) . show) (BL.putStrLn . BL.append "Your result: ") + =<< ( runExceptT $ + do + [announcementPath, readCap] <- lift getArgs + + -- Load server announcements + announcements <- mightFail "Failed to parse announcements: " $ decodeFileEither announcementPath + + -- Parse read capability + (mightFail "Failed to parse capability: " . pure . parse pCapability "<argv>" . T.pack) readCap + >>= \case + SDMFVerifier _ -> ExceptT . pure . Left . Failed $ "Nothing implemented for verifier capabilities." + SDMFWriter rwcap -> go announcements (writerReader rwcap) + SDMFReader rocap -> go announcements rocap + ) where - go announcements cap = do - -- Download & decode the shares - result <- download announcements cap announcementToMutableStorageServer + go :: Announcements -> Reader -> ExceptT Failed IO BL.ByteString + go (Announcements announcements) cap = withExceptT (Failed . ("Failed download: " <>) . show) $ download announcements cap announcementToMutableStorageServer - -- Show the result - putStrLn "Your result:" - either print (C8.putStrLn . BL.toStrict) result +mightFail :: (Functor m, Show a1) => String -> m (Either a1 a2) -> ExceptT Failed m a2 +mightFail s = withExceptT (Failed . (s <>) . show) . ExceptT diff --git a/gbs-downloader.cabal b/gbs-downloader.cabal index ee59009836230a852db53248c9e5bcaf2cc84042..eebb17bc3965347a402a3961b909d77894c4a55c 100644 --- a/gbs-downloader.cabal +++ b/gbs-downloader.cabal @@ -118,6 +118,7 @@ library , http-client-tls >=0.3.5.3 && <0.4 , http-types >=0.12.3 && <0.13 , lens >=4.0 && <5.3 + , mtl >=2.2.2 && <2.4 , network-uri >=2.6.3 && <2.7 , servant-client >=0.16.0.1 && <0.21 , servant-client-core >=0.16 && <0.21 @@ -157,6 +158,7 @@ executable download-chk , containers >=0.6.0.1 && <0.7 , gbs-downloader , megaparsec >=8.0 && <9.3 + , mtl >=2.2.2 && <2.4 , tahoe-chk >=0.2 && <0.3 , text >=1.2.3.1 && <1.3 , yaml >=0.11.5.0 && <0.11.9.0 || >=0.11.9.0.0 && <0.12 @@ -180,6 +182,7 @@ executable download-sdmf , containers >=0.6.0.1 && <0.7 , gbs-downloader , megaparsec >=8.0 && <9.3 + , mtl >=2.2.2 && <2.4 , tahoe-ssk >=0.3 && <0.4 , text >=1.2.3.1 && <1.3 , yaml >=0.11.5.0 && <0.11.9.0 || >=0.11.9.0.0 && <0.12 @@ -200,6 +203,7 @@ executable list-dircap , containers >=0.6.0.1 && <0.7 , gbs-downloader , megaparsec >=8.0 && <9.3 + , mtl >=2.2.2 && <2.4 , tahoe-chk >=0.2 && <0.3 , tahoe-directory >=0.1 && <0.2 , tahoe-ssk >=0.3 && <0.4 @@ -249,6 +253,7 @@ test-suite gbs-downloader-test , http-types >=0.12.3 && <0.13 , lens >=4.0 && <5.3 , memory >=0.15 && <0.17 + , mtl >=2.2.2 && <2.4 , servant-client >=0.16.0.1 && <0.21 , servant-client-core >=0.16 && <0.21 , tahoe-chk >=0.2 && <0.3 diff --git a/list-dircap/Main.hs b/list-dircap/Main.hs index c0adc747c73d34ba882233bf596f445a44d37074..a050462180c8b45e49f4c8a084fc3cb5563b1a88 100644 --- a/list-dircap/Main.hs +++ b/list-dircap/Main.hs @@ -10,6 +10,7 @@ import Tahoe.Announcement (Announcements (..)) import qualified Tahoe.Directory as TD import Text.Megaparsec (parse) +import Control.Monad.Except (runExceptT) import Tahoe.Download (announcementToImmutableStorageServer, announcementToMutableStorageServer, downloadDirectory) main :: IO () @@ -30,7 +31,7 @@ main = do where go announcements cap lookupServer = do -- Download & decode the shares - result <- downloadDirectory announcements cap lookupServer + result <- runExceptT $ downloadDirectory announcements cap lookupServer -- Show the result putStrLn "Your result:" diff --git a/src/Tahoe/Download.hs b/src/Tahoe/Download.hs index 4f0f41f55ef0f5354c4dcd0efbb46872065073e1..b7febdf8d3d470d3b690c5712af4191f07345791 100644 --- a/src/Tahoe/Download.hs +++ b/src/Tahoe/Download.hs @@ -19,6 +19,7 @@ module Tahoe.Download ( import Control.Concurrent.Async (mapConcurrently) import Control.Exception (Exception (displayException), SomeException, try) +import Control.Monad.Except (ExceptT (..), lift, throwError, withExceptT) import Control.Monad.IO.Class (MonadIO (liftIO)) import Data.Bifunctor (Bifunctor (first, second)) import Data.Binary (Word16) @@ -55,7 +56,7 @@ download :: -- the read capability has a Verifiable instance because Verifiable is -- what gives us the ability to locate the shares. If we located -- separately from decrypting this might be simpler. - (MonadIO m, Readable readCap, Verifiable v, Verifier readCap ~ v) => + (Readable readCap, Verifiable v, Verifier readCap ~ v) => -- | Information about the servers from which to consider downloading shares -- representing the application data. Map.Map StorageServerID StorageServerAnnouncement -> @@ -65,49 +66,35 @@ download :: LookupServer IO -> -- | Either a description of how the recovery failed or the recovered -- application data. - m (Either DownloadError LB.ByteString) + ExceptT DownloadError IO LB.ByteString download servers cap lookupServer = do print' ("Downloading: " <> show (getStorageIndex $ getVerifiable cap)) let verifier = getVerifiable cap let storageIndex = getStorageIndex verifier - -- TODO: If getRequiredTotal fails on the first storage server, we may - -- need to try more. If it fails for all of them, we need to represent - -- the failure coherently. - someParam <- liftIO $ firstRightM lookupServer (getRequiredTotal verifier) (Map.elems servers) - case someParam of - Left errs -> pure . Left $ if servers == mempty then NoConfiguredServers else NoReachableServers (StorageServerUnreachable <$> errs) - Right (required, _) -> do - locationE <- liftIO $ locateShares servers lookupServer storageIndex (fromIntegral required) - print' "Finished locating shares" - case locationE of - Left err -> do - print' "Got an error locating shares" - pure $ Left err - Right discovered -> do - print' "Found some shares, fetching them" - -- XXX note shares can contain failures - shares <- liftIO $ executeDownloadTasks storageIndex (makeDownloadTasks =<< discovered) - print' "Fetched the shares, decoding them" - s <- liftIO $ decodeShares cap shares required - print' "Decoded them" - pure s + (required, _) <- withExceptT noReachableServers (firstRightM lookupServer (getRequiredTotal verifier) (Map.elems servers)) + print' "Discovered required number of shares" + discovered <- ExceptT $ locateShares servers lookupServer storageIndex (fromIntegral required) + print' "Finished locating shares, fetching" + shares <- liftIO $ executeDownloadTasks storageIndex (makeDownloadTasks =<< discovered) + print' "Fetched the shares, decoding them" + ExceptT $ liftIO $ decodeShares cap shares required + where + noReachableServers = NoReachableServers . (StorageServerUnreachable <$>) {- | Apply a monadic operation to each element of a list and another monadic operation values in the resulting Rights. If all of the results are Lefts or Nothings, return a list of the values in the Lefts. Otherwise, return the *first* Right. -} -firstRightM :: Monad m => (a -> m (Either b c)) -> (c -> m (Maybe d)) -> [a] -> m (Either [b] d) -firstRightM _ _ [] = pure $ Left [] +firstRightM :: Monad m => (a -> m (Either b c)) -> (c -> m (Maybe d)) -> [a] -> ExceptT [b] m d +firstRightM _ _ [] = throwError [] firstRightM f op (x : xs) = do - s <- f x + s <- lift $ f x case s of - Left bs -> first (bs :) <$> recurse + Left bs -> (bs :) `withExceptT` recurse Right ss -> do - r <- op ss - case r of - Nothing -> recurse - Just d -> pure $ Right d + r <- lift $ op ss + maybe recurse pure r where recurse = firstRightM f op xs @@ -232,7 +219,7 @@ downloadShare storageIndex (shareNum, s) = do as a collection of entries. -} downloadDirectory :: - (MonadIO m, Readable readCap, Verifiable v, Verifier readCap ~ v) => + (Readable readCap, Verifiable v, Verifier readCap ~ v) => -- | Information about the servers from which to consider downloading shares -- representing the application data. Map.Map StorageServerID StorageServerAnnouncement -> @@ -242,12 +229,10 @@ downloadDirectory :: LookupServer IO -> -- | Either a description of how the recovery failed or the recovered -- application data. - m (Either DirectoryDownloadError Directory) + ExceptT DirectoryDownloadError IO Directory downloadDirectory anns (DirectoryCapability cap) lookupServer = do - bs <- download anns cap lookupServer - pure $ do - bs' <- first UnderlyingDownloadError bs - first (const DecodingError) $ Directory.parse (LB.toStrict bs') + bs <- UnderlyingDownloadError `withExceptT` download anns cap lookupServer + ExceptT . pure . first (const DecodingError) . Directory.parse . LB.toStrict $ bs data DirectoryDownloadError = UnderlyingDownloadError DownloadError diff --git a/test/Spec.hs b/test/Spec.hs index c5ba7736b431e7b185fc5cd529ba8799af27fc4a..269248fdae0a5c7eacbe24e03c75ec6186773124 100644 --- a/test/Spec.hs +++ b/test/Spec.hs @@ -5,6 +5,7 @@ module Main where import Control.Exception (Exception, throwIO) import Control.Lens (view) import Control.Monad (replicateM, when) +import Control.Monad.Except (MonadTrans (lift), runExceptT) import Control.Monad.IO.Class (liftIO) import Crypto.Cipher.AES (AES128) import Crypto.Cipher.Types (Cipher (cipherInit, cipherKeySize), KeySizeSpecifier (KeySizeEnum, KeySizeFixed, KeySizeRange), nullIV) @@ -144,10 +145,10 @@ tests = $ do -- If there are no servers then we can't possibly get enough -- shares to recover the application data. - result <- liftIO $ download mempty (trivialCap 1 1) noServers + result <- runExceptT $ download mempty (trivialCap 1 1) noServers assertEqual "download should fail with no servers" - (Left NoConfiguredServers) + (Left (NoReachableServers [])) result , testCase "no reachable servers" $ do -- If we can't contact any configured server then we can't @@ -158,7 +159,7 @@ tests = [ ("v0-abc123", ann) ] - result <- liftIO $ download anns (trivialCap 1 1) noServers + result <- runExceptT $ download anns (trivialCap 1 1) noServers assertEqual "download should fail with no reachable servers" (Left $ NoReachableServers [StorageServerUnreachable (URIParseError ann)]) @@ -179,7 +180,7 @@ tests = let openServer = simpleLookup [("somewhere", server)] -- Try to download the cap which requires three shares to reconstruct. - result <- liftIO $ download anns cap openServer + result <- runExceptT $ download anns cap openServer assertEqual "download should fail with not enough shares" (Left NotEnoughShares{notEnoughSharesNeeded = 3, notEnoughSharesFound = 2}) @@ -210,7 +211,7 @@ tests = let openServer = simpleLookup [("somewhere", somewhere), ("elsewhere", elsewhere)] -- Try to download the cap which requires three shares to reconstruct. - result <- liftIO $ download anns cap openServer + result <- runExceptT $ download anns cap openServer assertEqual "download should fail with not enough shares" (Left NotEnoughShares{notEnoughSharesNeeded = 3, notEnoughSharesFound = 2}) @@ -228,7 +229,7 @@ tests = let cap = trivialCap 3 13 -- Try to download the cap which requires three shares to reconstruct. - result <- liftIO $ download anns cap openServer + result <- runExceptT $ download anns cap openServer assertEqual "download should fail with details about unreachable server" (Left (NoReachableServers [StorageServerCommunicationError "BespokeFailure"])) @@ -291,7 +292,7 @@ tests = -- Try to download the cap which requires three shares to reconstruct. - result <- liftIO $ download anns cap openServer + result <- runExceptT $ download anns cap openServer assertEqual "download should fail with details about unreachable server" (Left (NotEnoughDecodedShares{notEnoughDecodedSharesNeeded = 3, notEnoughDecodedSharesFound = 0})) @@ -341,7 +342,7 @@ tests = serverAnnouncements = Map.fromSet makeAnn serverIDs' -- Recover the plaintext from the servers. - result <- liftIO $ download serverAnnouncements cap lookupServer + result <- lift $ runExceptT $ download serverAnnouncements cap lookupServer diff (Right plaintext) (==) result , testProperty "ssk success" $ property $ do @@ -385,7 +386,7 @@ tests = serverAnnouncements = Map.fromSet makeAnn serverIDs' -- Recover the plaintext from the servers. - result <- liftIO $ download serverAnnouncements readCap lookupServer + result <- lift $ runExceptT $ download serverAnnouncements readCap lookupServer diff (Right plaintext) (==) result , testCase "immutable upload/download to using Great Black Swamp" $ do pure ()