Yesod 静态文件类型安全路由变量不在范围内
Yesod static file type safe route variable not in scope
在下面的代码中,静态文件夹中有一张图片:bsd.jpg。
在 ajax 成功方法中,我想将 img src 指向静态图像并显示它。我不明白为什么我 运行 这段代码会出现错误:
upload.hs:91:22: error: Variable not in scope: bsd_jpg :: Route Static
#!/usr/bin/env stack
{- stack
--resolver lts-8.19
--install-ghc
runghc
--package yesod
--package yesod-static
--package persistent-sqlite
-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE ViewPatterns #-}
import Yesod
import Yesod.Static
import Data.Time (UTCTime)
import System.FilePath
import System.Directory (removeFile, doesFileExist, createDirectoryIfMissing)
import Control.Applicative ((<$>), (<*>))
import Control.Monad.Logger (runStdoutLoggingT)
import Data.Conduit
import qualified Data.Text as T
import Data.Text (unpack)
import qualified Data.ByteString.Lazy as DBL
import Data.Conduit.List (consume)
import Database.Persist
import Database.Persist.Sqlite
import Data.Time (getCurrentTime)
import qualified Data.Conduit.Text as CT
import qualified Data.Conduit.List as CL
share [mkPersist sqlSettings,mkMigrate "migrateAll"] [persistUpperCase|
Image
filename String
description Textarea Maybe
date UTCTime
deriving Show
|]
data App = App
{ getStatic :: Static -- ^ Settings for static file serving.
, connPool :: ConnectionPool
}
mkYesod "App" [parseRoutes|
/ ImagesR GET POST
/image/#ImageId ImageR DELETE
/static StaticR Static getStatic
/echo-body EchoBodyR PUT
|]
instance Yesod App where
maximumContentLength _ (Just ImagesR) = Just $ 200 * 1024 * 1024 -- 200 megabytes
maximumContentLength _ _ = Just $ 10 * 1024 * 1024 -- 10 megabytes
instance YesodPersist App where
type YesodPersistBackend App = SqlBackend
runDB action = do
App _ pool <- getYesod
runSqlPool action pool
instance RenderMessage App FormMessage where
renderMessage _ _ = defaultFormMessage
uploadDirectory :: FilePath
uploadDirectory = "static"
uploadForm :: Html -> MForm Handler (FormResult (FileInfo, Maybe Textarea, UTCTime), Widget)
uploadForm = renderBootstrap $ (,,)
<$> fileAFormReq "Image file"
<*> aopt textareaField "Image description" Nothing
<*> lift (liftIO getCurrentTime)
addStyle :: Widget
addStyle = do
-- Twitter Bootstrap
addStylesheetRemote "http://netdna.bootstrapcdn.com/twitter-bootstrap/2.1.0/css/bootstrap-combined.min.css"
addStylesheetRemote "https://www.w3schools.com/w3css/4/w3.css"
-- message style
toWidget [lucius|.message { padding: 10px 0; background: #ffffed; } |]
-- jQuery
addScriptRemote "http://ajax.googleapis.com/ajax/libs/jquery/1.8.0/jquery.min.js"
-- delete function
toWidget [julius|
$(function(){
function confirmDelete(link) {
if (confirm("Are you sure you want to delete this image?")) {
deleteImage(link);
};
}
function deleteImage(link) {
$.ajax({
type: "DELETE",
url: link.attr("data-img-url"),
}).done(function(msg) {
var table = link.closest("table");
link.closest("tr").remove();
var rowCount = $("td", table).length;
if (rowCount === 0) {
table.remove();
}
});
}
$("#screenshot").click(function(){
$.ajax({
// sending a JSON encoded body
contentType: "application/json",
// don't process the body, we'll render data into a valid string
processData: false,
url: "@{EchoBodyR}",
type: "PUT",
// notice the usage of stringify here
data: JSON.stringify([{name:"Alice",age:25}, {name:"Bob",age:30}]),
success: function(data) {
alert(data.body);
$("#screenshot-pic").attr("src","@{StaticR bsd_jpg}");
},
// this only refers to the data type of the *returned* data
dataType: "json"
});
return false;
});
$("a.delete").click(function() {
confirmDelete($(this));
return false;
});
});
|]
putEchoBodyR :: Handler Value
putEchoBodyR = do
texts <- rawRequestBody $$ CT.decode CT.utf8 =$ CL.consume
return $ object ["body" .= T.concat texts]
getImagesR :: Handler Html
getImagesR = do
((_, widget), enctype) <- runFormPost uploadForm
images <- runDB $ selectList [ImageFilename !=. ""] [Desc ImageDate]
mmsg <- getMessage
defaultLayout $ do
addStyle
[whamlet|$newline never
$maybe msg <- mmsg
<div .message>
<div .container>
#{msg}
<div .container>
<div .row>
<h2>
Upload new image
<button #screenshot class="w3-button w3-teal">Screenshot
<img #screenshot-pic class="w3-round">
<div .form-actions>
<form method=post enctype=#{enctype}>
^{widget}
<input .btn type=submit value="Upload">
$if not $ null images
<table .table>
<tr>
<th>
Image
<th>
Decription
<th>
Uploaded
<th>
Action
$forall Entity imageId image <- images
<tr>
<td>
<a href=#{imageFilePath $ imageFilename image}>
#{imageFilename image}
<td>
$maybe description <- imageDescription image
#{description}
<td>
#{show $ imageDate image}
<td>
<a href=# .delete data-img-url=@{ImageR imageId}>
delete
|]
postImagesR :: Handler Html
postImagesR = do
((result, widget), enctype) <- runFormPost uploadForm
case result of
FormSuccess (file, info, date) -> do
-- TODO: check if image already exists
-- save to image directory
filename <- writeToServer file
_ <- runDB $ insert (Image filename info date)
setMessage "Image saved"
redirect ImagesR
_ -> do
setMessage "Something went wrong"
redirect ImagesR
deleteImageR :: ImageId -> Handler ()
deleteImageR imageId = do
image <- runDB $ get404 imageId
let filename = imageFilename image
path = imageFilePath filename
liftIO $ removeFile path
-- only delete from database if file has been removed from server
stillExists <- liftIO $ doesFileExist path
case (not stillExists) of
False -> redirect ImagesR
True -> do
runDB $ delete imageId
setMessage "Image has been deleted."
redirect ImagesR
writeToServer :: FileInfo -> Handler FilePath
writeToServer file = do
let filename = unpack $ fileName file
path = imageFilePath filename
liftIO $ fileMove file path
return filename
imageFilePath :: String -> FilePath
imageFilePath f = uploadDirectory </> f
openConnectionCount :: Int
openConnectionCount = 10
main :: IO ()
main = do
pool <- runStdoutLoggingT $ createSqlitePool "images.db3" openConnectionCount
runSqlPool (runMigration migrateAll) pool
-- Get the static subsite, as well as the settings it is based on
createDirectoryIfMissing True uploadDirectory
static@(Static settings) <- static uploadDirectory
warp 3000 $ App static pool
我没看到你在哪里
import Yesod.Static (staticFiles)
-- This generates easy references to files in the static directory at compile time,
-- giving you compile-time verification that referenced files exist.
-- Warning: any files added to your static directory during run-time can't be
-- accessed this way. You'll have to use their FilePath or URL to access them.
--
-- For example, to refer to @static/js/script.js@ via an identifier, you'd use:
--
-- js_script_js
--
-- If the identifier is not available, you may use:
--
-- StaticFile ["js", "script.js"] []
staticFiles (appStaticDir compileTimeAppSettings)
此代码取自默认的基于 sqlite 的 Yesod 脚手架。如果您刚开始玩 Yesod,我建议您从它开始 (stack templates
+ stack new <selected-template>
)。
请注意 staticFiles
是模板 Haskell 函数,使用这些函数时有一些限制。具体来说,您不能使用来自生成它的同一个 .hs 文件的生成代码。
在下面的代码中,静态文件夹中有一张图片:bsd.jpg。 在 ajax 成功方法中,我想将 img src 指向静态图像并显示它。我不明白为什么我 运行 这段代码会出现错误:
upload.hs:91:22: error: Variable not in scope: bsd_jpg :: Route Static
#!/usr/bin/env stack
{- stack
--resolver lts-8.19
--install-ghc
runghc
--package yesod
--package yesod-static
--package persistent-sqlite
-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE ViewPatterns #-}
import Yesod
import Yesod.Static
import Data.Time (UTCTime)
import System.FilePath
import System.Directory (removeFile, doesFileExist, createDirectoryIfMissing)
import Control.Applicative ((<$>), (<*>))
import Control.Monad.Logger (runStdoutLoggingT)
import Data.Conduit
import qualified Data.Text as T
import Data.Text (unpack)
import qualified Data.ByteString.Lazy as DBL
import Data.Conduit.List (consume)
import Database.Persist
import Database.Persist.Sqlite
import Data.Time (getCurrentTime)
import qualified Data.Conduit.Text as CT
import qualified Data.Conduit.List as CL
share [mkPersist sqlSettings,mkMigrate "migrateAll"] [persistUpperCase|
Image
filename String
description Textarea Maybe
date UTCTime
deriving Show
|]
data App = App
{ getStatic :: Static -- ^ Settings for static file serving.
, connPool :: ConnectionPool
}
mkYesod "App" [parseRoutes|
/ ImagesR GET POST
/image/#ImageId ImageR DELETE
/static StaticR Static getStatic
/echo-body EchoBodyR PUT
|]
instance Yesod App where
maximumContentLength _ (Just ImagesR) = Just $ 200 * 1024 * 1024 -- 200 megabytes
maximumContentLength _ _ = Just $ 10 * 1024 * 1024 -- 10 megabytes
instance YesodPersist App where
type YesodPersistBackend App = SqlBackend
runDB action = do
App _ pool <- getYesod
runSqlPool action pool
instance RenderMessage App FormMessage where
renderMessage _ _ = defaultFormMessage
uploadDirectory :: FilePath
uploadDirectory = "static"
uploadForm :: Html -> MForm Handler (FormResult (FileInfo, Maybe Textarea, UTCTime), Widget)
uploadForm = renderBootstrap $ (,,)
<$> fileAFormReq "Image file"
<*> aopt textareaField "Image description" Nothing
<*> lift (liftIO getCurrentTime)
addStyle :: Widget
addStyle = do
-- Twitter Bootstrap
addStylesheetRemote "http://netdna.bootstrapcdn.com/twitter-bootstrap/2.1.0/css/bootstrap-combined.min.css"
addStylesheetRemote "https://www.w3schools.com/w3css/4/w3.css"
-- message style
toWidget [lucius|.message { padding: 10px 0; background: #ffffed; } |]
-- jQuery
addScriptRemote "http://ajax.googleapis.com/ajax/libs/jquery/1.8.0/jquery.min.js"
-- delete function
toWidget [julius|
$(function(){
function confirmDelete(link) {
if (confirm("Are you sure you want to delete this image?")) {
deleteImage(link);
};
}
function deleteImage(link) {
$.ajax({
type: "DELETE",
url: link.attr("data-img-url"),
}).done(function(msg) {
var table = link.closest("table");
link.closest("tr").remove();
var rowCount = $("td", table).length;
if (rowCount === 0) {
table.remove();
}
});
}
$("#screenshot").click(function(){
$.ajax({
// sending a JSON encoded body
contentType: "application/json",
// don't process the body, we'll render data into a valid string
processData: false,
url: "@{EchoBodyR}",
type: "PUT",
// notice the usage of stringify here
data: JSON.stringify([{name:"Alice",age:25}, {name:"Bob",age:30}]),
success: function(data) {
alert(data.body);
$("#screenshot-pic").attr("src","@{StaticR bsd_jpg}");
},
// this only refers to the data type of the *returned* data
dataType: "json"
});
return false;
});
$("a.delete").click(function() {
confirmDelete($(this));
return false;
});
});
|]
putEchoBodyR :: Handler Value
putEchoBodyR = do
texts <- rawRequestBody $$ CT.decode CT.utf8 =$ CL.consume
return $ object ["body" .= T.concat texts]
getImagesR :: Handler Html
getImagesR = do
((_, widget), enctype) <- runFormPost uploadForm
images <- runDB $ selectList [ImageFilename !=. ""] [Desc ImageDate]
mmsg <- getMessage
defaultLayout $ do
addStyle
[whamlet|$newline never
$maybe msg <- mmsg
<div .message>
<div .container>
#{msg}
<div .container>
<div .row>
<h2>
Upload new image
<button #screenshot class="w3-button w3-teal">Screenshot
<img #screenshot-pic class="w3-round">
<div .form-actions>
<form method=post enctype=#{enctype}>
^{widget}
<input .btn type=submit value="Upload">
$if not $ null images
<table .table>
<tr>
<th>
Image
<th>
Decription
<th>
Uploaded
<th>
Action
$forall Entity imageId image <- images
<tr>
<td>
<a href=#{imageFilePath $ imageFilename image}>
#{imageFilename image}
<td>
$maybe description <- imageDescription image
#{description}
<td>
#{show $ imageDate image}
<td>
<a href=# .delete data-img-url=@{ImageR imageId}>
delete
|]
postImagesR :: Handler Html
postImagesR = do
((result, widget), enctype) <- runFormPost uploadForm
case result of
FormSuccess (file, info, date) -> do
-- TODO: check if image already exists
-- save to image directory
filename <- writeToServer file
_ <- runDB $ insert (Image filename info date)
setMessage "Image saved"
redirect ImagesR
_ -> do
setMessage "Something went wrong"
redirect ImagesR
deleteImageR :: ImageId -> Handler ()
deleteImageR imageId = do
image <- runDB $ get404 imageId
let filename = imageFilename image
path = imageFilePath filename
liftIO $ removeFile path
-- only delete from database if file has been removed from server
stillExists <- liftIO $ doesFileExist path
case (not stillExists) of
False -> redirect ImagesR
True -> do
runDB $ delete imageId
setMessage "Image has been deleted."
redirect ImagesR
writeToServer :: FileInfo -> Handler FilePath
writeToServer file = do
let filename = unpack $ fileName file
path = imageFilePath filename
liftIO $ fileMove file path
return filename
imageFilePath :: String -> FilePath
imageFilePath f = uploadDirectory </> f
openConnectionCount :: Int
openConnectionCount = 10
main :: IO ()
main = do
pool <- runStdoutLoggingT $ createSqlitePool "images.db3" openConnectionCount
runSqlPool (runMigration migrateAll) pool
-- Get the static subsite, as well as the settings it is based on
createDirectoryIfMissing True uploadDirectory
static@(Static settings) <- static uploadDirectory
warp 3000 $ App static pool
我没看到你在哪里
import Yesod.Static (staticFiles)
-- This generates easy references to files in the static directory at compile time,
-- giving you compile-time verification that referenced files exist.
-- Warning: any files added to your static directory during run-time can't be
-- accessed this way. You'll have to use their FilePath or URL to access them.
--
-- For example, to refer to @static/js/script.js@ via an identifier, you'd use:
--
-- js_script_js
--
-- If the identifier is not available, you may use:
--
-- StaticFile ["js", "script.js"] []
staticFiles (appStaticDir compileTimeAppSettings)
此代码取自默认的基于 sqlite 的 Yesod 脚手架。如果您刚开始玩 Yesod,我建议您从它开始 (stack templates
+ stack new <selected-template>
)。
请注意 staticFiles
是模板 Haskell 函数,使用这些函数时有一些限制。具体来说,您不能使用来自生成它的同一个 .hs 文件的生成代码。