hash-addressed-cli-2.0.0.0: library/HashAddressed/App/Command/Examples/Write.hs
module HashAddressed.App.Command.Examples.Write
(
writeCommand,
)
where
import Essentials
import HashAddressed.App.Command.Type
import HashAddressed.App.HashFunction.Naming
import HashAddressed.App.HashFunction.Options
import HashAddressed.App.Meta.Initialization
import HashAddressed.App.Meta.Paths
import HashAddressed.App.Meta.Reading
import HashAddressed.App.Verbosity.Options
import HashAddressed.App.Verbosity.Printing
import HashAddressed.App.Verbosity.Type
import HashAddressed.HashFunction
import Control.Monad.IO.Class (liftIO)
import HashAddressed.Directory (WriteResult (..), WriteType (..))
import Prelude (FilePath, IO)
import Data.Foldable (fold)
import qualified Control.Monad as Monad
import qualified Control.Monad.Trans.Except as Except
import qualified Control.Monad.Trans.Resource as Resource
import qualified Data.ByteString as Strict
import qualified Data.ByteString as Strict.ByteString
import qualified HashAddressed.Directory
import qualified Options.Applicative as Options
import qualified System.IO as IO
import qualified Data.Sequence as Seq
import qualified Control.Exception.Safe as Exception
import qualified Data.Either as Either
import qualified System.Directory as Directory
writeCommand :: Command
writeCommand = Options.info (parser <**> Options.helper) $ Options.progDesc
"Copy from the standard input stream (or a file, see --source-file) \
\to a hash-addressed store (see --target-directory)"
where
parser :: Options.Parser (CommandAction ())
parser = do
optStoreDirectory :: FilePath <-
Options.strOption $ Options.long "target-directory" <>
Options.help "Where the hash-addressed files are located"
optSourceFile :: Maybe FilePath <-
Options.optional $ Options.strOption $ Options.long "source-file" <>
Options.help "Path of file to copy to the store; if this option is \
\not given, will read from standard input stream instead"
optLinks :: [FilePath] <-
Options.many $ Options.strOption $ Options.long "link" <>
Options.help "After writing, create a symbolic link at this path \
\that points to the hash-addressed file. \
\This option may be given more than once to create multiple links. \
\The destination path path must be empty and its parent directory \
\must already exist. The process returns a non-zero exit code if \
\any of the links cannot be created."
optInitializeStore :: Bool <-
Options.switch $ Options.long "initialize" <>
Options.help "Set up a hash-addressed store if one does not already exist. \
\If this option is given, --hash-function is required."
optHashFunction :: Maybe HashFunction <- Options.optional $
Options.option hashFunctionRead $ Options.long "hash-function" <>
Options.help ("If --initialize is given, use this flag to specify the hash \
\function. If a store exists, fail unless it used this hash function. "
<> hashFunctionInstructions)
optVerbosity :: Verbosity <- verbosityOption
pure do
hashFunction <-
case optInitializeStore of
True -> case optHashFunction of
Nothing -> Except.throwE $ Seq.singleton $ "--initialize requires --hash-function"
Just hf -> do
Monad.when optInitializeStore $ tryInitializeStore CreateIfNotPresent
optVerbosity hf optStoreDirectory
pure hf
False -> do
configHashFunction <- readHashFunctionFromConfig optStoreDirectory
case optHashFunction of
Just hf | hf /= configHashFunction -> Except.throwE $ Seq.singleton $
"--hash-function " <> showHashFunction hf <>
" does not match hash function " <> showHashFunction configHashFunction
<> " in " <> configFile optStoreDirectory
_ -> pure ()
pure configHashFunction
putVerboseLn optVerbosity $ "The hash function is "
<> showHashFunction hashFunction
let store = HashAddressed.Directory.init hashFunction optStoreDirectory
WriteResult{ contentAddressedFile, writeType } <- liftIO $ Resource.runResourceT @IO do
input <- case optSourceFile of
Nothing -> pure IO.stdin
Just inputFile -> do
(_, h) <- Resource.allocate (IO.openBinaryFile inputFile IO.ReadMode) IO.hClose
pure h
liftIO $ HashAddressed.Directory.writeStreaming store
\(writeChunk :: Strict.ByteString -> m ()) -> do
let
loop :: m ()
loop = do
x <- liftIO $ Strict.ByteString.hGetSome input 4096
case Strict.ByteString.null x of
False -> writeChunk x *> loop
True -> pure ()
loop
putNormalLn optVerbosity contentAddressedFile
putVerboseLn optVerbosity case writeType of
AlreadyPresent -> "The file was already present in the store; no change was made."
NewContent -> "One new file was added to the store."
linkFailures <- fmap fold $ liftIO $ optLinks & traverse \linkToBeCreated ->
Exception.tryIO (Directory.createFileLink contentAddressedFile linkToBeCreated) <&> \case
Either.Left _ -> Seq.singleton $ "Failed to create link " <> linkToBeCreated
Either.Right () -> Seq.empty
Monad.unless (Seq.null linkFailures) $ Except.throwE linkFailures