packages feed

hasherize-0.0.0.0: executable/Main.hs

module Main (main) where

import Control.Concurrent (getNumCapabilities)
import Control.Exception.Safe (bracket_, displayException)
import Control.Monad.IO.Class (MonadIO (liftIO))
import Control.Monad.STM (atomically)
import Data.ByteString (ByteString)
import Data.ByteString.Base16 qualified as Base16
import Data.ByteString.Builder qualified as BSB
import Data.ByteString.Lazy qualified as LBS
import Data.Csv qualified as CSV
import Data.Csv.Builder qualified as CSV.Builder
import Data.Either (Either (..))
import Data.Foldable (for_)
import Data.String (String)
import Data.Text (Text)
import Data.Text qualified as Text
import Data.Text.Encoding qualified as Text
import Data.Vector qualified as Vector
import Env qualified
import Essentials
import Hasherize (getHash)
import Ki qualified
import QSem (newQSem, signalQSem, waitQSem)
import System.Directory.OsPath qualified as Dir
import System.File.OsPath qualified as OsPath (withFile)
import System.IO (Handle, IO)
import System.IO qualified as IO
import System.OsPath (OsPath, (</>))
import System.OsPath qualified as OsPath
import System.OsString (OsString, osstr)
import Unfork (unforkAsyncIO_)
import Prelude ((*))

main :: IO ()
main = do
  -- Read environment variables
  Env {inputDirectory, outputDirectory, outputManifest} <- getHasherizeArgs

  -- Get a list of all the inputs
  inputNameList <- Dir.listDirectory inputDirectory

  capabilities <- getNumCapabilities

  -- How many threads
  let concurrency = capabilities * 2

  -- Set up a thread-safe function to write to the manifest CSV
  withManifestWriter outputManifest \writeToManifest' ->
    -- Establish a thread scope to start forking threads
    liftIO $ Ki.scoped \scope -> do
      semaphore <- newQSem concurrency

      -- Loop over the list of inputs
      for_ inputNameList \(inputName :: OsString) ->
        bracket_ (waitQSem semaphore) (signalQSem semaphore) $
          -- Start one thread per input
          Ki.fork scope do
            -- Create a symlink from original path to hashed path
            entry <- hasherize inputDirectory outputDirectory inputName
            -- Record the mapping in the manifest CSV
            writeToManifest' entry

      -- Wait for all threads to finish
      atomically $ Ki.awaitAll scope

hasherize ::
  (MonadIO m) =>
  -- | Input directory
  OsPath ->
  -- | Output directory
  OsPath ->
  -- | Name of file or directory being hasherized
  OsPath ->
  m (ManifestEntry ByteString)
hasherize inDirectory outDirectory inputName = do
  inputNameString :: String <- liftIO $ OsPath.decodeUtf inputName
  let inputNameText :: Text = Text.pack inputNameString

  -- How we'll refer to the input in filesystem operations
  let inputPath = inDirectory </> inputName

  -- Hash of the content
  hashText :: Text <- do
    hash :: ByteString <- getHash inputPath
    pure $ Text.decodeUtf8 $ Base16.encode hash

  -- The new input name with the hash included
  nameWithHash :: OsPath <- do
    hashOsString :: OsString <- liftIO $ OsPath.encodeUtf $ Text.unpack hashText
    pure $ hashOsString <> [osstr|-|] <> inputName

  -- Place the content in the output directory under its new alias
  createLink inputPath (outDirectory </> nameWithHash)

  -- Return the mapping to store in the manifest CSV
  pure $
    fmap Text.encodeUtf8 $
      ManifestEntry inputNameText (hashText <> "-" <> inputNameText)

data ManifestEntry a = ManifestEntry a a
  deriving stock (Functor, Foldable, Traversable)

instance CSV.ToRecord (ManifestEntry ByteString) where
  toRecord (ManifestEntry k v) = Vector.fromList [k, v]

createLink :: (MonadIO m) => OsPath -> OsPath -> m ()
createLink target source = do
  isDir <- liftIO $ Dir.doesDirectoryExist target
  target' <- liftIO $ Dir.makeAbsolute target
  liftIO $ (if isDir then Dir.createDirectoryLink else Dir.createFileLink) target' source

withManifestWriter ::
  (MonadIO m) =>
  OsPath ->
  ((ManifestEntry ByteString -> IO ()) -> IO a) ->
  m a
withManifestWriter path continue = liftIO $
  OsPath.withFile path IO.WriteMode \handle ->
    liftIO $ unforkAsyncIO_ (writeToManifest handle) continue

writeToManifest :: Handle -> ManifestEntry ByteString -> IO ()
writeToManifest handle =
  LBS.hPutStr handle . BSB.toLazyByteString . CSV.Builder.encodeRecord

data Env = Env
  { inputDirectory :: OsString,
    outputDirectory :: OsString,
    outputManifest :: OsString
  }

getHasherizeArgs :: (MonadIO m) => m Env
getHasherizeArgs = liftIO $ Env.parse id do
  inputDirectory <- Env.var pathReader "input-directory" mempty
  outputDirectory <- Env.var pathReader "output-directory" mempty
  outputManifest <- Env.var pathReader "output-manifest" mempty
  pure Env {inputDirectory, outputDirectory, outputManifest}

pathReader :: Env.Reader Env.Error OsPath
pathReader string = case OsPath.encodeUtf string of
  Left e -> Left $ Env.unread $ displayException e
  Right x -> Right x