6 import Codec.Compression.GZip
7 import Control.Applicative
9 import qualified Data.Ascii as A
10 import qualified Data.ByteString.Base64 as B64
11 import qualified Data.ByteString.Char8 as Strict
12 import qualified Data.ByteString.Lazy as Lazy
14 import Data.Digest.Pure.SHA
18 import Data.Time.Clock.POSIX
19 import Language.Haskell.Exts.Build
20 import Language.Haskell.Exts.Extension
21 import Language.Haskell.Exts.Pretty
22 import Language.Haskell.Exts.Syntax
23 import Network.HTTP.Lucu.ETag
24 import Network.HTTP.Lucu.MIMEType
25 import Network.HTTP.Lucu.MIMEType.DefaultExtensionMap
26 import Network.HTTP.Lucu.MIMEType.Guess
27 import Prelude.Unicode
28 import System.Console.GetOpt
29 import System.Environment
31 import System.Posix.Files
43 options ∷ [OptDescr CmdOpt]
44 options = [ Option ['o'] ["output"]
45 (ReqArg OptOutput "FILE")
48 , Option ['m'] ["module"]
49 (ReqArg OptModName "MODULE")
50 "Specify the resulting module name. (required)"
52 , Option ['s'] ["symbol"]
53 (ReqArg OptSymName "SYMBOL")
54 "Specify the resulting symbol name."
56 , Option ['t'] ["mime-type"]
57 (ReqArg OptMIMEType "TYPE")
58 "Specify the MIME Type of the file."
60 , Option ['e'] ["etag"]
61 (ReqArg OptETag "TAG")
62 "Specify the ETag of the file."
64 , Option ['h'] ["help"]
70 printUsage = do mapM_ putStrLn msg
71 putStr $ usageInfo "Options:" options
76 , concat [ " lucu-implant-file is an utility that generates "
77 , "Haskell code containing an arbitrary file to "
78 , "compile it directly into programs and serve it "
79 , "statically with the Lucu HTTP server."
83 , " lucu-implant-file [OPTIONS...] FILE"
88 main = do (opts, sources, errors) ← getOpt Permute options <$> getArgs
91 $ do mapM_ putStr errors
92 exitWith $ ExitFailure 1
94 when (any (≡ OptHelp) opts)
100 exitWith $ ExitFailure 1
102 when (length sources ≥ 2)
103 $ fail "too many input files."
105 generateHaskellSource opts (head sources)
107 generateHaskellSource ∷ [CmdOpt] → FilePath → IO ()
108 generateHaskellSource opts srcFile
109 = do modName ← getModuleName opts
110 symName ← getSymbolName opts modName
111 mimeType ← getMIMEType opts srcFile
112 lastMod ← getLastModified srcFile
113 input ← openInput srcFile
114 output ← openOutput opts
115 eTag ← getETag opts input
117 let compParams = defaultCompressParams { compressLevel = bestCompression }
118 gzippedData = compressWith compParams input
119 originalLen = Lazy.length input
120 gzippedLen = Lazy.length gzippedData
121 useGZip = originalLen > gzippedLen
122 rawB64 = B64.encode <$> Lazy.toChunks input
123 gzippedB64 = B64.encode <$> Lazy.toChunks gzippedData
125 header ← mkHeader srcFile originalLen gzippedLen useGZip mimeType eTag lastMod
127 let hsModule = mkModule modName symName imports decls
128 imports = mkImports useGZip
129 decls = concat ([ resourceDecl symName useGZip
131 , lastModifiedDecl lastMod
132 , contentTypeDecl mimeType
136 [ gunzipAndPutChunkDecl
137 , dataDecl (name "gzippedData") gzippedB64
140 [ dataDecl (name "rawData") rawB64 ]
143 hPutStrLn output header
144 hPutStrLn output (prettyPrint hsModule)
147 mkModule ∷ ModuleName → Name → [ImportDecl] → [Decl] → Module
148 mkModule modName symName imports decls
149 = let modPragma = [ LanguagePragma (⊥) [ name (show OverloadedStrings) ] ]
150 exports = [ EVar (UnQual symName) ]
152 Module (⊥) modName modPragma Nothing (Just exports) imports decls
154 mkImports ∷ Bool → [ImportDecl]
156 = [ ImportDecl (⊥) (ModuleName "Data.ByteString.Base64")
157 True False Nothing (Just (ModuleName "B64")) Nothing
158 , ImportDecl (⊥) (ModuleName "Data.ByteString.Lazy")
159 True False Nothing (Just (ModuleName "Lazy")) Nothing
160 , ImportDecl (⊥) (ModuleName "Data.Time")
161 False False Nothing Nothing Nothing
162 , ImportDecl (⊥) (ModuleName "Network.HTTP.Lucu")
163 False False Nothing Nothing Nothing
167 [ ImportDecl (⊥) (ModuleName "Blaze.ByteString.Builder.ByteString")
168 True False Nothing (Just (ModuleName "BB")) Nothing
169 , ImportDecl (⊥) (ModuleName "Codec.Compression.Zlib.Internal")
170 False False Nothing Nothing Nothing
171 , ImportDecl (⊥) (ModuleName "Data.Text")
172 True False Nothing (Just (ModuleName "T")) Nothing
177 resourceDecl ∷ Name → Bool → [Decl]
178 resourceDecl symName useGZip
179 = [ TypeSig (⊥) [symName] (TyCon (UnQual (name "ResourceDef")))
180 , nameBind (⊥) symName valExp
184 valExp = RecUpdate (function "emptyResource")
185 [ FieldUpdate (UnQual (name "resGet" )) resGet
186 , FieldUpdate (UnQual (name "resHead")) resHead
190 resGet | useGZip = resGetGZipped
191 | otherwise = resGetRaw
195 = function "Just" `app`
196 paren (doE [ foundEntityStmt
202 = function "Just" `app`
203 paren (doE [ foundEntityStmt
206 , conditionalOutputStmt
210 condVarName = name "gzipAllowed"
213 dataVarName = name "gzippedData"
219 (function "isEncodingAcceptable" `app` strE "gzip")
221 conditionalOutputStmt ∷ Stmt
222 conditionalOutputStmt
225 (doE [ setContentEncodingGZipStmt
226 , outputStmt (var dataVarName)
228 (function "gunzipAndPutChunk" `app` var dataVarName)
232 = function "Just" `app`
233 paren (doE [ foundEntityStmt
235 , outputStmt (function "rawData")
238 setContentEncodingGZipStmt ∷ Stmt
239 setContentEncodingGZipStmt
241 ( function "setContentEncoding"
243 listE [ strE "gzip" ]
246 foundEntityStmt ∷ Stmt
249 metaFunction "foundEntity"
250 [ var (name "entityTag")
251 , var (name "lastModified")
254 setContentTypeStmt ∷ Stmt
257 ( function "setContentType"
259 function "contentType"
262 outputStmt ∷ Exp → Stmt
264 = qualStmt $ function "putChunk" `app` e
266 entityTagDecl ∷ ETag → [Decl]
268 = [ TypeSig (⊥) [varName] (TyCon (UnQual (name "ETag")))
269 , nameBind (⊥) varName valExp
273 varName = name "entityTag"
276 valExp = function "parseETag" `app` strE (eTagToString eTag)
278 lastModifiedDecl ∷ UTCTime → [Decl]
279 lastModifiedDecl lastMod
280 = [ TypeSig (⊥) [varName] (TyCon (UnQual (name "UTCTime")))
281 , nameBind (⊥) varName valExp
285 varName = name "lastModified"
288 valExp = function "read" `app` strE (show lastMod)
290 contentTypeDecl ∷ MIMEType → [Decl]
292 = [ TypeSig (⊥) [varName] (TyCon (UnQual (name "MIMEType")))
293 , nameBind (⊥) varName valExp
297 varName = name "contentType"
300 valExp = function "parseMIMEType" `app` strE (mimeToString mime)
302 mimeToString ∷ MIMEType → String
303 mimeToString = A.toString ∘ A.fromAsciiBuilder ∘ printMIMEType
305 gunzipAndPutChunkDecl ∷ [Decl]
306 gunzipAndPutChunkDecl
307 = [ TypeSig (⊥) [funName]
308 (TyFun (TyCon (Qual (ModuleName "Lazy") (name "ByteString")))
310 , sfun (⊥) funName [] (UnGuardedRhs funExp) (binds goDecl)
314 funName = name "gunzipAndPutChunk"
319 tyResourceUnit ∷ Type
321 = TyApp (TyCon (UnQual (name "Resource")))
329 metaFunction "decompressWithErrors"
330 [ function "gzipFormat"
331 , function "defaultDecompressParams"
335 goDecl = [ TypeSig (⊥) [goName]
336 (TyFun (TyCon (UnQual (name "DecompressStream")))
338 , FunBind [ Match (⊥) goName [pvar (name "StreamEnd")]
339 Nothing (UnGuardedRhs endExp) (binds [])
340 , Match (⊥) goName [pApp (name "StreamChunk")
342 , pvar (name "xs") ]]
343 Nothing (UnGuardedRhs chunkExp) (binds [])
344 , Match (⊥) goName [pApp (name "StreamError")
346 , pvar (name "msg") ]]
347 Nothing (UnGuardedRhs errorExp) (binds [])
352 endExp = function "return" `app` tuple []
355 chunkExp = function "putBuilder"
357 paren ( qvar (ModuleName "BB") (name "fromByteString")
364 function "go" `app` var (name "xs")
367 errorExp = metaFunction "abort"
368 [ var (name "InternalServerError")
372 paren ( qvar (ModuleName "T") (name "pack")
374 paren ( strE "gunzip: "
383 dataDecl ∷ Name → [Strict.ByteString] → [Decl]
384 dataDecl varName chunks
385 = [ TypeSig (⊥) [varName] (TyCon (Qual (ModuleName "Lazy") (name "ByteString")))
386 , nameBind (⊥) varName valExp
390 valExp = qvar (ModuleName "Lazy") (name "fromChunks")
392 listE (chunkToExp <$> chunks)
394 chunkToExp ∷ Strict.ByteString → Exp
396 = qvar (ModuleName "B64") (name "decodeLenient")
398 strE (Strict.unpack chunk)
400 mkHeader ∷ FilePath → Int64 → Int64 → Bool → MIMEType → ETag → UTCTime → IO String
401 mkHeader srcFile originalLen gzippedLen useGZip mimeType eTag lastMod
402 = do localLastMod ← utcToLocalZonedTime lastMod
404 [ "{- DO NOT EDIT THIS FILE.\n"
405 , " This file is automatically generated by the lucu-implant-file program.\n"
407 , " Source: ", if srcFile ≡ "-" then
412 , " Original Length: ", show originalLen, " bytes\n"
414 " Compressed Length: " ⧺ show gzippedLen ⧺ " bytes\n" ⧺
415 " Compression: gzip\n"
417 " Compression: disabled\n"
418 , " MIME Type: ", mimeTypeToString mimeType, "\n"
419 , " ETag: ", eTagToString eTag, "\n"
420 , " Last Modified: ", show localLastMod, "\n"
424 eTagToString ∷ ETag → String
425 eTagToString = A.toString ∘ A.fromAsciiBuilder ∘ printETag
427 mimeTypeToString ∷ MIMEType → String
428 mimeTypeToString = A.toString ∘ A.fromAsciiBuilder ∘ printMIMEType
430 getModuleName ∷ [CmdOpt] → IO ModuleName
432 = case modNameOpts of
433 [] → fail "a module name must be given."
434 OptModName modName:[] → return $ ModuleName modName
435 _ → fail "too many --module options."
437 modNameOpts ∷ [CmdOpt]
438 modNameOpts = filter (\ x → case x of
442 getSymbolName ∷ [CmdOpt] → ModuleName → IO Name
443 getSymbolName opts (ModuleName modName)
444 = case symNameOpts of
445 [] → return defaultSymName
446 OptSymName symName:[] → return $ name symName
447 _ → fail "too many --symbol options."
449 symNameOpts ∷ [CmdOpt]
450 symNameOpts = filter (\ x → case x of
454 defaultSymName ∷ Name
456 = name $ headToLower $ getLastComp modName
458 headToLower ∷ String → String
459 headToLower [] = error "module name must not be empty"
460 headToLower (x:xs) = toLower x : xs
462 getLastComp ∷ String → String
463 getLastComp = reverse ∘ fst ∘ break (≡ '.') ∘ reverse
465 getMIMEType ∷ [CmdOpt] → FilePath → IO MIMEType
466 getMIMEType opts srcFile
467 = case mimeTypeOpts of
468 [] → return defaultType
470 → case A.fromChars ty of
471 Just a → return $ parseMIMEType a
472 Nothing → fail "MIME type must not contain any non-ASCII letters."
473 _ → fail "too many --mime-type options."
475 mimeTypeOpts ∷ [CmdOpt]
477 = filter (\ x → case x of
481 octetStream ∷ MIMEType
482 octetStream = parseMIMEType "application/octet-stream"
484 defaultType ∷ MIMEType
485 defaultType = fromMaybe octetStream
486 $ guessTypeByFileName defaultExtensionMap srcFile
488 getLastModified ∷ FilePath → IO UTCTime
489 getLastModified "-" = getCurrentTime
490 getLastModified fpath = (posixSecondsToUTCTime ∘ fromRational ∘ toRational ∘ modificationTime)
494 getETag ∷ [CmdOpt] → Lazy.ByteString → IO ETag
497 [] → return mkETagFromInput
498 OptETag str:[] → return $ strToETag str
499 _ → fail "too many --etag options."
502 eTagOpts = filter (\ x → case x of
506 mkETagFromInput ∷ ETag
508 = strToETag $ "SHA-1:" ⧺ showDigest (sha1 input)
510 strToETag ∷ String → ETag
512 = case A.fromChars str of
513 Just a → strongETag a
514 Nothing → error "ETag must not contain any non-ASCII letters."
516 openInput ∷ FilePath → IO Lazy.ByteString
517 openInput "-" = Lazy.getContents
518 openInput fpath = Lazy.readFile fpath
520 openOutput ∷ [CmdOpt] → IO Handle
524 OptOutput fpath:[] → openFile fpath WriteMode
525 _ → fail "two many --output options."
527 outputOpts ∷ [CmdOpt]
528 outputOpts = filter (\ x → case x of
533 作られるファイルの例 (壓縮されない場合):
534 ------------------------------------------------------------------------------
535 {- DO NOT EDIT THIS FILE.
536 This file is automatically generated by the lucu-implant-file program.
539 Original Length: 302 bytes
540 Compressed Length: 453 bytes -- これは Compression: disabled の時には無い
541 Compression: disabled
543 ETag: "d41d8cd98f00b204e9800998ecf8427e"
544 Last Modified: 2007-11-05 13:53:42.231882 JST
546 {-# LANGUAGE OverloadedStrings #-}
547 module Foo.Bar.Baz (baz) where
548 import qualified Data.ByteString.Base64 as B64
549 import qualified Data.ByteString.Lazy as Lazy
551 import Network.HTTP.Lucu
555 resUsesNativeThread = False
556 , resIsGreedy = False
558 = Just $ do foundEntity entityTag lastModified
559 setContentType contentType
562 = Just $ do foundEntity entityTag lastModified
563 setContentType contentType
566 , resDelete = Nothing
570 entityTag = strongETag "d41d8cd98f00b204e9800998ecf8427e"
572 lastModified ∷ UTCTime
573 lastModified = read "2007-11-05 04:47:56.008366 UTC"
575 contentType ∷ MIMEType
576 contentType = parseMIMEType "image/png"
578 rawData ∷ Lazy.ByteString
579 rawData = Lazy.fromChunks
580 [ B64.decodeLenient "IyEvdXNyL2Jpbi9lbnYgcnVuZ2hjCgppbXBvcnQgRG..."
581 , B64.decodeLenient "Otb/+DniOlRgAAAAYAAAAGAAAAB/6QOmToAEIGAAAA..."
583 ------------------------------------------------------------------------------
586 ------------------------------------------------------------------------------
588 import qualified Blaze.ByteString.Builder.ByteString as BB
589 import Codec.Compression.Zlib.Internal
590 import qualified Data.Text as T
592 -- ResourceDef は次のやうに變化
595 resUsesNativeThread = False
596 , resIsGreedy = False
598 = Just $ do foundEntity entityTag lastModified
599 setContentType contentType
601 gzipAllowed ← isEncodingAcceptable "gzip"
603 do setContentEncoding ["gzip"]
606 gunzipAndPutChunk gzippedData
608 = Just $ do foundEntity entityTag lastModified
609 setContentType contentType
612 , resDelete = Nothing
616 gunzipAndPutChunk :: Lazy.ByteString -> Resource ()
617 gunzipAndPutChunk = go . decompressWithErrors gzipFormat defaultDecompressParams
619 go :: DecompressStream -> Resource ()
620 go StreamEnd = return ()
621 go (StreamChunk x xs) = putBuilder (BB.fromByteString x) >> go xs
622 go (StreamError _ msg) = abort InternalServerError [] (Just (T.pack ("gunzip: " ++ msg)))
624 -- rawData の代はりに gzippedData
625 gzippedData ∷ Lazy.ByteString
626 gzippedData = Lazy.fromChunks
627 [ B64.decodeLenient "IyEvdXNyL2Jpbi9lbnYgcnVuZ2hjCgppbXBvcnQ..."
628 , B64.decodeLenient "Otb/+DniOlRgAAAAYAAAAGAAAAB/6QOmToAEIGA..."
630 ------------------------------------------------------------------------------