{-# 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: 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))