-
-
Notifications
You must be signed in to change notification settings - Fork 94
xftp-server: support postgresql backend #1755
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
704cdac
7a76102
1bf3211
2caf2e5
d703cfa
8e449b8
b0da982
6f4bf64
ff254b4
cde9f50
ae4888f
d6b6cd5
aacd873
dea62cc
d101a9b
dd395b4
0d28333
e5f6648
c1f978a
e831d5a
c306e9b
e659f4a
1c6f688
464e083
5de4f78
37b3ad0
e63e0be
8a8bda2
aee5558
6cac469
26bcc72
fcbb13e
b5055ad
1f8bd6b
3f81291
e530463
50c387d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,37 +1,53 @@ | ||
| {-# LANGUAGE CPP #-} | ||
| {-# LANGUAGE DataKinds #-} | ||
| {-# LANGUAGE DuplicateRecordFields #-} | ||
| {-# LANGUAGE GADTs #-} | ||
| {-# LANGUAGE KindSignatures #-} | ||
| {-# LANGUAGE RankNTypes #-} | ||
| {-# LANGUAGE LambdaCase #-} | ||
| {-# LANGUAGE NamedFieldPuns #-} | ||
| {-# LANGUAGE OverloadedStrings #-} | ||
| {-# LANGUAGE StrictData #-} | ||
|
|
||
| module Simplex.FileTransfer.Server.Env | ||
| ( XFTPServerConfig (..), | ||
| XFTPStoreConfig (..), | ||
| XFTPEnv (..), | ||
| XFTPRequest (..), | ||
| defaultInactiveClientExpiration, | ||
| defFileExpirationHours, | ||
| defaultFileExpiration, | ||
| newXFTPServerEnv, | ||
| countUsedStorage, | ||
| runWithStoreConfig, | ||
| checkFileStoreMode, | ||
| importToDatabase, | ||
| exportFromDatabase, | ||
| ) where | ||
|
|
||
| import Control.Logger.Simple | ||
| import Control.Monad | ||
| import Crypto.Random | ||
| import Data.Int (Int64) | ||
| import Data.List.NonEmpty (NonEmpty) | ||
| import qualified Data.Map.Strict as M | ||
| import Data.Time.Clock (getCurrentTime) | ||
| import Data.Word (Word32) | ||
| import Data.X509.Validation (Fingerprint (..)) | ||
| import Network.Socket | ||
| import qualified Network.TLS as T | ||
| import Simplex.FileTransfer.Protocol (FileCmd, FileInfo (..), XFTPFileId) | ||
| import Simplex.FileTransfer.Server.Stats | ||
| import Data.Ini (Ini) | ||
| import Simplex.FileTransfer.Server.Store | ||
| import Simplex.Messaging.Agent.Store.Shared (MigrationConfirmation) | ||
| #if defined(dbServerPostgres) | ||
| import Data.Functor (($>)) | ||
| import Data.Maybe (isNothing) | ||
| import Simplex.FileTransfer.Server.Store.Postgres (PostgresFileStore, importFileStore, exportFileStore) | ||
| import Simplex.FileTransfer.Server.Store.Postgres.Config (PostgresFileStoreCfg (..), defaultXFTPDBOpts) | ||
| import Simplex.Messaging.Server.CLI (iniDBOptions, settingIsOn) | ||
| import System.Directory (doesFileExist) | ||
| import System.Exit (exitFailure) | ||
| #endif | ||
| import Simplex.FileTransfer.Server.StoreLog | ||
| import Simplex.FileTransfer.Transport (VersionRangeXFTP) | ||
| import qualified Simplex.Messaging.Crypto as C | ||
|
|
@@ -42,10 +58,11 @@ import Simplex.Messaging.Util (tshow) | |
| import System.IO (IOMode (..)) | ||
| import UnliftIO.STM | ||
|
|
||
| data XFTPServerConfig = XFTPServerConfig | ||
| data XFTPServerConfig s = XFTPServerConfig | ||
| { xftpPort :: ServiceName, | ||
| controlPort :: Maybe ServiceName, | ||
| fileIdSize :: Int, | ||
| serverStoreCfg :: XFTPStoreConfig s, | ||
| storeLogFile :: Maybe FilePath, | ||
| filesPath :: FilePath, | ||
| -- | server storage quota | ||
|
|
@@ -88,9 +105,16 @@ defaultInactiveClientExpiration = | |
| checkInterval = 3600 -- seconds, 1 hours | ||
| } | ||
|
|
||
| data XFTPEnv = XFTPEnv | ||
| { config :: XFTPServerConfig, | ||
| store :: FileStore, | ||
| data XFTPStoreConfig s where | ||
| XSCMemory :: Maybe FilePath -> XFTPStoreConfig STMFileStore | ||
| #if defined(dbServerPostgres) | ||
| XSCDatabase :: PostgresFileStoreCfg -> XFTPStoreConfig PostgresFileStore | ||
| #endif | ||
|
|
||
| data XFTPEnv s = XFTPEnv | ||
| { config :: XFTPServerConfig s, | ||
| store :: s, | ||
| usedStorage :: TVar Int64, | ||
| storeLog :: Maybe (StoreLog 'WriteMode), | ||
| random :: TVar ChaChaDRG, | ||
| serverIdentity :: C.KeyHash, | ||
|
|
@@ -109,26 +133,91 @@ defaultFileExpiration = | |
| checkInterval = 2 * 3600 -- seconds, 2 hours | ||
| } | ||
|
|
||
| newXFTPServerEnv :: XFTPServerConfig -> IO XFTPEnv | ||
| newXFTPServerEnv config@XFTPServerConfig {storeLogFile, fileSizeQuota, xftpCredentials, httpCredentials} = do | ||
| newXFTPServerEnv :: FileStoreClass s => XFTPServerConfig s -> IO (XFTPEnv s) | ||
| newXFTPServerEnv config@XFTPServerConfig {serverStoreCfg, fileSizeQuota, xftpCredentials, httpCredentials} = do | ||
| random <- C.newRandom | ||
| store <- newFileStore | ||
| storeLog <- mapM (`readWriteFileStore` store) storeLogFile | ||
| used <- countUsedStorage <$> readTVarIO (files store) | ||
| atomically $ writeTVar (usedStorage store) used | ||
| (store, storeLog) <- case serverStoreCfg of | ||
| XSCMemory storeLogPath -> do | ||
| st <- newFileStore () | ||
| sl <- mapM (`readWriteFileStore` st) storeLogPath | ||
| atomically $ writeTVar (stmStoreLog st) sl | ||
| pure (st, sl) | ||
| #if defined(dbServerPostgres) | ||
| XSCDatabase dbCfg -> do | ||
| st <- newFileStore dbCfg | ||
| pure (st, Nothing) | ||
| #endif | ||
| used <- getUsedStorage store | ||
| usedStorage <- newTVarIO used | ||
| forM_ fileSizeQuota $ \quota -> do | ||
| logNote $ "Total / available storage: " <> tshow quota <> " / " <> tshow (quota - used) | ||
| when (quota < used) $ logWarn "WARNING: storage quota is less than used storage, no files can be uploaded!" | ||
| tlsServerCreds <- loadServerCredential xftpCredentials | ||
| httpServerCreds <- mapM loadServerCredential httpCredentials | ||
| Fingerprint fp <- loadFingerprint xftpCredentials | ||
| serverStats <- newFileServerStats =<< getCurrentTime | ||
| pure XFTPEnv {config, store, storeLog, random, tlsServerCreds, httpServerCreds, serverIdentity = C.KeyHash fp, serverStats} | ||
|
|
||
| countUsedStorage :: M.Map k FileRec -> Int64 | ||
| countUsedStorage = M.foldl' (\acc FileRec {fileInfo = FileInfo {size}} -> acc + fromIntegral size) 0 | ||
| pure XFTPEnv {config, store, usedStorage, storeLog, random, tlsServerCreds, httpServerCreds, serverIdentity = C.KeyHash fp, serverStats} | ||
|
|
||
| data XFTPRequest | ||
| = XFTPReqNew FileInfo (NonEmpty RcvPublicAuthKey) (Maybe BasicAuth) | ||
| | XFTPReqCmd XFTPFileId FileRec FileCmd | ||
| | XFTPReqPing | ||
|
|
||
| -- | Select and run the store config based on INI settings. | ||
| -- CPP guards for Postgres are handled here so Main.hs stays CPP-free. | ||
| runWithStoreConfig :: | ||
| Ini -> | ||
| String -> | ||
| Maybe FilePath -> | ||
| FilePath -> | ||
| MigrationConfirmation -> | ||
| (forall s. FileStoreClass s => XFTPStoreConfig s -> IO ()) -> | ||
| IO () | ||
| runWithStoreConfig _ini storeType storeLogFile_ _storeLogFilePath _confirmMigrations run = case storeType of | ||
| "memory" -> run $ XSCMemory storeLogFile_ | ||
| #if defined(dbServerPostgres) | ||
| "database" -> run $ XSCDatabase dbCfg | ||
| where | ||
| enableDbStoreLog' = settingIsOn "STORE_LOG" "db_store_log" _ini | ||
| dbStoreLogPath = enableDbStoreLog' $> _storeLogFilePath | ||
| dbCfg = PostgresFileStoreCfg {dbOpts = iniDBOptions _ini defaultXFTPDBOpts, dbStoreLogPath, confirmMigrations = _confirmMigrations} | ||
| #else | ||
| "database" -> error "Error: server binary is compiled without support for PostgreSQL database.\nPlease re-compile with `cabal build -fserver_postgres`." | ||
| #endif | ||
| _ -> error $ "Invalid store_files value: " <> storeType | ||
|
|
||
| -- | Validate startup config when store_files=database. | ||
| checkFileStoreMode :: Ini -> String -> FilePath -> IO () | ||
| #if defined(dbServerPostgres) | ||
| checkFileStoreMode ini storeType storeLogFilePath = case storeType of | ||
| "database" -> do | ||
| storeLogExists <- doesFileExist storeLogFilePath | ||
| let dbStoreLogOn = settingIsOn "STORE_LOG" "db_store_log" ini | ||
| when (storeLogExists && isNothing dbStoreLogOn) $ do | ||
| putStrLn $ "Error: store log file " <> storeLogFilePath <> " exists but store_files is `database`." | ||
| putStrLn "Use `file-server database import` to migrate, or set `db_store_log: on`." | ||
| exitFailure | ||
| _ -> pure () | ||
| #else | ||
| checkFileStoreMode _ _ _ = pure () | ||
| #endif | ||
|
|
||
| -- | Import StoreLog to PostgreSQL database. | ||
| importToDatabase :: FilePath -> Ini -> MigrationConfirmation -> IO () | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this belongs in Main.hs I think
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The current placement is deliberate - comment at Env.hs:165 says "CPP guards for Postgres are handled here so Main.hs stays CPP-free." Main.hs currently has zero CPP. Moving importToDatabase/exportFromDatabase to Main.hs would drag #if defined(dbServerPostgres) ... #else ... #endif blocks plus the Postgres.Config/importFileStore imports into Main.hs, same as runWithStoreConfig and checkFileStoreMode. |
||
| #if defined(dbServerPostgres) | ||
| importToDatabase storeLogFilePath ini _confirmMigrations = do | ||
| let dbCfg = PostgresFileStoreCfg {dbOpts = iniDBOptions ini defaultXFTPDBOpts, dbStoreLogPath = Nothing, confirmMigrations = _confirmMigrations} | ||
| importFileStore storeLogFilePath dbCfg | ||
| #else | ||
| importToDatabase _ _ _ = error "Error: server binary is compiled without support for PostgreSQL database.\nPlease re-compile with `cabal build -fserver_postgres`." | ||
| #endif | ||
|
|
||
| -- | Export PostgreSQL database to StoreLog. | ||
| exportFromDatabase :: FilePath -> Ini -> MigrationConfirmation -> IO () | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this too?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same reason. exportFromDatabase sits alongside importToDatabase under the same CPP guard in Env.hs for the same reason: concentrate all #if defined(dbServerPostgres) in one file so Main.hs stays CPP-free. Same tradeoff applies. |
||
| #if defined(dbServerPostgres) | ||
| exportFromDatabase storeLogFilePath ini _confirmMigrations = do | ||
| let dbCfg = PostgresFileStoreCfg {dbOpts = iniDBOptions ini defaultXFTPDBOpts, dbStoreLogPath = Nothing, confirmMigrations = _confirmMigrations} | ||
| exportFileStore storeLogFilePath dbCfg | ||
| #else | ||
| exportFromDatabase _ _ _ = error "Error: server binary is compiled without support for PostgreSQL database.\nPlease re-compile with `cabal build -fserver_postgres`." | ||
| #endif | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what would happen if server with DB in INI started with exe compiled without postgres? will it correctly exit with error?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Env.hs:183 handles it in the #else branch with error "Error: server binary is compiled without support for PostgreSQL database...". It exits non-zero with a clear message, so the check works.