packages feed

halfs-2.0: Halfs/Directory.hs

module Halfs.Directory
  ( DirHandle(..)
  , FileStat(..)
  , FileMode(..)
  , AccessRight(..)
  , FileType(..)
  , addDirEnt
  , addDirEnt_lckd
  , addDirEnt_lckd'
  , closeDirectory
  , find
  , findInDir
  , getDHINR_lckd
  , makeDirectory
  , newDirHandle
  , openDirectory
  , removeDirectory
  , rmDirEnt
  , rmDirEnt_lckd
  , syncDirectory
  , syncDirectory_lckd
  , withDirectory
  -- * for testing
  , DirectoryEntry(..)
  , DirectoryState(..)
  )
 where

import Control.Exception (assert)
import qualified Data.ByteString as BS
import qualified Data.Map as M
import Data.Serialize
import Foreign.C.Error

import Halfs.BlockMap
import Halfs.Classes
import Halfs.Errors
import Halfs.HalfsState
import Halfs.Monad
import Halfs.MonadUtils
import Halfs.Inode ( Inode(..)
                   , atomicReadInode
                   , blockAddrToInodeRef
                   , buildEmptyInodeEnc
                   , drefInode
                   , freeInode
                   , inodeRefToBlockAddr
                   , readStream
                   , withLockedInode
                   , writeStream
                   )
import Halfs.Protection
import Halfs.Types
import Halfs.Utils
import System.Device.BlockDevice

-- import Debug.Trace

type HalfsM b r l m a = HalfsT HalfsError (Maybe (HalfsState b r l m)) m a


--------------------------------------------------------------------------------
-- Directory manipulation and query functions

-- | Given a parent directory's inoderef, its owner, and its group,
-- generate a new, empty directory with the given name.
makeDirectory :: HalfsCapable b t r l m =>
                 InodeRef                 -- ^ inr to parent directory
              -> String                   -- ^ directory name
              -> UserID                   -- ^ user id for created directory
              -> GroupID                  -- ^ group id for created directory
              -> FileMode                 -- ^ initial perms for new directory
              -> HalfsM b r l m InodeRef  -- ^ on success, the inode ref to the
                                          --   created directory
makeDirectory parentIR dname user group perms =
  withDirectory parentIR $ \pdh -> do
    withDHLock pdh $ do
      -- Begin critical section over parent's DirHandle
      contents <- readRef (dhContents pdh)
      if M.member dname contents
       then throwError $ HE_ObjectExists dname
       else do
         bm  <- hasks hsBlockMap
         mir <- fmap blockAddrToInodeRef `fmap` alloc1 bm
         case mir of
           Nothing     -> throwError HE_AllocFailed
           Just thisIR -> do
             -- Build the directory inode and persist it
             dev  <- hasks hsBlockDev
             bstr <- lift $ buildEmptyInodeEnc
                              dev
                              Directory
                              perms
                              thisIR
                              parentIR
                              user
                              group
             assert (BS.length bstr == fromIntegral (bdBlockSize dev)) $ do
             lift $ bdWriteBlock dev (inodeRefToBlockAddr thisIR) bstr

             -- Add 'dname' to parent directory's contents
             addDirEnt_lckd pdh dname thisIR user group perms Directory
             return thisIR
      -- End critical section over parent's DirHandle

-- | Given a parent directory's inode ref, remove the directory with the given name.
removeDirectory :: HalfsCapable b t r l m =>
                   Maybe String -- ^ name to remove from parent
                                -- directory's content map (when Nothing,
                                -- leaves the the parent directory's
                                -- content map alone)
                -> InodeRef     -- ^ inr of directory to remove
                -> HalfsM b r l m ()
removeDirectory mdname inr = do
  -- TODO: Perms check (write perms on parent directory, etc.)
  dhMap <- hasks hsDHMap

  -- We lock the dirhandle map so (a) there's no contention for
  -- dirhandle lookup/creation for the directory we're removing and (b)
  -- so we can ensure that the directory is empty.

  withLockedRscRef dhMap $ \dhMapRef -> do
    dh <- lookupRM inr dhMapRef >>= maybe (newDirHandle inr) return
    withDHLock dh $ do
      -- begin dirhandle critical section
      contents <- readRef (dhContents dh)
      unless (M.null contents) $ HE_DirectoryNotEmpty `annErrno` eNOTEMPTY

      -- When we've been given a directory name, purge this dir's dirent from
      -- the parent directory.
      case mdname of
        Nothing    -> return ()
        Just dname ->
          withLockedInode inr $ do
            pinr <- inoParent `fmap` drefInode inr
            pdh  <- lookupRM pinr dhMapRef >>= maybe (newDirHandle pinr) return
            rmDirEnt pdh dname

      -- Invalidate dh so that all subsequent DH-mediated access fails
      writeRef (dhInode dh) Nothing
      deleteRM inr dhMapRef
      freeInode inr
      -- end dirhandle critical section

-- | Syncs directory contents to disk
syncDirectory :: HalfsCapable b t r l m =>
                 DirHandle r l
              -> HalfsM b r l m ()
syncDirectory dh = withDHLock dh $ syncDirectory_lckd dh

syncDirectory_lckd :: HalfsCapable b t r l m =>
                      DirHandle r l
                   -> HalfsM b r l m ()
syncDirectory_lckd dh = do
  -- Precond: (dhLock dh) is currently held (can we assert this? TODO)
  state <- readRef $ dhState dh

  -- TODO: Currently, we overwrite the entire DirectoryEntry list, truncating
  -- the directory's inode data stream as needed.  This is _braindead_, however.
  -- For OnlyAdded, we can just append to the stream; for OnlyDeleted, we can
  -- write only invalidating entries and employ incremental coalescing, etc.
  -- overwriteAll should be reserved for the VeryDirty case only.
  case state of
    Clean       -> return ()
    OnlyAdded   -> overwriteAll
    OnlyDeleted -> overwriteAll
    VeryDirty   -> overwriteAll
  where
    overwriteAll = do
      inr <- getDHINR_lckd dh
      writeStream inr 0 True
        =<< (encode . M.elems) `fmap` readRef (dhContents dh)
      lift . bdFlush =<< hasks hsBlockDev
      modifyRef (dhState dh) dirStTransClean

-- | Obtains an active directory handle for the directory at the given InodeRef
openDirectory :: HalfsCapable b t r l m =>
                 InodeRef
              -> HalfsM b r l m (DirHandle r l)
openDirectory inr = do
  -- TODO FIXME permissions checks!
  dhMap <- hasks hsDHMap
  mdh   <- withLockedRscRef dhMap (lookupRM inr)
  case mdh of
    Just dh -> return dh
    Nothing -> do
      dh <- newDirHandle inr
      withLockedRscRef dhMap $ \ref -> do
        -- If there's now a DirHandle in the map for our inode ref, prefer it to
        -- the one we just created; this is to safely avoid race conditions
        -- without extending the critical section over this entire function,
        -- which performs a potentially expensive BlockDevice read.
        mdh' <- lookupRM inr ref
        case mdh' of
          Just dh' -> return dh'
          Nothing  -> do
            insertRM inr dh ref
            return dh

closeDirectory :: HalfsCapable b t r l m =>
                  DirHandle r l
               -> HalfsM b r l m ()
closeDirectory dh = do
  syncDirectory dh
  return ()

-- | Add a directory entry for a file, directory, or symlink; expects
-- that the item does not already exist in the directory.  Thread-safe.
addDirEnt :: HalfsCapable b t r l m =>
             DirHandle r l
          -> String
          -> InodeRef
          -> UserID
          -> GroupID
          -> FileMode
          -> FileType
          -> HalfsM b r l m ()
addDirEnt dh name ir u g mode ftype =
  withDHLock dh $ addDirEnt_lckd dh name ir u g mode ftype

addDirEnt_lckd :: HalfsCapable b t r l m =>
                  DirHandle r l
               -> String
               -> InodeRef
               -> UserID
               -> GroupID
               -> FileMode
               -> FileType
               -> HalfsM b r l m ()
addDirEnt_lckd dh name inr u g mode ftype =
  addDirEnt_lckd' False dh $ DirEnt name inr u g mode ftype

addDirEnt_lckd' :: HalfsCapable b t r l m =>
                   Bool
                -> DirHandle r l
                -> DirectoryEntry
                -> HalfsM b r l m ()
addDirEnt_lckd' replaceOK dh de  = do
  -- Precond: (dhLock dh) is currently held (can we assert this? TODO)
  when (not replaceOK) $ do
    mfound <- lookupDE name dh
    maybe (return ()) (const $ throwError $ HE_ObjectExists name) mfound
  insertRM name de (dhContents dh)
  modifyRef (dhState dh) dirStTransAdd
  where
    name = deName de

-- | Remove a directory entry for a file, directory, or symlink; expects
-- that the item exists in the directory.  Thread-safe.
rmDirEnt :: HalfsCapable b t r l m =>
            DirHandle r l
         -> String
         -> HalfsM b r l m ()
rmDirEnt dh name =
  withDHLock dh $ rmDirEnt_lckd dh name

rmDirEnt_lckd :: HalfsCapable b t r l m =>
                 DirHandle r l
              -> String
              -> HalfsM b r l m ()
rmDirEnt_lckd dh name = do
  -- Precond: (dhLock dh) is currently held (can we assert this? TODO)
  -- begin sanity check
  mfound <- lookupDE name dh
  maybe (throwError $ HE_ObjectDNE name) (const $ return ()) mfound
  -- end sanity check
  deleteRM name (dhContents dh)
  modifyRef (dhState dh) dirStTransRm

-- | Finds a directory, file, or symlink given a starting inode
-- reference (i.e., the directory inode at which to begin the search)
-- and a list of path components.  Success is denoted using the DF_Found
-- constructor of the DirFindRslt type.
find :: HalfsCapable b t r l m =>
        InodeRef           -- ^ The starting inode reference
     -> FileType           -- ^ A match must be of this filetype
     -> [FilePath]         -- ^ Path components
     -> HalfsM b r l m (DirFindRslt InodeRef)
--
find startINR ftype [] = do
  ft <- atomicReadInode startINR inoFileType
  return $ foundRslt startINR ft ftype
--
find startINR ftype (pathComp:rest) = do
  dh <- openDirectory startINR
  sr <- findDE dh pathComp (if null rest then ftype else Directory)
  case sr of
    DF_NotFound         -> return $ DF_NotFound
    DF_WrongFileType ft -> return $ DF_WrongFileType ft
    DF_Found (de, _)    -> find (deInode de) ftype rest

-- | Locate the given directory entry typed file by filename in the
-- DirHandle's content map
findDE :: HalfsCapable b t r l m =>
          DirHandle r l
       -> String
       -> FileType
       -> HalfsM b r l m (DirFindRslt DirectoryEntry)
findDE dh fname ftype = do
  mde <- withDHLock dh $ lookupDE fname dh
  case mde of
    Nothing -> return DF_NotFound
    Just de -> return $ foundRslt de (deType de) ftype

-- Exportable version of findDE; doesn't expose DirectoryEntry to caller
findInDir :: HalfsCapable b t r l m =>
             DirHandle r l
          -> String
          -> FileType
          -> HalfsM b r l m (DirFindRslt InodeRef)
findInDir dh fname ftype = fmap deInode `fmap` findDE dh fname ftype

foundRslt :: a -> FileType -> FileType -> DirFindRslt a
foundRslt inr ft ftype =
  if ft `isFileType` ftype
   then DF_Found (inr, ft)
   else DF_WrongFileType ft


--------------------------------------------------------------------------------
-- Utility functions

newDirHandle :: HalfsCapable b t r l m =>
                InodeRef
             -> HalfsM b r l m (DirHandle r l)
newDirHandle inr = do
  rawDirBytes <- readStream inr 0 Nothing
  dirEnts     <- if BS.null rawDirBytes
                 then do return []
                 else case decode rawDirBytes of
                   Left msg -> throwError $ HE_DecodeFail_Directory msg
                   Right x  -> return x
  DirHandle
    `fmap` newRef (Just inr)
    `ap`   newRef (M.fromList $ map deName dirEnts `zip` dirEnts)
    `ap`   newRef Clean
    `ap`   newLock

-- Get directory handle's inode reference...
getDHINR_lckd :: HalfsCapable b t r l m =>
                 DirHandle r l
              -> HalfsM b r l m InodeRef
getDHINR_lckd dh = do
  -- Precond: (dhLock dh) has been acquired (TODO: can we assert this?)
  readRef (dhInode dh) >>= maybe (throwError HE_InvalidDirHandle) return

withDirectory :: HalfsCapable b t r l m =>
                 InodeRef
              -> (DirHandle r l -> HalfsM b r l m a)
              -> HalfsM b r l m a
withDirectory ir = hbracket (openDirectory ir) closeDirectory

isFileType :: FileType -> FileType -> Bool
isFileType _ AnyFileType = True
isFileType t1 t2         = t1 == t2

_showDH :: HalfsCapable b t r l m => DirHandle r l -> HalfsM b r l m String
_showDH dh = do
  withDHLock dh $ do
    state    <- readRef $ dhState dh
    contents <- readRef $ dhContents dh
    inr      <- getDHINR_lckd dh
    return $ "DirHandle { dhInode    = " ++ show inr
                    ++ ", dhContents = " ++ show contents
                    ++ ", dhState    = " ++ show state

dirStTransAdd :: DirectoryState -> DirectoryState
dirStTransAdd Clean     = OnlyAdded
dirStTransAdd OnlyAdded = OnlyAdded
dirStTransAdd _         = VeryDirty

dirStTransRm :: DirectoryState -> DirectoryState
dirStTransRm Clean       = OnlyDeleted
dirStTransRm OnlyDeleted = OnlyDeleted
dirStTransRm _           = VeryDirty

dirStTransClean :: DirectoryState -> DirectoryState
dirStTransClean = const Clean