on master help sign in
Add 'wai-app-static/' from commit '4d9f0d510096f4c904f33d03868758d3743caad2'

git-subtree-dir: wai-app-static git-subtree-mainline: 44589e3be4fc284043f4a8c31f9f5d9e57784556 git-subtree-split: 4d9f0d510096f4c904f33d03868758d3743caad2

commit579933168daafdf93378faf5ed69c92679ad8ce4 authorMichael Snoyman <michael@snoyman.com> dateFri 22 Jul 2011, 16:50 (15 years ago) parent44589e34d9f0d5
14 files changed+1468
wai-app-static/.gitignore+2 wai-app-static/LICENSE+25 wai-app-static/Network/Wai/Application/Static.hs+756 wai-app-static/README wai-app-static/Setup.lhs+7 wai-app-static/embedded-sample.hs+11 wai-app-static/folder.png wai-app-static/folder.svg+424 wai-app-static/haskell.png wai-app-static/sample.hs+9 wai-app-static/tests/a/b wai-app-static/tests/runtests.hs+162 wai-app-static/wai-app-static.cabal+71 wai-app-static/קרררר.html+1
wai-app-static/.gitignorenew file+2
@@ -0,0 +1,2 @@
1+dist/
2+cabal-dev
wai-app-static/LICENSEnew file+25
@@ -0,0 +1,25 @@
1+The following license covers this documentation, and the source code, except
2+where otherwise indicated.
3+
4+Copyright 2010, Michael Snoyman. All rights reserved.
5+
6+Redistribution and use in source and binary forms, with or without
7+modification, are permitted provided that the following conditions are met:
8+
9+* Redistributions of source code must retain the above copyright notice, this
10+ list of conditions and the following disclaimer.
11+
12+* Redistributions in binary form must reproduce the above copyright notice,
13+ this list of conditions and the following disclaimer in the documentation
14+ and/or other materials provided with the distribution.
15+
16+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY EXPRESS OR
17+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
18+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
19+EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,
20+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
21+NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
22+OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
23+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
24+OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
25+ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
wai-app-static/Network/Wai/Application/Static.hsnew file+756
@@ -0,0 +1,756 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+{-# LANGUAGE TemplateHaskell, CPP #-}
3+-- | Static file serving for WAI.
4+module Network.Wai.Application.Static
5+ ( -- * WAI application
6+ staticApp
7+ -- ** Settings
8+ , defaultWebAppSettings
9+ , defaultFileServerSettings
10+ , StaticSettings
11+ , ssFolder
12+ , ssMkRedirect
13+ , ssGetMimeType
14+ , ssListing
15+ , ssIndices
16+ , ssMaxAge
17+ -- * Generic, non-WAI code
18+ -- ** Mime types
19+ , MimeType
20+ , defaultMimeType
21+ -- ** Mime type by file extension
22+ , Extension
23+ , MimeMap
24+ , takeExtensions
25+ , defaultMimeTypes
26+ , mimeTypeByExt
27+ , defaultMimeTypeByExt
28+ -- ** Finding files
29+ , Pieces
30+ , pathFromPieces
31+ -- ** Directory listings
32+ , Listing
33+ , defaultListing
34+ -- ** Lookup functions
35+ , fileSystemLookup
36+ , embeddedLookup
37+ -- ** Embedded
38+ , Embedded
39+ , EmbeddedEntry (..)
40+ , toEmbedded
41+ -- ** Redirecting
42+ , defaultMkRedirect
43+ -- * Other data types
44+ , File (..)
45+ , FilePath (..)
46+ , toFilePath
47+ , fromFilePath
48+ , MaxAge (..)
49+ ) where
50+
51+import Prelude hiding (FilePath)
52+import qualified Prelude
53+import qualified Network.Wai as W
54+import qualified Network.HTTP.Types as H
55+import Data.Map (Map)
56+import qualified Data.Map as Map
57+import Data.ByteString (ByteString)
58+import System.Directory (doesFileExist, doesDirectoryExist, getDirectoryContents)
59+import qualified Data.ByteString.Char8 as S8
60+import qualified Data.ByteString.Lazy as L
61+import Data.ByteString.Lazy.Char8 ()
62+import System.PosixCompat.Files (fileSize, getFileStatus, modificationTime)
63+import System.Posix.Types (EpochTime)
64+import Control.Monad.IO.Class (liftIO)
65+import qualified Crypto.Hash.MD5 as MD5
66+import Control.Monad (filterM)
67+
68+import Text.Blaze ((!))
69+import qualified Text.Blaze.Html5 as H
70+import qualified Text.Blaze.Renderer.Utf8 as HU
71+import qualified Text.Blaze.Html5.Attributes as A
72+
73+import Blaze.ByteString.Builder (toByteString, fromByteString)
74+
75+import Data.Time
76+import Data.Time.Clock.POSIX
77+import System.Locale (defaultTimeLocale)
78+
79+import Data.FileEmbed (embedFile)
80+
81+import Data.Text (Text)
82+import qualified Data.Text as T
83+import qualified Data.Text.Encoding as TE
84+import qualified Data.Text.Encoding.Error as TEE
85+
86+import Control.Arrow ((&&&), second)
87+import Data.List (groupBy, sortBy, find, foldl')
88+import Data.Function (on)
89+import Data.Ord (comparing)
90+import qualified Data.ByteString.Base64 as B64
91+import Data.Either (rights)
92+import Data.Maybe (isJust, fromJust)
93+import Network.HTTP.Date (parseHTTPDate, epochTimeToHTTPDate, formatHTTPDate)
94+import Data.String (IsString (..))
95+
96+newtype FilePath = FilePath { unFilePath :: Text }
97+ deriving (Ord, Eq, Show)
98+instance IsString FilePath where
99+ fromString = toFilePath
100+
101+(</>) :: FilePath -> FilePath -> FilePath
102+(FilePath a) </> (FilePath b) = FilePath $ T.concat [a, "/", b]
103+
104+-- | A list of all possible extensions, starting from the largest.
105+takeExtensions :: FilePath -> [FilePath]
106+takeExtensions (FilePath s) =
107+ case T.break (== '.') s of
108+ (_, "") -> []
109+ (_, x) -> FilePath (T.drop 1 x) : takeExtensions (FilePath $ T.drop 1 x)
110+
111+type MimeType = ByteString
112+type Extension = FilePath
113+type MimeMap = Map Extension MimeType
114+
115+defaultMimeType :: MimeType
116+defaultMimeType = "application/octet-stream"
117+
118+-- taken from snap-core Snap.Util.FileServer
119+defaultMimeTypes :: MimeMap
120+defaultMimeTypes = Map.fromList [
121+ ( "apk" , "application/vnd.android.package-archive" ),
122+ ( "asc" , "text/plain" ),
123+ ( "asf" , "video/x-ms-asf" ),
124+ ( "asx" , "video/x-ms-asf" ),
125+ ( "avi" , "video/x-msvideo" ),
126+ ( "bz2" , "application/x-bzip" ),
127+ ( "c" , "text/plain" ),
128+ ( "class" , "application/octet-stream" ),
129+ ( "conf" , "text/plain" ),
130+ ( "cpp" , "text/plain" ),
131+ ( "css" , "text/css" ),
132+ ( "cxx" , "text/plain" ),
133+ ( "dtd" , "text/xml" ),
134+ ( "dvi" , "application/x-dvi" ),
135+ ( "epub" , "application/epub+zip" ),
136+ ( "gif" , "image/gif" ),
137+ ( "gz" , "application/x-gzip" ),
138+ ( "hs" , "text/plain" ),
139+ ( "htm" , "text/html" ),
140+ ( "html" , "text/html" ),
141+ ( "ico" , "image/vnd.microsoft.icon" ),
142+ ( "jar" , "application/x-java-archive" ),
143+ ( "jpeg" , "image/jpeg" ),
144+ ( "jpg" , "image/jpeg" ),
145+ ( "js" , "text/javascript" ),
146+ ( "log" , "text/plain" ),
147+ ( "m3u" , "audio/x-mpegurl" ),
148+ ( "mov" , "video/quicktime" ),
149+ ( "mp3" , "audio/mpeg" ),
150+ ( "mpeg" , "video/mpeg" ),
151+ ( "mpg" , "video/mpeg" ),
152+ ( "ogg" , "application/ogg" ),
153+ ( "pac" , "application/x-ns-proxy-autoconfig" ),
154+ ( "pdf" , "application/pdf" ),
155+ ( "png" , "image/png" ),
156+ ( "bmp" , "image/bmp" ),
157+ ( "ps" , "application/postscript" ),
158+ ( "qt" , "video/quicktime" ),
159+ ( "sig" , "application/pgp-signature" ),
160+ ( "spl" , "application/futuresplash" ),
161+ ( "svg" , "image/svg+xml" ),
162+ ( "swf" , "application/x-shockwave-flash" ),
163+ ( "tar" , "application/x-tar" ),
164+ ( "tar.bz2" , "application/x-bzip-compressed-tar" ),
165+ ( "tar.gz" , "application/x-tgz" ),
166+ ( "tbz" , "application/x-bzip-compressed-tar" ),
167+ ( "text" , "text/plain" ),
168+ ( "tgz" , "application/x-tgz" ),
169+ ( "torrent" , "application/x-bittorrent" ),
170+ ( "ttf" , "application/x-font-truetype" ),
171+ ( "txt" , "text/plain" ),
172+ ( "wav" , "audio/x-wav" ),
173+ ( "wax" , "audio/x-ms-wax" ),
174+ ( "wma" , "audio/x-ms-wma" ),
175+ ( "wmv" , "video/x-ms-wmv" ),
176+ ( "xbm" , "image/x-xbitmap" ),
177+ ( "xhtml" , "application/xhtml+xml" ),
178+ ( "xml" , "text/xml" ),
179+ ( "xpm" , "image/x-xpixmap" ),
180+ ( "xwd" , "image/x-xwindowdump" ),
181+ ( "zip" , "application/zip" )]
182+
183+mimeTypeByExt :: MimeMap
184+ -> MimeType -- ^ default mime type
185+ -> FilePath
186+ -> MimeType
187+mimeTypeByExt mm def =
188+ go . takeExtensions
189+ where
190+ go [] = def
191+ go (e:es) =
192+ case Map.lookup e mm of
193+ Nothing -> go es
194+ Just mt -> mt
195+
196+defaultMimeTypeByExt :: FilePath -> MimeType
197+defaultMimeTypeByExt = mimeTypeByExt defaultMimeTypes defaultMimeType
198+
199+data CheckPieces =
200+ -- | Just the etag hash or Nothing for no etag hash
201+ Redirect Pieces (Maybe ByteString)
202+ | Forbidden
203+ | NotFound
204+ | FileResponse File H.ResponseHeaders
205+ | NotModified
206+ | DirectoryResponse Folder
207+ -- TODO: add file size
208+ | SendContent MimeType L.ByteString
209+
210+safeInit :: [a] -> [a]
211+safeInit [] = []
212+safeInit xs = init xs
213+
214+filterButLast :: (a -> Bool) -> [a] -> [a]
215+filterButLast _ [] = []
216+filterButLast _ [x] = [x]
217+filterButLast f (x:xs)
218+ | f x = x : filterButLast f xs
219+ | otherwise = filterButLast f xs
220+
221+
222+unsafe :: FilePath -> Bool
223+unsafe (FilePath s)
224+ | T.null s = False
225+ | T.head s == '.' = True
226+ | otherwise = T.any (== '/') s
227+
228+nullFilePath :: FilePath -> Bool
229+nullFilePath = T.null . unFilePath
230+
231+stripTrailingSlash :: FilePath -> FilePath
232+stripTrailingSlash fp@(FilePath t)
233+ | T.null t || T.last t /= '/' = fp
234+ | otherwise = FilePath $ T.init t
235+
236+type Pieces = [FilePath]
237+
238+relativeDirFromPieces :: Pieces -> T.Text
239+relativeDirFromPieces pieces = T.concat $ map (const "../") (drop 1 pieces) -- last piece is not a dir
240+
241+pathFromPieces :: FilePath -> Pieces -> FilePath
242+pathFromPieces = foldl' (</>)
243+
244+checkSpecialDirListing :: Pieces -> Maybe CheckPieces
245+checkSpecialDirListing [".hidden", "folder.png"] =
246+ Just $ SendContent "image/png" $ L.fromChunks [$(embedFile "folder.png")]
247+checkSpecialDirListing [".hidden", "haskell.png"] =
248+ Just $ SendContent "image/png" $ L.fromChunks [$(embedFile "haskell.png")]
249+checkSpecialDirListing _ = Nothing
250+
251+checkPieces :: (Pieces -> IO FileLookup) -- ^ file lookup function
252+ -> [FilePath] -- ^ List of default index files. Cannot contain slashes.
253+ -> Pieces -- ^ parsed request
254+ -> W.Request
255+ -> MaxAge
256+ -> Bool
257+ -> IO CheckPieces
258+checkPieces fileLookup indices pieces req maxAge useHash
259+ | any unsafe pieces = return Forbidden
260+ | any nullFilePath $ safeInit pieces =
261+ return $ Redirect (filterButLast (not . nullFilePath) pieces) Nothing
262+ | otherwise = do
263+ let (isFile, isFolder) =
264+ case () of
265+ ()
266+ | null pieces -> (True, True)
267+ | nullFilePath (last pieces) -> (False, True)
268+ | otherwise -> (True, False)
269+
270+ fl <- fileLookup pieces
271+ case (fl, isFile) of
272+ (Nothing, _) -> return NotFound
273+ (Just (Right file), True) -> handleCache file
274+ (Just Right{}, False) -> return $ Redirect (init pieces) Nothing
275+ (Just (Left folder@(Folder _ contents)), _) -> do
276+ case checkIndices $ map fileName $ rights contents of
277+ Just index -> return $ Redirect (setLast pieces index) Nothing
278+ Nothing ->
279+ if isFolder
280+ then return $ DirectoryResponse folder
281+ else return $ Redirect (pieces ++ [""]) Nothing
282+ where
283+ headers = W.requestHeaders req
284+ queryString = W.queryString req
285+
286+ -- HTTP caching has a cache control header that you can set an expire time for a resource.
287+ -- Max-Age is easiest because it is a simple number
288+ -- a cache-control asset will only be downloaded once (if the browser maintains its cache)
289+ -- and the server will never be contacted for the resource again (until it expires)
290+ --
291+ -- A second caching mechanism is ETag and last-modified
292+ -- this form of caching is not as good as the static- the browser can avoid downloading the file, but it always need to send a request with the etag value or the last-modified value to the server to see if its copy is up to date
293+ --
294+ -- We should set a cache control and one of ETag or last-modifed whenever possible
295+ --
296+ -- In a Yesod web application we can append an etag parameter to static assets.
297+ -- This signals that both a max-age and ETag header should be set
298+ -- if there is no etag parameter
299+ -- * don't set the max-age
300+ -- * set ETag or last-modified
301+ -- * ETag must be calculated ahead of time.
302+ -- * last-modified is just the file mtime.
303+ handleCache file =
304+ if not useHash then lastModifiedCache file
305+ else do
306+ let mGetHash = fileGetHash file
307+ let etagParam = lookup "etag" queryString
308+
309+ case (etagParam, mGetHash) of
310+ (Just mEtag, Just getHash) -> do
311+ hash <- getHash
312+ if isJust mEtag && hash == fromJust mEtag
313+ then return $ FileResponse file $ ("ETag", hash):cacheControl
314+ else return $ Redirect pieces (Just hash)
315+ -- a file used to have an etag parameter, but no longer does
316+ (Just _, Nothing) -> return $ Redirect pieces Nothing
317+
318+ _ ->
319+ case (lookup "if-none-match" headers, mGetHash) of
320+ -- etag
321+ (mLastHash, Just getHash) -> do
322+ hash <- getHash
323+ case mLastHash of
324+ Just lastHash ->
325+ if hash == lastHash
326+ then return NotModified
327+ else return $ FileResponse file $ [("ETag", hash)]
328+ Nothing -> return $ FileResponse file $ [("ETag", hash)]
329+ (_, Nothing) -> lastModifiedCache file
330+
331+ lastModifiedCache file =
332+ case (lookup "if-modified-since" headers >>= parseHTTPDate, fileGetModified file) of
333+ (mLastSent, Just modified) -> do
334+ let mdate = epochTimeToHTTPDate modified in
335+ case mLastSent of
336+ Just lastSent ->
337+ if lastSent == mdate
338+ then return NotModified
339+ else return $ FileResponse file $ [("last-modified", formatHTTPDate mdate)]
340+ Nothing -> return $ FileResponse file $ [("last-modified", formatHTTPDate mdate)]
341+ _ -> return $ FileResponse file []
342+
343+ setLast :: Pieces -> FilePath -> Pieces
344+ setLast [] x = [x]
345+ setLast [""] x = [x]
346+ setLast (a:b) x = a : setLast b x
347+
348+ checkIndices :: [FilePath] -> Maybe FilePath
349+ checkIndices contents = find (flip elem indices) contents
350+
351+ cacheControl = case ccInt of
352+ Nothing -> []
353+ Just i -> [("Cache-Control", S8.append "max-age=" $ S8.pack $ show i)]
354+ where
355+ ccInt =
356+ case maxAge of
357+ NoMaxAge -> Nothing
358+ MaxAgeSeconds i -> Just i
359+ MaxAgeForever -> Just oneYear
360+ oneYear :: Int
361+ oneYear = 60 * 60 * 24 * 365
362+
363+type Listing = (Pieces -> Folder -> IO L.ByteString)
364+
365+
366+type FileLookup = Maybe (Either Folder File)
367+
368+data Folder = Folder
369+ { folderName :: FilePath
370+ , folderContents :: [Either Folder File]
371+ }
372+
373+data File = File
374+ { fileGetSize :: Int
375+ , fileToResponse :: H.Status -> H.ResponseHeaders -> W.Response
376+ , fileName :: FilePath
377+ , fileGetHash :: Maybe (IO ByteString)
378+ , fileGetModified :: Maybe EpochTime
379+ }
380+
381+data StaticSettings = StaticSettings
382+ { ssFolder :: Pieces -> IO FileLookup
383+ , ssMkRedirect :: Pieces -> ByteString -> ByteString
384+ , ssGetMimeType :: File -> IO MimeType
385+ , ssListing :: Maybe Listing
386+ , ssIndices :: [T.Text] -- index.html
387+ , ssMaxAge :: MaxAge
388+ , ssUseHash :: Bool
389+ }
390+
391+data MaxAge = NoMaxAge | MaxAgeSeconds Int | MaxAgeForever
392+
393+defaultMkRedirect :: Pieces -> ByteString -> S8.ByteString
394+defaultMkRedirect pieces newPath
395+ | S8.null newPath || S8.null relDir ||
396+ S8.last relDir /= '/' || S8.head newPath /= '/' =
397+ relDir `S8.append` newPath
398+ | otherwise = relDir `S8.append` S8.tail newPath
399+ where
400+ relDir = TE.encodeUtf8 (relativeDirFromPieces pieces)
401+
402+defaultWebAppSettings :: StaticSettings
403+defaultWebAppSettings = StaticSettings
404+ { ssFolder = fileSystemLookup "static"
405+ , ssMkRedirect = defaultMkRedirect
406+ , ssGetMimeType = return . defaultMimeTypeByExt . fileName
407+ , ssMaxAge = MaxAgeForever
408+ , ssListing = Nothing
409+ , ssIndices = []
410+ , ssUseHash = True
411+ }
412+
413+defaultFileServerSettings :: StaticSettings
414+defaultFileServerSettings = StaticSettings
415+ { ssFolder = fileSystemLookup "static"
416+ , ssMkRedirect = defaultMkRedirect
417+ , ssGetMimeType = return . defaultMimeTypeByExt . fileName
418+ , ssMaxAge = MaxAgeSeconds $ 60 * 60
419+ , ssListing = Just defaultListing
420+ , ssIndices = ["index.html", "index.htm"]
421+ , ssUseHash = False
422+ }
423+
424+fileHelper :: FilePath -> FilePath -> IO File
425+fileHelper fp name = do
426+ fs <- getFileStatus $ fromFilePath fp
427+ return File
428+ { fileGetSize = fromIntegral $ fileSize fs
429+ , fileToResponse = \s h -> W.ResponseFile s h (fromFilePath fp) Nothing
430+ , fileName = name
431+ , fileGetHash = Just $ do
432+ -- FIXME replace lazy IO with enumerators
433+ -- FIXME let's use a dictionary to cache these values?
434+ l <- L.readFile $ fromFilePath fp
435+ return $ runHashL l
436+ , fileGetModified = Just $ modificationTime fs
437+ }
438+
439+fileSystemLookup :: FilePath -> Pieces -> IO FileLookup
440+fileSystemLookup prefix pieces = do
441+ let fp = pathFromPieces prefix pieces
442+ fe <- doesFileExist $ fromFilePath fp
443+ if fe
444+ then fmap (Just . Right) $ fileHelper fp $ last pieces
445+ else do
446+ de <- doesDirectoryExist $ fromFilePath fp
447+ if de
448+ then do
449+ let isVisible ('.':_) = return False
450+ isVisible "" = return False
451+ isVisible _ = return True
452+ entries <- getDirectoryContents (fromFilePath fp) >>= filterM isVisible >>= mapM (\nameRaw -> do
453+ let name = toFilePath nameRaw
454+ let fp' = fp </> name
455+ fe' <- doesFileExist $ fromFilePath fp'
456+ if fe'
457+ then fmap Right $ fileHelper fp' name
458+ else return $ Left $ Folder name [])
459+ return $ Just $ Left $ Folder (error "413") entries
460+ else return Nothing
461+
462+type Embedded = Map.Map FilePath EmbeddedEntry
463+
464+data EmbeddedEntry = EEFile S8.ByteString | EEFolder Embedded
465+
466+embeddedLookup :: Embedded -> Pieces -> IO FileLookup
467+embeddedLookup root pieces =
468+ return $ elookup "<root>" pieces root
469+ where
470+ elookup :: FilePath -> [FilePath] -> Embedded -> FileLookup
471+ elookup p [] x = Just $ Left $ Folder p $ map toEntry $ Map.toList x
472+ elookup p [""] x = elookup p [] x
473+ elookup _ (p:ps) x =
474+ case Map.lookup p x of
475+ Nothing -> Nothing
476+ Just (EEFile f) ->
477+ case ps of
478+ [] -> Just $ Right $ bsToFile p f
479+ _ -> Nothing
480+ Just (EEFolder y) -> elookup p ps y
481+
482+toEntry :: (FilePath, EmbeddedEntry) -> Either Folder File
483+toEntry (name, EEFolder{}) = Left $ Folder name []
484+toEntry (name, EEFile bs) = Right $ File
485+ { fileGetSize = S8.length bs
486+ , fileToResponse = \s h -> W.ResponseBuilder s h $ fromByteString bs
487+ , fileName = name
488+ , fileGetHash = Just $ return $ runHash bs
489+ , fileGetModified = Nothing
490+ }
491+
492+toEmbedded :: [(Prelude.FilePath, S8.ByteString)] -> Embedded
493+toEmbedded fps =
494+ go texts
495+ where
496+ texts = map (\(x, y) -> (filter (not . T.null . unFilePath) $ toPieces x, y)) fps
497+ toPieces "" = []
498+ toPieces x =
499+ let (y, z) = break (== '/') x
500+ in toFilePath y : toPieces (drop 1 z)
501+ go :: [([FilePath], S8.ByteString)] -> Embedded
502+ go orig =
503+ Map.fromList $ map (second go') hoisted
504+ where
505+ next = map (\(x, y) -> (head x, (tail x, y))) orig
506+ grouped :: [[(FilePath, ([FilePath], S8.ByteString))]]
507+ grouped = groupBy ((==) `on` fst) $ sortBy (comparing fst) next
508+ hoisted :: [(FilePath, [([FilePath], S8.ByteString)])]
509+ hoisted = map (fst . head &&& map snd) grouped
510+ go' :: [([FilePath], S8.ByteString)] -> EmbeddedEntry
511+ go' [([], content)] = EEFile content
512+ go' x = EEFolder $ go $ filter (\y -> not $ null $ fst y) x
513+
514+bsToFile :: FilePath -> S8.ByteString -> File
515+bsToFile name bs = File
516+ { fileGetSize = S8.length bs
517+ , fileToResponse = \s h -> W.ResponseBuilder s h $ fromByteString bs
518+ , fileName = name
519+ , fileGetHash = Just $ return $ runHash bs
520+ , fileGetModified = Nothing
521+ }
522+
523+runHash :: S8.ByteString -> S8.ByteString
524+runHash = B64.encode . MD5.hash
525+
526+runHashL :: L.ByteString -> ByteString
527+runHashL = B64.encode . MD5.hashlazy
528+
529+staticApp :: StaticSettings -> W.Application
530+staticApp set req = staticAppPieces set (map FilePath $ W.pathInfo req) req
531+
532+status304, statusNotModified :: H.Status
533+status304 = H.Status 304 "Not Modified"
534+statusNotModified = status304
535+
536+-- alist helper functions
537+replace :: Eq a => a -> b -> [(a, b)] -> [(a, b)]
538+replace k v [] = [(k,v)]
539+replace k v (x:xs) | fst x == k = (k,v):xs
540+ | otherwise = x:replace k v xs
541+
542+remove :: Eq a => a -> [(a, b)] -> [(a, b)]
543+remove _ [] = []
544+remove k (x:xs) | fst x == k = xs
545+ | otherwise = x:remove k xs
546+
547+
548+staticAppPieces :: StaticSettings -> Pieces -> W.Application
549+staticAppPieces _ _ req
550+ | W.requestMethod req /= "GET" = return $ W.responseLBS
551+ H.status405
552+ [("Content-Type", "text/plain")]
553+ "Only GET is supported"
554+staticAppPieces ss pieces req = liftIO $ do
555+ let indices = ssIndices ss
556+ case checkSpecialDirListing pieces of
557+ Just res -> response res
558+ Nothing -> checkPieces (ssFolder ss) (map FilePath indices) pieces req (ssMaxAge ss) (ssUseHash ss) >>= response
559+ where
560+ response cp = case cp of
561+ FileResponse file ch -> do
562+ mimetype <- ssGetMimeType ss file
563+ let filesize = fileGetSize file
564+ let headers = ("Content-Type", mimetype)
565+ : ("Content-Length", S8.pack $ show filesize)
566+ : ch
567+ return $ fileToResponse file H.status200 headers
568+ NotModified ->
569+ return $ W.responseLBS statusNotModified
570+ [ ("Content-Type", "text/plain")
571+ ] "Not Modified"
572+ DirectoryResponse fp -> do
573+ case ssListing ss of
574+ (Just f) -> do
575+ lbs <- f pieces fp
576+ return $ W.responseLBS H.status200
577+ [ ("Content-Type", "text/html; charset=utf-8")
578+ ] lbs
579+ Nothing -> return $ W.responseLBS H.status403
580+ [ ("Content-Type", "text/plain")
581+ ] "Directory listings disabled"
582+ SendContent mt lbs -> do
583+ -- TODO: set caching headers
584+ return $ W.responseLBS H.status200
585+ [ ("Content-Type", mt)
586+ -- TODO: set Content-Length
587+ ] lbs
588+
589+ Redirect pieces' mHash -> do
590+ let loc = (ssMkRedirect ss) pieces' $ toByteString (H.encodePathSegments $ map unFilePath pieces')
591+ let qString = case mHash of
592+ Just hash -> replace "etag" (Just hash) (W.queryString req)
593+ Nothing -> remove "etag" (W.queryString req)
594+
595+ return $ W.responseLBS H.status301
596+ [ ("Content-Type", "text/plain")
597+ , ("Location", S8.append loc $ H.renderQuery True qString)
598+ ] "Redirect"
599+ Forbidden -> return $ W.responseLBS H.status403
600+ [ ("Content-Type", "text/plain")
601+ ] "Forbidden"
602+ NotFound -> return $ W.responseLBS H.status404
603+ [ ("Content-Type", "text/plain")
604+ ] "File not found"
605+
606+{-
607+The problem is that the System.Directory functions are a lie: they
608+claim to be using String, but it's really just a raw byte sequence.
609+We're assuming that non-Windows systems use UTF-8 encoding (there was
610+a discussion regarding this, it wasn't an arbitrary decision). So we
611+need to encode/decode the byte sequence to/from UTF8. That's the use
612+case for fixPathName/unfixPathName. I'm starting to use John
613+Millikin's system-filepath package for some stuff with work, and might
614+consider migrating over to it for this in the future.
615+-}
616+toFilePath :: Prelude.FilePath -> FilePath
617+#if defined(mingw32_HOST_OS)
618+toFilePath = FilePath . T.pack
619+#else
620+toFilePath = FilePath . TE.decodeUtf8With TEE.lenientDecode . S8.pack
621+#endif
622+
623+fromFilePath :: FilePath -> Prelude.FilePath
624+#if defined(mingw32_HOST_OS)
625+fromFilePath = T.unpack . unFilePath
626+#else
627+fromFilePath = S8.unpack . TE.encodeUtf8 . unFilePath
628+#endif
629+
630+-- Code below taken from Happstack: http://patch-tag.com/r/mae/happstack/snapshot/current/content/pretty/happstack-server/src/Happstack/Server/FileServe/BuildingBlocks.hs
631+defaultListing :: Listing
632+defaultListing pieces (Folder _ contents) = do
633+ let isTop = null pieces || pieces == [""]
634+ let fps'' :: [Either Folder File]
635+ fps'' = (if isTop then id else (Left (Folder ".." []) :)) contents
636+ return $ HU.renderHtml
637+ $ H.html $ do
638+ H.head $ do
639+ let title = T.unpack $ T.intercalate "/" $ map unFilePath pieces
640+ let title' = if null title then "root folder" else title
641+ H.title $ H.toHtml title'
642+ H.style $ H.toHtml $ unlines [ "table { margin: 0 auto; width: 760px; border-collapse: collapse; font-family: 'sans-serif'; }"
643+ , "table, th, td { border: 1px solid #353948; }"
644+ , "td.size { text-align: right; font-size: 0.7em; width: 50px }"
645+ , "td.date { text-align: right; font-size: 0.7em; width: 130px }"
646+ , "td { padding-right: 1em; padding-left: 1em; }"
647+ , "th.first { background-color: white; width: 24px }"
648+ , "td.first { padding-right: 0; padding-left: 0; text-align: center }"
649+ , "tr { background-color: white; }"
650+ , "tr.alt { background-color: #A3B5BA}"
651+ , "th { background-color: #3C4569; color: white; font-size: 1.125em; }"
652+ , "h1 { width: 760px; margin: 1em auto; font-size: 1em; font-family: sans-serif }"
653+ , "img { width: 20px }"
654+ , "a { text-decoration: none }"
655+ ]
656+ H.body $ do
657+ H.h1 $ showFolder $ map unFilePath $ filter (not . nullFilePath) pieces
658+ renderDirectoryContentsTable haskellSrc folderSrc fps''
659+ where
660+ image x = T.unpack $ T.concat [(relativeDirFromPieces pieces), ".hidden/", x, ".png"]
661+ folderSrc = image "folder"
662+ haskellSrc = image "haskell"
663+ showName "" = "root"
664+ showName x = x
665+ showFolder [] = "/"
666+ showFolder [x] = H.toHtml $ showName x
667+ showFolder (x:xs) = do
668+ let href = concat $ replicate (length xs) "../" :: String
669+ H.a ! A.href (H.toValue href) $ H.toHtml $ showName x
670+ " / " :: H.Html
671+ showFolder xs
672+
673+-- | a function to generate an HTML table showing the contents of a directory on the disk
674+--
675+-- This function generates most of the content of the
676+-- 'renderDirectoryContents' page. If you want to style the page
677+-- differently, or add google analytics code, etc, you can just create
678+-- a new page template to wrap around this HTML.
679+--
680+-- see also: 'getMetaData', 'renderDirectoryContents'
681+renderDirectoryContentsTable :: String
682+ -> String
683+ -> [Either Folder File]
684+ -> H.Html
685+renderDirectoryContentsTable haskellSrc folderSrc fps =
686+ H.table $ do H.thead $ do H.th ! (A.class_ "first") $ H.img ! (A.src $ H.toValue haskellSrc)
687+ H.th "Name"
688+ H.th "Modified"
689+ H.th "Size"
690+ H.tbody $ mapM_ mkRow (zip (sortBy sortMD fps) $ cycle [False, True])
691+ where
692+ sortMD :: Either Folder File -> Either Folder File -> Ordering
693+ sortMD Left{} Right{} = LT
694+ sortMD Right{} Left{} = GT
695+ sortMD (Left a) (Left b) = compare (folderName a) (folderName b)
696+ sortMD (Right a) (Right b) = compare (fileName a) (fileName b)
697+ mkRow :: (Either Folder File, Bool) -> H.Html
698+ mkRow (md, alt) =
699+ (if alt then (! A.class_ "alt") else id) $
700+ H.tr $ do
701+ H.td ! A.class_ "first"
702+ $ case md of
703+ Left{} -> H.img ! A.src (H.toValue folderSrc)
704+ ! A.alt "Folder"
705+ Right{} -> return ()
706+ let name = either folderName fileName md
707+ let isFile = either (const False) (const True) md
708+ H.td (H.a ! A.href (H.toValue $ unFilePath name `T.append` if isFile then "" else "/") $ H.toHtml $ unFilePath name)
709+ H.td ! A.class_ "date" $ H.toHtml $
710+ case md of
711+ Right File { fileGetModified = Just t } ->
712+ formatCalendarTime defaultTimeLocale "%d-%b-%Y %X" t
713+ _ -> ""
714+ H.td ! A.class_ "size" $ H.toHtml $
715+ case md of
716+ Right File { fileGetSize = s } -> prettyShow s
717+ Left{} -> ""
718+ formatCalendarTime a b c = formatTime a b $ posixSecondsToUTCTime (realToFrac c :: POSIXTime)
719+ prettyShow x
720+ | x > 1024 = prettyShowK $ x `div` 1024
721+ | otherwise = addCommas "B" x
722+ prettyShowK x
723+ | x > 1024 = prettyShowM $ x `div` 1024
724+ | otherwise = addCommas "KB" x
725+ prettyShowM x
726+ | x > 1024 = prettyShowG $ x `div` 1024
727+ | otherwise = addCommas "MB" x
728+ prettyShowG x = addCommas "GB" x
729+ addCommas s = (++ (' ' : s)) . reverse . addCommas' . reverse . show
730+ addCommas' (a:b:c:d:e) = a : b : c : ',' : addCommas' (d : e)
731+ addCommas' x = x
732+
733+{-
734+mdIsFile :: MetaData -> Bool
735+mdIsFile FileMetaData{} = True
736+mdIsFile FolderMetaData{} = False
737+
738+-- | look up the meta data associated with a file
739+getMetaData :: FilePath -- ^ path to directory on disk containing the entry
740+ -> FilePath -- ^ entry in that directory
741+ -> IO (Maybe MetaData)
742+getMetaData _ ('.':_) = return Nothing
743+getMetaData localPath fp = do
744+ let fp' = localPath ++ '/' : fp
745+ fe <- doesFileExist fp'
746+ let fpPretty = T.pack $ fixPathName fp
747+ if fe
748+ then do
749+ fs <- getFileStatus fp'
750+ let modTime = modificationTime fs
751+ let count = fileSize fs
752+ return $ Just $ FileMetaData fpPretty (Just modTime) count
753+ else do
754+ de <- doesDirectoryExist fp'
755+ return $ if de then Just (FolderMetaData fpPretty) else Nothing
756+-}
wai-app-static/READMEnew file

No changes to the content.

wai-app-static/Setup.lhsnew file+7
@@ -0,0 +1,7 @@
1+#!/usr/bin/env runhaskell
2+
3+> module Main where
4+> import Distribution.Simple
5+
6+> main :: IO ()
7+> main = defaultMain
wai-app-static/embedded-sample.hsnew file+11
@@ -0,0 +1,11 @@
1+{-# LANGUAGE TemplateHaskell #-}
2+import Network.Wai.Application.Static
3+import Network.Wai.Handler.Warp (run)
4+import Data.FileEmbed
5+
6+main :: IO ()
7+main = run 3000 $ staticApp defaultFileServerSettings
8+ { ssFolder = embeddedLookup $ toEmbedded $(embedDir ".")
9+ , ssIndices = []
10+ , ssMaxAge = NoMaxAge
11+ }
wai-app-static/folder.pngnew file

Binary file not shown.

wai-app-static/folder.svgnew file+424
@@ -0,0 +1,424 @@
1+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2+<!-- Created with Inkscape (http://www.inkscape.org/) -->
3+<svg
4+ xmlns:dc="http://purl.org/dc/elements/1.1/"
5+ xmlns:cc="http://creativecommons.org/ns#"
6+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
7+ xmlns:svg="http://www.w3.org/2000/svg"
8+ xmlns="http://www.w3.org/2000/svg"
9+ xmlns:xlink="http://www.w3.org/1999/xlink"
10+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
11+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
12+ width="48.000000px"
13+ height="48.000000px"
14+ id="svg97"
15+ sodipodi:version="0.32"
16+ inkscape:version="0.46"
17+ sodipodi:docbase="/home/jimmac/src/cvs/tango-icon-theme/scalable/places"
18+ sodipodi:docname="folder.svg"
19+ inkscape:export-filename="/home/jimmac/Desktop/horlander-style3.png"
20+ inkscape:export-xdpi="90.000000"
21+ inkscape:export-ydpi="90.000000"
22+ inkscape:output_extension="org.inkscape.output.svg.inkscape">
23+ <defs
24+ id="defs3">
25+ <inkscape:perspective
26+ sodipodi:type="inkscape:persp3d"
27+ inkscape:vp_x="0 : 24 : 1"
28+ inkscape:vp_y="0 : 1000 : 0"
29+ inkscape:vp_z="48 : 24 : 1"
30+ inkscape:persp3d-origin="24 : 16 : 1"
31+ id="perspective68" />
32+ <radialGradient
33+ inkscape:collect="always"
34+ xlink:href="#linearGradient5060"
35+ id="radialGradient6719"
36+ gradientUnits="userSpaceOnUse"
37+ gradientTransform="matrix(-2.774389,0,0,1.969706,112.7623,-872.8854)"
38+ cx="605.71429"
39+ cy="486.64789"
40+ fx="605.71429"
41+ fy="486.64789"
42+ r="117.14286" />
43+ <linearGradient
44+ inkscape:collect="always"
45+ id="linearGradient5060">
46+ <stop
47+ style="stop-color:black;stop-opacity:1;"
48+ offset="0"
49+ id="stop5062" />
50+ <stop
51+ style="stop-color:black;stop-opacity:0;"
52+ offset="1"
53+ id="stop5064" />
54+ </linearGradient>
55+ <radialGradient
56+ inkscape:collect="always"
57+ xlink:href="#linearGradient5060"
58+ id="radialGradient6717"
59+ gradientUnits="userSpaceOnUse"
60+ gradientTransform="matrix(2.774389,0,0,1.969706,-1891.633,-872.8854)"
61+ cx="605.71429"
62+ cy="486.64789"
63+ fx="605.71429"
64+ fy="486.64789"
65+ r="117.14286" />
66+ <linearGradient
67+ id="linearGradient5048">
68+ <stop
69+ style="stop-color:black;stop-opacity:0;"
70+ offset="0"
71+ id="stop5050" />
72+ <stop
73+ id="stop5056"
74+ offset="0.5"
75+ style="stop-color:black;stop-opacity:1;" />
76+ <stop
77+ style="stop-color:black;stop-opacity:0;"
78+ offset="1"
79+ id="stop5052" />
80+ </linearGradient>
81+ <linearGradient
82+ inkscape:collect="always"
83+ xlink:href="#linearGradient5048"
84+ id="linearGradient6715"
85+ gradientUnits="userSpaceOnUse"
86+ gradientTransform="matrix(2.774389,0,0,1.969706,-1892.179,-872.8854)"
87+ x1="302.85715"
88+ y1="366.64789"
89+ x2="302.85715"
90+ y2="609.50507" />
91+ <linearGradient
92+ inkscape:collect="always"
93+ id="linearGradient9806">
94+ <stop
95+ style="stop-color:#000000;stop-opacity:1;"
96+ offset="0"
97+ id="stop9808" />
98+ <stop
99+ style="stop-color:#000000;stop-opacity:0;"
100+ offset="1"
101+ id="stop9810" />
102+ </linearGradient>
103+ <linearGradient
104+ id="linearGradient9766">
105+ <stop
106+ style="stop-color:#6194cb;stop-opacity:1;"
107+ offset="0"
108+ id="stop9768" />
109+ <stop
110+ style="stop-color:#729fcf;stop-opacity:1;"
111+ offset="1"
112+ id="stop9770" />
113+ </linearGradient>
114+ <linearGradient
115+ id="linearGradient3096">
116+ <stop
117+ id="stop3098"
118+ offset="0"
119+ style="stop-color:#424242;stop-opacity:1;" />
120+ <stop
121+ id="stop3100"
122+ offset="1.0000000"
123+ style="stop-color:#777777;stop-opacity:1.0000000;" />
124+ </linearGradient>
125+ <linearGradient
126+ id="linearGradient319"
127+ inkscape:collect="always">
128+ <stop
129+ id="stop320"
130+ offset="0"
131+ style="stop-color:#ffffff;stop-opacity:1;" />
132+ <stop
133+ id="stop321"
134+ offset="1"
135+ style="stop-color:#ffffff;stop-opacity:0;" />
136+ </linearGradient>
137+ <linearGradient
138+ id="linearGradient1789">
139+ <stop
140+ style="stop-color:#202020;stop-opacity:1.0000000;"
141+ offset="0.0000000"
142+ id="stop1790" />
143+ <stop
144+ style="stop-color:#b9b9b9;stop-opacity:1.0000000;"
145+ offset="1.0000000"
146+ id="stop1791" />
147+ </linearGradient>
148+ <radialGradient
149+ inkscape:collect="always"
150+ xlink:href="#linearGradient1789"
151+ id="radialGradient238"
152+ gradientUnits="userSpaceOnUse"
153+ gradientTransform="matrix(1.055022,-2.734504e-2,0.177703,1.190929,-3.572177,-7.125301)"
154+ cx="20.706017"
155+ cy="37.517986"
156+ fx="20.706017"
157+ fy="37.517986"
158+ r="30.905205" />
159+ <linearGradient
160+ id="linearGradient3983">
161+ <stop
162+ style="stop-color:#ffffff;stop-opacity:0.87628865;"
163+ offset="0.0000000"
164+ id="stop3984" />
165+ <stop
166+ style="stop-color:#fffffe;stop-opacity:0.0000000;"
167+ offset="1.0000000"
168+ id="stop3985" />
169+ </linearGradient>
170+ <linearGradient
171+ inkscape:collect="always"
172+ xlink:href="#linearGradient3983"
173+ id="linearGradient491"
174+ gradientUnits="userSpaceOnUse"
175+ gradientTransform="matrix(1.516844,0.000000,0.000000,0.708978,-0.879573,-1.318166)"
176+ x1="6.2297964"
177+ y1="13.773066"
178+ x2="9.8980894"
179+ y2="66.834053" />
180+ <linearGradient
181+ gradientUnits="userSpaceOnUse"
182+ y2="46.689312"
183+ x2="12.853771"
184+ y1="32.567184"
185+ x1="13.035696"
186+ gradientTransform="matrix(1.317489,0.000000,0.000000,0.816256,-0.879573,-1.318166)"
187+ id="linearGradient322"
188+ xlink:href="#linearGradient319"
189+ inkscape:collect="always" />
190+ <linearGradient
191+ gradientUnits="userSpaceOnUse"
192+ y2="6.1802502"
193+ x2="15.514889"
194+ y1="31.367750"
195+ x1="18.112709"
196+ id="linearGradient3104"
197+ xlink:href="#linearGradient3096"
198+ inkscape:collect="always" />
199+ <linearGradient
200+ inkscape:collect="always"
201+ xlink:href="#linearGradient9766"
202+ id="linearGradient9772"
203+ x1="22.175976"
204+ y1="36.987999"
205+ x2="22.065331"
206+ y2="32.050499"
207+ gradientUnits="userSpaceOnUse" />
208+ <radialGradient
209+ inkscape:collect="always"
210+ xlink:href="#linearGradient9806"
211+ id="radialGradient9812"
212+ cx="24.35099"
213+ cy="41.591846"
214+ fx="24.35099"
215+ fy="41.591846"
216+ r="19.136078"
217+ gradientTransform="matrix(1.000000,0.000000,0.000000,0.242494,1.565588e-16,31.50606)"
218+ gradientUnits="userSpaceOnUse" />
219+ </defs>
220+ <sodipodi:namedview
221+ fill="#729fcf"
222+ id="base"
223+ pagecolor="#ffffff"
224+ bordercolor="#666666"
225+ borderopacity="0.10196078"
226+ inkscape:pageopacity="0.0"
227+ inkscape:pageshadow="2"
228+ inkscape:zoom="4"
229+ inkscape:cx="54.359127"
230+ inkscape:cy="-13.803699"
231+ inkscape:current-layer="layer1"
232+ showgrid="false"
233+ inkscape:grid-bbox="true"
234+ inkscape:document-units="px"
235+ inkscape:window-width="1026"
236+ inkscape:window-height="818"
237+ inkscape:window-x="169"
238+ inkscape:window-y="30"
239+ inkscape:showpageshadow="false"
240+ stroke="#3465a4" />
241+ <metadata
242+ id="metadata4">
243+ <rdf:RDF>
244+ <cc:Work
245+ rdf:about="">
246+ <dc:format>image/svg+xml</dc:format>
247+ <dc:type
248+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
249+ <dc:title>Folder Icon</dc:title>
250+ <dc:date />
251+ <dc:creator>
252+ <cc:Agent>
253+ <dc:title>Jakub Steiner</dc:title>
254+ </cc:Agent>
255+ </dc:creator>
256+ <cc:license
257+ rdf:resource="http://creativecommons.org/licenses/publicdomain/" />
258+ <dc:source>http://jimmac.musichall.cz</dc:source>
259+ <dc:subject>
260+ <rdf:Bag>
261+ <rdf:li>folder</rdf:li>
262+ <rdf:li>directory</rdf:li>
263+ </rdf:Bag>
264+ </dc:subject>
265+ </cc:Work>
266+ <cc:License
267+ rdf:about="http://creativecommons.org/licenses/publicdomain/">
268+ <cc:permits
269+ rdf:resource="http://creativecommons.org/ns#Reproduction" />
270+ <cc:permits
271+ rdf:resource="http://creativecommons.org/ns#Distribution" />
272+ <cc:permits
273+ rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
274+ </cc:License>
275+ </rdf:RDF>
276+ </metadata>
277+ <g
278+ id="layer1"
279+ inkscape:label="Folder"
280+ inkscape:groupmode="layer">
281+ <g
282+ style="display:inline"
283+ transform="matrix(2.262383e-2,0,0,2.086758e-2,43.38343,36.36962)"
284+ id="g6707">
285+ <rect
286+ style="opacity:0.40206185;color:black;fill:url(#linearGradient6715);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
287+ id="rect6709"
288+ width="1339.6335"
289+ height="478.35718"
290+ x="-1559.2523"
291+ y="-150.69685" />
292+ <path
293+ style="opacity:0.40206185;color:black;fill:url(#radialGradient6717);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
294+ d="M -219.61876,-150.68038 C -219.61876,-150.68038 -219.61876,327.65041 -219.61876,327.65041 C -76.744594,328.55086 125.78146,220.48075 125.78138,88.454235 C 125.78138,-43.572302 -33.655436,-150.68036 -219.61876,-150.68038 z "
295+ id="path6711"
296+ sodipodi:nodetypes="cccc" />
297+ <path
298+ sodipodi:nodetypes="cccc"
299+ id="path6713"
300+ d="M -1559.2523,-150.68038 C -1559.2523,-150.68038 -1559.2523,327.65041 -1559.2523,327.65041 C -1702.1265,328.55086 -1904.6525,220.48075 -1904.6525,88.454235 C -1904.6525,-43.572302 -1745.2157,-150.68036 -1559.2523,-150.68038 z "
301+ style="opacity:0.40206185;color:black;fill:url(#radialGradient6719);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" />
302+ </g>
303+ <path
304+ d="M 4.5217805,38.687417 C 4.5435766,39.103721 4.9816854,39.520026 5.3979900,39.520026 L 36.725011,39.520026 C 37.141313,39.520026 37.535823,39.103721 37.514027,38.687417 L 36.577584,11.460682 C 36.555788,11.044379 36.117687,10.628066 35.701383,10.628066 L 22.430510,10.628066 C 21.945453,10.628066 21.196037,10.312477 21.028866,9.5214338 L 20.417475,6.6283628 C 20.262006,5.8926895 19.535261,5.5904766 19.118957,5.5904766 L 4.3400975,5.5904766 C 3.9237847,5.5904766 3.5292767,6.0067807 3.5510726,6.4230849 L 4.5217805,38.687417 z "
305+ id="path216"
306+ style="fill:url(#radialGradient238);fill-opacity:1.0000000;fill-rule:nonzero;stroke:url(#linearGradient3104);stroke-width:1.0000000;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4.0000000;stroke-dasharray:none;stroke-opacity:1.0000000"
307+ sodipodi:nodetypes="ccccccssssccc" />
308+ <path
309+ sodipodi:nodetypes="cc"
310+ id="path9788"
311+ d="M 5.2265927,22.5625 L 35.492173,22.5625"
312+ style="opacity:0.11363633;color:#000000;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00000024;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" />
313+ <path
314+ sodipodi:nodetypes="cc"
315+ id="path9784"
316+ d="M 5.0421736,18.5625 L 35.489104,18.5625"
317+ style="opacity:0.11363633;color:#000000;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00000036;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" />
318+ <path
319+ style="opacity:0.11363633;color:#000000;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
320+ d="M 4.9806965,12.5625 L 35.488057,12.5625"
321+ id="path9778"
322+ sodipodi:nodetypes="cc" />
323+ <path
324+ style="opacity:0.11363633;color:#000000;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00000036;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
325+ d="M 5.3861577,32.5625 L 35.494881,32.5625"
326+ id="path9798"
327+ sodipodi:nodetypes="cc" />
328+ <path
329+ sodipodi:nodetypes="cc"
330+ id="path9800"
331+ d="M 5.5091398,34.5625 L 35.496893,34.5625"
332+ style="opacity:0.11363633;color:#000000;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00000024;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" />
333+ <path
334+ style="opacity:0.11363633;color:#000000;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00000036;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
335+ d="M 5.0421736,16.5625 L 35.489104,16.5625"
336+ id="path9782"
337+ sodipodi:nodetypes="cc" />
338+ <path
339+ sodipodi:nodetypes="cc"
340+ id="path9780"
341+ d="M 5.0114345,14.5625 L 35.48858,14.5625"
342+ style="opacity:0.11363633;color:#000000;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00000024;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" />
343+ <path
344+ sodipodi:nodetypes="cc"
345+ id="path9776"
346+ d="M 4.9220969,10.5625 L 20.202912,10.5625"
347+ style="opacity:0.11363633;color:#000000;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00000024;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" />
348+ <path
349+ style="opacity:0.11363633;color:#000000;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.99999982;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
350+ d="M 4.8737534,8.5624999 L 19.657487,8.5624999"
351+ id="path9774"
352+ sodipodi:nodetypes="cc" />
353+ <path
354+ style="opacity:0.11363633;color:#000000;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00000036;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
355+ d="M 5.3246666,28.5625 L 35.493876,28.5625"
356+ id="path9794"
357+ sodipodi:nodetypes="cc" />
358+ <path
359+ sodipodi:nodetypes="cc"
360+ id="path9792"
361+ d="M 5.2880638,26.5625 L 35.493184,26.5625"
362+ style="opacity:0.11363633;color:#000000;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" />
363+ <path
364+ style="opacity:0.11363633;color:#000000;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00000024;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
365+ d="M 5.2265927,24.5625 L 35.492173,24.5625"
366+ id="path9790"
367+ sodipodi:nodetypes="cc" />
368+ <path
369+ style="opacity:0.11363633;color:#000000;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00000012;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
370+ d="M 5.1958537,20.5625 L 35.491649,20.5625"
371+ id="path9786"
372+ sodipodi:nodetypes="cc" />
373+ <path
374+ sodipodi:nodetypes="cc"
375+ id="path9796"
376+ d="M 5.3246666,30.5625 L 35.493876,30.5625"
377+ style="opacity:0.11363633;color:#000000;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00000036;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" />
378+ <path
379+ style="opacity:0.11363633;color:#000000;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.00000024;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
380+ d="M 5.5091398,36.5625 L 35.496893,36.5625"
381+ id="path9802"
382+ sodipodi:nodetypes="cc" />
383+ <path
384+ style="color:#000000;fill:url(#linearGradient491);fill-opacity:1.0000000;fill-rule:nonzero;stroke:none;stroke-width:1.2138050;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4.0000000;stroke-dashoffset:0.0000000;stroke-opacity:1.0000000;visibility:visible;display:block;overflow:visible;opacity:0.45142857"
385+ d="M 6.0683430,38.864023 C 6.0846856,39.176251 5.8874317,39.384402 5.5697582,39.280326 L 5.5697582,39.280326 C 5.2520766,39.176251 5.0330270,38.968099 5.0166756,38.655870 L 4.0689560,6.5913839 C 4.0526131,6.2791558 4.2341418,6.0906134 4.5463699,6.0906134 L 18.968420,6.0429196 C 19.280648,6.0429196 19.900363,6.3433923 20.101356,7.3651014 L 20.674845,10.180636 C 20.247791,9.7153790 20.255652,9.7010175 20.037287,9.0239299 L 19.631192,7.7647478 C 19.412142,7.0371009 18.932991,6.9328477 18.620763,6.9328477 L 5.7329889,6.9328477 C 5.4207613,6.9328477 5.2235075,7.1409999 5.2398583,7.4532364 L 6.1778636,38.968099 L 6.0683430,38.864023 z "
386+ id="path219"
387+ sodipodi:nodetypes="cccccccccscccccc" />
388+ <g
389+ style="stroke-miterlimit:4.0000000;stroke-width:0.99946535;stroke:none;fill-rule:nonzero;fill-opacity:0.75706214;fill:#ffffff"
390+ id="g220"
391+ transform="matrix(1.040764,0.000000,5.449252e-2,1.040764,-8.670199,2.670594)"
392+ inkscape:export-filename="/home/jimmac/ximian_art/icons/nautilus/suse93/gnome-fs-directory.png"
393+ inkscape:export-xdpi="74.800003"
394+ inkscape:export-ydpi="74.800003">
395+ <path
396+ style="fill-opacity:0.50847459;fill:#ffffff"
397+ d="M 42.417183,8.5151772 C 42.422267,8.4180642 42.289022,8.2681890 42.182066,8.2681716 L 29.150665,8.2660527 C 29.150665,8.2660527 30.062379,8.8540072 31.352477,8.8622963 L 42.405974,8.9333167 C 42.417060,8.7215889 42.408695,8.6772845 42.417183,8.5151772 z "
398+ id="path221"
399+ sodipodi:nodetypes="cscscs" />
400+ </g>
401+ <path
402+ style="color:#000000;fill:url(#linearGradient9772);fill-opacity:1.0;fill-rule:nonzero;stroke:#3465a4;stroke-width:1.0000000;stroke-linecap:butt;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4.0000000;stroke-dasharray:none;stroke-dashoffset:0.0000000;stroke-opacity:1;visibility:visible;display:block"
403+ d="M 39.783532,39.510620 C 40.927426,39.466556 41.746608,38.414321 41.830567,37.189615 C 42.622354,25.640928 43.489927,15.957666 43.489927,15.957666 C 43.562082,15.710182 43.322016,15.462699 43.009787,15.462699 L 8.6386304,15.462699 C 8.6386304,15.462699 6.7883113,37.329591 6.7883113,37.329591 C 6.6737562,38.311657 6.3223038,39.134309 5.2384755,39.513304 L 39.783532,39.510620 z "
404+ id="path233"
405+ sodipodi:nodetypes="cscccscc"
406+ inkscape:export-filename="/home/jimmac/ximian_art/icons/nautilus/suse93/gnome-fs-directory.png"
407+ inkscape:export-xdpi="74.800003"
408+ inkscape:export-ydpi="74.800003" />
409+ <path
410+ sodipodi:nodetypes="ccsscsc"
411+ id="path304"
412+ d="M 9.6202444,16.463921 L 42.411343,16.528735 L 40.837297,36.530714 C 40.752975,37.602225 40.386619,37.958929 38.964641,37.958929 C 37.093139,37.958929 10.286673,37.926522 7.569899,37.926522 C 7.8034973,37.605711 7.9036547,36.937899 7.9049953,36.92191 L 9.6202444,16.463921 z "
413+ style="opacity:0.46590909;fill:none;fill-opacity:1.0000000;fill-rule:evenodd;stroke:url(#linearGradient322);stroke-width:0.99999970px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1.0000000" />
414+ <path
415+ sodipodi:nodetypes="ccccc"
416+ id="path323"
417+ d="M 9.6202481,16.223182 L 8.4536014,31.866453 C 8.4536014,31.866453 16.749756,27.718375 27.119949,27.718375 C 37.490142,27.718375 42.675239,16.223182 42.675239,16.223182 L 9.6202481,16.223182 z "
418+ style="fill:#ffffff;fill-opacity:0.089285679;fill-rule:evenodd;stroke:none;stroke-width:1.0000000px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1.0000000" />
419+ </g>
420+ <g
421+ inkscape:groupmode="layer"
422+ id="layer2"
423+ inkscape:label="pattern" />
424+</svg>
wai-app-static/haskell.pngnew file

Binary file not shown.

wai-app-static/sample.hsnew file+9
@@ -0,0 +1,9 @@
1+import Network.Wai.Application.Static
2+import Network.Wai.Handler.Warp (run)
3+
4+main :: IO ()
5+main = run 3000 $ staticApp defaultStaticSettings
6+ { ssFolder = fileSystemLookup "."
7+ , ssMaxAge = MaxAgeForever
8+ , ssIndices = []
9+ }
wai-app-static/tests/a/bnew file

No changes to the content.

wai-app-static/tests/runtests.hsnew file+162
@@ -0,0 +1,162 @@
1+{-# LANGUAGE OverloadedStrings, NoMonomorphismRestriction #-}
2+import Network.Wai.Application.Static
3+
4+import Test.Hspec.Monadic
5+import Test.Hspec.QuickCheck
6+import Test.Hspec.HUnit ()
7+import Test.HUnit ((@?=), assert)
8+import Distribution.Simple.Utils (isInfixOf)
9+import qualified Data.ByteString.Char8 as S8
10+import qualified Data.ByteString.Lazy.Char8 as L8
11+import qualified Data.Text as T
12+import qualified Data.Text.Encoding as TE
13+import System.PosixCompat.Files (getFileStatus, modificationTime)
14+
15+import Network.HTTP.Date
16+{-import System.Locale (defaultTimeLocale)-}
17+{-import Data.Time.Format (formatTime)-}
18+
19+import Network.Wai
20+import Network.Wai.Test
21+
22+import Network.Socket.Internal as Sock
23+import qualified Network.HTTP.Types as H
24+import Control.Monad.IO.Class (liftIO)
25+
26+defRequest :: Request
27+defRequest = Request {
28+ rawQueryString = ""
29+, queryString = []
30+, requestMethod = "GET"
31+, rawPathInfo = ""
32+, pathInfo = []
33+, requestHeaders = []
34+, serverName = "wai-test"
35+, httpVersion = H.http11
36+, serverPort = 80
37+, isSecure = False
38+, remoteHost = Sock.SockAddrInet 1 2
39+}
40+
41+setRawPathInfo :: Request -> S8.ByteString -> Request
42+setRawPathInfo r rawPinfo =
43+ let pInfo = T.split (== '/') $ TE.decodeUtf8 rawPinfo
44+ in r { rawPathInfo = rawPinfo, pathInfo = pInfo }
45+
46+
47+main :: IO a
48+main = hspecX $ do
49+ let must = liftIO . assert
50+
51+ let webApp = flip runSession $ staticApp defaultWebAppSettings {ssFolder = fileSystemLookup "tests"}
52+ let fileServerApp = flip runSession $ staticApp defaultFileServerSettings {ssFolder = fileSystemLookup "tests"}
53+
54+ let etag = "1B2M2Y8AsgTpgAmY7PhCfg=="
55+ let file = "a/b"
56+ let statFile = setRawPathInfo defRequest file
57+
58+
59+ describe "Pieces: pathFromPieces" $ do
60+ it "converts to a file path" $
61+ (pathFromPieces "prefix" ["a", "bc"]) @?= "prefix/a/bc"
62+
63+ prop "each piece is in file path" $ \piecesS ->
64+ let pieces = map (FilePath . T.pack) piecesS
65+ in all (\p -> ("/" ++ p) `isInfixOf` (T.unpack $ unFilePath $ pathFromPieces "root" $ pieces)) piecesS
66+
67+ describe "webApp" $ do
68+ it "403 for unsafe paths" $ webApp $
69+ flip mapM_ ["..", "."] $ \path ->
70+ assertStatus 403 =<<
71+ request (setRawPathInfo defRequest path)
72+
73+ it "200 for hidden paths" $ webApp $
74+ flip mapM_ [".hidden/folder.png", ".hidden/haskell.png"] $ \path ->
75+ assertStatus 200 =<<
76+ request (setRawPathInfo defRequest path)
77+
78+ it "404 for non-existant files" $ webApp $
79+ assertStatus 404 =<<
80+ request (setRawPathInfo defRequest "doesNotExist")
81+
82+ it "301 redirect when multiple slashes" $ webApp $ do
83+ req <- request (setRawPathInfo defRequest "a//b/c")
84+ assertStatus 301 req
85+ assertHeader "Location" "../../a/b/c" req
86+
87+ let absoluteApp = flip runSession $ staticApp $ defaultWebAppSettings {
88+ ssFolder = fileSystemLookup "tests", ssMkRedirect = \_ u -> S8.append "http://www.example.com" u
89+ }
90+ it "301 redirect when multiple slashes" $ absoluteApp $
91+ flip mapM_ ["/a//b/c", "a//b/c"] $ \path -> do
92+ req <- request (setRawPathInfo defRequest path)
93+ assertStatus 301 req
94+ assertHeader "Location" "http://www.example.com/a/b/c" req
95+
96+ describe "webApp when requesting a static asset" $ do
97+ it "200 and etag when no etag query parameters" $ webApp $ do
98+ req <- request statFile
99+ assertStatus 200 req
100+ assertNoHeader "Cache-Control" req
101+ assertHeader "ETag" etag req
102+ assertNoHeader "Last-Modified" req
103+
104+ it "200 when no cache headers and bad cache query string" $ webApp $ do
105+ flip mapM_ [Just "cached", Nothing] $ \badETag -> do
106+ req <- request statFile { queryString = [("etag", badETag)] }
107+ assertStatus 301 req
108+ assertHeader "Location" "../a/b?etag=1B2M2Y8AsgTpgAmY7PhCfg%3D%3D" req
109+ assertNoHeader "Cache-Control" req
110+ assertNoHeader "Last-Modified" req
111+
112+ it "Cache-Control set when etag parameter is correct" $ webApp $ do
113+ req <- request statFile { queryString = [("etag", Just etag)] }
114+ assertStatus 200 req
115+ assertHeader "Cache-Control" "max-age=31536000" req
116+ assertNoHeader "Last-Modified" req
117+
118+ it "200 when invalid in-none-match sent" $ webApp $
119+ flip mapM_ ["cached", ""] $ \badETag -> do
120+ req <- request statFile { requestHeaders = [("If-None-Match", badETag)] }
121+ assertStatus 200 req
122+ assertHeader "ETag" etag req
123+ assertNoHeader "Last-Modified" req
124+
125+ it "304 when valid if-none-match sent" $ webApp $ do
126+ req <- request statFile { requestHeaders = [("If-None-Match", etag)] }
127+ assertStatus 304 req
128+ assertNoHeader "Etag" req
129+ assertNoHeader "Last-Modified" req
130+
131+ describe "fileServerApp" $ do
132+ let fileDate = do
133+ stat <- liftIO $ getFileStatus $ "tests/" ++ file
134+ return $ formatHTTPDate . epochTimeToHTTPDate $ modificationTime stat
135+
136+ it "directory listing for index" $ fileServerApp $ do
137+ resp <- request (setRawPathInfo defRequest "a/")
138+ assertStatus 200 resp
139+ let body = simpleBody resp
140+ let contains a b = isInfixOf b (L8.unpack a)
141+ must $ body `contains` "<img src=\"../.hidden/haskell.png\" />"
142+ must $ body `contains` "<img src=\"../.hidden/folder.png\" alt=\"Folder\" />"
143+ must $ body `contains` "<a href=\"b\">b</a>"
144+
145+ it "200 when invalid if-modified-since header" $ fileServerApp $ do
146+ flip mapM_ ["123", ""] $ \badDate -> do
147+ req <- request statFile {
148+ requestHeaders = [("If-Modified-Since", badDate)]
149+ }
150+ assertStatus 200 req
151+ assertNoHeader "Cache-Control" req
152+ fdate <- fileDate
153+ assertHeader "Last-Modified" fdate req
154+
155+ it "304 when if-modified-since matches" $ fileServerApp $ do
156+ fdate <- fileDate
157+ req <- request statFile {
158+ requestHeaders = [("If-Modified-Since", fdate)]
159+ }
160+ assertStatus 304 req
161+ assertNoHeader "Cache-Control" req
162+
wai-app-static/wai-app-static.cabalnew file+71
@@ -0,0 +1,71 @@
1+name: wai-app-static
2+version: 0.3.0
3+license: BSD3
4+license-file: LICENSE
5+author: Michael Snoyman <michael@snoyman.com>
6+maintainer: Michael Snoyman <michael@snoyman.com>
7+synopsis: WAI application for static serving
8+description: Also provides some helper functions and datatypes for use outside of WAI.
9+category: Web, Yesod
10+stability: Stable
11+cabal-version: >= 1.8
12+build-type: Simple
13+homepage: http://www.yesodweb.com/
14+Extra-source-files: folder.png, haskell.png
15+
16+Flag print
17+ Description: print debug info
18+ Default: False
19+
20+library
21+ build-depends: base >= 4 && < 5
22+ , wai >= 0.4 && < 0.5
23+ , bytestring >= 0.9.1.4 && < 0.10
24+ , http-types >= 0.6 && < 0.7
25+ , transformers >= 0.2.2 && < 0.3
26+ , unix-compat >= 0.2 && < 0.3
27+ , directory >= 1.0 && < 1.2
28+ , containers >= 0.2 && < 0.5
29+ , blaze-html >= 0.4 && < 0.5
30+ , time >= 1.1.4 && < 1.3
31+ , old-locale >= 1.0.0.2 && < 1.1
32+ , file-embed >= 0.0.3.1 && < 0.1
33+ , text >= 0.5 && < 1.0
34+ , blaze-builder >= 0.2.1.4 && < 0.4
35+ , base64-bytestring >= 0.1 && < 0.2
36+ , cryptohash >= 0.7 && < 0.8
37+ , http-date
38+ exposed-modules: Network.Wai.Application.Static
39+ ghc-options: -Wall
40+ extensions: CPP
41+
42+ if flag(print)
43+ cpp-options: -DPRINT
44+
45+test-suite runtests
46+ hs-source-dirs: tests
47+ main-is: runtests.hs
48+ type: exitcode-stdio-1.0
49+
50+ build-depends: base >= 4 && < 5
51+ , hspec >= 0.6
52+ , HUnit
53+ , unix-compat >= 0.2 && < 0.3
54+ , time >= 1.1.4 && < 1.3
55+ , old-locale >= 1.0.0.2 && < 1.1
56+ , http-date
57+ , Cabal
58+ , wai-app-static >= 0.3
59+ , wai-test
60+ , wai
61+ , http-types
62+ , network
63+ , bytestring
64+ , text
65+ , transformers
66+ -- , containers
67+ ghc-options: -Wall
68+
69+source-repository head
70+ type: git
71+ location: git://github.com/snoyberg/wai-app-static.git
wai-app-static/קרררר.htmlnew file+1
@@ -0,0 +1 @@
1+<h1>HELLO WORLD</h1>