aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorDaniel Schadt <kingdread@gmx.de>2021-06-29 00:08:09 +0200
committerDaniel Schadt <kingdread@gmx.de>2021-06-29 00:08:09 +0200
commitdce5159d2a7c8d2043a9686cbeca76fec69fac87 (patch)
tree1afb5672902c8ba20b07e23ff56b0a45915d6359 /src
parent84ca6df46909d39585d96c555b431020f1fbf9c5 (diff)
downloadCana-dce5159d2a7c8d2043a9686cbeca76fec69fac87.tar.gz
Cana-dce5159d2a7c8d2043a9686cbeca76fec69fac87.tar.bz2
Cana-dce5159d2a7c8d2043a9686cbeca76fec69fac87.zip
Add static file server
Some simple code that allows Cana to serve files from a directory, and not just provide static responses.
Diffstat (limited to 'src')
-rw-r--r--src/Cana/Extra.hs91
1 files changed, 91 insertions, 0 deletions
diff --git a/src/Cana/Extra.hs b/src/Cana/Extra.hs
new file mode 100644
index 0000000..c776160
--- /dev/null
+++ b/src/Cana/Extra.hs
@@ -0,0 +1,91 @@
+{-# LANGUAGE OverloadedStrings #-}
+-- |
+-- Module : Cana.Extra
+--
+-- This module contains some extra goodies for writing a Gemini server with
+-- "Cana", for exmaple a static file server.
+
+module Cana.Extra
+ ( -- * High-Level handlers
+ staticFiles
+ ) where
+
+import Cana.Types
+import Cana.Protocol
+
+import Control.Applicative
+import Control.Monad
+import Control.Monad.Trans.Maybe
+
+import Data.String
+import Data.Maybe
+import qualified Data.Text as T
+import System.FilePath
+import System.Directory
+import Control.Monad.IO.Class
+import qualified Data.ByteString.Lazy as BSL
+import Data.ByteString.Lazy.Char8 (pack)
+import Network.URI
+import Network.Mime
+
+
+-- | Default name for the directory index file.
+defaultIndexFile :: String
+defaultIndexFile = "index.gmi"
+
+-- | The Gemini MIME type.
+geminiMime :: IsString a => a
+geminiMime = "text/gemini"
+
+-- | A static file handler.
+--
+-- This handler takes a file path and looks up any request in the given
+-- directory.
+staticFiles :: FilePath -- ^ Base directory where static files are searched.
+ -> Handler
+staticFiles basedir request = do
+ let reqPath = sanitize . uriPath . requestUri $ request
+ fsPath = basedir ++ reqPath
+ notFoundResponse = GeminiResponse
+ { responseStatus = codeNotFound
+ , responseMeta = "Those are not the capsules you are looking for"
+ , responseData = ""
+ }
+
+ dirIndex <- liftIO $ readDirIndex fsPath
+ fileContent <- liftIO $ readFileContent fsPath
+ return $ fromMaybe notFoundResponse (dirIndex <|> fileContent)
+ where
+ sanitize :: FilePath -> FilePath
+ sanitize = joinPath
+ . filter (\p -> dropTrailingPathSeparator p /= "..")
+ . splitPath
+ . normalise
+
+ readDirIndex :: FilePath -> IO (Maybe GeminiResponse)
+ readDirIndex path = runMaybeT $ do
+ guard $ hasTrailingPathSeparator path
+ liftIO (doesDirectoryExist path) >>= guard
+ liftIO (doesFileExist $ path </> defaultIndexFile) >>= guard
+ content <- liftIO $ BSL.readFile (path </> defaultIndexFile)
+ return GeminiResponse
+ { responseStatus = codeSuccess
+ , responseMeta = geminiMime
+ , responseData = content
+ }
+
+ readFileContent :: FilePath -> IO (Maybe GeminiResponse)
+ readFileContent path = runMaybeT $ do
+ liftIO (doesFileExist path) >>= guard
+ content <- liftIO $ BSL.readFile path
+ return GeminiResponse
+ { responseStatus = codeSuccess
+ , responseMeta = getMime path
+ , responseData = content
+ }
+
+ getMime :: FilePath -> BSL.ByteString
+ getMime path
+ | takeExtension path == ".gmi" = geminiMime
+ | otherwise = BSL.fromStrict
+ . defaultMimeLookup . T.pack $ takeFileName path