Feature/zip upload (#124)

* zip eos image on upload

* fix warning

* revert content length as its already set by module

* move stream zipping to cli upload

* refactor upload with simple gzip approach - progress bar broken

* increase timeout

* fix request body and unzip once uploaded

* unzipped in wrong fn; cleanup

* add progress bar for hashing and zipping, cleanup files

* add messaging to progress bars
This commit is contained in:
Lucy C
2022-09-19 15:18:14 -06:00
committed by GitHub
parent 3aef9dbf09
commit 648d69c14a
5 changed files with 68 additions and 20 deletions

1
.gitignore vendored
View File

@@ -38,3 +38,4 @@ start9-registry.aux
start9-registry.ps start9-registry.ps
shell.nix shell.nix
testdata/ testdata/
lbuild.sh

View File

@@ -378,7 +378,7 @@ makeAuthWare _ app req res = next
-- | Warp settings for the given foundation value. -- | Warp settings for the given foundation value.
warpSettings :: AppPort -> RegistryCtx -> Settings warpSettings :: AppPort -> RegistryCtx -> Settings
warpSettings port foundation = warpSettings port foundation =
setTimeout 60 $ setTimeout 90 $
setPort (fromIntegral port) $ setPort (fromIntegral port) $
setHost (appHost $ appSettings foundation) $ setHost (appHost $ appSettings foundation) $
setOnException setOnException

View File

@@ -13,8 +13,15 @@ module Cli.Cli (
) where ) where
import Conduit ( import Conduit (
ConduitT,
MonadIO,
awaitForever,
foldC, foldC,
runConduit, runConduit,
runConduitRes,
sinkFileCautious,
sourceFile,
yield,
(.|), (.|),
) )
import Control.Monad.Logger ( import Control.Monad.Logger (
@@ -29,6 +36,7 @@ import Crypto.Hash (
SHA256 (SHA256), SHA256 (SHA256),
hashWith, hashWith,
) )
import Crypto.Hash.Conduit (hashFile, sinkHash)
import Data.Aeson ( import Data.Aeson (
ToJSON, ToJSON,
eitherDecodeStrict, eitherDecodeStrict,
@@ -40,6 +48,7 @@ import Data.ByteArray.Encoding (
import Data.ByteString.Char8 qualified as B8 import Data.ByteString.Char8 qualified as B8
import Data.ByteString.Lazy qualified as LB import Data.ByteString.Lazy qualified as LB
import Data.Conduit.Process (readProcess) import Data.Conduit.Process (readProcess)
import Data.Conduit.Zlib (gzip)
import Data.Default import Data.Default
import Data.Functor.Contravariant (contramap) import Data.Functor.Contravariant (contramap)
import Data.HashMap.Internal.Strict ( import Data.HashMap.Internal.Strict (
@@ -70,9 +79,7 @@ import Handler.Admin (
PackageList (..), PackageList (..),
) )
import Lib.External.AppMgr (sourceManifest) import Lib.External.AppMgr (sourceManifest)
import Lib.Types.Core ( import Lib.Types.Core (PkgId (..))
PkgId (..),
)
import Lib.Types.Emver (Version (..)) import Lib.Types.Emver (Version (..))
import Lib.Types.Manifest (PackageManifest (..)) import Lib.Types.Manifest (PackageManifest (..))
import Network.HTTP.Client.Conduit ( import Network.HTTP.Client.Conduit (
@@ -82,6 +89,7 @@ import Network.HTTP.Client.Conduit (
observedStreamFile, observedStreamFile,
) )
import Network.HTTP.Client.TLS (newTlsManager) import Network.HTTP.Client.TLS (newTlsManager)
import Network.HTTP.Conduit (responseTimeoutMicro)
import Network.HTTP.Simple ( import Network.HTTP.Simple (
getResponseBody, getResponseBody,
getResponseStatus, getResponseStatus,
@@ -90,7 +98,9 @@ import Network.HTTP.Simple (
parseRequest, parseRequest,
setRequestBody, setRequestBody,
setRequestBodyJSON, setRequestBodyJSON,
setRequestHeaders, setRequestQueryString setRequestHeaders,
setRequestQueryString,
setRequestResponseTimeout,
) )
import Network.HTTP.Types (status200) import Network.HTTP.Types (status200)
import Network.URI ( import Network.URI (
@@ -153,6 +163,7 @@ import Startlude (
ReaderT (runReaderT), ReaderT (runReaderT),
Semigroup ((<>)), Semigroup ((<>)),
Show, Show,
SomeException,
String, String,
appendFile, appendFile,
const, const,
@@ -167,10 +178,12 @@ import Startlude (
fromMaybe, fromMaybe,
fst, fst,
headMay, headMay,
liftIO,
not, not,
panic, panic,
show, show,
snd, snd,
throwIO,
unlessM, unlessM,
void, void,
when, when,
@@ -189,6 +202,7 @@ import System.Directory (
getFileSize, getFileSize,
getHomeDirectory, getHomeDirectory,
listDirectory, listDirectory,
removeFile,
) )
import System.FilePath ( import System.FilePath (
takeDirectory, takeDirectory,
@@ -197,15 +211,19 @@ import System.FilePath (
) )
import System.ProgressBar ( import System.ProgressBar (
Progress (..), Progress (..),
ProgressBar,
Style (stylePrefix),
defStyle, defStyle,
incProgress,
msg,
newProgressBar, newProgressBar,
updateProgress, updateProgress,
) )
import UnliftIO.Exception (handle)
import Yesod ( import Yesod (
logError, logError,
logWarn, logWarn,
) )
import Crypto.Hash.Conduit (hashFile)
data Upload = Upload data Upload = Upload
@@ -215,6 +233,7 @@ data Upload = Upload
} }
deriving (Show) deriving (Show)
data EosUpload = EosUpload data EosUpload = EosUpload
{ eosRepoName :: !String { eosRepoName :: !String
, eosPath :: !FilePath , eosPath :: !FilePath
@@ -429,6 +448,7 @@ parsePkgCat = subparser $ command "categorize" (info cat $ progDesc "Add or remo
<*> strArgument (metavar "PACKAGE_ID") <*> strArgument (metavar "PACKAGE_ID")
<*> strArgument (metavar "CATEGORY") <*> strArgument (metavar "CATEGORY")
parseEosPublish :: Parser EosUpload parseEosPublish :: Parser EosUpload
parseEosPublish = parseEosPublish =
subparser $ subparser $
@@ -543,6 +563,7 @@ upload (Upload name mpkg shouldIndex) = do
noBody <- noBody <-
parseRequest ("POST " <> show publishCfgRepoLocation <> "/admin/v0/upload") parseRequest ("POST " <> show publishCfgRepoLocation <> "/admin/v0/upload")
<&> setRequestHeaders [("accept", "text/plain")] <&> setRequestHeaders [("accept", "text/plain")]
<&> setRequestResponseTimeout (responseTimeoutMicro (90_000_000)) -- 90 seconds
<&> applyBasicAuth (B8.pack publishCfgRepoUser) (B8.pack publishCfgRepoPass) <&> applyBasicAuth (B8.pack publishCfgRepoUser) (B8.pack publishCfgRepoPass)
size <- getFileSize pkg size <- getFileSize pkg
bar <- newProgressBar defStyle 30 (Progress 0 (fromIntegral size) ()) bar <- newProgressBar defStyle 30 (Progress 0 (fromIntegral size) ())
@@ -572,21 +593,38 @@ upload (Upload name mpkg shouldIndex) = do
sfs2prog :: StreamFileStatus -> Progress () sfs2prog :: StreamFileStatus -> Progress ()
sfs2prog StreamFileStatus{..} = Progress (fromIntegral readSoFar) (fromIntegral fileSize) () sfs2prog StreamFileStatus{..} = Progress (fromIntegral readSoFar) (fromIntegral fileSize) ()
eosUpload :: EosUpload -> IO () eosUpload :: EosUpload -> IO ()
eosUpload (EosUpload name img version) = do eosUpload (EosUpload name img version) = handle @_ @SomeException cleanup $ do
PublishCfgRepo{..} <- findNameInCfg name PublishCfgRepo{..} <- findNameInCfg name
noBody <- noBody <-
parseRequest ("POST " <> show publishCfgRepoLocation <> "/admin/v0/eos-upload") parseRequest ("POST " <> show publishCfgRepoLocation <> "/admin/v0/eos-upload")
<&> setRequestHeaders [("accept", "text/plain")] <&> setRequestHeaders [("accept", "text/plain")]
<&> setRequestResponseTimeout (responseTimeoutMicro (90_000_000)) -- 90 seconds
<&> setRequestHeaders [("Content-Encoding", "gzip")]
<&> applyBasicAuth (B8.pack publishCfgRepoUser) (B8.pack publishCfgRepoPass) <&> applyBasicAuth (B8.pack publishCfgRepoUser) (B8.pack publishCfgRepoPass)
size <- getFileSize img size <- getFileSize img
hashBar <- newProgressBar defStyle{stylePrefix = msg "Hashing"} 30 (Progress 0 (fromIntegral size) ())
runConduitRes $ sourceFile img .| transByteCounter hashBar .| sinkHash @_ @SHA256
hash <- hashFile @_ @SHA256 img hash <- hashFile @_ @SHA256 img
bar <- newProgressBar defStyle 30 (Progress 0 (fromIntegral size) ()) let compressedFilePath = "/tmp/eos.img.gz"
body <- observedStreamFile (updateProgress bar . const . sfs2prog) img zipBar <- newProgressBar defStyle{stylePrefix = msg "Gzipping"} 30 (Progress 0 (fromIntegral size) ())
runConduitRes $
sourceFile img
.| transByteCounter zipBar
.| gzip
.| sinkFileCautious compressedFilePath
compressedSize <- getFileSize compressedFilePath
fileBar <- newProgressBar defStyle{stylePrefix = msg "Uploading"} 30 (Progress 0 (fromIntegral compressedSize) ())
body <- observedStreamFile (updateProgress fileBar . const . sfs2prog) $ compressedFilePath
let withBody = setRequestBody body noBody let withBody = setRequestBody body noBody
let withQParams = setRequestQueryString [("version", Just $ show version), ("hash", Just $ convertToBase Base16 hash)] withBody let withQParams =
setRequestQueryString
[("version", Just $ show version), ("hash", Just $ convertToBase Base16 hash)]
withBody
manager <- newTlsManager manager <- newTlsManager
res <- runReaderT (httpLbs withQParams) manager res <- runReaderT (httpLbs withQParams) manager
removeFile compressedFilePath
if getResponseStatus res == status200 if getResponseStatus res == status200
then -- no output is successful then -- no output is successful
pure () pure ()
@@ -597,6 +635,16 @@ eosUpload (EosUpload name img version) = do
where where
sfs2prog :: StreamFileStatus -> Progress () sfs2prog :: StreamFileStatus -> Progress ()
sfs2prog StreamFileStatus{..} = Progress (fromIntegral readSoFar) (fromIntegral fileSize) () sfs2prog StreamFileStatus{..} = Progress (fromIntegral readSoFar) (fromIntegral fileSize) ()
transByteCounter :: MonadIO m => ProgressBar a -> ConduitT B8.ByteString B8.ByteString m ()
transByteCounter bar = awaitForever $ \bs -> do
let len = B8.length bs
liftIO $ incProgress bar len
yield bs
cleanup e = do
$logError $ show e
removeFile "/tmp/eos.img.gz"
throwIO e
index :: String -> String -> Version -> IO () index :: String -> String -> Version -> IO ()
index name pkg v = performHttp name "POST" [i|/admin/v0/index|] (IndexPkgReq (PkgId $ toS pkg) v) index name pkg v = performHttp name "POST" [i|/admin/v0/index|] (IndexPkgReq (PkgId $ toS pkg) v)

View File

@@ -10,6 +10,7 @@ import Conduit (
sinkFile, sinkFile,
(.|), (.|),
) )
import Control.Monad.Extra
import Control.Monad.Reader.Has (ask) import Control.Monad.Reader.Has (ask)
import Control.Monad.Trans.Maybe (MaybeT (..)) import Control.Monad.Trans.Maybe (MaybeT (..))
import Data.Aeson ( import Data.Aeson (
@@ -22,6 +23,7 @@ import Data.Aeson (
(.:?), (.:?),
(.=), (.=),
) )
import Data.Conduit.Zlib (ungzip)
import Data.HashMap.Internal.Strict ( import Data.HashMap.Internal.Strict (
HashMap, HashMap,
differenceWith, differenceWith,
@@ -35,6 +37,7 @@ import Data.List (
import Data.String.Interpolate.IsString ( import Data.String.Interpolate.IsString (
i, i,
) )
import Data.Time
import Database.Persist ( import Database.Persist (
Entity (entityKey), Entity (entityKey),
PersistStoreRead (get), PersistStoreRead (get),
@@ -64,9 +67,7 @@ import Lib.PkgRepository (
getPackages, getPackages,
getVersionsFor, getVersionsFor,
) )
import Lib.Types.Core ( import Lib.Types.Core (PkgId (unPkgId))
PkgId (unPkgId),
)
import Lib.Types.Emver (Version (..)) import Lib.Types.Emver (Version (..))
import Lib.Types.Manifest (PackageManifest (..)) import Lib.Types.Manifest (PackageManifest (..))
import Model ( import Model (
@@ -93,14 +94,11 @@ import Startlude (
Eq, Eq,
Int, Int,
Maybe (..), Maybe (..),
Monad ((>>=)),
Show, Show,
SomeException (..), SomeException (..),
Text, Text,
asum, asum,
fmap,
fromMaybe, fromMaybe,
getCurrentTime,
guarded, guarded,
hush, hush,
isNothing, isNothing,
@@ -110,8 +108,6 @@ import Startlude (
show, show,
toS, toS,
traverse, traverse,
void,
when,
zip, zip,
($), ($),
(&&&), (&&&),
@@ -197,8 +193,8 @@ postEosUploadR = do
createDirectoryIfMissing True resourcesTemp createDirectoryIfMissing True resourcesTemp
withTempDirectory resourcesTemp "neweos" $ \dir -> do withTempDirectory resourcesTemp "neweos" $ \dir -> do
let path = dir </> "eos" <.> "img" let path = dir </> "eos" <.> "img"
runConduit $ rawRequestBody .| sinkFile path runConduit $ rawRequestBody .| ungzip .| sinkFile path
_ <- runDB $ upsert (EosHash version hash) [EosHashHash =. hash] void . runDB $ upsert (EosHash version hash) [EosHashHash =. hash]
let targetPath = root </> show version let targetPath = root </> show version
removePathForcibly targetPath removePathForcibly targetPath
createDirectoryIfMissing True targetPath createDirectoryIfMissing True targetPath

View File

@@ -97,6 +97,7 @@ getVersionSpecFromQuery = do
Nothing -> sendResponseStatus status400 ("Invalid App Version Specification" :: Text) Nothing -> sendResponseStatus status400 ("Invalid App Version Specification" :: Text)
Just t -> pure t Just t -> pure t
getVersionFromQuery :: MonadHandler m => m (Maybe Version) getVersionFromQuery :: MonadHandler m => m (Maybe Version)
getVersionFromQuery = do getVersionFromQuery = do
versionString <- lookupGetParam "version" versionString <- lookupGetParam "version"
@@ -106,9 +107,11 @@ getVersionFromQuery = do
Nothing -> sendResponseStatus status400 ("Invalid Version" :: Text) Nothing -> sendResponseStatus status400 ("Invalid Version" :: Text)
Just t -> pure (Just t) Just t -> pure (Just t)
getHashFromQuery :: MonadHandler m => m (Maybe Text) getHashFromQuery :: MonadHandler m => m (Maybe Text)
getHashFromQuery = lookupGetParam "hash" getHashFromQuery = lookupGetParam "hash"
versionPriorityFromQueryIsMin :: MonadHandler m => m Bool versionPriorityFromQueryIsMin :: MonadHandler m => m Bool
versionPriorityFromQueryIsMin = do versionPriorityFromQueryIsMin = do
priorityString <- lookupGetParam "version-priority" priorityString <- lookupGetParam "version-priority"