{- git-annex command - - Copyright 2012-2016 Joey Hess - - Licensed under the GNU GPL version 3 or higher. -} 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 Git.FilePath import qualified Database.Keys import Annex.InodeSentinal import Utility.InodeCache cmd :: Command cmd = notDirect $ 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 -- Split on the last space, since a FilePath can contain whitespace, -- but a Key very rarely does. batchParser :: String -> Either String (FilePath, Key) batchParser s = case separate (== ' ') (reverse s) of (rk, rf) | null rk || null rf -> Left "Expected: \"file key\"" | otherwise -> case file2key (reverse rk) of Nothing -> Left "bad key" Just k -> Right (reverse rf, k) seek :: ReKeyOptions -> CommandSeek seek o = case batchOption o of Batch -> batchInput batchParser (batchCommandAction . start) NoBatch -> withPairs (start . parsekey) (reKeyThese o) where parsekey (file, skey) = (file, fromMaybe (giveup "bad key") (file2key skey)) start :: (FilePath, Key) -> CommandStart start (file, newkey) = ifAnnexed file go stop where go oldkey | oldkey == newkey = stop | otherwise = do showStart "rekey" file next $ perform file oldkey newkey perform :: FilePath -> Key -> Key -> CommandPerform perform file oldkey newkey = do ifM (inAnnex oldkey) ( unlessM (linkKey file oldkey newkey) $ giveup "failed" , unlessM (Annex.getState Annex.force) $ giveup $ file ++ " is not available (use --force to override)" ) next $ cleanup file oldkey newkey {- Make a hard link to the old key content (when supported), - to avoid wasting disk space. -} linkKey :: FilePath -> Key -> Key -> Annex Bool linkKey file oldkey newkey = ifM (isJust <$> isAnnexLink file) {- 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. -} ( getViaTmpFromDisk RetrievalAllKeysSecure DefaultVerify newkey $ \tmp -> unVerified $ do oldobj <- calcRepo (gitAnnexLocation oldkey) linkOrCopy' (return True) newkey oldobj tmp Nothing , do ic <- withTSDelta (liftIO . genInodeCache file) {- The file being rekeyed is itself an unlocked file, so if - it's linked to the old key, that link must be broken. -} oldobj <- calcRepo (gitAnnexLocation oldkey) v <- tryNonAsync $ modifyContent oldobj $ do replaceFile oldobj $ \tmp -> unlessM (checkedCopyFile oldkey file tmp Nothing) $ error "can't lock old key" freezeContent oldobj oldic <- withTSDelta (liftIO . genInodeCache oldobj) whenM (isUnmodified oldkey oldobj) $ Database.Keys.addInodeCaches oldkey (catMaybes [oldic]) case v of Left e -> do warning (show e) return False Right () -> do r <- linkToAnnex newkey file ic return $ case r of LinkAnnexFailed -> False LinkAnnexOk -> True LinkAnnexNoop -> True ) cleanup :: FilePath -> Key -> Key -> CommandCleanup cleanup file oldkey newkey = do ifM (isJust <$> isAnnexLink file) ( do -- Update symlink to use the new key. liftIO $ removeFile file addLink file newkey Nothing , do mode <- liftIO $ catchMaybeIO $ fileMode <$> getFileStatus file liftIO $ whenM (isJust <$> isPointerFile file) $ writePointerFile file newkey mode stagePointerFile file mode =<< hashPointerFile newkey Database.Keys.removeAssociatedFile oldkey =<< inRepo (toTopFilePath file) ) whenM (inAnnex newkey) $ logStatus newkey InfoPresent return True