packages feed

git-annex 6.20171124 → 6.20171214

raw patch · 98 files changed

+997/−766 lines, 98 filesdep +tagsoupdep −yesod-default

Dependencies added: tagsoup

Dependencies removed: yesod-default

Files

Annex.hs view
@@ -64,7 +64,6 @@ import Types.DesktopNotify import Types.CleanupActions import qualified Database.Keys.Handle as Keys-import Utility.Quvi (QuviVersion) import Utility.InodeCache import Utility.Url @@ -134,7 +133,6 @@ 	, errcounter :: Integer 	, unusedkeys :: Maybe (S.Set Key) 	, tempurls :: M.Map Key URLString-	, quviversion :: Maybe QuviVersion 	, existinghooks :: M.Map Git.Hook.Hook Bool 	, desktopnotify :: DesktopNotify 	, workers :: [Either AnnexState (Async AnnexState)]@@ -190,7 +188,6 @@ 		, errcounter = 0 		, unusedkeys = Nothing 		, tempurls = M.empty-		, quviversion = Nothing 		, existinghooks = M.empty 		, desktopnotify = mempty 		, workers = []
Annex/Content.hs view
@@ -21,6 +21,7 @@ 	prepTmp, 	withTmp, 	checkDiskSpace,+	needMoreDiskSpace, 	moveAnnex, 	populatePointerFile, 	linkToAnnex,@@ -42,11 +43,13 @@ 	dirKeys, 	withObjectLoc, 	staleKeysPrune,+	pruneTmpWorkDirBefore, 	isUnmodified, 	verifyKeyContent, 	VerifyConfig(..), 	Verification(..), 	unVerified,+	withTmpWorkDir, ) where  import System.IO.Unsafe (unsafeInterleaveIO)@@ -167,7 +170,7 @@ 	checkdirect contentfile lockfile = 		ifM (liftIO $ doesFileExist contentfile) 			( modifyContent lockfile $ liftIO $-				lockShared >>= \case+				lockShared lockfile >>= \case 					Nothing -> return is_locked 					Just lockhandle -> do 						dropLock lockhandle@@ -303,7 +306,7 @@ 	(ok, verification) <- action tmpfile 	if ok 		then ifM (verifyKeyContent v verification key tmpfile)-			( ifM (moveAnnex key tmpfile)+			( ifM (pruneTmpWorkDirBefore tmpfile (moveAnnex key)) 				( do 					logStatus key InfoPresent 					return True@@ -311,7 +314,7 @@ 				) 			, do 				warning "verification of content failed"-				liftIO $ nukeFile tmpfile+				pruneTmpWorkDirBefore tmpfile (liftIO . nukeFile) 				return False 			) 		-- On transfer failure, the tmp file is left behind, in case@@ -386,7 +389,7 @@ 	createAnnexDirectory (parentDir tmp) 	return tmp -{- Creates a temp file for a key, runs an action on it, and cleans up+{- Prepares a temp file for a key, runs an action on it, and cleans up  - the temp file. If the action throws an exception, the temp file is  - left behind, which allows for resuming.  -}@@ -394,7 +397,7 @@ withTmp key action = do 	tmp <- prepTmp key 	res <- action tmp-	liftIO $ nukeFile tmp+	pruneTmpWorkDirBefore tmp (liftIO . nukeFile) 	return res  {- Checks that there is disk space available to store a given key,@@ -429,16 +432,17 @@ 				let delta = need + reserve - have - alreadythere + inprogress 				let ok = delta <= 0 				unless ok $-					needmorespace delta+					warning $ needMoreDiskSpace delta 				return ok 			_ -> return True 	)   where 	dir = maybe (fromRepo gitAnnexDir) return destdir-	needmorespace n =-		warning $ "not enough free space, need " ++ -			roughSize storageUnits True n ++-			" more" ++ forcemsg++needMoreDiskSpace :: Integer -> String+needMoreDiskSpace n = "not enough free space, need " ++ +	roughSize storageUnits True n ++ " more" ++ forcemsg+  where 	forcemsg = " (use --force to override this check or adjust annex.diskreserve)"  {- Moves a key's content into .git/annex/objects/@@ -989,7 +993,8 @@ 	let stale = contents `exclude` dups  	dir <- fromRepo dirspec-	liftIO $ forM_ dups $ \t -> removeFile $ dir </> keyFile t+	forM_ dups $ \k ->+		pruneTmpWorkDirBefore (dir </> keyFile k) (liftIO . removeFile)  	if nottransferred 		then do@@ -997,6 +1002,43 @@ 				<$> getTransfers 			return $ filter (`S.notMember` inprogress) stale 		else return stale++{- Prune the work dir associated with the specified content file,+ - before performing an action that deletes the file, or moves it away.+ -+ - This preserves the invariant that the workdir never exists without+ - the content file.+ -}+pruneTmpWorkDirBefore :: FilePath -> (FilePath -> Annex a) -> Annex a+pruneTmpWorkDirBefore f action = do+	let workdir = gitAnnexTmpWorkDir f+	liftIO $ whenM (doesDirectoryExist workdir) $+		removeDirectoryRecursive workdir+	action f++{- Runs an action, passing it a temporary work directory where+ - it can write files while receiving the content of a key.+ -+ - On exception, or when the action returns Nothing,+ - the temporary work directory is left, so resumes can use it.+ -}+withTmpWorkDir :: Key -> (FilePath -> Annex (Maybe a)) -> Annex (Maybe a)+withTmpWorkDir key action = do+	-- Create the object file if it does not exist. This way,+	-- staleKeysPrune only has to look for object files, and can+	-- clean up gitAnnexTmpWorkDir for those it finds.+	obj <- prepTmp key+	unlessM (liftIO $ doesFileExist obj) $ do+		liftIO $ writeFile obj ""+		setAnnexFilePerm obj+	let tmpdir = gitAnnexTmpWorkDir obj+	liftIO $ createDirectoryIfMissing True tmpdir+	setAnnexDirPerm tmpdir+	res <- action tmpdir+	case res of+		Just _ -> liftIO $ removeDirectoryRecursive tmpdir+		Nothing -> noop+	return res  {- Finds items in the first, smaller list, that are not  - present in the second, larger list.
Annex/Init.hs view
@@ -8,6 +8,7 @@ {-# LANGUAGE CPP #-}  module Annex.Init (+	AutoInit(..), 	ensureInitialized, 	isInitialized, 	initialize,@@ -48,6 +49,23 @@ import qualified Utility.LockFile.Posix as Posix #endif +newtype AutoInit = AutoInit Bool++checkCanInitialize :: AutoInit -> Annex a -> Annex a+checkCanInitialize (AutoInit True) a = a+checkCanInitialize (AutoInit False) a = fromRepo Git.repoWorkTree >>= \case+	Nothing -> a+	Just wt -> liftIO (catchMaybeIO (readFile (wt </> ".noannex"))) >>= \case+		Nothing -> a+		Just noannexmsg -> ifM (Annex.getState Annex.force)+			( a+			, do+				warning "Initialization prevented by .noannex file (use --force to override)"+				unless (null noannexmsg) $+					warning noannexmsg+				giveup "Not initialized."+			)+ genDescription :: Maybe String -> Annex String genDescription (Just d) = return d genDescription Nothing = do@@ -59,8 +77,8 @@ 		Right username -> [username, at, hostname, ":", reldir] 		Left _ -> [hostname, ":", reldir] -initialize :: Maybe String -> Maybe Version -> Annex ()-initialize mdescription mversion = do+initialize :: AutoInit -> Maybe String -> Maybe Version -> Annex ()+initialize ai mdescription mversion = checkCanInitialize ai $ do 	{- Has to come before any commits are made as the shared 	 - clone heuristic expects no local objects. -} 	sharedclone <- checkSharedClone@@ -70,7 +88,7 @@ 	ensureCommit $ Annex.Branch.create  	prepUUID-	initialize' mversion+	initialize' (AutoInit True) mversion 	 	initSharedClone sharedclone @@ -79,8 +97,8 @@  -- Everything except for uuid setup, shared clone setup, and initial -- description.-initialize' :: Maybe Version -> Annex ()-initialize' mversion = do+initialize' :: AutoInit -> Maybe Version -> Annex ()+initialize' ai mversion = checkCanInitialize ai $ do 	checkLockSupport 	checkFifoSupport 	checkCrippledFileSystem@@ -131,7 +149,7 @@ ensureInitialized = getVersion >>= maybe needsinit checkUpgrade   where 	needsinit = ifM Annex.Branch.hasSibling-			( initialize Nothing Nothing+			( initialize (AutoInit True) Nothing Nothing 			, giveup "First run: git-annex init" 			) 
Annex/Link.hs view
@@ -55,11 +55,10 @@ 		check probefilecontent $ 			return Nothing   where-	check getlinktarget fallback = do-		v <- liftIO $ catchMaybeIO $ getlinktarget file-		case v of+	check getlinktarget fallback = +		liftIO (catchMaybeIO $ getlinktarget file) >>= \case 			Just l-				| isLinkToAnnex (fromInternalGitPath l) -> return v+				| isLinkToAnnex (fromInternalGitPath l) -> return (Just l) 				| otherwise -> return Nothing 			Nothing -> fallback 
Annex/Locations.hs view
@@ -1,6 +1,6 @@ {- git-annex file locations  -- - Copyright 2010-2015 Joey Hess <id@joeyh.name>+ - Copyright 2010-2017 Joey Hess <id@joeyh.name>  -  - Licensed under the GNU GPL version 3 or higher.  -}@@ -27,6 +27,7 @@ 	gitAnnexTmpMiscDir, 	gitAnnexTmpObjectDir, 	gitAnnexTmpObjectLocation,+	gitAnnexTmpWorkDir, 	gitAnnexBadDir, 	gitAnnexBadLocation, 	gitAnnexUnusedLog,@@ -250,6 +251,19 @@ {- The temp file to use for a given key's content. -} gitAnnexTmpObjectLocation :: Key -> Git.Repo -> FilePath gitAnnexTmpObjectLocation key r = gitAnnexTmpObjectDir r </> keyFile key++{- Given a temp file such as gitAnnexTmpObjectLocation, makes a name for a+ - subdirectory in the same location, that can be used as a work area+ - when receiving the key's content.+ -+ - There are ordering requirements for creating these directories;+ - use Annex.Content.withTmpWorkDir to set them up.+ -}+gitAnnexTmpWorkDir :: FilePath -> FilePath+gitAnnexTmpWorkDir p =+	let (dir, f) = splitFileName p+	-- Using a prefix avoids name conflict with any other keys.+	in dir </> "work." ++ f  {- .git/annex/bad/ is used for bad files found during fsck -} gitAnnexBadDir :: Git.Repo -> FilePath
Annex/MakeRepo.hs view
@@ -76,7 +76,7 @@  initRepo' :: Maybe String -> Maybe StandardGroup -> Annex () initRepo' desc mgroup = unlessM isInitialized $ do-	initialize desc Nothing+	initialize (AutoInit False) desc Nothing 	u <- getUUID 	maybe noop (defaultStandardGroup u) mgroup 	{- Ensure branch gets committed right away so it is
Annex/Notification.hs view
@@ -5,6 +5,7 @@  - Licensed under the GNU GPL version 3 or higher.  -} +{-# LANGUAGE TypeSynonymInstances, FlexibleInstances #-} {-# LANGUAGE CPP #-}  module Annex.Notification (NotifyWitness, noNotification, notifyTransfer, notifyDrop) where@@ -28,26 +29,28 @@ {- Wrap around an action that performs a transfer, which may run multiple  - attempts. Displays notification when supported and when the user asked  - for it. -}-notifyTransfer :: Direction -> AssociatedFile -> (NotifyWitness -> Annex Bool) -> Annex Bool-notifyTransfer _ (AssociatedFile Nothing) a = a NotifyWitness+notifyTransfer :: Transferrable t => Observable v => Direction -> t -> (NotifyWitness -> Annex v) -> Annex v #ifdef WITH_DBUS_NOTIFICATIONS-notifyTransfer direction (AssociatedFile (Just f)) a = do-	wanted <- Annex.getState Annex.desktopnotify-	if (notifyStart wanted || notifyFinish wanted)-		then do-			client <- liftIO DBus.Client.connectSession-			startnotification <- liftIO $ if notifyStart wanted-				then Just <$> Notify.notify client (startedTransferNote direction f)-				else pure Nothing-			ok <- a NotifyWitness-			when (notifyFinish wanted) $ liftIO $ void $ maybe -				(Notify.notify client $ finishedTransferNote ok direction f)-				(\n -> Notify.replace client n $ finishedTransferNote ok direction f)-				startnotification-			return ok-		else a NotifyWitness+notifyTransfer direction t a = case descTransfrerrable t of+	Nothing -> a NotifyWitness+	Just desc -> do+		wanted <- Annex.getState Annex.desktopnotify+		if (notifyStart wanted || notifyFinish wanted)+			then do+				client <- liftIO DBus.Client.connectSession+				startnotification <- liftIO $ if notifyStart wanted+					then Just <$> Notify.notify client (startedTransferNote direction desc)+					else pure Nothing+				res <- a NotifyWitness+				let ok = observeBool res+				when (notifyFinish wanted) $ liftIO $ void $ maybe +					(Notify.notify client $ finishedTransferNote ok direction desc)+					(\n -> Notify.replace client n $ finishedTransferNote ok direction desc)+					startnotification+				return res+			else a NotifyWitness #else-notifyTransfer _ (AssociatedFile (Just _)) a = a NotifyWitness+notifyTransfer _ _ a = a NotifyWitness #endif  notifyDrop :: AssociatedFile -> Bool -> Annex ()@@ -63,13 +66,13 @@ #endif  #ifdef WITH_DBUS_NOTIFICATIONS-startedTransferNote :: Direction -> FilePath -> Notify.Note+startedTransferNote :: Direction -> String -> Notify.Note startedTransferNote Upload   = mkNote Notify.Transfer Notify.Low iconUpload 	"Uploading" startedTransferNote Download = mkNote Notify.Transfer Notify.Low iconDownload 	"Downloading" -finishedTransferNote :: Bool -> Direction -> FilePath -> Notify.Note+finishedTransferNote :: Bool -> Direction -> String -> Notify.Note finishedTransferNote False Upload   = mkNote Notify.TransferError Notify.Normal iconFailure 	"Failed to upload" finishedTransferNote False Download = mkNote Notify.TransferError Notify.Normal iconFailure@@ -79,7 +82,7 @@ finishedTransferNote True  Download = mkNote Notify.TransferComplete Notify.Low iconSuccess 	"Finished downloading" -droppedNote :: Bool -> FilePath -> Notify.Note+droppedNote :: Bool -> String -> Notify.Note droppedNote False = mkNote Notify.TransferError Notify.Normal iconFailure 	"Failed to drop" droppedNote True  = mkNote Notify.TransferComplete Notify.Low iconSuccess
Annex/NumCopies.hs view
@@ -121,24 +121,21 @@ verifyEnoughCopiesToDrop nolocmsg key removallock need skip preverified tocheck dropaction nodropaction =  	helper [] [] preverified (nub tocheck)   where-	helper bad missing have [] = do-		p <- liftIO $ mkSafeDropProof need have removallock-		case p of+	helper bad missing have [] =+		liftIO (mkSafeDropProof need have removallock) >>= \case 			Right proof -> dropaction proof 			Left stillhave -> do 				notEnoughCopies key need stillhave (skip++missing) bad nolocmsg 				nodropaction 	helper bad missing have (c:cs)-		| isSafeDrop need have removallock = do-			p <- liftIO $ mkSafeDropProof need have removallock-			case p of+		| isSafeDrop need have removallock =+			liftIO (mkSafeDropProof need have removallock) >>= \case 				Right proof -> dropaction proof 				Left stillhave -> helper bad missing stillhave (c:cs) 		| otherwise = case c of 			UnVerifiedHere -> lockContentShared key contverified-			UnVerifiedRemote r -> checkremote r contverified $ do-				haskey <- Remote.hasKey r key-				case haskey of+			UnVerifiedRemote r -> checkremote r contverified $+				Remote.hasKey r key >>= \case 					Right True  -> helper bad missing (mkVerifiedCopy RecentlyVerifiedCopy r : have) cs 					Left _      -> helper (r:bad) missing have cs 					Right False -> helper bad (Remote.uuid r:missing) have cs
Annex/Perms.hs view
@@ -111,9 +111,8 @@ 	go GroupShared = want [ownerWriteMode, groupWriteMode] 	go AllShared = want writeModes 	go _ = return True-	want wantmode = do-		mmode <- liftIO $ catchMaybeIO $ fileMode <$> getFileStatus file-		return $ case mmode of+	want wantmode =+		liftIO (catchMaybeIO $ fileMode <$> getFileStatus file) >>= return . \case 			Nothing -> True 			Just havemode -> havemode == combineModes (havemode:wantmode) 
− Annex/Quvi.hs
@@ -1,33 +0,0 @@-{- quvi options for git-annex- -- - Copyright 2013 Joey Hess <id@joeyh.name>- -- - Licensed under the GNU GPL version 3 or higher.- -}--{-# LANGUAGE Rank2Types #-}--module Annex.Quvi where--import Annex.Common-import qualified Annex-import Utility.Quvi-import Utility.Url--withQuviOptions :: forall a. Query a -> [QuviParams] -> URLString -> Annex a-withQuviOptions a ps url = do-	v <- quviVersion-	opts <- map Param . annexQuviOptions <$> Annex.getGitConfig-	liftIO $ a v (concatMap (\mkp -> mkp v) ps ++ opts) url--quviSupported :: URLString -> Annex Bool-quviSupported u = liftIO . flip supported u =<< quviVersion--quviVersion :: Annex QuviVersion-quviVersion = go =<< Annex.getState Annex.quviversion-  where-	go (Just v) = return v-	go Nothing = do-		v <- liftIO probeVersion-		Annex.changeState $ \s -> s { Annex.quviversion = Just v }-		return v
Annex/SpecialRemote.hs view
@@ -81,8 +81,7 @@ 			(Just name, Right t) -> whenM (canenable u) $ do 				showSideAction $ "Auto enabling special remote " ++ name 				dummycfg <- liftIO dummyRemoteGitConfig-				res <- tryNonAsync $ setup t (Enable c) (Just u) Nothing c dummycfg-				case res of+				tryNonAsync (setup t (Enable c) (Just u) Nothing c dummycfg) >>= \case 					Left e -> warning (show e) 					Right _ -> return () 			_ -> return ()
Annex/Ssh.hs view
@@ -101,9 +101,8 @@ sshCachingInfo (host, port) = go =<< sshCacheDir   where 	go Nothing = return (Nothing, [])-	go (Just dir) = do-		r <- liftIO $ bestSocketPath $ dir </> hostport2socket host port-		return $ case r of+	go (Just dir) =+		liftIO (bestSocketPath $ dir </> hostport2socket host port) >>= return . \case 			Nothing -> (Nothing, []) 			Just socketfile -> (Just socketfile, sshConnectionCachingParams socketfile) @@ -190,8 +189,7 @@ 	liftIO $ createDirectoryIfMissing True $ parentDir socketfile 	let socketlock = socket2lock socketfile -	c <- Annex.getState Annex.concurrency-	case c of+	Annex.getState Annex.concurrency >>= \case 		Concurrent {} 			| annexUUID (remoteGitConfig gc) /= NoUUID -> 				makeconnection socketlock@@ -267,8 +265,7 @@ 		let lockfile = socket2lock socketfile 		unlockFile lockfile 		mode <- annexFileMode-		v <- noUmask mode $ tryLockExclusive (Just mode) lockfile-		case v of+		noUmask mode (tryLockExclusive (Just mode) lockfile) >>= \case 			Nothing -> noop 			Just lck -> do 				forceStopSsh socketfile
Annex/Transfer.hs view
@@ -5,7 +5,7 @@  - Licensed under the GNU GPL version 3 or higher.  -} -{-# LANGUAGE CPP, FlexibleInstances, BangPatterns #-}+{-# LANGUAGE CPP, BangPatterns #-}  module Annex.Transfer ( 	module X,@@ -27,7 +27,6 @@ import Utility.Metered import Annex.LockPool import Types.Key-import Types.Remote (Verification(..)) import qualified Types.Remote as Remote import Types.Concurrency @@ -35,23 +34,6 @@ import qualified Data.Map.Strict as M import Data.Ord -class Observable a where-	observeBool :: a -> Bool-	observeFailure :: a--instance Observable Bool where-	observeBool = id-	observeFailure = False--instance Observable (Bool, Verification) where-	observeBool = fst-	observeFailure = (False, UnVerified)--instance Observable (Either e Bool) where-	observeBool (Left _) = False-	observeBool (Right b) = b-	observeFailure = Right False- upload :: Observable v => UUID -> Key -> AssociatedFile -> RetryDecider -> (MeterUpdate -> Annex v) -> NotifyWitness -> Annex v upload u key f d a _witness = guardHaveUUID u $  	runTransfer (Transfer Upload u key) f d a@@ -110,8 +92,7 @@ 	prep tfile mode info = catchPermissionDenied (const prepfailed) $ do 		let lck = transferLockFile tfile 		createAnnexDirectory $ takeDirectory lck-		r <- tryLockExclusive (Just mode) lck-		case r of+		tryLockExclusive (Just mode) lck >>= \case 			Nothing -> return (Nothing, True) 			Just lockhandle -> ifM (checkSaneLock lck lockhandle) 				( do@@ -126,8 +107,7 @@ 	prep tfile _mode info = catchPermissionDenied (const prepfailed) $ do 		let lck = transferLockFile tfile 		createAnnexDirectory $ takeDirectory lck-		v <- catchMaybeIO $ liftIO $ lockExclusive lck-		case v of+		catchMaybeIO (liftIO $ lockExclusive lck) >>= \case 			Nothing -> return (Nothing, False) 			Just Nothing -> return (Nothing, True) 			Just (Just lockhandle) -> do@@ -153,17 +133,15 @@ 		dropLock lockhandle 		void $ tryIO $ removeFile lck #endif-	retry oldinfo metervar run = do-		v <- tryNonAsync run-		case v of-			Right b -> return b-			Left e -> do-				warning (show e)-				b <- getbytescomplete metervar-				let newinfo = oldinfo { bytesComplete = Just b }-				if shouldretry oldinfo newinfo-					then retry newinfo metervar run-					else return observeFailure+	retry oldinfo metervar run = tryNonAsync run >>= \case+		Right b -> return b+		Left e -> do+			warning (show e)+			b <- getbytescomplete metervar+			let newinfo = oldinfo { bytesComplete = Just b }+			if shouldretry oldinfo newinfo+				then retry newinfo metervar run+				else return observeFailure 	getbytescomplete metervar 		| transferDirection t == Upload = 			liftIO $ readMVar metervar
Annex/Url.hs view
@@ -31,11 +31,9 @@ 	<*> headers 	<*> options   where-	headers = do-		v <- annexHttpHeadersCommand <$> Annex.getGitConfig-		case v of-			Just cmd -> lines <$> liftIO (readProcess "sh" ["-c", cmd])-			Nothing -> annexHttpHeaders <$> Annex.getGitConfig+	headers = annexHttpHeadersCommand <$> Annex.getGitConfig >>= \case+		Just cmd -> lines <$> liftIO (readProcess "sh" ["-c", cmd])+		Nothing -> annexHttpHeaders <$> Annex.getGitConfig 	options = map Param . annexWebOptions <$> Annex.getGitConfig  withUrlOptions :: (U.UrlOptions -> IO a) -> Annex a
Annex/WorkTree.hs view
@@ -30,17 +30,15 @@  - looking for a pointer to a key in git.  -} lookupFile :: FilePath -> Annex (Maybe Key)-lookupFile file = do-	mkey <- isAnnexLink file-	case mkey of-		Just key -> makeret key-		Nothing -> ifM (versionSupportsUnlockedPointers <||> isDirect)-			( ifM (liftIO $ doesFileExist file)-				( maybe (return Nothing) makeret =<< catKeyFile file-				, return Nothing-				)-			, return Nothing +lookupFile file = isAnnexLink file >>= \case+	Just key -> makeret key+	Nothing -> ifM (versionSupportsUnlockedPointers <||> isDirect)+		( ifM (liftIO $ doesFileExist file)+			( maybe (return Nothing) makeret =<< catKeyFile file+			, return Nothing 			)+		, return Nothing +		)   where 	makeret = return . Just @@ -84,9 +82,8 @@ 		whenM (inAnnex k) $ do 			f <- fromRepo $ fromTopFilePath tf 			destmode <- liftIO $ catchMaybeIO $ fileMode <$> getFileStatus f-			replaceFile f $ \tmp -> do-				r <- linkFromAnnex k tmp destmode-				case r of+			replaceFile f $ \tmp ->+				linkFromAnnex k tmp destmode >>= \case 					LinkAnnexOk -> return () 					LinkAnnexNoop -> return () 					LinkAnnexFailed -> liftIO $
+ Annex/YoutubeDl.hs view
@@ -0,0 +1,181 @@+{- youtube-dl integration for git-annex+ -+ - Copyright 2017 Joey Hess <id@joeyh.name>+ -+ - Licensed under the GNU GPL version 3 or higher.+ -}++module Annex.YoutubeDl (+	youtubeDl,+	youtubeDlTo,+	youtubeDlSupported,+	youtubeDlCheck,+	youtubeDlFileName,+) where++import Annex.Common+import qualified Annex+import Annex.Content+import Annex.Url+import Utility.Url (URLString)+import Utility.DiskFree+import Utility.HtmlDetect+import Logs.Transfer++import Network.URI++-- Runs youtube-dl in a work directory, to download a single media file+-- from the url. Reutrns the path to the media file in the work directory.+--+-- If youtube-dl fails without writing any files to the work directory, +-- or is not installed, returns Right Nothing.+--+-- The work directory can contain files from a previous run of youtube-dl+-- and it will resume. It should not contain any other files though,+-- and youtube-dl needs to finish up with only one file in the directory+-- so we know which one it downloaded.+--+-- (Note that we can't use --output to specifiy the file to download to,+-- due to <https://github.com/rg3/youtube-dl/issues/14864>)+youtubeDl :: URLString -> FilePath -> Annex (Either String (Maybe FilePath))+youtubeDl url workdir+	| supportedScheme url = ifM (liftIO $ inPath "youtube-dl")+		( runcmd >>= \case+			Right True -> workdirfiles >>= \case+				(f:[]) -> return (Right (Just f))+				[] -> return nofiles+				fs -> return (toomanyfiles fs)+			Right False -> workdirfiles >>= \case+				[] -> return (Right Nothing)+				_ -> return (Left "youtube-dl download is incomplete. Run the command again to resume.")+			Left msg -> return (Left msg)+		, return (Right Nothing)+		)+	| otherwise = return (Right Nothing)+  where+	nofiles = Left "youtube-dl did not put any media in its work directory, perhaps it's been configured to store files somewhere else?"+	toomanyfiles fs = Left $ "youtube-dl downloaded multiple media files; git-annex is only able to deal with one per url: " ++ show fs+	workdirfiles = liftIO $ filterM (doesFileExist) =<< dirContents workdir+	runcmd = youtubeDlMaxSize workdir >>= \case+		Left msg -> return (Left msg)+		Right maxsize -> do+			quiet <- commandProgressDisabled+			opts <- youtubeDlOpts $ dlopts ++ maxsize ++ +				if quiet then [ Param "--quiet" ] else []+			ok <- liftIO $ boolSystem' "youtube-dl" opts $+				\p -> p { cwd = Just workdir }+			return (Right ok)+	dlopts = +		[ Param url+		-- To make youtube-dl only download one file when given a+		-- page with a video and a playlist, download only the video.+		, Param "--no-playlist"+		-- And when given a page with only a playlist, download only+		-- the first video on the playlist. (Assumes the video is+		-- somewhat stable, but this is the only way to prevent+		-- youtube-dl from downloading the whole playlist.)+		, Param "--playlist-items", Param "0"+		]++-- To honor annex.diskreserve, ask youtube-dl to not download too+-- large a media file. Factors in other downloads that are in progress,+-- and any files in the workdir that it may have partially downloaded+-- before.+youtubeDlMaxSize :: FilePath -> Annex (Either String [CommandParam])+youtubeDlMaxSize workdir = ifM (Annex.getState Annex.force)+	( return $ Right []+	, liftIO (getDiskFree workdir) >>= \case+		Just have -> do+			inprogress <- sizeOfDownloadsInProgress (const True)+			partial <- liftIO $ sum +				<$> (mapM getFileSize =<< dirContents workdir)+			reserve <- annexDiskReserve <$> Annex.getGitConfig+			let maxsize = have - reserve - inprogress + partial+			if maxsize > 0+				then return $ Right+					[ Param "--max-filesize"+					, Param (show maxsize)+					]+				else return $ Left $+					needMoreDiskSpace $+						negate maxsize + 1024+		Nothing -> return $ Right []+	)++-- Download a media file to a destination, +youtubeDlTo :: Key -> URLString -> FilePath -> Annex Bool+youtubeDlTo key url dest = do+	res <- withTmpWorkDir key $ \workdir ->+		youtubeDl url workdir >>= \case+			Right (Just mediafile) -> do+				liftIO $ renameFile mediafile dest+				return (Just True)+			Right Nothing -> return (Just False)+			Left msg -> do+				warning msg+				return Nothing+	return (fromMaybe False res)++-- youtube-dl supports downloading urls that are not html pages,+-- but we don't want to use it for such urls, since they can be downloaded+-- without it. So, this first downloads part of the content and checks +-- if it's a html page; only then is youtube-dl used.+htmlOnly :: URLString -> a -> Annex a -> Annex a+htmlOnly url fallback a = do+	uo <- getUrlOptions+	liftIO (downloadPartial url uo htmlPrefixLength) >>= \case+		Just bs | isHtmlBs bs -> a+		_ -> return fallback++youtubeDlSupported :: URLString -> Annex Bool+youtubeDlSupported url = either (const False) id <$> youtubeDlCheck url++-- Check if youtube-dl can find media in an url.+youtubeDlCheck :: URLString -> Annex (Either String Bool)+youtubeDlCheck url+	| supportedScheme url = catchMsgIO $ htmlOnly url False $ do+		opts <- youtubeDlOpts [ Param url, Param "--simulate" ]+		liftIO $ snd <$> processTranscript "youtube-dl" (toCommand opts) Nothing+	| otherwise = return (Right False)++-- Ask youtube-dl for the filename of media in an url.+--+-- (This is not always identical to the filename it uses when downloading.)+youtubeDlFileName :: URLString -> Annex (Either String FilePath)+youtubeDlFileName url+	| supportedScheme url = flip catchIO (pure . Left . show) $+		htmlOnly url nomedia go+	| otherwise = return nomedia+  where+	go = do+		-- Sometimes youtube-dl will fail with an ugly backtrace+		-- (eg, http://bugs.debian.org/874321)+		-- so catch stderr as well as stdout to avoid the user+		-- seeing it. --no-warnings avoids warning messages that+		-- are output to stdout.+		opts <- youtubeDlOpts+			[ Param url+			, Param "--get-filename"+			, Param "--no-warnings"+			]+		(output, ok) <- liftIO $ processTranscript "youtube-dl"+			(toCommand opts) Nothing+		return $ case (ok, lines output) of+			(True, (f:_)) | not (null f) -> Right f+			_ -> nomedia+	nomedia = Left "no media in url"++youtubeDlOpts :: [CommandParam] -> Annex [CommandParam]+youtubeDlOpts addopts = do+	opts <- map Param . annexYoutubeDlOptions <$> Annex.getGitConfig+	return (opts ++ addopts)++supportedScheme :: URLString -> Bool+supportedScheme url = case uriScheme <$> parseURIRelaxed url of+	Nothing -> False+	-- avoid ugly message from youtube-dl about not supporting file:+	Just "file:" -> False+	-- ftp indexes may look like html pages, and there's no point+	-- involving youtube-dl in a ftp download+	Just "ftp:" -> False+	Just _ -> True
Assistant/MakeRemote.hs view
@@ -79,17 +79,15 @@ 	go :: Int -> Annex RemoteName 	go n = do 		let fullname = if n == 0  then name else name ++ show n-		r <- Annex.SpecialRemote.findExisting fullname-		case r of+		Annex.SpecialRemote.findExisting fullname >>= \case 			Nothing -> setupSpecialRemote fullname remotetype config mcreds 				(Nothing, R.Init, Annex.SpecialRemote.newConfig fullname) 			Just _ -> go (n + 1)  {- Enables an existing special remote. -} enableSpecialRemote :: SpecialRemoteMaker-enableSpecialRemote name remotetype mcreds config = do-	r <- Annex.SpecialRemote.findExisting name-	case r of+enableSpecialRemote name remotetype mcreds config =+	Annex.SpecialRemote.findExisting name >>= \case 		Nothing -> error $ "Cannot find a special remote named " ++ name 		Just (u, c) -> setupSpecialRemote' False name remotetype config mcreds (Just u, R.Enable c, c) 
Assistant/NamedThread.hs view
@@ -35,9 +35,8 @@  - Named threads are run by a management thread, so if they crash  - an alert is displayed, allowing the thread to be restarted. -} startNamedThread :: UrlRenderer -> NamedThread -> Assistant ()-startNamedThread urlrenderer (NamedThread afterstartupsanitycheck name a) = do-	m <- startedThreads <$> getDaemonStatus-	case M.lookup name m of+startNamedThread urlrenderer (NamedThread afterstartupsanitycheck name a) =+	M.lookup name . startedThreads <$> getDaemonStatus >>= \case 		Nothing -> start 		Just (aid, _) -> do 			r <- liftIO (E.try (poll aid) :: IO (Either E.SomeException (Maybe (Either E.SomeException ()))))@@ -65,24 +64,22 @@ 			a 		void $ forkIO $ manager d aid 		return aid-	manager d aid = do-		r <- E.try (wait aid) :: IO (Either E.SomeException ())-		case r of-			Right _ -> noop-			Left e -> do-				let msg = unwords-					[ fromThreadName $ threadName d-					, "crashed:", show e-					]-				hPutStrLn stderr msg+	manager d aid = (E.try (wait aid) :: IO (Either E.SomeException ())) >>= \case+		Right _ -> noop+		Left e -> do+			let msg = unwords+				[ fromThreadName $ threadName d+				, "crashed:", show e+				]+			hPutStrLn stderr msg #ifdef WITH_WEBAPP-				button <- runAssistant d $ mkAlertButton True-					(T.pack "Restart Thread")-					urlrenderer -					(RestartThreadR name)-				runAssistant d $ void $ addAlert $-					(warningAlert (fromThreadName name) msg)-						{ alertButtons = [button] }+			button <- runAssistant d $ mkAlertButton True+				(T.pack "Restart Thread")+				urlrenderer +				(RestartThreadR name)+			runAssistant d $ void $ addAlert $+				(warningAlert (fromThreadName name) msg)+					{ alertButtons = [button] } #endif  namedThreadId :: NamedThread -> Assistant (Maybe ThreadId)
Backend.hs view
@@ -52,8 +52,7 @@ genKey :: KeySource -> Maybe Backend -> Annex (Maybe (Key, Backend)) genKey source preferredbackend = do 	b <- maybe defaultBackend return preferredbackend-	r <- B.getKey b source-	return $ case r of+	B.getKey b source >>= return . \case 		Nothing -> Nothing 		Just k -> Just (makesane k, b)   where
Backend/Hash.hs view
@@ -176,9 +176,8 @@ 	 	usehasher hashsize@(HashSize sz) = case shaHasher hashsize filesize of 		Left sha -> use sha-		Right (external, internal) -> do-			v <- liftIO $ externalSHA external sz file-			case v of+		Right (external, internal) ->+			liftIO (externalSHA external sz file) >>= \case 				Right r -> return r 				Left e -> do 					warning e
BuildInfo.hs view
@@ -81,7 +81,6 @@ 	-- Always enabled now, but users may be used to seeing these flags 	-- listed. 	, "Feeds"-	, "Quvi" 	]  -- Not a complete list, let alone a listing transitive deps, but only
CHANGELOG view
@@ -1,3 +1,28 @@+git-annex (6.20171214) unstable; urgency=medium++  * Use youtube-dl rather than quvi to download media from web pages,+    since quvi is not being actively developed and youtube-dl supports+    many more sites.+  * addurl --relaxed got slower, since youtube-dl has to hit the network+    to check for embedded media. If you relied on --relaxed not hitting the+    network for speed reasons, using --relaxed --raw will get the old level+    of speed, but can't be used for urls with embedded videos.+  * importfeed now downloads things linked to by feeds, even when they are+    not media files.+  * Removed no longer needed dependency on yesod-default.+  * Allow exporttree remotes to be marked as dead.+  * initremote, enableremote: Really support gpg subkeys suffixed with an+    exclamation mark, which forces gpg to use a specific subkey.+    (Previous try had a bug.)+  * lookupkey: Support being given an absolute filename to a file+    within the current git repository.+  * A top-level .noannex file will prevent git-annex init from being used+    in a repository. This is useful for repositories that have a policy+    reason not to use git-annex. The content of the file will be displayed+    to the user who tries to run git-annex init.++ -- Joey Hess <id@joeyh.name>  Thu, 14 Dec 2017 11:50:48 -0400+ git-annex (6.20171124) unstable; urgency=medium    * Display progress meter when uploading a key without size information,
CmdLine/Action.hs view
@@ -186,13 +186,11 @@ onlyActionOn :: Key -> CommandStart -> CommandStart onlyActionOn k a = onlyActionOn' k run   where-	run = do-		-- Run whole action, not just start stage, so other threads-		-- block until it's done.-		r <- callCommandAction' a-		case r of-			Nothing -> return Nothing-			Just r' -> return $ Just $ return $ Just $ return r'+	-- Run whole action, not just start stage, so other threads+	-- block until it's done.+	run = callCommandAction' a >>= \case+		Nothing -> return Nothing+		Just r' -> return $ Just $ return $ Just $ return r'  onlyActionOn' :: Key -> Annex a -> Annex a onlyActionOn' k a = go =<< Annex.getState Annex.concurrency
CmdLine/GitAnnexShell/Checks.hs view
@@ -21,12 +21,10 @@ checkNotReadOnly = checkEnv "GIT_ANNEX_SHELL_READONLY"  checkEnv :: String -> IO ()-checkEnv var = do-	v <- getEnv var-	case v of-		Nothing -> noop-		Just "" -> noop-		Just _ -> giveup $ "Action blocked by " ++ var+checkEnv var = getEnv var >>= \case+	Nothing -> noop+	Just "" -> noop+	Just _ -> giveup $ "Action blocked by " ++ var  checkDirectory :: Maybe FilePath -> IO () checkDirectory mdir = do
CmdLine/GitRemoteTorAnnex.hs view
@@ -19,14 +19,12 @@ import P2P.Auth  run :: [String] -> IO ()-run (_remotename:address:[]) = forever $ do-	-- gitremote-helpers protocol-	l <- getLine-	case l of+run (_remotename:address:[]) = forever $+	getLine >>= \case 		"capabilities" -> putStrLn "connect" >> ready 		"connect git-upload-pack" -> go UploadPack 		"connect git-receive-pack" -> go ReceivePack-		_ -> error $ "git-remote-helpers protocol error at " ++ show l+		l -> error $ "git-remote-helpers protocol error at " ++ show l   where 	(onionaddress, onionport) 		| '/' `elem` address = parseAddressPort $@@ -59,8 +57,6 @@ 		myuuid <- getUUID 		g <- Annex.gitRepo 		conn <- liftIO $ connectPeer g (TorAnnex address port)-		liftIO $ runNetProto conn $ do-			v <- auth myuuid authtoken-			case v of-				Just _theiruuid -> connect service stdin stdout-				Nothing -> giveup $ "authentication failed, perhaps you need to set " ++ p2pAuthTokenEnv+		liftIO $ runNetProto conn $ auth myuuid authtoken >>= \case+			Just _theiruuid -> connect service stdin stdout+			Nothing -> giveup $ "authentication failed, perhaps you need to set " ++ p2pAuthTokenEnv
CmdLine/Seek.hs view
@@ -84,8 +84,7 @@ 		(l, cleanup) <- inRepo $ LsTree.lsTree r 		forM_ l $ \i -> do 			let f = getTopFilePath $ LsTree.file i-			v <- catKey (LsTree.sha i)-			case v of+			catKey (LsTree.sha i) >>= \case 				Nothing -> noop 				Just k -> whenM (matcher $ MatchingKey k) $ 					commandAction $ a f k
Command.hs view
@@ -68,8 +68,7 @@ {- Undoes noMessages -} allowMessages :: Annex () allowMessages = do-	curr <- Annex.getState Annex.output-	case outputType curr of+	outputType <$> Annex.getState Annex.output >>= \case 		QuietOutput -> Annex.setOutput NormalOutput 		_ -> noop 	Annex.changeState $ \s -> s
Command/Add.hs view
@@ -98,31 +98,25 @@ 		)   where 	go = ifAnnexed file addpresent add-	add = do-		ms <- liftIO $ catchMaybeIO $ getSymbolicLinkStatus file-		case ms of-			Nothing -> stop-			Just s -				| not (isRegularFile s) && not (isSymbolicLink s) -> stop-				| otherwise -> do-					showStart "add" file-					next $ if isSymbolicLink s-						then next $ addFile file-						else perform file+	add = liftIO (catchMaybeIO $ getSymbolicLinkStatus file) >>= \case+		Nothing -> stop+		Just s +			| not (isRegularFile s) && not (isSymbolicLink s) -> stop+			| otherwise -> do+				showStart "add" file+				next $ if isSymbolicLink s+					then next $ addFile file+					else perform file 	addpresent key = ifM versionSupportsUnlockedPointers-		( do-			ms <- liftIO $ catchMaybeIO $ getSymbolicLinkStatus file-			case ms of-				Just s | isSymbolicLink s -> fixuplink key-				_ -> ifM (sameInodeCache file =<< Database.Keys.getInodeCaches key)-						( stop, add )+		( liftIO (catchMaybeIO $ getSymbolicLinkStatus file) >>= \case+			Just s | isSymbolicLink s -> fixuplink key+			_ -> ifM (sameInodeCache file =<< Database.Keys.getInodeCaches key)+				( stop, add ) 		, ifM isDirect-			( do-				ms <- liftIO $ catchMaybeIO $ getSymbolicLinkStatus file-				case ms of-					Just s | isSymbolicLink s -> fixuplink key-					_ -> ifM (goodContent key file)-						( stop , add )+			( liftIO (catchMaybeIO $ getSymbolicLinkStatus file) >>= \case+				Just s | isSymbolicLink s -> fixuplink key+				_ -> ifM (goodContent key file)+					( stop , add ) 			, fixuplink key 			) 		)
Command/AddUrl.hs view
@@ -1,6 +1,6 @@ {- git-annex command  -- - Copyright 2011-2014 Joey Hess <id@joeyh.name>+ - Copyright 2011-2017 Joey Hess <id@joeyh.name>  -  - Licensed under the GNU GPL version 3 or higher.  -}@@ -21,6 +21,7 @@ import Annex.Ingest import Annex.CheckIgnore import Annex.UUID+import Annex.YoutubeDl import Logs.Web import Types.KeySource import Types.UrlContents@@ -28,9 +29,8 @@ import Logs.Location import Utility.Metered import Utility.FileSystemEncoding+import Utility.HtmlDetect import qualified Annex.Transfer as Transfer-import Annex.Quvi-import qualified Utility.Quvi as Quvi  cmd :: Command cmd = notBareRepo $ withGlobalOptions [jobsOption, jsonOption, jsonProgressOption] $@@ -39,23 +39,23 @@  data AddUrlOptions = AddUrlOptions 	{ addUrls :: CmdParams-	, fileOption :: Maybe FilePath 	, pathdepthOption :: Maybe Int 	, prefixOption :: Maybe String 	, suffixOption :: Maybe String-	, relaxedOption :: Bool-	, rawOption :: Bool+	, downloadOptions :: DownloadOptions 	, batchOption :: BatchMode 	, batchFilesOption :: Bool 	} +data DownloadOptions = DownloadOptions+	{ relaxedOption :: Bool+	, rawOption :: Bool+	, fileOption :: Maybe FilePath+	}+ optParser :: CmdParamsDesc -> Parser AddUrlOptions optParser desc = AddUrlOptions 	<$> cmdParams desc-	<*> optional (strOption-		( long "file" <> metavar paramFile-		<> help "specify what file the url is added to"-		)) 	<*> optional (option auto 		( long "pathdepth" <> metavar paramNumber 		<> help "number of url path components to use in filename"@@ -68,25 +68,29 @@ 		( long "suffix" <> metavar paramValue 		<> help "add a suffix to the filename" 		))-	<*> parseRelaxedOption-	<*> parseRawOption+	<*> parseDownloadOptions True 	<*> parseBatchOption 	<*> switch 		( long "with-files" 		<> help "parse batch mode lines of the form \"$url $file\"" 		) -parseRelaxedOption :: Parser Bool-parseRelaxedOption = switch-	( long "relaxed"-	<> help "skip size check"-	)--parseRawOption :: Parser Bool-parseRawOption = switch-	( long "raw"-	<> help "disable special handling for torrents, quvi, etc"-	)+parseDownloadOptions :: Bool -> Parser DownloadOptions+parseDownloadOptions withfileoption = DownloadOptions+	<$> switch+		( long "relaxed"+		<> help "skip size check"+		)+	<*> switch+		( long "raw"+		<> help "disable special handling for torrents, youtube-dl, etc"+		)+	<*> if withfileoption+		then optional (strOption+			( long "file" <> metavar paramFile+			<> help "specify what file the url is added to"+			))+		else pure Nothing  seek :: AddUrlOptions -> CommandSeek seek o = allowConcurrentOutput $ do@@ -97,7 +101,7 @@   where 	go (o', u) = do 		r <- Remote.claimingUrl u-		if Remote.uuid r == webUUID || rawOption o'+		if Remote.uuid r == webUUID || rawOption (downloadOptions o') 			then void $ commandAction $ startWeb o' u 			else checkUrl r o' u @@ -107,13 +111,13 @@ 		let (u, f) = separate (== ' ') s 		in if null u || null f 			then Left ("parsed empty url or filename in input: " ++ s)-			else Right (o { fileOption = Just f }, u)+			else Right (o { downloadOptions = (downloadOptions o) { fileOption = Just f } }, u) 	| otherwise = Right (o, s)  checkUrl :: Remote -> AddUrlOptions -> URLString -> Annex () checkUrl r o u = do 	pathmax <- liftIO $ fileNameLengthLimit "."-	let deffile = fromMaybe (urlString2file u (pathdepthOption o) pathmax) (fileOption o)+	let deffile = fromMaybe (urlString2file u (pathdepthOption o) pathmax) (fileOption (downloadOptions o)) 	go deffile =<< maybe 		(error $ "unable to checkUrl of " ++ Remote.name r) 		(tryNonAsync . flip id u)@@ -121,50 +125,50 @@   where  	go _ (Left e) = void $ commandAction $ do-		showStart "addurl" u+		showStart' "addurl" (Just u) 		warning (show e) 		next $ next $ return False 	go deffile (Right (UrlContents sz mf)) = do-		let f = adjustFile o (fromMaybe (maybe deffile fromSafeFilePath mf) (fileOption o))-		void $ commandAction $-			startRemote r (relaxedOption o) f u sz+		let f = adjustFile o (fromMaybe (maybe deffile fromSafeFilePath mf) (fileOption (downloadOptions o)))+		void $ commandAction $ startRemote r o f u sz 	go deffile (Right (UrlMulti l))-		| isNothing (fileOption o) =+		| isNothing (fileOption (downloadOptions o)) = 			forM_ l $ \(u', sz, f) -> do 				let f' = adjustFile o (deffile </> fromSafeFilePath f) 				void $ commandAction $-					startRemote r (relaxedOption o) f' u' sz+					startRemote r o f' u' sz 		| otherwise = giveup $ unwords 			[ "That url contains multiple files according to the" 			, Remote.name r 			, " remote; cannot add it to a single file." 			] -startRemote :: Remote -> Bool -> FilePath -> URLString -> Maybe Integer -> CommandStart-startRemote r relaxed file uri sz = do+startRemote :: Remote -> AddUrlOptions -> FilePath -> URLString -> Maybe Integer -> CommandStart+startRemote r o file uri sz = do 	pathmax <- liftIO $ fileNameLengthLimit "." 	let file' = joinPath $ map (truncateFilePath pathmax) $ splitDirectories file-	showStart "addurl" file'+	showStart' "addurl" (Just uri) 	showNote $ "from " ++ Remote.name r -	next $ performRemote r relaxed uri file' sz+	showDestinationFile file'+	next $ performRemote r o uri file' sz -performRemote :: Remote -> Bool -> URLString -> FilePath -> Maybe Integer -> CommandPerform-performRemote r relaxed uri file sz = ifAnnexed file adduri geturi+performRemote :: Remote -> AddUrlOptions -> URLString -> FilePath -> Maybe Integer -> CommandPerform+performRemote r o uri file sz = ifAnnexed file adduri geturi   where 	loguri = setDownloader uri OtherDownloader-	adduri = addUrlChecked relaxed loguri (Remote.uuid r) checkexistssize+	adduri = addUrlChecked o loguri file (Remote.uuid r) checkexistssize 	checkexistssize key = return $ case sz of-		Nothing -> (True, True)-		Just n -> (True, n == fromMaybe n (keySize key))-	geturi = next $ isJust <$> downloadRemoteFile r relaxed uri file sz+		Nothing -> (True, True, loguri)+		Just n -> (True, n == fromMaybe n (keySize key), loguri)+	geturi = next $ isJust <$> downloadRemoteFile r (downloadOptions o) uri file sz -downloadRemoteFile :: Remote -> Bool -> URLString -> FilePath -> Maybe Integer -> Annex (Maybe Key)-downloadRemoteFile r relaxed uri file sz = checkCanAdd file $ do+downloadRemoteFile :: Remote -> DownloadOptions -> URLString -> FilePath -> Maybe Integer -> Annex (Maybe Key)+downloadRemoteFile r o uri file sz = checkCanAdd file $ do 	let urlkey = Backend.URL.fromUrl uri sz 	liftIO $ createDirectoryIfMissing True (parentDir file)-	ifM (Annex.getState Annex.fast <||> pure relaxed)+	ifM (Annex.getState Annex.fast <||> pure (relaxedOption o)) 		( do-			cleanup (Remote.uuid r) loguri file urlkey Nothing+			addWorkTree (Remote.uuid r) loguri file urlkey Nothing 			return (Just urlkey) 		, do 			-- Set temporary url for the urlkey@@ -181,24 +185,18 @@   where 	loguri = setDownloader uri OtherDownloader -startWeb :: AddUrlOptions -> String -> CommandStart-startWeb o s = go $ fromMaybe bad $ parseURI urlstring+startWeb :: AddUrlOptions -> URLString -> CommandStart+startWeb o urlstring = go $ fromMaybe bad $ parseURI urlstring   where-	(urlstring, downloader) = getDownloader s 	bad = fromMaybe (giveup $ "bad url " ++ urlstring) $ 		Url.parseURIRelaxed $ urlstring-	go url = case downloader of-		QuviDownloader -> usequvi-		_ -> ifM (quviSupported urlstring)-			( usequvi-			, regulardownload url-			)-	regulardownload url = do+	go url = do+		showStart' "addurl" (Just urlstring) 		pathmax <- liftIO $ fileNameLengthLimit "."-		urlinfo <- if relaxedOption o+		urlinfo <- if relaxedOption (downloadOptions o) 			then pure Url.assumeUrlExists 			else Url.withUrlOptions (Url.getUrlInfo urlstring)-		file <- adjustFile o <$> case fileOption o of+		file <- adjustFile o <$> case fileOption (downloadOptions o) of 			Just f -> pure f 			Nothing -> case Url.urlSuggestedFile urlinfo of 				Nothing -> pure $ url2file url (pathdepthOption o) pathmax@@ -209,79 +207,31 @@ 						( pure $ url2file url (pathdepthOption o) pathmax 						, pure f 						)-		showStart "addurl" file-		next $ performWeb (relaxedOption o) urlstring file urlinfo-	badquvi = giveup $ "quvi does not know how to download url " ++ urlstring-	usequvi = do-		page <- fromMaybe badquvi-			<$> withQuviOptions Quvi.forceQuery [Quvi.quiet, Quvi.httponly] urlstring-		let link = fromMaybe badquvi $ headMaybe $ Quvi.pageLinks page-		pathmax <- liftIO $ fileNameLengthLimit "."-		let file = adjustFile o $ flip fromMaybe (fileOption o) $-			truncateFilePath pathmax $ sanitizeFilePath $-				Quvi.pageTitle page ++ "." ++ fromMaybe "m" (Quvi.linkSuffix link)-		showStart "addurl" file-		next $ performQuvi (relaxedOption o) urlstring (Quvi.linkUrl link) file--performWeb :: Bool -> URLString -> FilePath -> Url.UrlInfo -> CommandPerform-performWeb relaxed url file urlinfo = ifAnnexed file addurl geturl-  where-	geturl = next $ isJust <$> addUrlFile relaxed url urlinfo file-	addurl = addUrlChecked relaxed url webUUID $ \k -> return $-		(Url.urlExists urlinfo, Url.urlSize urlinfo == keySize k)+		next $ performWeb o urlstring file urlinfo -performQuvi :: Bool -> URLString -> URLString -> FilePath -> CommandPerform-performQuvi relaxed pageurl videourl file = ifAnnexed file addurl geturl+performWeb :: AddUrlOptions -> URLString -> FilePath -> Url.UrlInfo -> CommandPerform+performWeb o url file urlinfo = ifAnnexed file addurl geturl   where-	quviurl = setDownloader pageurl QuviDownloader-	addurl key = next $ do-		cleanup webUUID quviurl file key Nothing-		return True-	geturl = next $ isJust <$> addUrlFileQuvi relaxed quviurl videourl file+	geturl = next $ isJust <$> addUrlFile (downloadOptions o) url urlinfo file+	addurl = addUrlChecked o url file webUUID $ \k -> +		ifM (pure (not (rawOption (downloadOptions o))) <&&> youtubeDlSupported url)+			( return (True, True, setDownloader url YoutubeDownloader)+			, return (Url.urlExists urlinfo, Url.urlSize urlinfo == keySize k, url)+			) -addUrlFileQuvi :: Bool -> URLString -> URLString -> FilePath -> Annex (Maybe Key)-addUrlFileQuvi relaxed quviurl videourl file = checkCanAdd file $ do-	let key = Backend.URL.fromUrl quviurl Nothing-	ifM (pure relaxed <||> Annex.getState Annex.fast)+{- Check that the url exists, and has the same size as the key,+ - and add it as an url to the key. -}+addUrlChecked :: AddUrlOptions -> URLString -> FilePath -> UUID -> (Key -> Annex (Bool, Bool, URLString)) -> Key -> CommandPerform+addUrlChecked o url file u checkexistssize key =+	ifM ((elem url <$> getUrls key) <&&> (elem u <$> loggedLocations key)) 		( do-			cleanup webUUID quviurl file key Nothing-			return (Just key)-		, do-			{- Get the size, and use that to check-			 - disk space. However, the size info is not-			 - retained, because the size of a video stream-			 - might change and we want to be able to download-			 - it later. -}-			urlinfo <- Url.withUrlOptions (Url.getUrlInfo videourl)-			let sizedkey = addSizeUrlKey urlinfo key-			checkDiskSpaceToGet sizedkey Nothing $ do-				tmp <- fromRepo $ gitAnnexTmpObjectLocation key-				showOutput-				ok <- Transfer.notifyTransfer Transfer.Download afile $-					Transfer.download webUUID key afile Transfer.forwardRetry $ \p -> do-						liftIO $ createDirectoryIfMissing True (parentDir tmp)-						downloadUrl key p [videourl] tmp-				if ok-					then do-						cleanup webUUID quviurl file key (Just tmp)-						return (Just key)-					else return Nothing-		)-  where-	afile = AssociatedFile (Just file)--addUrlChecked :: Bool -> URLString -> UUID -> (Key -> Annex (Bool, Bool)) -> Key -> CommandPerform-addUrlChecked relaxed url u checkexistssize key-	| relaxed = do-		setUrlPresent u key url-		next $ return True-	| otherwise = ifM ((elem url <$> getUrls key) <&&> (elem u <$> loggedLocations key))-		( next $ return True -- nothing to do+			showDestinationFile file+			next $ return True 		, do-			(exists, samesize) <- checkexistssize key-			if exists && samesize+			(exists, samesize, url') <- checkexistssize key+			if exists && (samesize || relaxedOption (downloadOptions o)) 				then do-					setUrlPresent u key url+					setUrlPresent u key url' 					next $ return True 				else do 					warning $ "while adding a new url to an already annexed file, " ++ if exists@@ -290,64 +240,120 @@ 					stop 		) -addUrlFile :: Bool -> URLString -> Url.UrlInfo -> FilePath -> Annex (Maybe Key)-addUrlFile relaxed url urlinfo file = checkCanAdd file $ do-	liftIO $ createDirectoryIfMissing True (parentDir file)-	ifM (Annex.getState Annex.fast <||> pure relaxed)-		( nodownload url urlinfo file-		, downloadWeb url urlinfo file+{- Downloads an url (except in fast or relaxed mode) and adds it to the+ - repository, normally at the specified FilePath. + - But, if youtube-dl supports the url, it will be written to a+ - different file, based on the title of the media. Unless the user+ - specified fileOption, which then forces using the FilePath.+ -}+addUrlFile :: DownloadOptions -> URLString -> Url.UrlInfo -> FilePath -> Annex (Maybe Key)+addUrlFile o url urlinfo file =+	ifM (Annex.getState Annex.fast <||> pure (relaxedOption o))+		( nodownloadWeb o url urlinfo file+		, downloadWeb o url urlinfo file 		) -downloadWeb :: URLString -> Url.UrlInfo -> FilePath -> Annex (Maybe Key)-downloadWeb url urlinfo file = do-	let dummykey = addSizeUrlKey urlinfo $ Backend.URL.fromUrl url Nothing-	let downloader f p = do+downloadWeb :: DownloadOptions -> URLString -> Url.UrlInfo -> FilePath -> Annex (Maybe Key)+downloadWeb o url urlinfo file =+	go =<< downloadWith' downloader urlkey webUUID url (AssociatedFile (Just file))+  where+	urlkey = addSizeUrlKey urlinfo $ Backend.URL.fromUrl url Nothing+	downloader f p = do 		showOutput-		downloadUrl dummykey p [url] f-	showAction $ "downloading " ++ url ++ " "-	downloadWith downloader dummykey webUUID url file+		downloadUrl urlkey p [url] f+	go Nothing = return Nothing+	-- If we downloaded a html file, try to use youtube-dl to+	-- extract embedded media.+	go (Just tmp) = ifM (pure (not (rawOption o)) <&&> liftIO (isHtml <$> readFile tmp))+		( tryyoutubedl tmp+		, normalfinish tmp+		)+	normalfinish tmp = checkCanAdd file $ do+		showDestinationFile file+		liftIO $ createDirectoryIfMissing True (parentDir file)+		finishDownloadWith tmp webUUID url file+	tryyoutubedl tmp = withTmpWorkDir mediakey $ \workdir ->+		Transfer.notifyTransfer Transfer.Download url $+			Transfer.download webUUID mediakey (AssociatedFile Nothing) Transfer.noRetry $ \_p ->+				youtubeDl url workdir >>= \case+					Right (Just mediafile) -> do+						pruneTmpWorkDirBefore tmp (liftIO . nukeFile)+						let dest = if isJust (fileOption o)+							then file+							else takeFileName mediafile+						checkCanAdd dest $ do+							showDestinationFile dest+							addWorkTree webUUID mediaurl dest mediakey (Just mediafile)+							return $ Just mediakey+					Right Nothing -> normalfinish tmp+					Left msg -> do+						warning msg+						return Nothing+	  where+		mediaurl = setDownloader url YoutubeDownloader+		mediakey = Backend.URL.fromUrl mediaurl Nothing +showDestinationFile :: FilePath -> Annex ()+showDestinationFile file = do+	showNote ("to " ++ file)+	maybeShowJSON $ JSONChunk [("file", file)]+ {- The Key should be a dummy key, based on the URL, which is used  - for this download, before we can examine the file and find its real key.  - For resuming downloads to work, the dummy key for a given url should be- - stable. -}+ - stable. For disk space checking to work, the dummy key should have+ - the size of the url already set.+ -+ - Downloads the url, sets up the worktree file, and returns the+ - real key.+ -} downloadWith :: (FilePath -> MeterUpdate -> Annex Bool) -> Key -> UUID -> URLString -> FilePath -> Annex (Maybe Key) downloadWith downloader dummykey u url file =-	checkDiskSpaceToGet dummykey Nothing $ do-		tmp <- fromRepo $ gitAnnexTmpObjectLocation dummykey-		ifM (runtransfer tmp)-			( do-				backend <- chooseBackend file-				let source = KeySource-					{ keyFilename = file-					, contentLocation = tmp-					, inodeCache = Nothing-					}-				k <- genKey source backend-				case k of-					Nothing -> return Nothing-					Just (key, _) -> do-						cleanup u url file key (Just tmp)-						return (Just key)-			, return Nothing-			)+	go =<< downloadWith' downloader dummykey u url afile   where-	runtransfer tmp =  Transfer.notifyTransfer Transfer.Download afile $-		Transfer.download u dummykey afile Transfer.forwardRetry $ \p -> do-			liftIO $ createDirectoryIfMissing True (parentDir tmp)-			downloader tmp p 	afile = AssociatedFile (Just file)+	go Nothing = return Nothing+	go (Just tmp) = finishDownloadWith tmp u url file +{- Like downloadWith, but leaves the dummy key content in+ - the returned location. -}+downloadWith' :: (FilePath -> MeterUpdate -> Annex Bool) -> Key -> UUID -> URLString -> AssociatedFile -> Annex (Maybe FilePath)+downloadWith' downloader dummykey u url afile =+	checkDiskSpaceToGet dummykey Nothing $ do+		tmp <- fromRepo $ gitAnnexTmpObjectLocation dummykey+		ok <- Transfer.notifyTransfer Transfer.Download url $+			Transfer.download u dummykey afile Transfer.forwardRetry $ \p -> do+				liftIO $ createDirectoryIfMissing True (parentDir tmp)+				downloader tmp p+		if ok+			then return (Just tmp)+			else return Nothing++finishDownloadWith :: FilePath -> UUID -> URLString -> FilePath -> Annex (Maybe Key)+finishDownloadWith tmp u url file = do+	backend <- chooseBackend file+	let source = KeySource+		{ keyFilename = file+		, contentLocation = tmp+		, inodeCache = Nothing+		}+	genKey source backend >>= \case+		Nothing -> return Nothing+		Just (key, _) -> do+			addWorkTree u url file key (Just tmp)+			return (Just key)+ {- Adds the url size to the Key. -} addSizeUrlKey :: Url.UrlInfo -> Key -> Key addSizeUrlKey urlinfo key = key { keySize = Url.urlSize urlinfo } -cleanup :: UUID -> URLString -> FilePath -> Key -> Maybe FilePath -> Annex ()-cleanup u url file key mtmp = case mtmp of+{- Adds worktree file to the repository. -}+addWorkTree :: UUID -> URLString -> FilePath -> Key -> Maybe FilePath -> Annex ()+addWorkTree u url file key mtmp = case mtmp of 	Nothing -> go 	Just tmp -> do 		-- Move to final location for large file check.-		liftIO $ renameFile tmp file+		pruneTmpWorkDirBefore tmp (\_ -> liftIO $ renameFile tmp file) 		largematcher <- largeFilesMatcher 		large <- checkFileMatcher largematcher file 		if large@@ -366,19 +372,37 @@ 			( do 				when (isJust mtmp) $ 					logStatus key InfoPresent-			, liftIO $ maybe noop nukeFile mtmp+			, maybe noop (\tmp -> pruneTmpWorkDirBefore tmp (liftIO . nukeFile)) mtmp 			) -nodownload :: URLString -> Url.UrlInfo -> FilePath -> Annex (Maybe Key)-nodownload url urlinfo file-	| Url.urlExists urlinfo = do-		let key = Backend.URL.fromUrl url (Url.urlSize urlinfo)-		cleanup webUUID url file key Nothing-		return (Just key)+nodownloadWeb :: DownloadOptions -> URLString -> Url.UrlInfo -> FilePath -> Annex (Maybe Key)+nodownloadWeb o url urlinfo file+	| Url.urlExists urlinfo = if rawOption o+		then nomedia+		else either (const nomedia) usemedia+			=<< youtubeDlFileName url 	| otherwise = do 		warning $ "unable to access url: " ++ url 		return Nothing+  where+	nomedia = do+		let key = Backend.URL.fromUrl url (Url.urlSize urlinfo)+		nodownloadWeb' url key file+	usemedia mediafile = do+		let dest = if isJust (fileOption o)+			then file+			else takeFileName mediafile+		let mediaurl = setDownloader url YoutubeDownloader+		let mediakey = Backend.URL.fromUrl mediaurl Nothing+		nodownloadWeb' mediaurl mediakey dest +nodownloadWeb' :: URLString -> Key -> FilePath -> Annex (Maybe Key)+nodownloadWeb' url key file = checkCanAdd file $ do+	showDestinationFile file+	liftIO $ createDirectoryIfMissing True (parentDir file)+	addWorkTree webUUID url file key Nothing+	return (Just key)+ url2file :: URI -> Maybe Int -> Int -> FilePath url2file url pathdepth pathmax = case pathdepth of 	Nothing -> truncateFilePath pathmax $ sanitizeFilePath fullurl@@ -411,7 +435,7 @@ checkCanAdd :: FilePath -> Annex (Maybe a) -> Annex (Maybe a) checkCanAdd file a = ifM (isJust <$> (liftIO $ catchMaybeIO $ getSymbolicLinkStatus file)) 	( do-		warning $ file ++ " already exists and is not annexed; not overwriting"+		warning $ file ++ " already exists; not overwriting" 		return Nothing 	, ifM ((not <$> Annex.getState Annex.force) <&&> checkIgnored file) 		( do
Command/Adjust.hs view
@@ -38,5 +38,5 @@ start :: Adjustment -> CommandStart start adj = do 	checkVersionSupported-	showStart "adjust" ""+	showStart' "adjust" Nothing 	next $ next $ enterAdjustedBranch adj
Command/CalcKey.hs view
@@ -19,10 +19,8 @@ 		(batchable run (pure ()))  run :: () -> String -> Annex Bool-run _ file = do-	mkb <- genKey (KeySource file file Nothing) Nothing-	case mkb of-		Just (k, _) -> do-			liftIO $ putStrLn $ key2file k-			return True-		Nothing -> return False+run _ file = genKey (KeySource file file Nothing) Nothing >>= \case+	Just (k, _) -> do+		liftIO $ putStrLn $ key2file k+		return True+	Nothing -> return False
Command/CheckPresentKey.hs view
@@ -52,12 +52,10 @@ 	k = toKey ks 	go Nothing [] = return NotPresent 	go (Just e) [] = return $ CheckFailure e-	go olderr (r:rs) = do-		v <- Remote.hasKey r k-		case v of-			Right True -> return Present-			Right False -> go olderr rs-			Left e -> go (Just e) rs+	go olderr (r:rs) = Remote.hasKey r k >>= \case+		Right True -> return Present+		Right False -> go olderr rs+		Left e -> go (Just e) rs  exitResult :: Result -> Annex a exitResult Present = liftIO exitSuccess
Command/Config.hs view
@@ -50,21 +50,20 @@ seek :: Action -> CommandSeek seek (SetConfig name val) = commandAction $ do 	allowMessages-	showStart name val+	showStart' name (Just val) 	next $ next $ do 		setGlobalConfig name val 		setConfig (ConfigKey name) val 		return True seek (UnsetConfig name) = commandAction $ do 	allowMessages-	showStart name "unset"+	showStart' name (Just "unset") 	next $ next $ do 		unsetGlobalConfig name 		unsetConfig (ConfigKey name) 		return True-seek (GetConfig name) = commandAction $ do-	mv <- getGlobalConfig name-	case mv of+seek (GetConfig name) = commandAction $+	getGlobalConfig name >>= \case 		Nothing -> stop 		Just v -> do 			liftIO $ putStrLn v
Command/ConfigList.hs view
@@ -45,7 +45,7 @@ 		else ifM (Annex.Branch.hasSibling <||> (isJust <$> Fields.getField Fields.autoInit)) 			( do 				liftIO checkNotReadOnly-				initialize Nothing Nothing+				initialize (AutoInit True) Nothing Nothing 				getUUID 			, return NoUUID 			)
Command/Dead.hs view
@@ -33,9 +33,8 @@  startKey :: Key -> CommandStart startKey key = do-	showStart "dead" (key2file key)-	ls <- keyLocations key-	case ls of+	showStart' "dead" (Just $ key2file key)+	keyLocations key >>= \case 		[] -> next $ performKey key 		_ -> giveup "This key is still known to be present in some locations; not marking as dead." 		
Command/Describe.hs view
@@ -22,7 +22,7 @@  start :: [String] -> CommandStart start (name:description) = do-	showStart "describe" name+	showStart' "describe" (Just name) 	u <- Remote.nameToUUID name 	next $ perform u $ unwords description start _ = giveup "Specify a repository and a description."	
Command/Direct.hs view
@@ -31,7 +31,7 @@  perform :: CommandPerform perform = do-	showStart "commit" ""+	showStart' "commit" Nothing 	showOutput 	_ <- inRepo $ Git.Branch.commitCommand Git.Branch.ManualCommit 		[ Param "-a"@@ -47,13 +47,11 @@ 	next cleanup   where 	go = whenAnnexed $ \f k -> do-		r <- toDirectGen k f-		case r of+		toDirectGen k f >>= \case 			Nothing -> noop 			Just a -> do 				showStart "direct" f-				r' <- tryNonAsync a-				case r' of+				tryNonAsync a >>= \case 					Left e -> warnlocked e 					Right _ -> showEndOk 		return Nothing@@ -65,6 +63,6 @@  cleanup :: CommandCleanup cleanup = do-	showStart "direct" ""+	showStart' "direct" Nothing 	setDirect True 	return True
Command/Drop.hs view
@@ -89,12 +89,12 @@  startLocal :: AssociatedFile -> ActionItem -> NumCopies -> Key -> [VerifiedCopy] -> CommandStart startLocal afile ai numcopies key preverified = stopUnless (inAnnex key) $ do-	showStart' "drop" key ai+	showStartKey "drop" key ai 	next $ performLocal key afile numcopies preverified  startRemote :: AssociatedFile -> ActionItem -> NumCopies -> Key -> Remote -> CommandStart startRemote afile ai numcopies key remote = do-	showStart' ("drop " ++ Remote.name remote) key ai+	showStartKey ("drop " ++ Remote.name remote) key ai 	next $ performRemote key afile numcopies remote  performLocal :: Key -> AssociatedFile -> NumCopies -> [VerifiedCopy] -> CommandPerform
Command/DropKey.hs view
@@ -42,7 +42,7 @@  start :: Key -> CommandStart start key = do-	showStart' "dropkey" key (mkActionItem key)+	showStartKey "dropkey" key (mkActionItem key) 	next $ perform key  perform :: Key -> CommandPerform
Command/DropUnused.hs view
@@ -55,5 +55,5 @@ performOther :: (Key -> Git.Repo -> FilePath) -> Key -> CommandPerform performOther filespec key = do 	f <- fromRepo $ filespec key-	liftIO $ nukeFile f+	pruneTmpWorkDirBefore f (liftIO . nukeFile) 	next $ return True
Command/EnableRemote.hs view
@@ -55,7 +55,7 @@ startNormalRemote :: Git.RemoteName -> [String] -> Git.Repo -> CommandStart startNormalRemote name restparams r 	| null restparams = do-		showStart "enableremote" name+		showStart' "enableremote" (Just name) 		next $ next $ do 			setRemoteIgnore r False 			r' <- Remote.Git.configRead False r@@ -68,8 +68,7 @@ startSpecialRemote name config Nothing = do 	m <- Annex.SpecialRemote.specialRemoteMap 	confm <- Logs.Remote.readRemoteLog-	v <- Remote.nameToUUID' name-	case v of+	Remote.nameToUUID' name >>= \case 		Right u | u `M.member` m -> 			startSpecialRemote name config $ 				Just (u, fromMaybe M.empty (M.lookup u confm))@@ -77,7 +76,7 @@ startSpecialRemote name config (Just (u, c)) = do 	let fullconfig = config `M.union` c	 	t <- either giveup return (Annex.SpecialRemote.findType fullconfig)-	showStart "enableremote" name+	showStart' "enableremote" (Just name) 	gc <- maybe (liftIO dummyRemoteGitConfig)  		(return . Remote.gitconfig) 		=<< Remote.byUUID u@@ -91,8 +90,7 @@ cleanupSpecialRemote :: UUID -> R.RemoteConfig -> CommandCleanup cleanupSpecialRemote u c = do 	Logs.Remote.configSet u c-	mr <- Remote.byUUID u-	case mr of+	Remote.byUUID u >>= \case 		Nothing -> noop 		Just r -> setRemoteIgnore (R.repo r) False 	return True
Command/EnableTor.hs view
@@ -51,7 +51,7 @@ 			Nothing -> giveup "Need user-id parameter." 			Just userid -> go uuid userid 		else do-			showStart "enable-tor" ""+			showStart' "enable-tor" Nothing 			gitannex <- liftIO readProgramFile 			let ps = [Param (cmdname cmd), Param (show curruserid)] 			sucommand <- liftIO $ mkSuCommand gitannex ps@@ -91,8 +91,7 @@ 		g <- Annex.gitRepo 		-- Connect but don't bother trying to auth, 		-- we just want to know if the tor circuit works.-		cv <- liftIO $ tryNonAsync $ connectPeer g addr-		case cv of+		liftIO (tryNonAsync $ connectPeer g addr) >>= \case 			Left e -> do 				warning $ "Unable to connect to hidden service. It may not yet have propigated to the Tor network. (" ++ show e ++ ") Will retry.." 				liftIO $ threadDelaySeconds (Seconds 2)
Command/Expire.hs view
@@ -59,12 +59,12 @@ start (Expire expire) noact actlog descs u = 	case lastact of 		Just ent | notexpired ent -> checktrust (== DeadTrusted) $ do-			showStart "unexpire" desc+			showStart' "unexpire" (Just desc) 			showNote =<< whenactive 			unless noact $ 				trustSet u SemiTrusted 		_ -> checktrust (/= DeadTrusted) $ do-			showStart "expire" desc+			showStart' "expire" (Just desc) 			showNote =<< whenactive 			unless noact $ 				trustSet u DeadTrusted
Command/Fix.hs view
@@ -82,8 +82,7 @@ makeHardLink file key = do 	replaceFile file $ \tmp -> do 		mode <- liftIO $ catchMaybeIO $ fileMode <$> getFileStatus file-		r <- linkFromAnnex key tmp mode-		case r of+		linkFromAnnex key tmp mode >>= \case 			LinkAnnexFailed -> error "unable to make hard link" 			_ -> noop 	next $ return True
Command/Forget.hs view
@@ -34,7 +34,7 @@  start :: ForgetOptions -> CommandStart start o = do-	showStart "forget" "git-annex"+	showStart' "forget" (Just "git-annex") 	c <- liftIO currentVectorClock 	let basets = addTransition c ForgetGitHistory noTransitions 	let ts = if dropDead o
Command/FromKey.hs view
@@ -41,7 +41,7 @@  startMass :: CommandStart startMass = do-	showStart "fromkey" "stdin"+	showStart' "fromkey" (Just "stdin") 	next massAdd  massAdd :: CommandPerform
Command/Fsck.hs view
@@ -103,15 +103,13 @@ 		earlyWarning "Warning: Fscking a repository that is currently marked as dead."  start :: Maybe Remote -> Incremental -> FilePath -> Key -> CommandStart-start from inc file key = do-	v <- Backend.getBackend file key-	case v of-		Nothing -> stop-		Just backend -> do-			numcopies <- getFileNumCopies file-			case from of-				Nothing -> go $ perform key file backend numcopies-				Just r -> go $ performRemote key afile backend numcopies r+start from inc file key = Backend.getBackend file key >>= \case+	Nothing -> stop+	Just backend -> do+		numcopies <- getFileNumCopies file+		case from of+			Nothing -> go $ perform key file backend numcopies+			Just r -> go $ performRemote key afile backend numcopies r   where 	go = runFsck inc (mkActionItem afile) key 	afile = AssociatedFile (Just file)@@ -142,9 +140,8 @@ 	dispatch (Left err) = do 		showNote err 		return False-	dispatch (Right True) = withtmp $ \tmpfile -> do-		r <- getfile tmpfile-		case r of+	dispatch (Right True) = withtmp $ \tmpfile ->+		getfile tmpfile >>= \case 			Nothing -> go True Nothing 			Just True -> go True (Just tmpfile) 			Just False -> do@@ -536,7 +533,7 @@ runFsck :: Incremental -> ActionItem -> Key -> Annex Bool -> CommandStart runFsck inc ai key a = ifM (needFsck inc key) 	( do-		showStart' "fsck" key ai+		showStartKey "fsck" key ai 		next $ do 			ok <- a 			when ok $
Command/Get.hs view
@@ -71,7 +71,7 @@ 					go $ Command.Move.fromPerform src False key afile   where 	go a = do-		showStart' "get" key ai+		showStartKey "get" key ai 		next a  perform :: Key -> AssociatedFile -> CommandPerform
Command/Group.hs view
@@ -24,7 +24,7 @@ start :: [String] -> CommandStart start (name:g:[]) = do 	allowMessages-	showStart "group" name+	showStart' "group" (Just name) 	u <- Remote.nameToUUID name 	next $ setGroup u g start (name:[]) = do
Command/GroupWanted.hs view
@@ -24,6 +24,6 @@ start (g:[]) = next $ performGet groupPreferredContentMapRaw g start (g:expr:[]) = do 	allowMessages-	showStart "groupwanted" g+	showStart' "groupwanted" (Just g) 	next $ performSet groupPreferredContentSet expr g start _ = giveup "Specify a group."
Command/ImportFeed.hs view
@@ -1,6 +1,6 @@ {- git-annex command  -- - Copyright 2013 Joey Hess <id@joeyh.name>+ - Copyright 2013-2017 Joey Hess <id@joeyh.name>  -  - Licensed under the GNU GPL version 3 or higher.  -}@@ -32,16 +32,16 @@ import Logs.Web import qualified Utility.Format import Utility.Tmp-import Command.AddUrl (addUrlFile, downloadRemoteFile, parseRelaxedOption, parseRawOption)+import Command.AddUrl (addUrlFile, downloadRemoteFile, parseDownloadOptions, DownloadOptions(..)) import Annex.Perms import Annex.UUID import Backend.URL (fromUrl)-import Annex.Quvi-import qualified Utility.Quvi as Quvi-import Command.AddUrl (addUrlFileQuvi)+import Annex.Content+import Annex.YoutubeDl import Types.MetaData import Logs.MetaData import Annex.MetaData+import Command.AddUrl (addWorkTree)  cmd :: Command cmd = notBareRepo $@@ -51,8 +51,7 @@ data ImportFeedOptions = ImportFeedOptions 	{ feedUrls :: CmdParams 	, templateOption :: Maybe String-	, relaxedOption :: Bool-	, rawOption :: Bool+	, downloadOptions :: DownloadOptions 	}  optParser :: CmdParamsDesc -> Parser ImportFeedOptions@@ -62,8 +61,7 @@ 		( long "template" <> metavar paramFormat 		<> help "template for filenames" 		))-	<*> parseRelaxedOption-	<*> parseRawOption+	<*> parseDownloadOptions False  seek :: ImportFeedOptions -> CommandSeek seek o = do@@ -72,7 +70,7 @@  start :: ImportFeedOptions -> Cache -> URLString -> CommandStart start opts cache url = do-	showStart "importfeed" url+	showStart' "importfeed" (Just url) 	next $ perform opts cache url  perform :: ImportFeedOptions -> Cache -> URLString -> CommandPerform@@ -101,7 +99,7 @@ 	, location :: DownloadLocation 	} -data DownloadLocation = Enclosure URLString | QuviLink URLString+data DownloadLocation = Enclosure URLString | MediaLink URLString  type ItemId = String @@ -141,14 +139,10 @@ 		Just (enclosureurl, _, _) -> return $  			Just $ ToDownload f u i $ Enclosure $  				fromFeed enclosureurl-		Nothing -> mkquvi f i-	mkquvi f i = case getItemLink i of-		Just link -> ifM (quviSupported $ fromFeed link)-			( return $ Just $ ToDownload f u i $ QuviLink $ -				fromFeed link-			, return Nothing-			)-		Nothing -> return Nothing+		Nothing -> case getItemLink i of+			Just link -> return $ Just $ ToDownload f u i $ +				MediaLink $ fromFeed link+			Nothing -> return Nothing  {- Feeds change, so a feed download cannot be resumed. -} downloadFeed :: URLString -> Annex (Maybe Feed)@@ -169,12 +163,19 @@ 	Enclosure url -> checkknown url $ 		rundownload url (takeWhile (/= '?') $ takeExtension url) $ \f -> do 			r <- Remote.claimingUrl url-			if Remote.uuid r == webUUID || rawOption opts+			if Remote.uuid r == webUUID || rawOption (downloadOptions opts) 				then do-					urlinfo <- if relaxedOption opts+					urlinfo <- if relaxedOption (downloadOptions opts) 						then pure Url.assumeUrlExists 						else Url.withUrlOptions (Url.getUrlInfo url)-					maybeToList <$> addUrlFile (relaxedOption opts) url urlinfo f+					let dlopts = (downloadOptions opts)+						-- force using the filename+						-- chosen here+						{ fileOption = Just f+						-- don't use youtube-dl+						, rawOption = True+						}+					maybeToList <$> addUrlFile dlopts url urlinfo f 				else do 					res <- tryNonAsync $ maybe 						(error $ "unable to checkUrl of " ++ Remote.name r)@@ -184,27 +185,26 @@ 						Left _ -> return [] 						Right (UrlContents sz _) -> 							maybeToList <$>-								downloadRemoteFile r (relaxedOption opts) url f sz+								downloadRemoteFile r (downloadOptions opts) url f sz 						Right (UrlMulti l) -> do 							kl <- forM l $ \(url', sz, subf) ->-								downloadRemoteFile r (relaxedOption opts) url' (f </> fromSafeFilePath subf) sz+								downloadRemoteFile r (downloadOptions opts) url' (f </> fromSafeFilePath subf) sz 							return $ if all isJust kl 								then catMaybes kl 								else [] 							-	QuviLink pageurl -> do-		let quviurl = setDownloader pageurl QuviDownloader-		checkknown quviurl $ do-			mp <- withQuviOptions Quvi.query [Quvi.quiet, Quvi.httponly] pageurl-			case mp of-				Nothing -> return False-				Just page -> case headMaybe $ Quvi.pageLinks page of-					Nothing -> return False-					Just link -> do-						let videourl = Quvi.linkUrl link-						checkknown videourl $-							rundownload videourl ("." ++ fromMaybe "m" (Quvi.linkSuffix link)) $ \f ->-								maybeToList <$> addUrlFileQuvi (relaxedOption opts) quviurl videourl f+	MediaLink linkurl -> do+		let mediaurl = setDownloader linkurl YoutubeDownloader+		let mediakey = Backend.URL.fromUrl mediaurl Nothing+		-- Old versions of git-annex that used quvi might have+		-- used the quviurl for this, so check i/f it's known+		-- to avoid adding it a second time.+		let quviurl = setDownloader linkurl QuviDownloader+		checkknown mediaurl $ checkknown quviurl $+			ifM (Annex.getState Annex.fast <||> pure (relaxedOption (downloadOptions opts)))+				( addmediafast linkurl mediaurl mediakey+				, downloadmedia linkurl mediaurl mediakey+				)   where 	forced = Annex.getState Annex.force @@ -264,6 +264,42 @@ 		checksameurl k = ifM (elem url <$> getUrls k) 			( return Nothing 			, tryanother+			)+	+	downloadmedia linkurl mediaurl mediakey+		| rawOption (downloadOptions opts) = downloadlink+		| otherwise = do+			r <- withTmpWorkDir mediakey $ \workdir -> do+				dl <- youtubeDl linkurl workdir+				case dl of+					Right (Just mediafile) -> do+						let ext = case takeExtension mediafile of+							[] -> ".m"+							s -> s+						ok <- rundownload linkurl ext $ \f -> do+							addWorkTree webUUID mediaurl f mediakey (Just mediafile)+							return [mediakey]+						return (Just ok)+					-- youtude-dl didn't support it, so+					-- download it as if the link were+					-- an enclosure.+					Right Nothing -> Just <$> downloadlink+					Left msg -> do+						warning msg+						return Nothing+			return (fromMaybe False r)+	  where+		downloadlink = performDownload opts cache todownload+			{ location = Enclosure linkurl }++	addmediafast linkurl mediaurl mediakey =+		ifM (pure (not (rawOption (downloadOptions opts)))+		     <&&> youtubeDlSupported linkurl)+			( rundownload linkurl ".m" $ \f -> do+				addWorkTree webUUID mediaurl f mediakey Nothing+				return [mediakey]+			, performDownload opts cache todownload+				{ location = Enclosure linkurl } 			)  defaultTemplate :: String
Command/Indirect.hs view
@@ -42,7 +42,7 @@  perform :: CommandPerform perform = do-	showStart "commit" ""+	showStart' "commit" Nothing 	whenM stageDirect $ do 		showOutput 		void $ inRepo $ Git.Branch.commitCommand Git.Branch.ManualCommit@@ -100,6 +100,6 @@ 	 cleanup :: CommandCleanup cleanup = do-	showStart "indirect" ""+	showStart' "indirect" Nothing 	showEndOk 	return True
Command/Init.hs view
@@ -40,12 +40,12 @@  start :: InitOptions -> CommandStart start os = do-	showStart "init" (initDesc os)+	showStart' "init" (Just $ initDesc os) 	next $ perform os  perform :: InitOptions -> CommandPerform perform os = do-	initialize +	initialize (AutoInit False) 		(if null (initDesc os) then Nothing else Just (initDesc os)) 		(initVersion os) 	Annex.SpecialRemote.autoEnable
Command/InitRemote.hs view
@@ -38,7 +38,7 @@ 				let c = newConfig name 				t <- either giveup return (findType config) -				showStart "initremote" name+				showStart' "initremote" (Just name) 				next $ perform t name $ M.union config c 			) 	)
Command/LookupKey.hs view
@@ -1,6 +1,6 @@ {- git-annex command  -- - Copyright 2013 Joey Hess <id@joeyh.name>+ - Copyright 2013-2017 Joey Hess <id@joeyh.name>  -  - Licensed under the GNU GPL version 3 or higher.  -}@@ -9,6 +9,7 @@  import Command import Annex.CatFile+import qualified Git.LsFiles  cmd :: Command cmd = notBareRepo $ noCommit $ noMessages $@@ -18,10 +19,21 @@ 		(batchable run (pure ()))  run :: () -> String -> Annex Bool-run _ file = do-	mk <- catKeyFile file-	case mk of+run _ file = seekSingleGitFile file >>= \case+	Nothing -> return False+	Just file' -> catKeyFile file' >>= \case 		Just k  -> do 			liftIO $ putStrLn $ key2file k 			return True 		Nothing -> return False++-- To support absolute filenames, pass through git ls-files.+-- But, this plumbing command does not recurse through directories.+seekSingleGitFile :: FilePath -> Annex (Maybe FilePath)+seekSingleGitFile file = do+	(l, cleanup) <- inRepo (Git.LsFiles.inRepo [file])+	r <- case l of+		(f:[]) | takeFileName f == takeFileName file -> return (Just f)+		_ -> return Nothing+	void $ liftIO cleanup+	return r
Command/Map.hs view
@@ -189,7 +189,7 @@ {- reads the config of a remote, with progress display -} scan :: Git.Repo -> Annex Git.Repo scan r = do-	showStart "map" $ Git.repoDescribe r+	showStart' "map" (Just $ Git.repoDescribe r) 	v <- tryScan r 	case v of 		Just r' -> do
Command/Merge.hs view
@@ -23,7 +23,7 @@  mergeBranch :: CommandStart mergeBranch = do-	showStart "merge" "git-annex"+	showStart' "merge" (Just "git-annex") 	next $ do 		Annex.Branch.update 		-- commit explicitly, in case no remote branches were merged
Command/MetaData.hs view
@@ -100,7 +100,7 @@ 			putStrLn . fromMetaValue 		stop 	_ -> do-		showStart' "metadata" k ai+		showStartKey "metadata" k ai 		next $ perform c o k  perform :: VectorClock -> MetaDataOptions -> Key -> CommandPerform@@ -164,7 +164,7 @@ 	Right k -> go k (mkActionItem k)   where 	go k ai = do-		showStart' "metadata" k ai+		showStartKey "metadata" k ai 		let o = MetaDataOptions 			{ forFiles = [] 			, getSet = if MetaData m == emptyMetaData
Command/Move.hs view
@@ -87,7 +87,7 @@ 				toHereStart move afile key ai  showMoveAction :: Bool -> Key -> ActionItem -> Annex ()-showMoveAction move = showStart' (if move then "move" else "copy")+showMoveAction move = showStartKey (if move then "move" else "copy")  {- Moves (or copies) the content of an annexed file to a remote.  -
Command/Multicast.hs view
@@ -78,7 +78,7 @@  genAddress :: CommandStart genAddress = do-	showStart "gen-address" ""+	showStart' "gen-address" Nothing 	k <- uftpKey 	(s, ok) <- case k of 		KeyContainer s -> liftIO $ genkey (Param s)@@ -130,7 +130,7 @@ 	whenM isDirect $ 		giveup "Sorry, multicast send cannot be done from a direct mode repository." 	-	showStart "generating file list" ""+	showStart' "generating file list" Nothing 	fs' <- seekHelper LsFiles.inRepo =<< workTreeItems fs 	matcher <- Limit.getMatcher 	let addlist f o = whenM (matcher $ MatchingFile $ FileInfo f f) $@@ -143,7 +143,7 @@ 	liftIO $ hClose h 	showEndOk -	showStart "sending files" ""+	showStart' "sending files" Nothing 	showOutput 	serverkey <- uftpKey 	u <- getUUID@@ -169,7 +169,7 @@  receive :: [CommandParam] -> CommandStart receive ups = do-	showStart "receiving multicast files" ""+	showStart' "receiving multicast files" Nothing 	showNote "Will continue to run until stopped by ctrl-c" 	 	showOutput
Command/NumCopies.hs view
@@ -48,7 +48,7 @@ startSet :: Int -> CommandStart startSet n = do 	allowMessages-	showStart "numcopies" (show n)+	showStart' "numcopies" (Just $ show n) 	next $ next $ do 		setGlobalNumCopies $ NumCopies n 		return True
Command/P2P.hs view
@@ -97,7 +97,7 @@ -- Address is read from stdin, to avoid leaking it in shell history. linkRemote :: RemoteName -> CommandStart linkRemote remotename = do-	showStart "p2p link" remotename+	showStart' "p2p link" (Just remotename) 	next $ next promptaddr   where 	promptaddr = do@@ -123,7 +123,7 @@ startPairing :: RemoteName -> [P2PAddress] -> CommandStart startPairing _ [] = giveup "No P2P networks are currrently available." startPairing remotename addrs = do-	showStart "p2p pair" remotename+	showStart' "p2p pair" (Just remotename) 	ifM (liftIO Wormhole.isInstalled) 		( next $ performPairing remotename addrs 		, giveup "Magic Wormhole is not installed, and is needed for pairing. Install it from your distribution or from https://github.com/warner/magic-wormhole/"
Command/RegisterUrl.hs view
@@ -27,10 +27,10 @@ start :: [String] -> CommandStart start (keyname:url:[]) = do 	let key = mkKey keyname-	showStart "registerurl" url+	showStart' "registerurl" (Just url) 	next $ perform key url start [] = do-	showStart "registerurl" "stdin"+	showStart' "registerurl" (Just "stdin") 	next massAdd start _ = giveup "specify a key and an url" 
Command/Reinit.hs view
@@ -25,7 +25,7 @@  start :: [String] -> CommandStart start ws = do-	showStart "reinit" s+	showStart' "reinit" (Just s) 	next $ perform s   where 	s = unwords ws@@ -36,6 +36,6 @@ 		then return $ toUUID s 		else Remote.nameToUUID s 	storeUUID u-	initialize' Nothing+	initialize' (AutoInit False) Nothing 	Annex.SpecialRemote.autoEnable 	next $ return True
Command/ResolveMerge.hs view
@@ -23,7 +23,7 @@  start :: CommandStart start = do-	showStart "resolvemerge" ""+	showStart' "resolvemerge" Nothing 	us <- fromMaybe nobranch <$> inRepo Git.Branch.current 	d <- fromRepo Git.localGitDir 	let merge_head = d </> "MERGE_HEAD"
Command/Schedule.hs view
@@ -28,7 +28,7 @@ 	parse (name:[]) = go name performGet 	parse (name:expr:[]) = go name $ \uuid -> do 		allowMessages-		showStart "schedule" name+		showStart' "schedule" (Just name) 		performSet expr uuid 	parse _ = giveup "Specify a repository." 
Command/SetPresentKey.hs view
@@ -23,7 +23,7 @@  start :: [String] -> CommandStart start (ks:us:vs:[]) = do-	showStart' "setpresentkey" k (mkActionItem k)+	showStartKey "setpresentkey" k (mkActionItem k) 	next $ perform k (toUUID us) s   where 	k = fromMaybe (giveup "bad key") (file2key ks)
Command/Sync.hs view
@@ -299,7 +299,7 @@ commit :: SyncOptions -> CommandStart commit o = stopUnless shouldcommit $ next $ next $ do 	commitmessage <- maybe commitMsg return (messageOption o)-	showStart "commit" ""+	showStart' "commit" Nothing 	Annex.Branch.commit "update" 	ifM isDirect 		( do@@ -342,7 +342,7 @@   where 	go Nothing = stop 	go (Just syncbranch) = do-		showStart "merge" $ Git.Ref.describe syncbranch+		showStart' "merge" (Just $ Git.Ref.describe syncbranch) 		next $ next $ merge currbranch mergeconfig resolvemergeoverride Git.Branch.ManualCommit syncbranch mergeLocal _ _ (Nothing, madj) = do 	b <- inRepo Git.Branch.currentUnsafe@@ -401,7 +401,7 @@  pullRemote :: SyncOptions -> [Git.Merge.MergeConfig] -> Remote -> CurrBranch -> CommandStart pullRemote o mergeconfig remote branch = stopUnless (pure $ pullOption o && wantpull) $ do-	showStart "pull" (Remote.name remote)+	showStart' "pull" (Just (Remote.name remote)) 	next $ do 		showOutput 		stopUnless fetch $@@ -438,7 +438,7 @@ pushRemote :: SyncOptions -> Remote -> CurrBranch -> CommandStart pushRemote _o _remote (Nothing, _) = stop pushRemote o remote (Just branch, _) = stopUnless (pure (pushOption o) <&&> needpush) $ do-	showStart "push" (Remote.name remote)+	showStart' "push" (Just (Remote.name remote)) 	next $ next $ do 		showOutput 		ok <- inRepoWithSshOptionsTo (Remote.repo remote) gc $@@ -651,7 +651,7 @@ 		, return [] 		) 	get have = includeCommandAction $ do-		showStart' "get" k (mkActionItem af)+		showStartKey "get" k (mkActionItem af) 		next $ next $ getKey' k af have  	wantput r@@ -703,7 +703,7 @@ cleanupLocal :: CurrBranch -> CommandStart cleanupLocal (Nothing, _) = stop cleanupLocal (Just currb, _) = do-	showStart "cleanup" "local"+	showStart' "cleanup" (Just "local") 	next $ next $ do 		delbranch $ syncBranch currb 		delbranch $ syncBranch $ Git.Ref.base $ Annex.Branch.name@@ -717,7 +717,7 @@ cleanupRemote :: Remote -> CurrBranch -> CommandStart cleanupRemote _ (Nothing, _) = stop cleanupRemote remote (Just b, _) = do-	showStart "cleanup" (Remote.name remote)+	showStart' "cleanup" (Just (Remote.name remote)) 	next $ next $ 		inRepo $ Git.Command.runBool 			[ Param "push"
Command/TestRemote.hs view
@@ -58,7 +58,7 @@  start :: Int -> RemoteName -> CommandStart start basesz name = do-	showStart "testremote" name+	showStart' "testremote" (Just name) 	fast <- Annex.getState Annex.fast 	r <- either giveup disableExportTree =<< Remote.byName' name 	rs <- catMaybes <$> mapM (adjustChunkSize r) (chunkSizes basesz fast)
Command/Trust.hs view
@@ -27,7 +27,7 @@   where 	start ws = do 		let name = unwords ws-		showStart c name+		showStart' c (Just name) 		u <- Remote.nameToUUID name 		next $ perform u 	perform uuid = do
Command/Ungroup.hs view
@@ -23,7 +23,7 @@  start :: [String] -> CommandStart start (name:g:[]) = do-	showStart "ungroup" name+	showStart' "ungroup" (Just name) 	u <- Remote.nameToUUID name 	next $ perform u g start _ = giveup "Specify a repository and a group."
Command/Unused.hs view
@@ -70,7 +70,7 @@ 		Just "." -> (".", checkUnused refspec) 		Just "here" -> (".", checkUnused refspec) 		Just n -> (n, checkRemoteUnused n refspec)-	showStart "unused" name+	showStart' "unused" (Just name) 	next perform  checkUnused :: RefSpec -> CommandPerform@@ -338,5 +338,5 @@ 		case M.lookup n m of 			Nothing -> search rest 			Just key -> do-				showStart message (show n)+				showStart' message (Just $ show n) 				next $ a key
Command/Upgrade.hs view
@@ -23,8 +23,8 @@  start :: CommandStart start = do-	showStart "upgrade" "."+	showStart' "upgrade" Nothing 	whenM (isNothing <$> getVersion) $ do-		initialize Nothing Nothing+		initialize (AutoInit False) Nothing Nothing 	r <- upgrade False latestVersion 	next $ next $ return r
Command/VAdd.hs view
@@ -23,7 +23,7 @@  start :: [String] -> CommandStart start params = do-	showStart "vadd" ""+	showStart' "vadd" Nothing 	withCurrentView $ \view -> do 		let (view', change) = refineView view $ 			map parseViewParam $ reverse params
Command/VCycle.hs view
@@ -27,7 +27,7 @@   where 	go Nothing = giveup "Not in a view." 	go (Just v) = do-		showStart "vcycle" ""+		showStart' "vcycle" Nothing 		let v' = v { viewComponents = vcycle [] (viewComponents v) } 		if v == v' 			then do
Command/VFilter.hs view
@@ -21,7 +21,7 @@  start :: [String] -> CommandStart start params = do-	showStart "vfilter" ""+	showStart' "vfilter" Nothing 	withCurrentView $ \view -> do 		let view' = filterView view $ 			map parseViewParam $ reverse params
Command/VPop.hs view
@@ -28,7 +28,7 @@   where 	go Nothing = giveup "Not in a view." 	go (Just v) = do-		showStart "vpop" (show num)+		showStart' "vpop" (Just $ show num) 		removeView v 		(oldvs, vs) <- splitAt (num - 1) . filter (sameparentbranch v) 			<$> recentViews
Command/View.hs view
@@ -27,7 +27,7 @@ start :: [String] -> CommandStart start [] = giveup "Specify metadata to include in view" start ps = do-	showStart "view" ""+	showStart' "view" Nothing 	view <- mkView ps 	go view  =<< currentView   where
Command/Wanted.hs view
@@ -35,7 +35,7 @@ 	start (rname:[]) = go rname (performGet getter) 	start (rname:expr:[]) = go rname $ \uuid -> do 		allowMessages-		showStart name rname+		showStart' name (Just rname) 		performSet setter expr uuid 	start _ = giveup "Specify a repository." 		
Command/Whereis.hs view
@@ -53,7 +53,7 @@  startKeys :: M.Map UUID Remote -> Key -> ActionItem -> CommandStart startKeys remotemap key ai = do-	showStart' "whereis" key ai+	showStartKey "whereis" key ai 	next $ perform remotemap key  perform :: M.Map UUID Remote -> Key -> CommandPerform
Logs/Trust.hs view
@@ -72,7 +72,7 @@ 		map (\r -> (Types.Remote.uuid r, UnTrusted)) exports 	logged <- trustMapRaw 	let configured = M.fromList $ mapMaybe configuredtrust l-	let m = M.union exportoverrides $+	let m = M.unionWith max exportoverrides $ 		M.union overrides $ 		M.union configured logged 	Annex.changeState $ \s -> s { Annex.trustmap = Just m }
Logs/Web.hs view
@@ -100,15 +100,15 @@ removeTempUrl key = Annex.changeState $ \s -> 	s { Annex.tempurls = M.delete key (Annex.tempurls s) } -data Downloader = WebDownloader | QuviDownloader | OtherDownloader+data Downloader = WebDownloader | YoutubeDownloader | QuviDownloader | OtherDownloader 	deriving (Eq, Show)  {- To keep track of how an url is downloaded, it's mangled slightly in- - the log. For quvi, "quvi:" is prefixed. For urls that are handled by- - some other remote, ":" is prefixed. -}+ - the log, with a prefix indicating when a Downloader is used. -} setDownloader :: URLString -> Downloader -> String setDownloader u WebDownloader = u setDownloader u QuviDownloader = "quvi:" ++ u+setDownloader u YoutubeDownloader = "yt:" ++ u setDownloader u OtherDownloader = ":" ++ u  setDownloader' :: URLString -> Remote -> String@@ -118,6 +118,9 @@  getDownloader :: URLString -> (URLString, Downloader) getDownloader u = case separate (== ':') u of-	("quvi", u') -> (u', QuviDownloader)+	("yt", u') -> (u', YoutubeDownloader)+	-- quvi is not used any longer; youtube-dl should be able to handle+	-- all urls it did.+	("quvi", u') -> (u', YoutubeDownloader) 	("", u') -> (u', OtherDownloader) 	_ -> (u, WebDownloader)
Messages.hs view
@@ -7,9 +7,10 @@  module Messages ( 	showStart,+	showStart',+	showStartKey, 	ActionItem, 	mkActionItem,-	showStart', 	showNote, 	showAction, 	showSideAction,@@ -66,8 +67,14 @@   where 	json = JSON.start command (Just file) Nothing -showStart' :: String -> Key -> ActionItem -> Annex ()-showStart' command key i = outputMessage json $+showStart' :: String -> Maybe String -> Annex ()+showStart' command mdesc = outputMessage json $+	command ++ (maybe "" (" " ++) mdesc) ++ " "+  where+	json = JSON.start command Nothing Nothing++showStartKey :: String -> Key -> ActionItem -> Annex ()+showStartKey command key i = outputMessage json $ 	command ++ " " ++ actionItemDesc i key ++ " "   where 	json = JSON.start command (actionItemWorkTreeFile i) (Just key)
Remote/Helper/Special.hs view
@@ -107,7 +107,7 @@ fileRetriever a k m callback = do 	f <- prepTmp k 	a f k m-	callback (FileContent f)+	pruneTmpWorkDirBefore f (callback . FileContent)  -- A Retriever that generates a lazy ByteString containing the Key's -- content, and passes it to a callback action which will fully consume it
Remote/Web.hs view
@@ -19,8 +19,7 @@ import Annex.UUID import Utility.Metered import qualified Annex.Url as Url-import Annex.Quvi-import qualified Utility.Quvi as Quvi+import Annex.YoutubeDl  remote :: RemoteType remote = RemoteType@@ -80,9 +79,7 @@ 		untilTrue urls $ \u -> do 			let (u', downloader) = getDownloader u 			case downloader of-				QuviDownloader -> do-					flip (downloadUrl key p) dest-						=<< withQuviOptions Quvi.queryLinks [Quvi.httponly, Quvi.quiet] u'+				YoutubeDownloader -> youtubeDlTo key u' dest 				_ -> downloadUrl key p [u'] dest  downloadKeyCheap :: Key -> AssociatedFile -> FilePath -> Annex Bool@@ -109,8 +106,7 @@ 	let (u', downloader) = getDownloader u 	showChecking u' 	case downloader of-		QuviDownloader ->-			Right <$> withQuviOptions Quvi.check [Quvi.httponly, Quvi.quiet] u'+		YoutubeDownloader -> youtubeDlCheck u' 		_ -> do 			Url.withUrlOptions $ catchMsgIO . 				Url.checkBoth u' (keySize key)@@ -126,4 +122,4 @@ getWebUrls key = filter supported <$> getUrls key   where 	supported u = snd (getDownloader u) -		`elem` [WebDownloader, QuviDownloader]+		`elem` [WebDownloader, YoutubeDownloader]
Test.hs view
@@ -675,7 +675,7 @@ 		annexeval $ do 			Database.Keys.closeDb 			dbdir <- Annex.fromRepo Annex.Locations.gitAnnexKeysDb-			liftIO $ removeDirectoryRecursive dbdir+			liftIO $ renameDirectory dbdir (dbdir ++ ".old") 		writeFile annexedfile "test_lock_v6_force content" 		not <$> git_annex "lock" [annexedfile] @? "lock of modified file failed to fail in v6 mode" 		git_annex "lock" ["--force", annexedfile] @? "lock --force of modified file failed in v6 mode"@@ -1810,7 +1810,7 @@ 		)   where 	isdirect = annexeval $ do-		Annex.Init.initialize Nothing Nothing+		Annex.Init.initialize (Annex.Init.AutoInit False) Nothing Nothing 		Config.isDirect  checkRepo :: Types.Annex a -> FilePath -> IO a
Types/GitConfig.hs view
@@ -67,7 +67,7 @@ 	, annexSyncContent :: Configurable Bool 	, annexDebug :: Bool 	, annexWebOptions :: [String]-	, annexQuviOptions :: [String]+	, annexYoutubeDlOptions :: [String] 	, annexAriaTorrentOptions :: [String] 	, annexWebDownloadCommand :: Maybe String 	, annexCrippledFileSystem :: Bool@@ -127,7 +127,7 @@ 		getmaybebool (annex "synccontent") 	, annexDebug = getbool (annex "debug") False 	, annexWebOptions = getwords (annex "web-options")-	, annexQuviOptions = getwords (annex "quvi-options")+	, annexYoutubeDlOptions = getwords (annex "youtube-dl-options") 	, annexAriaTorrentOptions = getwords (annex "aria-torrent-options") 	, annexWebDownloadCommand = getmaybe (annex "web-download-command") 	, annexCrippledFileSystem = getbool (annex "crippledfilesystem") False
Types/Transfer.hs view
@@ -5,11 +5,15 @@  - Licensed under the GNU GPL version 3 or higher.  -} +{-# LANGUAGE FlexibleInstances #-}+ module Types.Transfer where  import Types+import Types.Remote (Verification(..)) import Utility.PID import Utility.QuickCheck+import Utility.Url  import Data.Time.Clock.POSIX import Control.Concurrent@@ -66,3 +70,34 @@ 		-- associated file cannot be empty (but can be Nothing) 		<*> (AssociatedFile <$> arbitrary `suchThat` (/= Just "")) 		<*> arbitrary++class Observable a where+	observeBool :: a -> Bool+	observeFailure :: a++instance Observable Bool where+	observeBool = id+	observeFailure = False++instance Observable (Bool, Verification) where+	observeBool = fst+	observeFailure = (False, UnVerified)++instance Observable (Either e Bool) where+	observeBool (Left _) = False+	observeBool (Right b) = b+	observeFailure = Right False++instance Observable (Maybe a) where+	observeBool (Just _) = True+	observeBool Nothing = False+	observeFailure = Nothing++class Transferrable t where+	descTransfrerrable :: t -> Maybe String++instance Transferrable AssociatedFile where+	descTransfrerrable (AssociatedFile af) = af++instance Transferrable URLString where+	descTransfrerrable = Just
Utility/Gpg.hs view
@@ -171,7 +171,7 @@  {- "subkey!" tells gpg to force use of a specific subkey -} isForcedSubKey :: String -> Bool-isForcedSubKey s = "!" `isSuffixOf` s && all isHexDigit (drop 1 s)+isForcedSubKey s = "!" `isSuffixOf` s && all isHexDigit (drop 1 (reverse s))  type UserId = String 
+ Utility/HtmlDetect.hs view
@@ -0,0 +1,46 @@+{- html detection+ -+ - Copyright 2017 Joey Hess <id@joeyh.name>+ -+ - License: BSD-2-clause+ -}++module Utility.HtmlDetect where++import Text.HTML.TagSoup+import Data.Char+import qualified Data.ByteString.Lazy as B+import qualified Data.ByteString.Lazy.Char8 as B8++-- | Detect if a String is a html document.+--+-- The document many not be valid, or may be truncated, and will+-- still be detected as html, as long as it starts with a+-- "<html>" or "<!DOCTYPE html>" tag.+--+-- Html fragments like "<p>this</p>" are not detected as being html,+-- although some browsers may chose to render them as html.+isHtml :: String -> Bool+isHtml = evaluate . canonicalizeTags . parseTags . take htmlPrefixLength+  where+	evaluate (TagOpen "!DOCTYPE" ((t, _):_):_) = map toLower t == "html"+	evaluate (TagOpen "html" _:_) = True+	-- Allow some leading whitespace before the tag.+	evaluate (TagText t:rest)+		| all isSpace t = evaluate rest+		| otherwise = False+	-- It would be pretty weird to have a html comment before the html+	-- tag, but easy to allow for.+	evaluate (TagComment _:rest) = evaluate rest+	evaluate _ = False++-- | Detect if a ByteString is a html document.+isHtmlBs :: B.ByteString -> Bool+-- The encoding of the ByteString is not known, but isHtml only+-- looks for ascii strings.+isHtmlBs = isHtml . B8.unpack++-- | How much of the beginning of a html document is needed to detect it.+-- (conservatively)+htmlPrefixLength :: Int+htmlPrefixLength = 8192
− Utility/Quvi.hs
@@ -1,162 +0,0 @@-{- querying quvi (import qualified)- -- - Copyright 2013 Joey Hess <id@joeyh.name>- -- - License: BSD-2-clause- -}--{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE FlexibleContexts #-}--module Utility.Quvi where--import Common-import Utility.Url--import Data.Aeson-import qualified Data.ByteString as B-import qualified Data.ByteString.Lazy as BL-import qualified Data.Map as M-import Network.URI (uriAuthority, uriRegName)-import Data.Char--data QuviVersion-	= Quvi04-	| Quvi09-	| NoQuvi-	deriving (Show)--data Page = Page-	{ pageTitle :: String-	, pageLinks :: [Link]-	} deriving (Show)--data Link = Link-	{ linkSuffix :: Maybe String-	, linkUrl :: URLString-	} deriving (Show)--{- JSON instances for quvi 0.4. -}-instance FromJSON Page where-	parseJSON (Object v) = Page-		<$> v .: "page_title"-		<*> v .: "link"-	parseJSON _ = mzero--instance FromJSON Link where-	parseJSON (Object v) = Link-		<$> v .:? "file_suffix"-		<*> v .: "url"-	parseJSON _ = mzero--{- "enum" format used by quvi 0.9 -}-parseEnum :: String -> Maybe Page-parseEnum s = Page-	<$> get "QUVI_MEDIA_PROPERTY_TITLE"-	<*> ((:[]) <$>-		( Link-			<$> Just <$> (get "QUVI_MEDIA_STREAM_PROPERTY_CONTAINER")-			<*> get "QUVI_MEDIA_STREAM_PROPERTY_URL"-		)-	    )-  where-	get = flip M.lookup m-	m = M.fromList $ map (separate (== '=')) $ lines s--probeVersion :: IO QuviVersion-probeVersion = catchDefaultIO NoQuvi $-	examine <$> processTranscript "quvi" ["--version"] Nothing-  where-	examine (s, True)-		| "quvi v0.4" `isInfixOf` s = Quvi04-		| otherwise = Quvi09-	examine _ = NoQuvi--type Query a = QuviVersion -> [CommandParam] -> URLString -> IO a--{- Throws an error when quvi is not installed. -}-forceQuery :: Query (Maybe Page)-forceQuery v ps url = query' v ps url `catchNonAsync` onerr-  where-	onerr e = ifM (inPath "quvi")-		( giveup ("quvi failed: " ++ show e)-		, giveup "quvi is not installed"-		)--{- Returns Nothing if the page is not a video page, or quvi is not- - installed. -}-query :: Query (Maybe Page)-query v ps url = flip catchNonAsync (const $ return Nothing) (query' v ps url)--query' :: Query (Maybe Page)-query' Quvi09 ps url = parseEnum-	<$> readQuvi (toCommand $ [Param "dump", Param "-p", Param "enum"] ++ ps ++ [Param url])-query' Quvi04 ps url = do-	let p = proc "quvi" (toCommand $ ps ++ [Param url])-	decode . BL.fromStrict-		<$> withHandle StdoutHandle createProcessSuccess p B.hGetContents-query' NoQuvi _ _ = return Nothing--queryLinks :: Query [URLString]-queryLinks v ps url = maybe [] (map linkUrl . pageLinks) <$> query v ps url--{- Checks if quvi can still find a download link for an url.- - If quvi is not installed, returns False. -}-check :: Query Bool-check v ps url = maybe False (not . null . pageLinks) <$> query v ps url--{- Checks if an url is supported by quvi, as quickly as possible- - (without hitting it if possible), and without outputting- - anything. Also returns False if quvi is not installed. -}-supported :: QuviVersion -> URLString -> IO Bool-supported NoQuvi _ = return False-supported Quvi04 url = boolSystem "quvi"-		[ Param "--verbosity", Param "mute"-		, Param "--support"-		, Param url-		]-{- Use quvi-info to see if the url's domain is supported.- - If so, have to do a online verification of the url. -}-supported Quvi09 url = (firstlevel <&&> secondlevel)-		`catchNonAsync` (\_ -> return False)-  where-	firstlevel = case uriAuthority =<< parseURIRelaxed url of-		Nothing -> return False-		Just auth -> do-			let domain = map toLower $ uriRegName auth-			let basedomain = intercalate "." $ reverse $ take 2 $ reverse $ splitc '.' domain-			any (\h -> domain `isSuffixOf` h || basedomain `isSuffixOf` h) -				. map (map toLower) <$> listdomains Quvi09-	secondlevel = snd <$> processTranscript "quvi"-		(toCommand [Param "dump", Param "-o", Param url]) Nothing--listdomains :: QuviVersion -> IO [String]-listdomains Quvi09 = concatMap (splitc ',') -	. concatMap (drop 1 . words) -	. filter ("domains: " `isPrefixOf`) . lines-	<$> readQuvi (toCommand [Param "info", Param "-p", Param "domains"])-listdomains _ = return []--type QuviParams = QuviVersion -> [CommandParam]--{- Disables progress, but not information output. -}-quiet :: QuviParams--- Cannot use quiet as it now disables informational output.--- No way to disable progress.-quiet Quvi09 = [Param "--verbosity", Param "verbose"]-quiet Quvi04 = [Param "--verbosity", Param "quiet"]-quiet NoQuvi = []--{- Only return http results, not streaming protocols. -}-httponly :: QuviParams--- No way to do it with 0.9?-httponly Quvi04 = [Param "-c", Param "http"]-httponly _ = [] -- No way to do it with 0.9?--readQuvi :: [String] -> IO String-readQuvi ps = withHandle StdoutHandle createProcessSuccess p $ \h -> do-	r <- hGetContentsStrict h-	hClose h-	return r-  where-	p = proc "quvi" ps
Utility/Url.hs view
@@ -1,6 +1,6 @@ {- Url downloading.  -- - Copyright 2011-2014 Joey Hess <id@joeyh.name>+ - Copyright 2011-2017 Joey Hess <id@joeyh.name>  -  - License: BSD-2-clause  -}@@ -25,6 +25,7 @@ 	assumeUrlExists, 	download, 	downloadQuiet,+	downloadPartial, 	parseURIRelaxed, 	matchStatusCodeException, 	matchHttpExceptionContent,@@ -39,8 +40,10 @@ import qualified Data.CaseInsensitive as CI import qualified Data.ByteString as B import qualified Data.ByteString.UTF8 as B8+import qualified Data.ByteString.Lazy as L import Control.Monad.Trans.Resource import Network.HTTP.Conduit hiding (closeManager)+import Network.HTTP.Client (brRead, withResponse)  -- closeManager is needed with older versions of http-client, -- but not new versions, which warn about using it. Urgh.@@ -140,7 +143,7 @@  - also returning its size and suggested filename if available. -} getUrlInfo :: URLString -> UrlOptions -> IO UrlInfo getUrlInfo url uo = case parseURIRelaxed url of-	Just u -> case parseurlconduit (show u) of+	Just u -> case parseUrlConduit (show u) of 		Just req -> catchJust 			-- When http redirects to a protocol which  			-- conduit does not support, it will throw@@ -220,12 +223,6 @@ 			_ | isftp && isJust len -> good 			_ -> dne -#if MIN_VERSION_http_client(0,4,30)-	parseurlconduit = parseUrlThrow-#else-	parseurlconduit = parseUrl-#endif- -- Parse eg: attachment; filename="fname.ext" -- per RFC 2616 contentDispositionFilename :: String -> Maybe FilePath@@ -321,10 +318,44 @@ 		| quiet = [Param s] 		| otherwise = [] +{- Downloads at least the specified number of bytes from an url. -}+downloadPartial :: URLString -> UrlOptions -> Int -> IO (Maybe L.ByteString)+downloadPartial url uo n = case parseURIRelaxed url of+	Nothing -> return Nothing+	Just u -> go u `catchNonAsync` const (return Nothing)+  where+	go u = case parseUrlConduit (show u) of+		Nothing -> return Nothing+		Just req -> do+			mgr <- newManager managerSettings+			let req' = applyRequest uo req+			ret <- withResponse req' mgr $ \resp ->+				if responseStatus resp == ok200+					then Just <$> brread n [] (responseBody resp)+					else return Nothing+			liftIO $ closeManager mgr+			return ret++	-- could use brReadSome here, needs newer http-client dependency+	brread n' l rb+		| n' <= 0 = return (L.fromChunks (reverse l))+		| otherwise = do+			bs <- brRead rb+			if B.null bs+				then return (L.fromChunks (reverse l))+				else brread (n' - B.length bs) (bs:l) rb+ {- Allows for spaces and other stuff in urls, properly escaping them. -} parseURIRelaxed :: URLString -> Maybe URI parseURIRelaxed s = maybe (parseURIRelaxed' s) Just $ 	parseURI $ escapeURIString isAllowedInURI s++parseUrlConduit :: URLString -> Maybe Request+#if MIN_VERSION_http_client(0,4,30)+parseUrlConduit = parseUrlThrow+#else+parseUrlConduit = parseUrl+#endif  {- Some characters like '[' are allowed in eg, the address of  - an uri, but cannot appear unescaped further along in the uri.
doc/git-annex-addurl.mdwn view
@@ -10,8 +10,8 @@  Downloads each url to its own file, which is added to the annex. -When `quvi` is installed, urls are automatically tested to see if they-point to a video hosting site, and the video is downloaded instead.+When `youtube-dl` is installed, it's used to check for a video embedded in +a web page at the url, and that is added to the annex instead.    Urls to torrent files (including magnet links) will cause the content of the torrent to be downloaded, using `aria2c`.@@ -30,12 +30,17 @@  * `--relaxed` -  Avoid storing the size of the url's content, and accept whatever-  content is there at a future point. (Implies `--fast`.)+  Don't immediately download the url, and avoid storing the size of the+  url's content. This makes git-annex accept whatever content is there+  at a future point. +  This is the fastest option, but it still has to access the network+  to check if the url contains embedded media. When adding large numbers+  of urls, using `--relaxed --raw` is much faster.+   * `--raw` -  Prevent special handling of urls by quvi, bittorrent, and other+  Prevent special handling of urls by youtube-dl, bittorrent, and other   special remotes. This will for example, make addurl   download the .torrent file and not the contents it points to. 
doc/git-annex-importfeed.mdwn view
@@ -8,14 +8,13 @@  # DESCRIPTION -Imports the contents of podcast feeds. Only downloads files whose+Imports the contents of podcasts and other feeds. Only downloads files whose content has not already been added to the repository before, so you can delete, rename, etc the resulting files and repeated runs won't duplicate them. -When quvi is installed, links in the feed are tested to see if they-are on a video hosting site, and the video is downloaded. This allows-importing e.g., YouTube playlists.+When `youtube-dl` is installed, it's used to download links in the feed.+This allows importing e.g., YouTube playlists.  To make the import process add metadata to the imported files from the feed, `git config annex.genmetadata true`@@ -37,6 +36,23 @@ * `--relaxed`, `--fast`, `--raw`    These options behave the same as when using [[git-annex-addurl]](1).++* `--fast`++  Avoid immediately downloading urls. The url is still checked+  (via HEAD) to verify that it exists, and to get its size if possible.++* `--relaxed`++  Don't immediately download urls, and avoid storing the size of the+  url's content. This makes git-annex accept whatever content is there+  at a future point.++* `--raw`++  Prevent special handling of urls by youtube-dl, bittorrent, and other+  special remotes. This will for example, make importfeed+  download a .torrent file and not the contents it points to.  # SEE ALSO 
doc/git-annex-init.mdwn view
@@ -23,6 +23,11 @@  This command is entirely safe, although usually pointless, to run inside an already initialized git-annex repository.+  +A top-level `.noannex` file will prevent git-annex init from being used+in a repository. This is useful for repositories that have a policy+reason not to use git-annex. The content of the file will be displayed+to the user who tries to run git-annex init.  # OPTIONS 
doc/git-annex.mdwn view
@@ -1315,10 +1315,16 @@   Options to pass when running wget or curl.   For example, to force IPv4 only, set it to "-4" -* `annex.quvi-options`+* `annex.youtube-dl-options` -  Options to pass to quvi when using it to find the url to download for a-  video.+  Options to pass to youtube-dl when using it to find the url to download+  for a video.++  Some options may break git-annex's integration with youtube-dl. For+  example, the --output option could cause it to store files somewhere+  git-annex won't find them. Avoid setting here or in the youtube-dl config+  file any options that cause youtube-dl to download more than one file,+  or to store the file anywhere other than the current working directory.  * `annex.aria-torrent-options` 
git-annex.cabal view
@@ -1,5 +1,5 @@ Name: git-annex-Version: 6.20171124+Version: 6.20171214 Cabal-Version: >= 1.8 License: GPL-3 Maintainer: Joey Hess <id@joeyh.name>@@ -347,6 +347,7 @@    persistent,    persistent-template,    aeson,+   tagsoup,    unordered-containers,    feed (>= 0.3.9),    regex-tdfa,@@ -447,7 +448,6 @@   if flag(Webapp)     Build-Depends:      yesod (>= 1.2.6), -     yesod-default (>= 1.2.0),      yesod-static (>= 1.2.4),      yesod-form (>= 1.3.15),      yesod-core (>= 1.2.19),@@ -530,7 +530,6 @@     Annex.Path     Annex.Perms     Annex.Queue-    Annex.Quvi     Annex.ReplaceFile     Annex.SpecialRemote     Annex.Ssh@@ -546,6 +545,7 @@     Annex.View.ViewedFile     Annex.Wanted     Annex.WorkTree+    Annex.YoutubeDl     Assistant     Assistant.Alert     Assistant.Alert.Utility@@ -1001,6 +1001,7 @@     Utility.Glob     Utility.Gpg     Utility.Hash+    Utility.HtmlDetect     Utility.HumanNumber     Utility.HumanTime     Utility.InodeCache@@ -1032,7 +1033,6 @@     Utility.Process     Utility.Process.Shim     Utility.QuickCheck-    Utility.Quvi     Utility.Rsync     Utility.SRV     Utility.SafeCommand
stack.yaml view
@@ -20,7 +20,6 @@ - aws-0.17.1 - bloomfilter-2.0.1.0 - torrent-10000.1.1-- yesod-default-1.2.0 explicit-setup-deps:   git-annex: true resolver: lts-9.9