mirror of https://github.com/voidlizard/hbs2
HPoqtobDAT implement-create-bundle
This commit is contained in:
parent
5ffb44ac49
commit
ed279f1d34
|
@ -1,2 +1,3 @@
|
||||||
|
|
||||||
(fixme-set "workflow" "test" "CG2C18TK8v")
|
(fixme-set "assigned" "HPoqtobDAT" "voidlizard")
|
||||||
|
(fixme-set "workflow" "test" "HPoqtobDAT")
|
|
@ -81,6 +81,7 @@ library
|
||||||
, HBS2.Data.Types.Crypto
|
, HBS2.Data.Types.Crypto
|
||||||
, HBS2.Data.Types.Peer
|
, HBS2.Data.Types.Peer
|
||||||
, HBS2.Data.Types.Refs
|
, HBS2.Data.Types.Refs
|
||||||
|
, HBS2.Data.Bundle
|
||||||
, HBS2.Defaults
|
, HBS2.Defaults
|
||||||
, HBS2.Events
|
, HBS2.Events
|
||||||
, HBS2.Hash
|
, HBS2.Hash
|
||||||
|
@ -115,6 +116,7 @@ library
|
||||||
, HBS2.Prelude
|
, HBS2.Prelude
|
||||||
, HBS2.Prelude.Plated
|
, HBS2.Prelude.Plated
|
||||||
, HBS2.Storage
|
, HBS2.Storage
|
||||||
|
, HBS2.Storage.Operations
|
||||||
, HBS2.System.Logger.Simple
|
, HBS2.System.Logger.Simple
|
||||||
, HBS2.System.Logger.Simple.Class
|
, HBS2.System.Logger.Simple.Class
|
||||||
, Dialog.Core
|
, Dialog.Core
|
||||||
|
@ -181,6 +183,7 @@ library
|
||||||
, unordered-containers
|
, unordered-containers
|
||||||
, unliftio
|
, unliftio
|
||||||
, unliftio-core
|
, unliftio-core
|
||||||
|
, zlib
|
||||||
|
|
||||||
|
|
||||||
hs-source-dirs: lib
|
hs-source-dirs: lib
|
||||||
|
|
|
@ -0,0 +1,171 @@
|
||||||
|
{-# Language AllowAmbiguousTypes #-}
|
||||||
|
{-# Language UndecidableInstances #-}
|
||||||
|
module HBS2.Data.Bundle where
|
||||||
|
|
||||||
|
import HBS2.Prelude
|
||||||
|
import HBS2.Storage
|
||||||
|
import HBS2.Storage.Operations
|
||||||
|
import HBS2.Hash
|
||||||
|
import HBS2.Data.Types.Refs
|
||||||
|
import HBS2.Data.Detect
|
||||||
|
|
||||||
|
import Data.Word
|
||||||
|
|
||||||
|
import Codec.Compression.GZip as GZip
|
||||||
|
import Codec.Serialise
|
||||||
|
import Control.Monad
|
||||||
|
import Control.Monad.Trans.Maybe
|
||||||
|
import Data.ByteString.Lazy.Char8 (ByteString)
|
||||||
|
import Data.ByteString.Lazy.Char8 qualified as LBS
|
||||||
|
import Data.Functor
|
||||||
|
import Data.List qualified as List
|
||||||
|
import Data.Either
|
||||||
|
|
||||||
|
import Streaming.Prelude qualified as S
|
||||||
|
import Streaming()
|
||||||
|
|
||||||
|
{- HLINT ignore "Use newtype instead of data" -}
|
||||||
|
|
||||||
|
-- у нас может быть много способов хранить данные:
|
||||||
|
-- сжимать целиком (эффективно, но медленно)
|
||||||
|
-- сжимать по секциям (быстрее, но менее эффективно)
|
||||||
|
data BundleHead =
|
||||||
|
BundleHeadSimple
|
||||||
|
{ bundleHeadSectionSize :: Word16
|
||||||
|
}
|
||||||
|
deriving stock (Generic,Show)
|
||||||
|
|
||||||
|
instance Serialise BundleHead
|
||||||
|
|
||||||
|
bundleHeadSize :: Integral a => a
|
||||||
|
bundleHeadSize = 64
|
||||||
|
|
||||||
|
data BundleSection =
|
||||||
|
BundleSection
|
||||||
|
{ _bundleSectionSize :: Word32
|
||||||
|
, _bundleSectionHash :: Maybe HashRef
|
||||||
|
}
|
||||||
|
deriving stock (Generic,Show)
|
||||||
|
|
||||||
|
instance Serialise BundleSection
|
||||||
|
|
||||||
|
sectionHeadSize :: Integral a => a
|
||||||
|
sectionHeadSize = 64
|
||||||
|
|
||||||
|
-- TODO: implement-create-bundle
|
||||||
|
-- создаёт bundle в том же самом хранилище,
|
||||||
|
-- допустим.
|
||||||
|
-- Возвращает HashRef
|
||||||
|
-- Если нам нужен сам бандл - всегда сможем
|
||||||
|
-- сделать hbs2 cat.
|
||||||
|
-- t HashRef -- это что-то, что даёт нам список хэшей?
|
||||||
|
-- для каждого хэша надо бы уметь считать
|
||||||
|
-- его данные, и куда-то их записать в лог.
|
||||||
|
-- лог может быть очень большим. как его
|
||||||
|
-- писать-то? допустим, через tmp файл -- тогда
|
||||||
|
-- довольно прямолинейно, но функция требует
|
||||||
|
-- файловой системы.
|
||||||
|
-- можно поблочно, но это прямо довольно-таки сложно
|
||||||
|
-- и неатомарно. Как же сделать?
|
||||||
|
-- непрямолинейное решение:
|
||||||
|
-- пишем в буфер (какой?)
|
||||||
|
-- как понаписали достат. кол - то что? меркл
|
||||||
|
-- дерево у нас создаётся через toPtree
|
||||||
|
|
||||||
|
createBundle :: ( MonadIO m
|
||||||
|
, Storage sto HbSync ByteString IO
|
||||||
|
)
|
||||||
|
=> sto
|
||||||
|
-> [HashRef]
|
||||||
|
-> m (Maybe HashRef)
|
||||||
|
|
||||||
|
createBundle sto refs = runMaybeT do
|
||||||
|
-- читать блок из сторейджа
|
||||||
|
blocks <- forM refs $ \href -> do
|
||||||
|
blk <- MaybeT $ liftIO $ getBlock sto (fromHashRef href)
|
||||||
|
|
||||||
|
let compressed = compressWith params blk
|
||||||
|
let size = LBS.length compressed
|
||||||
|
let section = BundleSection (fromIntegral size) (Just href)
|
||||||
|
|
||||||
|
let sbs = serialise section
|
||||||
|
let pad = sectionHeadSize - LBS.length sbs
|
||||||
|
let pads = LBS.replicate pad '\x0'
|
||||||
|
|
||||||
|
pure (sbs <> pads <> compressed)
|
||||||
|
|
||||||
|
let buHead = serialise (BundleHeadSimple sectionHeadSize)
|
||||||
|
let buPadded = buHead <> LBS.replicate (bundleHeadSize - LBS.length buHead) '\x0'
|
||||||
|
|
||||||
|
let blob = buPadded <> mconcat blocks
|
||||||
|
|
||||||
|
wtf <- liftIO $ writeAsMerkle sto blob
|
||||||
|
|
||||||
|
pure (HashRef wtf)
|
||||||
|
|
||||||
|
where
|
||||||
|
params = defaultCompressParams { compressLevel = bestSpeed }
|
||||||
|
|
||||||
|
data ImportError =
|
||||||
|
ImportBlocksMissed [HashRef]
|
||||||
|
| ImportBadJournal
|
||||||
|
deriving stock (Eq,Show)
|
||||||
|
|
||||||
|
missedBlocks :: ImportError -> [HashRef]
|
||||||
|
missedBlocks = \case
|
||||||
|
ImportBlocksMissed blk -> blk
|
||||||
|
_ -> mempty
|
||||||
|
|
||||||
|
class MonadIO m => ImportBundle bundle m where
|
||||||
|
importBundle :: forall sto . Storage sto HbSync ByteString m
|
||||||
|
=> sto
|
||||||
|
-> ( (Maybe HashRef, ByteString) -> m () )
|
||||||
|
-> bundle
|
||||||
|
-> m (Either ImportError ())
|
||||||
|
|
||||||
|
|
||||||
|
instance MonadIO m => ImportBundle HashRef m where
|
||||||
|
-- можем только целиком считать в память, потом пройтись по секциям и записать объекты
|
||||||
|
-- по памяти капец, что может быть
|
||||||
|
importBundle sto action bundle = do
|
||||||
|
let h = fromHashRef bundle
|
||||||
|
|
||||||
|
res <- S.toList_ $
|
||||||
|
deepScan ScanDeep yieldMissed h (lift . getBlock sto) $ \ha -> do
|
||||||
|
-- FIXME: annoying-shit
|
||||||
|
when (ha /= h) do
|
||||||
|
lift (getBlock sto ha) >>= maybe (yieldMissed ha) (S.yield . Right)
|
||||||
|
|
||||||
|
let missed = lefts res
|
||||||
|
|
||||||
|
if not (null missed) then
|
||||||
|
pure $ Left $ ImportBlocksMissed $ foldMap missedBlocks missed
|
||||||
|
else do
|
||||||
|
let bss = mconcat (rights res)
|
||||||
|
let (hs, bs) = LBS.splitAt bundleHeadSize bss
|
||||||
|
-- FIXME: fix-possible-memory-exhausting
|
||||||
|
case deserialiseOrFail @BundleHead hs of
|
||||||
|
Left{} -> pure $ Left ImportBadJournal
|
||||||
|
Right hd -> go hd bs
|
||||||
|
|
||||||
|
where
|
||||||
|
yieldMissed = S.yield . Left . ImportBlocksMissed . pure . HashRef
|
||||||
|
|
||||||
|
go hd bs
|
||||||
|
| LBS.null bs = pure $ Right ()
|
||||||
|
| otherwise = do
|
||||||
|
let ss = bundleHeadSectionSize hd
|
||||||
|
let (bsh, allBsRest) = LBS.splitAt sectionHeadSize bs
|
||||||
|
case deserialiseOrFail @BundleSection bsh of
|
||||||
|
Left{} -> do
|
||||||
|
pure $ Left ImportBadJournal
|
||||||
|
|
||||||
|
Right header -> do
|
||||||
|
let sz = fromIntegral $ _bundleSectionSize header
|
||||||
|
let (blk, rest) = LBS.splitAt sz allBsRest
|
||||||
|
if LBS.length blk /= sz then do
|
||||||
|
pure $ Left ImportBadJournal
|
||||||
|
else do
|
||||||
|
action (_bundleSectionHash header, GZip.decompress blk)
|
||||||
|
go hd rest
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
{-# Language AllowAmbiguousTypes #-}
|
||||||
|
{-# Language UndecidableInstances #-}
|
||||||
|
module HBS2.Storage.Operations where
|
||||||
|
|
||||||
|
import HBS2.Prelude.Plated
|
||||||
|
import HBS2.Hash
|
||||||
|
import HBS2.Storage
|
||||||
|
import HBS2.Merkle
|
||||||
|
import HBS2.Data.Types.Refs
|
||||||
|
import HBS2.Defaults
|
||||||
|
|
||||||
|
import Streaming.Prelude qualified as S
|
||||||
|
import Streaming qualified as S
|
||||||
|
import Data.Functor
|
||||||
|
import Data.Function
|
||||||
|
|
||||||
|
import Data.Bifunctor
|
||||||
|
import Data.ByteString.Lazy (ByteString)
|
||||||
|
import Data.ByteString.Lazy qualified as B
|
||||||
|
|
||||||
|
|
||||||
|
class (MonadIO m, Storage storage hash block m) => MerkleWriter block hash storage m where
|
||||||
|
writeAsMerkle :: storage -> block -> m (Hash hash)
|
||||||
|
|
||||||
|
instance (MonadIO m, h ~ HbSync, Storage s h ByteString m) => MerkleWriter ByteString h s m where
|
||||||
|
writeAsMerkle sto bs = do
|
||||||
|
|
||||||
|
hashes <- S.each (B.unpack bs)
|
||||||
|
& S.chunksOf (fromIntegral defBlockSize)
|
||||||
|
& S.mapped (fmap (first B.pack) . S.toList)
|
||||||
|
& S.mapM (\blk -> enqueueBlock sto blk >> pure blk)
|
||||||
|
& S.map (HashRef . hashObject)
|
||||||
|
& S.toList_
|
||||||
|
|
||||||
|
-- FIXME: handle-hardcode
|
||||||
|
let pt = toPTree (MaxSize 256) (MaxNum 256) hashes -- FIXME: settings
|
||||||
|
makeMerkle 0 pt $ \(_,_,bss) -> void $ putBlock sto bss
|
||||||
|
|
|
@ -15,6 +15,7 @@ main =
|
||||||
testCase "testSimpleStorageRandomReadWrite" testSimpleStorageRandomReadWrite
|
testCase "testSimpleStorageRandomReadWrite" testSimpleStorageRandomReadWrite
|
||||||
, testCase "testSimpleStorageNoKeys" testSimpleStorageNoKeys
|
, testCase "testSimpleStorageNoKeys" testSimpleStorageNoKeys
|
||||||
, testCase "testSimpleStorageRefs" testSimpleStorageRefs
|
, testCase "testSimpleStorageRefs" testSimpleStorageRefs
|
||||||
|
, testCase "testSimpleStorageBundles" testSimpleStorageBundles
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,10 @@ import HBS2.OrDie
|
||||||
import HBS2.Hash
|
import HBS2.Hash
|
||||||
import HBS2.Clock
|
import HBS2.Clock
|
||||||
import HBS2.Prelude.Plated
|
import HBS2.Prelude.Plated
|
||||||
|
import HBS2.Data.Types.Refs
|
||||||
import HBS2.Storage
|
import HBS2.Storage
|
||||||
import HBS2.Storage.Simple
|
import HBS2.Storage.Simple
|
||||||
|
import HBS2.Data.Bundle
|
||||||
|
|
||||||
import Control.Monad.Except
|
import Control.Monad.Except
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
|
@ -23,6 +25,7 @@ import System.FilePath.Posix
|
||||||
import System.IO.Temp
|
import System.IO.Temp
|
||||||
import Test.QuickCheck
|
import Test.QuickCheck
|
||||||
import System.TimeIt
|
import System.TimeIt
|
||||||
|
import System.IO
|
||||||
|
|
||||||
import Test.Tasty.HUnit
|
import Test.Tasty.HUnit
|
||||||
|
|
||||||
|
@ -211,3 +214,44 @@ testSimpleStorageRefs = do
|
||||||
|
|
||||||
pure ()
|
pure ()
|
||||||
|
|
||||||
|
testSimpleStorageBundles :: IO ()
|
||||||
|
testSimpleStorageBundles = do
|
||||||
|
withSystemTempDirectory "simpleStorageTest" $ \dir -> do
|
||||||
|
|
||||||
|
let opts = [ StoragePrefix (dir </> ".storage")
|
||||||
|
]
|
||||||
|
|
||||||
|
storage <- simpleStorageInit opts :: IO (SimpleStorage HbSync)
|
||||||
|
|
||||||
|
worker <- async (simpleStorageWorker storage)
|
||||||
|
|
||||||
|
link worker
|
||||||
|
|
||||||
|
hPrint stderr "HERE I TEST BUNDLES"
|
||||||
|
|
||||||
|
-- тут я хочу сгенерить 100 рандомных байтстрок
|
||||||
|
bss <- generate $ replicateM 100 $ do
|
||||||
|
n <- choose (1, 1024)
|
||||||
|
LBS.pack <$> vectorOf n (choose (32, 126 :: Word8))
|
||||||
|
|
||||||
|
-- записать их при помощи putBlock
|
||||||
|
-- сохранить их хэши
|
||||||
|
hashes <- catMaybes <$> mapM (putBlock storage) bss
|
||||||
|
|
||||||
|
-- сделать bundle
|
||||||
|
bundle <- createBundle storage (fmap HashRef hashes) `orDie` "Can't create bundle"
|
||||||
|
|
||||||
|
-- удалить их
|
||||||
|
mapM_ (delBlock storage) hashes
|
||||||
|
|
||||||
|
-- убедиться, что реально удалены
|
||||||
|
here <- mapM (hasBlock storage) hashes
|
||||||
|
assertBool "all-blocks-deleted" (null (catMaybes here))
|
||||||
|
|
||||||
|
-- импортировать bundle
|
||||||
|
result <- importBundle storage (void . putBlock storage . snd) bundle
|
||||||
|
|
||||||
|
hereWeGoAgain <- mapM (hasBlock storage) hashes
|
||||||
|
assertBool "all-blocks-here-again" (not (null (catMaybes hereWeGoAgain)))
|
||||||
|
|
||||||
|
|
||||||
|
|
62
hbs2/Main.hs
62
hbs2/Main.hs
|
@ -13,6 +13,7 @@ import HBS2.Net.Proto.RefLog(RefLogKey(..))
|
||||||
import HBS2.Prelude.Plated
|
import HBS2.Prelude.Plated
|
||||||
import HBS2.Storage.Simple
|
import HBS2.Storage.Simple
|
||||||
import HBS2.Storage.Simple.Extra
|
import HBS2.Storage.Simple.Extra
|
||||||
|
import HBS2.Data.Bundle
|
||||||
import HBS2.OrDie
|
import HBS2.OrDie
|
||||||
|
|
||||||
|
|
||||||
|
@ -410,6 +411,7 @@ main = join . customExecParser (prefs showHelpOnError) $
|
||||||
<> command "show-peer-key" (info pShowPeerKey (progDesc "show peer key from credential file"))
|
<> command "show-peer-key" (info pShowPeerKey (progDesc "show peer key from credential file"))
|
||||||
<> command "groupkey-new" (info pNewGroupkey (progDesc "generates a new groupkey"))
|
<> command "groupkey-new" (info pNewGroupkey (progDesc "generates a new groupkey"))
|
||||||
<> command "reflog" (info pReflog (progDesc "reflog commands"))
|
<> command "reflog" (info pReflog (progDesc "reflog commands"))
|
||||||
|
<> command "bundle" (info pBundle(progDesc "bundle commands"))
|
||||||
)
|
)
|
||||||
|
|
||||||
common = do
|
common = do
|
||||||
|
@ -528,3 +530,63 @@ main = join . customExecParser (prefs showHelpOnError) $
|
||||||
print $ "deleted" <+> pretty d
|
print $ "deleted" <+> pretty d
|
||||||
hFlush stdout
|
hFlush stdout
|
||||||
|
|
||||||
|
pBundle = hsubparser ( command "create" (info pBundleCreate (progDesc "create bundle"))
|
||||||
|
<> command "list" (info pBundleList (progDesc "list bundle"))
|
||||||
|
<> command "import" (info pBundleImport (progDesc "import objects from bundle"))
|
||||||
|
)
|
||||||
|
|
||||||
|
pBundleCreate = do
|
||||||
|
o <- common
|
||||||
|
fname <- optional $ strOption (long "file" <> short 'f' <> help "hash list file (plaintext)")
|
||||||
|
pure $ withStore o $ \sto -> do
|
||||||
|
handle <- maybe (pure stdin) (flip openFile ReadMode . unOptFile) fname
|
||||||
|
|
||||||
|
ls <- hGetContents handle <&> lines
|
||||||
|
let hashes = mapMaybe (fromStringMay @HashRef) ls
|
||||||
|
|
||||||
|
when (length ls /= length hashes) do
|
||||||
|
die "Invalid hashref found"
|
||||||
|
|
||||||
|
bundle <- createBundle sto hashes `orDie` "can't create bundle"
|
||||||
|
|
||||||
|
print $ pretty bundle
|
||||||
|
|
||||||
|
|
||||||
|
pBundleImport = do
|
||||||
|
o <- common
|
||||||
|
mbHref <- strArgument (metavar "HASHREF")
|
||||||
|
pure $ withStore o $ \sto -> do
|
||||||
|
href <- pure (fromStringMay @HashRef mbHref) `orDie` "invalid hashref"
|
||||||
|
r <- importBundle sto (void . putBlock sto . snd) href
|
||||||
|
case r of
|
||||||
|
Right{} -> pure ()
|
||||||
|
Left e -> die (show e)
|
||||||
|
|
||||||
|
pBundleList = do
|
||||||
|
o <- common
|
||||||
|
mbHref <- strArgument (metavar "HASHREF")
|
||||||
|
doCheck <- optional (flag' False ( long "check" <> help "check hashes" )) <&> fromMaybe False
|
||||||
|
pure $ withStore o $ \sto -> do
|
||||||
|
href <- pure (fromStringMay @HashRef mbHref) `orDie` "invalid hashref"
|
||||||
|
r <- importBundle sto (outSection doCheck) href
|
||||||
|
case r of
|
||||||
|
Right{} -> pure ()
|
||||||
|
Left e -> die (show e)
|
||||||
|
|
||||||
|
where
|
||||||
|
outSection :: Bool -> (Maybe HashRef, ByteString) -> IO ()
|
||||||
|
|
||||||
|
outSection True x@(Just h, bs) = do
|
||||||
|
let hh = HashRef $ hashObject @HbSync bs
|
||||||
|
|
||||||
|
unless (hh == h) do
|
||||||
|
die $ "hash mismatch:" <> show (pretty h <+> pretty hh)
|
||||||
|
|
||||||
|
printHash x
|
||||||
|
|
||||||
|
outSection False x = printHash x
|
||||||
|
|
||||||
|
outSection _ x@(Nothing, _) = printHash x
|
||||||
|
|
||||||
|
printHash = void . print . pretty . fst
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue