packages feed

pfile-0.1.0.0: src/PFile/TrashCan.hs

{- |
Module:      PFile.TrashCan
Copyright:   (c) 2024 Illia Shkroba
License:     BSD3
Maintainer:  Illia Shkroba <is@pjwstk.edu.pl>
Stability:   unstable
Portability: non-portable (Non-Unix systems are not supported)

Types and functions for trashing filesystem's objects.
-}

{-# LANGUAGE BlockArguments    #-}
{-# LANGUAGE FlexibleContexts  #-}
{-# LANGUAGE LambdaCase        #-}
{-# LANGUAGE NamedFieldPuns    #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ViewPatterns      #-}

module PFile.TrashCan
  ( create
  , showCreateError
  , CreateError (..)
  , trash
  , showTrashError
  , TrashError (..)
  , restoreAll
  , restore
  , showRestoreEntryError
  , RestoreEntryError (..)
  , showRestoreError
  , RestoreError (..)
  , remove
  , showRemoveError
  , RemoveError (..)
  , dumpTrashed
  , showDumpTrashedError
  , DumpTrashedError (..)
  , trashed
  , TrashCan (TrashCan, root, dataRoot)
  ) where

import           Control.Monad.Writer       (MonadWriter)
import           Data.IORef
  ( IORef
  , modifyIORef'
  , newIORef
  , readIORef
  )
import           Data.List                  (delete)
import           PFile.Error
  ( liftIOWithError
  , modifyError
  , tellError
  )
import qualified PFile.Mount                as Mount
import           PFile.Path                 (doesPathExist, (<//>))
import qualified PFile.Path                 as Path
import qualified PFile.Profile.LinkHandling as LinkHandling
import           Protolude
import           System.Directory           (getTemporaryDirectory)
import           System.IO.Temp             (createTempDirectory)

-- | Create a new trash can.
--
-- @since 0.1.0.0
create :: (MonadError CreateError m, MonadIO m) => m TrashCan
create = do
  temporaryRoot <- getTemporaryDirectory
    `liftIOWithError` TemporaryDirectoryResolveError
  createTempDirectory temporaryRoot "pfile"
    `liftIOWithError` TemporaryDirectoryCreateError (Path.Absolute temporaryRoot)
    >>= \(Path.Absolute -> root) -> do
      _trashed <- newIORef [] & liftIO
      pure TrashCan {root, dataRoot = root <//> "data", _trashed}

showCreateError :: CreateError -> Text
showCreateError = \case
  TemporaryDirectoryResolveError cause
    -> "Unable to resolve temporary directory because of: " <> show cause
  TemporaryDirectoryCreateError path cause
    -> "Unable to create temporary directory in " <> Path.showAbsolute path
    <> " because of: " <> show cause

-- | Error thrown by 'create'.
--
-- @since 0.1.0.0
data CreateError
  = TemporaryDirectoryResolveError !IOException
  -- ^ 'IOException' was encountered during temporary directory resolving.
  | TemporaryDirectoryCreateError !Path.Absolute !IOException
  -- ^ 'IOException' was encountered during temporary directory creation.

-- | 'PFile.Mount.mount' 'PFile.Path.Absolute' inside of a 'TrashCan' with
-- 'PFile.Profile.LinkHandling.CopyLink' strategy for links.
--
-- @since 0.1.0.0
trash ::
     (MonadError TrashError m, MonadIO m) => Path.Absolute -> TrashCan -> m ()
trash originPath trashCan@TrashCan {dataRoot, _trashed} = do
  let Mount.Mount trashPath = Mount.mountPath (Mount.Root dataRoot) originPath
  whenM (doesPathExist trashPath) . throwError
    $ AlreadyTrashedError originPath trashCan
  Mount.mount LinkHandling.CopyLink (Mount.Root dataRoot) originPath
    & modifyError MountError
    & void
  modifyIORef' _trashed (trashPath :) & liftIO

showTrashError :: TrashError -> Text
showTrashError = \case
  AlreadyTrashedError path TrashCan {root}
    -> "Path " <> Path.showAbsolute path
    <> " is already trashed in: " <> Path.showAbsolute root <> "."
  MountError cause -> Mount.showMountError cause

-- | Error thrown by 'trash'.
--
-- @since 0.1.0.0
data TrashError
  = AlreadyTrashedError !Path.Absolute !TrashCan
  -- ^ 'PFile.Path.Absolute' is already trashed.
  | MountError !Mount.MountError
  -- ^ Error was encountered during 'PFile.Mount.mount'.

-- | 'restore' all 'trash'ed filesystem's objects back to their original
-- locations. When an error is encountered during 'restore', 'restoreAll'
-- terminates and provides successfully 'restore'ed entries via
-- a 'MonadWriter'.
--
-- @since 0.1.0.0
restoreAll :: (MonadWriter [RestoreEntryError] m, MonadIO m) => TrashCan -> m ()
restoreAll trashCan = trashed trashCan >>= traverse_ \path ->
  restore trashCan path
    & tellError (RestoreEntryError path)

-- | 'PFile.Mount.unmount' 'PFile.Path.Absolute' (turned into
-- 'PFile.Mount.Mount') from a 'TrashCan' back to its original location (before
-- 'trash').
--
-- @since 0.1.0.0
restore ::
     (MonadError RestoreError m, MonadIO m) => TrashCan -> Path.Absolute -> m ()
restore TrashCan {dataRoot, _trashed} path = do
  Mount.unmount (Mount.Root dataRoot) (Mount.Mount path)
    & modifyError UnmountError
    & void
  modifyIORef' _trashed (delete path) & liftIO

showRestoreEntryError :: RestoreEntryError -> Text
showRestoreEntryError = \case
  RestoreEntryError path cause
    -> "Unable to restore entry " <> Path.showAbsolute path
    <> " because of: " <> showRestoreError cause

-- | Error provided via 'MonadWriter' by 'restoreAll'.
--
-- @since 0.1.0.0
data RestoreEntryError
  = RestoreEntryError !Path.Absolute !RestoreError
  -- ^ Error was encountered during 'restore'.

showRestoreError :: RestoreError -> Text
showRestoreError = \case
  UnmountError cause -> Mount.showUnmountError cause

-- | Error thrown by 'restore'.
--
-- @since 0.1.0.0
newtype RestoreError
  = UnmountError Mount.UnmountError
  -- ^ Error was encountered during 'PFile.Mount.unmount'.

-- | Remove a 'TrashCan' forcibly.
--
-- @since 0.1.0.0
remove :: (MonadError RemoveError m, MonadIO m) => TrashCan -> m ()
remove TrashCan {root} =
  Path.remove root
    & modifyError TemporaryDirectoryRemoveError

showRemoveError :: RemoveError -> Text
showRemoveError = \case
  TemporaryDirectoryRemoveError cause
    -> "Unable to remove temporary directory because of: "
    <> Path.showRemoveError cause

-- | Error thrown by 'remove'.
--
-- @since 0.1.0.0
newtype RemoveError
  = TemporaryDirectoryRemoveError Path.RemoveError
  -- ^ Error was encountered during temporary directory removal.

-- | Dump 'trashed' to a file inside of the 'root' directory.
--
-- @since 0.1.0.0
dumpTrashed ::
     (MonadError DumpTrashedError m, MonadIO m) => TrashCan -> m Path.Absolute
dumpTrashed trashCan@TrashCan {root} = do
  paths <- trashed trashCan
  let dumped = unlines $ toS . Path.unAbsolute <$> paths
  Path.writeFile trashedPath dumped
    & modifyError DumpTrashedError
  pure trashedPath
  where
    trashedPath :: Path.Absolute
    trashedPath = root <//> "trashed.txt"

showDumpTrashedError :: DumpTrashedError -> Text
showDumpTrashedError = \case
  DumpTrashedError cause -> case cause of
    Path.CreateParentInWriteFileError {} -> Path.showWriteFileError cause
    Path.WriteFileError path ioCause
      -> "Unable to dump a list of trashed paths to " <> Path.showAbsolute path
      <> " because of: " <> show ioCause

-- | Error thrown by 'dumpTrashed'.
--
-- @since 0.1.0.0
newtype DumpTrashedError
  = DumpTrashedError Path.WriteFileError
  -- ^ Error was encountered during writing to a "trashed.txt" file inside of
  -- the 'root' directory.

-- | List of 'trash'ed filesystem's objects.
--
-- @since 0.1.0.0
trashed :: MonadIO m => TrashCan -> m [Path.Absolute]
trashed TrashCan {_trashed} = readIORef _trashed & liftIO

-- | Trash can for filesystem's objects.
--
-- @since 0.1.0.0
data TrashCan
  = TrashCan
      { root     :: !Path.Absolute
      -- ^ Root directory of a 'TrashCan'.
      , dataRoot :: !Path.Absolute
      -- ^ Directory of a 'TrashCan' where 'trash'ed filesystem's objects are
      -- located.
      , _trashed :: !(IORef [Path.Absolute])
      -- ^ List of 'trash'ed filesystem's objects in 'IORef'.
      }