@@ -32,6 +32,8 @@ module Network.Wai.Application.Static
32 , defaultStaticSettings
32 , defaultStaticSettings
33 , defaultPublicSettings
33 , defaultPublicSettings
34 , CacheSettings (..)
34 , CacheSettings (..)
35+ -- should be moved to common helper
37import qualified Network.Wai as W
39import qualified Network.Wai as W
@@ -60,7 +62,7 @@ import Data.Time
60import Data.Time.Clock.POSIX
62import Data.Time.Clock.POSIX
61import System.Locale (defaultTimeLocale)
63import System.Locale (defaultTimeLocale)
63−import Data.List (sortBy, intercalate)
65+import Data.List (sortBy)
64import Data.FileEmbed (embedFile)
66import Data.FileEmbed (embedFile)
66import qualified Data.Text as T
68import qualified Data.Text as T
@@ -194,22 +196,23 @@ filterButLast f (x:xs)
194 | otherwise = filterButLast f xs
196 | otherwise = filterButLast f xs
197−unsafe :: FilePath -> Bool
199+unsafe :: T.Text -> Bool
198−unsafe ('.':_) = True
200+unsafe s | T.null s = False
199−unsafe s = any (== '/') s
201+ | T.head s == '.' = True
202+ | otherwise = T.any (== '/') s
201stripTrailingSlash :: FilePath -> FilePath
204stripTrailingSlash :: FilePath -> FilePath
202stripTrailingSlash "/" = ""
205stripTrailingSlash "/" = ""
203stripTrailingSlash "" = ""
206stripTrailingSlash "" = ""
204stripTrailingSlash (x:xs) = x : stripTrailingSlash xs
207stripTrailingSlash (x:xs) = x : stripTrailingSlash xs
206−type Pieces = [Text]
209+type Pieces = [T.Text]
207−relativeFromPieces :: Pieces -> Text
210+relativeDirFromPieces :: Pieces -> T.Text
208−relativeFromPieces pieces = concatMap (const "../") (drop 1 pieces) -- last path which is not a dir
211+relativeDirFromPieces pieces = T.concat $ map (const "../") (drop 1 pieces) -- last piece is not a dir
210pathFromPieces :: FilePath -> Pieces -> FilePath
213pathFromPieces :: FilePath -> Pieces -> FilePath
211pathFromPieces prefix pieces =
214pathFromPieces prefix pieces =
212− concat $ prefix : map ((:) '/') (map unfixPathName pieces)
215+ concat $ prefix : map ((:) '/') (map unfixPathName $ map T.unpack pieces)
214checkPieces :: FilePath -- ^ static file prefix
217checkPieces :: FilePath -- ^ static file prefix
215 -> [FilePath] -- ^ List of default index files. Cannot contain slashes.
218 -> [FilePath] -- ^ List of default index files. Cannot contain slashes.
@@ -223,15 +226,15 @@ checkPieces _ _ [".hidden", "haskell.png"] _ _ =
223 return $ SendContent "image/png" $ L.fromChunks [$(embedFile "haskell.png")]
226 return $ SendContent "image/png" $ L.fromChunks [$(embedFile "haskell.png")]
224checkPieces prefix indices pieces cache req
227checkPieces prefix indices pieces cache req
225 | any unsafe pieces = return Forbidden
228 | any unsafe pieces = return Forbidden
226− | any null $ safeInit pieces =
229+ | any T.null $ safeInit pieces =
227− return $ Redirect $ filterButLast (not . null) pieces
230+ return $ Redirect $ filterButLast (not . T.null) pieces
228 | otherwise = do
231 | otherwise = do
229 let fp = pathFromPieces prefix pieces
232 let fp = pathFromPieces prefix pieces
230 let (isFile, isFolder) =
233 let (isFile, isFolder) =
231 case () of
234 case () of
233 | null pieces -> (True, True)
236 | null pieces -> (True, True)
234− | null (last pieces) -> (False, True)
237+ | T.null (last pieces) -> (False, True)
235 | otherwise -> (True, False)
238 | otherwise -> (True, False)
237 if not isFile then uncached fp isFile isFolder
240 if not isFile then uncached fp isFile isFolder
@@ -245,7 +248,7 @@ checkPieces prefix indices pieces cache req
245 (Just hash, Just lastHash) | hash == lastHash -> return NotModified
248 (Just hash, Just lastHash) | hash == lastHash -> return NotModified
246 _ -> trace "ETAG: no cache match" uncached fp isFile isFolder
249 _ -> trace "ETAG: no cache match" uncached fp isFile isFolder
247 Forever isStaticFile ->
250 Forever isStaticFile ->
248− if isStaticFile fp (W.queryString req) &&
251+ if isStaticFile fp (S8.drop 1 $ W.rawQueryString req) &&
249 (isJust $ lookup "If-Modified-Since" (W.requestHeaders req)) &&
252 (isJust $ lookup "If-Modified-Since" (W.requestHeaders req)) &&
250 (isNothing $ lookup "If-Unmodified-Since" (W.requestHeaders req))
253 (isNothing $ lookup "If-Unmodified-Since" (W.requestHeaders req))
251 then return NotModified
254 then return NotModified
@@ -266,7 +269,7 @@ checkPieces prefix indices pieces cache req
267 x <- checkIndices fp indices
270 x <- checkIndices fp indices
268 case x of
271 case x of
269− Just index -> return $ Redirect $ setLast pieces index
272+ Just index -> return $ Redirect $ setLast pieces (T.pack index)
270 Nothing ->
273 Nothing ->
271 if isFolder
274 if isFolder
272 then return $ DirectoryResponse fp
275 then return $ DirectoryResponse fp
@@ -297,31 +300,34 @@ defaultDirListing = StaticDirListing defaultListing []
297type CheckHashParam = (FilePath -> S8.ByteString -> Bool)
300type CheckHashParam = (FilePath -> S8.ByteString -> Bool)
298data CacheSettings = NoCache | Forever CheckHashParam | ETag (FilePath -> IO (Maybe S8.ByteString))
301data CacheSettings = NoCache | Forever CheckHashParam | ETag (FilePath -> IO (Maybe S8.ByteString))
300−{-MaxAge S8.ByteString -}
301−{-maxAge :: Int -> CacheSettings-}
302−{-maxAge = MaxAge . S8.pack . show-}
304oneYear :: Int
303oneYear :: Int
305oneYear = 60 * 60 * 24 * 365
304oneYear = 60 * 60 * 24 * 365
307data StaticSettings = StaticSettings
306data StaticSettings = StaticSettings
308 { ssFolder :: FilePath
307 { ssFolder :: FilePath
309− , ssMkRedirect :: Pieces -> FilePath -> S8.ByteString
308+ , ssMkRedirect :: Pieces -> ByteString -> S8.ByteString
310 , ssGetMimeType :: FilePath -> IO MimeType
309 , ssGetMimeType :: FilePath -> IO MimeType
311 , ssDirListing :: StaticDirListing
310 , ssDirListing :: StaticDirListing
312 , ssCacheSettings :: CacheSettings
311 , ssCacheSettings :: CacheSettings
314+defaultMkRedirect :: Pieces -> ByteString -> S8.ByteString
315+defaultMkRedirect pieces newPath =
316+ let relDir = TE.encodeUtf8 (relativeDirFromPieces pieces) in
317+ S8.append relDir (if (S8.last relDir) == '/' && (S8.head newPath) == '/'
318+ then S8.tail newPath
315defaultStaticSettings :: CacheSettings -> StaticSettings
321defaultStaticSettings :: CacheSettings -> StaticSettings
316defaultStaticSettings isStaticFile = StaticSettings { ssFolder = "static"
322defaultStaticSettings isStaticFile = StaticSettings { ssFolder = "static"
317− , ssMkRedirect = \pieces newPath -> S8.pack $ (relativeFromPieces pieces) ++ newPath
323+ , ssMkRedirect = defaultMkRedirect
318 , ssGetMimeType = return . defaultMimeTypeByExt
324 , ssGetMimeType = return . defaultMimeTypeByExt
319 , ssDirListing = defaultDirListing
325 , ssDirListing = defaultDirListing
320 , ssCacheSettings = isStaticFile
326 , ssCacheSettings = isStaticFile
322defaultPublicSettings :: CacheSettings -> StaticSettings
328defaultPublicSettings :: CacheSettings -> StaticSettings
323defaultPublicSettings etags = StaticSettings { ssFolder = "public"
329defaultPublicSettings etags = StaticSettings { ssFolder = "public"
324− , ssMkRedirect = \pieces newPath -> S8.pack $ (relativeFromPieces pieces) ++ newPath
330+ , ssMkRedirect = defaultMkRedirect
325 , ssGetMimeType = return . defaultMimeTypeByExt
331 , ssGetMimeType = return . defaultMimeTypeByExt
326 , ssDirListing = ListingForbidden
332 , ssDirListing = ListingForbidden
327 , ssCacheSettings = etags
333 , ssCacheSettings = etags
@@ -333,8 +339,8 @@ staticApp set req = do
333 let pieces = W.pathInfo req
339 let pieces = W.pathInfo req
334 staticAppPieces set pieces req
340 staticAppPieces set pieces req
336−status304, statusNotModified :: W.Status
342+status304, statusNotModified :: H.Status
337−status304 = W.Status 304 "Not Modified"
343+status304 = H.Status 304 "Not Modified"
338statusNotModified = status304
344statusNotModified = status304
340staticAppPieces :: StaticSettings -> Pieces -> W.Application
346staticAppPieces :: StaticSettings -> Pieces -> W.Application
@@ -349,12 +355,12 @@ staticAppPieces ss@StaticSettings{} pieces req = liftIO $ do
349 StaticDirListing _ is -> is
355 StaticDirListing _ is -> is
350 ListingForbidden -> []
356 ListingForbidden -> []
351 cp <- checkPieces (ssFolder ss) indices pieces cache req
357 cp <- checkPieces (ssFolder ss) indices pieces cache req
352− case debug cp of
358+ case cp of
353 FileResponse fp -> do
359 FileResponse fp -> do
354 mimetype <- (ssGetMimeType ss) fp
360 mimetype <- (ssGetMimeType ss) fp
355 filesize <- fileSize `fmap` getFileStatus fp
361 filesize <- fileSize `fmap` getFileStatus fp
356 ch <- setCacheHeaders cache fp
362 ch <- setCacheHeaders cache fp
357− return $ W.ResponseFile W.status200
363+ return $ W.ResponseFile H.status200
358 ( [ ("Content-Type", mimetype)
364 ( [ ("Content-Type", mimetype)
359 , ("Content-Length", S8.pack $ show filesize)
365 , ("Content-Length", S8.pack $ show filesize)
360 ] ++ ch ) fp Nothing
366 ] ++ ch ) fp Nothing
@@ -366,22 +372,26 @@ staticAppPieces ss@StaticSettings{} pieces req = liftIO $ do
366 case ssDirListing ss of
372 case ssDirListing ss of
367 StaticDirListing f _ -> do
373 StaticDirListing f _ -> do
368 lbs <- f pieces fp
374 lbs <- f pieces fp
369− return $ W.responseLBS W.status200
375+ return $ W.responseLBS H.status200
370 [ ("Content-Type", "text/html; charset=utf-8")
376 [ ("Content-Type", "text/html; charset=utf-8")
372− ListingForbidden -> return $ W.responseLBS W.status403
378+ ListingForbidden -> return $ W.responseLBS H.status403
373 [ ("Content-Type", "text/plain")
379 [ ("Content-Type", "text/plain")
374 ] "Directory listings disabled"
380 ] "Directory listings disabled"
375 SendContent mt lbs -> do
381 SendContent mt lbs -> do
376 -- TODO: set caching headers
382 -- TODO: set caching headers
377− return $ W.responseLBS W.status200
383+ return $ W.responseLBS H.status200
378 [ ("Content-Type", mt)
384 [ ("Content-Type", mt)
379 -- TODO: set Content-Length
385 -- TODO: set Content-Length
381 Redirect pieces' -> do
387 Redirect pieces' -> do
382− let loc =
388+ let loc = (ssMkRedirect ss) pieces' $ toByteString (H.encodePathSegments pieces')
391+ -- relativeDirFromPieces pieces = T.concat $ map (const "../") (drop 1 pieces) -- last piece is not a dir
392+ -- (ssMkRedirect ss) pieces' $ encodePathInfo pieces' []
383 toByteString $
393 toByteString $
384− foldr mappend (H.encodePathSegments $ map T.pack pieces') -- FIXME use Text
394+ foldr mappend (H.encodePathSegments pieces') -- FIXME use Text
385 $ map (const $ copyByteString "../") $ drop 1 pieces
395 $ map (const $ copyByteString "../") $ drop 1 pieces
386 return $ W.responseLBS H.status301
396 return $ W.responseLBS H.status301
387 [ ("Content-Type", "text/plain")
397 [ ("Content-Type", "text/plain")
@@ -395,9 +405,9 @@ staticAppPieces ss@StaticSettings{} pieces req = liftIO $ do
395 ] "File not found"
405 ] "File not found"
397 -- expires header: formatTime "%a, %d-%b-%Y %X GMT"
407 -- expires header: formatTime "%a, %d-%b-%Y %X GMT"
398− setCacheHeaders :: CacheSettings -> FilePath -> IO W.ResponseHeaders
408+ setCacheHeaders :: CacheSettings -> FilePath -> IO H.ResponseHeaders
399 setCacheHeaders (Forever isStaticFile) fp = return $
409 setCacheHeaders (Forever isStaticFile) fp = return $
400− if isStaticFile fp (W.queryString req)
410+ if isStaticFile fp (S8.drop 1 $ W.rawQueryString req)
401 then [("Cache-Control", S8.append "max-age=" $ S8.pack $ show oneYear)]
411 then [("Cache-Control", S8.append "max-age=" $ S8.pack $ show oneYear)]
403 setCacheHeaders NoCache _ = return []
413 setCacheHeaders NoCache _ = return []
@@ -407,6 +417,16 @@ staticAppPieces ss@StaticSettings{} pieces req = liftIO $ do
407 Just hash -> [("ETag", hash)]
417 Just hash -> [("ETag", hash)]
408 Nothing -> []
418 Nothing -> []
421+The problem is that the System.Directory functions are a lie: they
422+claim to be using String, but it's really just a raw byte sequence.
423+We're assuming that non-Windows systems use UTF-8 encoding (there was
424+a discussion regarding this, it wasn't an arbitrary decision). So we
425+need to encode/decode the byte sequence to/from UTF8. That's the use
426+case for fixPathName/unfixPathName. I'm starting to use John
427+Millikin's system-filepath package for some stuff with work, and might
428+consider migrating over to it for this in the future.
410fixPathName :: FilePath -> FilePath
430fixPathName :: FilePath -> FilePath
411#if defined(mingw32_HOST_OS)
431#if defined(mingw32_HOST_OS)
412fixPathName = id
432fixPathName = id
@@ -431,7 +451,7 @@ defaultListing pieces localPath = do
431 return $ HU.renderHtml
451 return $ HU.renderHtml
432 $ H.html $ do
452 $ H.html $ do
433 H.head $ do
453 H.head $ do
434− let title = intercalate "/" pieces
454+ let title = T.unpack $ T.intercalate "/" pieces
435 let title' = if null title then "root folder" else title
455 let title' = if null title then "root folder" else title
436 H.title $ H.string title'
456 H.title $ H.string title'
437 H.style $ H.string $ unlines [ "table { margin: 0 auto; width: 760px; border-collapse: collapse; font-family: 'sans-serif'; }"
457 H.style $ H.string $ unlines [ "table { margin: 0 auto; width: 760px; border-collapse: collapse; font-family: 'sans-serif'; }"
@@ -449,10 +469,10 @@ defaultListing pieces localPath = do
449 , "a { text-decoration: none }"
469 , "a { text-decoration: none }"
451 H.body $ do
471 H.body $ do
452− H.h1 $ showFolder $ "" : filter (not . null) pieces
472+ H.h1 $ showFolder $ map T.unpack $ filter (not . T.null) pieces
453 renderDirectoryContentsTable haskellSrc folderSrc $ catMaybes fps''
473 renderDirectoryContentsTable haskellSrc folderSrc $ catMaybes fps''
455− image x = (relativeFromPieces pieces) ++ ".hidden/" ++ x ++ ".png"
475+ image x = T.unpack $ T.concat [(relativeDirFromPieces pieces), ".hidden/", x, ".png"]
456 folderSrc = image "folder"
476 folderSrc = image "folder"
457 haskellSrc = image "haskell"
477 haskellSrc = image "haskell"
458 showName "" = "root"
478 showName "" = "root"
@@ -520,6 +540,7 @@ renderDirectoryContentsTable haskellSrc folderSrc fps =
520 addCommas' (a:b:c:d:e) = a : b : c : ',' : addCommas' (d : e)
540 addCommas' (a:b:c:d:e) = a : b : c : ',' : addCommas' (d : e)
521 addCommas' x = x
541 addCommas' x = x
543+mdName' :: MetaData -> FilePath
523mdName' = fixPathName . mdName
544mdName' = fixPathName . mdName
525data MetaData =
546data MetaData =