gcrypt: now supports rsync

Use rsync for gcrypt remotes that are not local to the disk.
(Note that I have punted on supporting http transport for now, it doesn't
seem likely to be very useful.)

This was mostly quite easy, it just uses the rsync special remote to handle
the transfers. The git repository url is converted to a RsyncOptions
structure, which required parsing it separately, since the rsync special
remote only supports rsync urls, which use a different format.

Note that annexed objects are now stored at the top of the gcrypt repo,
rather than inside annex/objects. This simplified the rsync suport,
since it doesn't have to arrange to create that directory. And git-annex
is not going to be run directly within gcrypt repos -- or if in some
strance scenario it was, it would make sense for it to not see the
encrypted objects.

This commit was sponsored by Sheila Miguez
This commit is contained in:
Joey Hess 2013-09-08 14:54:28 -04:00
parent 9477a07cbf
commit 3e079cdcd1
4 changed files with 105 additions and 64 deletions

View file

@ -28,6 +28,9 @@ import Remote.Helper.Encryptable
import Utility.Metered import Utility.Metered
import Crypto import Crypto
import Annex.UUID import Annex.UUID
import Annex.Ssh
import qualified Remote.Rsync
import Utility.Rsync
remote :: RemoteType remote :: RemoteType
remote = RemoteType { remote = RemoteType {
@ -52,33 +55,47 @@ gen gcryptr u c gc = do
gen' r'' u c gc gen' r'' u c gc
gen' :: Git.Repo -> UUID -> RemoteConfig -> RemoteGitConfig -> Annex Remote gen' :: Git.Repo -> UUID -> RemoteConfig -> RemoteGitConfig -> Annex Remote
gen' r u c gc = new <$> remoteCost gc defcst gen' r u c gc = do
where cst <- remoteCost gc $
defcst = if repoCheap r then nearlyCheapRemoteCost else expensiveRemoteCost if repoCheap r then nearlyCheapRemoteCost else expensiveRemoteCost
new cst = encryptableRemote c (rsynctransport, rsyncurl) <- rsyncTransport r
(store this) let rsyncopts = Remote.Rsync.genRsyncOpts c gc rsynctransport rsyncurl
(retrieve this) let this = Remote
{ uuid = u
, cost = cst
, name = Git.repoDescribe r
, storeKey = \_ _ _ -> noCrypto
, retrieveKeyFile = \_ _ _ _ -> noCrypto
, retrieveKeyFileCheap = \_ _ -> return False
, removeKey = remove this rsyncopts
, hasKey = checkPresent this rsyncopts
, hasKeyCheap = repoCheap r
, whereisKey = Nothing
, config = M.empty
, localpath = localpathCalc r
, repo = r
, gitconfig = gc { remoteGitConfig = Just $ extractGitConfig r }
, readonly = Git.repoIsHttp r
, globallyAvailable = globallyAvailableCalc r
, remotetype = remote
}
return $ encryptableRemote c
(store this rsyncopts)
(retrieve this rsyncopts)
this this
where
this = Remote rsyncTransport :: Git.Repo -> Annex ([CommandParam], String)
{ uuid = u rsyncTransport r
, cost = cst | "ssh://" `isPrefixOf` loc = sshtransport $ break (== '/') $ drop (length "ssh://") loc
, name = Git.repoDescribe r | "//:" `isInfixOf` loc = othertransport
, storeKey = \_ _ _ -> noCrypto | ":" `isInfixOf` loc = sshtransport $ separate (== ':') loc
, retrieveKeyFile = \_ _ _ _ -> noCrypto | otherwise = othertransport
, retrieveKeyFileCheap = \_ _ -> return False where
, removeKey = remove this loc = Git.repoLocation r
, hasKey = checkPresent this sshtransport (host, path) = do
, hasKeyCheap = repoCheap r opts <- sshCachingOptions (host, Nothing) []
, whereisKey = Nothing return (rsyncShell $ Param "ssh" : opts, host ++ ":" ++ path)
, config = M.empty othertransport = return ([], loc)
, localpath = localpathCalc r
, repo = r
, gitconfig = gc { remoteGitConfig = Just $ extractGitConfig r }
, readonly = Git.repoIsHttp r
, globallyAvailable = globallyAvailableCalc r
, remotetype = remote
}
noCrypto :: Annex a noCrypto :: Annex a
noCrypto = error "cannot use gcrypt remote without encryption enabled" noCrypto = error "cannot use gcrypt remote without encryption enabled"
@ -131,28 +148,29 @@ gCryptSetup mu c = go $ M.lookup "gitrepo" c
then return (c', u) then return (c', u)
else error "uuid mismatch" else error "uuid mismatch"
store :: Remote -> (Cipher, Key) -> Key -> MeterUpdate -> Annex Bool store :: Remote -> Remote.Rsync.RsyncOpts -> (Cipher, Key) -> Key -> MeterUpdate -> Annex Bool
store r (cipher, enck) k p store r rsyncopts (cipher, enck) k p
| not $ Git.repoIsUrl (repo r) = guardUsable (repo r) False $ | not $ Git.repoIsUrl (repo r) = guardUsable (repo r) False $
sendwith $ \meterupdate h -> do sendwith $ \meterupdate h -> do
createDirectoryIfMissing True $ parentDir dest createDirectoryIfMissing True $ parentDir dest
readBytes (meteredWriteFile meterupdate dest) h readBytes (meteredWriteFile meterupdate dest) h
return True return True
| Git.repoIsSsh (repo r) = sendwith $ \meterupdate h -> undefined | Git.repoIsSsh (repo r) = Remote.Rsync.storeEncrypted rsyncopts gpgopts (cipher, enck) k p
| otherwise = unsupportedUrl | otherwise = unsupportedUrl
where where
gpgopts = getGpgEncParams r
dest = gCryptLocation r enck dest = gCryptLocation r enck
sendwith a = metered (Just p) k $ \meterupdate -> sendwith a = metered (Just p) k $ \meterupdate ->
Annex.Content.sendAnnex k noop $ \src -> Annex.Content.sendAnnex k noop $ \src ->
liftIO $ catchBoolIO $ liftIO $ catchBoolIO $
encrypt (getGpgEncParams r) cipher (feedFile src) (a meterupdate) encrypt gpgopts cipher (feedFile src) (a meterupdate)
retrieve :: Remote -> (Cipher, Key) -> Key -> FilePath -> MeterUpdate -> Annex Bool retrieve :: Remote -> Remote.Rsync.RsyncOpts -> (Cipher, Key) -> Key -> FilePath -> MeterUpdate -> Annex Bool
retrieve r (cipher, enck) k d p retrieve r rsyncopts (cipher, enck) k d p
| not $ Git.repoIsUrl (repo r) = guardUsable (repo r) False $ do | not $ Git.repoIsUrl (repo r) = guardUsable (repo r) False $ do
retrievewith $ L.readFile src retrievewith $ L.readFile src
return True return True
| Git.repoIsSsh (repo r) = undefined | Git.repoIsSsh (repo r) = Remote.Rsync.retrieveEncrypted rsyncopts (cipher, enck) k d p
| otherwise = unsupportedUrl | otherwise = unsupportedUrl
where where
src = gCryptLocation r enck src = gCryptLocation r enck
@ -161,26 +179,29 @@ retrieve r (cipher, enck) k d p
decrypt cipher (feedBytes b) decrypt cipher (feedBytes b)
(readBytes $ meteredWriteFile meterupdate d) (readBytes $ meteredWriteFile meterupdate d)
remove :: Remote -> Key -> Annex Bool remove :: Remote -> Remote.Rsync.RsyncOpts -> Key -> Annex Bool
remove r k remove r rsyncopts k
| not $ Git.repoIsUrl (repo r) = guardUsable (repo r) False $ do | not $ Git.repoIsUrl (repo r) = guardUsable (repo r) False $ do
liftIO $ removeDirectoryRecursive (parentDir dest) liftIO $ removeDirectoryRecursive (parentDir dest)
return True return True
| Git.repoIsSsh (repo r) = undefined | Git.repoIsSsh (repo r) = Remote.Rsync.remove rsyncopts k
| otherwise = unsupportedUrl | otherwise = unsupportedUrl
where where
dest = gCryptLocation r k dest = gCryptLocation r k
checkPresent :: Remote -> Key -> Annex (Either String Bool) checkPresent :: Remote -> Remote.Rsync.RsyncOpts -> Key -> Annex (Either String Bool)
checkPresent r k checkPresent r rsyncopts k
| not $ Git.repoIsUrl (repo r) = | not $ Git.repoIsUrl (repo r) =
guardUsable (repo r) unknown $ guardUsable (repo r) unknown $
liftIO $ catchDefaultIO unknown $ liftIO $ catchDefaultIO unknown $
Right <$> doesFileExist (gCryptLocation r k) Right <$> doesFileExist (gCryptLocation r k)
| Git.repoIsSsh (repo r) = undefined | Git.repoIsSsh (repo r) = Remote.Rsync.checkPresent (repo r) rsyncopts k
| otherwise = unsupportedUrl | otherwise = unsupportedUrl
where where
unknown = Left $ "unable to check " ++ Git.repoDescribe (repo r) ++ show (repo r) unknown = Left $ "unable to check " ++ Git.repoDescribe (repo r) ++ show (repo r)
{- Annexed objects are stored directly under the top of the gcrypt repo
- (not in annex/objects), and are hashed using lower-case directories for max
- portability. -}
gCryptLocation :: Remote -> Key -> FilePath gCryptLocation :: Remote -> Key -> FilePath
gCryptLocation r key = Git.repoLocation (repo r) </> annexLocation key hashDirLower gCryptLocation r key = Git.repoLocation (repo r) </> keyPath key hashDirLower

View file

@ -7,7 +7,16 @@
{-# LANGUAGE CPP #-} {-# LANGUAGE CPP #-}
module Remote.Rsync (remote) where module Remote.Rsync (
remote,
storeEncrypted,
retrieveEncrypted,
remove,
checkPresent,
withRsyncScratchDir,
genRsyncOpts,
RsyncOpts
) where
import qualified Data.ByteString.Lazy as L import qualified Data.ByteString.Lazy as L
import qualified Data.Map as M import qualified Data.Map as M
@ -52,9 +61,10 @@ remote = RemoteType {
gen :: Git.Repo -> UUID -> RemoteConfig -> RemoteGitConfig -> Annex Remote gen :: Git.Repo -> UUID -> RemoteConfig -> RemoteGitConfig -> Annex Remote
gen r u c gc = do gen r u c gc = do
cst <- remoteCost gc expensiveRemoteCost cst <- remoteCost gc expensiveRemoteCost
(transport, url) <- rsyncTransport (transport, url) <- rsyncTransport gc $
let o = RsyncOpts url (transport ++ opts) escape fromMaybe (error "missing rsyncurl") $ remoteAnnexRsyncUrl gc
islocal = rsyncUrlIsPath $ rsyncUrl o let o = genRsyncOpts c gc transport url
let islocal = rsyncUrlIsPath $ rsyncUrl o
return $ encryptableRemote c return $ encryptableRemote c
(storeEncrypted o $ getGpgEncParams (c,gc)) (storeEncrypted o $ getGpgEncParams (c,gc))
(retrieveEncrypted o) (retrieveEncrypted o)
@ -79,6 +89,9 @@ gen r u c gc = do
, globallyAvailable = not $ islocal , globallyAvailable = not $ islocal
, remotetype = remote , remotetype = remote
} }
genRsyncOpts :: RemoteConfig -> RemoteGitConfig -> [CommandParam] -> RsyncUrl -> RsyncOpts
genRsyncOpts c gc transport url = RsyncOpts url (transport ++ opts) escape
where where
opts = map Param $ filter safe $ remoteAnnexRsyncOptions gc opts = map Param $ filter safe $ remoteAnnexRsyncOptions gc
escape = M.lookup "shellescape" c /= Just "no" escape = M.lookup "shellescape" c /= Just "no"
@ -89,28 +102,30 @@ gen r u c gc = do
| opt == "--delete" = False | opt == "--delete" = False
| opt == "--delete-excluded" = False | opt == "--delete-excluded" = False
| otherwise = True | otherwise = True
rawurl = fromMaybe (error "missing rsyncurl") $ remoteAnnexRsyncUrl gc
rsyncTransport :: RemoteGitConfig -> RsyncUrl -> Annex ([CommandParam], RsyncUrl)
rsyncTransport gc rawurl
| rsyncUrlIsShell rawurl =
(\rsh -> return (rsyncShell rsh, resturl)) =<<
case fromNull ["ssh"] (remoteAnnexRsyncTransport gc) of
"ssh":sshopts -> do
let (port, sshopts') = sshReadPort sshopts
host = takeWhile (/=':') resturl
-- Connection caching
(Param "ssh":) <$> sshCachingOptions
(host, port)
(map Param $ loginopt ++ sshopts')
"rsh":rshopts -> return $ map Param $ "rsh" :
loginopt ++ rshopts
rsh -> error $ "Unknown Rsync transport: "
++ unwords rsh
| otherwise = return ([], rawurl)
where
(login,resturl) = case separate (=='@') rawurl of (login,resturl) = case separate (=='@') rawurl of
(h, "") -> (Nothing, h) (h, "") -> (Nothing, h)
(l, h) -> (Just l, h) (l, h) -> (Just l, h)
loginopt = maybe [] (\l -> ["-l",l]) login loginopt = maybe [] (\l -> ["-l",l]) login
fromNull as xs | null xs = as fromNull as xs = if null xs then as else xs
| otherwise = xs
rsyncTransport = if rsyncUrlIsShell rawurl
then (\rsh -> return (rsyncShell rsh, resturl)) =<<
case fromNull ["ssh"] (remoteAnnexRsyncTransport gc) of
"ssh":sshopts -> do
let (port, sshopts') = sshReadPort sshopts
host = takeWhile (/=':') resturl
-- Connection caching
(Param "ssh":) <$> sshCachingOptions
(host, port)
(map Param $ loginopt ++ sshopts')
"rsh":rshopts -> return $ map Param $ "rsh" :
loginopt ++ rshopts
rsh -> error $ "Unknown Rsync transport: "
++ unwords rsh
else return ([], rawurl)
rsyncSetup :: Maybe UUID -> RemoteConfig -> Annex (RemoteConfig, UUID) rsyncSetup :: Maybe UUID -> RemoteConfig -> Annex (RemoteConfig, UUID)
rsyncSetup mu c = do rsyncSetup mu c = do

3
debian/changelog vendored
View file

@ -24,6 +24,9 @@ git-annex (4.20130828) UNRELEASED; urgency=low
* Remind user when annex-ignore is set for some remotes, if unable to * Remind user when annex-ignore is set for some remotes, if unable to
get or drop a file, possibly because it's on an ignored remote. get or drop a file, possibly because it's on an ignored remote.
* gpg: Force --no-textmode in case the user has it turned on in config. * gpg: Force --no-textmode in case the user has it turned on in config.
* Added gcrypt support. This combines a fully encrypted git
repository (using git-remote-gcrypt) with an encrypted git-annex special
remote.
-- Joey Hess <joeyh@debian.org> Tue, 27 Aug 2013 11:03:00 -0400 -- Joey Hess <joeyh@debian.org> Tue, 27 Aug 2013 11:03:00 -0400

View file

@ -21,6 +21,8 @@ gcrypt:
for gcrypt to use. This repository should be either empty, or an existing for gcrypt to use. This repository should be either empty, or an existing
gcrypt repositry. gcrypt repositry.
* `shellescape` - See [[rsync]] for the details of this option.
## notes ## notes
For git-annex to store files in a repository on a remote server, you need For git-annex to store files in a repository on a remote server, you need