packages feed

archive-1.2.12: System/Archive/AptMethods.hs

module System.Archive.AptMethods
    (updateViaAptMethods
    )
    where

import Control.Monad
import Data.Function
import Data.List
import Data.Maybe
import Debian.Apt.Methods
import Debian.Apt.Index hiding (update)
import Debian.Control.ByteString
import Debian.Mirror
import System.FilePath
import System.Directory
import System.Exit
import System.Posix.Files
import System.Unix.FilePath (dirName)
import Network.URI

test =
    fetchOrLink [] (fromJust $ parseURI "http://archive.ubuntu.com/ubuntu/dists/gutsy/Release") "/tmp/curr" (Just "/tmp/prev") 


myCallbacks :: FetchCallbacks
myCallbacks = 
    cliFetchCallbacks { uriDoneCB = 
                            \uri size lastModified resumePoint filename hashes imsHit -> 
                                putStrLn $ uriToString' uri ++ " downloaded" ++ maybe "." (" to " ++) filename
                      }

uriDoneCBLink existingFP = \uri size lastModified resumePoint filename hashes imsHit -> 
            if imsHit
             then do createLink existingFP (fromJust filename)
                     putStrLn $ uriToString' uri ++ " hardlinked."
             else putStrLn $ uriToString' uri ++ " downloaded" ++ maybe "." (" to " ++) filename

uriToString' uri = uriToString id uri ""
-- |if the target files already exists, we will always break any
-- hardlinks before attempting to fetch. So, even if the file is not
-- updated.
fetchOrLink :: [ConfigItem] -> URI -> FilePath -> Maybe FilePath -> IO Bool
fetchOrLink configItems remotePath localPath mExistingFP =
    do flip when (breakLink localPath) =<< fileExist localPath
       case mExistingFP of
         Nothing -> 
             fetch myCallbacks configItems remotePath localPath =<< getLastModified localPath
         (Just existingFP) -> 
             fetch (myCallbacks { uriDoneCB = uriDoneCBLink existingFP }) configItems remotePath localPath =<< getLastModified existingFP
       
-- * To Be Moved 

-- break a hardlink
breakLink :: FilePath -> IO ()
breakLink fp =
    do status <- getSymbolicLinkStatus fp
       when (linkCount status > 1) (copyFile fp fp)

{-

The apt methods do not break hard-links. Therefore we need to
explicitly break the hard-links for it.

We can divide the stuff we need to download into two categories:
 - files we only have a timestamp for (Release files, etc)
 - stuff we have md5sums for

If we have the md5sum some, then we can check the files on the disk
already, and link the ones we have. The ones we do not have, we can
download via the methods.

Since we do not have a good way to automatically enumerate the dists,
the list of dists to mirror will have to be an explicit list.

Steps:
-----

1. Download/hardlink the Release files for each dist
2. Parse Release files and generate list of all required files
3. Attempt to hardlink to local files
4. Attempt to download remaining files
5. Rename download directory if everything successfully retrieved

How to recover from aborted download:

1. The bulk of the data is the debs, not the release files 2. If we
have the ability to find old copies in more than one location, then we
can just add the previous .in-progress directory to the list of places
to look for .debs. However, we also need to validate the size/md5sum
of files in the .in-progress directory, since it is likely one of them
was only download halfway. However, our hardlink algorithm will
probably already have the option to check size and md5sum.

What should we do if a Release File is missing? How about throw an
exception? 

-}

updateViaAptMethods :: [FilePath]  -- ^ path to previous snapshot directories
       -> URI -- ^ base path to remote repository
       -> FilePath -- ^ base path to local directory
       -> [(String, [String])] -- ^ list of (dist, arches) to download
       -> IO (ExitCode)
updateViaAptMethods prevBasePaths remoteURI basePath dists =
    do -- fetch the relevant files from the 'dists' directory
       mapM_ fetchControlFiles dists
       -- the local dists directory now contains all the control files
       -- parse them and find out what pool files we need
       poolFiles <- liftM concat $ mapM (createPoolFileList basePath) dists
       missing <- liftM catMaybes $ mapM (fetchPoolFile prevBasePaths basePath) poolFiles
       if null missing
          then return (ExitSuccess)
          else return (ExitFailure 1)
    where
      prevBasePath = listToMaybe prevBasePaths
      fetchControlFiles (dist, arches) =
          do let distPath = "dists" </> dist
                 releaseFP = distPath </> "Release"
             ensureParentDirectoryExists (basePath </> releaseFP)
             fetchOrLink [] (remoteURI { uriPath = (uriPath remoteURI) </> releaseFP }) (basePath </> releaseFP) (fmap (</> releaseFP) prevBasePath)
             release <- mustParseControlFromFile (basePath </> releaseFP)
             let indexFiles = indexesInRelease (archFilter arches) release
                 fetch = (\fp -> ensureParentDirectoryExists (basePath </> distPath </> fp) >>
                                 (fetchOrLink [] (remoteURI { uriPath = (uriPath remoteURI) </> distPath </> fp })
                                                  (basePath </> distPath </> fp)
                                                  (fmap (</> (distPath </> fp)) prevBasePath)))
             -- download index files found in Release
             mapM_ (\ (_,_,fp) -> fetch fp) indexFiles
             -- download other files not found in Release
             mapM_ fetch (["Release.gpg"] ++ map (\arch -> "Contents-" ++ arch ++ ".gz") arches)
      fetchPoolFile :: [FilePath] -> FilePath -> FileTuple -> IO (Maybe FileTuple)
      fetchPoolFile prevBasePaths basePath ft@(checksum, size, filename) =
          do doneAlready <- fileExist (basePath </> filename) -- TODO: check size/md5sum
             if doneAlready
                then do putStrLn $ filename ++ " already exists in pool."
                        return Nothing
                else do
                  ensureParentDirectoryExists (basePath </> filename) -- silly, but this gets us a log message, so..
                  res <- fetchOrLink [] (remoteURI { uriPath = (uriPath remoteURI) </> filename }) (basePath </> filename) (fmap (</> filename) prevBasePath)
                  if res
                     then return Nothing
                     else return (Just ft)
             where 
               prevBasePath = listToMaybe prevBasePaths
      filename (_,_,fp) = fp
      ensureParentDirectoryExists filepath =
          do let dir = dirName filepath
             putStrLn $ "Ensuring " ++ dir ++ " exists."
             createDirectoryIfMissing True dir
      mustParseControlFromFile fp =
          do r <- parseControlFromFile fp
             case r of
               (Left e) -> error (show e)
               (Right c) -> return c
      createPoolFileList basePath (dist, arches) =
          do let distDir = (basePath </> "dists" </> dist)
             release <- mustParseControlFromFile (distDir </> "Release")
             let indexFiles = indexesInRelease (archFilter arches) release
             binaryIndexes <- findIndexes distDir "Packages" indexFiles
             binaryFiles   <- liftM concat $ mapM (makePackageFileListIO distDir) binaryIndexes
             sourceIndexes <- findIndexes distDir "Sources" indexFiles
             sourceFiles   <- liftM concat $ mapM (makeSourceFileListIO  distDir) sourceIndexes
             return (nubOn filename  (binaryFiles ++ sourceFiles))
      nubOn :: (Ord b) => (a -> b) -> [a] -> [a]
      nubOn selector list = map head $ groupBy ((==) `on` selector) $ sortBy (compare `on` selector) list
{-
    let releaseFPs = map ((\dist -> "dists" </> dist </> "Release") . fst) dists -- hrm, we need to do this on a dist by dist basis
        prevBasePath = listToMaybe prevBasePaths
    in do -- create dist directories
          mapM_ ((\dir -> putStrLn ("Ensuring " ++ dir ++ " exists.") >> createDirectoryIfMissing True dir) . (basePath </>) . dirName) releaseFPs
          -- fetch Release files
          mapM_ (\fp -> fetchOrLink [] (remoteURI { uriPath = (uriPath remoteURI) </> fp }) (basePath </> fp) (fmap (</> fp) prevBasePath) >>= 
                 (flip unless) (error $ "Failed to fetch: " ++ fp)) releaseFPs
          -- read the release files and get a list of the other indexes
          releases <- mapM mustParseControlFromFile (map (basePath </>) releaseFPs)
          let indexFiles = concatMap (indexesInRelease (const True)) releases
          -- add checks for size/checksums
          mapM_ (\fp -> fetchOrLink [] (remoteURI { uriPath = (uriPath remoteURI) </> "dists" </> fp }) (basePath </> "dists" </> fp) (fmap (</> ("dists" </> fp)) prevBasePath)) (map (\ (_,_,fp) -> fp) indexFiles)
          -- (distFiles, _) <- liftM (\l -> (concatMap fst l, concatMap snd l)) $ mapM (\ (dist, arches) -> makeDistFileList (archFilter arches) basePath dist) dists
          -- return ()
          -- get all the indexes
          -- mapM_ (
          -- mapM_ print distFiles
          -- mapM_ print (poolFiles)
          -- print (length distFiles)
    where
      mustParseControlFromFile fp =
          do r <- parseControlFromFile fp
             case r of
               (Left e) -> error (show e)
               (Right c) -> return c
                  

-}
          
          

-- | apply monadic filter to list until failure is encountered
-- return True if no failures
-- return False if failed
allM :: (Monad m) => (a -> m Bool) -> [a] -> m Bool
allM f [] = return True
allM f (h:t) =
    do b <- f h
       case b of
         False -> return False
         True -> allM f t