mirror of
https://github.com/commercialhaskell/stackage-server.git
synced 2026-01-23 09:21:56 +01:00
Blob storage and downloading from Hackage
This commit is contained in:
parent
3c5637dc6d
commit
95250c5b09
1
.gitignore
vendored
1
.gitignore
vendored
@ -12,3 +12,4 @@ yesod-devel/
|
|||||||
cabal.sandbox.config
|
cabal.sandbox.config
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.swp
|
*.swp
|
||||||
|
/dev-blob-store/
|
||||||
|
|||||||
@ -16,13 +16,16 @@ import Network.Wai.Middleware.RequestLogger
|
|||||||
import qualified Network.Wai.Middleware.RequestLogger as RequestLogger
|
import qualified Network.Wai.Middleware.RequestLogger as RequestLogger
|
||||||
import qualified Database.Persist
|
import qualified Database.Persist
|
||||||
import Control.Monad.Logger (runLoggingT)
|
import Control.Monad.Logger (runLoggingT)
|
||||||
|
import Control.Monad.Reader (runReaderT)
|
||||||
import Control.Concurrent (forkIO, threadDelay)
|
import Control.Concurrent (forkIO, threadDelay)
|
||||||
import System.Log.FastLogger (newStdoutLoggerSet, defaultBufSize)
|
import System.Log.FastLogger (newStdoutLoggerSet, defaultBufSize, flushLogStr)
|
||||||
import Network.Wai.Logger (clockDateCacher)
|
import Network.Wai.Logger (clockDateCacher)
|
||||||
import Yesod.Core.Types (loggerSet, Logger (Logger))
|
import Yesod.Core.Types (loggerSet, Logger (Logger))
|
||||||
import qualified System.Random.MWC as MWC
|
import qualified System.Random.MWC as MWC
|
||||||
import qualified Network.Wai as Wai
|
import qualified Network.Wai as Wai
|
||||||
import Network.Wai.Middleware.MethodOverride (methodOverride)
|
import Network.Wai.Middleware.MethodOverride (methodOverride)
|
||||||
|
import Data.BlobStore (fileStore)
|
||||||
|
import Data.Hackage
|
||||||
|
|
||||||
-- Import all relevant handler modules here.
|
-- Import all relevant handler modules here.
|
||||||
-- Don't forget to add new modules to your cabal file!
|
-- Don't forget to add new modules to your cabal file!
|
||||||
@ -30,6 +33,7 @@ import Handler.Home
|
|||||||
import Handler.Profile
|
import Handler.Profile
|
||||||
import Handler.Email
|
import Handler.Email
|
||||||
import Handler.ResetToken
|
import Handler.ResetToken
|
||||||
|
import Handler.HackageSdist
|
||||||
|
|
||||||
-- This line actually creates our YesodDispatch instance. It is the second half
|
-- This line actually creates our YesodDispatch instance. It is the second half
|
||||||
-- of the call to mkYesodData which occurs in Foundation.hs. Please see the
|
-- of the call to mkYesodData which occurs in Foundation.hs. Please see the
|
||||||
@ -83,6 +87,7 @@ makeFoundation conf = do
|
|||||||
let updateLoop = do
|
let updateLoop = do
|
||||||
threadDelay 1000000
|
threadDelay 1000000
|
||||||
updater
|
updater
|
||||||
|
flushLogStr loggerSet' -- FIXME include upstream!
|
||||||
updateLoop
|
updateLoop
|
||||||
_ <- forkIO updateLoop
|
_ <- forkIO updateLoop
|
||||||
|
|
||||||
@ -97,6 +102,9 @@ makeFoundation conf = do
|
|||||||
, persistConfig = dbconf
|
, persistConfig = dbconf
|
||||||
, appLogger = logger
|
, appLogger = logger
|
||||||
, genIO = gen
|
, genIO = gen
|
||||||
|
, blobStore =
|
||||||
|
case storeConfig $ appExtra conf of
|
||||||
|
BSCFile root -> fileStore root
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Perform database migration using our application's logging settings.
|
-- Perform database migration using our application's logging settings.
|
||||||
@ -104,6 +112,15 @@ makeFoundation conf = do
|
|||||||
(Database.Persist.runPool dbconf (runMigration migrateAll) p)
|
(Database.Persist.runPool dbconf (runMigration migrateAll) p)
|
||||||
(messageLoggerSource foundation logger)
|
(messageLoggerSource foundation logger)
|
||||||
|
|
||||||
|
-- Start the cabal file loader
|
||||||
|
void $ forkIO $ forever $ flip runLoggingT (messageLoggerSource foundation logger) $ do
|
||||||
|
when development $ liftIO $ threadDelay $ 5 * 60 * 1000000
|
||||||
|
eres <- tryAny $ runReaderT loadCabalFiles foundation
|
||||||
|
case eres of
|
||||||
|
Left e -> $logError $ tshow e
|
||||||
|
Right () -> return ()
|
||||||
|
liftIO $ threadDelay $ 30 * 60 * 1000000
|
||||||
|
|
||||||
return foundation
|
return foundation
|
||||||
|
|
||||||
-- for yesod devel
|
-- for yesod devel
|
||||||
|
|||||||
94
Data/BlobStore.hs
Normal file
94
Data/BlobStore.hs
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
module Data.BlobStore
|
||||||
|
( BlobStore (..)
|
||||||
|
, ToPath (..)
|
||||||
|
, fileStore
|
||||||
|
, HasBlobStore (..)
|
||||||
|
, storeWrite
|
||||||
|
, storeRead
|
||||||
|
, storeExists
|
||||||
|
) where
|
||||||
|
|
||||||
|
import ClassyPrelude.Yesod
|
||||||
|
import qualified Filesystem as F
|
||||||
|
import Control.Monad.Reader (MonadReader, ask)
|
||||||
|
import Control.Monad.Trans.Resource (release)
|
||||||
|
import qualified Aws
|
||||||
|
|
||||||
|
data BlobStore key = BlobStore
|
||||||
|
{ storeWrite' :: !(forall m. MonadIO m => key -> Acquire (Sink ByteString m ()))
|
||||||
|
, storeRead' :: !(forall m. MonadIO m => key -> Acquire (Maybe (Source m ByteString)))
|
||||||
|
, storeExists' :: !(forall m. MonadIO m => key -> m Bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
class HasBlobStore a key | a -> key where
|
||||||
|
getBlobStore :: a -> BlobStore key
|
||||||
|
instance HasBlobStore (BlobStore key) key where
|
||||||
|
getBlobStore = id
|
||||||
|
|
||||||
|
storeWrite :: (MonadResource m, MonadReader env m, HasBlobStore env key)
|
||||||
|
=> key
|
||||||
|
-> Consumer ByteString m ()
|
||||||
|
storeWrite key = do
|
||||||
|
store <- liftM getBlobStore ask
|
||||||
|
(releaseKey, sink) <- allocateAcquire $ storeWrite' store key
|
||||||
|
toConsumer sink
|
||||||
|
release releaseKey
|
||||||
|
|
||||||
|
storeRead :: (MonadResource m, MonadReader env m, HasBlobStore env key)
|
||||||
|
=> key
|
||||||
|
-> m (Maybe (Source m ByteString))
|
||||||
|
storeRead key = do
|
||||||
|
store <- liftM getBlobStore ask
|
||||||
|
(releaseKey, msrc) <- allocateAcquire $ storeRead' store key
|
||||||
|
case msrc of
|
||||||
|
Nothing -> do
|
||||||
|
release releaseKey
|
||||||
|
return Nothing
|
||||||
|
Just src -> return $ Just $ src >> release releaseKey
|
||||||
|
|
||||||
|
storeExists :: (MonadIO m, MonadReader env m, HasBlobStore env key)
|
||||||
|
=> key
|
||||||
|
-> m Bool
|
||||||
|
storeExists key = do
|
||||||
|
store <- liftM getBlobStore ask
|
||||||
|
storeExists' store key
|
||||||
|
|
||||||
|
class ToPath a where
|
||||||
|
toPath :: a -> [Text]
|
||||||
|
|
||||||
|
fileStore :: ToPath key
|
||||||
|
=> FilePath -- ^ root
|
||||||
|
-> BlobStore key
|
||||||
|
fileStore root = BlobStore
|
||||||
|
{ storeWrite' = \key -> sinkHandle <$> mkAcquire
|
||||||
|
(do
|
||||||
|
let fp = toFP key
|
||||||
|
F.createTree $ directory fp
|
||||||
|
F.openFile fp F.WriteMode)
|
||||||
|
hClose
|
||||||
|
, storeRead' = \key -> (fmap sourceHandle) <$> mkAcquire
|
||||||
|
((Just <$> F.openFile (toFP key) F.ReadMode)
|
||||||
|
`catch` \e ->
|
||||||
|
if isDoesNotExistError e
|
||||||
|
then return Nothing
|
||||||
|
else throwIO e)
|
||||||
|
(maybe (return ()) hClose)
|
||||||
|
, storeExists' = liftIO . F.isFile . toFP
|
||||||
|
}
|
||||||
|
where
|
||||||
|
toFP key = foldl' (\x y -> x </> fpFromText y) root (toPath key)
|
||||||
|
|
||||||
|
{-
|
||||||
|
-- | Note: Only use with data which will never be modified!
|
||||||
|
cachedS3Store :: (BackupToS3 key, ToPath key)
|
||||||
|
=> FilePath -- ^ cache directory
|
||||||
|
-> Aws.Bucket
|
||||||
|
-> Text -- ^ prefix within bucket
|
||||||
|
-> BlobStore key
|
||||||
|
cachedS3Store cache bucket prefix = BlobStore
|
||||||
|
{ storeWrite' = \key ->
|
||||||
|
}
|
||||||
|
|
||||||
|
class BackupToS3 key where
|
||||||
|
shouldBackup :: key -> Bool
|
||||||
|
-}
|
||||||
103
Data/Hackage.hs
Normal file
103
Data/Hackage.hs
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
module Data.Hackage
|
||||||
|
( loadCabalFiles
|
||||||
|
, sourceHackageSdist
|
||||||
|
) where
|
||||||
|
|
||||||
|
import ClassyPrelude.Yesod
|
||||||
|
import Types
|
||||||
|
import Data.BlobStore
|
||||||
|
import Data.Conduit.Lazy (MonadActive (..), lazyConsume)
|
||||||
|
import Control.Monad.Logger (LoggingT)
|
||||||
|
import qualified Codec.Archive.Tar as Tar
|
||||||
|
import Control.Monad.Reader (MonadReader, ask)
|
||||||
|
import Control.Monad.Trans.Resource (release)
|
||||||
|
import qualified Data.Text as T
|
||||||
|
import Data.Conduit.Zlib (ungzip)
|
||||||
|
|
||||||
|
loadCabalFiles :: ( MonadActive m
|
||||||
|
, MonadBaseControl IO m
|
||||||
|
, MonadThrow m
|
||||||
|
, MonadIO m
|
||||||
|
, MonadReader env m
|
||||||
|
, HasHttpManager env
|
||||||
|
, HasBlobStore env StoreKey
|
||||||
|
, HasHackageRoot env
|
||||||
|
, MonadLogger m
|
||||||
|
)
|
||||||
|
=> m ()
|
||||||
|
loadCabalFiles = do
|
||||||
|
HackageRoot root <- liftM getHackageRoot ask
|
||||||
|
$logDebug $ "Entering loadCabalFiles, root == " ++ root
|
||||||
|
req <- parseUrl $ unpack $ root ++ "/00-index.tar.gz"
|
||||||
|
withResponse req $ \res -> do
|
||||||
|
$logDebug $ "Got a response, processing"
|
||||||
|
bss <- lazyConsume $ responseBody res $= ungzip
|
||||||
|
loop $ Tar.read $ fromChunks bss
|
||||||
|
where
|
||||||
|
loop (Tar.Next entry entries) = go entry >> loop entries
|
||||||
|
loop Tar.Done = return ()
|
||||||
|
loop (Tar.Fail e) = throwM e
|
||||||
|
|
||||||
|
go entry = do
|
||||||
|
case Tar.entryContent entry of
|
||||||
|
Tar.NormalFile lbs _
|
||||||
|
| Just (name, version) <- parseFilePath (Tar.entryPath entry) -> do
|
||||||
|
let key = HackageCabal name version
|
||||||
|
exists <- storeExists key
|
||||||
|
store <- liftM getBlobStore ask
|
||||||
|
unless exists $ withAcquire (storeWrite' store key) $ \sink ->
|
||||||
|
sourceLazy lbs $$ sink
|
||||||
|
_ -> return ()
|
||||||
|
|
||||||
|
parseFilePath :: String -> Maybe (PackageName, Version)
|
||||||
|
parseFilePath s =
|
||||||
|
case filter (not . null) $ T.split (== '/') $ pack s of
|
||||||
|
(name:version:_) -> Just (PackageName name, Version version)
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
|
sourceHackageSdist :: ( MonadIO m
|
||||||
|
, MonadThrow m
|
||||||
|
, MonadBaseControl IO m
|
||||||
|
, MonadResource m
|
||||||
|
, MonadReader env m
|
||||||
|
, HasHttpManager env
|
||||||
|
, HasHackageRoot env
|
||||||
|
, HasBlobStore env StoreKey
|
||||||
|
)
|
||||||
|
=> PackageName
|
||||||
|
-> Version
|
||||||
|
-> m (Maybe (Source m ByteString))
|
||||||
|
sourceHackageSdist name version = do
|
||||||
|
let key = HackageSdist name version
|
||||||
|
msrc1 <- storeRead key
|
||||||
|
case msrc1 of
|
||||||
|
Just src -> return $ Just src
|
||||||
|
Nothing -> do
|
||||||
|
HackageRoot root <- liftM getHackageRoot ask
|
||||||
|
let url = concat
|
||||||
|
[ root
|
||||||
|
, "/"
|
||||||
|
, toPathPiece name
|
||||||
|
, "/"
|
||||||
|
, toPathPiece version
|
||||||
|
, "/"
|
||||||
|
, toPathPiece name
|
||||||
|
, "-"
|
||||||
|
, toPathPiece version
|
||||||
|
, ".tar.gz"
|
||||||
|
]
|
||||||
|
req' <- parseUrl $ unpack url
|
||||||
|
let req = req' { checkStatus = \_ _ _ -> Nothing }
|
||||||
|
exists <- withResponse req $ \res ->
|
||||||
|
if responseStatus res == status200
|
||||||
|
then do
|
||||||
|
responseBody res $$ storeWrite key
|
||||||
|
return True
|
||||||
|
else return False
|
||||||
|
if exists
|
||||||
|
then storeRead key
|
||||||
|
else return Nothing
|
||||||
|
|
||||||
|
-- FIXME orphan
|
||||||
|
instance MonadActive m => MonadActive (LoggingT m) where
|
||||||
|
monadActive = lift monadActive
|
||||||
@ -17,6 +17,8 @@ import Text.Hamlet (hamletFile)
|
|||||||
import Yesod.Core.Types (Logger)
|
import Yesod.Core.Types (Logger)
|
||||||
import Data.Slug (safeMakeSlug, HasGenIO (getGenIO), randomSlug, Slug)
|
import Data.Slug (safeMakeSlug, HasGenIO (getGenIO), randomSlug, Slug)
|
||||||
import qualified System.Random.MWC as MWC
|
import qualified System.Random.MWC as MWC
|
||||||
|
import Data.BlobStore
|
||||||
|
import Types
|
||||||
|
|
||||||
-- | The site argument for your application. This can be a good place to
|
-- | The site argument for your application. This can be a good place to
|
||||||
-- keep settings and values requiring initialization before your application
|
-- keep settings and values requiring initialization before your application
|
||||||
@ -30,14 +32,21 @@ data App = App
|
|||||||
, persistConfig :: Settings.PersistConf
|
, persistConfig :: Settings.PersistConf
|
||||||
, appLogger :: Logger
|
, appLogger :: Logger
|
||||||
, genIO :: !MWC.GenIO
|
, genIO :: !MWC.GenIO
|
||||||
|
, blobStore :: !(BlobStore StoreKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
instance HasBlobStore App StoreKey where
|
||||||
|
getBlobStore = blobStore
|
||||||
|
|
||||||
instance HasGenIO App where
|
instance HasGenIO App where
|
||||||
getGenIO = genIO
|
getGenIO = genIO
|
||||||
|
|
||||||
instance HasHttpManager App where
|
instance HasHttpManager App where
|
||||||
getHttpManager = httpManager
|
getHttpManager = httpManager
|
||||||
|
|
||||||
|
instance HasHackageRoot App where
|
||||||
|
getHackageRoot = hackageRoot . appExtra . settings
|
||||||
|
|
||||||
-- This is where we define all of the routes in our application. For a full
|
-- This is where we define all of the routes in our application. For a full
|
||||||
-- explanation of the syntax, please see:
|
-- explanation of the syntax, please see:
|
||||||
-- http://www.yesodweb.com/book/routing-and-handlers
|
-- http://www.yesodweb.com/book/routing-and-handlers
|
||||||
|
|||||||
19
Handler/HackageSdist.hs
Normal file
19
Handler/HackageSdist.hs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
module Handler.HackageSdist where
|
||||||
|
|
||||||
|
import Import
|
||||||
|
import Data.Hackage
|
||||||
|
|
||||||
|
getHackageSdistR :: PackageName -> Version -> Handler TypedContent
|
||||||
|
getHackageSdistR name version = do
|
||||||
|
msrc <- sourceHackageSdist name version
|
||||||
|
case msrc of
|
||||||
|
Nothing -> notFound
|
||||||
|
Just src -> do
|
||||||
|
addHeader "content-disposition" $ concat
|
||||||
|
[ "attachment; filename=\""
|
||||||
|
, toPathPiece name
|
||||||
|
, "-"
|
||||||
|
, toPathPiece version
|
||||||
|
, ".tar.gz"
|
||||||
|
]
|
||||||
|
respondSource "application/x-gzip" $ mapOutput (Chunk . toBuilder) src
|
||||||
@ -5,6 +5,7 @@ module Import
|
|||||||
import ClassyPrelude.Yesod as Import
|
import ClassyPrelude.Yesod as Import
|
||||||
import Foundation as Import
|
import Foundation as Import
|
||||||
import Model as Import
|
import Model as Import
|
||||||
|
import Types as Import
|
||||||
import Settings as Import
|
import Settings as Import
|
||||||
import Settings.Development as Import
|
import Settings.Development as Import
|
||||||
import Settings.StaticFiles as Import
|
import Settings.StaticFiles as Import
|
||||||
|
|||||||
19
Settings.hs
19
Settings.hs
@ -14,6 +14,8 @@ import Yesod.Default.Util
|
|||||||
import Data.Yaml
|
import Data.Yaml
|
||||||
import Settings.Development
|
import Settings.Development
|
||||||
import Text.Hamlet
|
import Text.Hamlet
|
||||||
|
import Data.Aeson (withText)
|
||||||
|
import Types
|
||||||
|
|
||||||
-- | Which Persistent backend this site is using.
|
-- | Which Persistent backend this site is using.
|
||||||
type PersistConf = PostgresConf
|
type PersistConf = PostgresConf
|
||||||
@ -63,7 +65,22 @@ widgetFile = (if development then widgetFileReload
|
|||||||
widgetFileSettings
|
widgetFileSettings
|
||||||
|
|
||||||
data Extra = Extra
|
data Extra = Extra
|
||||||
|
{ storeConfig :: !BlobStoreConfig
|
||||||
|
, hackageRoot :: !HackageRoot
|
||||||
|
}
|
||||||
deriving Show
|
deriving Show
|
||||||
|
|
||||||
parseExtra :: DefaultEnv -> Object -> Parser Extra
|
parseExtra :: DefaultEnv -> Object -> Parser Extra
|
||||||
parseExtra _ _o = return Extra
|
parseExtra _ o = Extra
|
||||||
|
<$> o .: "blob-store"
|
||||||
|
<*> (HackageRoot <$> o .: "hackage-root")
|
||||||
|
|
||||||
|
data BlobStoreConfig = BSCFile !FilePath
|
||||||
|
deriving Show
|
||||||
|
|
||||||
|
instance FromJSON BlobStoreConfig where
|
||||||
|
parseJSON = withText "BlobStoreConfig" $ \t ->
|
||||||
|
case () of
|
||||||
|
()
|
||||||
|
| Just root <- stripPrefix "file:" t -> return $ BSCFile $ fpFromText root
|
||||||
|
| otherwise -> fail $ "Invalid BlobStoreConfig: " ++ show t
|
||||||
|
|||||||
36
Types.hs
Normal file
36
Types.hs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
module Types where
|
||||||
|
|
||||||
|
import ClassyPrelude.Yesod
|
||||||
|
import Data.BlobStore (ToPath (..))
|
||||||
|
import Text.Blaze (ToMarkup)
|
||||||
|
|
||||||
|
newtype PackageName = PackageName { unPackageName :: Text }
|
||||||
|
deriving (Show, Read, Typeable, Eq, Ord, Hashable, PathPiece, ToMarkup)
|
||||||
|
newtype Version = Version { unVersion :: Text }
|
||||||
|
deriving (Show, Read, Typeable, Eq, Ord, Hashable, PathPiece, ToMarkup)
|
||||||
|
newtype PackageSetIdent = PackageSetIdent { unPackageSetIdent :: Text }
|
||||||
|
deriving (Show, Read, Typeable, Eq, Ord, Hashable, PathPiece, ToMarkup)
|
||||||
|
|
||||||
|
data StoreKey = HackageCabal !PackageName !Version
|
||||||
|
| HackageSdist !PackageName !Version
|
||||||
|
| CabalIndex !PackageSetIdent
|
||||||
|
| CustomSdist !PackageSetIdent !PackageName !Version
|
||||||
|
|
||||||
|
instance ToPath StoreKey where
|
||||||
|
toPath (HackageCabal name version) = ["hackage", toPathPiece name, toPathPiece version ++ ".cabal"]
|
||||||
|
toPath (HackageSdist name version) = ["hackage", toPathPiece name, toPathPiece version ++ ".tar.gz"]
|
||||||
|
toPath (CabalIndex ident) = ["cabal-index", toPathPiece ident ++ ".tar.gz"]
|
||||||
|
toPath (CustomSdist ident name version) =
|
||||||
|
[ "custom-tarball"
|
||||||
|
, toPathPiece ident
|
||||||
|
, toPathPiece name
|
||||||
|
, toPathPiece version ++ ".tar.gz"
|
||||||
|
]
|
||||||
|
|
||||||
|
newtype HackageRoot = HackageRoot { unHackageRoot :: Text }
|
||||||
|
deriving (Show, Read, Typeable, Eq, Ord, Hashable, PathPiece, ToMarkup)
|
||||||
|
|
||||||
|
class HasHackageRoot a where
|
||||||
|
getHackageRoot :: a -> HackageRoot
|
||||||
|
instance HasHackageRoot HackageRoot where
|
||||||
|
getHackageRoot = id
|
||||||
@ -8,3 +8,4 @@
|
|||||||
/profile ProfileR GET PUT
|
/profile ProfileR GET PUT
|
||||||
/email/#EmailId EmailR DELETE
|
/email/#EmailId EmailR DELETE
|
||||||
/reset-token ResetTokenR POST
|
/reset-token ResetTokenR POST
|
||||||
|
/hackage/#PackageName/#Version HackageSdistR GET
|
||||||
|
|||||||
@ -2,9 +2,11 @@ Default: &defaults
|
|||||||
host: "*4" # any IPv4 host
|
host: "*4" # any IPv4 host
|
||||||
port: 3000
|
port: 3000
|
||||||
approot: "http://localhost:3000"
|
approot: "http://localhost:3000"
|
||||||
|
hackage-root: http://hackage.haskell.org/packages/archive
|
||||||
|
|
||||||
Development:
|
Development:
|
||||||
<<: *defaults
|
<<: *defaults
|
||||||
|
blob-store: file:dev-blob-store
|
||||||
|
|
||||||
Testing:
|
Testing:
|
||||||
<<: *defaults
|
<<: *defaults
|
||||||
|
|||||||
@ -20,10 +20,14 @@ library
|
|||||||
Settings.StaticFiles
|
Settings.StaticFiles
|
||||||
Settings.Development
|
Settings.Development
|
||||||
Data.Slug
|
Data.Slug
|
||||||
|
Data.BlobStore
|
||||||
|
Data.Hackage
|
||||||
|
Types
|
||||||
Handler.Home
|
Handler.Home
|
||||||
Handler.Profile
|
Handler.Profile
|
||||||
Handler.Email
|
Handler.Email
|
||||||
Handler.ResetToken
|
Handler.ResetToken
|
||||||
|
Handler.HackageSdist
|
||||||
|
|
||||||
if flag(dev) || flag(library-only)
|
if flag(dev) || flag(library-only)
|
||||||
cpp-options: -DDEVELOPMENT
|
cpp-options: -DDEVELOPMENT
|
||||||
@ -47,6 +51,9 @@ library
|
|||||||
ViewPatterns
|
ViewPatterns
|
||||||
TypeSynonymInstances
|
TypeSynonymInstances
|
||||||
FlexibleInstances
|
FlexibleInstances
|
||||||
|
RankNTypes
|
||||||
|
FunctionalDependencies
|
||||||
|
PatternGuards
|
||||||
|
|
||||||
build-depends: base >= 4 && < 5
|
build-depends: base >= 4 && < 5
|
||||||
, yesod >= 1.2.5 && < 1.3
|
, yesod >= 1.2.5 && < 1.3
|
||||||
@ -80,7 +87,11 @@ library
|
|||||||
, mtl >= 2.1 && < 2.2
|
, mtl >= 2.1 && < 2.2
|
||||||
, blaze-markup >= 0.6 && < 0.7
|
, blaze-markup >= 0.6 && < 0.7
|
||||||
, ghc-prim
|
, ghc-prim
|
||||||
, ghc-prim
|
, system-fileio
|
||||||
|
, resourcet
|
||||||
|
, aws >= 0.9 && < 0.10
|
||||||
|
, conduit-extra
|
||||||
|
, tar >= 0.4 && < 0.5
|
||||||
|
|
||||||
executable stackage-server
|
executable stackage-server
|
||||||
if flag(library-only)
|
if flag(library-only)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user