move: --safe/--unsafe and potential drop race fix

move: Added --safe option, which makes move honor numcopies settings.
Also --unsafe enables the default behavior, anticipating that the
default may one day change.

This commit was sponsored by Ethan Aubin.
This commit is contained in:
Joey Hess 2018-04-09 16:09:00 -04:00
parent ae530f043e
commit af8546990d
No known key found for this signature in database
GPG key ID: DB12DB0FF05F8F38
11 changed files with 187 additions and 74 deletions

View file

@ -11,6 +11,7 @@ module Annex.NumCopies (
module Types.NumCopies, module Types.NumCopies,
module Logs.NumCopies, module Logs.NumCopies,
getFileNumCopies, getFileNumCopies,
getAssociatedFileNumCopies,
getGlobalFileNumCopies, getGlobalFileNumCopies,
getNumCopies, getNumCopies,
deprecatedNumCopies, deprecatedNumCopies,
@ -69,6 +70,10 @@ getFileNumCopies f = fromSources
, deprecatedNumCopies , deprecatedNumCopies
] ]
getAssociatedFileNumCopies :: AssociatedFile -> Annex NumCopies
getAssociatedFileNumCopies (AssociatedFile afile) =
maybe getNumCopies getFileNumCopies afile
{- This is the globally visible numcopies value for a file. So it does {- This is the globally visible numcopies value for a file. So it does
- not include local configuration in the git config or command line - not include local configuration in the git config or command line
- options. -} - options. -}

View file

@ -1,3 +1,11 @@
git-annex (6.20180410) UNRELEASED; urgency=medium
* move: Added --safe option, which makes move honor numcopies settings.
Also --unsafe enables the default behavior, anticipating that the
default may one day change.
-- Joey Hess <id@joeyh.name> Mon, 09 Apr 2018 14:03:28 -0400
git-annex (6.20180409) upstream; urgency=medium git-annex (6.20180409) upstream; urgency=medium
* Added adb special remote which allows exporting files to Android devices. * Added adb special remote which allows exporting files to Android devices.

View file

@ -50,7 +50,7 @@ seek o = allowConcurrentOutput $ do
Batch -> batchInput Right (batchCommandAction . go) Batch -> batchInput Right (batchCommandAction . go)
NoBatch -> withKeyOptions NoBatch -> withKeyOptions
(keyOptions o) (autoMode o) (keyOptions o) (autoMode o)
(Command.Move.startKey (fromToOptions o) False) (Command.Move.startKey (fromToOptions o) Command.Move.RemoveNever)
(withFilesInGit go) (withFilesInGit go)
=<< workTreeItems (copyFiles o) =<< workTreeItems (copyFiles o)
@ -59,7 +59,7 @@ seek o = allowConcurrentOutput $ do
- sending non-preferred content. -} - sending non-preferred content. -}
start :: CopyOptions -> FilePath -> Key -> CommandStart start :: CopyOptions -> FilePath -> Key -> CommandStart
start o file key = stopUnless shouldCopy $ start o file key = stopUnless shouldCopy $
Command.Move.start (fromToOptions o) False file key Command.Move.start (fromToOptions o) Command.Move.RemoveNever file key
where where
shouldCopy shouldCopy
| autoMode o = want <||> numCopiesCheck file key (<) | autoMode o = want <||> numCopiesCheck file key (<)

View file

@ -202,8 +202,8 @@ requiredContent = do
{- In auto mode, only runs the action if there are enough {- In auto mode, only runs the action if there are enough
- copies on other semitrusted repositories. -} - copies on other semitrusted repositories. -}
checkDropAuto :: Bool -> Maybe Remote -> AssociatedFile -> Key -> (NumCopies -> CommandStart) -> CommandStart checkDropAuto :: Bool -> Maybe Remote -> AssociatedFile -> Key -> (NumCopies -> CommandStart) -> CommandStart
checkDropAuto automode mremote (AssociatedFile afile) key a = checkDropAuto automode mremote afile key a =
go =<< maybe getNumCopies getFileNumCopies afile go =<< getAssociatedFileNumCopies afile
where where
go numcopies go numcopies
| automode = do | automode = do

View file

@ -68,7 +68,7 @@ start' expensivecheck from key afile ai = onlyActionOn key $
Nothing -> go $ perform key afile Nothing -> go $ perform key afile
Just src -> Just src ->
stopUnless (Command.Move.fromOk src key) $ stopUnless (Command.Move.fromOk src key) $
go $ Command.Move.fromPerform src False key afile go $ Command.Move.fromPerform src Command.Move.RemoveNever key afile
where where
go a = do go a = do
showStartKey "get" key ai showStartKey "get" key ai

View file

@ -55,7 +55,7 @@ start o file k = startKey o afile k (mkActionItem afile)
startKey :: MirrorOptions -> AssociatedFile -> Key -> ActionItem -> CommandStart startKey :: MirrorOptions -> AssociatedFile -> Key -> ActionItem -> CommandStart
startKey o afile key ai = onlyActionOn key $ case fromToOptions o of startKey o afile key ai = onlyActionOn key $ case fromToOptions o of
ToRemote r -> checkFailedTransferDirection ai Upload $ ifM (inAnnex key) ToRemote r -> checkFailedTransferDirection ai Upload $ ifM (inAnnex key)
( Command.Move.toStart False afile key ai =<< getParsed r ( Command.Move.toStart Command.Move.RemoveNever afile key ai =<< getParsed r
, do , do
numcopies <- getnumcopies numcopies <- getnumcopies
Command.Drop.startRemote afile ai numcopies key =<< getParsed r Command.Drop.startRemote afile ai numcopies key =<< getParsed r

View file

@ -1,6 +1,6 @@
{- git-annex command {- git-annex command
- -
- Copyright 2010-2017 Joey Hess <id@joeyh.name> - Copyright 2010-2018 Joey Hess <id@joeyh.name>
- -
- Licensed under the GNU GPL version 3 or higher. - Licensed under the GNU GPL version 3 or higher.
-} -}
@ -28,6 +28,7 @@ cmd = withGlobalOptions [jobsOption, jsonOptions, jsonProgressOption, annexedMat
data MoveOptions = MoveOptions data MoveOptions = MoveOptions
{ moveFiles :: CmdParams { moveFiles :: CmdParams
, fromToOptions :: FromToHereOptions , fromToOptions :: FromToHereOptions
, removeWhenOptions :: RemoveWhen
, keyOptions :: Maybe KeyOptions , keyOptions :: Maybe KeyOptions
, batchOption :: BatchMode , batchOption :: BatchMode
} }
@ -36,6 +37,7 @@ optParser :: CmdParamsDesc -> Parser MoveOptions
optParser desc = MoveOptions optParser desc = MoveOptions
<$> cmdParams desc <$> cmdParams desc
<*> parseFromToHereOptions <*> parseFromToHereOptions
<*> parseRemoveWhenOptions
<*> optional (parseKeyOptions <|> parseFailedTransfersOption) <*> optional (parseKeyOptions <|> parseFailedTransfersOption)
<*> parseBatchOption <*> parseBatchOption
@ -43,64 +45,75 @@ instance DeferredParseClass MoveOptions where
finishParse v = MoveOptions finishParse v = MoveOptions
<$> pure (moveFiles v) <$> pure (moveFiles v)
<*> finishParse (fromToOptions v) <*> finishParse (fromToOptions v)
<*> pure (removeWhenOptions v)
<*> pure (keyOptions v) <*> pure (keyOptions v)
<*> pure (batchOption v) <*> pure (batchOption v)
data RemoveWhen = RemoveSafe | RemoveUnsafe | RemoveNever
deriving (Show, Eq)
parseRemoveWhenOptions :: Parser RemoveWhen
parseRemoveWhenOptions =
flag' RemoveSafe
( long "safe"
<> short 's'
<> help "preserve numcopies"
)
<|> flag' RemoveUnsafe
(long "unsafe"
<> short 'u'
<> help "do not preserve numcopies (default)"
)
<|> pure RemoveUnsafe
seek :: MoveOptions -> CommandSeek seek :: MoveOptions -> CommandSeek
seek o = allowConcurrentOutput $ do seek o = allowConcurrentOutput $ do
let go = whenAnnexed $ start (fromToOptions o) True let go = whenAnnexed $ start (fromToOptions o) (removeWhenOptions o)
case batchOption o of case batchOption o of
Batch -> batchInput Right (batchCommandAction . go) Batch -> batchInput Right (batchCommandAction . go)
NoBatch -> withKeyOptions (keyOptions o) False NoBatch -> withKeyOptions (keyOptions o) False
(startKey (fromToOptions o) True) (startKey (fromToOptions o) (removeWhenOptions o))
(withFilesInGit go) (withFilesInGit go)
=<< workTreeItems (moveFiles o) =<< workTreeItems (moveFiles o)
start :: FromToHereOptions -> Bool -> FilePath -> Key -> CommandStart start :: FromToHereOptions -> RemoveWhen -> FilePath -> Key -> CommandStart
start fromto move f k = start' fromto move afile k (mkActionItem afile) start fromto removewhen f k =
start' fromto removewhen afile k (mkActionItem afile)
where where
afile = AssociatedFile (Just f) afile = AssociatedFile (Just f)
startKey :: FromToHereOptions -> Bool -> Key -> ActionItem -> CommandStart startKey :: FromToHereOptions -> RemoveWhen -> Key -> ActionItem -> CommandStart
startKey fromto move = start' fromto move (AssociatedFile Nothing) startKey fromto removewhen = start' fromto removewhen (AssociatedFile Nothing)
start' :: FromToHereOptions -> Bool -> AssociatedFile -> Key -> ActionItem -> CommandStart start' :: FromToHereOptions -> RemoveWhen -> AssociatedFile -> Key -> ActionItem -> CommandStart
start' fromto move afile key ai = onlyActionOn key $ start' fromto removewhen afile key ai = onlyActionOn key $
case fromto of case fromto of
Right (FromRemote src) -> Right (FromRemote src) ->
checkFailedTransferDirection ai Download $ checkFailedTransferDirection ai Download $
fromStart move afile key ai =<< getParsed src fromStart removewhen afile key ai =<< getParsed src
Right (ToRemote dest) -> Right (ToRemote dest) ->
checkFailedTransferDirection ai Upload $ checkFailedTransferDirection ai Upload $
toStart move afile key ai =<< getParsed dest toStart removewhen afile key ai =<< getParsed dest
Left ToHere -> Left ToHere ->
checkFailedTransferDirection ai Download $ checkFailedTransferDirection ai Download $
toHereStart move afile key ai toHereStart removewhen afile key ai
showMoveAction :: Bool -> Key -> ActionItem -> Annex () showMoveAction :: RemoveWhen -> Key -> ActionItem -> Annex ()
showMoveAction move = showStartKey (if move then "move" else "copy") showMoveAction RemoveNever = showStartKey "copy"
showMoveAction _ = showStartKey "move"
{- Moves (or copies) the content of an annexed file to a remote. toStart :: RemoveWhen -> AssociatedFile -> Key -> ActionItem -> Remote -> CommandStart
- toStart removewhen afile key ai dest = do
- If the remote already has the content, it is still removed from
- the current repository.
-
- Note that unlike drop, this does not honor numcopies.
- A file's content can be moved even if there are insufficient copies to
- allow it to be dropped.
-}
toStart :: Bool -> AssociatedFile -> Key -> ActionItem -> Remote -> CommandStart
toStart move afile key ai dest = do
u <- getUUID u <- getUUID
ishere <- inAnnex key ishere <- inAnnex key
if not ishere || u == Remote.uuid dest if not ishere || u == Remote.uuid dest
then stop -- not here, so nothing to do then stop -- not here, so nothing to do
else toStart' dest move afile key ai else toStart' dest removewhen afile key ai
toStart' :: Remote -> Bool -> AssociatedFile -> Key -> ActionItem -> CommandStart toStart' :: Remote -> RemoveWhen -> AssociatedFile -> Key -> ActionItem -> CommandStart
toStart' dest move afile key ai = do toStart' dest removewhen afile key ai = do
fast <- Annex.getState Annex.fast fast <- Annex.getState Annex.fast
if fast && not move if fast && removewhen == RemoveNever
then ifM (expectedPresent dest key) then ifM (expectedPresent dest key)
( stop ( stop
, go True (pure $ Right False) , go True (pure $ Right False)
@ -108,16 +121,16 @@ toStart' dest move afile key ai = do
else go False (Remote.hasKey dest key) else go False (Remote.hasKey dest key)
where where
go fastcheck isthere = do go fastcheck isthere = do
showMoveAction move key ai showMoveAction removewhen key ai
next $ toPerform dest move key afile fastcheck =<< isthere next $ toPerform dest removewhen key afile fastcheck =<< isthere
expectedPresent :: Remote -> Key -> Annex Bool expectedPresent :: Remote -> Key -> Annex Bool
expectedPresent dest key = do expectedPresent dest key = do
remotes <- Remote.keyPossibilities key remotes <- Remote.keyPossibilities key
return $ dest `elem` remotes return $ dest `elem` remotes
toPerform :: Remote -> Bool -> Key -> AssociatedFile -> Bool -> Either String Bool -> CommandPerform toPerform :: Remote -> RemoveWhen -> Key -> AssociatedFile -> Bool -> Either String Bool -> CommandPerform
toPerform dest move key afile fastcheck isthere = toPerform dest removewhen key afile fastcheck isthere =
case isthere of case isthere of
Left err -> do Left err -> do
showNote err showNote err
@ -139,18 +152,38 @@ toPerform dest move key afile fastcheck isthere =
Remote.logStatus dest key InfoPresent Remote.logStatus dest key InfoPresent
where where
finish :: Annex () -> CommandPerform finish :: Annex () -> CommandPerform
finish setpresentremote finish setpresentremote = case removewhen of
| move = lockContentForRemoval key $ \contentlock -> do RemoveNever -> do
-- Drop content before updating location logs,
-- in case disk space is very low this frees up
-- space before writing data to disk.
removeAnnex contentlock
next $ do
setpresentremote
Command.Drop.cleanupLocal key
| otherwise = next $ do
setpresentremote setpresentremote
return True next $ return True
_ -> lockContentForRemoval key $ \contentlock -> do
numcopies <- case removewhen of
RemoveUnsafe -> pure (NumCopies 1)
_ -> getAssociatedFileNumCopies afile
u <- getUUID
let drophere proof = do
liftIO $ debugM "drop" $ unwords
[ "Dropping from here"
, "proof:"
, show proof
]
-- Drop content before updating location logs,
-- in case disk space is very low this frees
-- up space before writing data to disk.
removeAnnex contentlock
next $ do
setpresentremote
Command.Drop.cleanupLocal key
let faileddrophere = do
warning "Not enough copies exist to drop from here (use --unsafe to avoid this check)"
next $ do
setpresentremote
return True
(tocheck, verified) <- verifiableCopies key [u]
verifyEnoughCopiesToDrop "" key (Just contentlock)
numcopies [] verified
(UnVerifiedRemote dest : tocheck)
drophere faileddrophere
{- Moves (or copies) the content of an annexed file from a remote {- Moves (or copies) the content of an annexed file from a remote
- to the current repository. - to the current repository.
@ -158,14 +191,14 @@ toPerform dest move key afile fastcheck isthere =
- If the current repository already has the content, it is still removed - If the current repository already has the content, it is still removed
- from the remote. - from the remote.
-} -}
fromStart :: Bool -> AssociatedFile -> Key -> ActionItem -> Remote -> CommandStart fromStart :: RemoveWhen -> AssociatedFile -> Key -> ActionItem -> Remote -> CommandStart
fromStart move afile key ai src fromStart removewhen afile key ai src
| move = go | removewhen == RemoveNever = stopUnless (not <$> inAnnex key) go
| otherwise = stopUnless (not <$> inAnnex key) go | otherwise = go
where where
go = stopUnless (fromOk src key) $ do go = stopUnless (fromOk src key) $ do
showMoveAction move key ai showMoveAction removewhen key ai
next $ fromPerform src move key afile next $ fromPerform src removewhen key afile
fromOk :: Remote -> Key -> Annex Bool fromOk :: Remote -> Key -> Annex Bool
fromOk src key = go =<< Annex.getState Annex.force fromOk src key = go =<< Annex.getState Annex.force
@ -181,12 +214,12 @@ fromOk src key = go =<< Annex.getState Annex.force
remotes <- Remote.keyPossibilities key remotes <- Remote.keyPossibilities key
return $ u /= Remote.uuid src && elem src remotes return $ u /= Remote.uuid src && elem src remotes
fromPerform :: Remote -> Bool -> Key -> AssociatedFile -> CommandPerform fromPerform :: Remote -> RemoveWhen -> Key -> AssociatedFile -> CommandPerform
fromPerform src move key afile = do fromPerform src removewhen key afile = do
showAction $ "from " ++ Remote.name src showAction $ "from " ++ Remote.name src
ifM (inAnnex key) ifM (inAnnex key)
( dispatch move True ( dispatch removewhen True
, dispatch move =<< go , dispatch removewhen =<< go
) )
where where
go = notifyTransfer Download afile $ go = notifyTransfer Download afile $
@ -194,12 +227,16 @@ fromPerform src move key afile = do
getViaTmp (RemoteVerify src) key $ \t -> getViaTmp (RemoteVerify src) key $ \t ->
Remote.retrieveKeyFile src key afile t p Remote.retrieveKeyFile src key afile t p
dispatch _ False = stop -- failed dispatch _ False = stop -- failed
dispatch False True = next $ return True -- copy complete dispatch RemoveNever True = next $ return True -- copy complete
-- Finish by dropping from remote, taking care to verify that -- Finish move by dropping from remote, when verified
-- the copy here has not been lost somehow. -- numcopies or RemoveUnsafe allows.
-- (NumCopies is 1 since we're moving.) dispatch _ True = do
dispatch True True = verifyEnoughCopiesToDrop "" key Nothing numcopies <- case removewhen of
(NumCopies 1) [] [] [UnVerifiedHere] dropremote faileddropremote RemoveUnsafe -> pure (NumCopies 1)
_ -> getAssociatedFileNumCopies afile
(tocheck, verified) <- verifiableCopies key [Remote.uuid src]
verifyEnoughCopiesToDrop "" key Nothing numcopies [] verified
tocheck dropremote faileddropremote
dropremote proof = do dropremote proof = do
liftIO $ debugM "drop" $ unwords liftIO $ debugM "drop" $ unwords
[ "Dropping from remote" [ "Dropping from remote"
@ -209,21 +246,26 @@ fromPerform src move key afile = do
] ]
ok <- Remote.removeKey src key ok <- Remote.removeKey src key
next $ Command.Drop.cleanupRemote key src ok next $ Command.Drop.cleanupRemote key src ok
faileddropremote = giveup "Unable to drop from remote." faileddropremote = case removewhen of
RemoveUnsafe -> giveup "Unable to drop from remote."
RemoveSafe -> do
warning "Not enough copies exist to drop from remote (use --unsafe to avoid this check)"
next $ return True
RemoveNever -> next $ return True
{- Moves (or copies) the content of an annexed file from reachable remotes {- Moves (or copies) the content of an annexed file from reachable remotes
- to the current repository. - to the current repository.
- -
- When moving, the content is removed from all the reachable remotes. -} - When moving, the content is removed from all the reachable remotes. -}
toHereStart :: Bool -> AssociatedFile -> Key -> ActionItem -> CommandStart toHereStart :: RemoveWhen -> AssociatedFile -> Key -> ActionItem -> CommandStart
toHereStart move afile key ai toHereStart removewhen afile key ai
| move = go | removewhen == RemoveNever = stopUnless (not <$> inAnnex key) go
| otherwise = stopUnless (not <$> inAnnex key) go | otherwise = go
where where
go = do go = do
rs <- Remote.keyPossibilities key rs <- Remote.keyPossibilities key
forM_ rs $ \r -> forM_ rs $ \r ->
includeCommandAction $ do includeCommandAction $ do
showMoveAction move key ai showMoveAction removewhen key ai
next $ fromPerform r move key afile next $ fromPerform r removewhen key afile
stop stop

View file

@ -667,7 +667,7 @@ syncFile ebloom rs af k = onlyActionOn' k $ do
, return [] , return []
) )
put dest = includeCommandAction $ put dest = includeCommandAction $
Command.Move.toStart' dest False af k (mkActionItem af) Command.Move.toStart' dest Command.Move.RemoveNever af k (mkActionItem af)
{- When a remote has an export-tracking branch, change the export to {- When a remote has an export-tracking branch, change the export to
- follow the current content of the branch. Otherwise, transfer any files - follow the current content of the branch. Otherwise, transfer any files

View file

@ -0,0 +1,25 @@
`git annex move` does not honor numcopies. It is the only
git-annex command to not honor numcopies by default.
This can be surprising, and it complicates git-annex's story about
attempting to preserve numcopies since there's this exception on the side.
Also, `git annex move --to untrusted-repo` drops the local copy even if the
untrusted copy is the only remaining copy, which is another unique thing
about move.
Should `git annex move --safe` become the default, and `git annex move
--unsafe` be needed to get the current behavior?
(Note that the `-u` short option makes that easy enough to type for those
of us who have workflows using the current behavior.)
Such a change would break workflows, and potentially quite a lot of
examples in the documentation might need to be updated. Although with the
default numcopies=1, the move behavior would not change (except when moving
onto an untrusted repository, ), which will limit the imact some.
There could be a transition period where move warns when run w/o
--safe/--unsafe.
--[[Joey]]

View file

@ -0,0 +1,18 @@
[[!comment format=mdwn
username="joey"
subject="""comment 2"""
date="2018-04-09T17:47:38Z"
content="""
You're exactly right; the move command bypasses the normal numcopies check,
in a way that is otherwise (AFAICR) only ever done when using --force.
There's also no way to get exactly the behavior of a numcopies honoring
move. As you note copy followed by drop has that behavior, but all the
drops then happen at the end, which is clumsy. There's clearly a good case
for an option to make move honor numcopies if it does not by default.
I've added that as `git annex move --safe`.
I feel that the divergence from everything else in honoring numcopies by
default is important enough to treat as a bug, so I opened
[[bugs/move_violates_numcopies]].
"""]]

View file

@ -10,6 +10,10 @@ git annex move `[path ...] [--from=remote|--to=remote|--to=here]`
Moves the content of files from or to another remote. Moves the content of files from or to another remote.
Note that by default, this command does not try to preserve the configured
minimum number of copies of files. It is the only git-annex command to not
do so.
# OPTIONS # OPTIONS
* `--from=remote` * `--from=remote`
@ -25,6 +29,17 @@ Moves the content of files from or to another remote.
Move the content of files from all reachable remotes to the local Move the content of files from all reachable remotes to the local
repository. repository.
* `--safe` `-s`
Preserve the configured minimum number of copies of files, by only
dropping content when there are enough other copies, and otherwise
only copying to the destination.
* `--unsafe` `-u`
Do not preserve the configured minimum number of copies of files;
always move the content between repositories. Currently the default.
* `--jobs=N` `-JN` * `--jobs=N` `-JN`
Enables parallel transfers with up to the specified number of jobs Enables parallel transfers with up to the specified number of jobs