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
134 dataDecl (name "gzippedData") gzippedB64
136 dataDecl (name "rawData") rawB64
139 hPutStrLn output header
140 hPutStrLn output (prettyPrint hsModule)
143 mkModule ∷ ModuleName → Name → [ImportDecl] → [Decl] → Module
144 mkModule modName symName imports decls
145 = let modPragma = [ LanguagePragma (⊥) [ name (show OverloadedStrings) ] ]
146 exports = [ EVar (UnQual symName) ]
148 Module (⊥) modName modPragma Nothing (Just exports) imports decls
150 mkImports ∷ Bool → [ImportDecl]
152 = [ ImportDecl (⊥) (ModuleName "Data.ByteString.Base64")
153 True False Nothing (Just (ModuleName "B64")) Nothing
154 , ImportDecl (⊥) (ModuleName "Data.ByteString.Lazy")
155 True False Nothing (Just (ModuleName "Lazy")) Nothing
156 , ImportDecl (⊥) (ModuleName "Data.Time")
157 False False Nothing Nothing Nothing
158 , ImportDecl (⊥) (ModuleName "Network.HTTP.Lucu")
159 False False Nothing Nothing Nothing
162 [ ImportDecl (⊥) (ModuleName "Codec.Compression.GZip")
163 False False Nothing Nothing Nothing
167 resourceDecl ∷ Name → Bool → [Decl]
168 resourceDecl symName useGZip
169 = [ TypeSig (⊥) [symName] (TyCon (UnQual (name "ResourceDef")))
170 , nameBind (⊥) symName valExp
174 valExp = RecUpdate (var $ name "emptyResource")
175 [ FieldUpdate (UnQual (name "resGet" )) resGet
176 , FieldUpdate (UnQual (name "resHead")) resHead
180 resGet | useGZip = resGetGZipped
181 | otherwise = resGetRaw
185 = infixApp (var $ name "Just")
187 (doE [ foundEntityStmt
193 = infixApp (var $ name "Just")
195 (doE [ foundEntityStmt
198 , conditionalOutputStmt
202 condVarName = name "gzipAllowed"
205 dataVarName = name "gzippedData"
211 (metaFunction "isEncodingAcceptable" [strE "gzip"])
213 conditionalOutputStmt ∷ Stmt
214 conditionalOutputStmt
217 (doE [ setContentEncodingGZipStmt
218 , outputStmt (var dataVarName)
220 (metaFunction "output"
221 [paren (metaFunction "decompress" [var dataVarName])])
225 = infixApp (var $ name "Just")
227 (doE [ foundEntityStmt
229 , outputStmt (var $ name "rawData")
232 setContentEncodingGZipStmt ∷ Stmt
233 setContentEncodingGZipStmt
235 metaFunction "setContentEncoding"
236 [ listE [ strE "gzip" ] ]
238 foundEntityStmt ∷ Stmt
241 metaFunction "foundEntity"
242 [ var $ name "entityTag"
243 , var $ name "lastModified"
246 setContentTypeStmt ∷ Stmt
249 metaFunction "setContentType"
250 [var $ name "contentType"]
252 outputStmt ∷ Exp → Stmt
255 metaFunction "output" [e]
257 entityTagDecl ∷ ETag → [Decl]
259 = [ TypeSig (⊥) [varName] (TyCon (UnQual (name "ETag")))
260 , nameBind (⊥) varName valExp
264 varName = name "entityTag"
267 valExp = metaFunction "parseETag" [strE $ eTagToString eTag]
269 lastModifiedDecl ∷ UTCTime → [Decl]
270 lastModifiedDecl lastMod
271 = [ TypeSig (⊥) [varName] (TyCon (UnQual (name "UTCTime")))
272 , nameBind (⊥) varName valExp
276 varName = name "lastModified"
279 valExp = metaFunction "read" [strE $ show lastMod]
281 contentTypeDecl ∷ MIMEType → [Decl]
283 = [ TypeSig (⊥) [varName] (TyCon (UnQual (name "MIMEType")))
284 , nameBind (⊥) varName valExp
288 varName = name "contentType"
291 valExp = metaFunction "parseMIMEType" [strE $ mimeToString mime]
293 mimeToString ∷ MIMEType → String
294 mimeToString = A.toString ∘ A.fromAsciiBuilder ∘ printMIMEType
296 dataDecl ∷ Name → [Strict.ByteString] → [Decl]
297 dataDecl varName chunks
298 = [ TypeSig (⊥) [varName] (TyCon (Qual (ModuleName "Lazy") (name "ByteString")))
299 , nameBind (⊥) varName valExp
303 valExp = qvar (ModuleName "Lazy") (name "fromChunks")
305 listE (chunkToExp <$> chunks)
307 chunkToExp ∷ Strict.ByteString → Exp
309 = qvar (ModuleName "B64") (name "decodeLenient")
311 strE (Strict.unpack chunk)
313 mkHeader ∷ FilePath → Int64 → Int64 → Bool → MIMEType → ETag → UTCTime → IO String
314 mkHeader srcFile originalLen gzippedLen useGZip mimeType eTag lastMod
315 = do localLastMod ← utcToLocalZonedTime lastMod
317 [ "{- DO NOT EDIT THIS FILE.\n"
318 , " This file is automatically generated by the lucu-implant-file program.\n"
320 , " Source: ", if srcFile ≡ "-" then
325 , " Original Length: ", show originalLen, " bytes\n"
327 " Compressed Length: " ⧺ show gzippedLen ⧺ " bytes\n" ⧺
328 " Compression: gzip\n"
330 " Compression: disabled\n"
331 , " MIME Type: ", show mimeType, "\n"
332 , " ETag: ", eTagToString eTag, "\n"
333 , " Last Modified: ", show localLastMod, "\n"
337 eTagToString ∷ ETag → String
338 eTagToString = A.toString ∘ A.fromAsciiBuilder ∘ printETag
340 getModuleName ∷ [CmdOpt] → IO ModuleName
342 = case modNameOpts of
343 [] → fail "a module name must be given."
344 OptModName modName:[] → return $ ModuleName modName
345 _ → fail "too many --module options."
347 modNameOpts ∷ [CmdOpt]
348 modNameOpts = filter (\ x → case x of
352 getSymbolName ∷ [CmdOpt] → ModuleName → IO Name
353 getSymbolName opts (ModuleName modName)
354 = case symNameOpts of
355 [] → return defaultSymName
356 OptSymName symName:[] → return $ name symName
357 _ → fail "too many --symbol options."
359 symNameOpts ∷ [CmdOpt]
360 symNameOpts = filter (\ x → case x of
364 defaultSymName ∷ Name
366 = name $ headToLower $ getLastComp modName
368 headToLower ∷ String → String
369 headToLower [] = error "module name must not be empty"
370 headToLower (x:xs) = toLower x : xs
372 getLastComp ∷ String → String
373 getLastComp = reverse ∘ fst ∘ break (≡ '.') ∘ reverse
375 getMIMEType ∷ [CmdOpt] → FilePath → IO MIMEType
376 getMIMEType opts srcFile
377 = case mimeTypeOpts of
378 [] → return defaultType
380 → case A.fromChars ty of
381 Just a → return $ parseMIMEType a
382 Nothing → fail "MIME type must not contain any non-ASCII letters."
383 _ → fail "too many --mime-type options."
385 mimeTypeOpts ∷ [CmdOpt]
387 = filter (\ x → case x of
391 octetStream ∷ MIMEType
392 octetStream = parseMIMEType "application/octet-stream"
394 defaultType ∷ MIMEType
395 defaultType = fromMaybe octetStream
396 $ guessTypeByFileName defaultExtensionMap srcFile
398 getLastModified ∷ FilePath → IO UTCTime
399 getLastModified "-" = getCurrentTime
400 getLastModified fpath = (posixSecondsToUTCTime ∘ fromRational ∘ toRational ∘ modificationTime)
404 getETag ∷ [CmdOpt] → Lazy.ByteString → IO ETag
407 [] → return mkETagFromInput
408 OptETag str:[] → return $ strToETag str
409 _ → fail "too many --etag options."
412 eTagOpts = filter (\ x → case x of
416 mkETagFromInput ∷ ETag
418 = strToETag $ "SHA-1:" ⧺ showDigest (sha1 input)
420 strToETag ∷ String → ETag
422 = case A.fromChars str of
423 Just a → strongETag a
424 Nothing → error "ETag must not contain any non-ASCII letters."
426 openInput ∷ FilePath → IO Lazy.ByteString
427 openInput "-" = Lazy.getContents
428 openInput fpath = Lazy.readFile fpath
430 openOutput ∷ [CmdOpt] → IO Handle
434 OptOutput fpath:[] → openFile fpath WriteMode
435 _ → fail "two many --output options."
437 outputOpts ∷ [CmdOpt]
438 outputOpts = filter (\ x → case x of
443 作られるファイルの例 (壓縮されない場合):
444 ------------------------------------------------------------------------------
445 {- DO NOT EDIT THIS FILE.
446 This file is automatically generated by the lucu-implant-file program.
449 Original Length: 302 bytes
450 Compressed Length: 453 bytes -- これは Compression: disabled の時には無い
451 Compression: disabled
453 ETag: "d41d8cd98f00b204e9800998ecf8427e"
454 Last Modified: 2007-11-05 13:53:42.231882 JST
456 {-# LANGUAGE OverloadedStrings #-}
457 module Foo.Bar.Baz (baz) where
458 import qualified Data.ByteString.Base64 as B64
459 import qualified Data.ByteString.Lazy as Lazy
461 import Network.HTTP.Lucu
465 resUsesNativeThread = False
466 , resIsGreedy = False
468 = Just $ do foundEntity entityTag lastModified
469 setContentType contentType
472 = Just $ do foundEntity entityTag lastModified
473 setContentType contentType
476 , resDelete = Nothing
480 entityTag = strongETag "d41d8cd98f00b204e9800998ecf8427e"
482 lastModified ∷ UTCTime
483 lastModified = read "2007-11-05 04:47:56.008366 UTC"
485 contentType ∷ MIMEType
486 contentType = parseMIMEType "image/png"
488 rawData ∷ Lazy.ByteString
489 rawData = Lazy.fromChunks
490 [ B64.decodeLenient "IyEvdXNyL2Jpbi9lbnYgcnVuZ2hjCgppbXBvcnQgRG..."
491 , B64.decodeLenient "Otb/+DniOlRgAAAAYAAAAGAAAAB/6QOmToAEIGAAAA..."
493 ------------------------------------------------------------------------------
496 ------------------------------------------------------------------------------
498 import Codec.Compression.GZip
500 -- ResourceDef は次のやうに變化
503 resUsesNativeThread = False
504 , resIsGreedy = False
506 = Just $ do foundEntity entityTag lastModified
507 setContentType contentType
509 gzipAllowed ← isEncodingAcceptable "gzip"
511 do setContentEncoding ["gzip"]
514 output (decompress gzippedData)
516 = Just $ do foundEntity entityTag lastModified
517 setContentType contentType
520 , resDelete = Nothing
523 -- rawData の代はりに gzippedData
524 gzippedData ∷ Lazy.ByteString
525 gzippedData = Lazy.fromChunks
526 [ B64.decodeLenient "IyEvdXNyL2Jpbi9lbnYgcnVuZ2hjCgppbXBvcnQ..."
527 , B64.decodeLenient "Otb/+DniOlRgAAAAYAAAAGAAAAB/6QOmToAEIGA..."
529 ------------------------------------------------------------------------------