debian-3.18.1: Debian/Repo/Repository.hs
{-# LANGUAGE ScopedTypeVariables #-}
module Debian.Repo.Repository
( UploadFile(..)
, prepareRepository
, repoArchList
, readPkgVersion
, showPkgVersion
, invalidRevision
, verifyUploadURI
, uploadRemote
) where
import Control.Exception (Exception(..))
import Control.Monad.Trans
import Control.Monad.State (get, put)
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as L
import Data.List
import Data.Maybe
import qualified Data.Set as Set
import qualified Debian.Control.ByteString as B -- required despite warning
import qualified Debian.Control.String as S
import Debian.Extra.CIO (tMessage, printOutput)
import Debian.Repo.Changes
import Debian.Repo.IO
import Debian.Repo.LocalRepository
import Debian.Repo.Types
import Debian.Shell
import Debian.URI
import Debian.Version
import Extra.Bool
import Extra.Either
import Extra.Files
import Extra.List
import Extra.Net
import Extra.SSH
import Extra.CIO
import System.FilePath
import System.Unix.Process
import System.Cmd
import System.Directory
import qualified System.IO as IO
import System.IO.Unsafe
import System.Time
import Text.Regex
-- |The file produced by dupload when a package upload attempt is made.
data UploadFile = Upload FilePath String DebianVersion Arch
-- |This is a remote repository which we have queried to find out the
-- names, sections, and supported architectures of its releases.
--data VerifiedRepo = VerifiedRepo URI [ReleaseInfo]
{- instance Show VerifiedRepo where
show (VerifiedRepo uri _) = "Verified Repository " ++ show uri -- ++ " " ++ show dists
instance Ord VerifiedRepo where
compare a b = compare (repoURI a) (repoURI b)
instance Eq VerifiedRepo where
a == b = compare a b == EQ -}
-- |This is a repository whose structure we haven't examined
-- to determine what release it contains.
--data UnverifiedRepo = UnverifiedRepo URI
{- instance Show UnverifiedRepo where
show (UnverifiedRepo uri) = "Unverified Repository " ++ show uri -- ++ " (unverified)"
instance Ord UnverifiedRepo where
compare a b = compare (repoURI a) (repoURI b)
instance Eq UnverifiedRepo where
a == b = compare a b == EQ -}
-- | Prepare a repository, which may be remote or local depending on
-- the URI.
prepareRepository :: CIO m => URI -> AptIOT m Repository
prepareRepository uri =
do state <- get
repo <- maybe newRepo return (lookupRepository uri state)
put (insertRepository uri repo state)
return repo
where
newRepo =
case uriScheme uri of
"file:" -> prepareLocalRepository (EnvPath (EnvRoot "") (uriPath uri)) Nothing >>= return . LocalRepo
-- FIXME: We only want to verifyRepository on demand.
-- Perhaps we want to use System.IO.Unsafe.unsafeInterleaveIO?
_ -> verifyRepository (UnverifiedRepo (show uri))
-- _ -> return . Repository . UnverifiedRepo $ uri
{-# NOINLINE verifyRepository #-}
verifyRepository :: CIO m => Repository -> AptIOT m Repository
verifyRepository (UnverifiedRepo uri) =
do --tio (vHPutStrBl IO.stderr 0 $ "Verifying repository " ++ show uri ++ "...")
-- Use unsafeInterleaveIO to avoid querying the repository
-- until the value is actually needed.
lift (vPutStrBl 2 ("verifyRepository " ++ uri))
releaseInfo <- do lift (vPutChar 2 '*')
liftIO . unsafeInterleaveIO . getReleaseInfoRemote . fromJust . parseURI $ uri
{- tio (vHPutStrLn IO.stderr 0 $ "\n" {- -> VerifiedRepo " ++ show uri ++ " " ++ show releaseInfo -} ) -}
return $ VerifiedRepo uri releaseInfo
verifyRepository x = return x
-- |Get the list of releases of a remote repository.
getReleaseInfoRemote :: URI -> IO [ReleaseInfo]
getReleaseInfoRemote uri =
IO.hPutStr IO.stderr ("(verifying " ++ uriToString' uri ++ ".") >>
dirFromURI distsURI >>=
either (error . show) verify >>= return . catMaybes >>=
(\ result -> IO.hPutStr IO.stderr ")" >> return result)
where
distsURI = uri {uriPath = uriPath uri </> "dists/"}
verify names =
do let dists = map parseReleaseName names
releaseFiles <- mapM getReleaseFile dists
let releasePairs = zip3 (map getSuite releaseFiles) releaseFiles dists
return $ map (uncurry3 getReleaseInfo) releasePairs
releaseNameField releaseFile = case fmap B.unpack (B.fieldValue "Origin" releaseFile) of Just "Debian" -> "Codename"; _ -> "Suite"
getReleaseInfo :: Maybe B.ByteString -> B.Paragraph -> ReleaseName -> Maybe ReleaseInfo
getReleaseInfo Nothing _ _ = Nothing
getReleaseInfo (Just dist) _ relname | (parseReleaseName (B.unpack dist)) /= relname = Nothing
getReleaseInfo (Just dist) info _ = Just $ makeReleaseInfo "" info (parseReleaseName (B.unpack dist)) []
getSuite releaseFile = B.fieldValue (releaseNameField releaseFile) releaseFile
getReleaseFile :: ReleaseName -> IO (S.Paragraph' B.ByteString)
getReleaseFile distName =
do IO.hPutChar IO.stderr '.'
release <- fileFromURI releaseURI >>= return . either Left (Right . B.concat . L.toChunks)
let control = either Left (either (Left . ErrorCall . show) Right . B.parseControl (show uri)) release
case control of
Right (B.Control [info]) -> return info
_ -> error ("Failed to get release info from dist " ++ show (relName distName) ++ ", uri " ++ show releaseURI)
where
releaseURI = distURI {uriPath = uriPath distURI </> "Release"}
distURI = distsURI {uriPath = uriPath distsURI </> releaseName' distName}
uncurry3 :: (a -> b -> c -> d) -> (a, b, c) -> d
uncurry3 f (a, b, c) = f a b c
-- |Make sure we can access the upload uri without typing a password.
verifyUploadURI :: CIO m => Bool -> URI -> AptIOT m ()
verifyUploadURI doExport uri =
case doExport of
True -> export
False -> verify
where
export =
do liftIO $ uncurry sshExport (uriDest uri)
verify
mkdir
verify =
do result <- liftIO $ uncurry sshVerify (uriDest uri)
case result of
False -> error $ "Unable to reach " ++ uriToString' uri ++ ", consider using --ssh-export"
True -> return ()
mkdir
uriDest uri =
let auth = maybe (error "Internal error 8") id (uriAuthority uri) in
let port =
case uriPort auth of
(':' : number) -> Just (read number)
"" -> Nothing
x -> error $ "Internal error 9: invalid port " ++ x in
(uriUserInfo auth ++ uriRegName auth, port)
mkdir :: CIO m => AptIOT m ()
mkdir =
case uriAuthority uri of
Nothing -> error $ "Internal error 7"
Just auth ->
do let cmd = "ssh " ++ uriUserInfo auth ++ uriRegName auth ++ uriPort auth ++ " mkdir -p " ++ uriPath uri ++ "/incoming"
result <- liftIO $ system cmd
case result of
ExitSuccess -> return ()
_ -> error $ "Failure: " ++ cmd
-- | Upload all the packages in a local repository to a the incoming
-- directory of a remote repository (using dupload.)
uploadRemote :: CIO m
=> LocalRepository -- ^ Local repository holding the packages.
-> URI -- ^ URI of upload repository
-> AptIOT m [Either String ([Output], TimeDiff)]
uploadRemote repo uri =
do uploaded <- liftIO (uploadFind (outsidePath root)) >>=
return . Set.fromList . map uploadKey . rightOnly
(accepted, rejected) <- liftIO (findChangesFiles (outsidePath root)) >>= return . (\x -> (x, [])) >>=
return . accept (notUploaded uploaded) (\ x -> (x, "Already uploaded")) >>=
return . rejectOlder >>=
acceptM (liftIO . validRevision) (\ x -> (x, "Invalid revision"))
case rejected of
[] -> return ()
_ -> lift (vPutStr 0 ("Rejected:\n " ++ consperse "\n " (map showReject rejected) ++ "\n"))
case accepted of
[] -> do lift (vPutStr 0 "Nothing to upload."); return []
_ -> do mapM (lift . dupload uri (outsidePath root)) (map Debian.Repo.Changes.path accepted)
where
root = repoRoot repo
rejectOlder :: ([ChangesFile], [(ChangesFile, String)]) -> ([ChangesFile], [(ChangesFile, String)])
rejectOlder (accept, reject) =
(accept', (map tag reject' ++ reject))
where accept' = map head sortedGroups
reject' = concat . map tail $ sortedGroups
sortedGroups = map (sortBy compareVersions) (groupByNameAndDist accept)
tag x = (x, "Not the newest version in incoming")
compareVersions a b = compare (changeVersion b) (changeVersion a)
groupByNameAndDist = groupBy equalNameAndDist . sortBy compareNameAndDist
equalNameAndDist a b = compareNameAndDist a b == EQ
compareNameAndDist a b =
case compare (changePackage a) (changePackage b) of
EQ -> compare (changeRelease a) (changeRelease b)
x -> x
notUploaded uploaded changes = not . Set.member (Debian.Repo.Changes.key changes) $ uploaded
validRevision c =
do
let dscPath = changeDir c </> changePackage c ++ "_" ++ show (changeVersion c) ++ ".dsc"
doesFileExist dscPath >>= cond (S.parseControlFromFile dscPath >>= either (error . show) (checkRevision dscPath)) (return True)
where
checkRevision _dscPath (S.Control [p]) =
case maybe Nothing parseRevision (S.fieldValue "Revision" p) of
Nothing -> return False
Just (x, _) | x == invalidRevision -> return False
Just _ -> return True
checkRevision dscPath _ = error ("Invalid .dsc file: " ++ show dscPath)
showReject (changes, tag) = Debian.Repo.Changes.name changes ++ ": " ++ tag
uploadKey :: UploadFile -> (String, DebianVersion, Arch)
uploadKey (Upload _ name ver arch) = (name, ver, arch)
uploadLoad :: FilePath -> String -> (Either [String] UploadFile)
uploadLoad dir file =
case parseUploadFilename file of
Just (name, ver, arch) -> Right $ Upload dir name ver arch
Nothing -> Left ["Couldn't parse upload filename: " ++ file]
uploadFind :: FilePath -> IO [Either [String] UploadFile]
uploadFind dir =
getDirectoryContents dir >>=
return . filter (isSuffixOf ".upload") >>=
return . map (uploadLoad dir)
{-
base :: UploadFile -> String
base (Upload _ name ver arch) = name ++ "_" ++ show ver ++ "_" ++ show arch
-}
-- filename name version arch ext
parseUploadFilename :: String -> Maybe (String, DebianVersion, Arch)
parseUploadFilename name =
case matchRegex (mkRegex "^(.*/)?([^_]*)_(.*)_([^.]*)\\.upload$") name of
Just [_, name, version, arch] -> Just (name, parseDebianVersion version, Binary arch)
_ -> error ("Invalid .upload file name: " ++ name)
invalidRevision = "none"
-- | Parse the "Revision:" value describing the origin of the
-- package's source and the dependency versions used to build it:
-- Revision: <revisionstring> dep1=ver1 dep2=ver2 ...
parseRevision :: String -> Maybe (String, [PkgVersion])
parseRevision s =
case words s of
[] -> Nothing
(revision : buildDeps) -> Just (revision, map readPkgVersion buildDeps)
showPkgVersion :: PkgVersion -> String
showPkgVersion v = show v
readPkgVersion :: String -> PkgVersion
readPkgVersion s = case mapSnd (parseDebianVersion . (drop 1)) (span (/= '=') s) of
(n, v) -> PkgVersion { getName = n, getVersion = v }
mapSnd f (a, b) = (a, f b)
accept :: (a -> Bool) -> (a -> (a, String)) -> ([a], [(a, String)]) -> ([a], [(a, String)])
accept p tag (accepted, rejected) =
(accepted', map tag rejected' ++ rejected)
where (accepted', rejected') = partition p accepted
acceptM :: (Monad m) => (a -> m Bool) -> (a -> (a, String)) -> ([a], [(a, String)]) -> m ([a], [(a, String)])
acceptM p tag (accept, reject) =
do (accept', reject') <- partitionM p accept
return (accept', (map tag reject' ++ reject))
-- |Run dupload on a changes file with an optional host (--to)
-- argument.
dupload :: CIO m
=> URI -- user
-> FilePath -- The directory containing the .changes file
-> String -- The name of the .changes file to upload
-> m (Either String ([Output], TimeDiff))
dupload uri dir changesFile =
case uriAuthority uri of
Nothing -> error ("Invalid Upload-URI: " ++ uriToString' uri)
Just auth ->
do
let config = ("package config;\n" ++
"$cfg{'default'} = {\n" ++
" fqdn => \"" ++ uriRegName auth ++ uriPort auth ++ "\",\n" ++
" method => \"scpb\",\n" ++
" login => \"" ++ init (uriUserInfo auth) ++ "\",\n" ++
" incoming => \"" ++ uriPath uri ++ "/incoming\",\n" ++
" dinstall_runs => 1,\n" ++
"};\n\n" ++
"$preupload{'changes'} = '';\n\n" ++
"1;\n")
liftIO $ replaceFile (dir ++ "/dupload.conf") config
liftIO (lazyCommand (cmd changesFile) L.empty) >>=
tMessage ("Uploading " ++ show changesFile) >>=
printOutput >>=
dotOutput 128 >>=
(\ output -> timeTask (checkResult fail (return (Right output)) output)) >>=
(\ (result, elapsed) -> return (either Left (\ output -> Right (output, elapsed)) result))
--style' $ runCommandQuietlyTimed (cmd changesFile)
where
{-
style' = setStyle (setStart (Just ("Uploading " ++ show changesFile)) .
setError (Just "dupload failed") .
setEcho True)
-}
fail n =
ePutStrBl message >> return (Left message)
where message = "dupload failed: " ++ cmd changesFile ++ " -> " ++ show n
cmd file = "cd " ++ dir ++ " && dupload --to default -c " ++ file
repoArchList :: Repo r => r -> [Arch]
repoArchList repo =
listIntersection (map releaseInfoArchitectures (repoReleaseInfo repo))