on master help sign in
Add support for serving file ranges

- Move a lot of the relevant code from warp to wai and export it from Network.Wai.Internal, because the file stat()ing and Range header handling is the responsibility of the HTTP2Application. - To offset the inconvenience of this, we provide 'respondFile' and 'respondFileRange' that act a lot like 'responseFile' in HTTP/1.x land. The former infers the range from the headers, while the latter takes an explicit range. - Remove the 'Maybe' from the FilePart; all file parts must be explicit when they get to the sender. As with the Range header, this is usually handled by one of the convenience functions.

commit7c34e4582ccfabafd9903b1ff12ac3a5a4e823dc authorAndrew Pritchard <awpr@google.com> dateMon 31 Aug 2015, 23:36 (11 years ago) parent7cffbbc
7 files changed+189−119
wai/Network/Wai/HTTP2.hs+69−11 wai/Network/Wai/Internal.hs+95−2 wai/wai.cabal+2 warp/Network/Wai/Handler/Warp/HTTP2/Sender.hs+10−17 warp/Network/Wai/Handler/Warp/HTTP2/Types.hs+1−1 warp/Network/Wai/Handler/Warp/RequestHeader.hs+2−28 warp/Network/Wai/Handler/Warp/Response.hs+10−60
wai/Network/Wai/HTTP2.hs+69−11
@@ -14,9 +14,14 @@ module Network.Wai.HTTP2
1414 , PushPromise(..)
1515 , promiseHeaders
1616 -- * Conveniences
17− , SimpleBody
1817 , promoteApplication
18+ -- ** 'Responder's
1919 , responder
20+ , respondFile
21+ , respondFilePart
22+ , respondNotFound
23+ -- ** 'Body's
24+ , SimpleBody
2025 , streamFilePart
2126 , streamBuilder
2227 , streamSimple
@@ -31,10 +36,13 @@ import qualified Network.HTTP.Types as H
3136
3237import Network.Wai (Application)
3338import Network.Wai.Internal
34− ( FilePart
35− , Request
39+ ( FilePart(..)
40+ , Request(requestHeaders)
3641 , Response(..)
3742 , ResponseReceived(..)
43+ , adjustForFilePart
44+ , chooseFilePart
45+ , tryGetFileSize
3846 )
3947
4048-- | Headers sent after the end of a data stream, as defined by section 4.1.2 of
@@ -54,7 +62,7 @@ data PushPromise = PushPromise
5462type HTTP2Application = Request -> PushFunc -> Responder
5563
5664-- | Part of a streaming response -- either a 'Builder' or a range of a file.
57−data Chunk = FileChunk FilePath (Maybe FilePart) | BuilderChunk Builder
65+data Chunk = FileChunk FilePath FilePart | BuilderChunk Builder
5866
5967-- | The streaming body of a response. Equivalent to
6068-- 'Network.Wai.StreamingBody' except that it can also write file ranges and
@@ -91,10 +99,50 @@ promiseHeaders p =
9199 , (":scheme", promisedScheme p)
92100 ] ++ promisedHeader p
93101
94−-- | Create a response body consisting of a single range of a file.
95−streamFilePart :: FilePath -> Maybe FilePart -> Body ()
102+-- | Create a response body consisting of a single range of a file. Does not
103+-- set Content-Length or Content-Range headers. For that, use
104+-- 'respondFilePart' or 'respondFile'.
105+streamFilePart :: FilePath -> FilePart -> Body ()
96106streamFilePart path part write _ = write $ FileChunk path part
97107
108+-- | Respond with a single range of a file, adding the Content-Length and
109+-- Content-Range headers and changing the status to 206 as appropriate.
110+--
111+-- If you want the range to be inferred automatically from the Range header,
112+-- use 'respondFile' instead.
113+respondFilePart :: H.Status -> H.ResponseHeaders -> FilePath -> FilePart -> Responder
114+respondFilePart s h path part respond = do
115+ let (s', h') = adjustForFilePart s h part
116+ respond s' h' $ streamFilePart path part
117+ return []
118+
119+-- | Serve the requested range of the specified file (based on the Range
120+-- header), using the given 'H.Status' and 'H.ResponseHeaders' as a base. If
121+-- the file is not accessible, the status will be replaced with 404 and a
122+-- default not-found message will be served. If a partial file is requested,
123+-- the status will be replaced with 206 and the Content-Range header will be
124+-- added. The Content-Length header will always be added.
125+respondFile :: H.Status -> H.ResponseHeaders -> FilePath -> H.RequestHeaders -> Responder
126+respondFile s h path req respond = do
127+ fileSize <- tryGetFileSize path
128+ case fileSize of
129+ Left _ -> respondNotFound h respond
130+ Right size -> respondFileExists s h path size req respond
131+
132+-- As 'respondFile', but with prior knowledge of the file's existence and size.
133+respondFileExists :: H.Status -> H.ResponseHeaders -> FilePath -> Integer -> H.RequestHeaders -> Responder
134+respondFileExists s h path size reqHdrs =
135+ respondFilePart s h path $ chooseFilePart size $ lookup H.hRange reqHdrs
136+
137+-- | Respond with a minimal 404 page with the given headers.
138+respondNotFound :: H.ResponseHeaders -> Responder
139+respondNotFound h respond = do
140+ respond H.notFound404 h' $ streamBuilder "File not found."
141+ return []
142+ where
143+ contentType = (H.hContentType, "text/plain; charset=utf-8")
144+ h' = contentType:filter ((/=H.hContentType) . fst) h
145+
98146-- | Create a response body consisting of a single builder.
99147streamBuilder :: Builder -> Body ()
100148streamBuilder builder write _ = write $ BuilderChunk builder
@@ -110,15 +158,25 @@ streamSimple body write flush = body (write . BuilderChunk) flush
110158
111159-- | Use a normal WAI 'Response' to send the response. Useful if you're
112160-- sharing code between HTTP\/2 applications and HTTP\/1 applications.
113−responder :: Response -> Responder
114−responder response respond = case response of
115− (ResponseFile s h path part) -> [] <$ respond s h (streamFilePart path part)
161+--
162+-- The 'Request' is used to determine the right file range to serve for
163+-- 'ResponseFile'.
164+responder :: Request -> Response -> Responder
165+responder req response respond = case response of
116166 (ResponseBuilder s h b) -> [] <$ respond s h (streamBuilder b)
117167 (ResponseStream s h body) -> [] <$ respond s h (streamSimple body)
118− (ResponseRaw _ fallback) -> responder fallback respond
168+ (ResponseRaw _ fallback) -> responder req fallback respond
169+ (ResponseFile s h path mpart) -> go respond
170+ where
171+ -- Hooray, forcing things to be universally quantified.
172+ go :: Responder
173+ go = maybe
174+ (respondFile s h path $ requestHeaders req)
175+ (respondFilePart s h path)
176+ mpart
119177
120178-- | Promote a normal WAI 'Application' to an 'HTTP2Application' by ignoring
121179-- the HTTP/2-specific features.
122180promoteApplication :: Application -> HTTP2Application
123181promoteApplication app req _ respond = [] <$ app req respond'
124− where respond' r = ResponseReceived <$ responder r respond
182+ where respond' r = ResponseReceived <$ responder req r respond
wai/Network/Wai/Internal.hs+95−2
@@ -1,6 +1,7 @@
11{-# OPTIONS_HADDOCK not-home #-}
22{-# LANGUAGE DeriveDataTypeable #-}
33{-# LANGUAGE CPP #-}
4+{-# LANGUAGE OverloadedStrings #-}
45{-# LANGUAGE RankNTypes #-}
56{-# LANGUAGE RecordWildCards #-}
67-- | Internal constructors and helper functions. Note that no guarantees are
@@ -8,14 +9,22 @@
89module Network.Wai.Internal where
910
1011import Blaze.ByteString.Builder (Builder)
11−import qualified Data.ByteString as B
12+import Control.Exception (IOException, try)
13+import qualified Data.ByteString as B hiding (pack)
14+import qualified Data.ByteString.Builder as B
15+import qualified Data.ByteString.Char8 as B (pack, readInteger)
16+import qualified Data.ByteString.Lazy as L
17+import Data.Maybe (listToMaybe)
1218import Data.Text (Text)
1319import Data.Typeable (Typeable)
14−import Data.Vault.Lazy (Vault)
20+import Data.Vault.Lazy (Vault)
1521import Data.Word (Word64)
1622import qualified Network.HTTP.Types as H
23+import qualified Network.HTTP.Types.Header as HH
1724import Network.Socket (SockAddr)
25+import Numeric (showInt)
1826import Data.List (intercalate)
27+import qualified System.PosixCompat.Files as P
1928
2029-- | Information on the request sent by the client. This abstracts away the
2130-- details of the underlying implementation.
@@ -145,3 +154,87 @@ data FilePart = FilePart
145154-- Since 3.0.0
146155data ResponseReceived = ResponseReceived
147156 deriving Typeable
157+
158+-- | Look up the size of a file in 'Right' or the 'IOException' in 'Left'.
159+tryGetFileSize :: FilePath -> IO (Either IOException Integer)
160+tryGetFileSize path =
161+ fmap (fromIntegral . P.fileSize) <$> try (P.getFileStatus path)
162+
163+-- | \"Content-Range\".
164+hContentRange :: H.HeaderName
165+hContentRange = "Content-Range"
166+
167+-- | @contentRangeHeader beg end total@ constructs a Content-Range 'H.Header'
168+-- for the range specified.
169+contentRangeHeader :: Integer -> Integer -> Integer -> H.Header
170+contentRangeHeader beg end total = (hContentRange, range)
171+ where
172+ range = B.pack
173+ -- building with ShowS
174+ $ 'b' : 'y': 't' : 'e' : 's' : ' '
175+ : (if beg > end then ('*':) else
176+ showInt beg
177+ . ('-' :)
178+ . showInt end)
179+ ( '/'
180+ : showInt total "")
181+
182+-- | Given the full size of a file and optionally a Range header value,
183+-- determine the range to serve by parsing the range header and obeying it, or
184+-- serving the whole file if it's absent or malformed.
185+chooseFilePart :: Integer -> Maybe B.ByteString -> FilePart
186+chooseFilePart size Nothing = FilePart 0 size size
187+chooseFilePart size (Just range) = case parseByteRanges range >>= listToMaybe of
188+ -- Range is broken
189+ Nothing -> FilePart 0 size size
190+ Just hrange -> checkRange hrange
191+ where
192+ checkRange (H.ByteRangeFrom beg) = fromRange beg (size - 1)
193+ checkRange (H.ByteRangeFromTo beg end) = fromRange beg (min (size - 1) end)
194+ checkRange (H.ByteRangeSuffix count) = fromRange (max 0 (size - count)) (size - 1)
195+
196+ fromRange beg end = FilePart beg (end - beg + 1) size
197+
198+-- | Adjust the given 'H.Status' and 'H.ResponseHeaders' based on the given
199+-- 'FilePart'. This means replacing the status with 206 if the response is
200+-- partial, and adding the Content-Length (always) and Content-Range (if
201+-- appropriate) headers.
202+adjustForFilePart :: H.Status -> H.ResponseHeaders -> FilePart -> (H.Status, H.ResponseHeaders)
203+adjustForFilePart s h part = (s', h'')
204+ where
205+ off = filePartOffset part
206+ len = filePartByteCount part
207+ size = filePartFileSize part
208+
209+ contentRange = contentRangeHeader off (off + len - 1) size
210+ lengthBS = L.toStrict $ B.toLazyByteString $ B.integerDec len
211+ s' = if filePartByteCount part /= size then H.partialContent206 else s
212+ h' = (H.hContentLength, lengthBS):h
213+ h'' = (if len == size then id else (contentRange:)) h'
214+
215+-- | Parse the value of a Range header into a 'HH.ByteRanges'.
216+parseByteRanges :: B.ByteString -> Maybe HH.ByteRanges
217+parseByteRanges bs1 = do
218+ bs2 <- stripPrefix "bytes=" bs1
219+ (r, bs3) <- range bs2
220+ ranges (r:) bs3
221+ where
222+ range bs2 = do
223+ (i, bs3) <- B.readInteger bs2
224+ if i < 0 -- has prefix "-" ("-0" is not valid, but here treated as "0-")
225+ then Just (HH.ByteRangeSuffix (negate i), bs3)
226+ else do
227+ bs4 <- stripPrefix "-" bs3
228+ case B.readInteger bs4 of
229+ Just (j, bs5) | j >= i -> Just (HH.ByteRangeFromTo i j, bs5)
230+ _ -> Just (HH.ByteRangeFrom i, bs4)
231+ ranges front bs3
232+ | B.null bs3 = Just (front [])
233+ | otherwise = do
234+ bs4 <- stripPrefix "," bs3
235+ (r, bs5) <- range bs4
236+ ranges (front . (r:)) bs5
237+
238+ stripPrefix x y
239+ | x `B.isPrefixOf` y = Just (B.drop (B.length x) y)
240+ | otherwise = Nothing
wai/wai.cabal+2
@@ -21,10 +21,12 @@ Source-repository head
2121Library
2222 Build-Depends: base >= 4 && < 5
2323 , bytestring >= 0.9.1.4
24+ , bytestring-builder >= 0.10.4.0 && < 10.7
2425 , blaze-builder >= 0.2.1.4 && < 0.5
2526 , network >= 2.2.1.5
2627 , http-types >= 0.7
2728 , text >= 0.7
29+ , unix-compat >= 0.2
2830 , vault >= 0.3 && < 0.4
2931 Exposed-modules: Network.Wai
3032 Network.Wai.HTTP2
warp/Network/Wai/Handler/Warp/HTTP2/Sender.hs+10−17
@@ -28,7 +28,6 @@ import Network.Wai.Handler.Warp.HTTP2.Types
2828import Network.Wai.Handler.Warp.IORef
2929import qualified Network.Wai.Handler.Warp.Settings as S
3030import Network.Wai.Handler.Warp.Types
31−import qualified System.PosixCompat.Files as P
3231
3332#ifdef WINDOWS
3433import qualified System.IO as IO
@@ -296,17 +295,16 @@ runStreamBuilder ii buf0 room0 sq = loop buf0 room0 0
296295 B.Done -> loop (buf `plusPtr` len) (room - len) total'
297296 B.More _ writer -> return (LOne writer, Nothing, total')
298297 B.Chunk bs writer -> return (LTwo bs writer, Nothing, total')
299− Just (SFile path mpart) -> do
300− (leftover, len) <- runStreamFile ii buf room path mpart
298+ Just (SFile path part) -> do
299+ (leftover, len) <- runStreamFile ii buf room path part
301300 let !total' = total + len
302301 return (leftover, Nothing, total')
303302 -- TODO if file part is done, go back to loop
304303 Just SFlush -> return (LZero, Nothing, total)
305304 Just (SFinish trailers) -> return (LZero, Just trailers, total)
306305
307−-- | Open the file, determine the range we should send, and start reading into
308−-- the send buffer.
309−runStreamFile :: InternalInfo -> Buffer -> BufSize -> FilePath -> Maybe FilePart
306+-- | Open the file and start reading into the send buffer.
307+runStreamFile :: InternalInfo -> Buffer -> BufSize -> FilePath -> FilePart
310308 -> IO (Leftover, BytesFilled)
311309
312310-- | Read the given (OS-specific) file representation into the buffer. On
@@ -316,8 +314,9 @@ runStreamFile :: InternalInfo -> Buffer -> BufSize -> FilePath -> Maybe FilePart
316314readOpenFile :: OpenFile -> Buffer -> BufSize -> Integer -> IO Int
317315
318316#ifdef WINDOWS
319−runStreamFile _ buf room path mpart = do
320− (start, bytes) <- fileStartEnd path mpart
317+runStreamFile _ buf room path part = do
318+ let start = filePartOffset part
319+ bytes = filePartByteCount part
321320 -- fixme: how to close Handle? GC does it at this moment.
322321 h <- IO.openBinaryFile path IO.ReadMode
323322 IO.hSeek h IO.AbsoluteSeek start
@@ -325,8 +324,9 @@ runStreamFile _ buf room path mpart = do
325324
326325readOpenFile h buf room _ = IO.hGetBufSome h buf room
327326#else
328−runStreamFile ii buf room path mpart = do
329− (start, bytes) <- fileStartEnd path mpart
327+runStreamFile ii buf room path part = do
328+ let start = filePartOffset part
329+ bytes = filePartByteCount part
330330 (fd, refresh) <- case fdCacher ii of
331331 Just fdcache -> getFd fdcache path
332332 Nothing -> do
@@ -355,13 +355,6 @@ fillBufFile buf room f start bytes refresh = do
355355 LZero
356356 return (leftover, len)
357357
358−fileStartEnd :: FilePath -> Maybe FilePart -> IO (Integer, Integer)
359−fileStartEnd path Nothing = do
360− end <- fromIntegral . P.fileSize <$> P.getFileStatus path
361− return (0, end)
362−fileStartEnd _ (Just part) =
363− return (filePartOffset part, filePartByteCount part)
364−
365358mini :: Int -> Integer -> Int
366359mini i n
367360 | fromIntegral i < n = i
warp/Network/Wai/Handler/Warp/HTTP2/Types.hs+1−1
@@ -100,7 +100,7 @@ data Sequence = SFinish Trailers
100100 -- ^ Any buffered data should be sent immediately.
101101 | SBuilder Builder
102102 -- ^ Append a chunk of data to the stream.
103− | SFile FilePath (Maybe FilePart)
103+ | SFile FilePath FilePart
104104 -- ^ Append a chunk of a file's contents to the stream.
105105
106106-- | A message from the sender to a stream's dedicated waiter thread.
warp/Network/Wai/Handler/Warp/RequestHeader.hs+2−28
@@ -9,7 +9,7 @@ module Network.Wai.Handler.Warp.RequestHeader (
99import Control.Exception (throwIO)
1010import Control.Monad (when)
1111import qualified Data.ByteString as S
12−import qualified Data.ByteString.Char8 as B (unpack, readInteger)
12+import qualified Data.ByteString.Char8 as B (unpack)
1313import Data.ByteString.Internal (ByteString(..), memchr)
1414import qualified Data.CaseInsensitive as CI
1515import Data.Word (Word8)
@@ -18,7 +18,7 @@ import Foreign.Ptr (Ptr, plusPtr, minusPtr, nullPtr)
1818import Foreign.Storable (peek)
1919import qualified Network.HTTP.Types as H
2020import Network.Wai.Handler.Warp.Types
21−import qualified Network.HTTP.Types.Header as HH
21+import Network.Wai.Internal (parseByteRanges)
2222-- $setup
2323-- >>> :set -XOverloadedStrings
2424
@@ -133,29 +133,3 @@ parseHeader s =
133133 let (k, rest) = S.break (== 58) s -- ':'
134134 rest' = S.dropWhile (\c -> c == 32 || c == 9) $ S.drop 1 rest
135135 in (CI.mk k, rest')
136−
137−parseByteRanges :: S.ByteString -> Maybe HH.ByteRanges
138−parseByteRanges bs1 = do
139− bs2 <- stripPrefix "bytes=" bs1
140− (r, bs3) <- range bs2
141− ranges (r:) bs3
142− where
143− range bs2 = do
144− (i, bs3) <- B.readInteger bs2
145− if i < 0 -- has prefix "-" ("-0" is not valid, but here treated as "0-")
146− then Just (HH.ByteRangeSuffix (negate i), bs3)
147− else do
148− bs4 <- stripPrefix "-" bs3
149− case B.readInteger bs4 of
150− Just (j, bs5) | j >= i -> Just (HH.ByteRangeFromTo i j, bs5)
151− _ -> Just (HH.ByteRangeFrom i, bs4)
152− ranges front bs3
153− | S.null bs3 = Just (front [])
154− | otherwise = do
155− bs4 <- stripPrefix "," bs3
156− (r, bs5) <- range bs4
157− ranges (front . (r:)) bs5
158−
159− stripPrefix x y
160− | x `S.isPrefixOf` y = Just (S.drop (S.length x) y)
161− | otherwise = Nothing
warp/Network/Wai/Handler/Warp/Response.hs+10−60
@@ -33,7 +33,7 @@ import qualified Data.ByteString.Char8 as B (pack)
3333import qualified Data.CaseInsensitive as CI
3434import Data.Function (on)
3535import Data.List (deleteBy)
36−import Data.Maybe (isJust, listToMaybe)
36+import Data.Maybe (isJust, fromMaybe)
3737#if MIN_VERSION_base(4,5,0)
3838# if __GLASGOW_HASKELL__ < 709
3939import Data.Monoid (mempty)
@@ -51,14 +51,11 @@ import qualified Network.Wai.Handler.Warp.Date as D
5151import qualified Network.Wai.Handler.Warp.FdCache as F
5252import Network.Wai.Handler.Warp.Header
5353import Network.Wai.Handler.Warp.IO (toBufIOWith)
54−import Network.Wai.Handler.Warp.RequestHeader (parseByteRanges)
5554import Network.Wai.Handler.Warp.ResponseHeader
5655import qualified Network.Wai.Handler.Warp.Timeout as T
5756import Network.Wai.Handler.Warp.Types
5857import Network.Wai.Internal
59−import Numeric (showInt)
6058import qualified Paths_warp
61−import qualified System.PosixCompat.Files as P
6259
6360#if !MIN_VERSION_base(4,5,0)
6461(<>) :: Monoid m => m -> m -> m
@@ -68,11 +65,6 @@ import qualified System.PosixCompat.Files as P
6865-- $setup
6966-- >>> :set -XOverloadedStrings
7067
71−mapRight :: (b -> c) -> Either a b -> Either a c
72−mapRight f eith = case eith of
73− Right x -> Right (f x)
74− Left l -> Left l
75−
7668----------------------------------------------------------------
7769
7870fileRange :: H.Status -> H.ResponseHeaders -> FilePath
@@ -80,8 +72,7 @@ fileRange :: H.Status -> H.ResponseHeaders -> FilePath
8072 -> IO (Either IOException
8173 (H.Status, H.ResponseHeaders, Integer, Integer))
8274fileRange s0 hs0 path Nothing mRange =
83− mapRight (fileRangeSized s0 hs0 Nothing mRange . fromIntegral . P.fileSize) <$>
84− try (P.getFileStatus path)
75+ fmap (fileRangeSized s0 hs0 Nothing mRange) <$> tryGetFileSize path
8576fileRange s0 hs0 _ mPart@(Just part) mRange =
8677 return . Right $ fileRangeSized s0 hs0 mPart mRange size
8778 where
@@ -92,40 +83,18 @@ fileRangeSized :: H.Status -> H.ResponseHeaders
9283 -> (H.Status, H.ResponseHeaders, Integer, Integer)
9384fileRangeSized s0 hs0 mPart mRange fileSize = (s, hs, beg, len)
9485 where
95− (beg, end, len, isEntire) = checkPartRange fileSize mPart mRange
96− hs1 = addContentLength len hs0
97− hs | isEntire = hs1
98− | otherwise = addContentRange beg end fileSize hs1
99− s | isEntire = s0
100− | otherwise = H.status206
86+ (beg, _end, len, _entire) = checkPartRange fileSize mPart mRange
87+ (s, hs) = adjustForFilePart s0 hs0 $ FilePart beg len fileSize
10188
10289checkPartRange :: Integer -> Maybe FilePart -> Maybe HeaderValue
10390 -> (Integer, Integer, Integer, Bool)
104−checkPartRange fileSize = checkPart
91+checkPartRange fileSize mpart mrange = (beg, end, len, isEntire)
10592 where
106− checkPart Nothing Nothing = (0, fileSize - 1, fileSize, True)
107− checkPart Nothing (Just range) = case parseByteRanges range >>= listToMaybe of
108− -- Range is broken
109− Nothing -> (0, fileSize - 1, fileSize, True)
110− Just hrange -> checkRange hrange
111− -- Ignore Range if FilePart is specified.
112− -- We assume that an application handled Range and specified
113− -- FilePart.
114− checkPart (Just part) _ = (beg, end, len, isEntire)
115− where
116− beg = filePartOffset part
117− len = filePartByteCount part
118− end = beg + len - 1
119− isEntire = beg == 0 && len == fileSize
120−
121− checkRange (H.ByteRangeFrom beg) = fromRange beg (fileSize - 1)
122− checkRange (H.ByteRangeFromTo beg end) = fromRange beg (min (fileSize - 1) end)
123− checkRange (H.ByteRangeSuffix count) = fromRange (max 0 (fileSize - count)) (fileSize - 1)
124−
125− fromRange beg end = (beg, end, len, isEntire)
126− where
127− len = end - beg + 1
128− isEntire = beg == 0 && len == fileSize
93+ part = fromMaybe (chooseFilePart fileSize mrange) mpart
94+ beg = filePartOffset part
95+ len = filePartByteCount part
96+ end = beg + len - 1
97+ isEntire = beg == 0 && len == fileSize
12998
13099----------------------------------------------------------------
131100
@@ -396,25 +365,6 @@ addAcceptRanges hdrs = (hAcceptRanges, "bytes") : hdrs
396365addTransferEncoding :: H.ResponseHeaders -> H.ResponseHeaders
397366addTransferEncoding hdrs = (hTransferEncoding, "chunked") : hdrs
398367
399−addContentLength :: Integer -> H.ResponseHeaders -> H.ResponseHeaders
400−addContentLength cl hdrs = (H.hContentLength, len) : hdrs
401− where
402− len = B.pack $ show cl
403−
404−addContentRange :: Integer -> Integer -> Integer
405− -> H.ResponseHeaders -> H.ResponseHeaders
406−addContentRange beg end total hdrs = (hContentRange, range) : hdrs
407− where
408− range = B.pack
409− -- building with ShowS
410− $ 'b' : 'y': 't' : 'e' : 's' : ' '
411− : (if beg > end then ('*':) else
412− showInt beg
413− . ('-' :)
414− . showInt end)
415− ( '/'
416− : showInt total "")
417−
418368addDate :: D.DateCache -> IndexedHeader -> H.ResponseHeaders -> IO H.ResponseHeaders
419369addDate dc rspidxhdr hdrs = case rspidxhdr ! idxDate of
420370 Nothing -> do