{-# LANGUAGE OverloadedStrings #-}
module Control.Shell.Extract
(
extract, extractWith, supportedExtensions, canExtract
, ExtractOptions, separateDirectory, removeArchive
, defaultExtractOptions
) where
import Control.Shell
import qualified Data.ByteString.Char8 as BS
import qualified Data.Text as T
import Network.Mime
import System.IO.Unsafe
data ExtractOptions = ExtractOptions
{
separateDirectory :: Bool
, removeArchive :: Bool
}
defaultExtractOptions :: ExtractOptions
defaultExtractOptions = ExtractOptions
{ separateDirectory = True
, removeArchive = False
}
supportedExtensions :: [String]
supportedExtensions = unsafePerformIO $ do
env <- shell_ $ getShellEnv
res <- runSh (env {envStdErr = envStdOut env}) $ do
tar <- (capture (run "tar" ["-?"]) >> pure tarExts) `orElse` pure []
z7 <- (capture (run "7z" []) >> pure [".7z"]) `orElse` pure []
rar <- (capture (run "unrar" []) >> pure [".rar"]) `orElse` pure []
z <- (capture (run "unzip" []) >> pure [".zip"]) `orElse` pure []
xz <- (capture (run "xz" ["--help"]) >> pure [".xz"]) `orElse` pure []
bz2 <- (capture (run "bunzip2" ["--help"]) >> pure [".bz2"]) `orElse` pure []
gz <- (capture (run "gunzip" ["--help"]) >> pure [".gz"]) `orElse` pure []
pure $ concat [tar, z7, rar, z, xz, bz2, gz]
case res of
Left _ -> pure []
Right xs -> pure xs
where
tarExts = [".tar.gz", ".tar.bz2", ".tar.xz", ".tbz2", ".tgz", ".tar"]
canExtract :: FilePath -> Bool
canExtract f =
any (and . zipWith (==) f') (map reverse supportedExtensions)
where
f' = reverse f
extract :: FilePath -> Shell ()
extract = extractWith defaultExtractOptions
extractWith :: ExtractOptions -> FilePath -> Shell ()
extractWith opts file = do
archivedir <- pwd
let archive = archivedir </> file
mkdir True outputDir
case extractCmd archive of
Just (cmd, args)
| canExtract file -> inDirectory outputDir $ do
void . capture $ run cmd (args ++ [archive])
moveOutputToWorkDir archive
when (separateDirectory opts) $ void $ try mergeOneLevelRoot
when (removeArchive opts) $ rm archive
| otherwise ->
supportFail cmd
Nothing ->
mimeFail
where
outputDir
| separateDirectory opts = takeBasestName file
| otherwise = "."
mimeFail = fail $ concat
[ "mime type does not seem to be an archive: "
, BS.unpack $ defaultMimeLookup (T.pack file)
]
supportFail cmd = fail $ concat
[ "unable to unpack archive, as the program `" ++ cmd ++ "' "
, "was not found"
]
mergeOneLevelRoot :: Shell ()
mergeOneLevelRoot = do
[dir] <- ls "."
guard $ isDirectory dir
inCustomTempDirectory "." $ do
mv (".." </> dir) ("." </> dir)
files <- ls dir
mapM_ (\f -> mv (dir </> f) (".." </> f)) files
extractCmd :: FilePath -> Maybe (FilePath, [String])
extractCmd f =
case defaultMimeLookup (T.pack f) of
"application/x-7z-compressed" -> Just ("7z", ["x"])
"application/zip" -> Just ("unzip", ["-o"])
"application/x-rar-compressed" -> Just ("unrar", ["x"])
"application/x-tar" -> Just ("tar", ["-xf"])
"application/x-tgz" -> Just ("tar", ["-xzf"])
"application/x-bzip-compressed-tar" -> Just ("tar", ["-xjf"])
"application/x-bzip" -> Just ("bunzip2", ["-k"])
"application/x-gzip" -> Just ("gunzip", ["-k"])
"application/x-xz"
| "application/x-tar" <- defaultMimeLookup (T.pack $ dropExtension f)
-> Just ("tar", ["-xJf"])
| otherwise -> Just ("xz", ["-dk"])
_ -> Nothing
moveOutputToWorkDir :: FilePath -> Shell ()
moveOutputToWorkDir f = when needsWorkaround $ mv f' (takeFileName f')
where
f' = dropExtension f
needsWorkaround =
case defaultMimeLookup (T.pack f) of
"application/x-bzip" -> True
"application/x-gzip" -> True
"application/x-xz" ->
"application/x-tar" /= defaultMimeLookup (T.pack f')
_ -> False
takeBasestName :: FilePath -> FilePath
takeBasestName f
| f == f' = f
| unknownExt f = f
| otherwise = takeBasestName f'
where
f' = takeBaseName f
unknownExt f = defaultMimeLookup (T.pack f) == "application/octet-stream"