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."
61 (ReqArg OptETag "TAG")
62 "Specify the ETag of the file."
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 mType ← getMIMEType opts srcFile
112 lastMod ← getLastModified srcFile
113 input ← openInput srcFile
114 output ← openOutput opts
115 tag ← 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 mType tag lastMod
127 let hsModule = mkModule modName symName imports decls
128 imports = mkImports useGZip
129 decls = concat [ resourceDecl symName useGZip
131 , lastModifiedDecl lastMod
132 , contentTypeDecl mType
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
166 resourceDecl ∷ Name → Bool → [Decl]
167 resourceDecl symName useGZip
168 = [ TypeSig (⊥) [symName] (TyCon (UnQual (name "ResourceDef")))
169 , nameBind (⊥) symName valExp
173 valExp = RecUpdate (function "emptyResource")
174 [ FieldUpdate (UnQual (name "resGet" )) resGet
175 , FieldUpdate (UnQual (name "resHead")) resHead
179 resGet | useGZip = resGetGZipped
180 | otherwise = resGetRaw
184 = function "Just" `app`
185 paren (doE [ foundEntityStmt
191 = function "Just" `app`
192 paren (doE [ foundEntityStmt
195 , conditionalOutputStmt
199 condVarName = name "gzipAllowed"
202 dataVarName = name "gzippedData"
208 (function "isEncodingAcceptable" `app` strE "gzip")
210 conditionalOutputStmt ∷ Stmt
211 conditionalOutputStmt
214 (doE [ setContentEncodingGZipStmt
215 , putChunksStmt (var dataVarName)
219 (function "decompress" `app` var dataVarName)))
223 = function "Just" `app`
224 paren (doE [ foundEntityStmt
226 , putChunksStmt (function "rawData")
229 setContentEncodingGZipStmt ∷ Stmt
230 setContentEncodingGZipStmt
232 ( function "setContentEncoding"
234 listE [ strE "gzip" ]
237 foundEntityStmt ∷ Stmt
240 metaFunction "foundEntity"
241 [ var (name "entityTag")
242 , var (name "lastModified")
245 setContentTypeStmt ∷ Stmt
248 ( function "setContentType"
250 function "contentType"
253 putChunksExp ∷ Exp → Exp
254 putChunksExp = app (function "putChunks")
256 putChunksStmt ∷ Exp → Stmt
257 putChunksStmt = qualStmt ∘ putChunksExp
259 entityTagDecl ∷ ETag → [Decl]
261 = [ TypeSig (⊥) [varName] (TyCon (UnQual (name "ETag")))
262 , nameBind (⊥) varName valExp
266 varName = name "entityTag"
269 valExp = function "parseETag" `app` strE (eTagToString tag)
271 lastModifiedDecl ∷ UTCTime → [Decl]
272 lastModifiedDecl lastMod
273 = [ TypeSig (⊥) [varName] (TyCon (UnQual (name "UTCTime")))
274 , nameBind (⊥) varName valExp
275 , InlineSig (⊥) False AlwaysActive (UnQual varName)
279 varName = name "lastModified"
282 valExp = function "read" `app` strE (show lastMod)
284 contentTypeDecl ∷ MIMEType → [Decl]
286 = [ TypeSig (⊥) [varName] (TyCon (UnQual (name "MIMEType")))
287 , nameBind (⊥) varName valExp
288 , InlineSig (⊥) False AlwaysActive (UnQual varName)
292 varName = name "contentType"
295 valExp = function "parseMIMEType" `app` strE (mimeToString mime)
297 mimeToString ∷ MIMEType → String
298 mimeToString = A.toString ∘ A.fromAsciiBuilder ∘ printMIMEType
300 dataDecl ∷ Name → [Strict.ByteString] → [Decl]
301 dataDecl varName chunks
302 = [ TypeSig (⊥) [varName] (TyCon (Qual (ModuleName "Lazy") (name "ByteString")))
303 , nameBind (⊥) varName valExp
304 , InlineSig (⊥) False AlwaysActive (UnQual varName)
308 valExp = qvar (ModuleName "Lazy") (name "fromChunks")
310 listE (chunkToExp <$> chunks)
312 chunkToExp ∷ Strict.ByteString → Exp
314 = qvar (ModuleName "B64") (name "decodeLenient")
316 strE (Strict.unpack chunk)
318 mkHeader ∷ FilePath → Int64 → Int64 → Bool → MIMEType → ETag → UTCTime → IO String
319 mkHeader srcFile originalLen gzippedLen useGZip mType tag lastMod
320 = do localLastMod ← utcToLocalZonedTime lastMod
322 [ "{- DO NOT EDIT THIS FILE.\n"
323 , " This file is automatically generated by the lucu-implant-file program.\n"
325 , " Source: ", if srcFile ≡ "-" then
330 , " Original Length: ", show originalLen, " bytes\n"
332 " Compressed Length: " ⧺ show gzippedLen ⧺ " bytes\n" ⧺
333 " Compression: gzip\n"
335 " Compression: disabled\n"
336 , " MIME Type: ", mimeTypeToString mType, "\n"
337 , " ETag: ", eTagToString tag, "\n"
338 , " Last Modified: ", show localLastMod, "\n"
342 eTagToString ∷ ETag → String
343 eTagToString = A.toString ∘ A.fromAsciiBuilder ∘ printETag
345 mimeTypeToString ∷ MIMEType → String
346 mimeTypeToString = A.toString ∘ A.fromAsciiBuilder ∘ printMIMEType
348 getModuleName ∷ [CmdOpt] → IO ModuleName
350 = case modNameOpts of
351 [] → fail "a module name must be given."
352 OptModName modName:[] → return $ ModuleName modName
353 _ → fail "too many --module options."
355 modNameOpts ∷ [CmdOpt]
356 modNameOpts = filter (\ x → case x of
360 getSymbolName ∷ [CmdOpt] → ModuleName → IO Name
361 getSymbolName opts (ModuleName modName)
362 = case symNameOpts of
363 [] → return defaultSymName
364 OptSymName symName:[] → return $ name symName
365 _ → fail "too many --symbol options."
367 symNameOpts ∷ [CmdOpt]
368 symNameOpts = filter (\ x → case x of
372 defaultSymName ∷ Name
374 = name $ headToLower $ getLastComp modName
376 headToLower ∷ String → String
377 headToLower [] = error "module name must not be empty"
378 headToLower (x:xs) = toLower x : xs
380 getLastComp ∷ String → String
381 getLastComp = reverse ∘ fst ∘ break (≡ '.') ∘ reverse
383 getMIMEType ∷ [CmdOpt] → FilePath → IO MIMEType
384 getMIMEType opts srcFile
385 = case mimeTypeOpts of
386 [] → return defaultType
388 → case A.fromChars ty of
389 Just a → return $ parseMIMEType a
390 Nothing → fail "MIME type must not contain any non-ASCII letters."
391 _ → fail "too many --mime-type options."
393 mimeTypeOpts ∷ [CmdOpt]
395 = filter (\ x → case x of
399 octetStream ∷ MIMEType
400 octetStream = parseMIMEType "application/octet-stream"
402 defaultType ∷ MIMEType
403 defaultType = fromMaybe octetStream
404 $ guessTypeByFileName defaultExtensionMap srcFile
406 getLastModified ∷ FilePath → IO UTCTime
407 getLastModified "-" = getCurrentTime
408 getLastModified fpath = (posixSecondsToUTCTime ∘ fromRational ∘ toRational ∘ modificationTime)
412 getETag ∷ [CmdOpt] → Lazy.ByteString → IO ETag
415 [] → return mkETagFromInput
416 OptETag str:[] → return $ strToETag str
417 _ → fail "too many --etag options."
420 eTagOpts = filter (\ x → case x of
424 mkETagFromInput ∷ ETag
426 = strToETag $ "SHA-1:" ⧺ showDigest (sha1 input)
428 strToETag ∷ String → ETag
430 = case A.fromChars str of
431 Just a → strongETag a
432 Nothing → error "ETag must not contain any non-ASCII letters."
434 openInput ∷ FilePath → IO Lazy.ByteString
435 openInput "-" = Lazy.getContents
436 openInput fpath = Lazy.readFile fpath
438 openOutput ∷ [CmdOpt] → IO Handle
442 OptOutput fpath:[] → openFile fpath WriteMode
443 _ → fail "two many --output options."
445 outputOpts ∷ [CmdOpt]
446 outputOpts = filter (\ x → case x of
451 作られるファイルの例 (壓縮されない場合):
452 ------------------------------------------------------------------------------
453 {- DO NOT EDIT THIS FILE.
454 This file is automatically generated by the lucu-implant-file program.
457 Original Length: 302 bytes
458 Compressed Length: 453 bytes -- これは Compression: disabled の時には無い
459 Compression: disabled
461 ETag: "d41d8cd98f00b204e9800998ecf8427e"
462 Last Modified: 2007-11-05 13:53:42.231882 JST
464 {-# LANGUAGE OverloadedStrings #-}
465 module Foo.Bar.Baz (baz) where
466 import qualified Data.ByteString.Base64 as B64
467 import qualified Data.ByteString.Lazy as Lazy
469 import Network.HTTP.Lucu
473 resUsesNativeThread = False
474 , resIsGreedy = False
476 = Just $ do foundEntity entityTag lastModified
477 setContentType contentType
480 = Just $ do foundEntity entityTag lastModified
481 setContentType contentType
484 , resDelete = Nothing
488 entityTag = strongETag "d41d8cd98f00b204e9800998ecf8427e"
490 lastModified ∷ UTCTime
491 {-# NOINLINE lastModified #-}
492 lastModified = read "2007-11-05 04:47:56.008366 UTC"
494 contentType ∷ MIMEType
495 {-# NOINLINE contentType #-}
496 contentType = parseMIMEType "image/png"
498 rawData ∷ Lazy.ByteString
499 {-# NOINLINE rawData #-}
500 rawData = Lazy.fromChunks
501 [ B64.decodeLenient "IyEvdXNyL2Jpbi9lbnYgcnVuZ2hjCgppbXBvcnQgRG..."
502 , B64.decodeLenient "Otb/+DniOlRgAAAAYAAAAGAAAAB/6QOmToAEIGAAAA..."
504 ------------------------------------------------------------------------------
507 ------------------------------------------------------------------------------
509 import Codec.Compression.Zlib
511 -- ResourceDef は次のやうに變化
514 resUsesNativeThread = False
515 , resIsGreedy = False
517 = Just $ do foundEntity entityTag lastModified
518 setContentType contentType
520 gzipAllowed ← isEncodingAcceptable "gzip"
522 do setContentEncoding ["gzip"]
523 putChunks gzippedData
525 putChunks (decompress gzippedData)
527 = Just $ do foundEntity entityTag lastModified
528 setContentType contentType
531 , resDelete = Nothing
534 -- rawData の代はりに gzippedData
535 gzippedData ∷ Lazy.ByteString
536 {-# NOINLINE gzippedData #-}
537 gzippedData = Lazy.fromChunks
538 [ B64.decodeLenient "IyEvdXNyL2Jpbi9lbnYgcnVuZ2hjCgppbXBvcnQ..."
539 , B64.decodeLenient "Otb/+DniOlRgAAAAYAAAAGAAAAB/6QOmToAEIGA..."
541 ------------------------------------------------------------------------------