30c2728d65
doc/todo/distributed_migration.mdwn discusses security of distributed migration, and this was identified as necessary to do.
154 lines
4.7 KiB
Haskell
154 lines
4.7 KiB
Haskell
{- git-annex command
|
|
-
|
|
- Copyright 2012-2023 Joey Hess <id@joeyh.name>
|
|
-
|
|
- Licensed under the GNU AGPL version 3 or higher.
|
|
-}
|
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
|
|
module Command.ReKey where
|
|
|
|
import Command
|
|
import qualified Annex
|
|
import Annex.Content
|
|
import Annex.Ingest
|
|
import Annex.Link
|
|
import Annex.Perms
|
|
import Annex.ReplaceFile
|
|
import Logs.Location
|
|
import Annex.InodeSentinal
|
|
import Annex.WorkTree
|
|
import Logs.Migrate
|
|
import Utility.InodeCache
|
|
import qualified Utility.RawFilePath as R
|
|
|
|
import System.PosixCompat.Files (linkCount, fileMode)
|
|
|
|
cmd :: Command
|
|
cmd = withAnnexOptions [jsonOptions] $
|
|
command "rekey" SectionPlumbing
|
|
"change keys used for files"
|
|
(paramRepeating $ paramPair paramPath paramKey)
|
|
(seek <$$> optParser)
|
|
|
|
data ReKeyOptions = ReKeyOptions
|
|
{ reKeyThese :: CmdParams
|
|
, batchOption :: BatchMode
|
|
}
|
|
|
|
optParser :: CmdParamsDesc -> Parser ReKeyOptions
|
|
optParser desc = ReKeyOptions
|
|
<$> cmdParams desc
|
|
<*> parseBatchOption False
|
|
|
|
-- Split on the last space, since a FilePath can contain whitespace,
|
|
-- but a Key very rarely does.
|
|
batchParser :: String -> Annex (Either String (RawFilePath, Key))
|
|
batchParser s = case separate (== ' ') (reverse s) of
|
|
(rk, rf)
|
|
| null rk || null rf -> return $ Left "Expected: \"file key\""
|
|
| otherwise -> case deserializeKey (reverse rk) of
|
|
Nothing -> return $ Left "bad key"
|
|
Just k -> do
|
|
let f = reverse rf
|
|
f' <- liftIO $ relPathCwdToFile (toRawFilePath f)
|
|
return $ Right (f', k)
|
|
|
|
seek :: ReKeyOptions -> CommandSeek
|
|
seek o = case batchOption o of
|
|
Batch fmt -> batchOnly Nothing (reKeyThese o) $
|
|
batchInput fmt batchParser
|
|
(batchCommandAction . uncurry start)
|
|
NoBatch -> withPairs
|
|
(\(si, p) -> commandAction (start si (parsekey p)))
|
|
(reKeyThese o)
|
|
where
|
|
parsekey (file, skey) =
|
|
(toRawFilePath file, fromMaybe (giveup "bad key") (deserializeKey skey))
|
|
|
|
start :: SeekInput -> (RawFilePath, Key) -> CommandStart
|
|
start si (file, newkey) = lookupKey file >>= \case
|
|
Just k -> go k
|
|
Nothing -> stop
|
|
where
|
|
go oldkey
|
|
| oldkey == newkey = stop
|
|
| otherwise = starting "rekey" ai si $
|
|
perform file oldkey newkey
|
|
|
|
ai = ActionItemTreeFile file
|
|
|
|
perform :: RawFilePath -> Key -> Key -> CommandPerform
|
|
perform file oldkey newkey = do
|
|
ifM (inAnnex oldkey)
|
|
( unlessM (linkKey file oldkey newkey) $
|
|
giveup "failed creating link from old to new key"
|
|
, unlessM (Annex.getRead Annex.force) $ do
|
|
qp <- coreQuotePath <$> Annex.getGitConfig
|
|
giveup $ decodeBS $ quote qp $ QuotedPath file
|
|
<> " is not available (use --force to override)"
|
|
)
|
|
next $ cleanup file newkey $ const noop
|
|
|
|
{- Make a hard link to the old key content (when supported),
|
|
- to avoid wasting disk space. -}
|
|
linkKey :: RawFilePath -> Key -> Key -> Annex Bool
|
|
linkKey file oldkey newkey = ifM (isJust <$> isAnnexLink file)
|
|
( linkKey' DefaultVerify oldkey newkey
|
|
, do
|
|
{- The file being rekeyed is itself an unlocked file; if
|
|
- it's hard linked to the old key, that link must be broken. -}
|
|
oldobj <- calcRepo (gitAnnexLocation oldkey)
|
|
v <- tryNonAsync $ do
|
|
st <- liftIO $ R.getFileStatus file
|
|
when (linkCount st > 1) $ do
|
|
freezeContent oldobj
|
|
replaceWorkTreeFile (fromRawFilePath file) $ \tmp -> do
|
|
unlessM (checkedCopyFile oldkey oldobj tmp Nothing) $
|
|
giveup "can't lock old key"
|
|
thawContent tmp
|
|
ic <- withTSDelta (liftIO . genInodeCache file)
|
|
case v of
|
|
Left e -> do
|
|
warning (UnquotedString (show e))
|
|
return False
|
|
Right () -> do
|
|
r <- linkToAnnex newkey file ic
|
|
return $ case r of
|
|
LinkAnnexFailed -> False
|
|
LinkAnnexOk -> True
|
|
LinkAnnexNoop -> True
|
|
)
|
|
|
|
{- If the object file is already hardlinked to elsewhere, a hard
|
|
- link won't be made by getViaTmpFromDisk, but a copy instead.
|
|
- This avoids hard linking to content linked to an
|
|
- unlocked file, which would leave the new key unlocked
|
|
- and vulnerable to corruption. -}
|
|
linkKey' :: VerifyConfig -> Key -> Key -> Annex Bool
|
|
linkKey' v oldkey newkey =
|
|
getViaTmpFromDisk RetrievalAllKeysSecure v newkey (AssociatedFile Nothing) $ \tmp -> unVerified $ do
|
|
oldobj <- calcRepo (gitAnnexLocation oldkey)
|
|
isJust <$> linkOrCopy' (return True) newkey oldobj tmp Nothing
|
|
|
|
cleanup :: RawFilePath -> Key -> (MigrationRecord -> Annex ()) -> CommandCleanup
|
|
cleanup file newkey a = do
|
|
newkeyrec <- ifM (isJust <$> isAnnexLink file)
|
|
( do
|
|
-- Update symlink to use the new key.
|
|
sha <- genSymlink file newkey Nothing
|
|
stageSymlink file sha
|
|
return (MigrationRecord sha)
|
|
, do
|
|
mode <- liftIO $ catchMaybeIO $ fileMode <$> R.getFileStatus file
|
|
liftIO $ whenM (isJust <$> isPointerFile file) $
|
|
writePointerFile file newkey mode
|
|
sha <- hashPointerFile newkey
|
|
stagePointerFile file mode sha
|
|
return (MigrationRecord sha)
|
|
)
|
|
whenM (inAnnex newkey) $
|
|
logStatus newkey InfoPresent
|
|
a newkeyrec
|
|
return True
|