packages feed

hasherize (empty) → 0.0.0.0

raw patch · 6 files changed

+405/−0 lines, 6 filesdep +basedep +base16-bytestringdep +bytestring

Dependencies added: base, base16-bytestring, bytestring, cassava, containers, cryptohash-sha256, directory, envparse, file-io, filepath, hasherize, ki, mtl, qsem, quaalude, safe-exceptions, stm, text, unfork, vector

Files

+ changelog.md view
@@ -0,0 +1,3 @@+## 0.0.0.0 - 2023-07-04++Initial release
+ executable/Main.hs view
@@ -0,0 +1,142 @@+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
+ hasherize.cabal view
@@ -0,0 +1,70 @@+cabal-version: 3.0++name: hasherize+version: 0.0.0.0+synopsis: Hash digests for files and directories+category: Filesystem, Hash++description:+    This library can produce a hash for filesystem content, which can be+    either a file or a directory.++    The name of the file or directory is not included in the hash digest.+    The digest of a directory is based on the names and contents of the+    files contained therein.+    No other filesystem metadata (timestamps, permissions, etc.) is+    included in the digest.++license: Apache-2.0+license-file: license.txt++author: Chris Martin+maintainer: Chris Martin, Julie Moronuki++homepage: https://github.com/typeclasses/hasherize++extra-source-files: *.md++common base+    default-language: GHC2021+    ghc-options: -Wall+    default-extensions:+        ApplicativeDo+        BlockArguments+        DerivingStrategies+        LambdaCase+        NoImplicitPrelude+        OverloadedStrings+        QuasiQuotes+    build-depends:+      , base ^>= 4.18+      , bytestring ^>= 0.11+      , containers ^>= 0.6.7+      , cryptohash-sha256 ^>= 0.11.102+      , directory ^>= 1.3.8+      , file-io ^>= 0.1+      , filepath ^>= 1.4.100+      , mtl ^>= 2.3.1+      , quaalude ^>= 0.0+      , text ^>= 2.0.2++library+    import: base+    hs-source-dirs: library+    exposed-modules: Hasherize++executable hasherize+    import: base+    hs-source-dirs: executable+    main-is: Main.hs+    build-depends:+      , base16-bytestring ^>= 1.0.2+      , cassava ^>= 0.5+      , envparse ^>= 0.5+      , hasherize+      , ki ^>= 1.0+      , qsem ^>= 0.1+      , safe-exceptions ^>= 0.1.7+      , stm ^>= 2.5.1+      , unfork ^>= 1.0.0+      , vector ^>= 0.12.3
+ library/Hasherize.hs view
@@ -0,0 +1,95 @@+module Hasherize (getHash) where++import Control.Monad (foldM)+import Control.Monad.Fail (fail)+import Control.Monad.IO.Class (MonadIO (liftIO))+import Control.Monad.State (evalState, evalStateT, get, modify')+import Crypto.Hash.SHA256 qualified as Hash+import Data.ByteString (ByteString)+import Data.ByteString qualified as BS+import Data.Foldable (for_)+import Data.Function (fix, flip)+import Data.List (map)+import Data.Map (Map)+import Data.Map qualified as Map+import Data.Sequence (Seq)+import Data.Sequence qualified as Seq+import Data.Text (Text)+import Data.Text qualified as Text+import Data.Text.Encoding qualified as Text+import Essentials+import System.Directory.OsPath qualified as Dir+import System.File.OsPath qualified as OsPath (withFile)+import System.IO qualified as IO+import System.OsPath (OsPath, (</>))+import System.OsPath qualified as OsPath+import Text.Show (show)++-- | Calculate the hash of a file or directory+getHash :: (MonadIO m) => OsPath -> m ByteString+getHash path = do+  files <- Map.toAscList <$> enumerateFiles path+  liftIO $ flip evalStateT Hash.init do+    for_ files \(k, v) -> do+      modify' $ flip Hash.update $ hashAbstractPath k+      modify' . flip Hash.update =<< getFileHash (path <//> v)+    Hash.finalize <$> get++getFileHash :: (MonadIO m) => OsPath -> m ByteString+getFileHash path = liftIO $ OsPath.withFile path IO.ReadMode \handle ->+  flip evalStateT Hash.init $ fix \continue -> do+    bs <- liftIO $ BS.hGetSome handle 4_096+    case BS.null bs of+      True -> Hash.finalize <$> get+      False -> do+        modify' $ flip Hash.update bs+        continue++newtype AbstractPath = AbstractPath (Seq Text)+  deriving newtype (Semigroup, Monoid, Eq, Ord)+  deriving stock (Show)++hashAbstractPath :: AbstractPath -> ByteString+hashAbstractPath (AbstractPath xs) = flip evalState Hash.init do+  for_ xs \x -> do+    modify' $ flip Hash.update $ Hash.hash $ Text.encodeUtf8 x+  Hash.finalize <$> get++abstractPathSingleton :: Text -> AbstractPath+abstractPathSingleton = AbstractPath . Seq.singleton++data OsPath' = This | OsPath' OsPath+  deriving stock (Show)++(<//>) :: OsPath -> OsPath' -> OsPath+(<//>) x = \case This -> x; OsPath' y -> (x </> y)++enumerateFiles :: forall m. (MonadIO m) => OsPath -> m (Map AbstractPath OsPath')+enumerateFiles x =+  getFType x >>= \case+    File -> pure (Map.singleton mempty This)+    Directory -> do+      let enumerateChild :: Map AbstractPath OsPath' -> OsPath -> m (Map AbstractPath OsPath')+          enumerateChild m name = do+            nameText <- Text.pack <$> liftIO (OsPath.decodeUtf name)++            let contextualize :: (AbstractPath, OsPath') -> (AbstractPath, OsPath')+                contextualize (k, v) = (abstractPathSingleton nameText <> k, OsPath' (name <//> v))++            m' <- enumerateFiles (x </> name)+            pure $ m <> Map.fromList (map contextualize (Map.toList m'))++      children <- liftIO $ Dir.listDirectory x++      foldM enumerateChild Map.empty children++data FType = File | Directory++getFType :: (MonadIO m) => OsPath -> m FType+getFType x =+  liftIO (Dir.doesFileExist x) >>= \case+    True -> pure File+    False ->+      liftIO (Dir.doesDirectoryExist x) >>= \case+        True -> pure Directory+        False -> liftIO (fail $ "Neither file nor directory: " <> show x)
+ license.txt view
@@ -0,0 +1,13 @@+Copyright 2023 Mission Valley Software LLC++Licensed under the Apache License, Version 2.0 (the "License");+you may not use this file except in compliance with the License.+You may obtain a copy of the License at++    http://www.apache.org/licenses/LICENSE-2.0++Unless required by applicable law or agreed to in writing, software+distributed under the License is distributed on an "AS IS" BASIS,+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+See the License for the specific language governing permissions and+limitations under the License.
+ readme.md view
@@ -0,0 +1,82 @@+## Executable++The `hasherize` executable creates a copy of a directory in which each+direct child of the directory has a hasherized name.++For example, suppose the input directory looks like this:++```+├── favicon.ico+└── fonts+    ├── css+    │   └── fonts.css+    └── fonts+        ├── FiraMono-Bold.woff2+        └── FiraMono-Regular.woff2+```++The output directory will look something like this:++```+├── d7555833df923044e820a82a040850268d80ca2a8b4ade4a7ecb1aaa709f503a-favicon.ico+└── b7114fb092b385a4419e4aafa30075ded6fe3232219dcb75f49afbc24bbee39c-fonts+    ├── css+    │   └── fonts.css+    └── fonts+        ├── FiraMono-Bold.woff2+        └── FiraMono-Regular.woff2+```++Additionally, a CSV file will be generated that maps input names to hashed names:++```+favicon.ico,d7555833df923044e820a82a040850268d80ca2a8b4ade4a7ecb1aaa709f503a-favicon.ico+fonts,b7114fb092b385a4419e4aafa30075ded6fe3232219dcb75f49afbc24bbee39c-fonts+```++### Parameters++The input and output paths must be specified by environment variables:++* `input-directory` - Where to read input from+* `output-directory` - Where to write hashed files to+* `output-manifest` - Where to put the CSV file+++## Library++This library provides a function called `getHash` which computes a hash+of a file or directory.++```haskell+Hasherize.getHash :: (MonadIO m) => OsPath -> m ByteString+```+++## Hashing system++A *path segment* is Unicode text.+The hash of a path segment is the SHA-256 digest of its UTF-8 encoding.++A *path* is a list of path segments.+The hash of a path is determined by hashing each segment, concatenating,+and taking a SHA-256 digest.++An *entry* consists of a path and the content of a file.+The hash of an entry is determined by hashing the path, hashing the+content, concatenating, and taking a SHA-256 digest.++The *enumeration* of a path is a list of entries.++* If the path is a file *x*, its enumeration consists of a single+  entry, *([], x)*.+* If the path is a directory, its enumeration consists of an entry for+  each file recursively contained within the directory, sorted by path.+  We assume that the file system contains no directory cycles.++The result returned by the `getHash` function is produced by enumerating+the path, hashing each entry, concatenating, and taking a SHA-256 digest.++The `hasherize` executable produces a hashed file name by concatenating+the hash digest in hexadecimal form, the hyphen character `-`, and the+original file name.