rofi-extras/app/rofi-dev.hs

890 lines
30 KiB
Haskell
Raw Normal View History

2021-03-18 00:41:04 -04:00
{-# LANGUAGE MultiParamTypeClasses #-}
2021-03-19 23:23:45 -04:00
{-# LANGUAGE ScopedTypeVariables #-}
2020-04-23 23:32:29 -04:00
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
2021-02-14 19:33:10 -05:00
{-# LANGUAGE ViewPatterns #-}
2020-04-23 23:32:29 -04:00
--------------------------------------------------------------------------------
-- | rofi-dev - a rofi prompt for mountable devices
--
-- Like all "mount helpers" this is basically a wrapper for low-level utilities
-- the mount things from the command line. It also creates/destroys mountpoint
-- paths given a specific location for such mountpoints.
2020-04-23 23:32:29 -04:00
module Main (main) where
import Bitwarden.Internal
2020-04-23 23:32:29 -04:00
import Control.Monad
2020-05-01 21:29:54 -04:00
import Control.Monad.Reader
2020-04-23 23:32:29 -04:00
import Data.Either
import Data.List
2020-05-01 21:29:54 -04:00
import Data.List.Split (splitOn)
import qualified Data.Map as M
import qualified Data.Map.Ordered as O
2020-04-23 23:32:29 -04:00
import Data.Maybe
2021-03-19 23:23:45 -04:00
import qualified Data.Text as T
2021-03-18 00:41:04 -04:00
import qualified Data.Vector as V
import Data.Yaml
import GHC.Generics()
2020-04-23 23:32:29 -04:00
import Rofi.Command
import Text.Printf
2020-05-01 21:29:54 -04:00
import System.Console.GetOpt
2020-04-23 23:32:29 -04:00
import System.Directory
import System.Environment
2021-02-14 19:33:10 -05:00
import System.Exit (ExitCode(..))
2020-05-01 21:29:54 -04:00
import System.FilePath.Posix
import System.Posix.User (getEffectiveUserName)
2020-04-23 23:32:29 -04:00
import System.Process
2020-05-01 21:29:54 -04:00
import UnliftIO.Exception
2020-04-23 23:32:29 -04:00
main :: IO ()
main = getArgs >>= parse
2020-05-01 21:29:54 -04:00
parse :: [String] -> IO ()
parse args = case getOpt Permute options args of
2021-03-19 23:23:45 -04:00
-- (o, n, []) -> initMountConf n >>= \i -> runMounts $ foldl (flip id) i o
(o, n, []) -> runMounts $ foldl (flip id) (defaultOpts n) o
(_, _, errs) -> ioError $ userError $ concat errs ++ usageInfo h options
2020-05-01 21:29:54 -04:00
where
h = "Usage: rofi-dev [OPTIONS] [-- ROFI-OPTIONS]"
2021-03-19 23:23:45 -04:00
defaultOpts r = Opts
{ optsConfig = Nothing
, optsAlias = Nothing
, optsUnmount = False
, optsRofiArgs = r
}
2020-05-01 21:29:54 -04:00
2021-03-19 23:23:45 -04:00
options :: [OptDescr (Opts -> Opts)]
2020-05-01 21:29:54 -04:00
options =
2021-03-18 00:41:04 -04:00
[ Option ['c'] ["config"]
2021-03-19 23:23:45 -04:00
(ReqArg (\s m -> m { optsConfig = Just s } ) "CONF")
2021-03-18 00:41:04 -04:00
"The path to the config file"
2021-03-19 23:23:45 -04:00
, Option ['m'] ["mount"]
(ReqArg (\s m -> m { optsAlias = Just s } ) "ALIAS")
"Mount the device specified by ALIAS directly"
, Option ['u'] ["unmount"] (NoArg (\m -> m { optsUnmount = True } ))
"Unmount the device specified by ALIAS instead of mounting it."
2020-05-01 21:29:54 -04:00
]
2021-03-19 23:23:45 -04:00
data Opts = Opts
{ optsConfig :: Maybe FilePath
, optsAlias :: Maybe String
, optsUnmount :: Bool
, optsRofiArgs :: [String]
} deriving Show
--------------------------------------------------------------------------------
-- | Static configuration
--
2021-03-18 00:41:04 -04:00
-- This is defined in a YAML file which describes how to mount each device. Here
-- I define a parser for said YAML file
defaultTries :: Integer
defaultTries = 2
2021-03-19 23:23:45 -04:00
(.:&) :: FromJSON a => Object -> T.Text -> Parser (V.Vector a)
(.:&) o t = o .:? t .!= V.empty
2021-03-18 00:41:04 -04:00
data MountConfig = MountConfig
{ _mountMountPoint :: FilePath
, _mountLabel :: Maybe String
} deriving Show
instance FromJSON MountConfig where
parseJSON = withObject "devices" $ \o -> MountConfig
<$> o .: "mountpoint"
<*> o .:? "label"
2021-03-19 23:23:45 -04:00
data DependsConfig = DependsConfig
{ _dependsVeracrypt :: V.Vector String
, _dependsSSHFS :: V.Vector String
, _dependsCIFS :: V.Vector String
} deriving Show
instance FromJSON DependsConfig where
parseJSON = withObject "depends" $ \o -> DependsConfig
<$> o .:& "veracrypt"
<*> o .:& "sshfs"
<*> o .:& "cifs"
2021-03-18 00:41:04 -04:00
data BitwardenConfig = BitwardenConfig
{ _bitwardenKey :: String
, _bitwardenTries :: Integer }
deriving Show
instance FromJSON BitwardenConfig where
parseJSON = withObject "bitwarden" $ \o -> BitwardenConfig
<$> o .: "key"
<*> o .:? "tries" .!= defaultTries
newtype LibSecretConfig = LibSecretConfig
{ _libsecretAttributes :: M.Map String String }
deriving Show
instance FromJSON LibSecretConfig where
parseJSON = withObject "libsecret" $ \o -> LibSecretConfig
<$> o .: "attributes"
newtype PromptConfig = PromptConfig
{ _promptTries :: Integer }
deriving Show
instance FromJSON PromptConfig where
parseJSON = withObject "libsecret" $ \o -> PromptConfig
<$> o .: "tries" .!= defaultTries
data PasswordConfig = PasswordConfig
{ _passwordBitwarden :: Maybe BitwardenConfig
, _passwordLibSecret :: Maybe LibSecretConfig
, _passwordPrompt :: Maybe PromptConfig
}
deriving Show
instance FromJSON PasswordConfig where
parseJSON = withObject "password" $ \o -> PasswordConfig
<$> o .:? "bitwarden"
<*> o .:? "libsecret"
<*> o .:? "prompt"
data VeracryptConfig = VeracryptConfig
{ _veracryptMount :: MountConfig
, _veracryptVolume :: String
2021-03-19 23:23:45 -04:00
, _veracryptDepends :: Maybe DependsConfig
2021-03-18 00:41:04 -04:00
, _veracryptPassword :: Maybe PasswordConfig
} deriving Show
instance FromJSON VeracryptConfig where
parseJSON = withObject "veracrypt" $ \o -> VeracryptConfig
<$> o .: "mount"
<*> o .: "volume"
2021-03-19 23:23:45 -04:00
<*> o .:? "depends"
2021-03-18 00:41:04 -04:00
<*> o .:? "password"
data SSHFSConfig = SSHFSConfig
{ _sshfsMount :: MountConfig
, _sshfsRemote :: String
2021-03-19 23:23:45 -04:00
, _sshfsDepends :: Maybe DependsConfig
2021-03-18 00:41:04 -04:00
} deriving Show
instance FromJSON SSHFSConfig where
parseJSON = withObject "sshfs" $ \o -> SSHFSConfig
<$> o .: "mount"
<*> o .: "remote"
2021-03-19 23:23:45 -04:00
<*> o .:? "depends"
2021-03-18 00:41:04 -04:00
data CIFSConfig = CIFSConfig
{ _cifsMount :: MountConfig
, _cifsRemote :: String
2021-03-19 23:23:45 -04:00
, _cifsDepends :: Maybe DependsConfig
2021-03-18 00:41:04 -04:00
, _cifsPassword :: Maybe PasswordConfig
} deriving Show
instance FromJSON CIFSConfig where
parseJSON = withObject "cifs" $ \o -> CIFSConfig
<$> o .: "mount"
<*> o .: "remote"
2021-03-19 23:23:45 -04:00
<*> o .:? "depends"
<*> o .:? "password"
-- data DeviceConfig = VeracryptConfig
-- { _veracryptMount :: MountConfig
-- , _veracryptVolume :: String
-- , _veracryptDepends :: Maybe DependsConfig
-- , _veracryptPassword :: Maybe PasswordConfig
-- } | SSHFSConfig
-- { _sshfsMount :: MountConfig
-- , _sshfsRemote :: String
-- , _sshfsDepends :: Maybe DependsConfig
-- } | CIFSConfig
-- { _cifsMount :: MountConfig
-- , _cifsRemote :: String
-- , _cifsDepends :: Maybe DependsConfig
-- , _cifsPassword :: Maybe PasswordConfig
-- } deriving Show
-- instance FromJSON DeviceConfig where
-- parseJSON = withObject "devices" $ \o -> do
-- devType <- o .: "type"
-- case devType of
-- "cifs" -> CIFSConfig
-- <$> o .: "mount"
-- <*> o .: "remote"
-- <*> o .:? "depends"
-- <*> o .:? "password"
-- "sshfs" -> SSHFSConfig
-- <$> o .: "mount"
-- <*> o .: "remote"
-- <*> o .:? "depends"
-- "veracrypt" -> VeracryptConfig
-- <$> o .: "mount"
-- <*> o .: "volume"
-- <*> o .:? "depends"
-- <*> o .:? "password"
-- _ -> fail "unknown device type"
2021-03-18 00:41:04 -04:00
data DevicesConfig = DevicesConfig
2021-03-19 23:23:45 -04:00
{ _veracryptConfigs :: M.Map String VeracryptConfig
, _sshfsConfigs :: M.Map String SSHFSConfig
, _cifsConfigs :: M.Map String CIFSConfig
2021-03-18 00:41:04 -04:00
} deriving Show
instance FromJSON DevicesConfig where
parseJSON = withObject "devices" $ \o -> DevicesConfig
<$> o .: "veracrypt"
<*> o .: "sshfs"
<*> o .: "cifs"
data StaticConfig = StaticConfig
2021-03-19 23:23:45 -04:00
{ _staticconfigTmpPath :: Maybe String
, _staticconfigDevices :: Maybe DevicesConfig
-- , _staticconfigDevices :: M.Map String DeviceConfig
2021-03-18 00:41:04 -04:00
} deriving Show
instance FromJSON StaticConfig where
parseJSON = withObject "devices" $ \o -> StaticConfig
<$> o .:? "mountdir"
<*> o .:? "devices"
--------------------------------------------------------------------------------
-- | Static Devices typeclass
--
-- A class to represent devices defined in the static configuration (eg the YAML
-- file). Its methods define the machinery to extract specific devies types
-- from the parse tree.
class Mountable m => StaticDevice m a where
2021-03-19 23:23:45 -04:00
fromConfig :: DevicesConfig -> (DevicesConfig -> M.Map String a) -> RofiIO MountConf [m]
fromConfig s f = fromConfig' s (M.elems . f)
2021-03-18 00:41:04 -04:00
2021-03-19 23:23:45 -04:00
fromConfig' :: DevicesConfig -> (DevicesConfig -> [a]) -> RofiIO MountConf [m]
fromConfig' s f = do
v <- asks mountconfVolatilePath
mapM (configToDev v s) $ f s
configToDev :: FilePath -> DevicesConfig -> a -> RofiIO MountConf m
2021-03-18 00:41:04 -04:00
--------------------------------------------------------------------------------
-- | Global config used in the reader monad stack
--
-- This is defined by the mount options on the command line, and holds:
-- - a map between mountpoints and a means to get passwords when mounting those
-- mountpoints
-- - a mount directory where mountpoints will be created if needed (defaults
-- to '/tmp/media/USER'
-- - any arguments to be passed to the rofi command
data MountConf = MountConf
2021-03-19 23:23:45 -04:00
{ mountconfVolatilePath :: FilePath
, mountconfRofiArgs :: [String]
, mountconfStaticDevs :: Maybe DevicesConfig
}
instance RofiConf MountConf where
2021-03-19 23:23:45 -04:00
defArgs MountConf { mountconfRofiArgs = a } = a
2020-05-01 21:29:54 -04:00
--------------------------------------------------------------------------------
-- | Password-getting functions
2021-03-18 00:41:04 -04:00
type PasswordGetter = IO (Maybe String)
2020-05-01 21:29:54 -04:00
runSecret :: [(String, String)] -> PasswordGetter
runSecret kvs = readCmdSuccess "secret-tool" ("lookup":kvs') ""
2020-05-01 21:29:54 -04:00
where
kvs' = concatMap (\(k, v) -> [k, v]) kvs
runBitwarden :: String -> PasswordGetter
runBitwarden pname = ((password . login) <=< find (\i -> name i == pname))
<$> getItems
2021-03-18 00:41:04 -04:00
runPromptLoop :: Integer -> PasswordGetter -> PasswordGetter
runPromptLoop n pwd = do
res <- pwd
if isNothing res then
if n <= 0 then return Nothing else runPromptLoop (n-1) pwd
else return res
configToPwd :: PasswordConfig -> PasswordGetter
configToPwd PasswordConfig{ _passwordBitwarden = b
, _passwordLibSecret = s
, _passwordPrompt = p
} =
getBW b `runMaybe` getLS s `runMaybe` getPrompt p
where
getBW (Just BitwardenConfig{ _bitwardenKey = k, _bitwardenTries = n }) =
runPromptLoop n $ runBitwarden k
getBW _ = return Nothing
getLS (Just LibSecretConfig{ _libsecretAttributes = a }) =
runSecret $ M.toList a
getLS _ = return Nothing
getPrompt (Just PromptConfig{ _promptTries = n }) =
runPromptLoop n readPassword
getPrompt _ = return Nothing
runMaybe x y = do
res <- x
if isNothing res then y else return res
--------------------------------------------------------------------------------
-- | Main prompt
--
-- This command will have one Rofi prompt and will display all available
-- mounts grouped by device type (eg removable, sshfs, cifs, etc). I like
-- pretty things, so ensure the entries are aligned properly as well
2020-04-23 23:32:29 -04:00
2021-03-19 23:23:45 -04:00
runMounts :: Opts -> IO ()
runMounts opts = do
static <- join <$> traverse parseStaticConfig (optsConfig opts)
defaultTmpPath <- ("/tmp/media" </>) <$> getEffectiveUserName
let tmpPath = fromMaybe defaultTmpPath (_staticconfigTmpPath =<< static)
let staticDevs = _staticconfigDevices =<< static
let mountconf = MountConf
{ mountconfVolatilePath = tmpPath
, mountconfRofiArgs = optsRofiArgs opts
, mountconfStaticDevs = staticDevs
}
let byAlias = mountByAlias $ optsUnmount opts
let byPrompt = runPrompt =<< getGroups
runRofiIO mountconf $ maybe byPrompt byAlias $ optsAlias opts
parseStaticConfig :: FilePath -> IO (Maybe StaticConfig)
parseStaticConfig p = do
res <- decodeFileEither p
case res of
Left e -> print e >> return Nothing
Right c -> return $ Just c
2020-05-01 21:29:54 -04:00
runPrompt :: RofiConf c => [RofiGroup c] -> RofiIO c ()
runPrompt gs = selectAction $ emptyMenu
{ groups = gs
, prompt = Just "Select Device"
}
getGroups :: RofiIO MountConf [RofiGroup MountConf]
getGroups = do
2021-02-14 19:33:10 -05:00
sysd <- io getSystemdDevices
2021-03-19 23:23:45 -04:00
(cifsDevs, sshfsDevs, vcDevs) <- getStaticDevices
2020-05-01 21:29:54 -04:00
sequence
2021-03-18 00:41:04 -04:00
[ mkGroup2 "SSHFS Devices" (filterSysd SystemdSSHFS sysd) sshfsDevs
, mkGroup "CIFS Devices" cifsDevs
, mkGroup2 "Veracrypt Devices" (filterSysd SystemdVeracrypt sysd) vcDevs
2021-02-14 19:33:10 -05:00
, mkGroup "Removable Devices" =<< getRemovableDevices
2020-05-01 21:29:54 -04:00
, mkGroup "MTP Devices" =<< getMTPDevices
]
2021-02-14 19:33:10 -05:00
where
filterSysd t = filter (\s -> sysdType s == t)
2020-05-01 21:29:54 -04:00
2021-03-19 23:23:45 -04:00
mountByAlias :: Bool -> String -> RofiIO MountConf ()
mountByAlias unmountFlag alias = do
static <- asks mountconfStaticDevs
forM_ static $ \static' -> do
volatilePath <- asks mountconfVolatilePath
c <- toDev volatilePath static' _cifsConfigs alias
s <- toDev volatilePath static' _sshfsConfigs alias
v <- toDev volatilePath static' _veracryptConfigs alias
mountIfJust (c :: Maybe CIFS)
$ mountIfJust (s :: Maybe SSHFS)
$ mountIfJust (v :: Maybe VeraCrypt) (return ())
where
toDev v s f = mapM (configToDev v s) . aliasToDevice s f
mountIfJust a b = if isNothing a then b else forM_ a $ flip mount unmountFlag
aliasToDevice :: DevicesConfig -> (DevicesConfig -> M.Map String a) -> String -> Maybe a
aliasToDevice d f = flip M.lookup (f d)
getStaticDevices :: RofiIO MountConf ([CIFS], [SSHFS], [VeraCrypt])
getStaticDevices = do
static <- asks mountconfStaticDevs
maybe (return ( [] :: [CIFS], [] :: [SSHFS], [] :: [VeraCrypt]))
(\c -> liftM3 (,,)
(fromConfig c _cifsConfigs)
(fromConfig c _sshfsConfigs)
(fromConfig c _veracryptConfigs))
static
2020-05-01 21:29:54 -04:00
mkGroup :: Mountable d => String -> [d] -> RofiIO MountConf (RofiGroup MountConf)
2021-02-14 19:33:10 -05:00
mkGroup header devs = sortGroup header <$> mapM mkAction devs
mkGroup2 :: (Mountable d, Mountable e) => String
-> [d] -> [e] -> RofiIO MountConf (RofiGroup MountConf)
mkGroup2 header devs1 devs2 = do
r1 <- mapM mkAction devs1
r2 <- mapM mkAction devs2
return $ sortGroup header (r1 ++ r2)
sortGroup :: String -> [(String, RofiIO MountConf ())] -> RofiGroup MountConf
sortGroup header = titledGroup header . alignEntries . toRofiActions
2020-04-23 23:32:29 -04:00
alignSep :: String
alignSep = " | "
alignSepPre :: String
alignSepPre = "@@@"
2020-05-01 21:29:54 -04:00
alignEntries :: RofiActions c -> RofiActions c
2020-04-23 23:32:29 -04:00
alignEntries = O.fromList . withKeys . O.assocs
where
withKeys as = let (ks, vs) = unzip as in zip (align ks) vs
align = fmap (intercalate alignSep)
. transpose
. mapToLast pad
. transpose
. fmap (splitOn alignSepPre)
2020-04-23 23:32:29 -04:00
pad xs = let m = getMax xs in fmap (\x -> take m (x ++ repeat ' ')) xs
getMax = maximum . fmap length
mapToLast _ [] = []
mapToLast _ [x] = [x]
mapToLast f (x:xs) = f x : mapToLast f xs
--------------------------------------------------------------------------------
-- | Removable devices
--
-- A device which can be removed (which is all the devices we care about)
2020-05-01 21:29:54 -04:00
-- This can be minimally described by a device DEVICESPEC and LABEL.
2020-04-23 23:32:29 -04:00
data Removable = Removable
2020-05-01 21:29:54 -04:00
{ deviceSpec :: String
2020-04-23 23:32:29 -04:00
, label :: String
}
deriving (Eq, Show)
instance Mountable Removable where
mount Removable { deviceSpec = d, label = l } m =
2020-05-02 00:13:33 -04:00
io $ runMountNotify "udisksctl" [c, "-b", d] l m
where
c = if m then "unmount" else "mount"
2020-05-01 21:29:54 -04:00
allInstalled _ = fmap isJust $ io $ findExecutable "udisksctl"
2020-04-23 23:32:29 -04:00
2020-05-01 21:29:54 -04:00
isMounted Removable { deviceSpec = d } = elem d <$> io curDeviceSpecs
2020-04-23 23:32:29 -04:00
2020-05-01 21:29:54 -04:00
fmtEntry Removable { deviceSpec = d, label = l } = l ++ alignSepPre ++ d
2020-04-23 23:32:29 -04:00
-- | Return list of possible rofi actions for removable devices
-- A 'removable device' is defined as a hotplugged device with a filesystem as
-- reported by 'lsblk'. If the LABEL does not exist on the filesystem, the
-- label shown on the prompt will be 'SIZE Volume' where size is the size of
-- the device
2020-05-01 21:29:54 -04:00
getRemovableDevices :: RofiConf c => RofiIO c [Removable]
getRemovableDevices = fromLines toDev . lines
2020-05-01 21:29:54 -04:00
<$> io (readProcess "lsblk" ["-n", "-r", "-o", columns] "")
2020-04-23 23:32:29 -04:00
where
columns = "FSTYPE,HOTPLUG,PATH,LABEL,SIZE"
-- can't use 'words' here since it will drop spaces in the front
toDev line = case splitBy ' ' line of
("":_) -> Nothing
2020-05-01 21:29:54 -04:00
[_, "1", d, "", s] -> mk d $ s ++ " Volume"
[_, "1", d, l, _] -> mk d l
2020-04-23 23:32:29 -04:00
_ -> Nothing
2020-05-01 21:29:54 -04:00
mk d l = Just $ Removable { deviceSpec = d, label = l }
2020-04-23 23:32:29 -04:00
--------------------------------------------------------------------------------
-- | CIFS Devices
--
-- This wraps the Removable device (since it is removable) and also adds its
-- own mount options and passwords for authentication.
2020-04-23 23:32:29 -04:00
2021-03-19 23:23:45 -04:00
data CIFS = CIFS Removable FilePath (Maybe PasswordGetter) Dependency
instance Show CIFS where
show (CIFS r f _ d) = unwords [show r, show f, "<Pwd>", show d]
2020-04-23 23:32:29 -04:00
instance Mountable CIFS where
2021-03-19 23:23:45 -04:00
mount (CIFS Removable{ label = l } m getPwd deps) False = do
mountDependencies deps
2020-05-01 21:29:54 -04:00
bracketOnError_
(mkDirMaybe m)
(rmDirMaybe m)
$ io $ do
res <- case getPwd of
Just pwd -> do
p <- maybe [] (\p -> [("PASSWD", p)]) <$> pwd
readCmdEither' "mount" [m] "" p
Nothing -> readCmdEither "mount" [m] ""
notifyMounted (isRight res) False l
2021-03-19 23:23:45 -04:00
mount (CIFS Removable{ label = l } m _ _) True = umountNotify l m
2020-05-01 21:29:54 -04:00
allInstalled _ = io $ isJust <$> findExecutable "mount.cifs"
2021-03-19 23:23:45 -04:00
isMounted (CIFS _ dir _ _) = io $ isDirMounted dir
2020-05-01 21:29:54 -04:00
2021-03-19 23:23:45 -04:00
fmtEntry (CIFS r _ _ _) = fmtEntry r
2020-05-01 21:29:54 -04:00
2021-03-18 00:41:04 -04:00
instance StaticDevice CIFS CIFSConfig where
2021-03-19 23:23:45 -04:00
configToDev v s CIFSConfig { _cifsMount = MountConfig { _mountMountPoint = m }
, _cifsRemote = t
, _cifsDepends = d
, _cifsPassword = p } = do
2021-03-18 00:41:04 -04:00
let r = Removable { deviceSpec = smartSlashPrefix t, label = takeFileName m }
2021-03-19 23:23:45 -04:00
d' <- maybe (return initDependencies) (getDependencies s) d
return $ CIFS r (appendRoot v m) (configToPwd <$> p) d'
2021-03-18 00:41:04 -04:00
where
smartSlashPrefix a = if "//" `isPrefixOf` a then a else "//" ++ a
2020-04-23 23:32:29 -04:00
--------------------------------------------------------------------------------
-- | SSHFS Devices
--
-- This wraps the Removable device (since it is removable) and also adds its
-- own mount options. If the path does not point to an aliased entry in the ssh
-- config that specifies the port, hostname, user, and identity file, these
-- need to be passed as mount options.
2021-03-19 23:23:45 -04:00
data SSHFS = SSHFS Removable FilePath Dependency deriving Show
2020-04-23 23:32:29 -04:00
instance Mountable SSHFS where
2021-03-19 23:23:45 -04:00
mount (SSHFS Removable{ deviceSpec = d, label = l } m deps) False = do
mountDependencies deps
2020-05-01 21:29:54 -04:00
bracketOnError_
(mkDirMaybe m)
(rmDirMaybe m)
2021-03-18 00:41:04 -04:00
(io $ runMountNotify "sshfs" [d, m] l False)
2020-04-23 23:32:29 -04:00
2021-03-19 23:23:45 -04:00
mount (SSHFS Removable{ label = l } m _) True = umountNotify l m
2020-04-23 23:32:29 -04:00
2020-05-01 21:29:54 -04:00
allInstalled _ = fmap isJust $ io $ findExecutable "sshfs"
2020-04-23 23:32:29 -04:00
2021-03-19 23:23:45 -04:00
isMounted (SSHFS _ dir _) = io $ isDirMounted dir
2020-04-23 23:32:29 -04:00
2021-03-19 23:23:45 -04:00
fmtEntry (SSHFS r _ _) = fmtEntry r
2020-04-23 23:32:29 -04:00
2021-03-18 00:41:04 -04:00
instance StaticDevice SSHFS SSHFSConfig where
2021-03-19 23:23:45 -04:00
configToDev v s SSHFSConfig { _sshfsMount = MountConfig { _mountMountPoint = m }
, _sshfsDepends = d
, _sshfsRemote = t } = do
2021-03-18 00:41:04 -04:00
let r = Removable { deviceSpec = t, label = takeFileName m }
2021-03-19 23:23:45 -04:00
d' <- maybe (return initDependencies) (getDependencies s) d
return $ SSHFS r (appendRoot v m) d'
2020-04-23 23:32:29 -04:00
2020-08-15 13:54:33 -04:00
--------------------------------------------------------------------------------
-- | VeraCrypt Devices
--
2021-03-19 23:23:45 -04:00
data VeraCrypt = VeraCrypt Removable FilePath (Maybe PasswordGetter) Dependency
instance Show VeraCrypt where
show (VeraCrypt r f _ d) = unwords [show r, show f, show d]
2020-08-15 13:54:33 -04:00
instance Mountable VeraCrypt where
2021-03-19 23:23:45 -04:00
mount (VeraCrypt Removable{ deviceSpec = s, label = l } m getPwd deps) False = do
mountDependencies deps
bracketOnError_ (mkDirMaybe m) (rmDirMaybe m) mountMaybe
where
mountMaybe = io $ maybe (runVeraCryptWith "" []) (runVeraCryptWithPwd =<<) getPwd
runVeraCryptWithPwd = maybe notifyFail (\p -> runVeraCryptWith p ["--stdin"])
runVeraCryptWith stdin args = (\res -> notifyMounted (isRight res) False l)
=<< runVeraCrypt stdin ([s, m] ++ args)
notifyFail = notify "dialog-error-symbolic" $
printf "Failed to get volume password for %s" l
2020-08-15 13:54:33 -04:00
2021-03-19 23:23:45 -04:00
mount (VeraCrypt Removable{ label = l } m _ _) True = io $ do
res <- runVeraCrypt "" ["-d", m]
2020-12-12 00:11:00 -05:00
notifyMounted (isRight res) True l
2020-08-15 13:54:33 -04:00
2020-12-12 00:11:00 -05:00
allInstalled _ = io $ isJust <$> findExecutable "veracrypt"
2020-08-15 13:54:33 -04:00
2021-03-19 23:23:45 -04:00
isMounted (VeraCrypt _ dir _ _) = io $ isDirMounted dir
2020-08-15 13:54:33 -04:00
2021-03-19 23:23:45 -04:00
fmtEntry (VeraCrypt r _ _ _) = fmtEntry r
2020-08-15 13:54:33 -04:00
-- NOTE: the user is assumed to have added themselves to the sudoers file so
-- that this command will work
runVeraCrypt :: String -> [String] -> IO (Either (Int, String, String) String)
runVeraCrypt stdin args = do
readCmdEither "sudo" (defaultArgs ++ args) stdin
2020-12-12 00:11:00 -05:00
where
defaultArgs = ["/usr/bin/veracrypt", "--text", "--non-interactive"]
2020-12-12 00:11:00 -05:00
2021-03-18 00:41:04 -04:00
instance StaticDevice VeraCrypt VeracryptConfig where
2021-03-19 23:23:45 -04:00
configToDev v s VeracryptConfig { _veracryptMount = MountConfig { _mountMountPoint = m }
, _veracryptVolume = t
, _veracryptDepends = d
, _veracryptPassword = p } = do
2021-03-18 00:41:04 -04:00
let r = Removable { deviceSpec = t, label = takeFileName m }
2021-03-19 23:23:45 -04:00
d' <- maybe (return initDependencies) (getDependencies s) d
return $ VeraCrypt r (appendRoot v m) (configToPwd <$> p) d'
--------------------------------------------------------------------------------
-- | Dependencies
--
-- Define a data structure that allows one device to depend on another. Since
-- each device is different and has a different typeclass instance, need to
-- include slots for all possible devices. For now only deal with static
-- devices.
data Dependency = Dependency
{ dependencySSHFS :: [SSHFS]
, dependencyCIFS :: [CIFS]
, dependencyVeracrypt :: [VeraCrypt]
} deriving Show
initDependencies :: Dependency
initDependencies = Dependency [] [] []
getDependencies :: DevicesConfig -> DependsConfig -> RofiIO MountConf Dependency
getDependencies devConf DependsConfig { _dependsCIFS = c
, _dependsSSHFS = s
, _dependsVeracrypt = v} = do
c' <- getDepConfigs c _cifsConfigs
s' <- getDepConfigs s _sshfsConfigs
v' <- getDepConfigs v _veracryptConfigs
return Dependency
{ dependencyCIFS = c'
, dependencySSHFS = s'
, dependencyVeracrypt = v'
}
where
getDepConfigs aliases getConfig = fromConfig' devConf
$ M.elems . M.filterWithKey (\k _ -> k `elem` V.toList aliases) . getConfig
mountDependencies :: Dependency -> RofiIO MountConf ()
mountDependencies Dependency { dependencyCIFS = c
, dependencySSHFS = s
, dependencyVeracrypt = v
} =
mountAll c >> mountAll s >> mountAll v
where
mountAll :: Mountable a => [a] -> RofiIO MountConf ()
mountAll = mapM_ (\d -> isMounted d >>= (\r -> unless r $ mount d False))
2020-08-15 13:54:33 -04:00
2020-04-23 23:32:29 -04:00
--------------------------------------------------------------------------------
-- | MTP devices
--
-- These devices are a bit special because they are not based on Removable
-- devices (eg they don't have a label and a device spec). Instead they
-- are defined by a bus:device path. The program used for this is jmtpfs
-- (which seems to be the fastest and most robust)
2020-04-23 23:32:29 -04:00
data MTPFS = MTPFS
2020-05-01 21:29:54 -04:00
{ bus :: String
, device :: String
, mountpoint :: FilePath
, description :: String
2020-04-23 23:32:29 -04:00
}
deriving (Eq, Show)
instance Mountable MTPFS where
mount MTPFS {..} False = do
-- TODO add autodismount to options
let dev = "-device=" ++ bus ++ "," ++ device
2020-05-01 21:29:54 -04:00
bracketOnError_
(mkDirMaybe mountpoint)
(rmDirMaybe mountpoint)
(io $ runMountNotify "jmtpfs" [dev, mountpoint] description False)
2020-04-23 23:32:29 -04:00
2020-05-01 21:29:54 -04:00
mount MTPFS { mountpoint = m, description = d } True = umountNotify d m
2020-04-23 23:32:29 -04:00
2020-05-01 21:29:54 -04:00
-- | return True always since the list won't even show without jmtpfs
allInstalled _ = return True
2020-04-23 23:32:29 -04:00
2020-05-01 21:29:54 -04:00
isMounted MTPFS { mountpoint = dir } = io $ isDirMounted dir
2020-04-23 23:32:29 -04:00
fmtEntry MTPFS { description = d } = d
-- | Return list of all available MTP devices
2020-05-01 21:29:54 -04:00
getMTPDevices :: RofiIO MountConf [MTPFS]
getMTPDevices = do
2021-03-19 23:23:45 -04:00
dir <- asks mountconfVolatilePath
2020-05-01 21:29:54 -04:00
res <- io $ readProcess "jmtpfs" ["-l"] ""
return $ fromLines (toDev dir) $ toDevList res
2020-04-23 23:32:29 -04:00
where
toDevList = reverse
. takeWhile (not . isPrefixOf "Available devices")
. reverse
. lines
2020-05-01 21:29:54 -04:00
toDev dir s = case splitOn ", " s of
2020-04-23 23:32:29 -04:00
[busNum, devNum, _, _, desc, vendor] -> let d = unwords [vendor, desc]
in Just $ MTPFS
{ bus = busNum
, device = devNum
2020-05-01 21:29:54 -04:00
, mountpoint = dir </> canonicalize d
2020-04-23 23:32:29 -04:00
, description = d
}
_ -> Nothing
canonicalize = mapMaybe repl
repl c
| c `elem` ("\"*/:<>?\\|" :: String) = Nothing
| c == ' ' = Just '-'
| otherwise = Just c
2021-02-14 19:33:10 -05:00
--------------------------------------------------------------------------------
-- | Systemd typeclass
data SystemdMountType = SystemdVeracrypt | SystemdSSHFS deriving (Eq, Show)
data Systemd = Systemd
{ sysdType :: SystemdMountType
, sysdInstance :: String
}
deriving (Eq, Show)
instance Mountable Systemd where
mount s@Systemd { sysdInstance = i } m = let
unit = fmtSysdInstanceName s
2021-03-18 00:41:04 -04:00
operation = if m then "stop" else "start" in
io $ runMountNotify "systemctl" ["--user", operation, unit] i m
2021-02-14 19:33:10 -05:00
allInstalled Systemd { sysdType = SystemdVeracrypt } =
io $ isJust <$> findExecutable "veracrypt"
allInstalled Systemd { sysdType = SystemdSSHFS } =
io $ isJust <$> findExecutable "sshfs"
isMounted s = let
unit = fmtSysdInstanceName s
args = ["--user", "is-active", "--quiet", unit] in
io $ (\(ec, _, _) -> ec == ExitSuccess)
<$> readProcessWithExitCode "systemctl" args ""
fmtEntry Systemd { sysdInstance = i } = i ++ alignSepPre ++ "Systemd"
fmtSysdInstanceName :: Systemd -> String
fmtSysdInstanceName Systemd { sysdType = SystemdVeracrypt, sysdInstance = i } =
"mount-veracrypt@" ++ i ++ ".service"
fmtSysdInstanceName Systemd { sysdType = SystemdSSHFS, sysdInstance = i } =
"mount-sshfs@" ++ i ++ ".service"
getSystemdDevices :: IO [Systemd]
getSystemdDevices = do
systemdHome <- io $ getXdgDirectory XdgConfig "systemd/user"
io $ mapMaybe toDev
<$> (filterM (doesDirectoryExist . (systemdHome </>))
=<< listDirectory systemdHome)
where
toDev (splitInstance "mount-veracrypt@" -> Just s) =
Just $ Systemd { sysdType = SystemdVeracrypt , sysdInstance = s }
toDev (splitInstance "mount-sshfs@" -> Just s) =
Just $ Systemd { sysdType = SystemdSSHFS , sysdInstance = s }
toDev _ = Nothing
splitInstance p = fmap (takeWhile (not . (==) '.')) . stripPrefix p
--------------------------------------------------------------------------------
-- | Mountable typeclass
--
-- Let this class represent anything that can be mounted. The end goal is to
-- create a Rofi action which will define an entry in the rofi prompt for the
-- device at hand. In order to make an action, we need functions to mount the
-- device, check if the necessary mounting program(s) is installed, make the
-- entry to go in the prompt, and test if the device is mounted.
class Mountable a where
-- | Mount the given type (or dismount if False is passed)
mount :: a -> Bool -> RofiIO MountConf ()
-- | Check if the mounting utilities are present
allInstalled :: a -> RofiIO MountConf Bool
-- | Return a string to go in the Rofi menu for the given type
fmtEntry :: a -> String
-- | Determine if the given type is mounted or not
isMounted :: a -> RofiIO MountConf Bool
-- | Given a mountable type, return a rofi action (string to go in the
-- Rofi prompt and an action to perform when it is selected)
mkAction :: a -> RofiIO MountConf (String, RofiIO MountConf ())
mkAction dev = do
m <- isMounted dev
i <- allInstalled dev
let a = when i $ mount dev m
let s = mountedPrefix m i ++ fmtEntry dev
return (s, a)
where
mountedPrefix False True = " "
mountedPrefix True True = "* "
mountedPrefix _ False = "! "
2020-04-23 23:32:29 -04:00
--------------------------------------------------------------------------------
-- | Low-level mount functions
-- ASSUME these will never fail because the format of /proc/mounts is fixed
2020-05-01 21:29:54 -04:00
curMountField :: Int -> IO [String]
curMountField i = fmap ((!! i) . words) . lines <$> readFile "/proc/mounts"
curDeviceSpecs :: IO [String]
curDeviceSpecs = curMountField 0
curMountpoints :: IO [String]
curMountpoints = curMountField 1
-- ASSUME the base mount path will always be created because
-- 'createDirectoryIfMissing' will make parents if missing, and that removing
-- all the directories will leave the base mount path intact regardless of if it
-- was present before doing anything (which matters here since I'm putting the
-- base path in /tmp, so all this is saying is that umounting everything will
-- leave /tmp/media/USER without removing all the way down to /tmp)
2020-05-01 21:29:54 -04:00
mkDirMaybe :: FilePath -> RofiIO MountConf ()
mkDirMaybe fp = whenInMountDir fp $ io $ createDirectoryIfMissing True fp
rmDirMaybe :: FilePath -> RofiIO MountConf ()
rmDirMaybe fp = whenInMountDir fp $ unlessMountpoint fp
2021-03-19 23:23:45 -04:00
$ asks mountconfVolatilePath >>= io . rmUntil fp
2020-05-01 21:29:54 -04:00
where
rmUntil cur target = unless (target == cur) $ do
removePathForcibly cur
rmUntil (takeDirectory cur) target
whenInMountDir :: FilePath -> RofiIO MountConf () -> RofiIO MountConf ()
whenInMountDir fp f = do
2021-03-19 23:23:45 -04:00
mDir <- asks mountconfVolatilePath
2020-05-01 21:29:54 -04:00
when (mDir `isPrefixOf` fp) f
unlessMountpoint :: FilePath -> RofiIO MountConf () -> RofiIO MountConf ()
unlessMountpoint fp f = do
mounted <- io $ isDirMounted fp
unless mounted f
isDirMounted :: FilePath -> IO Bool
isDirMounted fp = elem fp <$> curMountpoints
2020-04-23 23:32:29 -04:00
runMountNotify :: String -> [String] -> String -> Bool -> IO ()
runMountNotify cmd args msg mounted = do
res <- readCmdEither cmd args ""
notifyMounted (isRight res) mounted msg
2020-08-15 13:54:33 -04:00
umountNotify' :: String -> String -> FilePath -> RofiIO MountConf ()
umountNotify' cmd msg dir = finally
(io $ runMountNotify cmd [dir] msg True)
(rmDirMaybe dir)
2020-08-15 13:54:33 -04:00
umountNotify :: String -> FilePath -> RofiIO MountConf ()
umountNotify = umountNotify' "umount"
-- | Send a notification indicating the mount succeeded
notifyMounted :: Bool -> Bool -> String -> IO ()
notifyMounted succeeded mounted label = notify icon body
where
(format, icon) = if succeeded
2020-05-02 00:13:25 -04:00
then ("Successfully %sed %s", "dialog-information-symbolic")
else ("Failed to %s %s", "dialog-error-symbolic")
m = if mounted then "unmount" else "mount" :: String
body = printf format m label
notify :: String -> String -> IO ()
notify icon body = void $ spawnProcess "notify-send" ["-i", icon, body]
2020-04-23 23:32:29 -04:00
--------------------------------------------------------------------------------
-- | Other functions
fromLines :: (String -> Maybe a) -> [String] -> [a]
fromLines f = mapMaybe (f . stripWS)
2020-04-23 23:32:29 -04:00
-- TODO this exists somewhere...
splitBy :: Char -> String -> [String]
splitBy delimiter = foldr f [[]]
where
f _ [] = []
f c l@(x:xs) | c == delimiter = []:l
| otherwise = (c:x):xs
2021-03-18 00:41:04 -04:00
appendRoot :: FilePath -> FilePath -> FilePath
appendRoot root path = if isRelative path then root </> path else path