summaryrefslogtreecommitdiff
path: root/Remote/Tahoe.hs
diff options
context:
space:
mode:
Diffstat (limited to 'Remote/Tahoe.hs')
-rw-r--r--Remote/Tahoe.hs229
1 files changed, 229 insertions, 0 deletions
diff --git a/Remote/Tahoe.hs b/Remote/Tahoe.hs
new file mode 100644
index 000000000..48dd569b4
--- /dev/null
+++ b/Remote/Tahoe.hs
@@ -0,0 +1,229 @@
+{- Tahoe-LAFS special remotes.
+ -
+ - Tahoe capabilities for accessing objects stored in the remote
+ - are preserved in the remote state log.
+ -
+ - In order to allow multiple clones of a repository to access the same
+ - tahoe repository, git-annex needs to store the introducer furl,
+ - and the shared-convergence-secret. These are stored in the remote
+ - configuration, when embedcreds is enabled.
+ -
+ - Using those creds, git-annex sets up a tahoe configuration directory in
+ - ~/.tahoe/git-annex/UUID/
+ -
+ - Tahoe has its own encryption, so git-annex's encryption is not used.
+ -
+ - Copyright 2013 Joey Hess <joey@kitenet.net>
+ -
+ - Licensed under the GNU GPL version 3 or higher.
+ -}
+
+{-# LANGUAGE OverloadedStrings #-}
+
+module Remote.Tahoe (remote) where
+
+import qualified Data.Map as M
+import Data.Aeson
+import Data.ByteString.Lazy.UTF8 (fromString)
+
+import Common.Annex
+import Types.Remote
+import qualified Git
+import Config
+import Config.Cost
+import Remote.Helper.Special
+import Annex.UUID
+import Annex.Content
+import Logs.RemoteState
+import Utility.UserInfo
+import Utility.Metered
+import Utility.Env
+
+type TahoeConfigDir = FilePath
+type SharedConvergenceSecret = String
+type IntroducerFurl = String
+type Capability = String
+
+remote :: RemoteType
+remote = RemoteType {
+ typename = "tahoe",
+ enumerate = findSpecialRemotes "tahoe",
+ generate = gen,
+ setup = tahoeSetup
+}
+
+gen :: Git.Repo -> UUID -> RemoteConfig -> RemoteGitConfig -> Annex (Maybe Remote)
+gen r u c gc = do
+ cst <- remoteCost gc expensiveRemoteCost
+ configdir <- liftIO $ maybe (defaultTahoeConfigDir u) return (remoteAnnexTahoe gc)
+ return $ Just $ Remote {
+ uuid = u,
+ cost = cst,
+ name = Git.repoDescribe r,
+ storeKey = store u configdir,
+ retrieveKeyFile = retrieve u configdir,
+ retrieveKeyFileCheap = \_ _ -> return False,
+ removeKey = remove,
+ hasKey = checkPresent u configdir,
+ hasKeyCheap = False,
+ whereisKey = Nothing,
+ remoteFsck = Nothing,
+ repairRepo = Nothing,
+ config = c,
+ repo = r,
+ gitconfig = gc,
+ localpath = Nothing,
+ readonly = False,
+ globallyAvailable = True,
+ remotetype = remote
+ }
+
+tahoeSetup :: Maybe UUID -> RemoteConfig -> Annex (RemoteConfig, UUID)
+tahoeSetup mu c = do
+ furl <- fromMaybe (fromMaybe missingfurl $ M.lookup furlk c)
+ <$> liftIO (getEnv "TAHOE_FURL")
+ u <- maybe (liftIO genUUID) return mu
+ configdir <- liftIO $ defaultTahoeConfigDir u
+ scs <- liftIO $ tahoeConfigure configdir furl (M.lookup scsk c)
+ let c' = if M.lookup "embedcreds" c == Just "yes"
+ then flip M.union c $ M.fromList
+ [ (furlk, furl)
+ , (scsk, scs)
+ ]
+ else c
+ gitConfigSpecialRemote u c' "tahoe" configdir
+ return (c', u)
+ where
+ scsk = "shared-convergence-secret"
+ furlk = "introducer-furl"
+ missingfurl = error "Set TAHOE_FURL to the introducer furl to use."
+
+store :: UUID -> TahoeConfigDir -> Key -> AssociatedFile -> MeterUpdate -> Annex Bool
+store u configdir k _f _p = sendAnnex k noop $ \src -> do
+ liftIO $ startTahoeDaemon configdir
+ parsePut <$> liftIO (readTahoe configdir "put" [File src]) >>= maybe
+ (return False)
+ (\cap -> storeCapability u k cap >> return True)
+
+retrieve :: UUID -> TahoeConfigDir -> Key -> AssociatedFile -> FilePath -> MeterUpdate -> Annex Bool
+retrieve u configdir k _f d _p = go =<< getCapability u k
+ where
+ go Nothing = return False
+ go (Just cap) = liftIO $ do
+ startTahoeDaemon configdir
+ boolTahoe configdir "get" [Param cap, File d]
+
+remove :: Key -> Annex Bool
+remove _k = do
+ warning "content cannot be removed from tahoe remote"
+ return False
+
+checkPresent :: UUID -> TahoeConfigDir -> Key -> Annex (Either String Bool)
+checkPresent u configdir k = go =<< getCapability u k
+ where
+ go Nothing = return (Right False)
+ go (Just cap) = liftIO $ do
+ startTahoeDaemon configdir
+ parseCheck <$> readTahoe configdir "check"
+ [ Param "--raw"
+ , Param cap
+ ]
+
+defaultTahoeConfigDir :: UUID -> IO TahoeConfigDir
+defaultTahoeConfigDir u = do
+ h <- myHomeDir
+ return $ h </> ".tahoe" </> "git-annex" </> fromUUID u
+
+tahoeConfigure :: TahoeConfigDir -> IntroducerFurl -> Maybe SharedConvergenceSecret -> IO SharedConvergenceSecret
+tahoeConfigure configdir furl mscs = do
+ unlessM (createClient configdir furl) $
+ error "tahoe create-client failed"
+ maybe noop (writeSharedConvergenceSecret configdir) mscs
+ startTahoeDaemon configdir
+ getSharedConvergenceSecret configdir
+
+createClient :: TahoeConfigDir -> IntroducerFurl -> IO Bool
+createClient configdir furl = do
+ createDirectoryIfMissing True (parentDir configdir)
+ boolTahoe configdir "create-client"
+ [ Param "--nickname", Param "git-annex"
+ , Param "--introducer", Param furl
+ ]
+
+writeSharedConvergenceSecret :: TahoeConfigDir -> SharedConvergenceSecret -> IO ()
+writeSharedConvergenceSecret configdir scs =
+ writeFile (convergenceFile configdir) (unlines [scs])
+
+{- The tahoe daemon writes the convergenceFile shortly after it starts
+ - (it does not need to connect to the network). So, try repeatedly to read
+ - the file, for up to 1 minute. To avoid reading a partially written
+ - file, look for the newline after the value. -}
+getSharedConvergenceSecret :: TahoeConfigDir -> IO SharedConvergenceSecret
+getSharedConvergenceSecret configdir = go (60 :: Int)
+ where
+ f = convergenceFile configdir
+ go n
+ | n == 0 = error $ "tahoe did not write " ++ f ++ " after 1 minute. Perhaps the daemon failed to start?"
+ | otherwise = do
+ v <- catchMaybeIO (readFile f)
+ case v of
+ Just s | "\n" `isSuffixOf` s || "\r" `isSuffixOf` s ->
+ return $ takeWhile (`notElem` "\n\r") s
+ _ -> go (n - 1)
+
+convergenceFile :: TahoeConfigDir -> FilePath
+convergenceFile configdir = configdir </> "private" </> "convergence"
+
+{- XXX Avoid starting tahoe if it is already running. -}
+startTahoeDaemon :: TahoeConfigDir -> IO ()
+startTahoeDaemon configdir = void $ boolTahoe configdir "start" []
+
+boolTahoe :: TahoeConfigDir -> String -> [CommandParam] -> IO Bool
+boolTahoe configdir command params = boolSystem "tahoe" $
+ tahoeParams configdir command params
+
+readTahoe :: TahoeConfigDir -> String -> [CommandParam] -> IO String
+readTahoe configdir command params = catchDefaultIO "" $
+ readProcess "tahoe" $ toCommand $
+ tahoeParams configdir command params
+
+tahoeParams :: TahoeConfigDir -> String -> [CommandParam] -> [CommandParam]
+tahoeParams configdir command params =
+ Param command : Param "-d" : File configdir : params
+
+storeCapability :: UUID -> Key -> Capability -> Annex ()
+storeCapability u k cap = setRemoteState u k cap
+
+getCapability :: UUID -> Key -> Annex (Maybe Capability)
+getCapability u k = getRemoteState u k
+
+{- tahoe put outputs a single line, containing the capability. -}
+parsePut :: String -> Maybe Capability
+parsePut s = case lines s of
+ [cap] | "URI" `isPrefixOf` cap -> Just cap
+ _ -> Nothing
+
+{- tahoe check --raw outputs a json document.
+ - Its contents will vary (for LIT capabilities, it lacks most info),
+ - but should always contain a results object with a healthy value
+ - that's true or false.
+ -}
+parseCheck :: String -> Either String Bool
+parseCheck s = maybe parseerror (Right . healthy . results) (decode $ fromString s)
+ where
+ parseerror
+ | null s = Left "tahoe check failed to run"
+ | otherwise = Left "unable to parse tahoe check output"
+
+data CheckRet = CheckRet { results :: Results }
+data Results = Results { healthy :: Bool }
+
+instance FromJSON CheckRet where
+ parseJSON (Object v) = CheckRet
+ <$> v .: "results"
+ parseJSON _ = mzero
+
+instance FromJSON Results where
+ parseJSON (Object v) = Results
+ <$> v .: "healthy"
+ parseJSON _ = mzero