-
Notifications
You must be signed in to change notification settings - Fork 131
/
Copy pathGlobalCache.hs
171 lines (144 loc) Β· 6.87 KB
/
GlobalCache.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
module Spago.GlobalCache where
import Spago.Prelude
import Spago.Env
import qualified Codec.Archive.Tar as Tar
import qualified Codec.Compression.GZip as GZip
import qualified Control.Foldl as Fold
import qualified Control.Retry as Retry
import qualified Data.ByteString as BS
import qualified Data.Map.Strict as Map
import qualified Data.Text as Text
import qualified Network.HTTP.Simple as Http
import qualified System.FilePath as FilePath
import qualified Turtle
import UnliftIO.Directory (XdgDirectory(XdgCache), getXdgDirectory)
newtype CommitHash = CommitHash Text
deriving (Ord, Eq, Show, Read, Generic, Data, ToJSON, FromJSON)
newtype Tag = Tag Text
deriving (Ord, Eq, Show, Read, Generic, Data, ToJSONKey, FromJSONKey, FromJSON, ToJSON)
data RepoMetadataV1 = RepoMetadataV1
{ commits :: [CommitHash]
, latest :: Maybe Tag
, owner :: Text
, tags :: (Map Tag CommitHash)
} deriving (Show, Generic)
instance FromJSON RepoMetadataV1
instance ToJSON RepoMetadataV1
type ReposMetadataV1 = Map PackageName RepoMetadataV1
-- | A package is "globally cacheable" if:
-- * it's a GitHub repo
-- * the ref we have is a commit or a tag -- i.e. "immutable enough", so e.g. not a branch
--
-- So here we check that one of the two is true, and if so we run the callback with the
-- URL of the .tar.gz archive on GitHub, otherwise another callback for when it's not
globallyCache
:: HasLogFunc env
=> (PackageName, Repo, Text)
-> FilePath.FilePath
-> ReposMetadataV1
-> (FilePath.FilePath -> RIO env ())
-> RIO env ()
-> RIO env ()
globallyCache (packageName, Repo url, ref) downloadDir metadata cacheableCallback notCacheableCallback = do
logDebug $ "Running `globallyCache`: " <> displayShow packageName <> " " <> display url <> " " <> display ref
case (Text.stripPrefix "https://github.com/" url)
>>= (Text.stripSuffix ".git")
>>= (Just . Text.split (== '/')) of
Just [owner, repo] -> do
case (isTag <|> isCommit) of
Nothing -> notCacheableCallback -- TODO: nice error?
Just _ -> do
let archiveUrl = "https://github.com/" <> owner <> "/" <> repo <> "/archive/" <> ref <> ".tar.gz"
logDebug $ "About to fetch tarball for " <> display archiveUrl
fetchTarball downloadDir archiveUrl
Turtle.fold (Turtle.ls $ Turtle.decodeString downloadDir) Fold.head >>= \case
Just resultDir -> do
cacheableCallback $ Turtle.encodeString resultDir
Nothing -> do
die [ "Could not find the result directory when unpacking the archive " <> displayShow archiveUrl ]
where
_ -> do
logDebug $ "Not caching repo because URL doesn't have the form of 'https://github.com/<ORG>/<REPO>.git': " <> display url
notCacheableCallback -- TODO: error?
where
isTag = do
RepoMetadataV1{..} <- Map.lookup packageName metadata
void $ Map.lookup (Tag ref) tags
return ref
isCommit = do
RepoMetadataV1{..} <- Map.lookup packageName metadata
case elem (CommitHash ref) commits of
True -> return ref
False -> empty
-- | Download the GitHub Index cache from the `package-sets-metadata` repo
getMetadata :: (HasLogFunc env, HasGlobalCache env) => RIO env ReposMetadataV1
getMetadata = do
logDebug "Running `getMetadata`"
GlobalCache globalCacheDir cacheFlag <- view (the @GlobalCache)
logDebug $ "Global cache directory: " <> displayShow globalCacheDir
let metaURL = "https://raw.githubusercontent.com/spacchetti/package-sets-metadata/master/metadataV1.json"
globalPathToMeta = globalCacheDir </> "metadataV1.json"
maybeToMonoid :: Monoid a => Maybe a -> a
maybeToMonoid m = case m of
Nothing -> mempty
Just a -> a
downloadMeta = handleAny
(\err -> do
logDebug $ "Metadata fetch failed with exception: " <> display err
logWarn "Unable to download GitHub metadata, global cache will be disabled"
pure mempty)
(do
metaBS <- Http.getResponseBody `fmap` Http.httpBS metaURL
case decodeStrict' metaBS of
Nothing -> do
logWarn "Unable to parse GitHub metadata, global cache will be disabled"
pure mempty
Just meta -> do
assertDirectory globalCacheDir
liftIO $ BS.writeFile globalPathToMeta metaBS
pure meta)
case cacheFlag of
-- If we need to skip the cache we just get an empty map
Just SkipCache -> pure mempty
-- If we need to download a new cache we can skip checking the local filesystem
Just NewCache -> do
logInfo "Downloading a new packages cache metadata from GitHub.."
downloadMeta
-- Otherwise we check first
Nothing -> do
logInfo "Searching for packages cache metadata.."
-- Check if the metadata is in global cache and fresher than 1 day
shouldRefreshFile globalPathToMeta >>= \case
-- If we should not download it, read from file
False -> do
logInfo "Recent packages cache metadata found, using it.."
fmap maybeToMonoid $ liftIO $ decodeFileStrict globalPathToMeta
-- Otherwise download it, write it to file, and return it
True -> do
logInfo "Unable to find packages cache metadata, downloading from GitHub.."
downloadMeta
-- | Directory in which spago will put its global cache
-- `getXdgDirectory XdgCache` tries to find the folder pointed by
-- `$XDG_CACHE_HOME`, otherwise it uses:
-- - (on Linux/MacOS) the folder pointed by `$HOME/.cache`, or
-- - (on Windows) the folder pointed by `LocalAppData`
getGlobalCacheDir :: (MonadUnliftIO m, HasLogFunc env, MonadReader env m) => m FilePath.FilePath
getGlobalCacheDir = do
globalCache <- liftIO $ getXdgDirectory XdgCache "spago" <|> pure ".spago-global-cache"
-- N.B. `assertDirectory` will `die` internally which immediately prints the
-- filesystem-related error message prior to us catching it.
assertDirectory globalCache `catch` annotate
pure globalCache
where annotate (ExitFailure _) = die . pure $ e
where e = "hint: The global cache is located at $XDG_CACHE_HOME/spago. Set $XDG_CACHE_HOME to control its location."
annotate _ = pure ()
-- | Fetch the tarball at `archiveUrl` and unpack it into `destination`
fetchTarball :: HasLogFunc env => FilePath.FilePath -> Text -> RIO env ()
fetchTarball destination archiveUrl = do
logDebug $ "Fetching " <> display archiveUrl
tarballUrl <- Http.parseRequest $ Text.unpack archiveUrl
lbs <- fmap Http.getResponseBody
-- We retry a couple of times here to avoid transient network errors
$ Retry.recoverAll (Retry.fullJitterBackoff 100000 <> Retry.limitRetries 2)
$ \_retryStatus -> Http.httpLBS tarballUrl
liftIO $ Tar.unpack destination $ Tar.read $ GZip.decompress lbs