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