debian-3.23: 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))