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 +13/−0
- README.md +102/−0
- Setup.hs +2/−0
- System/Apotiki/Ar.hs +49/−0
- System/Apotiki/Config.hs +21/−0
- System/Apotiki/Debian/Control.hs +42/−0
- System/Apotiki/Debian/Package.hs +39/−0
- System/Apotiki/Debian/Release.hs +201/−0
- System/Apotiki/FileInfo.hs +16/−0
- System/Apotiki/Signature.hs +29/−0
- System/Apotiki/Tar.hs +31/−0
- System/Apotiki/Utils.hs +19/−0
- apotiki.cabal +72/−0
- apotiki.hs +88/−0
- static/apotiki.js +31/−0
- static/details.html +60/−0
- static/index.html +38/−0
- static/listing.html +32/−0
- static/post.html +58/−0
+ 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+===================================++++([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>++