zip-cmd-1.0.1: app/zip-cmd.hs
{-# language LambdaCase, RecordWildCards #-}
import Control.Monad
import Control.Monad.IO.Class
import Options.Applicative
import Options.Applicative.Extra
import Data.Version
import Data.List
import Data.Text (Text)
import Data.ByteString (ByteString)
import System.FilePath
import Codec.Archive.Zip
import Data.Time.Clock
import Data.Word
import qualified ShellWords
import qualified Paths_zip_cmd as ZipCmd
data ZipCommand
= CreateArchive
{ zipPath :: FilePath
}
| WithArchive
{ zipPath :: FilePath
}
| DoesEntryExist
{ entryPath :: FilePath
}
| GetEntry
{ entryPath :: FilePath
}
| SaveEntry
{ entryPath :: FilePath
, targetPath :: FilePath
}
| CheckEntry
{ entryPath :: FilePath
}
| UnpackInto
{ targetPath :: FilePath
}
| GetArchiveComment
| GetArchiveDescription
| AddEntry
{ compressionMethod :: CompressionMethod
, entryContent :: ByteString
, entryPath :: FilePath
}
| LoadEntry
{ compressionMethod :: CompressionMethod
, entryPath :: FilePath
, sourcePath :: FilePath
}
| CopyEntry
{ zipPath :: FilePath
, sourcePath :: FilePath
, targetPath :: FilePath
}
| PackDirRecur
{ compressionMethod :: CompressionMethod
, sourcePath :: FilePath
, entryPathPrefix :: Maybe FilePath
}
| RenameEntry
{ sourcePath :: FilePath
, targetPath :: FilePath
}
| DeleteEntry
{ entryPath :: FilePath
}
| Recompress
{ compressionMethod :: CompressionMethod
, entryPath :: FilePath
}
| SetEntryComment
{ comment :: Text
, entryPath :: FilePath
}
| DeleteEntryComment
{ entryPath :: FilePath
}
| SetModTime
{ time :: UTCTime
, entryPath :: FilePath
}
| AddExtraField
{ tag :: Word16
, body :: ByteString
, entryPath :: FilePath
}
| DeleteExtraField
{ tag :: Word16
, entryPath :: FilePath
}
| SetExternalFileAttrs
{ attributes :: Word32
, entryPath :: FilePath
}
| SetArchiveComment
{ comment :: Text
}
| DeleteArchiveComment
| RunZipCommandsFromFile
{ commandsPath :: FilePath
}
deriving Show
evalCommand :: ZipCommand -> ZipArchive ()
evalCommand = \case
DoesEntryExist{..} -> join (doesEntryExist <$> mkEntrySelector entryPath) >>= liftIO . print
GetEntry{..} -> join (getEntry <$> mkEntrySelector entryPath) >>= liftIO . print
SaveEntry{..} -> join (saveEntry <$> mkEntrySelector entryPath <*> pure targetPath)
CheckEntry{..} -> join (checkEntry <$> mkEntrySelector entryPath) >>= liftIO . print
UnpackInto{..} -> unpackInto targetPath
GetArchiveComment -> getArchiveComment >>= maybe (pure ()) (liftIO . print)
GetArchiveDescription -> getArchiveDescription >>= liftIO . print
AddEntry{..} -> join (addEntry compressionMethod entryContent <$> mkEntrySelector entryPath)
LoadEntry{..} -> join (loadEntry compressionMethod <$> mkEntrySelector entryPath <*> pure sourcePath)
CopyEntry{..} -> join (copyEntry zipPath <$> mkEntrySelector sourcePath <*> mkEntrySelector targetPath)
PackDirRecur{..} -> packDirRecur compressionMethod (\p -> mkEntrySelector $ maybe p (</> p) entryPathPrefix) sourcePath
RenameEntry{..} -> join (renameEntry <$> mkEntrySelector sourcePath <*> mkEntrySelector targetPath)
DeleteEntry{..} -> join (deleteEntry <$> mkEntrySelector entryPath)
Recompress{..} -> join (recompress compressionMethod <$> mkEntrySelector entryPath)
SetEntryComment{..} -> join (setEntryComment comment <$> mkEntrySelector entryPath)
DeleteEntryComment{..} -> join (deleteEntryComment <$> mkEntrySelector entryPath)
SetModTime{..} -> join (setModTime time <$> mkEntrySelector entryPath)
AddExtraField{..} -> join (addExtraField tag body <$> mkEntrySelector entryPath)
DeleteExtraField{..} -> join (deleteExtraField tag <$> mkEntrySelector entryPath)
SetExternalFileAttrs{..} -> join (setExternalFileAttrs attributes <$> mkEntrySelector entryPath)
SetArchiveComment{..} -> setArchiveComment comment
DeleteArchiveComment -> deleteArchiveComment
evalCmdZipArchive :: [ZipCommand] -> ZipArchive [ZipCommand]
evalCmdZipArchive cmds = case cmds of
[] -> pure []
CreateArchive{} : _ -> pure cmds
WithArchive{} : _ -> pure cmds
RunZipCommandsFromFile{..} : cs -> do
cmdList <- liftIO $ readCommandsFromFile commandsPath
evalCmdZipArchive $ cmdList ++ cs
c : cs -> do
evalCommand c
evalCmdZipArchive cs
evalCmdIO :: [ZipCommand] -> IO ()
evalCmdIO = \case
CreateArchive{..} : cmds -> do
remainingCmds <- createArchive zipPath (evalCmdZipArchive cmds)
evalCmdIO remainingCmds
WithArchive{..} : cmds -> do
remainingCmds <- withArchive zipPath (evalCmdZipArchive cmds)
evalCmdIO remainingCmds
RunZipCommandsFromFile{..} : cmds -> do
cmdList <- readCommandsFromFile commandsPath
evalCmdIO $ cmdList ++ cmds
c : cmds -> do
putStrLn $ "ignore: " ++ show c
evalCmdIO cmds
[] -> pure ()
readCommandsFromFile :: FilePath -> IO [ZipCommand]
readCommandsFromFile fname = do
args <- ShellWords.parse <$> readFile fname >>= \case
Right a -> pure a
Left err -> fail err
handleParseResult $ execParserPure defaultPrefs opts args
compressionMethods :: String
compressionMethods = intercalate "|" $ map show ([minBound..maxBound] :: [CompressionMethod])
zipCommand :: Parser ZipCommand
zipCommand = hsubparser $ mconcat
[ command "CreateArchive"
(info
(CreateArchive
<$> strOption (long "zipPath" <> metavar "FILENAME" <> help "Location of the archive file to create")
)
(progDesc "Create a new archive given its location. This will silently overwrite the specified file if it already exists.")
)
, command "WithArchive"
(info
(WithArchive
<$> strOption (long "zipPath" <> metavar "FILENAME" <> help "Location of the archive to work with")
)
(progDesc "Work with an existing archive.")
)
, command "DoesEntryExist"
(info
(DoesEntryExist
<$> strOption (long "entryPath" <> metavar "FILENAME" <> help "Selector that identifies archive entry")
)
(progDesc "Check whether the specified entry exists in the archive.")
)
, command "GetEntry"
(info
(GetEntry
<$> strOption (long "entryPath" <> metavar "FILENAME" <> help "Selector that identifies archive entry")
)
(progDesc "Get contents of a specific archive entry as a strict ByteString. It's not recommended to use this on big entries, because it will suck out a lot of memory.")
)
, command "SaveEntry"
(info
(SaveEntry
<$> strOption (long "entryPath" <> metavar "FILENAME" <> help "Selector that identifies archive entry")
<*> strOption (long "targetPath" <> metavar "FILENAME" <> help "Where to save the file")
)
(progDesc "Save a specific archive entry as a file in the file system.")
)
, command "CheckEntry"
(info
(CheckEntry
<$> strOption (long "entryPath" <> metavar "FILENAME" <> help "Selector that identifies archive entry")
)
(progDesc "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.")
)
, command "UnpackInto"
(info
(UnpackInto
<$> strOption (long "targetPath" <> metavar "DIRNAME" <> help "Where to unpack the archive")
)
(progDesc "Unpack the archive into the specified directory. The directory will be created if it does not exist.")
)
, command "GetArchiveComment"
(info
(pure GetArchiveComment)
(progDesc "Get the archive comment.")
)
, command "GetArchiveDescription"
(info
(pure GetArchiveDescription)
(progDesc "Get the archive description record.")
)
, command "AddEntry"
(info
(AddEntry
<$> option auto (long "compressionMethod" <> metavar compressionMethods <> help "The compression method to use")
<*> strOption (long "entryContent" <> metavar "BYTESTRING" <> help "Entry contents")
<*> strOption (long "entryPath" <> metavar "FILENAME" <> help "Name of the entry to add")
)
(progDesc "Add a new entry to the archive given its contents in binary form.")
)
, command "LoadEntry"
(info
(LoadEntry
<$> option auto (long "compressionMethod" <> metavar compressionMethods <> help "The compression method to use")
<*> strOption (long "entryPath" <> metavar "FILENAME" <> help "Name of the entry to add")
<*> strOption (long "sourcePath" <> metavar "FILENAME" <> help "Path to the file to add")
)
(progDesc "Load an entry from a given file.")
)
, command "CopyEntry"
(info
(CopyEntry
<$> strOption (long "zipPath" <> metavar "FILENAME" <> help "Path to the archive to copy from")
<*> strOption (long "sourcePath" <> metavar "FILENAME" <> help "Name of the entry (in the source archive) to copy")
<*> strOption (long "targetPath" <> metavar "FILENAME" <> help "Name of the entry to insert (in current archive)")
)
(progDesc "Copy an entry “as is” from another zip archive.")
)
, command "PackDirRecur"
(info
(PackDirRecur
<$> option auto (long "compressionMethod" <> metavar compressionMethods <> help "The compression method to use")
<*> strOption (long "sourcePath" <> metavar "FILENAME" <> help "Path to the directory to add")
<*> optional (strOption (long "entryPathPrefix" <> metavar "DIRNAME" <> help "Path prefix of the entries in the archive"))
)
(progDesc "Add an directory to the archive. Please note that due to the design of the library, empty sub-directories will not be added.")
)
, command "RenameEntry"
(info
(RenameEntry
<$> strOption (long "sourcePath" <> metavar "FILENAME" <> help "The original entry name")
<*> strOption (long "targetPath" <> metavar "FILENAME" <> help "The new entry name")
)
(progDesc "Rename an entry in the archive. If the entry does not exist, nothing will happen.")
)
, command "DeleteEntry"
(info
(DeleteEntry
<$> strOption (long "entryPath" <> metavar "FILENAME" <> help "Selector that identifies archive entry")
)
(progDesc "Delete an entry from the archive, if it does not exist, nothing will happen.")
)
, command "Recompress"
(info
(Recompress
<$> option auto (long "compressionMethod" <> metavar compressionMethods <> help "The new compression method")
<*> strOption (long "entryPath" <> metavar "FILENAME" <> help "Name of the entry to re-compress")
)
(progDesc "Change compression method of an entry, if it does not exist, nothing will happen.")
)
, command "SetEntryComment"
(info
(SetEntryComment
<$> strOption (long "comment" <> metavar "TEXT" <> help "Text of the comment")
<*> strOption (long "entryPath" <> metavar "FILENAME" <> help "Name of the entry to comment on")
)
(progDesc "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.")
)
, command "DeleteEntryComment"
(info
(DeleteEntryComment
<$> strOption (long "entryPath" <> metavar "FILENAME" <> help "Selector that identifies archive entry")
)
(progDesc "Delete an entry's comment, if that entry does not exist, nothing will happen.")
)
, command "SetModTime"
(info
(SetModTime
<$> option auto (long "time" <> metavar "UTCTime" <> help "New modification time")
<*> strOption (long "entryPath" <> metavar "FILENAME" <> help "Name of the entry to modify")
)
(progDesc "Set the last modification date/time. The specified entry may be missing, in that case the action has no effect.")
)
, command "AddExtraField"
(info
(AddExtraField
<$> option auto (long "tag" <> metavar "WORD16" <> help "Tag (header id) of the extra field to add")
<*> strOption (long "body" <> metavar "BYTESTRING" <> help "Body of the field")
<*> strOption (long "entryPath" <> metavar "FILENAME" <> help "Name of the entry to modify")
)
(progDesc "Add an extra field. The specified entry may be missing, in that case this action has no effect.")
)
, command "DeleteExtraField"
(info
(DeleteExtraField
<$> option auto (long "tag" <> metavar "WORD16" <> help "Tag (header id) of the extra field to delete")
<*> strOption (long "entryPath" <> metavar "FILENAME" <> help "Name of the entry to modify")
)
(progDesc "Delete an extra field by its type (tag). The specified entry may be missing, in that case this action has no effect.")
)
, command "SetExternalFileAttrs"
(info
(SetExternalFileAttrs
<$> option auto (long "attributes" <> metavar "WORD32" <> help "External file attributes")
<*> strOption (long "entryPath" <> metavar "FILENAME" <> help "Name of the entry to modify")
)
(progDesc "Set external file attributes. This function can be used to set file permissions.")
)
, command "SetArchiveComment"
(info
(SetArchiveComment
<$> strOption (long "comment" <> metavar "TEXT" <> help "Text of the comment")
)
(progDesc "Set the comment of the entire archive.")
)
, command "DeleteArchiveComment"
(info
(pure DeleteArchiveComment)
(progDesc "Delete the archive's comment if it's present.")
)
, command "RunZipCommandsFromFile"
(info
(RunZipCommandsFromFile
<$> strOption (long "commandsPath" <> metavar "FILENAME" <> help "Location of the zip commands file to execute")
)
(progDesc "Executes zip commands from file.")
)
]
{-
TODO:
- support regex patterns for filtering (i.e. unpack only the selected content)
-}
version :: Parser (a -> a)
version = option (readerAbort versionMsg) $
mconcat
[ help "Show program version."
, long "version"
, short 'v'
, noArgError versionMsg
, value id
, metavar ""
]
where
versionMsg = InfoMsg . showVersion $ ZipCmd.version
opts :: ParserInfo [ZipCommand]
opts = info (many zipCommand <**> helper <**> version) fullDesc
main :: IO ()
main = do
cmds <- execParser opts
evalCmdIO cmds