-
Notifications
You must be signed in to change notification settings - Fork 197
Architecture: types
All that the server needs to know about a feature is in the HackageModule
type:
data HackageModule = HackageModule {
featureName :: String,
resources :: [Resource],
dumpBackup :: Maybe (BlobStorage -> IO [BackupEntry]),
restoreBackup :: Maybe (BlobStorage -> RestoreBackup)
}
The name should be an alphabetical string, preferrably just one word, which shouldn't conflict with other features' names.
The [Resource]
field is the most important one: it defines which pages the feature will serve. If you make a Resource
at /animal/:type/:name
, then visiting http://website.com/animal/monkey/alexander
will ask that Resource what the best response is.
The two backup fields, which are both optional, can be used to export and import a human-readable representation of a feature's data. This is not used for persistent state (since happstack-state is), only for periodic snapshots.
Features will probably want to provide additional information other than what's available in the HackageModule
field. For that reason, the actual feature object can be any data structure whatsoever, so long as you can get a HackageModule
from it. In other words, it has to implement the HackageFeature
typeclass:
class HackageFeature a where
getFeature :: a -> HackageModule
initHooks :: a -> [IO ()]
(The initHooks are a list of miscellaneous actions to do on start up. This includes running hooks, initializing caches, forking maintenance threads, and so on. The default implementation of this is const []
.)
Each feature provides an initialization function for its own feature object. This function usually just takes the global server config and possibly other feature objects as its argument. It's up to the top-level server code to pass the initialization function the required parameters. Once all of the feature objects have been initialized, all of their initHooks are called. Then, getFeature is called on all of the feature objects, yielding a list of HackageModule
s. Depending on the startup mode, this list can be used to import a package tarball, export one, or start serving web pages.
data Server = Server {
serverTxControl :: MVar TxControl,
serverFeatures :: [HackageModule],
serverPort :: Int,
serverConfig :: Config
}
This is the top-level server type, and it's created in preperation of calling simpleHTTP. The happstack-state control is created before initializing the features. If there comes a time when transaction controls can be split up by handle instead of requiring a global IORef, maybe this could be split off into features which 'own' each data component. The [HackageModule]
field is created from hackageFeatures :: Config -> IO [HackageModule]
in Distribution.Server.Features
. The rest are from config options on command line.
data Config = Config {
serverStore :: BlobStorage,
serverStaticDir :: FilePath,
serverURI :: URI.URIAuth
}
Come straight from command line options. All features are passed this structure when they are initialized.
PackageIndex
operations are defined in Distribution.Server.PackageIndex
. It should be imported qualified.
The central PackageIndex PkgInfo
, from PackagesState
in Distribution.Server.Packages.State
, is essentially a Map PackageName [PkgInfo]
which obeys certain invariants (no duplicate versions, no non-empty lists, etc.). This type comes straight from the cabal-install codebase. Although presently the only querying function is GetPackagesState
, the modification functions are InsertPkgIfAbsent
and MergePkg
; call them with their wrappers from the core feature to activate the package index-syncing hooks.
PkgInfo
comes from Distribution.Server.Packages.Types
. It is the primary type of a package. It takes up some space, I think, which could be reduced by an option to parse cabal files on the fly.
-- | The information we keep about a particular version of a package.
--
-- Previous versions of this package name and version may exist as well.
-- We normally disallow re-uploading but may make occasional exceptions.
data PkgInfo = PkgInfo {
pkgInfoId :: !PackageIdentifier,
-- | The information held in a parsed .cabal file (used by cabal-install)
pkgDesc :: !GenericPackageDescription,
-- | The .cabal file text.
pkgData :: !ByteString,
-- | The actual package .tar.gz file. It is optional for making an incomplete
-- mirror, e.g. using archives of just the latest packages, or perhaps for a
-- multipart upload process.
-- The canonical tarball URL points to the most recently uploaded package.
pkgTarball :: ![(BlobId, UploadInfo)],
-- | Previous data. The UploadInfo does *not* indicate when the ByteString was
-- uploaded, but rather when it was replaced. This way, pkgUploadData won't change
-- even if a cabal file is changed.
-- Should be updated whenever a tarball is uploaded (see mergePkg state function)
pkgDataOld :: ![(ByteString, UploadInfo)],
-- | When the package was created. Imports will override this with time in their logs.
pkgUploadData :: !UploadInfo
}
type UploadInfo = (UTCTime, UserId)
This is the central user database in Hackage, in Distribution.Server.Users.*
modules, particularly Types
, Users
, and State
. Every user has a unique UserId
(a wrapper around an Int
).
data Users = Users {
-- | A map from UserId to UserInfo
userIdMap :: !(IntMap UserInfo),
-- | A map from active UserNames to the UserId for that name
userNameMap :: !(Map UserName UserId),
-- | A map from a UserName to all UserIds which ever used that name
totalNameMap :: !(Map UserName IntSet),
-- | The next available UserId
nextId :: !UserId
}
newtype UserId = UserId Int
newtype UserName = UserName String
data UserInfo = UserInfo {
userName :: UserName,
userStatus :: UserStatus
}
data UserStatus = Deleted
| Historical
| Active !AccountEnabled UserAuth
data AccountEnabled = Enabled | Disabled
-- Distribution.Server.Users.Types
data UserAuth = UserAuth PasswdHash AuthType
-- Distribution.Server.Auth.Types
newtype PasswdPlain = PasswdPlain String
newtype PasswdHash = PasswdHash String
data AuthType = BasicAuth | DigestAuth
Authorization in Hackage gives a choice on authentication type: a password with a basic (crypt) hash can only be used for basic authentication, or any other sort of authentication that requires the browser to send the username and password in plain text.
Digest auth allows for the use of the more secure HTTP digest authorization, but it cannot be obtained from crypted information. It can also be used for basic authentication. It's preferred over the BasicAuth
format because of its flexibility.