hsftp 1.4.0 → 1.5.0
raw patch · 19 files changed
+421/−422 lines, 19 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
- CmdOptions: options :: Mode (CmdArgs Options)
- Commands: download :: ReaderIO Int
- Commands: upload :: ReaderIO Int
- Config: Config :: String -> Int -> String -> String -> FilePath -> Config
- Config: [configHost] :: Config -> String
- Config: [configKnownHosts] :: Config -> FilePath
- Config: [configPassword] :: Config -> String
- Config: [configPort] :: Config -> Int
- Config: [configUser] :: Config -> String
- Config: data Config
- Config: instance Data.Aeson.Types.FromJSON.FromJSON Config.Remote
- Config: instance Data.Aeson.Types.FromJSON.FromJSON Config.YamlConfig
- Config: instance GHC.Show.Show Config.Config
- Config: instance GHC.Show.Show Config.Remote
- Config: instance GHC.Show.Show Config.YamlConfig
- Config: mkConfig :: YamlConfig -> IO Config
- Options: Down :: Direction
- Options: Options :: FilePath -> String -> [String] -> Direction -> FilePath -> FilePath -> Maybe FilePath -> Int -> Bool -> [FilePath] -> Options
- Options: Up :: Direction
- Options: [archive] :: Options -> Maybe FilePath
- Options: [conf] :: Options -> FilePath
- Options: [direction] :: Options -> Direction
- Options: [dryRun] :: Options -> Bool
- Options: [dst] :: Options -> FilePath
- Options: [extensions] :: Options -> [String]
- Options: [fromDate] :: Options -> String
- Options: [others] :: Options -> [FilePath]
- Options: [src] :: Options -> FilePath
- Options: [verbose] :: Options -> Int
- Options: data Direction
- Options: data Options
- Options: instance Data.Data.Data Options.Direction
- Options: instance Data.Data.Data Options.Options
- Options: instance GHC.Classes.Eq Options.Direction
- Options: instance GHC.Show.Show Options.Direction
- Options: instance GHC.Show.Show Options.Options
- Reader: Env :: String -> Int -> String -> String -> FilePath -> FilePath -> FilePath -> [String] -> Maybe FilePath -> Integer -> Bool -> Env
- Reader: [archiveTo] :: Env -> Maybe FilePath
- Reader: [date] :: Env -> Integer
- Reader: [hostName] :: Env -> String
- Reader: [knownHosts] :: Env -> FilePath
- Reader: [noOp] :: Env -> Bool
- Reader: [password] :: Env -> String
- Reader: [port] :: Env -> Int
- Reader: [transferExtensions] :: Env -> [String]
- Reader: [transferFrom] :: Env -> FilePath
- Reader: [transferTo] :: Env -> FilePath
- Reader: [user] :: Env -> String
- Reader: data Env
- Reader: type ReaderIO = ReaderT Env IO
- Util: createFile :: FilePath -> IO ()
- Util: toDate :: String -> UTCTime
- Util: toEpoch :: UTCTime -> Integer
+ Hsftp.CmdOptions: options :: Mode (CmdArgs Options)
+ Hsftp.Commands: download :: ReaderIO Int
+ Hsftp.Commands: upload :: ReaderIO Int
+ Hsftp.Config: Config :: String -> Int -> String -> String -> FilePath -> Config
+ Hsftp.Config: [configHost] :: Config -> String
+ Hsftp.Config: [configKnownHosts] :: Config -> FilePath
+ Hsftp.Config: [configPassword] :: Config -> String
+ Hsftp.Config: [configPort] :: Config -> Int
+ Hsftp.Config: [configUser] :: Config -> String
+ Hsftp.Config: data Config
+ Hsftp.Config: instance Data.Aeson.Types.FromJSON.FromJSON Hsftp.Config.Remote
+ Hsftp.Config: instance Data.Aeson.Types.FromJSON.FromJSON Hsftp.Config.YamlConfig
+ Hsftp.Config: instance GHC.Show.Show Hsftp.Config.Config
+ Hsftp.Config: instance GHC.Show.Show Hsftp.Config.Remote
+ Hsftp.Config: instance GHC.Show.Show Hsftp.Config.YamlConfig
+ Hsftp.Config: mkConfig :: YamlConfig -> IO Config
+ Hsftp.Options: Down :: Direction
+ Hsftp.Options: Options :: FilePath -> String -> [String] -> Direction -> FilePath -> FilePath -> Maybe FilePath -> Int -> Bool -> [FilePath] -> Options
+ Hsftp.Options: Up :: Direction
+ Hsftp.Options: [archive] :: Options -> Maybe FilePath
+ Hsftp.Options: [conf] :: Options -> FilePath
+ Hsftp.Options: [direction] :: Options -> Direction
+ Hsftp.Options: [dryRun] :: Options -> Bool
+ Hsftp.Options: [dst] :: Options -> FilePath
+ Hsftp.Options: [extensions] :: Options -> [String]
+ Hsftp.Options: [fromDate] :: Options -> String
+ Hsftp.Options: [others] :: Options -> [FilePath]
+ Hsftp.Options: [src] :: Options -> FilePath
+ Hsftp.Options: [verbose] :: Options -> Int
+ Hsftp.Options: data Direction
+ Hsftp.Options: data Options
+ Hsftp.Options: instance Data.Data.Data Hsftp.Options.Direction
+ Hsftp.Options: instance Data.Data.Data Hsftp.Options.Options
+ Hsftp.Options: instance GHC.Classes.Eq Hsftp.Options.Direction
+ Hsftp.Options: instance GHC.Show.Show Hsftp.Options.Direction
+ Hsftp.Options: instance GHC.Show.Show Hsftp.Options.Options
+ Hsftp.Reader: Env :: String -> Int -> String -> String -> String -> String -> String -> [String] -> Maybe String -> Integer -> Bool -> Env
+ Hsftp.Reader: [archiveTo] :: Env -> Maybe String
+ Hsftp.Reader: [date] :: Env -> Integer
+ Hsftp.Reader: [hostName] :: Env -> String
+ Hsftp.Reader: [knownHosts] :: Env -> String
+ Hsftp.Reader: [noOp] :: Env -> Bool
+ Hsftp.Reader: [password] :: Env -> String
+ Hsftp.Reader: [port] :: Env -> Int
+ Hsftp.Reader: [transferExtensions] :: Env -> [String]
+ Hsftp.Reader: [transferFrom] :: Env -> String
+ Hsftp.Reader: [transferTo] :: Env -> String
+ Hsftp.Reader: [user] :: Env -> String
+ Hsftp.Reader: data Env
+ Hsftp.Reader: instance GHC.Show.Show Hsftp.Reader.Env
+ Hsftp.Reader: type ReaderIO a = ReaderT Env IO a
+ Hsftp.Util: createFile :: FilePath -> IO ()
+ Hsftp.Util: toDate :: String -> UTCTime
+ Hsftp.Util: toEpoch :: UTCTime -> Integer
Files
- CHANGELOG.md +16/−0
- README.md +1/−1
- app/Main.hs +6/−11
- hsftp.cabal +7/−7
- src/CmdOptions.hs +0/−41
- src/Commands.hs +0/−92
- src/Config.hs +0/−103
- src/Hsftp/CmdOptions.hs +41/−0
- src/Hsftp/Commands.hs +90/−0
- src/Hsftp/Config.hs +103/−0
- src/Hsftp/Options.hs +51/−0
- src/Hsftp/Reader.hs +38/−0
- src/Hsftp/Util.hs +63/−0
- src/Options.hs +0/−51
- src/Reader.hs +0/−47
- src/Util.hs +0/−63
- test/TestCommands.hs +2/−3
- test/TestReader.hs +1/−1
- test/TestUtil.hs +2/−2
CHANGELOG.md view
@@ -1,3 +1,19 @@+## v1.5.0 (2025-09-02)++### Feat++- **namespace**: move source files to the Hsftp namespace++### CI++- **workflows**: assign permissions to workflows++## v1.4.1 (2025-08-21)++### Feat++- **commands**: do not copy metadata when archiving files after upload+ ## v1.4.0 (2025-06-09) ### Build
README.md view
@@ -6,7 +6,7 @@ -------------- ```-Hsftp 1.4.0. Usage: hsftp OPTION+Hsftp 1.5.0. Usage: hsftp OPTION hsftp [OPTIONS] [ITEM]
app/Main.hs view
@@ -2,24 +2,19 @@ ( main ) where -import CmdOptions ( options )--import Commands ( download, upload )--import Config- import Control.Monad ( when ) import Control.Monad.Reader import qualified Data.Yaml as Y -import Options ( Direction (..), Options (..) )--import Reader ( Env (..) )+import Hsftp.CmdOptions ( options )+import Hsftp.Commands ( download, upload )+import Hsftp.Config+import Hsftp.Options ( Direction (..), Options (..) )+import Hsftp.Reader ( Env (..) )+import Hsftp.Util ( createFile, toDate, toEpoch ) import System.Console.CmdArgs ( cmdArgsRun )--import Util ( createFile, toDate, toEpoch ) -- | Loads the environment configuration from a file.
hsftp.cabal view
@@ -1,6 +1,6 @@ cabal-version: 2.2 name: hsftp-version: 1.4.0+version: 1.5.0 license: BSD-3-Clause license-file: LICENSE copyright: (c) 2024-present IOcrafts@@ -23,12 +23,12 @@ library exposed-modules:- CmdOptions- Commands- Config- Options- Reader- Util+ Hsftp.CmdOptions+ Hsftp.Commands+ Hsftp.Config+ Hsftp.Options+ Hsftp.Reader+ Hsftp.Util pkgconfig-depends: libssh2 hs-source-dirs: src
− src/CmdOptions.hs
@@ -1,41 +0,0 @@-{-|-Module : CmdOptions-Description : Command-line options.-Copyright : (c) IOcrafts, 2024-present-License : BSD-Maintainer : Maurizio Dusi-Stability : stable-Portability : POSIX--This module holds the command-line options accepted by executable.--}--module CmdOptions- ( options- ) where--import Data.Version ( showVersion )--import Options ( Direction (..), Options (..) )--import Paths_hsftp ( version )--import System.Console.CmdArgs ( CmdArgs, Mode, args, cmdArgsMode,- enum, explicit, groupname, help, name,- program, summary, typ, (&=) )----- | Defines the command line options for the `hsftp` program.-options :: Mode (CmdArgs Options)-options = cmdArgsMode $ Options- { conf = "" &= typ "FILE" &= help "Load conf from file"- , fromDate = "" &= typ "DATE" &= help "Filter files by date (YYYY-MM-DD HH:MM UTC|PST|...)" &= explicit &= name "from-date"- , extensions = [] &= help "Filter files by extensions"- , direction = enum [Up &= help "upload", Down &= help "download"]- , src = "" &= typ "DIR" &= help "Folder to transfer from" &= explicit &= name "transfer-from"- , dst = "" &= typ "DIR" &= help "Folder to transfer to" &= explicit &= name "transfer-to"- , archive = Nothing &= typ "DIR" &= help "Folder to archive to after upload" &= explicit &= name "archive-to"- , verbose = 0 &= groupname "\nMiscellaneous" &= help "Verbose level: 1, 2 or 3" &= explicit &= name "verbose"- , dryRun = False &= help "Do a dry-run (\"No-op\") transfer." &= explicit &= name "dry-run" &= name "n"- , others = [] &= args- } &= summary ("Hsftp " <> showVersion version <> ". Usage: hsftp OPTION") &= program "hsftp"
− src/Commands.hs
@@ -1,92 +0,0 @@-{-|-Module : Commands-Description : Supported commands.-Copyright : (c) IOcrafts, 2024-present-License : BSD-Maintainer : Maurizio Dusi-Stability : stable-Portability : POSIX--This module holds a collection of supported commands.--}---module Commands- ( download- , upload- ) where---import Control.Monad ( filterM, unless )-import Control.Monad.Reader--import Data.Bits ( (.&.) )-import qualified Data.ByteString.Char8 as C--import Network.SSH.Client.LibSSH2-import Network.SSH.Client.LibSSH2.Foreign ( SftpAttributes (..) )--import Reader ( Env (..), ReaderIO )--import System.Directory ( copyFileWithMetadata,- doesFileExist,- getModificationTime,- listDirectory,- removeFile )-import System.FilePath ( isExtensionOf, (</>) )--import Util ( toEpoch )---{-|- Download files from a remote server using SFTP.- Both remote and local folders must exist.- The function returns the number of files downloaded.--}-download :: ReaderIO Int-download = do- Env{..} <- ask-- liftIO $ withSFTPUser knownHosts user password hostName port $ \sftp -> do- allFiles <- sftpListDir sftp transferFrom- let byDate x = (toInteger . saMtime . snd) x >= date- byExtension x = null transferExtensions || or [extension `isExtensionOf` (C.unpack . fst) x | extension <- transferExtensions]- isFile = (== 0o100000) . (.&. 0o170000) . saPermissions . snd- files = filter (\x -> byDate x && byExtension x && isFile x) allFiles- getFile f = do- let f' = C.unpack f- src = transferFrom </> f'- dst = transferTo </> f'- sftpReceiveFile sftp dst src- unless noOp $ mapM_ (getFile . fst) files- return $ length files--{-|- Upload files to a remote server using SFTP.- Both remote and local folders must exist.- The function returns the number of files uploaded.--}-upload :: ReaderIO Int-upload = do- Env{..} <- ask- let byExtension x = null transferExtensions || or [extension `isExtensionOf` x | extension <- transferExtensions]- byDate = fmap ( (>= date) . toEpoch ) . getModificationTime- allFiles <- liftIO $ listDirectory transferFrom >>=- filterM ( doesFileExist . (transferFrom </>) ) >>=- filterM ( byDate . (transferFrom </>) )- let files = filter byExtension allFiles-- unless (noOp || null files) $- liftIO $ withSFTPUser knownHosts user password hostName port $ \sftp -> do- let putFile f = do- let src = transferFrom </> f- dst = transferTo </> f- sftpSendFile sftp src dst 0o664- archiveFile f = case archiveTo of- Nothing -> return ()- Just d -> do- let src = transferFrom </> f- dst = d </> f- copyFileWithMetadata src dst >> removeFile src- mapM_ (\x -> putFile x >> archiveFile x) files- return $ length files
− src/Config.hs
@@ -1,103 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--{-|-Module : Config-Description : Process the YAML configuration file.-Copyright : (c) IOcrafts, 2024-present-License : BSD-Maintainer : Maurizio Dusi-Stability : stable-Portability : POSIX--This module parses a YAML file with configuration options.--__Example of conf.yaml:__--@- remote:- hostname: sftp.domain.com- port: 22- username: username- password: password- known_hosts: \/home\/user\/.ssh\/known_hosts-@---}--module Config- ( Config (..)- , mkConfig- ) where--import Control.Monad ( MonadPlus (mzero) )--import Data.Aeson ( FromJSON (parseJSON), (.!=), (.:) )-import qualified Data.Yaml as Y----- | Represents the configuration settings for the application.-data Config- = Config { -- | The host address of the server.- configHost :: String- -- | The port number to connect to.- , configPort :: Int- -- | The username for authentication.- , configUser :: String- -- | The password for authentication.- , configPassword :: String- -- | The file path to the known hosts file.- , configKnownHosts :: FilePath- }- deriving (Show)---- | Represents a YAML configuration with a remote value.-newtype YamlConfig- = YamlConfig { yamlRemote :: Remote }- deriving (Show)---- | Represents a remote SFTP configuration.-data Remote- = Remote { remoteHost :: String- -- ^ SFTP site- , remotePort :: Int- -- ^ SFTP port- , remoteUser :: String- -- ^ SFTP username- , remotePassword :: String- -- ^ SFTP password- , remoteKnownHosts :: FilePath- -- ^ Path to the known_hosts file- }- deriving (Show)---- | Create a 'Config' from a 'YamlConfig'.------ This function takes a 'YamlConfig' and extracts the necessary fields to create a 'Config' object.--- It returns an 'IO' action that produces the resulting 'Config'.-mkConfig :: YamlConfig -> IO Config-mkConfig YamlConfig{..} = do- let Remote {..} = yamlRemote- return $- Config { configHost = remoteHost- , configPort = remotePort- , configUser = remoteUser- , configPassword = remotePassword- , configKnownHosts = remoteKnownHosts- }----- | Parses a JSON object into a 'YamlConfig' value.-instance FromJSON YamlConfig where- parseJSON (Y.Object v) =- YamlConfig <$> v .: "remote"- parseJSON _ = mzero---- | Parses a JSON object into a 'Remote' data type.-instance FromJSON Remote where- parseJSON (Y.Object v) =- Remote <$> v .: "hostname"- <*> v .: "port" .!= 22- <*> v .: "username"- <*> v .: "password"- <*> v .: "known_hosts"- parseJSON _ = mzero
+ src/Hsftp/CmdOptions.hs view
@@ -0,0 +1,41 @@+{-|+Module : Hsftp.CmdOptions+Description : Command-line options.+Copyright : (c) IOcrafts, 2024-present+License : BSD+Maintainer : Maurizio Dusi+Stability : stable+Portability : POSIX++This module holds the command-line options accepted by executable.+-}++module Hsftp.CmdOptions+ ( options+ ) where++import Data.Version ( showVersion )++import Hsftp.Options ( Direction (..), Options (..) )++import Paths_hsftp ( version )++import System.Console.CmdArgs ( CmdArgs, Mode, args, cmdArgsMode,+ enum, explicit, groupname, help, name,+ program, summary, typ, (&=) )+++-- | Defines the command line options for the `hsftp` program.+options :: Mode (CmdArgs Options)+options = cmdArgsMode $ Options+ { conf = "" &= typ "FILE" &= help "Load conf from file"+ , fromDate = "" &= typ "DATE" &= help "Filter files by date (YYYY-MM-DD HH:MM UTC|PST|...)" &= explicit &= name "from-date"+ , extensions = [] &= help "Filter files by extensions"+ , direction = enum [Up &= help "upload", Down &= help "download"]+ , src = "" &= typ "DIR" &= help "Folder to transfer from" &= explicit &= name "transfer-from"+ , dst = "" &= typ "DIR" &= help "Folder to transfer to" &= explicit &= name "transfer-to"+ , archive = Nothing &= typ "DIR" &= help "Folder to archive to after upload" &= explicit &= name "archive-to"+ , verbose = 0 &= groupname "\nMiscellaneous" &= help "Verbose level: 1, 2 or 3" &= explicit &= name "verbose"+ , dryRun = False &= help "Do a dry-run (\"No-op\") transfer." &= explicit &= name "dry-run" &= name "n"+ , others = [] &= args+ } &= summary ("Hsftp " <> showVersion version <> ". Usage: hsftp OPTION") &= program "hsftp"
+ src/Hsftp/Commands.hs view
@@ -0,0 +1,90 @@+{-|+Module : Hsftp.Commands+Description : Supported commands.+Copyright : (c) IOcrafts, 2024-present+License : BSD+Maintainer : Maurizio Dusi+Stability : stable+Portability : POSIX++This module holds a collection of supported commands.+-}+++module Hsftp.Commands+ ( download+ , upload+ ) where+++import Control.Monad ( filterM, unless )+import Control.Monad.Reader++import Data.Bits ( (.&.) )+import qualified Data.ByteString.Char8 as C++import Hsftp.Reader ( Env (..), ReaderIO )+import Hsftp.Util ( toEpoch )++import Network.SSH.Client.LibSSH2+import Network.SSH.Client.LibSSH2.Foreign ( SftpAttributes (..) )++import System.Directory ( copyFile, doesFileExist,+ getModificationTime,+ listDirectory,+ removeFile )+import System.FilePath ( isExtensionOf, (</>) )+++{-|+ Download files from a remote server using SFTP.+ Both remote and local folders must exist.+ The function returns the number of files downloaded.+-}+download :: ReaderIO Int+download = do+ Env{..} <- ask++ liftIO $ withSFTPUser knownHosts user password hostName port $ \sftp -> do+ allFiles <- sftpListDir sftp transferFrom+ let byDate x = (toInteger . saMtime . snd) x >= date+ byExtension x = null transferExtensions || or [extension `isExtensionOf` (C.unpack . fst) x | extension <- transferExtensions]+ isFile = (== 0o100000) . (.&. 0o170000) . saPermissions . snd+ files = filter (\x -> byDate x && byExtension x && isFile x) allFiles+ getFile f = do+ let f' = C.unpack f+ src = transferFrom </> f'+ dst = transferTo </> f'+ sftpReceiveFile sftp dst src+ unless noOp $ mapM_ (getFile . fst) files+ return $ length files++{-|+ Upload files to a remote server using SFTP.+ Both remote and local folders must exist.+ The function returns the number of files uploaded.+-}+upload :: ReaderIO Int+upload = do+ Env{..} <- ask+ let byExtension x = null transferExtensions || or [extension `isExtensionOf` x | extension <- transferExtensions]+ byDate = fmap ( (>= date) . toEpoch ) . getModificationTime+ allFiles <- liftIO $ listDirectory transferFrom >>=+ filterM ( doesFileExist . (transferFrom </>) ) >>=+ filterM ( byDate . (transferFrom </>) )+ let files = filter byExtension allFiles++ unless (noOp || null files) $+ liftIO $ withSFTPUser knownHosts user password hostName port $ \sftp -> do+ let putFile f = do+ let src = transferFrom </> f+ dst = transferTo </> f+ sftpSendFile sftp src dst 0o664+ archiveFile f = case archiveTo of+ Nothing -> return ()+ Just d -> do+ let src = transferFrom </> f+ dst = d </> f+ copyFile src dst >> removeFile src+ mapM_ (\x -> putFile x >> archiveFile x) files+ return $ length files
+ src/Hsftp/Config.hs view
@@ -0,0 +1,103 @@+{-# LANGUAGE OverloadedStrings #-}++{-|+Module : Hsftp.Config+Description : Process the YAML configuration file.+Copyright : (c) IOcrafts, 2024-present+License : BSD+Maintainer : Maurizio Dusi+Stability : stable+Portability : POSIX++This module parses a YAML file with configuration options.++__Example of conf.yaml:__++@+ remote:+ hostname: sftp.domain.com+ port: 22+ username: username+ password: password+ known_hosts: \/home\/user\/.ssh\/known_hosts+@++-}++module Hsftp.Config+ ( Config (..)+ , mkConfig+ ) where++import Control.Monad ( MonadPlus (mzero) )++import Data.Aeson ( FromJSON (parseJSON), (.!=), (.:) )+import qualified Data.Yaml as Y+++-- | Represents the configuration settings for the application.+data Config+ = Config { -- | The host address of the server.+ configHost :: String+ -- | The port number to connect to.+ , configPort :: Int+ -- | The username for authentication.+ , configUser :: String+ -- | The password for authentication.+ , configPassword :: String+ -- | The file path to the known hosts file.+ , configKnownHosts :: FilePath+ }+ deriving (Show)++-- | Represents a YAML configuration with a remote value.+newtype YamlConfig+ = YamlConfig { yamlRemote :: Remote }+ deriving (Show)++-- | Represents a remote SFTP configuration.+data Remote+ = Remote { remoteHost :: String+ -- ^ SFTP site+ , remotePort :: Int+ -- ^ SFTP port+ , remoteUser :: String+ -- ^ SFTP username+ , remotePassword :: String+ -- ^ SFTP password+ , remoteKnownHosts :: FilePath+ -- ^ Path to the known_hosts file+ }+ deriving (Show)++-- | Create a 'Config' from a 'YamlConfig'.+--+-- This function takes a 'YamlConfig' and extracts the necessary fields to create a 'Config' object.+-- It returns an 'IO' action that produces the resulting 'Config'.+mkConfig :: YamlConfig -> IO Config+mkConfig YamlConfig{..} = do+ let Remote {..} = yamlRemote+ return $+ Config { configHost = remoteHost+ , configPort = remotePort+ , configUser = remoteUser+ , configPassword = remotePassword+ , configKnownHosts = remoteKnownHosts+ }+++-- | Parses a JSON object into a 'YamlConfig' value.+instance FromJSON YamlConfig where+ parseJSON (Y.Object v) =+ YamlConfig <$> v .: "remote"+ parseJSON _ = mzero++-- | Parses a JSON object into a 'Remote' data type.+instance FromJSON Remote where+ parseJSON (Y.Object v) =+ Remote <$> v .: "hostname"+ <*> v .: "port" .!= 22+ <*> v .: "username"+ <*> v .: "password"+ <*> v .: "known_hosts"+ parseJSON _ = mzero
+ src/Hsftp/Options.hs view
@@ -0,0 +1,51 @@+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE DerivingStrategies #-}++{-|+Module : Hsftp.Options+Description : Holds the options for the hsftp utility.+Copyright : (c) IOcrafts, 2024-present+License : BSD+Maintainer : Maurizio Dusi+Stability : stable+Portability : POSIX++This module holds the available options for the hsftp utility.+-}++module Hsftp.Options+ ( Direction (..)+ , Options (..)+ ) where++import Data.Data ( Data, Typeable )++data Direction+ = Up+ | Down+ deriving (Data, Eq, Show)++-- | Represents the options for the program.+data Options+ = Options { conf :: FilePath+ -- ^ Path to the configuration file.+ , fromDate :: String+ -- ^ Filter files by date (see 'toDate' for details on supported formats).+ , extensions :: [String]+ -- ^ Filter files by extensions.+ , direction :: Direction+ -- ^ Direction of the transfer.+ , src :: FilePath+ -- ^ Transfer from this folder (folder must exist).+ , dst :: FilePath+ -- ^ Transfer to this folder (folder must exist).+ , archive :: Maybe FilePath+ -- ^ Archive into this folder after successful upload (folder must exist).+ , verbose :: Int+ -- ^ Verbose level.+ , dryRun :: Bool+ -- ^ Do a dry-run (no-op) transfer.+ , others :: [FilePath]+ -- ^ List of files and/or folders.+ }+ deriving stock (Data, Show, Typeable)
+ src/Hsftp/Reader.hs view
@@ -0,0 +1,38 @@+{-|+Module : Hsftp.Reader+Description : Monad for keeping shared state.+Copyright : (c) IOcrafts, 2024-present+License : BSD+Maintainer : Maurizio Dusi+Stability : stable+Portability : POSIX++This module provides the Reader monad for keeping shared state.++-}+++module Hsftp.Reader+ ( Env (..)+ , ReaderIO+ ) where++import Control.Monad.Reader++data Env+ = Env { hostName :: String+ , port :: Int+ , knownHosts :: String+ , user :: String+ , password :: String+ , transferFrom :: String+ , transferTo :: String+ , transferExtensions :: [String]+ , archiveTo :: Maybe String+ , date :: Integer+ , noOp :: Bool+ }+ deriving (Show)+++type ReaderIO a = ReaderT Env IO a
+ src/Hsftp/Util.hs view
@@ -0,0 +1,63 @@+{-|+Module : Hsftp.Util+Description : Collections of utility functions.+Copyright : (c) IOcrafts, 2024-present+License : BSD+Maintainer : Maurizio Dusi+Stability : stable+Portability : POSIX++This module provides utility functions for file, date and time manipulation.++-}+++module Hsftp.Util+ ( createFile+ , toDate+ , toEpoch+ ) where++import Control.Monad ( unless )++import Data.Time++import System.Directory ( doesFileExist )++{-|+ Convert a string to seconds since Epoch.+ The input string should be in the format %F %R %Z (YYYY-MM-DD HH-mm and abbreviated time zone name).+ If the parsing fails, it defaults to the beginning of Epoch (i.e., zero).++ Example usage:++ >>> toDate "2022-01-01 12:00 UTC"+ 2022-01-01 12:00:00 UTC+-}+toDate :: String -> UTCTime+toDate d = case parseTimeM True defaultTimeLocale "%F %R %Z" d of+ Just x -> x+ Nothing -> UTCTime (fromGregorian 1970 01 01) (secondsToDiffTime 0)++{-|+ Convert a 'UTCTime' value to seconds since Epoch.++ Example usage:++ >>> toEpoch (UTCTime (fromGregorian 2022 01 01) (secondsToDiffTime 0))+ 1640995200+-}+toEpoch :: UTCTime -> Integer+toEpoch d = read $ formatTime defaultTimeLocale "%s" d++{-|+ Create a file if it does not exist.++ Example usage:++ >>> createFile "test.txt"+-}+createFile :: FilePath -> IO ()+createFile cfile = do+ fileExists <- doesFileExist cfile+ unless fileExists $ writeFile cfile ""
− src/Options.hs
@@ -1,51 +0,0 @@-{-# LANGUAGE DeriveDataTypeable #-}-{-# LANGUAGE DerivingStrategies #-}--{-|-Module : Options-Description : Holds the options for the hedictl utility.-Copyright : (c) IOcrafts, 2024-present-License : BSD-Maintainer : Maurizio Dusi-Stability : stable-Portability : POSIX--This module holds the available options for the hedictl utility.--}--module Options- ( Direction (..)- , Options (..)- ) where--import Data.Data ( Data, Typeable )--data Direction- = Up- | Down- deriving (Data, Eq, Show)---- | Represents the options for the program.-data Options- = Options { conf :: FilePath- -- ^ Path to the configuration file.- , fromDate :: String- -- ^ Filter files by date (see 'toDate' for details on supported formats).- , extensions :: [String]- -- ^ Filter files by extensions.- , direction :: Direction- -- ^ Direction of the transfer.- , src :: FilePath- -- ^ Transfer from this folder (folder must exist).- , dst :: FilePath- -- ^ Transfer to this folder (folder must exist).- , archive :: Maybe FilePath- -- ^ Archive into this folder after successful upload (folder must exist).- , verbose :: Int- -- ^ Verbose level.- , dryRun :: Bool- -- ^ Do a dry-run (no-op) transfer.- , others :: [FilePath]- -- ^ List of files and/or folders.- }- deriving stock (Data, Show, Typeable)
− src/Reader.hs
@@ -1,47 +0,0 @@-{-|-Module : Reader-Description : Holds environment variables.-Copyright : (c) IOcrafts, 2024-present-License : BSD-Maintainer : Maurizio Dusi-Stability : stable-Portability : POSIX--This module holds the environment variables used by the program.--}--module Reader- ( Env (..)- , ReaderIO- ) where--import Control.Monad.Reader ( ReaderT )----- | Represents the environment configuration for the SFTP client.-data Env- = Env { hostName :: String- -- ^ The hostname of the SFTP server.- , port :: Int- -- ^ The port number to connect to.- , user :: String- -- ^ The username for authentication.- , password :: String- -- ^ The password for authentication.- , knownHosts :: FilePath- -- ^ The path to the known hosts file.- , transferFrom :: FilePath- -- ^ The source file path for transfer.- , transferTo :: FilePath- -- ^ The destination file path for transfer.- , transferExtensions :: [String]- -- ^ The list of file extensions to transfer.- , archiveTo :: Maybe FilePath- -- ^ Optional path to archive transferred files.- , date :: Integer- -- ^ The date for filtering files to transfer.- , noOp :: Bool- -- ^ Whether or not to perform the actual transfer.- }--type ReaderIO = ReaderT Env IO
− src/Util.hs
@@ -1,63 +0,0 @@-{-|-Module : Util-Description : Collections of utility functions.-Copyright : (c) IOcrafts, 2024-present-License : BSD-Maintainer : Maurizio Dusi-Stability : stable-Portability : POSIX--This module provides utility functions for file, date and time manipulation.---}---module Util- ( createFile- , toDate- , toEpoch- ) where--import Control.Monad ( unless )--import Data.Time--import System.Directory ( doesFileExist )--{-|- Convert a string to seconds since Epoch.- The input string should be in the format %F %R %Z (YYYY-MM-DD HH-mm and abbreviated time zone name).- If the parsing fails, it defaults to the beginning of Epoch (i.e., zero).-- Example usage:-- >>> toDate "2022-01-01 12:00 UTC"- 2022-01-01 12:00:00 UTC--}-toDate :: String -> UTCTime-toDate d = case parseTimeM True defaultTimeLocale "%F %R %Z" d of- Just x -> x- Nothing -> UTCTime (fromGregorian 1970 01 01) (secondsToDiffTime 0)--{-|- Convert a 'UTCTime' value to seconds since Epoch.-- Example usage:-- >>> toEpoch (UTCTime (fromGregorian 2022 01 01) (secondsToDiffTime 0))- 1640995200--}-toEpoch :: UTCTime -> Integer-toEpoch d = read $ formatTime defaultTimeLocale "%s" d--{-|- Create a file if it does not exist.-- Example usage:-- >>> createFile "test.txt"--}-createFile :: FilePath -> IO ()-createFile cfile = do- fileExists <- doesFileExist cfile- unless fileExists $ writeFile cfile ""
test/TestCommands.hs view
@@ -19,12 +19,11 @@ , sftpUploadTests ) where -import Commands ( download, upload )- import Control.Monad ( filterM ) import Control.Monad.Reader -import Reader ( Env (..) )+import Hsftp.Commands ( download, upload )+import Hsftp.Reader ( Env (..) ) import System.Directory ( createDirectoryIfMissing, doesFileExist, listDirectory,
test/TestReader.hs view
@@ -14,7 +14,7 @@ ( readerTests ) where -import Reader ( Env (..) )+import Hsftp.Reader ( Env (..) ) import Test.Tasty ( TestTree, testGroup ) import Test.Tasty.HUnit ( testCase, (@?=) )
test/TestUtil.hs view
@@ -16,13 +16,13 @@ import Data.Time +import Hsftp.Util ( createFile, toDate, toEpoch )+ import System.Directory ( doesFileExist ) import System.IO.Temp ( withTempFile ) import Test.Tasty ( TestTree, testGroup ) import Test.Tasty.HUnit ( testCase, (@?=) )--import Util ( createFile, toDate, toEpoch ) utilTests :: TestTree utilTests =