external backends wip
It's able to start them up, the only thing not implemented is generating and verifying keys. And, the key translation for HasExt.
This commit is contained in:
parent
b5d6a36db0
commit
f75be32166
24 changed files with 482 additions and 136 deletions
|
@ -349,12 +349,13 @@ getViaTmpFromDisk rsp v key action = checkallowed $ do
|
||||||
-- RetrievalSecurityPolicy would cause verification to always fail.
|
-- RetrievalSecurityPolicy would cause verification to always fail.
|
||||||
checkallowed a = case rsp of
|
checkallowed a = case rsp of
|
||||||
RetrievalAllKeysSecure -> a
|
RetrievalAllKeysSecure -> a
|
||||||
RetrievalVerifiableKeysSecure
|
RetrievalVerifiableKeysSecure -> ifM (Backend.isVerifiable key)
|
||||||
| Backend.isVerifiable key -> a
|
( a
|
||||||
| otherwise -> ifM (annexAllowUnverifiedDownloads <$> Annex.getGitConfig)
|
, ifM (annexAllowUnverifiedDownloads <$> Annex.getGitConfig)
|
||||||
( a
|
( a
|
||||||
, warnUnverifiableInsecure key >> return False
|
, warnUnverifiableInsecure key >> return False
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
{- Verifies that a file is the expected content of a key.
|
{- Verifies that a file is the expected content of a key.
|
||||||
-
|
-
|
||||||
|
@ -373,12 +374,13 @@ getViaTmpFromDisk rsp v key action = checkallowed $ do
|
||||||
verifyKeyContent :: RetrievalSecurityPolicy -> VerifyConfig -> Verification -> Key -> FilePath -> Annex Bool
|
verifyKeyContent :: RetrievalSecurityPolicy -> VerifyConfig -> Verification -> Key -> FilePath -> Annex Bool
|
||||||
verifyKeyContent rsp v verification k f = case (rsp, verification) of
|
verifyKeyContent rsp v verification k f = case (rsp, verification) of
|
||||||
(_, Verified) -> return True
|
(_, Verified) -> return True
|
||||||
(RetrievalVerifiableKeysSecure, _)
|
(RetrievalVerifiableKeysSecure, _) -> ifM (Backend.isVerifiable k)
|
||||||
| Backend.isVerifiable k -> verify
|
( verify
|
||||||
| otherwise -> ifM (annexAllowUnverifiedDownloads <$> Annex.getGitConfig)
|
, ifM (annexAllowUnverifiedDownloads <$> Annex.getGitConfig)
|
||||||
( verify
|
( verify
|
||||||
, warnUnverifiableInsecure k >> return False
|
, warnUnverifiableInsecure k >> return False
|
||||||
)
|
)
|
||||||
|
)
|
||||||
(_, UnVerified) -> ifM (shouldVerify v)
|
(_, UnVerified) -> ifM (shouldVerify v)
|
||||||
( verify
|
( verify
|
||||||
, return True
|
, return True
|
||||||
|
@ -391,9 +393,11 @@ verifyKeyContent rsp v verification k f = case (rsp, verification) of
|
||||||
Just size -> do
|
Just size -> do
|
||||||
size' <- liftIO $ catchDefaultIO 0 $ getFileSize f
|
size' <- liftIO $ catchDefaultIO 0 $ getFileSize f
|
||||||
return (size' == size)
|
return (size' == size)
|
||||||
verifycontent = case Types.Backend.verifyKeyContent =<< Backend.maybeLookupBackendVariety (fromKey keyVariety k) of
|
verifycontent = Backend.maybeLookupBackendVariety (fromKey keyVariety k) >>= \case
|
||||||
Nothing -> return True
|
Nothing -> return True
|
||||||
Just verifier -> verifier k f
|
Just b -> case Types.Backend.verifyKeyContent b of
|
||||||
|
Nothing -> return True
|
||||||
|
Just verifier -> verifier k f
|
||||||
|
|
||||||
warnUnverifiableInsecure :: Key -> Annex ()
|
warnUnverifiableInsecure :: Key -> Annex ()
|
||||||
warnUnverifiableInsecure k = warning $ unwords
|
warnUnverifiableInsecure k = warning $ unwords
|
||||||
|
@ -512,12 +516,13 @@ moveAnnex key src = ifM (checkSecureHashes' key)
|
||||||
alreadyhave = liftIO $ removeFile src
|
alreadyhave = liftIO $ removeFile src
|
||||||
|
|
||||||
checkSecureHashes :: Key -> Annex (Maybe String)
|
checkSecureHashes :: Key -> Annex (Maybe String)
|
||||||
checkSecureHashes key
|
checkSecureHashes key = ifM (Backend.isCryptographicallySecure key)
|
||||||
| Backend.isCryptographicallySecure key = return Nothing
|
( return Nothing
|
||||||
| otherwise = ifM (annexSecureHashesOnly <$> Annex.getGitConfig)
|
, ifM (annexSecureHashesOnly <$> Annex.getGitConfig)
|
||||||
( return $ Just $ "annex.securehashesonly blocked adding " ++ decodeBS (formatKeyVariety (fromKey keyVariety key)) ++ " key"
|
( return $ Just $ "annex.securehashesonly blocked adding " ++ decodeBS (formatKeyVariety (fromKey keyVariety key)) ++ " key"
|
||||||
, return Nothing
|
, return Nothing
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
checkSecureHashes' :: Key -> Annex Bool
|
checkSecureHashes' :: Key -> Annex Bool
|
||||||
checkSecureHashes' key = checkSecureHashes key >>= \case
|
checkSecureHashes' key = checkSecureHashes key >>= \case
|
||||||
|
|
|
@ -13,8 +13,8 @@ import Git.Env
|
||||||
import Utility.Shell
|
import Utility.Shell
|
||||||
import Messages.Progress
|
import Messages.Progress
|
||||||
|
|
||||||
import Control.Concurrent.STM
|
|
||||||
import Control.Concurrent.Async
|
import Control.Concurrent.Async
|
||||||
|
import System.Log.Logger (debugM)
|
||||||
|
|
||||||
data ExternalAddonProcess = ExternalAddonProcess
|
data ExternalAddonProcess = ExternalAddonProcess
|
||||||
{ externalSend :: Handle
|
{ externalSend :: Handle
|
||||||
|
@ -23,6 +23,7 @@ data ExternalAddonProcess = ExternalAddonProcess
|
||||||
-- immediately.
|
-- immediately.
|
||||||
, externalShutdown :: Bool -> IO ()
|
, externalShutdown :: Bool -> IO ()
|
||||||
, externalPid :: ExternalAddonPID
|
, externalPid :: ExternalAddonPID
|
||||||
|
, externalProgram :: String
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExternalAddonPID = Int
|
type ExternalAddonPID = Int
|
||||||
|
@ -31,8 +32,8 @@ data ExternalAddonStartError
|
||||||
= ProgramNotInstalled String
|
= ProgramNotInstalled String
|
||||||
| ProgramFailure String
|
| ProgramFailure String
|
||||||
|
|
||||||
startExternalAddonProcess :: String -> TVar ExternalAddonPID-> Annex (Either ExternalAddonStartError ExternalAddonProcess)
|
startExternalAddonProcess :: String -> ExternalAddonPID -> Annex (Either ExternalAddonStartError ExternalAddonProcess)
|
||||||
startExternalAddonProcess basecmd pidvar = do
|
startExternalAddonProcess basecmd pid = do
|
||||||
errrelayer <- mkStderrRelayer
|
errrelayer <- mkStderrRelayer
|
||||||
g <- Annex.gitRepo
|
g <- Annex.gitRepo
|
||||||
cmdpath <- liftIO $ searchPath basecmd
|
cmdpath <- liftIO $ searchPath basecmd
|
||||||
|
@ -47,16 +48,12 @@ startExternalAddonProcess basecmd pidvar = do
|
||||||
}
|
}
|
||||||
p <- propgit g basep
|
p <- propgit g basep
|
||||||
tryNonAsync (createProcess p) >>= \case
|
tryNonAsync (createProcess p) >>= \case
|
||||||
Right v -> (Right <$> started errrelayer v)
|
Right v -> (Right <$> started cmd errrelayer v)
|
||||||
`catchNonAsync` const (runerr cmdpath)
|
`catchNonAsync` const (runerr cmdpath)
|
||||||
Left _ -> runerr cmdpath
|
Left _ -> runerr cmdpath
|
||||||
|
|
||||||
started errrelayer pall@(Just hin, Just hout, Just herr, ph) = do
|
started cmd errrelayer pall@(Just hin, Just hout, Just herr, ph) = do
|
||||||
stderrelay <- async $ errrelayer herr
|
stderrelay <- async $ errrelayer herr
|
||||||
pid <- atomically $ do
|
|
||||||
n <- succ <$> readTVar pidvar
|
|
||||||
writeTVar pidvar n
|
|
||||||
return n
|
|
||||||
let shutdown forcestop = do
|
let shutdown forcestop = do
|
||||||
cancel stderrelay
|
cancel stderrelay
|
||||||
if forcestop
|
if forcestop
|
||||||
|
@ -71,8 +68,9 @@ startExternalAddonProcess basecmd pidvar = do
|
||||||
, externalReceive = hout
|
, externalReceive = hout
|
||||||
, externalPid = pid
|
, externalPid = pid
|
||||||
, externalShutdown = shutdown
|
, externalShutdown = shutdown
|
||||||
|
, externalProgram = cmd
|
||||||
}
|
}
|
||||||
started _ _ = giveup "internal"
|
started _ _ _ = giveup "internal"
|
||||||
|
|
||||||
propgit g p = do
|
propgit g p = do
|
||||||
environ <- propGitEnv g
|
environ <- propGitEnv g
|
||||||
|
@ -85,3 +83,11 @@ startExternalAddonProcess basecmd pidvar = do
|
||||||
path <- intercalate ":" <$> getSearchPath
|
path <- intercalate ":" <$> getSearchPath
|
||||||
return $ Left $ ProgramNotInstalled $
|
return $ Left $ ProgramNotInstalled $
|
||||||
"Cannot run " ++ basecmd ++ " -- It is not installed in PATH (" ++ path ++ ")"
|
"Cannot run " ++ basecmd ++ " -- It is not installed in PATH (" ++ path ++ ")"
|
||||||
|
|
||||||
|
protocolDebug :: ExternalAddonProcess -> Bool -> String -> IO ()
|
||||||
|
protocolDebug external sendto line = debugM "external" $ unwords
|
||||||
|
[ externalProgram external ++
|
||||||
|
"[" ++ show (externalPid external) ++ "]"
|
||||||
|
, if sendto then "<--" else "-->"
|
||||||
|
, line
|
||||||
|
]
|
||||||
|
|
|
@ -177,14 +177,15 @@ runTransfer' ignorelock t afile retrydecider transferaction = enteringStage Tran
|
||||||
- tend to be configured to reject it, so Upload is also prevented.
|
- tend to be configured to reject it, so Upload is also prevented.
|
||||||
-}
|
-}
|
||||||
checkSecureHashes :: Observable v => Transfer -> Annex v -> Annex v
|
checkSecureHashes :: Observable v => Transfer -> Annex v -> Annex v
|
||||||
checkSecureHashes t a
|
checkSecureHashes t a = ifM (isCryptographicallySecure (transferKey t))
|
||||||
| isCryptographicallySecure (transferKey t) = a
|
( a
|
||||||
| otherwise = ifM (annexSecureHashesOnly <$> Annex.getGitConfig)
|
, ifM (annexSecureHashesOnly <$> Annex.getGitConfig)
|
||||||
( do
|
( do
|
||||||
warning $ "annex.securehashesonly blocked transfer of " ++ decodeBS (formatKeyVariety variety) ++ " key"
|
warning $ "annex.securehashesonly blocked transfer of " ++ decodeBS (formatKeyVariety variety) ++ " key"
|
||||||
return observeFailure
|
return observeFailure
|
||||||
, a
|
, a
|
||||||
)
|
)
|
||||||
|
)
|
||||||
where
|
where
|
||||||
variety = fromKey keyVariety (transferKey t)
|
variety = fromKey keyVariety (transferKey t)
|
||||||
|
|
||||||
|
|
|
@ -117,7 +117,7 @@ distributionDownloadComplete d dest cleanup t
|
||||||
| otherwise = cleanup
|
| otherwise = cleanup
|
||||||
where
|
where
|
||||||
k = mkKey $ const $ distributionKey d
|
k = mkKey $ const $ distributionKey d
|
||||||
fsckit f = case Backend.maybeLookupBackendVariety (fromKey keyVariety k) of
|
fsckit f = Backend.maybeLookupBackendVariety (fromKey keyVariety k) >>= \case
|
||||||
Nothing -> return $ Just f
|
Nothing -> return $ Just f
|
||||||
Just b -> case Types.Backend.verifyKeyContent b of
|
Just b -> case Types.Backend.verifyKeyContent b of
|
||||||
Nothing -> return $ Just f
|
Nothing -> return $ Just f
|
||||||
|
|
62
Backend.hs
62
Backend.hs
|
@ -6,12 +6,13 @@
|
||||||
-}
|
-}
|
||||||
|
|
||||||
module Backend (
|
module Backend (
|
||||||
list,
|
builtinList,
|
||||||
defaultBackend,
|
defaultBackend,
|
||||||
genKey,
|
genKey,
|
||||||
getBackend,
|
getBackend,
|
||||||
chooseBackend,
|
chooseBackend,
|
||||||
lookupBackendVariety,
|
lookupBackendVariety,
|
||||||
|
lookupBuiltinBackendVariety,
|
||||||
maybeLookupBackendVariety,
|
maybeLookupBackendVariety,
|
||||||
isStableKey,
|
isStableKey,
|
||||||
isCryptographicallySecure,
|
isCryptographicallySecure,
|
||||||
|
@ -26,16 +27,18 @@ import Types.KeySource
|
||||||
import qualified Types.Backend as B
|
import qualified Types.Backend as B
|
||||||
import Utility.Metered
|
import Utility.Metered
|
||||||
|
|
||||||
-- When adding a new backend, import it here and add it to the list.
|
-- When adding a new backend, import it here and add it to the builtinList.
|
||||||
import qualified Backend.Hash
|
import qualified Backend.Hash
|
||||||
import qualified Backend.WORM
|
import qualified Backend.WORM
|
||||||
import qualified Backend.URL
|
import qualified Backend.URL
|
||||||
|
import qualified Backend.External
|
||||||
|
|
||||||
import qualified Data.Map as M
|
import qualified Data.Map as M
|
||||||
import qualified Data.ByteString.Char8 as S8
|
import qualified Data.ByteString.Char8 as S8
|
||||||
|
|
||||||
list :: [Backend]
|
{- Build-in backends. Does not include externals. -}
|
||||||
list = Backend.Hash.backends ++ Backend.WORM.backends ++ Backend.URL.backends
|
builtinList :: [Backend]
|
||||||
|
builtinList = Backend.Hash.backends ++ Backend.WORM.backends ++ Backend.URL.backends
|
||||||
|
|
||||||
{- Backend to use by default when generating a new key. -}
|
{- Backend to use by default when generating a new key. -}
|
||||||
defaultBackend :: Annex Backend
|
defaultBackend :: Annex Backend
|
||||||
|
@ -44,9 +47,9 @@ defaultBackend = maybe cache return =<< Annex.getState Annex.backend
|
||||||
cache = do
|
cache = do
|
||||||
n <- maybe (annexBackend <$> Annex.getGitConfig) (return . Just)
|
n <- maybe (annexBackend <$> Annex.getGitConfig) (return . Just)
|
||||||
=<< Annex.getState Annex.forcebackend
|
=<< Annex.getState Annex.forcebackend
|
||||||
let b = case n of
|
b <- case n of
|
||||||
Just name | valid name -> lookupname name
|
Just name | valid name -> lookupname name
|
||||||
_ -> Prelude.head list
|
_ -> pure (Prelude.head builtinList)
|
||||||
Annex.changeState $ \s -> s { Annex.backend = Just b }
|
Annex.changeState $ \s -> s { Annex.backend = Just b }
|
||||||
return b
|
return b
|
||||||
valid name = not (null name)
|
valid name = not (null name)
|
||||||
|
@ -72,12 +75,16 @@ genKey source meterupdate preferredbackend = do
|
||||||
| otherwise = c
|
| otherwise = c
|
||||||
|
|
||||||
getBackend :: FilePath -> Key -> Annex (Maybe Backend)
|
getBackend :: FilePath -> Key -> Annex (Maybe Backend)
|
||||||
getBackend file k = case maybeLookupBackendVariety (fromKey keyVariety k) of
|
getBackend file k = maybeLookupBackendVariety (fromKey keyVariety k) >>= \case
|
||||||
Just backend -> return $ Just backend
|
Just backend -> return $ Just backend
|
||||||
Nothing -> do
|
Nothing -> do
|
||||||
warning $ "skipping " ++ file ++ " (unknown backend " ++ decodeBS (formatKeyVariety (fromKey keyVariety k)) ++ ")"
|
warning $ "skipping " ++ file ++ " (" ++ unknownBackendVarietyMessage (fromKey keyVariety k) ++ ")"
|
||||||
return Nothing
|
return Nothing
|
||||||
|
|
||||||
|
unknownBackendVarietyMessage :: KeyVariety -> String
|
||||||
|
unknownBackendVarietyMessage v =
|
||||||
|
"unknown backend " ++ decodeBS (formatKeyVariety v)
|
||||||
|
|
||||||
{- Looks up the backend that should be used for a file.
|
{- Looks up the backend that should be used for a file.
|
||||||
- That can be configured on a per-file basis in the gitattributes file,
|
- That can be configured on a per-file basis in the gitattributes file,
|
||||||
- or forced with --backend. -}
|
- or forced with --backend. -}
|
||||||
|
@ -85,29 +92,38 @@ chooseBackend :: FilePath -> Annex (Maybe Backend)
|
||||||
chooseBackend f = Annex.getState Annex.forcebackend >>= go
|
chooseBackend f = Annex.getState Annex.forcebackend >>= go
|
||||||
where
|
where
|
||||||
go Nothing = maybeLookupBackendVariety . parseKeyVariety . encodeBS
|
go Nothing = maybeLookupBackendVariety . parseKeyVariety . encodeBS
|
||||||
<$> checkAttr "annex.backend" f
|
=<< checkAttr "annex.backend" f
|
||||||
go (Just _) = Just <$> defaultBackend
|
go (Just _) = Just <$> defaultBackend
|
||||||
|
|
||||||
{- Looks up a backend by variety. May fail if unsupported or disabled. -}
|
{- Looks up a backend by variety. May fail if unsupported or disabled. -}
|
||||||
lookupBackendVariety :: KeyVariety -> Backend
|
lookupBackendVariety :: KeyVariety -> Annex Backend
|
||||||
lookupBackendVariety v = fromMaybe unknown $ maybeLookupBackendVariety v
|
lookupBackendVariety v = fromMaybe (giveup (unknownBackendVarietyMessage v))
|
||||||
where
|
<$> maybeLookupBackendVariety v
|
||||||
unknown = giveup $ "unknown backend " ++ decodeBS (formatKeyVariety v)
|
|
||||||
|
|
||||||
maybeLookupBackendVariety :: KeyVariety -> Maybe Backend
|
lookupBuiltinBackendVariety :: KeyVariety -> Backend
|
||||||
maybeLookupBackendVariety v = M.lookup v varietyMap
|
lookupBuiltinBackendVariety v = fromMaybe (giveup (unknownBackendVarietyMessage v)) $
|
||||||
|
maybeLookupBuiltinBackendVariety v
|
||||||
|
|
||||||
|
maybeLookupBackendVariety :: KeyVariety -> Annex (Maybe Backend)
|
||||||
|
maybeLookupBackendVariety (ExternalKey s hasext) =
|
||||||
|
Just <$> Backend.External.makeBackend s hasext
|
||||||
|
maybeLookupBackendVariety v =
|
||||||
|
pure $ M.lookup v varietyMap
|
||||||
|
|
||||||
|
maybeLookupBuiltinBackendVariety :: KeyVariety -> Maybe Backend
|
||||||
|
maybeLookupBuiltinBackendVariety v = M.lookup v varietyMap
|
||||||
|
|
||||||
varietyMap :: M.Map KeyVariety Backend
|
varietyMap :: M.Map KeyVariety Backend
|
||||||
varietyMap = M.fromList $ zip (map B.backendVariety list) list
|
varietyMap = M.fromList $ zip (map B.backendVariety builtinList) builtinList
|
||||||
|
|
||||||
isStableKey :: Key -> Bool
|
isStableKey :: Key -> Annex Bool
|
||||||
isStableKey k = maybe False (`B.isStableKey` k)
|
isStableKey k = maybe False (`B.isStableKey` k)
|
||||||
(maybeLookupBackendVariety (fromKey keyVariety k))
|
<$> maybeLookupBackendVariety (fromKey keyVariety k)
|
||||||
|
|
||||||
isCryptographicallySecure :: Key -> Bool
|
isCryptographicallySecure :: Key -> Annex Bool
|
||||||
isCryptographicallySecure k = maybe False (`B.isCryptographicallySecure` k)
|
isCryptographicallySecure k = maybe False (`B.isCryptographicallySecure` k)
|
||||||
(maybeLookupBackendVariety (fromKey keyVariety k))
|
<$> maybeLookupBackendVariety (fromKey keyVariety k)
|
||||||
|
|
||||||
isVerifiable :: Key -> Bool
|
isVerifiable :: Key -> Annex Bool
|
||||||
isVerifiable k = maybe False (isJust . B.verifyKeyContent)
|
isVerifiable k = maybe False (isJust . B.verifyKeyContent)
|
||||||
(maybeLookupBackendVariety (fromKey keyVariety k))
|
<$> maybeLookupBackendVariety (fromKey keyVariety k)
|
||||||
|
|
290
Backend/External.hs
Normal file
290
Backend/External.hs
Normal file
|
@ -0,0 +1,290 @@
|
||||||
|
{- git-annex external backend
|
||||||
|
-
|
||||||
|
- Copyright 2020 Joey Hess <id@joeyh.name>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU AGPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
{-# LANGUAGE TypeSynonymInstances #-}
|
||||||
|
|
||||||
|
module Backend.External (makeBackend) where
|
||||||
|
|
||||||
|
import Annex.Common
|
||||||
|
import Annex.ExternalAddonProcess
|
||||||
|
import Types.Key
|
||||||
|
import Types.Backend
|
||||||
|
import Types.KeySource
|
||||||
|
import Utility.Metered
|
||||||
|
import qualified Utility.SimpleProtocol as Proto
|
||||||
|
|
||||||
|
import qualified Data.ByteString as S
|
||||||
|
import qualified Data.Map.Strict as M
|
||||||
|
import Control.Concurrent
|
||||||
|
import System.IO.Unsafe (unsafePerformIO)
|
||||||
|
|
||||||
|
newtype ExternalBackendName = ExternalBackendName S.ByteString
|
||||||
|
deriving (Show, Eq, Ord)
|
||||||
|
|
||||||
|
-- Makes Backend representing an external backend of any type.
|
||||||
|
-- If the program is not available or doesn't work, makes a Backend
|
||||||
|
-- that cannot generate or verify keys, but that still lets the keys be
|
||||||
|
-- basically used.
|
||||||
|
makeBackend :: S.ByteString -> HasExt -> Annex Backend
|
||||||
|
makeBackend bname hasext = withExternalState ebname $ \st -> do
|
||||||
|
withExternalAddon st (pure unavailbackend) $ \_ext -> do
|
||||||
|
canverify <- handleRequest st CANVERIFY (pure False) $ \case
|
||||||
|
CANVERIFY_YES -> result True
|
||||||
|
CANVERIFY_NO -> result False
|
||||||
|
_ -> Nothing
|
||||||
|
isstable <- handleRequest st ISSTABLE (pure False) $ \case
|
||||||
|
ISSTABLE_YES -> result True
|
||||||
|
ISSTABLE_NO -> result False
|
||||||
|
_ -> Nothing
|
||||||
|
iscryptographicallysecure <- handleRequest st ISCRYPTOGRAPHICALLYSECURE (pure False) $ \case
|
||||||
|
ISCRYPTOGRAPHICALLYSECURE_YES -> result True
|
||||||
|
ISCRYPTOGRAPHICALLYSECURE_NO -> result False
|
||||||
|
_ -> Nothing
|
||||||
|
return $ Backend
|
||||||
|
{ backendVariety = ExternalKey bname hasext
|
||||||
|
, genKey = Just $ genKeyExternal ebname
|
||||||
|
, verifyKeyContent = if canverify
|
||||||
|
then Just $ verifyKeyContentExternal ebname
|
||||||
|
else Nothing
|
||||||
|
, canUpgradeKey = Nothing
|
||||||
|
, fastMigrate = Nothing
|
||||||
|
, isStableKey = const isstable
|
||||||
|
, isCryptographicallySecure = const iscryptographicallysecure
|
||||||
|
}
|
||||||
|
where
|
||||||
|
ebname = ExternalBackendName bname
|
||||||
|
unavailbackend = Backend
|
||||||
|
{ backendVariety = ExternalKey bname hasext
|
||||||
|
, genKey = Nothing
|
||||||
|
, verifyKeyContent = Nothing
|
||||||
|
, canUpgradeKey = Nothing
|
||||||
|
, fastMigrate = Nothing
|
||||||
|
, isStableKey = const False
|
||||||
|
, isCryptographicallySecure = const False
|
||||||
|
}
|
||||||
|
|
||||||
|
genKeyExternal :: ExternalBackendName -> KeySource -> MeterUpdate -> Annex Key
|
||||||
|
genKeyExternal bname ks p = error "TODO"
|
||||||
|
|
||||||
|
verifyKeyContentExternal :: ExternalBackendName -> Key -> FilePath -> Annex Bool
|
||||||
|
verifyKeyContentExternal bname k f = error "TODO"
|
||||||
|
|
||||||
|
-- State about a running external backend program.
|
||||||
|
data ExternalState = ExternalState
|
||||||
|
{ externalAddonProcess :: Either ExternalAddonStartError ExternalAddonProcess
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRequest :: ExternalState -> Request -> Annex a -> ResponseHandler a -> Annex a
|
||||||
|
handleRequest st req whenunavail responsehandler =
|
||||||
|
withExternalAddon st whenunavail $ \p -> do
|
||||||
|
sendMessage p req
|
||||||
|
receiveResponse p responsehandler (Just . handleAsyncMessage)
|
||||||
|
where
|
||||||
|
handleAsyncMessage (ERROR err) = do
|
||||||
|
warning ("external special remote error: " ++ err)
|
||||||
|
whenunavail
|
||||||
|
|
||||||
|
withExternalAddon :: ExternalState -> a -> (ExternalAddonProcess -> a) -> a
|
||||||
|
withExternalAddon st whenunavail a = case externalAddonProcess st of
|
||||||
|
Right addon -> a addon
|
||||||
|
Left _ -> whenunavail
|
||||||
|
|
||||||
|
sendMessage :: Proto.Sendable m => ExternalAddonProcess -> m -> Annex ()
|
||||||
|
sendMessage p m = liftIO $ do
|
||||||
|
protocolDebug p True line
|
||||||
|
hPutStrLn (externalSend p) line
|
||||||
|
hFlush (externalSend p)
|
||||||
|
where
|
||||||
|
line = unwords $ Proto.formatMessage m
|
||||||
|
|
||||||
|
{- A response handler can yeild a result, or it can request that another
|
||||||
|
- message be consumed from the external. -}
|
||||||
|
data ResponseHandlerResult a
|
||||||
|
= Result a
|
||||||
|
| GetNextMessage (ResponseHandler a)
|
||||||
|
|
||||||
|
type ResponseHandler a = Response -> Maybe (Annex (ResponseHandlerResult a))
|
||||||
|
|
||||||
|
result :: a -> Maybe (Annex (ResponseHandlerResult a))
|
||||||
|
result = Just . return . Result
|
||||||
|
|
||||||
|
{- Waits for a message from the external backend, and passes it to the
|
||||||
|
- apppropriate handler.
|
||||||
|
-
|
||||||
|
- If the handler returns Nothing, this is a protocol error.
|
||||||
|
-}
|
||||||
|
receiveResponse
|
||||||
|
:: ExternalAddonProcess
|
||||||
|
-> ResponseHandler a
|
||||||
|
-> (AsyncMessage -> Maybe (Annex a))
|
||||||
|
-> Annex a
|
||||||
|
receiveResponse p handleresponse handleasync =
|
||||||
|
go =<< liftIO (catchMaybeIO $ hGetLine $ externalReceive p)
|
||||||
|
where
|
||||||
|
go Nothing = protocolError False ""
|
||||||
|
go (Just s) = do
|
||||||
|
liftIO $ protocolDebug p False s
|
||||||
|
case Proto.parseMessage s :: Maybe Response of
|
||||||
|
Just resp -> case handleresponse resp of
|
||||||
|
Nothing -> protocolError True s
|
||||||
|
Just callback -> callback >>= \case
|
||||||
|
Result a -> return a
|
||||||
|
GetNextMessage handleresponse' ->
|
||||||
|
receiveResponse p handleresponse' handleasync
|
||||||
|
Nothing -> case Proto.parseMessage s :: Maybe AsyncMessage of
|
||||||
|
Just msg -> maybe (protocolError True s) id (handleasync msg)
|
||||||
|
Nothing -> protocolError False s
|
||||||
|
|
||||||
|
protocolError parsed s = giveup $ "external backend protocol error, unexpectedly received \"" ++ s ++ "\" " ++
|
||||||
|
if parsed
|
||||||
|
then "(message not allowed at this time)"
|
||||||
|
else "(unable to parse message)"
|
||||||
|
|
||||||
|
-- Information about pools of of running external backends that are
|
||||||
|
-- available to use is stored in this global.
|
||||||
|
{-# NOINLINE poolVar #-}
|
||||||
|
poolVar :: MVar (M.Map ExternalBackendName (ExternalAddonPID, [ExternalState]))
|
||||||
|
poolVar = unsafePerformIO $ newMVar M.empty
|
||||||
|
|
||||||
|
-- Starts a new instance of an external backend.
|
||||||
|
-- Does not add it to the poolVar; caller should add it once it's done
|
||||||
|
-- using it.
|
||||||
|
newExternalState :: ExternalBackendName -> ExternalAddonPID -> Annex ExternalState
|
||||||
|
newExternalState (ExternalBackendName name) pid = do
|
||||||
|
st <- startExternalAddonProcess basecmd pid
|
||||||
|
st' <- case st of
|
||||||
|
Left (ProgramNotInstalled msg) -> warnonce msg >> return st
|
||||||
|
Left (ProgramFailure msg) -> warnonce msg >> return st
|
||||||
|
Right p -> do
|
||||||
|
sendMessage p GETVERSION
|
||||||
|
v <- receiveResponse p
|
||||||
|
(\resp -> case resp of
|
||||||
|
VERSION v -> result v
|
||||||
|
_ -> Nothing
|
||||||
|
)
|
||||||
|
(const Nothing)
|
||||||
|
if v `notElem` supportedProtocolVersions
|
||||||
|
then do
|
||||||
|
warnonce (basecmd ++ " uses an unsupported version of the external backend protocol")
|
||||||
|
return $ Left (ProgramFailure "bad protocol version")
|
||||||
|
else return (Right p)
|
||||||
|
return $ ExternalState { externalAddonProcess = st' }
|
||||||
|
where
|
||||||
|
basecmd = "git-annex-backend-X" ++ decodeBS' name
|
||||||
|
warnonce msg = when (pid == 1) $
|
||||||
|
warning msg
|
||||||
|
|
||||||
|
-- Runs an action with an ExternalState, starting a new external backend
|
||||||
|
-- process if necessary. It is returned to the pool once the action
|
||||||
|
-- finishes successfully. On exception, it's shut down.
|
||||||
|
withExternalState :: ExternalBackendName -> (ExternalState -> Annex a) -> Annex a
|
||||||
|
withExternalState bname a = do
|
||||||
|
st <- get
|
||||||
|
r <- a st `onException` shutdown st
|
||||||
|
put st -- only when no exception is thrown
|
||||||
|
return r
|
||||||
|
where
|
||||||
|
get = do
|
||||||
|
m <- liftIO $ takeMVar poolVar
|
||||||
|
case fromMaybe (1, []) (M.lookup bname m) of
|
||||||
|
(pid, []) -> do
|
||||||
|
let m' = M.insert bname (succ pid, []) m
|
||||||
|
liftIO $ putMVar poolVar m'
|
||||||
|
newExternalState bname pid
|
||||||
|
(pid, (st:rest)) -> do
|
||||||
|
let m' = M.insert bname (pid, rest) m
|
||||||
|
liftIO $ putMVar poolVar m'
|
||||||
|
return st
|
||||||
|
put st = liftIO $ modifyMVar_ poolVar $
|
||||||
|
pure . M.adjust (\(pid, l) -> (pid, st:l)) bname
|
||||||
|
shutdown st = liftIO $
|
||||||
|
withExternalAddon st noop (flip externalShutdown False)
|
||||||
|
|
||||||
|
-- This is a key as seen by the protocol consumer. When the "E" variant
|
||||||
|
-- of the external backend is in use, it does not include an extension.
|
||||||
|
-- And it's assumed not to contain spaces or newlines, or anything besides
|
||||||
|
-- ascii alphanumerics, because the protocol does not allow keys containing
|
||||||
|
-- such things.
|
||||||
|
newtype ProtoKey = ProtoKey Key
|
||||||
|
deriving (Show)
|
||||||
|
|
||||||
|
instance Proto.Serializable ProtoKey where
|
||||||
|
serialize (ProtoKey k) = Proto.serialize k
|
||||||
|
deserialize = fmap ProtoKey . Proto.deserialize
|
||||||
|
|
||||||
|
data Request
|
||||||
|
= GETVERSION
|
||||||
|
| CANVERIFY
|
||||||
|
| ISSTABLE
|
||||||
|
| ISCRYPTOGRAPHICALLYSECURE
|
||||||
|
| GENKEY FilePath
|
||||||
|
| VERIFYKEYCONTENT ProtoKey FilePath
|
||||||
|
deriving (Show)
|
||||||
|
|
||||||
|
data Response
|
||||||
|
= VERSION ProtocolVersion
|
||||||
|
| CANVERIFY_YES
|
||||||
|
| CANVERIFY_NO
|
||||||
|
| ISSTABLE_YES
|
||||||
|
| ISSTABLE_NO
|
||||||
|
| ISCRYPTOGRAPHICALLYSECURE_YES
|
||||||
|
| ISCRYPTOGRAPHICALLYSECURE_NO
|
||||||
|
| GENKEY_SUCCESS ProtoKey
|
||||||
|
| GENKEY_FAILURE ErrorMsg
|
||||||
|
| VERIFYKEYCONTENT_SUCCESS
|
||||||
|
| VERIFYKEYCONTENT_FAILURE
|
||||||
|
| PROGRESS BytesProcessed
|
||||||
|
| DEBUG String
|
||||||
|
deriving (Show)
|
||||||
|
|
||||||
|
data AsyncMessage
|
||||||
|
= ERROR ErrorMsg
|
||||||
|
deriving (Show)
|
||||||
|
|
||||||
|
type ErrorMsg = String
|
||||||
|
|
||||||
|
newtype ProtocolVersion = ProtocolVersion Int
|
||||||
|
deriving (Show, Eq)
|
||||||
|
|
||||||
|
supportedProtocolVersions :: [ProtocolVersion]
|
||||||
|
supportedProtocolVersions = [ProtocolVersion 1]
|
||||||
|
|
||||||
|
instance Proto.Serializable ProtocolVersion where
|
||||||
|
serialize (ProtocolVersion n) = show n
|
||||||
|
deserialize = ProtocolVersion <$$> readish
|
||||||
|
|
||||||
|
instance Proto.Sendable AsyncMessage where
|
||||||
|
formatMessage (ERROR err) = [ "ERROR", Proto.serialize err ]
|
||||||
|
|
||||||
|
instance Proto.Receivable AsyncMessage where
|
||||||
|
parseCommand "ERROR" = Proto.parse1 ERROR
|
||||||
|
parseCommand _ = Proto.parseFail
|
||||||
|
|
||||||
|
instance Proto.Sendable Request where
|
||||||
|
formatMessage GETVERSION = ["GETVERSION"]
|
||||||
|
formatMessage CANVERIFY = ["CANVERIFY"]
|
||||||
|
formatMessage ISSTABLE = ["ISSTABLE"]
|
||||||
|
formatMessage ISCRYPTOGRAPHICALLYSECURE = ["ISCRYPTOGRAPHICALLYSECURE"]
|
||||||
|
formatMessage (GENKEY file) = ["GENKEY", Proto.serialize file]
|
||||||
|
formatMessage (VERIFYKEYCONTENT key file) =
|
||||||
|
["VERIFYKEYCONTENT", Proto.serialize key, Proto.serialize file]
|
||||||
|
|
||||||
|
instance Proto.Receivable Response where
|
||||||
|
parseCommand "VERSION" = Proto.parse1 VERSION
|
||||||
|
parseCommand "CANVERIFY-YES" = Proto.parse0 CANVERIFY_YES
|
||||||
|
parseCommand "CANVERIFY-NO" = Proto.parse0 CANVERIFY_NO
|
||||||
|
parseCommand "ISSTABLE-YES" = Proto.parse0 ISSTABLE_YES
|
||||||
|
parseCommand "ISSTABLE-NO" = Proto.parse0 ISSTABLE_NO
|
||||||
|
parseCommand "ISCRYPTOGRAPHICALLYSECURE-YES" = Proto.parse0 ISCRYPTOGRAPHICALLYSECURE_YES
|
||||||
|
parseCommand "ISCRYPTOGRAPHICALLYSECURE-NO" = Proto.parse0 ISCRYPTOGRAPHICALLYSECURE_NO
|
||||||
|
parseCommand "GENKEY-SUCCESS" = Proto.parse1 GENKEY_SUCCESS
|
||||||
|
parseCommand "GENKEY-FAILURE" = Proto.parse1 GENKEY_FAILURE
|
||||||
|
parseCommand "VERIFYKEYCONTENT-SUCCESS" = Proto.parse0 VERIFYKEYCONTENT_SUCCESS
|
||||||
|
parseCommand "VERIFYKEYCONTENT-FAILURE" = Proto.parse0 VERIFYKEYCONTENT_FAILURE
|
||||||
|
parseCommand "PROGRESS" = Proto.parse1 PROGRESS
|
||||||
|
parseCommand "DEBUG" = Proto.parse1 DEBUG
|
||||||
|
parseCommand _ = Proto.parseFail
|
|
@ -440,4 +440,4 @@ completeRemotes = completer $ mkCompleter $ \input -> do
|
||||||
|
|
||||||
completeBackends :: HasCompleter f => Mod f a
|
completeBackends :: HasCompleter f => Mod f a
|
||||||
completeBackends = completeWith $
|
completeBackends = completeWith $
|
||||||
map (decodeBS . formatKeyVariety . Backend.backendVariety) Backend.list
|
map (decodeBS . formatKeyVariety . Backend.backendVariety) Backend.builtinList
|
||||||
|
|
|
@ -199,7 +199,7 @@ performRemote key afile backend numcopies remote =
|
||||||
|
|
||||||
startKey :: Maybe Remote -> Incremental -> (Key, ActionItem) -> NumCopies -> CommandStart
|
startKey :: Maybe Remote -> Incremental -> (Key, ActionItem) -> NumCopies -> CommandStart
|
||||||
startKey from inc (key, ai) numcopies =
|
startKey from inc (key, ai) numcopies =
|
||||||
case Backend.maybeLookupBackendVariety (fromKey keyVariety key) of
|
Backend.maybeLookupBackendVariety (fromKey keyVariety key) >>= \case
|
||||||
Nothing -> stop
|
Nothing -> stop
|
||||||
Just backend -> runFsck inc ai key $
|
Just backend -> runFsck inc ai key $
|
||||||
case from of
|
case from of
|
||||||
|
@ -261,7 +261,7 @@ verifyLocationLog key keystatus ai = do
|
||||||
- insecure hash is present. This should only be able to happen
|
- insecure hash is present. This should only be able to happen
|
||||||
- if the repository already contained the content before the
|
- if the repository already contained the content before the
|
||||||
- config was set. -}
|
- config was set. -}
|
||||||
when (present && not (Backend.isCryptographicallySecure key)) $
|
whenM (pure present <&&> (not <$> Backend.isCryptographicallySecure key)) $
|
||||||
whenM (annexSecureHashesOnly <$> Annex.getGitConfig) $
|
whenM (annexSecureHashesOnly <$> Annex.getGitConfig) $
|
||||||
warning $ "** Despite annex.securehashesonly being set, " ++ obj ++ " has content present in the annex using an insecure " ++ decodeBS (formatKeyVariety (fromKey keyVariety key)) ++ " key"
|
warning $ "** Despite annex.securehashesonly being set, " ++ obj ++ " has content present in the annex using an insecure " ++ decodeBS (formatKeyVariety (fromKey keyVariety key)) ++ " key"
|
||||||
|
|
||||||
|
|
|
@ -131,7 +131,10 @@ clean file = do
|
||||||
-- Look up the backend that was used for this file
|
-- Look up the backend that was used for this file
|
||||||
-- before, so that when git re-cleans a file its
|
-- before, so that when git re-cleans a file its
|
||||||
-- backend does not change.
|
-- backend does not change.
|
||||||
let oldbackend = maybe Nothing (maybeLookupBackendVariety . fromKey keyVariety) oldkey
|
oldbackend <- maybe
|
||||||
|
(pure Nothing)
|
||||||
|
(maybeLookupBackendVariety . fromKey keyVariety)
|
||||||
|
oldkey
|
||||||
-- Can't restage associated files because git add
|
-- Can't restage associated files because git add
|
||||||
-- runs this and has the index locked.
|
-- runs this and has the index locked.
|
||||||
let norestage = Restage False
|
let norestage = Restage False
|
||||||
|
|
|
@ -288,7 +288,7 @@ test runannex mkr mkk =
|
||||||
Nothing -> return True
|
Nothing -> return True
|
||||||
runannex a' @? "failed"
|
runannex a' @? "failed"
|
||||||
present r k b = (== Right b) <$> Remote.hasKey r k
|
present r k b = (== Right b) <$> Remote.hasKey r k
|
||||||
fsck _ k = case maybeLookupBackendVariety (fromKey keyVariety k) of
|
fsck _ k = maybeLookupBackendVariety (fromKey keyVariety k) >>= \case
|
||||||
Nothing -> return True
|
Nothing -> return True
|
||||||
Just b -> case Types.Backend.verifyKeyContent b of
|
Just b -> case Types.Backend.verifyKeyContent b of
|
||||||
Nothing -> return True
|
Nothing -> return True
|
||||||
|
|
|
@ -59,7 +59,8 @@ showPackageVersion = do
|
||||||
vinfo "build flags" $ unwords buildFlags
|
vinfo "build flags" $ unwords buildFlags
|
||||||
vinfo "dependency versions" $ unwords dependencyVersions
|
vinfo "dependency versions" $ unwords dependencyVersions
|
||||||
vinfo "key/value backends" $ unwords $
|
vinfo "key/value backends" $ unwords $
|
||||||
map (decodeBS . formatKeyVariety . B.backendVariety) Backend.list
|
map (decodeBS . formatKeyVariety . B.backendVariety) Backend.builtinList
|
||||||
|
++ ["X*"]
|
||||||
vinfo "remote types" $ unwords $ map R.typename Remote.remoteTypes
|
vinfo "remote types" $ unwords $ map R.typename Remote.remoteTypes
|
||||||
vinfo "operating system" $ unwords [os, arch]
|
vinfo "operating system" $ unwords [os, arch]
|
||||||
vinfo "supported repository versions" $
|
vinfo "supported repository versions" $
|
||||||
|
|
2
Limit.hs
2
Limit.hs
|
@ -306,7 +306,7 @@ addSecureHash :: Annex ()
|
||||||
addSecureHash = addLimit $ Right limitSecureHash
|
addSecureHash = addLimit $ Right limitSecureHash
|
||||||
|
|
||||||
limitSecureHash :: MatchFiles Annex
|
limitSecureHash :: MatchFiles Annex
|
||||||
limitSecureHash _ = checkKey $ pure . isCryptographicallySecure
|
limitSecureHash _ = checkKey isCryptographicallySecure
|
||||||
|
|
||||||
{- Adds a limit to skip files that are too large or too small -}
|
{- Adds a limit to skip files that are too large or too small -}
|
||||||
addLargerThan :: String -> Annex ()
|
addLargerThan :: String -> Annex ()
|
||||||
|
|
|
@ -185,7 +185,7 @@ externalSetup _ mu _ c gc = do
|
||||||
-- error out if the user provided an unexpected config.
|
-- error out if the user provided an unexpected config.
|
||||||
_ <- either giveup return . parseRemoteConfig c'
|
_ <- either giveup return . parseRemoteConfig c'
|
||||||
=<< strictRemoteConfigParser external
|
=<< strictRemoteConfigParser external
|
||||||
handleRequest external INITREMOTE Nothing $ \resp -> case resp of
|
handleRequest external INITREMOTE Nothing $ \case
|
||||||
INITREMOTE_SUCCESS -> result ()
|
INITREMOTE_SUCCESS -> result ()
|
||||||
INITREMOTE_FAILURE errmsg -> Just $ giveup $
|
INITREMOTE_FAILURE errmsg -> Just $ giveup $
|
||||||
respErrorMessage "INITREMOTE" errmsg
|
respErrorMessage "INITREMOTE" errmsg
|
||||||
|
@ -381,7 +381,7 @@ handleRequestExport :: External -> ExportLocation -> (SafeKey -> Request) -> Key
|
||||||
handleRequestExport external loc mkreq k mp responsehandler = do
|
handleRequestExport external loc mkreq k mp responsehandler = do
|
||||||
withExternalState external $ \st -> do
|
withExternalState external $ \st -> do
|
||||||
checkPrepared st external
|
checkPrepared st external
|
||||||
sendMessage st external (EXPORT loc)
|
sendMessage st (EXPORT loc)
|
||||||
handleRequestKey external mkreq k mp responsehandler
|
handleRequestKey external mkreq k mp responsehandler
|
||||||
|
|
||||||
handleRequest' :: ExternalState -> External -> Request -> Maybe MeterUpdate -> ResponseHandler a -> Annex a
|
handleRequest' :: ExternalState -> External -> Request -> Maybe MeterUpdate -> ResponseHandler a -> Annex a
|
||||||
|
@ -392,7 +392,7 @@ handleRequest' st external req mp responsehandler
|
||||||
| otherwise = go
|
| otherwise = go
|
||||||
where
|
where
|
||||||
go = do
|
go = do
|
||||||
sendMessage st external req
|
sendMessage st req
|
||||||
loop
|
loop
|
||||||
loop = receiveMessage st external responsehandler
|
loop = receiveMessage st external responsehandler
|
||||||
(\rreq -> Just $ handleRemoteRequest rreq >> loop)
|
(\rreq -> Just $ handleRemoteRequest rreq >> loop)
|
||||||
|
@ -489,8 +489,8 @@ handleRequest' st external req mp responsehandler
|
||||||
|
|
||||||
handleAsyncMessage (ERROR err) = giveup $ "external special remote error: " ++ err
|
handleAsyncMessage (ERROR err) = giveup $ "external special remote error: " ++ err
|
||||||
|
|
||||||
send = sendMessage st external
|
send = sendMessage st
|
||||||
senderror = sendMessage st external . ERROR
|
senderror = sendMessage st . ERROR
|
||||||
|
|
||||||
credstorage setting u = CredPairStorage
|
credstorage setting u = CredPairStorage
|
||||||
{ credPairFile = base
|
{ credPairFile = base
|
||||||
|
@ -503,9 +503,9 @@ handleRequest' st external req mp responsehandler
|
||||||
withurl mk uri = handleRemoteRequest $ mk $
|
withurl mk uri = handleRemoteRequest $ mk $
|
||||||
setDownloader (show uri) OtherDownloader
|
setDownloader (show uri) OtherDownloader
|
||||||
|
|
||||||
sendMessage :: Sendable m => ExternalState -> External -> m -> Annex ()
|
sendMessage :: Sendable m => ExternalState -> m -> Annex ()
|
||||||
sendMessage st external m = liftIO $ do
|
sendMessage st m = liftIO $ do
|
||||||
protocolDebug external st True line
|
protocolDebug (externalAddonProcess st) True line
|
||||||
hPutStrLn h line
|
hPutStrLn h line
|
||||||
hFlush h
|
hFlush h
|
||||||
where
|
where
|
||||||
|
@ -513,7 +513,7 @@ sendMessage st external m = liftIO $ do
|
||||||
h = externalSend (externalAddonProcess st)
|
h = externalSend (externalAddonProcess st)
|
||||||
|
|
||||||
{- A response handler can yeild a result, or it can request that another
|
{- A response handler can yeild a result, or it can request that another
|
||||||
- message be consumed from the external result. -}
|
- message be consumed from the external. -}
|
||||||
data ResponseHandlerResult a
|
data ResponseHandlerResult a
|
||||||
= Result a
|
= Result a
|
||||||
| GetNextMessage (ResponseHandler a)
|
| GetNextMessage (ResponseHandler a)
|
||||||
|
@ -539,7 +539,7 @@ receiveMessage st external handleresponse handlerequest handleasync =
|
||||||
where
|
where
|
||||||
go Nothing = protocolError False ""
|
go Nothing = protocolError False ""
|
||||||
go (Just s) = do
|
go (Just s) = do
|
||||||
liftIO $ protocolDebug external st False s
|
liftIO $ protocolDebug (externalAddonProcess st) False s
|
||||||
case parseMessage s :: Maybe Response of
|
case parseMessage s :: Maybe Response of
|
||||||
Just resp -> case handleresponse resp of
|
Just resp -> case handleresponse resp of
|
||||||
Nothing -> protocolError True s
|
Nothing -> protocolError True s
|
||||||
|
@ -557,14 +557,6 @@ receiveMessage st external handleresponse handlerequest handleasync =
|
||||||
then "(command not allowed at this time)"
|
then "(command not allowed at this time)"
|
||||||
else "(unable to parse command)"
|
else "(unable to parse command)"
|
||||||
|
|
||||||
protocolDebug :: External -> ExternalState -> Bool -> String -> IO ()
|
|
||||||
protocolDebug external st sendto line = debugM "external" $ unwords
|
|
||||||
[ externalRemoteProgram (externalType external) ++
|
|
||||||
"[" ++ show (externalPid (externalAddonProcess st)) ++ "]"
|
|
||||||
, if sendto then "<--" else "-->"
|
|
||||||
, line
|
|
||||||
]
|
|
||||||
|
|
||||||
{- While the action is running, the ExternalState provided to it will not
|
{- While the action is running, the ExternalState provided to it will not
|
||||||
- be available to any other calls.
|
- be available to any other calls.
|
||||||
-
|
-
|
||||||
|
@ -600,8 +592,12 @@ withExternalState external a = do
|
||||||
{- Starts an external remote process running, and checks VERSION and
|
{- Starts an external remote process running, and checks VERSION and
|
||||||
- exchanges EXTENSIONS. -}
|
- exchanges EXTENSIONS. -}
|
||||||
startExternal :: External -> Annex ExternalState
|
startExternal :: External -> Annex ExternalState
|
||||||
startExternal external =
|
startExternal external = do
|
||||||
startExternalAddonProcess basecmd (externalLastPid external) >>= \case
|
pid <- liftIO $ atomically $ do
|
||||||
|
n <- succ <$> readTVar (externalLastPid external)
|
||||||
|
writeTVar (externalLastPid external) n
|
||||||
|
return n
|
||||||
|
startExternalAddonProcess basecmd pid >>= \case
|
||||||
Left (ProgramFailure err) -> giveup err
|
Left (ProgramFailure err) -> giveup err
|
||||||
Left (ProgramNotInstalled err) ->
|
Left (ProgramNotInstalled err) ->
|
||||||
case (lookupName (unparsedRemoteConfig (externalDefaultConfig external)), remoteAnnexReadOnly <$> externalGitConfig external) of
|
case (lookupName (unparsedRemoteConfig (externalDefaultConfig external)), remoteAnnexReadOnly <$> externalGitConfig external) of
|
||||||
|
@ -626,13 +622,13 @@ startExternal external =
|
||||||
startproto st
|
startproto st
|
||||||
return st
|
return st
|
||||||
where
|
where
|
||||||
basecmd = externalRemoteProgram $ externalType external
|
basecmd = "git-annex-remote-" ++ externalType external
|
||||||
startproto st = do
|
startproto st = do
|
||||||
receiveMessage st external
|
receiveMessage st external
|
||||||
(const Nothing)
|
(const Nothing)
|
||||||
(checkVersion st external)
|
(checkVersion st)
|
||||||
(const Nothing)
|
(const Nothing)
|
||||||
sendMessage st external (EXTENSIONS supportedExtensionList)
|
sendMessage st (EXTENSIONS supportedExtensionList)
|
||||||
-- It responds with a EXTENSIONS_RESPONSE; that extensions
|
-- It responds with a EXTENSIONS_RESPONSE; that extensions
|
||||||
-- list is reserved for future expansion. UNSUPPORTED_REQUEST
|
-- list is reserved for future expansion. UNSUPPORTED_REQUEST
|
||||||
-- is also accepted.
|
-- is also accepted.
|
||||||
|
@ -650,15 +646,12 @@ stopExternal external = liftIO $ do
|
||||||
l <- atomically $ swapTVar (externalState external) []
|
l <- atomically $ swapTVar (externalState external) []
|
||||||
mapM_ (flip (externalShutdown . externalAddonProcess) False) l
|
mapM_ (flip (externalShutdown . externalAddonProcess) False) l
|
||||||
|
|
||||||
externalRemoteProgram :: ExternalType -> String
|
checkVersion :: ExternalState -> RemoteRequest -> Maybe (Annex ())
|
||||||
externalRemoteProgram externaltype = "git-annex-remote-" ++ externaltype
|
checkVersion st (VERSION v) = Just $
|
||||||
|
|
||||||
checkVersion :: ExternalState -> External -> RemoteRequest -> Maybe (Annex ())
|
|
||||||
checkVersion st external (VERSION v) = Just $
|
|
||||||
if v `elem` supportedProtocolVersions
|
if v `elem` supportedProtocolVersions
|
||||||
then noop
|
then noop
|
||||||
else sendMessage st external (ERROR "unsupported VERSION")
|
else sendMessage st (ERROR "unsupported VERSION")
|
||||||
checkVersion _ _ _ = Nothing
|
checkVersion _ _ = Nothing
|
||||||
|
|
||||||
{- If repo has not been prepared, sends PREPARE.
|
{- If repo has not been prepared, sends PREPARE.
|
||||||
-
|
-
|
||||||
|
|
4
Remote/External/Types.hs
vendored
4
Remote/External/Types.hs
vendored
|
@ -375,10 +375,6 @@ instance Proto.Serializable Availability where
|
||||||
deserialize "LOCAL" = Just LocallyAvailable
|
deserialize "LOCAL" = Just LocallyAvailable
|
||||||
deserialize _ = Nothing
|
deserialize _ = Nothing
|
||||||
|
|
||||||
instance Proto.Serializable BytesProcessed where
|
|
||||||
serialize (BytesProcessed n) = show n
|
|
||||||
deserialize = BytesProcessed <$$> readish
|
|
||||||
|
|
||||||
instance Proto.Serializable [(URLString, Size, FilePath)] where
|
instance Proto.Serializable [(URLString, Size, FilePath)] where
|
||||||
serialize = unwords . map go
|
serialize = unwords . map go
|
||||||
where
|
where
|
||||||
|
|
|
@ -120,10 +120,13 @@ storeChunks
|
||||||
-> Annex ()
|
-> Annex ()
|
||||||
storeChunks u chunkconfig encryptor k f p storer checker =
|
storeChunks u chunkconfig encryptor k f p storer checker =
|
||||||
case chunkconfig of
|
case chunkconfig of
|
||||||
(UnpaddedChunks chunksize) | isStableKey k -> do
|
(UnpaddedChunks chunksize) -> ifM (isStableKey k)
|
||||||
h <- liftIO $ openBinaryFile f ReadMode
|
( do
|
||||||
go chunksize h
|
h <- liftIO $ openBinaryFile f ReadMode
|
||||||
liftIO $ hClose h
|
go chunksize h
|
||||||
|
liftIO $ hClose h
|
||||||
|
, storer k (FileContent f) p
|
||||||
|
)
|
||||||
_ -> storer k (FileContent f) p
|
_ -> storer k (FileContent f) p
|
||||||
where
|
where
|
||||||
go chunksize h = do
|
go chunksize h = do
|
||||||
|
|
|
@ -11,7 +11,6 @@ module Remote.Helper.ExportImport where
|
||||||
|
|
||||||
import Annex.Common
|
import Annex.Common
|
||||||
import Types.Remote
|
import Types.Remote
|
||||||
import Types.Backend
|
|
||||||
import Types.Key
|
import Types.Key
|
||||||
import Types.ProposedAccepted
|
import Types.ProposedAccepted
|
||||||
import Backend
|
import Backend
|
||||||
|
@ -311,8 +310,8 @@ adjustExportImport r rs = case getRemoteConfigValue exportTreeField (config r) o
|
||||||
db <- getexportdb dbv
|
db <- getexportdb dbv
|
||||||
liftIO $ Export.getExportTree db k
|
liftIO $ Export.getExportTree db k
|
||||||
|
|
||||||
retrieveKeyFileFromExport dbv k _af dest p
|
retrieveKeyFileFromExport dbv k _af dest p = ifM (isVerifiable k)
|
||||||
| maybe False (isJust . verifyKeyContent) (maybeLookupBackendVariety (fromKey keyVariety k)) = do
|
( do
|
||||||
locs <- getexportlocs dbv k
|
locs <- getexportlocs dbv k
|
||||||
case locs of
|
case locs of
|
||||||
[] -> ifM (liftIO $ atomically $ readTVar $ getexportinconflict dbv)
|
[] -> ifM (liftIO $ atomically $ readTVar $ getexportinconflict dbv)
|
||||||
|
@ -322,4 +321,5 @@ adjustExportImport r rs = case getRemoteConfigValue exportTreeField (config r) o
|
||||||
(l:_) -> do
|
(l:_) -> do
|
||||||
retrieveExport (exportActions r) k l dest p
|
retrieveExport (exportActions r) k l dest p
|
||||||
return UnVerified
|
return UnVerified
|
||||||
| otherwise = giveup $ "exported content cannot be verified due to using the " ++ decodeBS (formatKeyVariety (fromKey keyVariety k)) ++ " backend"
|
, giveup $ "exported content cannot be verified due to using the " ++ decodeBS (formatKeyVariety (fromKey keyVariety k)) ++ " backend"
|
||||||
|
)
|
||||||
|
|
|
@ -581,7 +581,7 @@ backendWORM :: Types.Backend
|
||||||
backendWORM = backend_ "WORM"
|
backendWORM = backend_ "WORM"
|
||||||
|
|
||||||
backend_ :: String -> Types.Backend
|
backend_ :: String -> Types.Backend
|
||||||
backend_ = Backend.lookupBackendVariety . Types.Key.parseKeyVariety . encodeBS
|
backend_ = Backend.lookupBuiltinBackendVariety . Types.Key.parseKeyVariety . encodeBS
|
||||||
|
|
||||||
getKey :: Types.Backend -> FilePath -> IO Types.Key
|
getKey :: Types.Backend -> FilePath -> IO Types.Key
|
||||||
getKey b f = case Types.Backend.genKey b of
|
getKey b f = case Types.Backend.genKey b of
|
||||||
|
|
27
Types/Key.hs
27
Types/Key.hs
|
@ -1,6 +1,6 @@
|
||||||
{- git-annex Key data type
|
{- git-annex Key data type
|
||||||
-
|
-
|
||||||
- Copyright 2011-2019 Joey Hess <id@joeyh.name>
|
- Copyright 2011-2020 Joey Hess <id@joeyh.name>
|
||||||
-
|
-
|
||||||
- Licensed under the GNU AGPL version 3 or higher.
|
- Licensed under the GNU AGPL version 3 or higher.
|
||||||
-}
|
-}
|
||||||
|
@ -36,6 +36,7 @@ import qualified Data.Attoparsec.ByteString as A
|
||||||
import qualified Data.Attoparsec.ByteString.Char8 as A8
|
import qualified Data.Attoparsec.ByteString.Char8 as A8
|
||||||
import Utility.FileSystemEncoding
|
import Utility.FileSystemEncoding
|
||||||
import Data.List
|
import Data.List
|
||||||
|
import Data.Char
|
||||||
import System.Posix.Types
|
import System.Posix.Types
|
||||||
import Foreign.C.Types
|
import Foreign.C.Types
|
||||||
import Data.Monoid
|
import Data.Monoid
|
||||||
|
@ -215,6 +216,8 @@ data KeyVariety
|
||||||
| MD5Key HasExt
|
| MD5Key HasExt
|
||||||
| WORMKey
|
| WORMKey
|
||||||
| URLKey
|
| URLKey
|
||||||
|
-- A key that is handled by some external backend.
|
||||||
|
| ExternalKey S.ByteString HasExt
|
||||||
-- Some repositories may contain keys of other varieties,
|
-- Some repositories may contain keys of other varieties,
|
||||||
-- which can still be processed to some extent.
|
-- which can still be processed to some extent.
|
||||||
| OtherKey S.ByteString
|
| OtherKey S.ByteString
|
||||||
|
@ -246,6 +249,7 @@ hasExt (SHA1Key (HasExt b)) = b
|
||||||
hasExt (MD5Key (HasExt b)) = b
|
hasExt (MD5Key (HasExt b)) = b
|
||||||
hasExt WORMKey = False
|
hasExt WORMKey = False
|
||||||
hasExt URLKey = False
|
hasExt URLKey = False
|
||||||
|
hasExt (ExternalKey _ (HasExt b)) = b
|
||||||
hasExt (OtherKey s) = (snd <$> S8.unsnoc s) == Just 'E'
|
hasExt (OtherKey s) = (snd <$> S8.unsnoc s) == Just 'E'
|
||||||
|
|
||||||
sameExceptExt :: KeyVariety -> KeyVariety -> Bool
|
sameExceptExt :: KeyVariety -> KeyVariety -> Bool
|
||||||
|
@ -273,6 +277,7 @@ formatKeyVariety v = case v of
|
||||||
MD5Key e -> adde e "MD5"
|
MD5Key e -> adde e "MD5"
|
||||||
WORMKey -> "WORM"
|
WORMKey -> "WORM"
|
||||||
URLKey -> "URL"
|
URLKey -> "URL"
|
||||||
|
ExternalKey s e -> adde e ("X" <> s)
|
||||||
OtherKey s -> s
|
OtherKey s -> s
|
||||||
where
|
where
|
||||||
adde (HasExt False) s = s
|
adde (HasExt False) s = s
|
||||||
|
@ -330,10 +335,16 @@ parseKeyVariety "BLAKE2SP224" = Blake2spKey (HashSize 224) (HasExt False)
|
||||||
parseKeyVariety "BLAKE2SP224E" = Blake2spKey (HashSize 224) (HasExt True)
|
parseKeyVariety "BLAKE2SP224E" = Blake2spKey (HashSize 224) (HasExt True)
|
||||||
parseKeyVariety "BLAKE2SP256" = Blake2spKey (HashSize 256) (HasExt False)
|
parseKeyVariety "BLAKE2SP256" = Blake2spKey (HashSize 256) (HasExt False)
|
||||||
parseKeyVariety "BLAKE2SP256E" = Blake2spKey (HashSize 256) (HasExt True)
|
parseKeyVariety "BLAKE2SP256E" = Blake2spKey (HashSize 256) (HasExt True)
|
||||||
parseKeyVariety "SHA1" = SHA1Key (HasExt False)
|
parseKeyVariety "SHA1" = SHA1Key (HasExt False)
|
||||||
parseKeyVariety "SHA1E" = SHA1Key (HasExt True)
|
parseKeyVariety "SHA1E" = SHA1Key (HasExt True)
|
||||||
parseKeyVariety "MD5" = MD5Key (HasExt False)
|
parseKeyVariety "MD5" = MD5Key (HasExt False)
|
||||||
parseKeyVariety "MD5E" = MD5Key (HasExt True)
|
parseKeyVariety "MD5E" = MD5Key (HasExt True)
|
||||||
parseKeyVariety "WORM" = WORMKey
|
parseKeyVariety "WORM" = WORMKey
|
||||||
parseKeyVariety "URL" = URLKey
|
parseKeyVariety "URL" = URLKey
|
||||||
parseKeyVariety b = OtherKey b
|
parseKeyVariety b
|
||||||
|
| "X" `S.isPrefixOf` b =
|
||||||
|
let b' = S.tail b
|
||||||
|
in if S.last b' == fromIntegral (ord 'E')
|
||||||
|
then ExternalKey (S.init b') (HasExt True)
|
||||||
|
else ExternalKey b' (HasExt False)
|
||||||
|
| otherwise = OtherKey b
|
||||||
|
|
|
@ -199,7 +199,7 @@ lookupKey1 file = do
|
||||||
Right l -> makekey l
|
Right l -> makekey l
|
||||||
where
|
where
|
||||||
getsymlink = takeFileName <$> readSymbolicLink file
|
getsymlink = takeFileName <$> readSymbolicLink file
|
||||||
makekey l = case maybeLookupBackendVariety (fromKey keyVariety k) of
|
makekey l = maybeLookupBackendVariety (fromKey keyVariety k) >>= \case
|
||||||
Nothing -> do
|
Nothing -> do
|
||||||
unless (null kname || null bname ||
|
unless (null kname || null bname ||
|
||||||
not (isLinkToAnnex (toRawFilePath l))) $
|
not (isLinkToAnnex (toRawFilePath l))) $
|
||||||
|
|
|
@ -47,6 +47,7 @@ import Utility.Percentage
|
||||||
import Utility.DataUnits
|
import Utility.DataUnits
|
||||||
import Utility.HumanTime
|
import Utility.HumanTime
|
||||||
import Utility.ThreadScheduler
|
import Utility.ThreadScheduler
|
||||||
|
import Utility.SimpleProtocol as Proto
|
||||||
|
|
||||||
import qualified Data.ByteString.Lazy as L
|
import qualified Data.ByteString.Lazy as L
|
||||||
import qualified Data.ByteString as S
|
import qualified Data.ByteString as S
|
||||||
|
@ -439,3 +440,7 @@ bandwidthMeter mtotalsize (BytesProcessed old, before) (BytesProcessed new, now)
|
||||||
Just $ fromDuration $ Duration $
|
Just $ fromDuration $ Duration $
|
||||||
(totalsize - new) `div` bytespersecond
|
(totalsize - new) `div` bytespersecond
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
|
|
||||||
|
instance Proto.Serializable BytesProcessed where
|
||||||
|
serialize (BytesProcessed n) = show n
|
||||||
|
deserialize = BytesProcessed <$$> readish
|
||||||
|
|
|
@ -1,11 +1,30 @@
|
||||||
When a file is annexed, a [[key|internals/key_format]] is generated from its content and/or filesystem
|
When a file is annexed, a [[key|internals/key_format]] is generated from
|
||||||
metadata. The file checked into git symlinks to the key. This key can later
|
its content and/or filesystem metadata. The file checked into git symlinks
|
||||||
be used to retrieve the file's content (its value).
|
to the key. This key can later be used to retrieve the file's content (its
|
||||||
|
value).
|
||||||
|
|
||||||
Multiple pluggable key-value backends are supported, and a single repository
|
Multiple key-value backends are supported, and a single repository
|
||||||
can use different ones for different files.
|
can use different ones for different files.
|
||||||
|
|
||||||
These are the recommended backends to use.
|
## configuring which backend to use
|
||||||
|
|
||||||
|
The `annex.backend` git-config setting can be used to configure the
|
||||||
|
default backend to use when adding new files.
|
||||||
|
|
||||||
|
For finer control of what backend is used when adding different types of
|
||||||
|
files, the `.gitattributes` file can be used. The `annex.backend`
|
||||||
|
attribute can be set to the name of the backend to use for matching files.
|
||||||
|
|
||||||
|
For example, to use the SHA256E backend for sound files, which tend to be
|
||||||
|
smallish and might be modified or copied over time,
|
||||||
|
while using the WORM backend for everything else, you could set
|
||||||
|
in `.gitattributes`:
|
||||||
|
|
||||||
|
* annex.backend=WORM
|
||||||
|
*.mp3 annex.backend=SHA256E
|
||||||
|
*.ogg annex.backend=SHA256E
|
||||||
|
|
||||||
|
## recommended backends to use
|
||||||
|
|
||||||
* `SHA256E` -- The default backend for new files, combines a 256 bit SHA-2
|
* `SHA256E` -- The default backend for new files, combines a 256 bit SHA-2
|
||||||
hash of the file's content with the file's extension. This allows
|
hash of the file's content with the file's extension. This allows
|
||||||
|
@ -38,6 +57,8 @@ These are the recommended backends to use.
|
||||||
-- Fast [Blake2 hash](https://blake2.net/) variants optimised for
|
-- Fast [Blake2 hash](https://blake2.net/) variants optimised for
|
||||||
8-way CPUs.
|
8-way CPUs.
|
||||||
|
|
||||||
|
## non-cryptograpgically secure backends
|
||||||
|
|
||||||
The backends below do not guarantee cryptographically that the
|
The backends below do not guarantee cryptographically that the
|
||||||
content of an annexed file remains unchanged.
|
content of an annexed file remains unchanged.
|
||||||
|
|
||||||
|
@ -56,9 +77,23 @@ content of an annexed file remains unchanged.
|
||||||
that may be attached to a key (from any backend) indicating the key's location
|
that may be attached to a key (from any backend) indicating the key's location
|
||||||
on the web or in one of [[special_remotes]].
|
on the web or in one of [[special_remotes]].
|
||||||
|
|
||||||
|
## external backends
|
||||||
|
|
||||||
|
While most backends are built into git-annex, it also supports external
|
||||||
|
backends. These are programs with names like `git-annex-backend-XFOO`,
|
||||||
|
which can be provided by others. See [[design/external_backend_protocol]]
|
||||||
|
for details about how to write them.
|
||||||
|
|
||||||
|
Here's a list of external backends. Edit this page to add yours to the list.
|
||||||
|
|
||||||
|
* [[design/external_backend_protocol/git-annex-backend-XFOO]]
|
||||||
|
is a demo program implementing the protocol with a shell script.
|
||||||
|
|
||||||
|
## notes
|
||||||
|
|
||||||
If you want to be able to prove that you're working with the same file
|
If you want to be able to prove that you're working with the same file
|
||||||
contents that were checked into a repository earlier, you should avoid
|
contents that were checked into a repository earlier, you should avoid
|
||||||
using the non-cryptographically-secure backends, and will need to use
|
using non-cryptographically-secure backends, and will need to use
|
||||||
signed git commits. See [[tips/using_signed_git_commits]] for details.
|
signed git commits. See [[tips/using_signed_git_commits]] for details.
|
||||||
|
|
||||||
Retrieval of WORM and URL from many [[special_remotes]] is prohibited
|
Retrieval of WORM and URL from many [[special_remotes]] is prohibited
|
||||||
|
@ -68,20 +103,4 @@ Note that the various 512 and 384 length hashes result in long paths,
|
||||||
which are known to not work on Windows. If interoperability on Windows is a
|
which are known to not work on Windows. If interoperability on Windows is a
|
||||||
concern, avoid those.
|
concern, avoid those.
|
||||||
|
|
||||||
The `annex.backend` git-config setting can be used to configure the
|
|
||||||
default backend to use when adding new files.
|
|
||||||
|
|
||||||
For finer control of what backend is used when adding different types of
|
|
||||||
files, the `.gitattributes` file can be used. The `annex.backend`
|
|
||||||
attribute can be set to the name of the backend to use for matching files.
|
|
||||||
|
|
||||||
For example, to use the SHA256E backend for sound files, which tend to be
|
|
||||||
smallish and might be modified or copied over time,
|
|
||||||
while using the WORM backend for everything else, you could set
|
|
||||||
in `.gitattributes`:
|
|
||||||
|
|
||||||
* annex.backend=WORM
|
|
||||||
*.mp3 annex.backend=SHA256E
|
|
||||||
*.ogg annex.backend=SHA256E
|
|
||||||
|
|
||||||
See also: [[git-annex-examinekey]]
|
See also: [[git-annex-examinekey]]
|
||||||
|
|
|
@ -179,11 +179,8 @@ It's important that two different programs don't use the same name, because
|
||||||
that would result in bad behavior if the wrong program were used with a
|
that would result in bad behavior if the wrong program were used with a
|
||||||
repository with keys generated by the other program.
|
repository with keys generated by the other program.
|
||||||
|
|
||||||
Here is a list of programs, to avoid picking the same name. Edit this page
|
To avoid picking the same name, there is a list of known external backend
|
||||||
to add yours to the list.
|
programs in [[backends]].
|
||||||
|
|
||||||
* [[git-annex-backend-XFOO]] is a demo program implementing this protocol
|
|
||||||
with a shell script.
|
|
||||||
|
|
||||||
## signals
|
## signals
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
hashfile {
|
hashfile () {
|
||||||
local contentfile="$1"
|
local contentfile="$1"
|
||||||
# could send PROGRESS while doing this, but it's
|
# could send PROGRESS while doing this, but it's
|
||||||
# hard to implement that in shell
|
# hard to implement that in shell
|
||||||
|
|
|
@ -698,7 +698,7 @@ Executable git-annex
|
||||||
Annex.WorkTree
|
Annex.WorkTree
|
||||||
Annex.YoutubeDl
|
Annex.YoutubeDl
|
||||||
Backend
|
Backend
|
||||||
-- Backend.External
|
Backend.External
|
||||||
Backend.Hash
|
Backend.Hash
|
||||||
Backend.URL
|
Backend.URL
|
||||||
Backend.Utilities
|
Backend.Utilities
|
||||||
|
|
Loading…
Reference in a new issue