forked from haskell/hackage-server
-
Notifications
You must be signed in to change notification settings - Fork 0
/
MirrorClient.hs
269 lines (233 loc) · 10.2 KB
/
MirrorClient.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
module Main (main) where
-- stdlib
import Control.Exception
import Control.Monad
import Control.Monad.Trans
import Data.List
import Network.Browser
import System.Directory
import System.Environment
import System.Exit (exitWith, ExitCode(..))
import System.FilePath
import System.IO
import System.IO.Error
import System.Process (callCommand)
import qualified Codec.Archive.Tar as Tar
import qualified Codec.Compression.GZip as GZ
import qualified Data.ByteString.Lazy as BS
import qualified Data.Set as Set
-- Cabal
import Distribution.Package
import Distribution.Simple.Utils hiding (warn)
import Distribution.Text
import Distribution.Verbosity
import Distribution.Version
-- hackage
import Distribution.Client (PkgIndexInfo(..))
import Distribution.Client.Cron (cron, rethrowSignalsAsExceptions, Signal(..))
import Distribution.Client.Mirror.CmdLine
import Distribution.Client.Mirror.Config
import Distribution.Client.Mirror.Repo
import Distribution.Client.Mirror.Session
import Distribution.Client.Mirror.State
import Distribution.Server.Util.Merge
import Paths_hackage_server (version)
import qualified Distribution.Server.Util.GZip as GZip
-- hackage-security
import qualified Hackage.Security.Client.Repository.HttpLib as Sec
{-------------------------------------------------------------------------------
Application entry point
-------------------------------------------------------------------------------}
main :: IO ()
main = toplevelHandler $ do
rethrowSignalsAsExceptions [SIGABRT, SIGINT, SIGQUIT, SIGTERM]
hSetBuffering stdout LineBuffering
args <- getArgs
(verbosity, opts) <- validateOpts args
(env, st) <- mirrorInit verbosity opts
case continuous opts of
Nothing -> mirrorOneShot verbosity opts env st
Just interval -> cron verbosity interval
(mirrorIteration verbosity opts env) st
toplevelHandler :: IO a -> IO a
toplevelHandler =
handle $ \ioe -> do
hFlush stdout
pname <- getProgName
hPutStrLn stderr (pname ++ ": " ++ formatIOError ioe)
exitWith (ExitFailure 1)
{-------------------------------------------------------------------------------
One-shot versus continuous mirroring
-------------------------------------------------------------------------------}
mirrorOneShot :: Verbosity -> MirrorOpts -> MirrorEnv -> MirrorState -> IO ()
mirrorOneShot verbosity opts env st = do
(merr, _) <- mirrorOnce verbosity opts env st
case merr of
Nothing -> return ()
Just theError -> fail (formatMirrorError theError)
mirrorIteration :: Verbosity -> MirrorOpts -> MirrorEnv
-> MirrorState -> IO MirrorState
mirrorIteration verbosity opts env st = do
(merr, st') <- mirrorOnce verbosity opts { mo_keepGoing = True } env st
when (st' /= st) $
savePackagesState env st'
case merr of
Nothing -> return ()
Just Interrupted -> throw UserInterrupt
Just theError -> do
warn verbosity (formatMirrorError theError)
notice verbosity "Abandoning this mirroring attempt."
return st'
{-------------------------------------------------------------------------------
Main mirroring logic
-------------------------------------------------------------------------------}
mirrorOnce :: Verbosity -> MirrorOpts -> MirrorEnv
-> MirrorState -> IO (Maybe MirrorError, MirrorState)
mirrorOnce verbosity opts
(MirrorEnv srcCacheDir dstCacheDir missingPkgsFile unmirrorablePkgsFile)
st@(MirrorState missingPkgs unmirrorablePkgs) =
mirrorSession (mo_keepGoing opts) $ do
httpLib <- mirrorAskHttpLib
liftCont (initRepos httpLib) $ \(sourceRepo, targetRepo) -> do
srcIndex <- downloadSourceIndex sourceRepo
dstIndex <- readCachedTargetIndex verbosity targetRepo
let pkgsMissingFromDest = diffIndex srcIndex dstIndex
pkgsToMirror
| null (selectedPkgs opts) = pkgsMissingFromDest
| otherwise = subsetIndex (selectedPkgs opts)
pkgsMissingFromDest
pkgsToMirror' = filter (\(PkgIndexInfo pkg _ _ _) ->
pkg `Set.notMember` missingPkgs
&& pkg `Set.notMember` unmirrorablePkgs )
pkgsToMirror
mirrorCount = length pkgsToMirror'
ignoreCount = length pkgsToMirror - mirrorCount
if mirrorCount == 0
then liftIO $ notice verbosity $ "No packages to mirror"
else do
liftIO $ notice verbosity $
show mirrorCount ++ " packages to mirror."
++ if ignoreCount == 0 then ""
else " Ignoring " ++ show ignoreCount
++ " package(s) that cannot be mirrored\n(for details see "
++ missingPkgsFile ++ " and " ++ unmirrorablePkgsFile ++ ")"
mirrorPackages verbosity opts sourceRepo targetRepo pkgsToMirror'
finalizeMirror sourceRepo targetRepo
cacheTargetIndex sourceRepo targetRepo
case mirrorPostHook (mirrorConfig opts) of
Nothing -> return ()
Just postHook -> liftIO $ callCommand postHook
where
mirrorSession :: Bool
-> MirrorSession a -> IO (Maybe MirrorError, MirrorState)
mirrorSession keepGoing action =
liftM (\(eerr, st') -> (either Just (const Nothing) eerr,
fromErrorState st')) $
runMirrorSession verbosity keepGoing (toErrorState st) $ do
browserAction $ do
setUserAgent ("hackage-mirror/" ++ display version)
setErrHandler (warn verbosity)
setOutHandler (debug verbosity)
setAllowBasicAuth True
setCheckForProxy True
action
initRepos :: Sec.HttpLib -> ((SourceRepo, TargetRepo) -> IO a) -> IO a
initRepos httpLib callback =
withSourceRepo verbosity
httpLib
srcCacheDir
(mirrorSource (mirrorConfig opts))
$ \sourceRepo ->
withTargetRepo dstCacheDir
(mirrorTarget (mirrorConfig opts) )
$ \targetRepo ->
callback (sourceRepo, targetRepo)
mirrorPackages :: Verbosity
-> MirrorOpts
-> SourceRepo
-> TargetRepo
-> [PkgIndexInfo]
-> MirrorSession ()
mirrorPackages verbosity opts sourceRepo targetRepo pkgsToMirror = do
authenticate targetRepo
mapM_ (mirrorPackage verbosity opts sourceRepo targetRepo) pkgsToMirror
mirrorPackage :: Verbosity
-> MirrorOpts
-> SourceRepo
-> TargetRepo
-> PkgIndexInfo
-> MirrorSession ()
mirrorPackage verbosity opts sourceRepo targetRepo pkginfo = do
liftIO $ notice verbosity $ "mirroring " ++ display pkgid
go `mirrorFinally` removeTempFiles
where
go :: MirrorSession ()
go = do
rsp <- downloadPackage sourceRepo pkgid locCab locTgz
case rsp of
Just theError ->
notifyResponse (GetPackageFailed theError pkgid)
Nothing -> do
notifyResponse GetPackageOk
liftIO $ sanitiseTarball verbosity (stateDir opts) locTgz
uploadPackage targetRepo (mirrorUploaders opts) pkginfo locCab locTgz
removeTempFiles :: MirrorSession ()
removeTempFiles = liftIO $ handle ignoreDoesNotExist $ do
removeFile locTgz
removeFile locCab
ignoreDoesNotExist :: IOException -> IO ()
ignoreDoesNotExist ex = if isDoesNotExistError ex then return ()
else throwIO ex
PkgIndexInfo pkgid _ _ _ = pkginfo
locTgz = stateDir opts </> display pkgid <.> "tar.gz"
locCab = stateDir opts </> display pkgid <.> "cabal"
{-------------------------------------------------------------------------------
Operations on the the index
-------------------------------------------------------------------------------}
diffIndex :: [PkgIndexInfo] -> [PkgIndexInfo] -> [PkgIndexInfo]
diffIndex as bs =
[ pkg | OnlyInLeft pkg <- mergeBy (comparing mirrorPkgId)
(sortBy (comparing mirrorPkgId) as)
(sortBy (comparing mirrorPkgId) bs) ]
where
mirrorPkgId (PkgIndexInfo pkgid _ _ _) = pkgid
subsetIndex :: [PackageId] -> [PkgIndexInfo] -> [PkgIndexInfo]
subsetIndex pkgids =
filter (\(PkgIndexInfo pkgid' _ _ _) -> anyMatchPackage pkgid')
where
anyMatchPackage :: PackageId -> Bool
anyMatchPackage pkgid' =
any (\pkgid -> matchPackage pkgid pkgid') pkgids
matchPackage :: PackageId -> PackageId -> Bool
matchPackage (PackageIdentifier name (Version [] _))
(PackageIdentifier name' _ ) = name == name'
matchPackage pkgid pkgid' = pkgid == pkgid'
{-------------------------------------------------------------------------------
Auxiliary: dealing with tarballs
-------------------------------------------------------------------------------}
-- Some package tarballs have extraneous stuff in them that causes
-- them to fail the "tarbomb" test in the server. This cleans them
-- up before uploading.
sanitiseTarball :: Verbosity -> FilePath -> FilePath -> IO ()
sanitiseTarball verbosity tmpdir tgzpath = do
tgz <- BS.readFile tgzpath
let add _ (Left e) = Left e
add entry (Right entries) = Right (entry:entries)
eallentries = Tar.foldEntries add (Right []) (Left . show) $
Tar.read (GZip.decompressNamed tgzpath tgz)
case eallentries of
Left e -> warn verbosity e
Right allentries -> do
let okentries = filter dirOK allentries
newtgz = GZ.compress $ Tar.write $ reverse okentries
when (length allentries /= length okentries) $
warn verbosity $ "sanitising tarball for " ++ tgzpath
(tmpfp, tmph) <- openBinaryTempFileWithDefaultPermissions tmpdir "tmp.tgz"
hClose tmph
BS.writeFile tmpfp newtgz
renameFile tmpfp tgzpath
where
basedir = dropExtension $ takeBaseName tgzpath
dirOK entry = case splitDirectories (Tar.entryPath entry) of
(d:_) -> d == basedir
_ -> False