mirror of
https://github.com/simplex-chat/zip.git
synced 2026-06-03 09:17:33 +00:00
592 lines
20 KiB
Haskell
592 lines
20 KiB
Haskell
-- |
|
||
-- Module : Codec.Archive.Zip
|
||
-- Copyright : © 2016–2017 Mark Karpov
|
||
-- License : BSD 3 clause
|
||
--
|
||
-- Maintainer : Mark Karpov <markkarpov@openmailbox.org>
|
||
-- Stability : experimental
|
||
-- Portability : portable
|
||
--
|
||
-- The module provides everything you may need to manipulate Zip archives.
|
||
-- There are three things that should be clarified right away, to avoid
|
||
-- confusion in the future.
|
||
--
|
||
-- First, we use 'EntrySelector' type that can be obtained from 'Path' 'Rel'
|
||
-- 'File' paths. This method may seem awkward at first, but it will protect
|
||
-- you from problems with portability when your archive is unpacked on a
|
||
-- different platform. Using of well-typed paths is also something you
|
||
-- should consider doing in your projects anyway. Even if you don't want to
|
||
-- use the "Path" module in your project, it's easy to marshal 'FilePath' to
|
||
-- 'Path' just before using functions from the library.
|
||
--
|
||
-- The second thing, that is rather a consequence of the first, is that
|
||
-- there is no way to add directories, or to be precise, /empty directories/
|
||
-- to your archive. This approach is used in Git, and I find it quite sane.
|
||
--
|
||
-- Finally, the third feature of the library is that it does not modify
|
||
-- archive instantly, because doing so on every manipulation would often be
|
||
-- inefficient. Instead we maintain collection of pending actions that can
|
||
-- be turned into an optimized procedure that efficiently modifies archive
|
||
-- in one pass. Normally this should be of no concern to you, because all
|
||
-- actions are performed automatically when you leave the realm of
|
||
-- 'ZipArchive' monad. If, however, you ever need to force update, the
|
||
-- 'commit' function is your friend. There are even “undo” functions, by the
|
||
-- way.
|
||
--
|
||
-- An example of a program that prints list of archive entries:
|
||
--
|
||
-- > import Codec.Archive.Zip
|
||
-- > import Path.IO (resolveFile')
|
||
-- > import System.Environment (getArgs)
|
||
-- > import qualified Data.Map as M
|
||
-- >
|
||
-- > main :: IO ()
|
||
-- > main = do
|
||
-- > [fp] <- getArgs
|
||
-- > path <- resolveFile' fp
|
||
-- > entries <- withArchive path (M.keys <$> getEntries)
|
||
-- > mapM_ print entries
|
||
--
|
||
-- Create a Zip archive with a Hello World file:
|
||
--
|
||
-- > import Codec.Archive.Zip
|
||
-- > import Path (parseRelFile)
|
||
-- > import Path.IO (resolveFile')
|
||
-- > import System.Environment (getArgs)
|
||
-- >
|
||
-- > main :: IO ()
|
||
-- > main = do
|
||
-- > [fp] <- getArgs
|
||
-- > path <- resolveFile' fp
|
||
-- > s <- parseRelFile "hello-world.txt" >>= mkEntrySelector
|
||
-- > createArchive path (addEntry Store "Hello, World!" s)
|
||
--
|
||
-- Extract contents of specific file and print it:
|
||
--
|
||
-- > import Codec.Archive.Zip
|
||
-- > import Path (parseRelFile)
|
||
-- > import Path.IO (resolveFile')
|
||
-- > import System.Environment (getArgs)
|
||
-- > import qualified Data.ByteString.Char8 as B
|
||
-- >
|
||
-- > main :: IO ()
|
||
-- > main = do
|
||
-- > [fp,f] <- getArgs
|
||
-- > path <- resolveFile' fp
|
||
-- > s <- parseRelFile f >>= mkEntrySelector
|
||
-- > bs <- withArchive path (getEntry s)
|
||
-- > B.putStrLn bs
|
||
|
||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||
|
||
module Codec.Archive.Zip
|
||
( -- * Types
|
||
-- ** Entry selector
|
||
EntrySelector
|
||
, mkEntrySelector
|
||
, unEntrySelector
|
||
, getEntryName
|
||
, EntrySelectorException (..)
|
||
-- ** Entry description
|
||
, EntryDescription (..)
|
||
, CompressionMethod (..)
|
||
-- ** Archive description
|
||
, ArchiveDescription (..)
|
||
-- ** Exceptions
|
||
, ZipException (..)
|
||
-- * Archive monad
|
||
, ZipArchive
|
||
, createArchive
|
||
, withArchive
|
||
-- * Retrieving information
|
||
, getEntries
|
||
, doesEntryExist
|
||
, getEntryDesc
|
||
, getEntry
|
||
, getEntrySource
|
||
, sourceEntry
|
||
, saveEntry
|
||
, checkEntry
|
||
, unpackInto
|
||
, getArchiveComment
|
||
, getArchiveDescription
|
||
-- * Modifying archive
|
||
-- ** Adding entries
|
||
, addEntry
|
||
, sinkEntry
|
||
, loadEntry
|
||
, copyEntry
|
||
, packDirRecur
|
||
-- ** Modifying entries
|
||
, renameEntry
|
||
, deleteEntry
|
||
, recompress
|
||
, setEntryComment
|
||
, deleteEntryComment
|
||
, setModTime
|
||
, addExtraField
|
||
, deleteExtraField
|
||
, forEntries
|
||
-- ** Operations on archive as a whole
|
||
, setArchiveComment
|
||
, deleteArchiveComment
|
||
-- ** Control over editing
|
||
, undoEntryChanges
|
||
, undoArchiveChanges
|
||
, undoAll
|
||
, commit )
|
||
where
|
||
|
||
import Codec.Archive.Zip.Type
|
||
import Control.Monad
|
||
import Control.Monad.Catch
|
||
import Control.Monad.State.Strict
|
||
import Control.Monad.Trans.Resource (ResourceT, runResourceT, MonadResource)
|
||
import Data.ByteString (ByteString)
|
||
import Data.Conduit (Source, Sink, ($$), yield)
|
||
import Data.Map.Strict (Map, (!))
|
||
import Data.Sequence (Seq, (|>))
|
||
import Data.Text (Text)
|
||
import Data.Time.Clock (UTCTime)
|
||
import Data.Word (Word16)
|
||
import Path
|
||
import Path.IO
|
||
import qualified Codec.Archive.Zip.Internal as I
|
||
import qualified Data.Conduit.Binary as CB
|
||
import qualified Data.Conduit.List as CL
|
||
import qualified Data.Map.Strict as M
|
||
import qualified Data.Sequence as S
|
||
import qualified Data.Set as E
|
||
|
||
----------------------------------------------------------------------------
|
||
-- Archive monad
|
||
|
||
-- | Monad that provides context necessary for performing operations on zip
|
||
-- archives. It's intentionally opaque and not a monad transformer to limit
|
||
-- number of actions that can be performed in it to those provided by this
|
||
-- module and their combinations.
|
||
|
||
newtype ZipArchive a = ZipArchive
|
||
{ unZipArchive :: StateT ZipState IO a
|
||
} deriving ( Functor
|
||
, Applicative
|
||
, Monad
|
||
, MonadIO
|
||
, MonadThrow
|
||
, MonadCatch
|
||
, MonadMask )
|
||
|
||
-- | Internal state record used by the 'ZipArchive' monad.
|
||
|
||
data ZipState = ZipState
|
||
{ zsFilePath :: Path Abs File
|
||
-- ^ Absolute path to zip archive
|
||
, zsEntries :: Map EntrySelector EntryDescription
|
||
-- ^ Actual collection of entries
|
||
, zsArchive :: ArchiveDescription
|
||
-- ^ Info about the whole archive
|
||
, zsActions :: Seq I.PendingAction
|
||
-- ^ Pending actions
|
||
}
|
||
|
||
-- | Create a new archive given its location and action that describes how
|
||
-- to create contents of the archive. This will silently overwrite the
|
||
-- specified file if it already exists. See 'withArchive' if you want to
|
||
-- work with an existing archive.
|
||
|
||
createArchive :: (MonadIO m, MonadCatch m)
|
||
=> Path b File -- ^ Location of archive file to create
|
||
-> ZipArchive a -- ^ Actions that form archive's content
|
||
-> m a
|
||
createArchive path m = do
|
||
apath <- makeAbsolute path
|
||
ignoringAbsence (removeFile apath)
|
||
let st = ZipState
|
||
{ zsFilePath = apath
|
||
, zsEntries = M.empty
|
||
, zsArchive = ArchiveDescription Nothing 0 0
|
||
, zsActions = S.empty }
|
||
action = unZipArchive (liftM2 const m commit)
|
||
liftIO (evalStateT action st)
|
||
|
||
-- | Work with an existing archive. See 'createArchive' if you want to
|
||
-- create a new archive instead.
|
||
--
|
||
-- This operation may fail with:
|
||
--
|
||
-- * @isAlreadyInUseError@ if the file is already open and cannot be
|
||
-- reopened;
|
||
--
|
||
-- * @isDoesNotExistError@ if the file does not exist;
|
||
--
|
||
-- * @isPermissionError@ if the user does not have permission to open
|
||
-- the file;
|
||
--
|
||
-- * 'ParsingFailed' when specified archive is something this library
|
||
-- cannot parse (this includes multi-disk archives, for example).
|
||
--
|
||
-- Please note that entries with invalid (non-portable) file names may be
|
||
-- missing in list of entries. Files that are compressed with unsupported
|
||
-- compression methods are skipped as well. Also, if several entries would
|
||
-- collide on some operating systems (such as Windows, because of its
|
||
-- case-insensitivity), only one of them will be available, because
|
||
-- 'EntrySelector' is case-insensitive. These are the consequences of the
|
||
-- design decision to make it impossible to create non-portable archives
|
||
-- with this library.
|
||
|
||
withArchive :: (MonadIO m, MonadThrow m)
|
||
=> Path b File -- ^ Location of archive to work with
|
||
-> ZipArchive a -- ^ Actions on that archive
|
||
-> m a
|
||
withArchive path m = do
|
||
apath <- canonicalizePath path
|
||
(desc, entries) <- liftIO (I.scanArchive apath)
|
||
let st = ZipState
|
||
{ zsFilePath = apath
|
||
, zsEntries = entries
|
||
, zsArchive = desc
|
||
, zsActions = S.empty }
|
||
action = unZipArchive (liftM2 const m commit)
|
||
liftIO (evalStateT action st)
|
||
|
||
----------------------------------------------------------------------------
|
||
-- Retrieving information
|
||
|
||
-- | Retrieve description of all archive entries. This is an efficient
|
||
-- operation that can be used for example to list all entries in archive. Do
|
||
-- not hesitate to use the function frequently: scanning of archive happens
|
||
-- only once anyway.
|
||
--
|
||
-- Please note that the returned value only reflects actual contents of
|
||
-- archive in file system, non-committed actions do not influence the list
|
||
-- of entries, see 'commit' for more information.
|
||
|
||
getEntries :: ZipArchive (Map EntrySelector EntryDescription)
|
||
getEntries = ZipArchive (gets zsEntries)
|
||
|
||
-- | Check whether the specified entry exists in the archive. This is a
|
||
-- simple shortcut defined as:
|
||
--
|
||
-- > doesEntryExist s = M.member s <$> getEntries
|
||
|
||
doesEntryExist :: EntrySelector -> ZipArchive Bool
|
||
doesEntryExist s = M.member s <$> getEntries
|
||
|
||
-- | Get 'EntryDescription' for specified entry. This is a simple shortcut
|
||
-- defined as:
|
||
--
|
||
-- > getEntryDesc s = M.lookup s <$> getEntries
|
||
|
||
getEntryDesc :: EntrySelector -> ZipArchive (Maybe EntryDescription)
|
||
getEntryDesc s = M.lookup s <$> getEntries
|
||
|
||
-- | Get contents of specific archive entry as strict 'ByteString'. It's not
|
||
-- recommended to use this on big entries, because it will suck out a lot of
|
||
-- memory. For big entries, use conduits: 'sourceEntry'.
|
||
--
|
||
-- Throws: 'EntryDoesNotExist'.
|
||
|
||
getEntry
|
||
:: EntrySelector -- ^ Selector that identifies archive entry
|
||
-> ZipArchive ByteString -- ^ Contents of the entry
|
||
getEntry s = sourceEntry s (CL.foldMap id)
|
||
|
||
-- | Get entry source.
|
||
--
|
||
-- Throws: 'EntryDoesNotExist'.
|
||
--
|
||
-- @since 0.1.3
|
||
|
||
getEntrySource
|
||
:: MonadResource m
|
||
=> EntrySelector
|
||
-> ZipArchive (Source m ByteString)
|
||
getEntrySource s = do
|
||
path <- getFilePath
|
||
mdesc <- M.lookup s <$> getEntries
|
||
case mdesc of
|
||
Nothing -> throwM (EntryDoesNotExist path s)
|
||
Just desc -> return (I.sourceEntry path desc True)
|
||
|
||
-- | Stream contents of an archive entry to the given 'Sink'.
|
||
--
|
||
-- Throws: 'EntryDoesNotExist'.
|
||
|
||
sourceEntry
|
||
:: EntrySelector
|
||
-- ^ Selector that identifies archive entry
|
||
-> Sink ByteString (ResourceT IO) a
|
||
-- ^ Sink where to stream entry contents
|
||
-> ZipArchive a
|
||
-- ^ Contents of the entry (if found)
|
||
sourceEntry s sink = do
|
||
src <- getEntrySource s
|
||
(liftIO . runResourceT) (src $$ sink)
|
||
|
||
-- | Save a specific archive entry as a file in the file system.
|
||
--
|
||
-- Throws: 'EntryDoesNotExist'.
|
||
|
||
saveEntry
|
||
:: EntrySelector -- ^ Selector that identifies archive entry
|
||
-> Path b File -- ^ Where to save the file
|
||
-> ZipArchive ()
|
||
saveEntry s path = sourceEntry s (CB.sinkFile (toFilePath path))
|
||
|
||
-- | Calculate CRC32 check sum and compare it with the value read from the
|
||
-- archive. The function returns 'True' when the check sums are the same —
|
||
-- that is, the data is not corrupted.
|
||
--
|
||
-- Throws: 'EntryDoesNotExist'.
|
||
|
||
checkEntry
|
||
:: EntrySelector -- ^ Selector that identifies archive entry
|
||
-> ZipArchive Bool -- ^ Is the entry intact?
|
||
checkEntry s = do
|
||
calculated <- sourceEntry s I.crc32Sink
|
||
given <- edCRC32 . (! s) <$> getEntries
|
||
-- ↑ NOTE We can assume that entry exists for sure because otherwise
|
||
-- 'sourceEntry' would have thrown 'EntryDoesNotExist' already.
|
||
return (calculated == given)
|
||
|
||
-- | Unpack the entire archive into the specified directory. The directory
|
||
-- will be created if it does not exist.
|
||
|
||
unpackInto :: Path b Dir -> ZipArchive ()
|
||
unpackInto dir' = do
|
||
selectors <- M.keysSet <$> getEntries
|
||
unless (null selectors) $ do
|
||
dir <- liftIO (makeAbsolute dir')
|
||
liftIO (ensureDir dir)
|
||
let dirs = E.map (parent . (dir </>) . unEntrySelector) selectors
|
||
forM_ dirs (liftIO . ensureDir)
|
||
forM_ selectors $ \s ->
|
||
saveEntry s (dir </> unEntrySelector s)
|
||
|
||
-- | Get the archive comment.
|
||
|
||
getArchiveComment :: ZipArchive (Maybe Text)
|
||
getArchiveComment = adComment <$> getArchiveDescription
|
||
|
||
-- | Get the archive description record.
|
||
|
||
getArchiveDescription :: ZipArchive ArchiveDescription
|
||
getArchiveDescription = ZipArchive (gets zsArchive)
|
||
|
||
----------------------------------------------------------------------------
|
||
-- Modifying archive
|
||
|
||
-- | Add a new entry to the archive given its contents in binary form.
|
||
|
||
addEntry
|
||
:: CompressionMethod -- ^ Compression method to use
|
||
-> ByteString -- ^ Entry contents
|
||
-> EntrySelector -- ^ Name of entry to add
|
||
-> ZipArchive ()
|
||
addEntry t b s = addPending (I.SinkEntry t (yield b) s)
|
||
|
||
-- | Stream data from the specified source to an archive entry.
|
||
|
||
sinkEntry
|
||
:: CompressionMethod -- ^ Compression method to use
|
||
-> Source (ResourceT IO) ByteString -- ^ Source of entry contents
|
||
-> EntrySelector -- ^ Name of entry to add
|
||
-> ZipArchive ()
|
||
sinkEntry t src s = addPending (I.SinkEntry t src s)
|
||
|
||
-- | Load an entry from a given file.
|
||
|
||
loadEntry
|
||
:: CompressionMethod -- ^ Compression method to use
|
||
-> (Path Abs File -> ZipArchive EntrySelector) -- ^ How to get 'EntrySelector'
|
||
-> Path b File -- ^ Path to file to add
|
||
-> ZipArchive ()
|
||
loadEntry t f path = do
|
||
apath <- liftIO (canonicalizePath path)
|
||
s <- f apath
|
||
modTime <- liftIO (getModificationTime path)
|
||
let src = CB.sourceFile (toFilePath apath)
|
||
addPending (I.SinkEntry t src s)
|
||
addPending (I.SetModTime modTime s)
|
||
|
||
-- | Copy an entry “as is” from another zip archive. If the entry does not
|
||
-- exist in that archive, 'EntryDoesNotExist' will be eventually thrown.
|
||
|
||
copyEntry
|
||
:: Path b File -- ^ Path to archive to copy from
|
||
-> EntrySelector -- ^ Name of entry (in source archive) to copy
|
||
-> EntrySelector -- ^ Name of entry to insert (in actual archive)
|
||
-> ZipArchive ()
|
||
copyEntry path s' s = do
|
||
apath <- liftIO (canonicalizePath path)
|
||
addPending (I.CopyEntry apath s' s)
|
||
|
||
-- | Add an entire directory to the archive. Please note that due to the
|
||
-- design of the library, empty sub-directories won't be added.
|
||
--
|
||
-- The action can throw the same exceptions as 'listDirRecur' and
|
||
-- 'InvalidEntrySelector'.
|
||
|
||
packDirRecur
|
||
:: CompressionMethod -- ^ Compression method to use
|
||
-> (Path Abs File -> ZipArchive EntrySelector) -- ^ How to get 'EntrySelector'
|
||
-> Path b Dir -- ^ Path to directory to add
|
||
-> ZipArchive ()
|
||
packDirRecur t f path = do
|
||
files <- snd <$> liftIO (listDirRecur path)
|
||
mapM_ (loadEntry t f) files
|
||
|
||
-- | Rename an entry in the archive. If the entry does not exist, nothing
|
||
-- will happen.
|
||
|
||
renameEntry
|
||
:: EntrySelector -- ^ Original entry name
|
||
-> EntrySelector -- ^ New entry name
|
||
-> ZipArchive ()
|
||
renameEntry old new = addPending (I.RenameEntry old new)
|
||
|
||
-- | Delete an entry from the archive, if it does not exist, nothing will
|
||
-- happen.
|
||
|
||
deleteEntry :: EntrySelector -> ZipArchive ()
|
||
deleteEntry s = addPending (I.DeleteEntry s)
|
||
|
||
-- | Change compression method of an entry, if it does not exist, nothing
|
||
-- will happen.
|
||
|
||
recompress
|
||
:: CompressionMethod -- ^ New compression method
|
||
-> EntrySelector -- ^ Name of entry to re-compress
|
||
-> ZipArchive ()
|
||
recompress t s = addPending (I.Recompress t s)
|
||
|
||
-- | Set an entry comment, if that entry does not exist, nothing will
|
||
-- happen. Note that if binary representation of the comment is longer than
|
||
-- 65535 bytes, it will be truncated on writing.
|
||
|
||
setEntryComment
|
||
:: Text -- ^ Text of the comment
|
||
-> EntrySelector -- ^ Name of entry to comment on
|
||
-> ZipArchive ()
|
||
setEntryComment text s = addPending (I.SetEntryComment text s)
|
||
|
||
-- | Delete an entry's comment, if that entry does not exist, nothing will
|
||
-- happen.
|
||
|
||
deleteEntryComment :: EntrySelector -> ZipArchive ()
|
||
deleteEntryComment s = addPending (I.DeleteEntryComment s)
|
||
|
||
-- | Set the “last modification” date\/time. The specified entry may be
|
||
-- missing, in that case the action has no effect.
|
||
|
||
setModTime
|
||
:: UTCTime -- ^ New modification time
|
||
-> EntrySelector -- ^ Name of entry to modify
|
||
-> ZipArchive ()
|
||
setModTime time s = addPending (I.SetModTime time s)
|
||
|
||
-- | Add an extra field. The specified entry may be missing, in that case
|
||
-- this action has no effect.
|
||
|
||
addExtraField
|
||
:: Word16 -- ^ Tag (header id) of extra field to add
|
||
-> ByteString -- ^ Body of the field
|
||
-> EntrySelector -- ^ Name of entry to modify
|
||
-> ZipArchive ()
|
||
addExtraField n b s = addPending (I.AddExtraField n b s)
|
||
|
||
-- | Delete an extra field by its type (tag). The specified entry may be
|
||
-- missing, in that case this action has no effect.
|
||
|
||
deleteExtraField
|
||
:: Word16 -- ^ Tag (header id) of extra field to delete
|
||
-> EntrySelector -- ^ Name of entry to modify
|
||
-> ZipArchive ()
|
||
deleteExtraField n s = addPending (I.DeleteExtraField n s)
|
||
|
||
-- | Perform an action on every entry in the archive.
|
||
|
||
forEntries
|
||
:: (EntrySelector -> ZipArchive ()) -- ^ Action to perform
|
||
-> ZipArchive ()
|
||
forEntries action = getEntries >>= mapM_ action . M.keysSet
|
||
|
||
-- | Set comment of the entire archive.
|
||
|
||
setArchiveComment :: Text -> ZipArchive ()
|
||
setArchiveComment text = addPending (I.SetArchiveComment text)
|
||
|
||
-- | Delete the archive comment if it's present.
|
||
|
||
deleteArchiveComment :: ZipArchive ()
|
||
deleteArchiveComment = addPending I.DeleteArchiveComment
|
||
|
||
-- | Undo changes to a specific archive entry.
|
||
|
||
undoEntryChanges :: EntrySelector -> ZipArchive ()
|
||
undoEntryChanges s = modifyActions f
|
||
where f = S.filter ((/= Just s) . I.targetEntry)
|
||
|
||
-- | Undo changes to the archive as a whole (archive's comment).
|
||
|
||
undoArchiveChanges :: ZipArchive ()
|
||
undoArchiveChanges = modifyActions f
|
||
where f = S.filter ((/= Nothing) . I.targetEntry)
|
||
|
||
-- | Undo all changes made in this editing session.
|
||
|
||
undoAll :: ZipArchive ()
|
||
undoAll = modifyActions (const S.empty)
|
||
|
||
-- | Archive contents are not modified instantly, but instead changes are
|
||
-- collected as “pending actions” that should be committed, in order to
|
||
-- efficiently modify archive in one pass. The actions are committed
|
||
-- automatically when program leaves the realm of 'ZipArchive' monad (i.e.
|
||
-- as part of 'createArchive' or 'withArchive'), or can be forced explicitly
|
||
-- with the help of this function. Once committed, changes take place in the
|
||
-- file system and cannot be undone.
|
||
|
||
commit :: ZipArchive ()
|
||
commit = do
|
||
file <- getFilePath
|
||
odesc <- getArchiveDescription
|
||
oentries <- getEntries
|
||
actions <- getPending
|
||
exists <- doesFileExist file
|
||
unless (S.null actions && exists) $ do
|
||
liftIO (I.commit file odesc oentries actions)
|
||
-- NOTE The most robust way to update internal description of the
|
||
-- archive is to scan it again — manual manipulations with descriptions
|
||
-- of entries are too error-prone. We also want to erase all pending
|
||
-- actions because 'I.commit' executes them all by definition.
|
||
(ndesc, nentries) <- liftIO (I.scanArchive file)
|
||
ZipArchive . modify $ \st -> st
|
||
{ zsEntries = nentries
|
||
, zsArchive = ndesc
|
||
, zsActions = S.empty }
|
||
|
||
----------------------------------------------------------------------------
|
||
-- Helpers
|
||
|
||
-- | Get the path of the actual archive file from inside of 'ZipArchive'
|
||
-- monad.
|
||
|
||
getFilePath :: ZipArchive (Path Abs File)
|
||
getFilePath = ZipArchive (gets zsFilePath)
|
||
|
||
-- | Get the collection of pending actions.
|
||
|
||
getPending :: ZipArchive (Seq I.PendingAction)
|
||
getPending = ZipArchive (gets zsActions)
|
||
|
||
-- | Modify the collection of pending actions in some way.
|
||
|
||
modifyActions :: (Seq I.PendingAction -> Seq I.PendingAction) -> ZipArchive ()
|
||
modifyActions f = ZipArchive (modify g)
|
||
where g st = st { zsActions = f (zsActions st) }
|
||
|
||
-- | Add a new action to the list of pending actions.
|
||
|
||
addPending :: I.PendingAction -> ZipArchive ()
|
||
addPending a = modifyActions (|> a)
|