@@ -0,0 +1,273 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+module Http
3+ ( Manager
4+ , getManager
5+ , toUrl
6+ , post
7+ , post'
8+ , Header
9+ , accept
10+ , Timeout(..)
11+ , Body(..)
12+ , Multi.Part
13+ , filePart
14+ , stringPart
15+ , BodyReader
16+ , readByteString
17+ , Sha
18+ , shaToChars
19+ , readArchive
20+ , writeArchive
21+ , Error(..)
22+ )
23+ where
24+
25+
26+import Prelude hiding (zip)
27+import qualified Codec.Archive.Zip as Zip
28+import Control.Exception (SomeException, handle)
29+import qualified Data.Binary as Binary
30+import qualified Data.Binary.Get as Binary
31+import qualified Data.ByteString.Char8 as BS
32+import qualified Data.Digest.Pure.SHA as SHA
33+import qualified Data.String as String
34+import Network.HTTP (urlEncodeVars)
35+import Network.HTTP.Client
36+import Network.HTTP.Client.TLS (tlsManagerSettings)
37+import Network.HTTP.Types.Header (Header, hAccept, hAcceptEncoding, hUserAgent)
38+import Network.HTTP.Types.Method (methodPost)
39+import qualified Network.HTTP.Client.MultipartFormData as Multi
40+import qualified System.Directory as Dir
41+
42+import qualified Elm.Version as V
43+
44+
45+
46+-- MANAGER
47+
48+
49+getManager :: IO Manager
50+getManager =
51+ newManager tlsManagerSettings
52+
53+
54+
55+-- URL
56+
57+
58+toUrl :: String -> [(String,String)] -> String
59+toUrl url params =
60+ case params of
61+ [] -> url
62+ _:_ -> url ++ "?" ++ urlEncodeVars params
63+
64+
65+
66+-- POST
67+
68+
69+post
70+ :: Manager
71+ -> String
72+ -> [Header]
73+ -> (Error -> e)
74+ -> (BS.ByteString -> IO (Either e a))
75+ -> IO (Either e a)
76+post manager url headers onError onSuccess =
77+ handle (handleSomeException url onError) $
78+ handle (handleHttpException url onError) $
79+ do req0 <- parseUrlThrow url
80+ let req1 =
81+ req0
82+ { method = methodPost
83+ , requestHeaders = addDefaultHeaders headers
84+ }
85+ withResponse req1 manager $ \response ->
86+ do chunks <- brConsume (responseBody response)
87+ onSuccess (BS.concat chunks)
88+
89+
90+post'
91+ :: Manager
92+ -> String
93+ -> [Header]
94+ -> Timeout
95+ -> Body
96+ -> (Error -> e)
97+ -> (BodyReader -> IO (Either e a))
98+ -> IO (Either e a)
99+post' manager url headers timeout body onError onSuccess =
100+ handle (handleSomeException url onError) $
101+ handle (handleHttpException url onError) $
102+ do req0 <- parseUrlThrow url
103+ req1 <-
104+ addBody body $
105+ addTimeout timeout $
106+ req0
107+ { method = methodPost
108+ , requestHeaders = addDefaultHeaders headers
109+ }
110+ withResponse req1 manager (onSuccess . responseBody)
111+
112+
113+
114+-- HEADERS
115+
116+
117+addDefaultHeaders :: [Header] -> [Header]
118+addDefaultHeaders headers =
119+ (hUserAgent, userAgent) : (hAcceptEncoding, "gzip") : headers
120+
121+
122+{-# NOINLINE userAgent #-}
123+userAgent :: BS.ByteString
124+userAgent =
125+ BS.pack ("elm/" ++ V.toChars V.compiler)
126+
127+
128+accept :: BS.ByteString -> Header
129+accept mime =
130+ (hAccept, mime)
131+
132+
133+
134+-- TIMEOUT
135+
136+
137+data Timeout
138+ = DefaultTimeout
139+ | NeverTimeout
140+
141+
142+addTimeout :: Timeout -> Request -> Request
143+addTimeout timeout req =
144+ case timeout of
145+ DefaultTimeout ->
146+ req
147+
148+ NeverTimeout ->
149+ req { responseTimeout = responseTimeoutNone }
150+
151+
152+
153+-- BODY
154+
155+
156+data Body
157+ = NoBody
158+ | FormDataBody [Multi.Part]
159+
160+
161+addBody :: Body -> Request -> IO Request
162+addBody body req =
163+ case body of
164+ NoBody ->
165+ return req
166+
167+ FormDataBody parts ->
168+ Multi.formDataBody parts req
169+
170+
171+filePart :: String -> FilePath -> Multi.Part
172+filePart name filePath =
173+ Multi.partFileSource (String.fromString name) filePath
174+
175+
176+stringPart :: String -> String -> Multi.Part
177+stringPart name string =
178+ Multi.partBS (String.fromString name) (BS.pack string)
179+
180+
181+
182+-- EXCEPTIONS
183+
184+
185+data Error
186+ = BadUrl String String
187+ | BadHttp String HttpExceptionContent
188+ | BadMystery String SomeException
189+
190+
191+handleHttpException :: String -> (Error -> e) -> HttpException -> IO (Either e a)
192+handleHttpException url onError httpException =
193+ case httpException of
194+ InvalidUrlException _ reason ->
195+ return (Left (onError (BadUrl url reason)))
196+
197+ HttpExceptionRequest _ content ->
198+ return (Left (onError (BadHttp url content)))
199+
200+
201+handleSomeException :: String -> (Error -> e) -> SomeException -> IO (Either e a)
202+handleSomeException url onError exception =
203+ return (Left (onError (BadMystery url exception)))
204+
205+
206+
207+-- BODY READERS
208+
209+
210+readByteString :: BodyReader -> IO BS.ByteString
211+readByteString bodyReader =
212+ BS.concat <$> brConsume bodyReader
213+
214+
215+type Sha = SHA.Digest SHA.SHA1State
216+
217+
218+shaToChars :: Sha -> String
219+shaToChars =
220+ SHA.showDigest
221+
222+
223+readArchive :: BodyReader -> IO (Maybe (Sha, Zip.Archive))
224+readArchive body =
225+ readArchiveHelp body $
226+ AS 0 SHA.sha1Incremental (Binary.runGetIncremental Binary.get)
227+
228+
229+data ArchiveState =
230+ AS
231+ { _len :: !Int
232+ , _sha :: !(Binary.Decoder SHA.SHA1State)
233+ , _zip :: !(Binary.Decoder Zip.Archive)
234+ }
235+
236+
237+readArchiveHelp :: BodyReader -> ArchiveState -> IO (Maybe (Sha, Zip.Archive))
238+readArchiveHelp body (AS len sha zip) =
239+ case zip of
240+ Binary.Fail _ _ _ ->
241+ return Nothing
242+
243+ Binary.Partial k ->
244+ do chunk <- brRead body
245+ readArchiveHelp body $
246+ AS
247+ { _len = len + BS.length chunk
248+ , _sha = Binary.pushChunk sha chunk
249+ , _zip = k (if BS.null chunk then Nothing else Just chunk)
250+ }
251+
252+ Binary.Done _ _ archive ->
253+ return $ Just ( SHA.completeSha1Incremental sha len, archive )
254+
255+
256+
257+-- WRITE ARCHIVE
258+
259+
260+writeArchive :: Zip.Archive -> FilePath -> FilePath -> IO ()
261+writeArchive archive destination newRoot =
262+ do Dir.createDirectoryIfMissing True destination
263+ let opts = [Zip.OptDestination destination]
264+ mapM_ (Zip.writeEntry opts . replaceRoot newRoot) (Zip.zEntries archive)
265+
266+
267+replaceRoot :: String -> Zip.Entry -> Zip.Entry
268+replaceRoot root entry =
269+ let
270+ rootless =
271+ dropWhile (/='/') (Zip.eRelativePath entry)
272+ in
273+ entry { Zip.eRelativePath = root ++ rootless }