packages feed

apotiki (empty) → 0.5.1

raw patch · 19 files changed

+943/−0 lines, 19 filesdep +aesondep +attoparsecdep +basesetup-changed

Dependencies added: aeson, attoparsec, base, binary, bytestring, containers, crypto-api, cryptohash, directory, http-types, old-time, openpgp, openpgp-asciiarmor, openpgp-crypto-api, scotty, strict, tar, text, transformers, wai-extra, wai-middleware-static, zlib

Files

+ LICENSE view
@@ -0,0 +1,13 @@+Copyright 2014 Pierre-Yves Ritschard <pyr@spootnik.org>++Permission to use, copy, modify, and distribute this software for any+purpose with or without fee is hereby granted, provided that the above+notice and this permission notice appear in all copies.++THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ README.md view
@@ -0,0 +1,102 @@+apotiki: a faster debian repository+===================================++![apotiki](http://i.imgur.com/3Jmupwb.jpg)++([image source](http://commons.wikimedia.org/wiki/File:A_view_of_the_map_repository_at_The_National_Archives.jpg))++apotiki generates debian repositories fast. its goal is+to be a great companion to [fpm](https://github.com/jordansissel/fpm) and+[jenkins](http://jenkins-ci.org).++apotiki operates with the following features and constraints:++* Supports a single debian release+* Supports a single debian component+* Supports an arbitrary number of architectures which need to be preprovisionned+* Requires a valid PGP private key for signing++## The Story++You operate a production environment and rely on software that is more recent than is+available on a standard Debian or Ubuntu distribution ? Apotiki helps you distribute+software by creating a separate debian repository which you can add to your apt sources.++Turns out there's already software available for this, such as [freight](https://github.com/rcrowley/freight),+apotiki's angle is to work very fast for the most common use case.++## Companion software++[fpm](https://github.com/jordansissel/fpm) is a great tool to build Debian packages with.+It can produce packages from directories, gems, npm or pip libraries.++[jenkins](http://jenkins-ci.org) or [travis-ci](http://travis-ci.com) can produce artifacts by running+scripts.++## Using++apotiki has two modes of operation, try not to mix the two too much:++* `apotiki insert`: pushes a list of packages, given on the command line to the repo +* `apotiki web`: start up a web service on port 8000 to display the repository and accept new packages++Running apotiki with no arguments or `help` will tell you a bit about usage.++If you wish to submit packages to the repository with curl here is the relevant command line+assuming your package file is `package-foo.deb`++```bash+curl -X POST -F "package=@/path/to/package-foo.deb" http://repo-host:8000/repo+```++## Building++Apotiki is a haskell program and relies on both the ghc compiler and+cabal. They are probably already available in your platform of choice.++Once cabal is installed, just run:++```bash+cabal install+```++Alternatively, you can build apotiki with docker. Just run:++```+sudo docker build .+```++The resulting container will have the built cabal executable.++## Installing++You can either run `cabal install` locally or distribute the built+executable available in `dist/build/apotiki/apotiki`.++## Configuring++For now the configuration is a serialized haskell structure:++```haskell+ApotikiConfig {+  keyPath = "/etc/apotiki.key",      -- path to a PGP private key+  architectures = ["amd64", "i386"], -- list of supported architectures+  component = "main",                -- debian release component+  release = "precise",               -- debian release name+  label = "Apotiki",                 -- release label+  origin = "Apotiki",                -- release origin+  repoDir = "/srv/repo"              -- repository location, expose via http+}+```+The PGP private key you wish to use can be exported with:++```+gpg --export-secret-keys repository-key@your.domain > /etc/apotiki.key+```++The config file path can be controlled with the `APOTIKI_CONFIG` environment+variable.++## Caveats++Error handling is suboptimal to say the least. we'll get there.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ System/Apotiki/Ar.hs view
@@ -0,0 +1,49 @@+module System.Apotiki.Ar (arFromData, arFromFile, ArEntry (..)) where+import Data.Attoparsec.Combinator (manyTill)+import Data.ByteString.Char8 (pack, unpack)+import qualified Data.Map as M+import qualified Data.ByteString as B+import qualified Data.Attoparsec.ByteString as P++armag = "!<arch>\n" -- magic header+arfmag = "`\n" -- magic pad++data ArEntry = ArEntry { -- A file entry in an ar file+  entryDate :: Int,+  entryGid  :: Int,+  entryUid  :: Int,+  entryMode :: Int,+  entrySize :: Int,+  entryData :: B.ByteString+  } deriving Show++type ArMapEntry = (String, ArEntry)+type ArMap = M.Map String ArEntry++arEntryParser :: P.Parser ArMapEntry+arEntryParser = do+  let not_space = (\c -> (c /= '/') && (c /= ' '))+  name <- (takeWhile not_space . unpack) `fmap` P.take 16+  date <- (read . unpack) `fmap` P.take 12+  gid <- (read . unpack) `fmap` P.take 6+  uid <- (read . unpack) `fmap` P.take 6+  mode <- (read . unpack) `fmap` P.take 8+  size <- (read . unpack) `fmap` P.take 10+  magic <- P.string $ pack arfmag+  payload <- P.take size+  padding <- if size `mod` 2 == 1 then P.string $ pack "\n" else P.take 0+  return (name, ArEntry date gid uid mode size payload)++arParser :: P.Parser ArMap+arParser = do+  magic <- P.string $ pack armag+  entries <- manyTill arEntryParser P.endOfInput+  return (M.fromList entries)++arFromData :: B.ByteString -> Either String ArMap+arFromData input = P.parseOnly arParser input++arFromFile :: String -> IO (Either String ArMap)+arFromFile path = do+  content <- B.readFile path+  return (arFromData content)
+ System/Apotiki/Config.hs view
@@ -0,0 +1,21 @@+module System.Apotiki.Config where++data ApotikiConfig = ApotikiConfig {+  keyPath :: String,+  architectures :: [String],+  component :: String,+  release :: String,+  label   :: String,+  origin  :: String,+  repoDir :: String+  } deriving (Show, Read)++configKeyPath ApotikiConfig { keyPath = x } = x+configArchs ApotikiConfig { architectures = x } = x+configComponent ApotikiConfig { component = x } = x+configRelease ApotikiConfig { release = x } = x+configPoolDir ApotikiConfig { repoDir = x } = x ++ "/pool"+configDistDir ApotikiConfig { repoDir = x } = x ++ "/dists"+configRepoDir ApotikiConfig { repoDir = x } = x+configOrigin ApotikiConfig { origin = x } = x+configLabel ApotikiConfig { label = x } = x
+ System/Apotiki/Debian/Control.hs view
@@ -0,0 +1,42 @@+module System.Apotiki.Debian.Control (DebInfo, ctlFromData) where+import System.Apotiki.Utils+import Data.Attoparsec.Combinator (manyTill, many1)+import Data.ByteString.Char8 (pack, unpack)+import Data.List (intersperse)+import qualified Data.Map as M+import qualified Data.ByteString as B+import qualified Data.Attoparsec.ByteString as P++type DebInfo = M.Map String String++ctlValParser :: P.Parser String+ctlValParser = do+  P.string $ pack " "+  val <- unpack `fmap` P.takeWhile (P.notInClass "\n")+  P.string $ pack "\n"+  return val++ctlFlatDescParser = (concat . intersperse "\n ") `fmap` many1 ctlValParser++ctlEntryParser :: P.Parser (String, String)+ctlEntryParser = do+  k <- unpack `fmap` P.takeWhile (P.notInClass ":")+  P.string $ pack ":"+  v <- if (k == "Description") then ctlFlatDescParser else ctlValParser+  return (k, strip v)++ctlParser :: P.Parser DebInfo+ctlParser = M.fromList `fmap` many1 ctlEntryParser++ctlFromData :: B.ByteString -> Either String DebInfo+ctlFromData input = P.parseOnly ctlParser input++ctlFromFile :: String -> IO (Either String DebInfo)+ctlFromFile path = do+  content <- B.readFile path+  return (ctlFromData content)++test :: IO ()+test = do+  info <- ctlFromFile "/tmp/control"+  putStrLn $ show $ info
+ System/Apotiki/Debian/Package.hs view
@@ -0,0 +1,39 @@+module System.Apotiki.Debian.Package where+import System.Apotiki.Ar+import System.Apotiki.Tar+import System.Apotiki.FileInfo+import System.Apotiki.Utils+import System.Apotiki.Config+import System.Apotiki.Debian.Control+import Data.List+import System.Directory+import Data.ByteString.Char8 (pack, unpack)+import qualified Data.ByteString as B+import qualified Data.Map as M++writeToPool :: String -> (DebInfo, B.ByteString) -> IO ()+writeToPool repodir (info, payload) = do+  let path = info M.! "Filename"+  let dir_path = reverse $ snd $ break (== '/') $ reverse path+  putStrLn $ "found filename: " ++ path+  createDirectoryIfMissing True (repodir ++ "/" ++ dir_path)+  B.writeFile (repodir ++ "/" ++ path) payload+  B.writeFile (repodir ++ "/" ++ dir_path ++ "control") $ pack (show info)++toDebInfo :: String -> DebInfo+toDebInfo input = output where Right output = ctlFromData $ pack $ input++debInfo :: ApotikiConfig -> B.ByteString -> DebInfo+debInfo config payload =+  M.union (fileinfo payload) (M.insert "Filename" path debinfo) where+    Right archive = (arFromData payload)+    ArEntry {entryData = entry} = archive M.! "control.tar.gz"+    debinfo = toDebInfo $ getStrictControl entry+    arch = case M.lookup "Architecture" debinfo of+      Nothing -> "NOARCH"+      Just x -> x+    pkg = case M.lookup "Package" debinfo of+      Nothing -> "NOPKG"+      Just x -> x+    pooldir = configPoolDir config+    path = "pool/" ++ arch ++ "/" ++ pkg ++ "/" ++ pkg ++ ".deb"
+ System/Apotiki/Debian/Release.hs view
@@ -0,0 +1,201 @@+module System.Apotiki.Debian.Release where+import System.Apotiki.Debian.Package+import System.Apotiki.Debian.Control+import System.Apotiki.FileInfo+import System.Apotiki.Config+import System.Apotiki.Signature+import System.Directory+import System.IO+import Data.List+import Data.Function+import Data.ByteString.Char8 (pack,unpack)+import Data.Aeson+import qualified System.IO.Strict as SIO+import qualified Codec.Compression.GZip as Z+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as BL+import qualified Data.Map as M++type ArchRelease = M.Map String DebInfo+type Release = M.Map String ArchRelease++flatinfo fileinfo = [fileinfo M.! "Size", fileinfo M.! "MD5sum",+                     fileinfo M.! "SHA1", fileinfo M.! "SHA256"]++unrollEntry (k,v) = k ++ ": " ++ v+unrollMap input = concat $ intersperse "\n" $ map unrollEntry $ M.assocs input++unroll :: [DebInfo] -> String+unroll input = (concat $ intersperse "\n\n" $ map unrollMap input) ++ "\n"++pkgControl pooldir arch pkg = do+  let path = pooldir ++ "/" ++ arch ++ "/" ++ pkg ++ "/control"+  fd <- openFile path ReadMode+  control_data <- SIO.hGetContents fd+  let output = (read control_data :: DebInfo)+  hClose fd+  return output++archRelease pooldir arch = do+  let path = pooldir ++ "/" ++ arch+  entries <- getDirectoryContents path+  let pkgs = filter ((/= '.') . head) entries+  controls <- mapM (pkgControl pooldir arch) pkgs+  return (M.fromList $ zip pkgs controls)++loadRelease :: String -> IO (Release)+loadRelease pooldir = do+  entries <- getDirectoryContents pooldir+  let archs  = filter ((/= '.') . head) entries+  arch_releases <- mapM (archRelease pooldir) archs+  return (M.fromList $ zip archs arch_releases)++releaseJSON pooldir = do+  release <- loadRelease pooldir+  let encoded = release+  return encoded++same_arch x y = (fst x) == (fst y)++releaseByArch archs debinfo =+  if arch == "all" then+    zip archs (repeat debinfo)+  else+    [(arch, debinfo)]+  where arch = debinfo M.! "Architecture"++releaseDescr (_,deb) = (package, deb) where+  package = deb M.! "Package"++releaseMap debs = (fst $ head $ debs,+                   M.fromList $ map releaseDescr debs)++releaseFrom archs debs = M.fromList release_map where+  all_debs = concatMap (releaseByArch archs) debs+  sorted = sortBy (compare `on` fst) all_debs+  by_arch = groupBy same_arch sorted+  release_map = map releaseMap by_arch++mergeRelease new old arch =+  M.union new_arch old_arch where+    new_arch = case (M.lookup arch new) of+      Nothing -> M.fromList []+      Just x -> x+    old_arch = case (M.lookup arch old) of+      Nothing -> M.fromList []+      Just x -> x++updateRelease archs old new = M.fromList (zip archs updated) where+  updated = map (mergeRelease new old) archs++getPkg :: ApotikiConfig -> (String, ArchRelease) -> (String, B.ByteString)+getPkg config (arch, release) = (relpath, pack str_data) where+  distdir = (configDistDir config)+  component = (configComponent config)+  relname = (configRelease config)+  origin = (configOrigin config)+  label = (configLabel config)+  path = concat $ intersperse "/"+         [distdir, relname, component,+          ("binary-" ++ arch), "Packages"]+  relpath = concat $ intersperse "/"+            [component, ("binary-" ++ arch), "Packages"]+  str_data = unroll $ map snd $ M.assocs release++writePackages :: ApotikiConfig -> (String, B.ByteString) -> IO (String, [String])+writePackages config (relpath, payload) = do+  let path = concat $ intersperse "/" [(configDistDir config),+                                       (configRelease config),+                                       relpath]+  B.writeFile path payload+  return (relpath, flatinfo $ fileinfo payload)++writeGzPackages :: ApotikiConfig -> (String, B.ByteString) -> IO (String, [String])+writeGzPackages config (relpath, payload) = do+  let path = concat $ intersperse "/" [(configDistDir config),+                                       (configRelease config),+                                       relpath ++ ".gz"]+  let gzpayload = B.concat $ BL.toChunks $ Z.compress $ BL.fromChunks [payload]+  B.writeFile path gzpayload+  return (relpath ++ ".gz", flatinfo $ fileinfo gzpayload)++writeArchRelease :: ApotikiConfig -> (String, ArchRelease) -> IO (String, [String])+writeArchRelease config (arch,release) = do+  let distdir = (configDistDir config)+  let component = (configComponent config)+  let relname = (configRelease config)+  let origin = (configOrigin config)+  let label = (configLabel config)+  let path = concat $ intersperse "/"+             [distdir, relname, component,+              ("binary-" ++ arch), "Release"]+  let relpath = concat $ intersperse "/"+                [component, ("binary-" ++ arch), "Release"]++  let payload = pack $ unroll [M.fromList [("Archive", relname),+                                           ("Component", component),+                                           ("Origin", origin),+                                           ("Label", label),+                                           ("Architecture", arch)]]+  B.writeFile path payload+  return (relpath, flatinfo $ fileinfo payload)++md5info (path, [size, sum, _, _]) = " " ++ sum ++ " " ++ size ++ " " ++ path+sha1info (path, [size, _, sum, _]) = " " ++ sum ++ " " ++ size ++ " " ++ path+sha256info (path, [size, _, _, sum]) = " " ++ sum ++ " " ++ size ++ " " ++ path++writeGlobalRelease :: ApotikiConfig -> [(String, [String])] -> IO ()+writeGlobalRelease config info = do+  let archs = concat $ intersperse " " (configArchs config)+  let origin = configOrigin config+  let label = configLabel config+  let release = configRelease config+  let component = configComponent config+  let md5s = concat $ intersperse "\n" $ map md5info info+  let sha1s = concat $ intersperse "\n" $ map sha1info info+  let sha256s = concat $ intersperse "\n" $ map sha256info info+  let sums = concat $ intersperse "\n" $ ["MD5Sum:",+                                          md5s,+                                          "SHA1Sum:",+                                          sha1s,+                                          "SHA256Sum:",+                                          sha256s]+  let summary = concat $ intersperse "\n" ["Origin: " ++ origin,+                                           "Label: " ++ label,+                                           "Suite: " ++ release,+                                           "Codename: " ++ release,+                                           "Components: " ++ component,+                                           "Architectures: " ++ archs]+  let payload = pack $ summary ++ "\n" ++ sums ++ "\n"+  let path = concat $ intersperse "/" [(configDistDir config),+                                       (configRelease config),+                                       "Release"]++  (keys, (time, rng)) <- get_key (configKeyPath config)+  let pgp = sign_msg keys time rng payload+  B.writeFile (path ++ ".gpg") pgp+  B.writeFile path payload+++releaseMkDir distdir release component arch =+  createDirectoryIfMissing True $ concat $ intersperse "/" [distdir,+                                                            release,+                                                            component,+                                                            "binary-" ++ arch]+releaseMkDirs ApotikiConfig {repoDir = repodir,+                             architectures = archs,+                             release = release,+                             component = component} = do+  mapM_ (releaseMkDir (repodir ++ "/dists") release component) archs++writeRelease :: ApotikiConfig -> Release -> IO ()+writeRelease config release = do+  releaseMkDirs config+  let pkgs = map (getPkg config) (M.assocs release)+  release_files <- mapM (writeArchRelease config) (M.assocs release)+  putStrLn $ "wrote release files: " ++ (show $ length release_files)+  pkg_files <- mapM (writePackages config) pkgs+  putStrLn $ "wrote package files: " ++ (show $ length pkg_files)+  pkg_gz_files <- mapM (writeGzPackages config) pkgs+  putStrLn $ "wrote package compressed files: " ++ (show $ length pkg_gz_files)+  writeGlobalRelease config $ concat [release_files, pkg_files, pkg_gz_files]
+ System/Apotiki/FileInfo.hs view
@@ -0,0 +1,16 @@+module System.Apotiki.FileInfo (sha256sum, sha1sum, md5sum, fileinfo) where+import qualified Data.ByteString as B+import qualified Crypto.Hash.SHA256 as SHA256+import qualified Crypto.Hash.SHA1 as SHA1+import qualified Crypto.Hash.MD5 as MD5+import qualified Text.Printf as P+import qualified Data.Map as M++gensum input hashfn = B.unpack (hashfn input) >>= P.printf "%02x"+sha256sum input = gensum input SHA256.hash+sha1sum input = gensum input SHA1.hash+md5sum input = gensum input MD5.hash+fileinfo input = M.fromList [("Size", show $ B.length input),+                             ("MD5sum", md5sum input),+                             ("SHA1", sha1sum input),+                             ("SHA256", sha256sum input)]
+ System/Apotiki/Signature.hs view
@@ -0,0 +1,29 @@+module System.Apotiki.Signature (sign_msg, get_key) where+import System.Time (getClockTime, ClockTime(..))+import Crypto.Random+import Data.OpenPGP+import qualified Data.Binary as Binary+import qualified Data.OpenPGP.CryptoAPI as PGP+import qualified Data.ByteString.Lazy as B+import qualified Data.ByteString as BS+import Codec.Encryption.OpenPGP.ASCIIArmor as Armor+import Codec.Encryption.OpenPGP.ASCIIArmor.Types as Armor++get_key keypath = do+  keys <- Binary.decodeFile keypath+  TOD tod _ <- getClockTime+  rng <- newGenIO :: IO SystemRandom+  let time = (fromIntegral tod :: Integer)+  return (keys, (time, rng))++sign_msg :: (CryptoRandomGen g) => Message -> Integer -> g -> BS.ByteString -> BS.ByteString+sign_msg  keys time rng payload =+  Armor.encode [armor] where+    wtime = (fromIntegral time :: Binary.Word32)+    lazy_payload = B.fromChunks [payload]+    pgp_payload = LiteralDataPacket 'b' "" wtime lazy_payload+    input = (DataSignature pgp_payload [])+    (DataSignature _ [sig], _) = PGP.sign keys input SHA256 [] time rng+    options = [("Version", "OpenPrivacy 0.99"), ("Hash", "SHA256")]+    encoded_sig = Binary.encode sig+    armor = Armor.Armor Armor.ArmorSignature options encoded_sig
+ System/Apotiki/Tar.hs view
@@ -0,0 +1,31 @@+module System.Apotiki.Tar (getControl, getStrictControl) where+import Data.List+import qualified Data.Map as M+import qualified Codec.Archive.Tar as Tar+import qualified Data.ByteString as BS+import qualified Data.ByteString.Lazy as B+import qualified Data.ByteString.Lazy.Char8 as BC+import qualified Codec.Compression.GZip as Z++tarEntryList :: Tar.Entries Tar.FormatError -> [Tar.Entry] -> [Tar.Entry]+tarEntryList entries outlist =+  case entries of+    Tar.Next entry (more) -> (tarEntryList more (entry:outlist))+    Tar.Done -> outlist+    Tar.Fail e -> error (show e)++tarEntryPayload :: Tar.EntryContent -> String+tarEntryPayload (Tar.NormalFile payload size) = BC.unpack payload++getStrictControl :: BS.ByteString -> String+getStrictControl content =+  getControl $ B.fromChunks [content]++getControl :: B.ByteString -> String+getControl content =+  tarEntryPayload $ Tar.entryContent entry+    where unzipped = Z.decompress content+          entries = tarEntryList (Tar.read unzipped) []+          entry = case find ((== "./control") . Tar.entryPath) entries of+            Just entry -> entry+            Nothing -> error (show $ map Tar.entryPath entries)
+ System/Apotiki/Utils.hs view
@@ -0,0 +1,19 @@+module System.Apotiki.Utils (strip) where+import Data.List++-- inspired by MissingH++wschars :: String+wschars = " \t\r\n"++is_white :: Char -> Bool+is_white c = isInfixOf [c] wschars++lstrip :: String -> String+lstrip s = dropWhile is_white s++rstrip :: String -> String+rstrip = reverse . lstrip . reverse++strip :: String -> String+strip = lstrip . rstrip
+ apotiki.cabal view
@@ -0,0 +1,72 @@+-- Initial apotiki.cabal generated by cabal init.  For further +-- documentation, see http://haskell.org/cabal/users-guide/++name:                apotiki+version:             0.5.1+synopsis:            a faster debian repository+description:+    apotiki generates debian repositories fast. its goal is+    to be a great companion to fpm and jenkins.+    .+    apotiki operates with the following features and constraints:+    .+    - Supports a single debian release+    .+    - Supports a single debian component+    .+    - Supports an arbitrary number of architectures which need to be preprovisionned+    .+    - Requires a valid PGP private key for signing++-- description:         +homepage:            https://github.com/pyr/apotiki+license:             MIT+license-file:        LICENSE+author:              Pierre-Yves Ritschard+maintainer:          pyr@spootnik.org+-- copyright:           +category:            System+build-type:          Simple+-- extra-source-files:  +cabal-version:       >=1.10+extra-source-files:  README.md+                     System/Apotiki/Debian/*.hs+                     System/Apotiki/*.hs+                     static/*.js static/*.html++library+  exposed-modules:     System.Apotiki.Ar+                       System.Apotiki.Debian.Package+                       System.Apotiki.Debian.Release+  build-depends:       base >=4.6 && <4.7, bytestring >= 0.10.0.2,+                       containers >= 0.5.0.0, cryptohash >= 0.9.1,+                       zlib >= 0.5.4.1, tar >= 0.4.0.1, aeson >= 0.6.2.1,+                       attoparsec >= 0.10.4.0, directory >= 1.2.0.1,+                       openpgp-asciiarmor >= 0.1, openpgp-crypto-api >= 0.6.3,+                       binary >= 0.7.1.0, openpgp >= 0.6.1, strict >= 0.3.2,+                       crypto-api == 0.12.2.2, old-time >= 1.1.0.1,+                       scotty >= 0.6.2, wai-extra >= 2.0.3.1, text >= 1.0.0.1,+                       http-types >= 0.8.3, transformers >= 0.3.0.0,+                       transformers >= 0.3.0.0, wai-middleware-static >= 0.4.0.2+  default-language:    Haskell2010+  -- other-modules:       +  -- other-extensions:    ++executable apotiki+  main-is: apotiki.hs+  ghc-options: -threaded+  -- other-modules:       +  -- other-extensions:    +  build-depends:       base >=4.6 && <4.7, bytestring >= 0.10.0.2,+                       containers >= 0.5.0.0, cryptohash >= 0.9.1,+                       zlib >= 0.5.4.1, tar >= 0.4.0.1, aeson >= 0.6.2.1,+                       attoparsec >= 0.10.4.0, directory >= 1.2.0.1,+                       openpgp-asciiarmor >= 0.1, openpgp-crypto-api >= 0.6.3,+                       binary >= 0.7.1.0, openpgp >= 0.6.1, strict >= 0.3.2,+                       crypto-api == 0.12.2.2, old-time >= 1.1.0.1,+                       scotty >= 0.6.2, wai-extra >= 2.0.3.1, text >= 1.0.0.1,+                       http-types >= 0.8.3, transformers >= 0.3.0.0,+                       transformers >= 0.3.0.0, wai-middleware-static >= 0.4.0.2+                       +  -- hs-source-dirs:      +  default-language:    Haskell2010
+ apotiki.hs view
@@ -0,0 +1,88 @@+{-# LANGUAGE OverloadedStrings #-}++module Main where+import System.Apotiki.Debian.Package+import System.Apotiki.Debian.Release+import System.Apotiki.Config+import Data.Map (keys)+import System.Environment+import System.Directory+import Control.Exception++import Network.HTTP.Types.Status+import Web.Scotty+import Data.Text (pack)+import Data.Text.Lazy (unpack)+import Data.ByteString.Lazy (toChunks)+import Network.Wai.Middleware.RequestLogger+import Network.Wai.Parse++import Data.Aeson (object, (.=))+import Control.Monad (guard)+import System.IO.Error (isDoesNotExistError)+import Control.Monad.IO.Class (liftIO)+import qualified Data.ByteString as B++import Network.Wai.Middleware.Static++setupRepo config = do+  createDirectoryIfMissing True (configDistDir config)+  createDirectoryIfMissing True (configPoolDir config)++main :: IO ()+main = do+  -- first fetch our config+  result <-  tryJust (guard . isDoesNotExistError) $ getEnv "APOTIKI_CONFIG"+  let confpath = case result of+        Left e -> "/etc/apotiki.conf"+        Right val -> val+  confdata <- readFile confpath+  let config = read confdata :: ApotikiConfig+  args <- getArgs+  runCommand config args++runCommand config [] = runCommand config ["help"]++runCommand config ("help":debfiles) = do+  putStrLn "usage: apotiki {web, insert} [packages]"++runCommand config ("web":_) = do+  setupRepo config+  scotty 8000 $ do+    middleware $ staticPolicy (noDots >-> addBase "static")+    get "/" $ do+      redirect "/index.html"+    get "/repo" $ do+      repo <- liftIO (releaseJSON $ configPoolDir config)+      json repo+    post "/repo" $ do+      indata <- files+      let debfiles = [B.concat $ toChunks $ fileContent fi | (_,fi) <- indata]+      liftIO $ insertPackages config debfiles+      redirect "/index.html"++runCommand config ("insert":filenames) = do+  setupRepo config+  debfiles <- mapM B.readFile filenames+  insertPackages config debfiles++insertPackages config debfiles = do+  -- now load our view of the world+  old_release <- loadRelease $ configPoolDir config+  putStrLn $ "got previous release: "  ++ (show $ length $ keys old_release)++  let debinfo = map (debInfo config) debfiles+  let archs = configArchs config+  let pending_release = releaseFrom archs debinfo++  putStrLn $ "got pending release: "  ++ (show $ length $ keys pending_release)++  -- merge old and new release+  let release = updateRelease archs old_release pending_release++  writeRelease config release++  -- write package to their destination+  mapM_ (writeToPool $ configRepoDir config) $ zip debinfo debfiles++  putStrLn "done updating repository"
+ static/apotiki.js view
@@ -0,0 +1,31 @@+var app = angular.module('repo', ['ngRoute']);++app.controller('Apotiki', function($scope, $routeParams, $location, $http) {++    $scope.repolist = [];+    $scope.repo = {};++    $scope.refresh = function () {+        $http.get('/repo').success(function (data) {+            $scope.repo = data;+            for (var k in data) {+                for (var subk in data[k]) {+                    $scope.repolist.push(data[k][subk]);+                }+            }+        });+    };++    if ($routeParams.name) { $scope.name = $routeParams.name; }+    if ($routeParams.arch) { $scope.arch = $routeParams.arch; }++    $scope.refresh();+});++app.config(function($routeProvider) {+    $routeProvider+        .when('/repo', {templateUrl: 'listing.html', controller: 'Apotiki'})+        .when('/repo/:arch/:name', {templateUrl: 'details.html', controller: 'Apotiki'})+        .when('/post', {templateUrl: 'post.html'})+        .otherwise({redirectTo: '/repo'});+});
+ static/details.html view
@@ -0,0 +1,60 @@+<div class="panel panel-default">+  +  <div class="panel-heading">+    Package: {{repo[arch][name]['Package']}}+  </div>+  +  <div class="panel-body">+    <div class="row">+      <div class="col-md-2"><strong>Package</strong></div>+      <div class="col-md-10">{{repo[arch][name]['Package']}}</div>+    </div>+    <div class="row">+      <div class="col-md-2"><strong>Version</strong></div>+      <div class="col-md-10">{{repo[arch][name]['Version']}}</div>+    </div>+    <div class="row">+      <div class="col-md-2"><strong>Architecture</strong></div>+      <div class="col-md-10">{{repo[arch][name]['Architecture']}}</div>+    </div>+    <div class="row">+      <div class="col-md-2"><strong>Maintainer</strong></div>+      <div class="col-md-10">{{repo[arch][name]['Maintainer']}}</div>+    </div>+    <div class="row">+      <div class="col-md-2"><strong>License</strong></div>+      <div class="col-md-10">{{repo[arch][name]['License']}}</div>+    </div>+    <div class="row">+      <div class="col-md-2"><strong>Installed Size</strong></div>+      <div class="col-md-10">{{repo[arch][name]['Installed-Size']}}</div>+    </div>+    <div class="row">+      <div class="col-md-2"><strong>Priority</strong></div>+      <div class="col-md-10">{{repo[arch][name]['Priority']}}</div>+    </div>+    <div class="row">+      <div class="col-md-2"><strong>Depends</strong></div>+      <div class="col-md-10">{{repo[arch][name]['Depends']}}</div>+    </div>+    <div class="row">+      <div class="col-md-2"><strong>Homepage</strong></div>+      <div class="col-md-10"><a href="{{repo[arch][name]['Homepage']}}">{{repo[arch][name]['Homepage']}}</a></div>+    </div>+    <div class="row">+      <div class="col-md-2"><strong>Vendor</strong></div>+      <div class="col-md-10">{{repo[arch][name]['Vendor']}}</div>+    </div>+    <div class="row">+      <div class="col-md-2"><strong>Section</strong></div>+      <div class="col-md-10">{{repo[arch][name]['Section']}}</div>+    </div>+    <div class="row">+      <div class="col-md-2"><strong>Description</strong></div>+      <div class="col-md-10">+        <pre ng-bind="repo[arch][name]['Description']"></pre>+      </div>+    </div>+  </div>+  +</div>
+ static/index.html view
@@ -0,0 +1,38 @@+<!doctype html>+<html lang="en">++  <head>+    <title>apotiki debian repository</title>+    <meta name="viewport" content="width=device-width, initial-scale=1.0">+    <link href="//netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">+    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.8/angular.min.js"></script>+    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.8/angular-route.js"></script>+    <script src="/apotiki.js"></script>+  </head>++  <body ng-app="repo">+    <div class="navbar navbar-default" role="navigation">+      <div class="container">+        <div class="navbar-header">+          <a class="navbar-brand" href="#">apotiki</a>+        </div>+        <div class="navbar-collapse collapse">+          <ul class="nav navbar-nav">+            <li><a href="#list">Listing</a></li>+            <li><a href="/post.html">Submit</a></li>+          </ul>+        </div>+      </div>+    </div>+      +    <div class="container">+      <div class="row">+        <div class="col-lg-8 col-lg-offset-2">+          <ng-view></ng-view>+        </div>+      </div>+    </div>+    +  </body>+  +</html>
+ static/listing.html view
@@ -0,0 +1,32 @@+<div class="panel panel-default">+  +  <div class="panel-heading">+    <div class="row">+      <div class="col-sm-2">+        <h4>Package listing</h4>+      </div>+      <div class="col-sm-4 col-sm-offset-6">+        <form class="form-inline">+          <input type="search" ng-model="searchterm" class="form-control">+        </form>+      </div>+    </div>+  </div>+  +  <div class="panel-body">+    <table class="table table-striped">+      <thead>+        <th>Package</th>+        <th>Version</th>+        <th>Architecture</th>+      </thead>+      <tbody>+        <tr ng-repeat="pkg in repolist | filter:searchterm">+          <td><a href="#repo/{{pkg['Architecture']}}/{{pkg['Package']}}">{{pkg['Package']}}</a></td>+          <td>{{pkg['Version']}}</td>+          <td>{{pkg['Architecture']}}</td>+        </tr>+      </tbody>+    </table>+  </div>+</div>
+ static/post.html view
@@ -0,0 +1,58 @@+<!doctype html>+<html lang="en">++  <head>+    <title>apotiki debian repository</title>+    <meta name="viewport" content="width=device-width, initial-scale=1.0">+    <link href="//netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">+  </head>++  <body ng-app="repo">+    <div class="navbar navbar-default" role="navigation">+      <div class="container">+        <div class="navbar-header">+          <a class="navbar-brand" href="#">apotiki</a>+        </div>+        <div class="navbar-collapse collapse">+          <ul class="nav navbar-nav">+            <li><a href="/index.html#list">Listing</a></li>+            <li><a href="/post.html">Submit</a></li>+          </ul>+        </div>+      </div>+    </div>+      +    <div class="container">+      <div class="row">+        <div class="col-lg-8 col-lg-offset-2">++<div class="panel panel-default">+  +  <div class="panel-heading">+    <h4>Package</h4>+  </div>+  +  <div class="panel-body">+    <form method="POST" action="/repo" enctype="multipart/form-data" role="form">+      <div class="form-group">+        <label>Choose package</label>+        <input type="file" name="file" id="file">+        <p class="help-block">+          Package file will overwrite any previous versions.+        </p>+      </div>+      <button type="submit" class="btn btn-default">Submit</button>+    </form>+  </div>+</div>+++        </div>+      </div>+    </div>+    +  </body>+  +</html>++