packages feed

qnap-decrypt-0.3.0: app/Main.hs

{-# LANGUAGE RecordWildCards #-}

module Main
  ( main
  ) where

import           Control.Exception   (displayException)
import           Control.Monad       (void, when)
import           Data.Semigroup      ((<>))
import qualified Options.Applicative as O
import           System.Directory    (createDirectoryIfMissing, doesDirectoryExist, doesFileExist, listDirectory,
                                      renameFile)
import           System.FilePath     (takeDirectory, takeFileName, (</>))
import           System.IO           (hClose, hPutStrLn, openBinaryTempFile, stderr)

import           Crypto.QNAP         (decrypt)

data Command
  = Command
    { commandSingleFile  :: Bool
    , commandInline      :: Bool
    , commandPassword    :: String
    , commandSource      :: String
    , commandDestination :: String
    , commandVerbose     :: Bool
    }
  deriving (Show)

command :: O.Parser Command
command =
  O.hsubparser $
    O.command
      "dir"
      ( O.info
          parseDirectory
          (O.progDesc "decrypt a directory")
      ) <>
    O.command
      "dir-in-place"
        ( O.info
            parseDirectoryInPlace
            (O.progDesc "decrypt a directory in place")
        ) <>
    O.command
      "file"
        ( O.info
            parseFile
            (O.progDesc "decrypt a file")
        ) <>
    O.command
      "file-in-place"
        ( O.info
            parseFileInPlace
            (O.progDesc "decrypt a file in place")
        )
  where
    parseDirectory =
      Command False False
        <$> O.strOption
          (  O.long "password"
          <> O.short 'p'
          <> O.metavar "PASS"
          <> O.help "Password"
          )
        <*> O.strOption
          (  O.long "source"
          <> O.short 's'
          <> O.metavar "SRC"
          <> O.help "Directory to be decrypted"
          )
        <*> O.strOption
          (  O.long "destination"
          <> O.short 'd'
          <> O.metavar "DST"
          <> O.help "Directory to store the decrypted files"
          )
        <*> O.switch
          (  O.long "verbose"
          <> O.short 'v'
          <> O.help "Print all decrypted files"
          )
    parseDirectoryInPlace =
      Command False True
        <$> O.strOption
          (  O.long "password"
          <> O.short 'p'
          <> O.metavar "PASS"
          <> O.help "Password"
          )
        <*> O.strOption
          (  O.long "directory"
          <> O.short 'd'
          <> O.metavar "DIR"
          <> O.help "Directory to be decrypted in place"
          )
        <*> pure ""
        <*> O.switch
          (  O.long "verbose"
          <> O.short 'v'
          <> O.help "Print all decrypted files"
          )
    parseFile =
      Command True False
        <$> O.strOption
          (  O.long "password"
          <> O.short 'p'
          <> O.metavar "PASS"
          <> O.help "Password"
          )
        <*> O.strOption
          (  O.long "source"
          <> O.short 's'
          <> O.metavar "SRC"
          <> O.help "File to be decrypted"
          )
        <*> O.strOption
          (  O.long "destination"
          <> O.short 'd'
          <> O.metavar "DST"
          <> O.help "File/directory to store the decrypted file"
          )
        <*> O.switch
          (  O.long "verbose"
          <> O.short 'v'
          <> O.help "Print all decrypted files"
          )
    parseFileInPlace =
      Command True True
        <$> O.strOption
          (  O.long "password"
          <> O.short 'p'
          <> O.metavar "PASS"
          <> O.help "Password"
          )
        <*> O.strOption
          (  O.long "file"
          <> O.short 'f'
          <> O.metavar "FILE"
          <> O.help "File to be decrypted in place"
          )
        <*> pure ""
        <*> O.switch
          (  O.long "verbose"
          <> O.short 'v'
          <> O.help "Print all decrypted files"
          )

main :: IO ()
main = do
  Command{..} <-
    O.execParser $
      O.info (command O.<**> O.helper)
        (  O.fullDesc
        <> O.progDesc "Decrypt QNAP files/folders"
        <> O.footer "WARNING: The target file/directory will be overwritten"
        )
  case (commandSingleFile, commandInline) of
    (False, False) -> cmdDir commandVerbose commandPassword [(commandSource, commandDestination)]
    (False, True)  -> cmdDirInPlace commandVerbose commandPassword [commandSource]
    (True, False)  -> cmdFile commandVerbose commandPassword commandSource commandDestination
    (True, True)   -> cmdFileInPlace commandVerbose commandPassword commandSource

cmdDir :: Bool -> String -> [(FilePath, FilePath)] -> IO ()
cmdDir verbose pass = go
  where
    go [] = return ()
    go ((src, dst):stack) = do
      isFile <- doesFileExist src
      isDir <- doesDirectoryExist src
      case (isFile, isDir) of
        (True, False) -> do
          cmdFile verbose pass src dst
          go stack
        (False, True) -> do
          files <- listDirectory src
          createDirectoryIfMissing True dst
          go (map (\file -> (src </> file, dst </> file)) files ++ stack)
        _ ->
          return ()

cmdDirInPlace :: Bool -> String -> [FilePath] -> IO ()
cmdDirInPlace verbose pass = go
  where
    go [] = return ()
    go (entry:stack) = do
      isFile <- doesFileExist entry
      isDir <- doesDirectoryExist entry
      case (isFile, isDir) of
        (True, False) -> do
          cmdFileInPlace verbose pass entry
          go stack
        (False, True) -> do
          files <- listDirectory entry
          go (map (entry </>) files ++ stack)
        _ ->
          return ()

cmdFile :: Bool -> String -> FilePath -> FilePath -> IO ()
cmdFile verbose pass src dst = do
  dstIsDir <- doesDirectoryExist dst
  let
    dst' =
      if dstIsDir
        then dst </> takeFileName src
        else dst
  void $ decrypt' verbose pass src dst'

cmdFileInPlace :: Bool -> String -> FilePath -> IO ()
cmdFileInPlace verbose pass file = do
  (tempFileName, tmpHandle) <- openBinaryTempFile (takeDirectory file) (takeFileName file ++ ".temp.qnap")
  hClose tmpHandle
  success <- decrypt' verbose pass file tempFileName
  -- the decrypt removes the output file in case of an error -> no need to do it here also
  when success $
    renameFile tempFileName file

decrypt' :: Bool -> String -> FilePath -> FilePath -> IO Bool
decrypt' verbose pass src dst = do
  result <- decrypt pass src dst
  case result of
    Just err -> do
      hPutStrLn stderr (src ++ ": " ++ displayException err)
      return False
    Nothing -> do
      when verbose $
        hPutStrLn stderr (src ++ ": OK")
      return True