1 import Codec.Binary.Base64
2 import Codec.Compression.GZip
5 import qualified Data.ByteString.Lazy as Lazy (ByteString)
6 import qualified Data.ByteString.Lazy as L hiding (ByteString)
8 import Data.Digest.SHA2
12 import Data.Time.Clock.POSIX
14 import Language.Haskell.Pretty
15 import Language.Haskell.Syntax
16 import Network.HTTP.Lucu.MIMEType
17 import Network.HTTP.Lucu.MIMEType.DefaultExtensionMap
18 import Network.HTTP.Lucu.MIMEType.Guess
19 import System.Console.GetOpt
20 import System.Environment
22 import System.Posix.Files
35 options :: [OptDescr CmdOpt]
36 options = [ Option ['o'] ["output"]
37 (ReqArg OptOutput "FILE")
40 , Option ['m'] ["module"]
41 (ReqArg OptModName "MODULE")
42 "Specify the resulting module name. (required)"
44 , Option ['s'] ["symbol"]
45 (ReqArg OptSymName "SYMBOL")
46 "Specify the resulting symbol name."
48 , Option ['t'] ["mime-type"]
49 (ReqArg OptMIMEType "TYPE")
50 "Specify the MIME Type of the file."
52 , Option ['e'] ["etag"]
53 (ReqArg OptETag "TAG")
54 "Specify the ETag of the file."
56 , Option ['h'] ["help"]
63 printUsage = do putStrLn ""
64 putStrLn "Description:"
65 putStrLn (" lucu-implant-file is an utility that generates " ++
66 "Haskell code containing an arbitrary file to " ++
67 "compile it directly into programs and serve it " ++
68 "statically with the Lucu HTTP server.")
71 putStrLn " lucu-implant-file [OPTIONS...] FILE"
73 putStr $ usageInfo "Options:" options
78 main = do (opts, sources, errors) <- return . getOpt Permute options =<< getArgs
80 when (not $ null errors)
81 $ do mapM_ putStr errors
82 exitWith $ ExitFailure 1
84 when (any (\ x -> x == OptHelp) opts)
90 exitWith $ ExitFailure 1
92 when (length sources >= 2)
93 $ error "too many input files."
95 generateHaskellSource opts (head sources)
98 generateHaskellSource :: [CmdOpt] -> FilePath -> IO ()
99 generateHaskellSource opts srcFile
100 = do modName <- getModuleName opts
101 symName <- getSymbolName opts modName
102 mimeType <- getMIMEType opts srcFile
103 lastMod <- getLastModified srcFile
104 input <- openInput srcFile
105 output <- openOutput opts
106 eTag <- getETag opts input
108 let gzippedData = compressWith BestCompression input
109 originalLen = L.length input
110 gzippedLen = L.length gzippedData
111 useGZip = originalLen > gzippedLen
112 rawB64 = encode $ L.unpack input
113 gzippedB64 = encode $ L.unpack gzippedData
115 header <- mkHeader srcFile originalLen gzippedLen useGZip mimeType eTag lastMod
117 let hsModule = HsModule undefined (Module modName) (Just exports) imports decls
118 exports = [HsEVar (UnQual (HsIdent symName))]
119 imports = [ HsImportDecl undefined (Module "Codec.Binary.Base64")
120 False Nothing Nothing
121 , HsImportDecl undefined (Module "Data.ByteString.Lazy")
122 True (Just (Module "L")) Nothing
123 , HsImportDecl undefined (Module "Data.Time")
124 False Nothing Nothing
125 , HsImportDecl undefined (Module "Network.HTTP.Lucu")
126 False Nothing Nothing
130 [ HsImportDecl undefined (Module "Control.Monad")
131 False Nothing Nothing
132 , HsImportDecl undefined (Module "Codec.Compression.GZip")
133 False Nothing Nothing
137 decls = declResourceDef
149 declResourceDef :: [HsDecl]
151 = [ HsTypeSig undefined [HsIdent symName]
153 (HsTyCon (UnQual (HsIdent "ResourceDef"))))
154 , HsFunBind [HsMatch undefined (HsIdent symName)
155 [] (HsUnGuardedRhs defResourceDef) []]
158 defResourceDef :: HsExp
160 = let defResGet = if useGZip
161 then defResGetGZipped
164 (HsRecConstr (UnQual (HsIdent "ResourceDef"))
165 [ HsFieldUpdate (UnQual (HsIdent "resUsesNativeThread"))
166 (HsCon (UnQual (HsIdent "False")))
167 , HsFieldUpdate (UnQual (HsIdent "resIsGreedy"))
168 (HsCon (UnQual (HsIdent "False")))
169 , HsFieldUpdate (UnQual (HsIdent "resGet")) defResGet
170 , HsFieldUpdate (UnQual (HsIdent "resHead"))
171 (HsCon (UnQual (HsIdent "Nothing")))
172 , HsFieldUpdate (UnQual (HsIdent "resPost"))
173 (HsCon (UnQual (HsIdent "Nothing")))
174 , HsFieldUpdate (UnQual (HsIdent "resPut"))
175 (HsCon (UnQual (HsIdent "Nothing")))
176 , HsFieldUpdate (UnQual (HsIdent "resDelete"))
177 (HsCon (UnQual (HsIdent "Nothing")))
181 defResGetGZipped :: HsExp
183 = let doExp = HsDo [ doFoundEntity
186 , doConditionalOutput
189 = HsQualifier (HsApp (HsApp (HsVar (UnQual (HsIdent "foundEntity")))
190 (HsVar (UnQual (HsIdent "entityTag"))))
191 (HsVar (UnQual (HsIdent "lastModified"))))
193 = HsQualifier (HsApp (HsVar (UnQual (HsIdent "setContentType")))
194 (HsVar (UnQual (HsIdent "contentType"))))
196 = HsGenerator undefined
197 (HsPVar (HsIdent "mustGunzip"))
198 (HsApp (HsApp (HsVar (UnQual (HsIdent "liftM")))
199 (HsVar (UnQual (HsIdent "not"))))
201 (HsApp (HsVar (UnQual (HsIdent "isEncodingAcceptable")))
202 (HsLit (HsString "gzip")))))
205 (HsIf (HsVar (UnQual (HsIdent "mustGunzip")))
209 = (HsApp (HsVar (UnQual (HsIdent "outputLBS")))
211 (HsApp (HsVar (UnQual (HsIdent "decompress")))
212 (HsVar (UnQual (HsIdent "gzippedData"))))))
214 = HsDo [ doSetContentEncodingGZip
217 doSetContentEncodingGZip
218 = HsQualifier (HsApp (HsVar (UnQual (HsIdent "setContentEncoding")))
219 (HsList [HsLit (HsString "gzip")]))
221 = HsQualifier (HsApp (HsVar (UnQual (HsIdent "outputLBS")))
222 (HsVar (UnQual (HsIdent "gzippedData"))))
224 HsApp (HsCon (UnQual (HsIdent "Just")))
227 defResGetRaw :: HsExp
229 = let doExp = HsDo [ doFoundEntity
234 = HsQualifier (HsApp (HsApp (HsVar (UnQual (HsIdent "foundEntity")))
235 (HsVar (UnQual (HsIdent "entityTag"))))
236 (HsVar (UnQual (HsIdent "lastModified"))))
238 = HsQualifier (HsApp (HsVar (UnQual (HsIdent "setContentType")))
239 (HsVar (UnQual (HsIdent "contentType"))))
241 = HsQualifier (HsApp (HsVar (UnQual (HsIdent "outputLBS")))
242 (HsVar (UnQual (HsIdent "rawData"))))
244 HsApp (HsCon (UnQual (HsIdent "Just")))
247 declEntityTag :: [HsDecl]
249 = [ HsTypeSig undefined [HsIdent "entityTag"]
251 (HsTyCon (UnQual (HsIdent "ETag"))))
252 , HsFunBind [HsMatch undefined (HsIdent "entityTag")
253 [] (HsUnGuardedRhs defEntityTag) []]
256 defEntityTag :: HsExp
258 = HsApp (HsVar (UnQual (HsIdent "strongETag")))
259 (HsLit (HsString eTag))
261 declLastModified :: [HsDecl]
263 = [ HsTypeSig undefined [HsIdent "lastModified"]
265 (HsTyCon (UnQual (HsIdent "UTCTime"))))
266 , HsFunBind [HsMatch undefined (HsIdent "lastModified")
267 [] (HsUnGuardedRhs defLastModified) []]
270 defLastModified :: HsExp
272 = HsApp (HsVar (UnQual (HsIdent "read")))
273 (HsLit (HsString $ show lastMod))
276 declContentType :: [HsDecl]
278 = [ HsTypeSig undefined [HsIdent "contentType"]
280 (HsTyCon (UnQual (HsIdent "MIMEType"))))
281 , HsFunBind [HsMatch undefined (HsIdent "contentType")
282 [] (HsUnGuardedRhs defContentType) []]
285 defContentType :: HsExp
287 = HsApp (HsVar (UnQual (HsIdent "read")))
288 (HsLit (HsString $ show mimeType))
290 declGZippedData :: [HsDecl]
292 = [ HsTypeSig undefined [HsIdent "gzippedData"]
294 (HsTyCon (Qual (Module "L") (HsIdent "ByteString"))))
295 , HsFunBind [HsMatch undefined (HsIdent "gzippedData")
296 [] (HsUnGuardedRhs defGZippedData) []]
299 defGZippedData :: HsExp
301 = HsApp (HsVar (Qual (Module "L") (HsIdent "pack")))
303 (HsApp (HsVar (UnQual (HsIdent "decode")))
304 (HsLit (HsString gzippedB64))))
306 declRawData :: [HsDecl]
308 = [ HsTypeSig undefined [HsIdent "rawData"]
310 (HsTyCon (Qual (Module "L") (HsIdent "ByteString"))))
311 , HsFunBind [HsMatch undefined (HsIdent "rawData")
312 [] (HsUnGuardedRhs defRawData) []]
317 = HsApp (HsVar (Qual (Module "L") (HsIdent "pack")))
319 (HsApp (HsVar (UnQual (HsIdent "decode")))
320 (HsLit (HsString rawB64))))
322 hPutStrLn output header
323 hPutStrLn output (prettyPrint hsModule)
327 mkHeader :: FilePath -> Int64 -> Int64 -> Bool -> MIMEType -> String -> UTCTime -> IO String
328 mkHeader srcFile originalLen gzippedLen useGZip mimeType eTag lastMod
329 = do localLastMod <- utcToLocalZonedTime lastMod
330 return ("{- DO NOT EDIT THIS FILE.\n" ++
331 " This file is automatically generated by the lucu-implant-file program.\n" ++
333 " Source: " ++ (if srcFile == "-"
335 else srcFile) ++ "\n" ++
336 " Original Length: " ++ show originalLen ++ " bytes\n" ++
338 then " Compressed Length: " ++ show gzippedLen ++ " bytes\n" ++
339 " Compression: gzip\n"
340 else " Compression: disabled\n") ++
341 " MIME Type: " ++ show mimeType ++ "\n" ++
342 " ETag: " ++ eTag ++ "\n" ++
343 " Last Modified: " ++ show localLastMod ++ "\n" ++
347 getModuleName :: [CmdOpt] -> IO String
349 = let modNameOpts = filter (\ x -> case x of
354 [] -> error "a module name must be given."
355 (OptModName modName):[] -> return modName
356 _ -> error "too many --module options."
359 getSymbolName :: [CmdOpt] -> String -> IO String
360 getSymbolName opts modName
361 = let symNameOpts = filter (\ x -> case x of
364 -- モジュール名をピリオドで分割した時の最後の項目の先頭文字を
366 defaultSymName = mkDefault modName
367 mkDefault = headToLower . getLastComp
368 headToLower = \ str -> case str of
369 [] -> error "module name must not be empty"
370 (x:xs) -> toLower x : xs
371 getLastComp = reverse . fst . break (== '.') . reverse
374 [] -> return defaultSymName
375 (OptSymName symName):[] -> return symName
376 _ -> error "too many --symbol options."
379 getMIMEType :: [CmdOpt] -> FilePath -> IO MIMEType
380 getMIMEType opts srcFile
381 = let mimeTypeOpts = filter (\ x -> case x of
382 OptMIMEType _ -> True
384 defaultType = fromMaybe (read "application/octet-stream")
385 $ guessTypeByFileName defaultExtensionMap srcFile
388 [] -> return defaultType
389 (OptMIMEType mimeType):[] -> return $ read mimeType
390 _ -> error "too many --mime-type options."
393 getLastModified :: FilePath -> IO UTCTime
394 getLastModified "-" = getCurrentTime
395 getLastModified fpath = getFileStatus fpath
396 >>= return . posixSecondsToUTCTime . fromRational . toRational . modificationTime
399 getETag :: [CmdOpt] -> Lazy.ByteString -> IO String
401 = let eTagOpts = filter (\ x -> case x of
406 [] -> return mkETagFromInput
407 (OptETag str):[] -> return str
408 _ -> error "too many --etag options."
410 mkETagFromInput :: String
411 mkETagFromInput = "SHA-1:" ++ (toHex $ toOctets $ sha256 $ L.unpack input)
413 toHex :: [Word8] -> String
415 toHex (x:xs) = hexByte (fromIntegral x) ++ toHex xs
417 hexByte :: Int -> String
419 = hex4bit ((n `shiftR` 4) .&. 0x0F) : hex4bit (n .&. 0x0F) : []
421 hex4bit :: Int -> Char
423 | n < 10 = (chr $ ord '0' + n )
424 | n < 16 = (chr $ ord 'a' + n - 10)
425 | otherwise = undefined
428 openInput :: FilePath -> IO Lazy.ByteString
429 openInput "-" = L.getContents
430 openInput fpath = L.readFile fpath
433 openOutput :: [CmdOpt] -> IO Handle
435 = let outputOpts = filter (\ x -> case x of
441 (OptOutput fpath):[] -> openFile fpath WriteMode
442 _ -> error "two many --output options."
446 作られるファイルの例 (壓縮されない場合):
447 ------------------------------------------------------------------------------
448 {- DO NOT EDIT THIS FILE.
449 This file is automatically generated by the lucu-implant-file program.
452 Original Length: 302 bytes
453 Compressed Length: 453 bytes -- これは Compression: disabled の時には無い
454 Compression: disabled
456 ETag: d41d8cd98f00b204e9800998ecf8427e
457 Last Modified: 2007-11-05 13:53:42.231882 JST
459 module Foo.Bar.Baz (baz) where
460 import Codec.Binary.Base64
461 import qualified Data.ByteString.Lazy as L
463 import Network.HTTP.Lucu
467 resUsesNativeThread = False
468 , resIsGreedy = False
470 = Just (do foundEntity entityTag lastModified
471 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 = read "image/png"
488 rawData :: L.ByteString
489 rawData = L.pack (decode "IyEvdXNyL2Jpbi9lbnYgcnVuZ2hjCgppbXBvcnQgRGlzdHJ...")
490 ------------------------------------------------------------------------------
493 ------------------------------------------------------------------------------
496 import Codec.Compression.GZip
498 -- ResourceDef は次のやうに變化
501 resUsesNativeThread = False
502 , resIsGreedy = False
504 = Just (do foundEntity entityTag lastModified
505 setContentType contentType
507 mustGunzip <- liftM not (isEncodingAcceptable "gzip")
509 outputLBS (decompress gzippedData)
511 do setContentEncoding ["gzip"]
512 outputLBS gzippedData
516 , resDelete = Nothing
519 -- rawData の代はりに gzippedData
520 gzippedData :: L.ByteString
521 gzippedData = L.pack (decode "Otb/+DniOlRgAAAAYAAAAGAAAAB/6QOmToAEIGAAAAB...")
522 ------------------------------------------------------------------------------