2014-08-01 19:09:49 +00:00
|
|
|
{- git-annex command
|
|
|
|
-
|
2020-01-14 16:35:08 +00:00
|
|
|
- Copyright 2014-2020 Joey Hess <id@joeyh.name>
|
2014-08-01 19:09:49 +00:00
|
|
|
-
|
2019-03-13 19:48:14 +00:00
|
|
|
- Licensed under the GNU AGPL version 3 or higher.
|
2014-08-01 19:09:49 +00:00
|
|
|
-}
|
|
|
|
|
2023-04-10 16:56:45 +00:00
|
|
|
{-# LANGUAGE RankNTypes, DeriveFunctor, PackageImports, OverloadedStrings #-}
|
2020-04-29 17:16:43 +00:00
|
|
|
|
2014-08-01 19:09:49 +00:00
|
|
|
module Command.TestRemote where
|
|
|
|
|
|
|
|
import Command
|
|
|
|
import qualified Annex
|
|
|
|
import qualified Remote
|
2014-08-01 20:50:24 +00:00
|
|
|
import qualified Types.Remote as Remote
|
2020-07-20 18:06:05 +00:00
|
|
|
import qualified Types.Backend
|
2014-08-01 19:09:49 +00:00
|
|
|
import Types.KeySource
|
|
|
|
import Annex.Content
|
2019-01-17 16:39:29 +00:00
|
|
|
import Annex.WorkTree
|
2014-08-01 19:09:49 +00:00
|
|
|
import Backend
|
2019-01-17 16:39:29 +00:00
|
|
|
import Logs.Location
|
2014-08-01 19:09:49 +00:00
|
|
|
import qualified Backend.Hash
|
|
|
|
import Utility.Tmp
|
|
|
|
import Utility.Metered
|
2014-08-01 20:50:24 +00:00
|
|
|
import Utility.DataUnits
|
2014-08-01 21:16:20 +00:00
|
|
|
import Utility.CopyFile
|
2014-08-01 19:09:49 +00:00
|
|
|
import Types.Messages
|
2017-11-08 18:22:05 +00:00
|
|
|
import Types.Export
|
2020-01-14 16:35:08 +00:00
|
|
|
import Types.RemoteConfig
|
fix encryption of content to gcrypt and git-lfs
Fix serious regression in gcrypt and encrypted git-lfs remotes.
Since version 7.20200202.7, git-annex incorrectly stored content
on those remotes without encrypting it.
Problem was, Remote.Git enumerates all git remotes, including git-lfs
and gcrypt. It then dispatches to those. So, Remote.List used the
RemoteConfigParser from Remote.Git, instead of from git-lfs or gcrypt,
and that parser does not know about encryption fields, so did not
include them in the ParsedRemoteConfig. (Also didn't include other
fields specific to those remotes, perhaps chunking etc also didn't
get through.)
To fix, had to move RemoteConfig parsing down into the generate methods
of each remote, rather than doing it in Remote.List.
And a consequence of that was that ParsedRemoteConfig had to change to
include the RemoteConfig that got parsed, so that testremote can
generate a new remote based on an existing remote.
(I would have rather fixed this just inside Remote.Git, but that was not
practical, at least not w/o re-doing work that Remote.List already did.
Big ugly mostly mechanical patch seemed preferable to making git-annex
slower.)
2020-02-26 21:20:56 +00:00
|
|
|
import Types.ProposedAccepted
|
2020-01-15 15:22:36 +00:00
|
|
|
import Annex.SpecialRemote.Config (exportTreeField)
|
2014-08-01 20:50:24 +00:00
|
|
|
import Remote.Helper.Chunked
|
2020-04-30 17:49:22 +00:00
|
|
|
import Remote.Helper.Encryptable (encryptionField, highRandomQualityField)
|
2015-07-11 04:42:32 +00:00
|
|
|
import Git.Types
|
2014-08-01 19:09:49 +00:00
|
|
|
|
|
|
|
import Test.Tasty
|
|
|
|
import Test.Tasty.Runners
|
|
|
|
import Test.Tasty.HUnit
|
|
|
|
import "crypto-api" Crypto.Random
|
|
|
|
import qualified Data.ByteString as B
|
2014-08-01 21:16:20 +00:00
|
|
|
import qualified Data.ByteString.Lazy as L
|
2014-08-01 20:50:24 +00:00
|
|
|
import qualified Data.Map as M
|
2020-05-13 18:03:00 +00:00
|
|
|
import Data.Either
|
2020-04-28 21:19:07 +00:00
|
|
|
import Control.Concurrent.STM hiding (check)
|
2014-08-01 19:09:49 +00:00
|
|
|
|
2015-07-08 16:33:27 +00:00
|
|
|
cmd :: Command
|
2015-07-11 04:42:32 +00:00
|
|
|
cmd = command "testremote" SectionTesting
|
|
|
|
"test transfers to/from a remote"
|
|
|
|
paramRemote (seek <$$> optParser)
|
2014-08-01 20:50:24 +00:00
|
|
|
|
2015-07-11 04:42:32 +00:00
|
|
|
data TestRemoteOptions = TestRemoteOptions
|
|
|
|
{ testRemote :: RemoteName
|
|
|
|
, sizeOption :: ByteSize
|
2019-01-17 16:39:29 +00:00
|
|
|
, testReadonlyFile :: [FilePath]
|
2015-07-11 04:42:32 +00:00
|
|
|
}
|
2014-08-01 19:09:49 +00:00
|
|
|
|
2015-07-11 04:42:32 +00:00
|
|
|
optParser :: CmdParamsDesc -> Parser TestRemoteOptions
|
|
|
|
optParser desc = TestRemoteOptions
|
|
|
|
<$> argument str ( metavar desc )
|
|
|
|
<*> option (str >>= maybe (fail "parse error") return . readSize dataUnits)
|
|
|
|
( long "size" <> metavar paramSize
|
|
|
|
<> value (1024 * 1024)
|
|
|
|
<> help "base key size (default 1MiB)"
|
|
|
|
)
|
2019-01-17 16:39:29 +00:00
|
|
|
<*> many testreadonly
|
|
|
|
where
|
|
|
|
testreadonly = option str
|
|
|
|
( long "test-readonly" <> metavar paramFile
|
|
|
|
<> help "readonly test object"
|
|
|
|
)
|
2014-08-01 19:09:49 +00:00
|
|
|
|
2015-07-11 04:42:32 +00:00
|
|
|
seek :: TestRemoteOptions -> CommandSeek
|
2019-01-17 16:39:29 +00:00
|
|
|
seek = commandAction . start
|
2015-07-11 04:42:32 +00:00
|
|
|
|
2019-01-17 16:39:29 +00:00
|
|
|
start :: TestRemoteOptions -> CommandStart
|
2023-04-08 19:48:32 +00:00
|
|
|
start o = starting "testremote" (ActionItemOther (Just (UnquotedString (testRemote o)))) si $ do
|
2022-06-28 19:28:14 +00:00
|
|
|
fast <- Annex.getRead Annex.fast
|
2020-06-22 18:25:49 +00:00
|
|
|
cache <- liftIO newRemoteVariantCache
|
|
|
|
r <- either giveup (disableExportTree cache)
|
|
|
|
=<< Remote.byName' (testRemote o)
|
2019-01-17 16:39:29 +00:00
|
|
|
ks <- case testReadonlyFile o of
|
|
|
|
[] -> if Remote.readonly r
|
|
|
|
then giveup "This remote is readonly, so you need to use the --test-readonly option."
|
|
|
|
else do
|
|
|
|
showAction "generating test keys"
|
|
|
|
mapM randKey (keySizes basesz fast)
|
2023-04-10 16:56:45 +00:00
|
|
|
fs -> mapM (getReadonlyKey r . toRawFilePath) fs
|
2019-01-17 16:39:29 +00:00
|
|
|
let r' = if null (testReadonlyFile o)
|
|
|
|
then r
|
|
|
|
else r { Remote.readonly = True }
|
2020-04-30 17:49:22 +00:00
|
|
|
let drs = if Remote.readonly r'
|
|
|
|
then [Described "remote" (pure (Just r'))]
|
2020-06-22 18:25:49 +00:00
|
|
|
else remoteVariants cache (Described "remote" (pure r')) basesz fast
|
2020-04-30 16:59:20 +00:00
|
|
|
unavailr <- Remote.mkUnavailable r'
|
2020-04-30 17:13:08 +00:00
|
|
|
let exportr = if Remote.readonly r'
|
2019-01-17 16:39:29 +00:00
|
|
|
then return Nothing
|
2020-06-22 18:25:49 +00:00
|
|
|
else exportTreeVariant cache r'
|
2020-04-30 17:49:22 +00:00
|
|
|
perform drs unavailr exportr ks
|
2019-01-17 16:39:29 +00:00
|
|
|
where
|
|
|
|
basesz = fromInteger $ sizeOption o
|
2020-09-14 20:49:33 +00:00
|
|
|
si = SeekInput [testRemote o]
|
2014-08-01 19:09:49 +00:00
|
|
|
|
2020-04-30 17:49:22 +00:00
|
|
|
perform :: [Described (Annex (Maybe Remote))] -> Maybe Remote -> Annex (Maybe Remote) -> [Key] -> CommandPerform
|
|
|
|
perform drs unavailr exportr ks = do
|
2021-04-02 19:26:21 +00:00
|
|
|
st <- liftIO . newTVarIO =<< (,)
|
|
|
|
<$> Annex.getState id
|
|
|
|
<*> Annex.getRead id
|
2020-04-29 19:48:09 +00:00
|
|
|
let tests = testGroup "Remote Tests" $ mkTestTrees
|
2020-04-29 17:16:43 +00:00
|
|
|
(runTestCase st)
|
2020-04-30 17:49:22 +00:00
|
|
|
drs
|
2020-04-30 16:59:20 +00:00
|
|
|
(pure unavailr)
|
2020-04-30 17:13:08 +00:00
|
|
|
exportr
|
2020-04-29 17:16:43 +00:00
|
|
|
(map (\k -> Described (desck k) (pure k)) ks)
|
2014-08-01 19:09:49 +00:00
|
|
|
ok <- case tryIngredients [consoleTestReporter] mempty tests of
|
|
|
|
Nothing -> error "No tests found!?"
|
|
|
|
Just act -> liftIO act
|
2020-04-30 17:49:22 +00:00
|
|
|
rs <- catMaybes <$> mapM getVal drs
|
2014-08-01 20:50:24 +00:00
|
|
|
next $ cleanup rs ks ok
|
2014-08-01 19:09:49 +00:00
|
|
|
where
|
2020-04-29 17:16:43 +00:00
|
|
|
desck k = unwords [ "key size", show (fromKey keySize k) ]
|
2014-08-01 20:50:24 +00:00
|
|
|
|
2020-06-22 18:25:49 +00:00
|
|
|
remoteVariants :: RemoteVariantCache -> Described (Annex Remote) -> Int -> Bool -> [Described (Annex (Maybe Remote))]
|
|
|
|
remoteVariants cache dr basesz fast =
|
|
|
|
concatMap (encryptionVariants cache) $
|
2020-04-30 17:49:22 +00:00
|
|
|
map chunkvariant (chunkSizes basesz fast)
|
|
|
|
where
|
|
|
|
chunkvariant sz = Described (getDesc dr ++ " chunksize=" ++ show sz) $ do
|
|
|
|
r <- getVal dr
|
2020-06-22 18:25:49 +00:00
|
|
|
adjustChunkSize cache r sz
|
2020-04-30 17:49:22 +00:00
|
|
|
|
2020-06-22 18:25:49 +00:00
|
|
|
adjustChunkSize :: RemoteVariantCache -> Remote -> Int -> Annex (Maybe Remote)
|
|
|
|
adjustChunkSize cache r chunksize = adjustRemoteConfig cache r $
|
fix encryption of content to gcrypt and git-lfs
Fix serious regression in gcrypt and encrypted git-lfs remotes.
Since version 7.20200202.7, git-annex incorrectly stored content
on those remotes without encrypting it.
Problem was, Remote.Git enumerates all git remotes, including git-lfs
and gcrypt. It then dispatches to those. So, Remote.List used the
RemoteConfigParser from Remote.Git, instead of from git-lfs or gcrypt,
and that parser does not know about encryption fields, so did not
include them in the ParsedRemoteConfig. (Also didn't include other
fields specific to those remotes, perhaps chunking etc also didn't
get through.)
To fix, had to move RemoteConfig parsing down into the generate methods
of each remote, rather than doing it in Remote.List.
And a consequence of that was that ParsedRemoteConfig had to change to
include the RemoteConfig that got parsed, so that testremote can
generate a new remote based on an existing remote.
(I would have rather fixed this just inside Remote.Git, but that was not
practical, at least not w/o re-doing work that Remote.List already did.
Big ugly mostly mechanical patch seemed preferable to making git-annex
slower.)
2020-02-26 21:20:56 +00:00
|
|
|
M.insert chunkField (Proposed (show chunksize))
|
2014-08-01 21:52:40 +00:00
|
|
|
|
|
|
|
-- Variants of a remote with no encryption, and with simple shared
|
|
|
|
-- encryption. Gpg key based encryption is not tested.
|
2020-06-22 18:25:49 +00:00
|
|
|
encryptionVariants :: RemoteVariantCache -> Described (Annex (Maybe Remote)) -> [Described (Annex (Maybe Remote))]
|
|
|
|
encryptionVariants cache dr = [noenc, sharedenc]
|
2020-04-30 17:49:22 +00:00
|
|
|
where
|
|
|
|
noenc = Described (getDesc dr ++ " encryption=none") $
|
|
|
|
getVal dr >>= \case
|
|
|
|
Nothing -> return Nothing
|
2020-06-22 18:25:49 +00:00
|
|
|
Just r -> adjustRemoteConfig cache r $
|
2020-04-30 17:49:22 +00:00
|
|
|
M.insert encryptionField (Proposed "none")
|
|
|
|
sharedenc = Described (getDesc dr ++ " encryption=shared") $
|
|
|
|
getVal dr >>= \case
|
|
|
|
Nothing -> return Nothing
|
2020-06-22 18:25:49 +00:00
|
|
|
Just r -> adjustRemoteConfig cache r $
|
2020-04-30 17:49:22 +00:00
|
|
|
M.insert encryptionField (Proposed "shared") .
|
|
|
|
M.insert highRandomQualityField (Proposed "false")
|
2014-08-01 21:52:40 +00:00
|
|
|
|
2017-11-08 18:22:05 +00:00
|
|
|
-- Variant of a remote with exporttree disabled.
|
2020-06-22 18:25:49 +00:00
|
|
|
disableExportTree :: RemoteVariantCache -> Remote -> Annex Remote
|
2023-04-10 17:38:14 +00:00
|
|
|
disableExportTree cache r = maybe (giveup "failed disabling exportree") return
|
2020-06-22 18:25:49 +00:00
|
|
|
=<< adjustRemoteConfig cache r (M.delete exportTreeField)
|
2017-11-08 18:22:05 +00:00
|
|
|
|
|
|
|
-- Variant of a remote with exporttree enabled.
|
2020-06-22 18:25:49 +00:00
|
|
|
exportTreeVariant :: RemoteVariantCache -> Remote -> Annex (Maybe Remote)
|
|
|
|
exportTreeVariant cache r = ifM (Remote.isExportSupported r)
|
|
|
|
( adjustRemoteConfig cache r $
|
fix encryption of content to gcrypt and git-lfs
Fix serious regression in gcrypt and encrypted git-lfs remotes.
Since version 7.20200202.7, git-annex incorrectly stored content
on those remotes without encrypting it.
Problem was, Remote.Git enumerates all git remotes, including git-lfs
and gcrypt. It then dispatches to those. So, Remote.List used the
RemoteConfigParser from Remote.Git, instead of from git-lfs or gcrypt,
and that parser does not know about encryption fields, so did not
include them in the ParsedRemoteConfig. (Also didn't include other
fields specific to those remotes, perhaps chunking etc also didn't
get through.)
To fix, had to move RemoteConfig parsing down into the generate methods
of each remote, rather than doing it in Remote.List.
And a consequence of that was that ParsedRemoteConfig had to change to
include the RemoteConfig that got parsed, so that testremote can
generate a new remote based on an existing remote.
(I would have rather fixed this just inside Remote.Git, but that was not
practical, at least not w/o re-doing work that Remote.List already did.
Big ugly mostly mechanical patch seemed preferable to making git-annex
slower.)
2020-02-26 21:20:56 +00:00
|
|
|
M.insert encryptionField (Proposed "none") .
|
|
|
|
M.insert exportTreeField (Proposed "yes")
|
2017-11-08 18:22:05 +00:00
|
|
|
, return Nothing
|
|
|
|
)
|
|
|
|
|
2020-06-22 18:25:49 +00:00
|
|
|
-- The Annex wrapper is used by Test; it should return the same TMVar
|
|
|
|
-- each time run.
|
|
|
|
type RemoteVariantCache = Annex (TVar (M.Map RemoteConfig Remote))
|
|
|
|
|
|
|
|
newRemoteVariantCache :: IO RemoteVariantCache
|
|
|
|
newRemoteVariantCache = newTVarIO M.empty >>= return . pure
|
|
|
|
|
2014-08-01 21:52:40 +00:00
|
|
|
-- Regenerate a remote with a modified config.
|
2020-06-22 18:25:49 +00:00
|
|
|
adjustRemoteConfig :: RemoteVariantCache -> Remote -> (Remote.RemoteConfig -> Remote.RemoteConfig) -> Annex (Maybe Remote)
|
|
|
|
adjustRemoteConfig getcache r adjustconfig = do
|
|
|
|
cache <- getcache
|
|
|
|
m <- liftIO $ atomically $ readTVar cache
|
fix encryption of content to gcrypt and git-lfs
Fix serious regression in gcrypt and encrypted git-lfs remotes.
Since version 7.20200202.7, git-annex incorrectly stored content
on those remotes without encrypting it.
Problem was, Remote.Git enumerates all git remotes, including git-lfs
and gcrypt. It then dispatches to those. So, Remote.List used the
RemoteConfigParser from Remote.Git, instead of from git-lfs or gcrypt,
and that parser does not know about encryption fields, so did not
include them in the ParsedRemoteConfig. (Also didn't include other
fields specific to those remotes, perhaps chunking etc also didn't
get through.)
To fix, had to move RemoteConfig parsing down into the generate methods
of each remote, rather than doing it in Remote.List.
And a consequence of that was that ParsedRemoteConfig had to change to
include the RemoteConfig that got parsed, so that testremote can
generate a new remote based on an existing remote.
(I would have rather fixed this just inside Remote.Git, but that was not
practical, at least not w/o re-doing work that Remote.List already did.
Big ugly mostly mechanical patch seemed preferable to making git-annex
slower.)
2020-02-26 21:20:56 +00:00
|
|
|
let ParsedRemoteConfig _ origc = Remote.config r
|
2020-06-22 18:25:49 +00:00
|
|
|
let newc = adjustconfig origc
|
|
|
|
case M.lookup newc m of
|
|
|
|
Just r' -> return (Just r')
|
|
|
|
Nothing -> do
|
|
|
|
repo <- Remote.getRepo r
|
|
|
|
v <- Remote.generate (Remote.remotetype r)
|
|
|
|
repo
|
|
|
|
(Remote.uuid r)
|
|
|
|
newc
|
|
|
|
(Remote.gitconfig r)
|
|
|
|
(Remote.remoteStateHandle r)
|
|
|
|
case v of
|
|
|
|
Just r' -> liftIO $ atomically $
|
|
|
|
modifyTVar' cache $ M.insert newc r'
|
|
|
|
Nothing -> return ()
|
|
|
|
return v
|
2014-08-01 19:09:49 +00:00
|
|
|
|
2020-04-29 17:16:43 +00:00
|
|
|
data Described t = Described
|
|
|
|
{ getDesc :: String
|
|
|
|
, getVal :: t
|
2021-01-11 17:31:36 +00:00
|
|
|
} deriving Functor
|
2020-04-29 17:16:43 +00:00
|
|
|
|
|
|
|
type RunAnnex = forall a. Annex a -> IO a
|
|
|
|
|
2021-04-02 19:26:21 +00:00
|
|
|
runTestCase :: TVar (Annex.AnnexState, Annex.AnnexRead) -> RunAnnex
|
2020-04-29 17:16:43 +00:00
|
|
|
runTestCase stv a = do
|
|
|
|
st <- atomically $ readTVar stv
|
|
|
|
(r, st') <- Annex.run st $ do
|
|
|
|
Annex.setOutput QuietOutput
|
|
|
|
a
|
|
|
|
atomically $ writeTVar stv st'
|
|
|
|
return r
|
|
|
|
|
|
|
|
-- Note that the same remotes and keys should be produced each time
|
|
|
|
-- the provided actions are called.
|
2020-04-29 19:48:09 +00:00
|
|
|
mkTestTrees
|
2020-04-29 17:16:43 +00:00
|
|
|
:: RunAnnex
|
2020-04-30 17:49:22 +00:00
|
|
|
-> [Described (Annex (Maybe Remote))]
|
2020-04-30 16:59:20 +00:00
|
|
|
-> Annex (Maybe Remote)
|
2020-04-30 17:13:08 +00:00
|
|
|
-> Annex (Maybe Remote)
|
2020-04-29 17:16:43 +00:00
|
|
|
-> [Described (Annex Key)]
|
2020-04-29 19:48:09 +00:00
|
|
|
-> [TestTree]
|
2020-04-30 16:59:20 +00:00
|
|
|
mkTestTrees runannex mkrs mkunavailr mkexportr mkks = concat $
|
|
|
|
[ [ testGroup "unavailable remote" (testUnavailable runannex mkunavailr (getVal (Prelude.head mkks))) ]
|
2020-04-29 17:16:43 +00:00
|
|
|
, [ testGroup (desc mkr mkk) (test runannex (getVal mkr) (getVal mkk)) | mkk <- mkks, mkr <- mkrs ]
|
|
|
|
, [ testGroup (descexport mkk1 mkk2) (testExportTree runannex mkexportr (getVal mkk1) (getVal mkk2)) | mkk1 <- take 2 mkks, mkk2 <- take 2 (reverse mkks) ]
|
|
|
|
]
|
|
|
|
where
|
|
|
|
desc r k = intercalate "; " $ map unwords
|
|
|
|
[ [ getDesc k ]
|
|
|
|
, [ getDesc r ]
|
|
|
|
]
|
|
|
|
descexport k1 k2 = intercalate "; " $ map unwords
|
|
|
|
[ [ "exporttree=yes" ]
|
|
|
|
, [ getDesc k1 ]
|
|
|
|
, [ getDesc k2 ]
|
|
|
|
]
|
|
|
|
|
2020-04-30 17:49:22 +00:00
|
|
|
test :: RunAnnex -> Annex (Maybe Remote) -> Annex Key -> [TestTree]
|
2020-04-29 17:16:43 +00:00
|
|
|
test runannex mkr mkk =
|
|
|
|
[ check "removeKey when not present" $ \r k ->
|
2020-10-23 19:27:45 +00:00
|
|
|
whenwritable r $ runBool (remove r k)
|
2020-04-29 17:16:43 +00:00
|
|
|
, check ("present " ++ show False) $ \r k ->
|
|
|
|
whenwritable r $ present r k False
|
|
|
|
, check "storeKey" $ \r k ->
|
2020-10-23 19:27:45 +00:00
|
|
|
whenwritable r $ runBool (store r k)
|
2020-04-29 17:16:43 +00:00
|
|
|
, check ("present " ++ show True) $ \r k ->
|
|
|
|
whenwritable r $ present r k True
|
|
|
|
, check "storeKey when already present" $ \r k ->
|
2020-10-23 19:27:45 +00:00
|
|
|
whenwritable r $ runBool (store r k)
|
2020-04-29 17:16:43 +00:00
|
|
|
, check ("present " ++ show True) $ \r k -> present r k True
|
|
|
|
, check "retrieveKeyFile" $ \r k -> do
|
2020-07-25 15:54:34 +00:00
|
|
|
lockContentForRemoval k noop removeAnnex
|
2020-04-29 17:16:43 +00:00
|
|
|
get r k
|
|
|
|
, check "fsck downloaded object" fsck
|
2021-04-21 17:01:41 +00:00
|
|
|
, check "retrieveKeyFile resume from 0" $ \r k -> do
|
|
|
|
tmp <- fromRawFilePath <$> prepTmp k
|
|
|
|
liftIO $ writeFile tmp ""
|
|
|
|
lockContentForRemoval k noop removeAnnex
|
|
|
|
get r k
|
|
|
|
, check "fsck downloaded object" fsck
|
2020-04-29 17:16:43 +00:00
|
|
|
, check "retrieveKeyFile resume from 33%" $ \r k -> do
|
2019-12-11 18:12:22 +00:00
|
|
|
loc <- fromRawFilePath <$> Annex.calcRepo (gitAnnexLocation k)
|
2020-11-02 20:31:28 +00:00
|
|
|
tmp <- fromRawFilePath <$> prepTmp k
|
2014-08-01 21:16:20 +00:00
|
|
|
partial <- liftIO $ bracket (openBinaryFile loc ReadMode) hClose $ \h -> do
|
|
|
|
sz <- hFileSize h
|
|
|
|
L.hGet h $ fromInteger $ sz `div` 3
|
|
|
|
liftIO $ L.writeFile tmp partial
|
2020-07-25 15:54:34 +00:00
|
|
|
lockContentForRemoval k noop removeAnnex
|
2020-04-29 17:16:43 +00:00
|
|
|
get r k
|
|
|
|
, check "fsck downloaded object" fsck
|
|
|
|
, check "retrieveKeyFile resume from end" $ \r k -> do
|
2019-12-11 18:12:22 +00:00
|
|
|
loc <- fromRawFilePath <$> Annex.calcRepo (gitAnnexLocation k)
|
2020-11-02 20:31:28 +00:00
|
|
|
tmp <- fromRawFilePath <$> prepTmp k
|
2014-08-27 00:06:43 +00:00
|
|
|
void $ liftIO $ copyFileExternal CopyAllMetaData loc tmp
|
2020-07-25 15:54:34 +00:00
|
|
|
lockContentForRemoval k noop removeAnnex
|
2020-04-29 17:16:43 +00:00
|
|
|
get r k
|
|
|
|
, check "fsck downloaded object" fsck
|
|
|
|
, check "removeKey when present" $ \r k ->
|
2020-10-23 19:27:45 +00:00
|
|
|
whenwritable r $ runBool (remove r k)
|
2020-04-29 17:16:43 +00:00
|
|
|
, check ("present " ++ show False) $ \r k ->
|
|
|
|
whenwritable r $ present r k False
|
2014-08-01 19:09:49 +00:00
|
|
|
]
|
|
|
|
where
|
2020-04-29 17:16:43 +00:00
|
|
|
whenwritable r a
|
|
|
|
| Remote.readonly r = return True
|
|
|
|
| otherwise = a
|
|
|
|
check desc a = testCase desc $ do
|
2020-04-30 17:49:22 +00:00
|
|
|
let a' = mkr >>= \case
|
|
|
|
Just r -> do
|
|
|
|
k <- mkk
|
|
|
|
a r k
|
|
|
|
Nothing -> return True
|
2020-04-29 17:16:43 +00:00
|
|
|
runannex a' @? "failed"
|
|
|
|
present r k b = (== Right b) <$> Remote.hasKey r k
|
2020-07-29 19:23:18 +00:00
|
|
|
fsck _ k = maybeLookupBackendVariety (fromKey keyVariety k) >>= \case
|
2014-08-01 21:16:20 +00:00
|
|
|
Nothing -> return True
|
2020-07-20 18:06:05 +00:00
|
|
|
Just b -> case Types.Backend.verifyKeyContent b of
|
2014-08-01 21:16:20 +00:00
|
|
|
Nothing -> return True
|
2024-05-15 21:57:27 +00:00
|
|
|
Just verifier -> do
|
|
|
|
loc <- Annex.calcRepo (gitAnnexLocation k)
|
|
|
|
verifier k loc
|
disk free checking for unsized keys
Improve disk free space checking when transferring unsized keys to
local git remotes. Since the size of the object file is known, can
check that instead.
Getting unsized keys from local git remotes does not check the actual
object size. It would be harder to handle that direction because the size
check is run locally, before anything involving the remote is done. So it
doesn't know the size of the file on the remote.
Also, transferring unsized keys to other remotes, including ssh remotes and
p2p remotes don't do disk size checking for unsized keys. This would need a
change in protocol.
(It does seem like it would be possible to implement the same thing for
directory special remotes though.)
In some sense, it might be better to not ever do disk free checking for
unsized keys, than to do it only sometimes. A user might notice this
direction working and consider it a bug that the other direction does not.
On the other hand, disk reserve checking is not implemented for most
special remotes at all, and yet it is implemented for a few, which is also
inconsistent, but best effort. And so doing this best effort seems to make
some sense. Fundamentally, if the user wants the size to always be checked,
they should not use unsized keys.
Sponsored-by: Brock Spratlen on Patreon
2024-01-16 18:29:10 +00:00
|
|
|
get r k = logStatusAfter k $ getViaTmp (Remote.retrievalSecurityPolicy r) (RemoteVerify r) k (AssociatedFile Nothing) Nothing $ \dest ->
|
2021-08-17 16:41:36 +00:00
|
|
|
tryNonAsync (Remote.retrieveKeyFile r k (AssociatedFile Nothing) (fromRawFilePath dest) nullMeterUpdate (RemoteVerify r)) >>= \case
|
2020-05-13 21:05:56 +00:00
|
|
|
Right v -> return (True, v)
|
|
|
|
Left _ -> return (False, UnVerified)
|
2024-07-01 14:42:27 +00:00
|
|
|
store r k = Remote.storeKey r k (AssociatedFile Nothing) Nothing nullMeterUpdate
|
toward SafeDropProof expiry checking
Added Maybe POSIXTime to SafeDropProof, which gets set when the proof is
based on a LockedCopy. If there are several LockedCopies, it uses the
closest expiry time. That is not optimal, it may be that the proof
expires based on one LockedCopy but another one has not expired. But
that seems unlikely to really happen, and anyway the user can just
re-run a drop if it fails due to expiry.
Pass the SafeDropProof to removeKey, which is responsible for checking
it for expiry in situations where that could be a problem. Which really
only means in Remote.Git.
Made Remote.Git check expiry when dropping from a local remote.
Checking expiry when dropping from a P2P remote is not yet implemented.
P2P.Protocol.remove has SafeDropProof plumbed through to it for that
purpose.
Fixing the remaining 2 build warnings should complete this work.
Note that the use of a POSIXTime here means that if the clock gets set
forward while git-annex is in the middle of a drop, it may say that
dropping took too long. That seems ok. Less ok is that if the clock gets
turned back a sufficient amount (eg 5 minutes), proof expiry won't be
noticed. It might be better to use the Monotonic clock, but that doesn't
advance when a laptop is suspended, and while there is the linux
Boottime clock, that is not available on other systems. Perhaps a
combination of POSIXTime and the Monotonic clock could detect laptop
suspension and also detect clock being turned back?
There is a potential future flag day where
p2pDefaultLockContentRetentionDuration is not assumed, but is probed
using the P2P protocol, and peers that don't support it can no longer
produce a LockedCopy. Until that happens, when git-annex is
communicating with older peers there is a risk of data loss when
a ssh connection closes during LOCKCONTENT.
2024-07-04 16:23:46 +00:00
|
|
|
remove r k = Remote.removeKey r Nothing k
|
2020-04-28 21:19:07 +00:00
|
|
|
|
2020-04-30 17:13:08 +00:00
|
|
|
testExportTree :: RunAnnex -> Annex (Maybe Remote) -> Annex Key -> Annex Key -> [TestTree]
|
|
|
|
testExportTree runannex mkr mkk1 mkk2 =
|
2020-04-29 17:16:43 +00:00
|
|
|
[ check "check present export when not present" $ \ea k1 _k2 ->
|
|
|
|
not <$> checkpresentexport ea k1
|
|
|
|
, check "remove export when not present" $ \ea k1 _k2 ->
|
2020-10-23 19:27:45 +00:00
|
|
|
runBool (removeexport ea k1)
|
2020-04-29 17:16:43 +00:00
|
|
|
, check "store export" $ \ea k1 _k2 ->
|
2020-10-23 19:27:45 +00:00
|
|
|
runBool (storeexport ea k1)
|
2020-04-29 17:16:43 +00:00
|
|
|
, check "check present export after store" $ \ea k1 _k2 ->
|
|
|
|
checkpresentexport ea k1
|
|
|
|
, check "store export when already present" $ \ea k1 _k2 ->
|
2020-10-23 19:27:45 +00:00
|
|
|
runBool (storeexport ea k1)
|
2020-04-29 17:16:43 +00:00
|
|
|
, check "retrieve export" $ \ea k1 _k2 ->
|
|
|
|
retrieveexport ea k1
|
|
|
|
, check "store new content to export" $ \ea _k1 k2 ->
|
2020-10-23 19:27:45 +00:00
|
|
|
runBool (storeexport ea k2)
|
2020-04-29 17:16:43 +00:00
|
|
|
, check "check present export after store of new content" $ \ea _k1 k2 ->
|
|
|
|
checkpresentexport ea k2
|
|
|
|
, check "retrieve export new content" $ \ea _k1 k2 ->
|
|
|
|
retrieveexport ea k2
|
|
|
|
, check "remove export" $ \ea _k1 k2 ->
|
2020-10-23 19:27:45 +00:00
|
|
|
runBool (removeexport ea k2)
|
2020-04-29 17:16:43 +00:00
|
|
|
, check "check present export after remove" $ \ea _k1 k2 ->
|
|
|
|
not <$> checkpresentexport ea k2
|
|
|
|
, check "retrieve export fails after removal" $ \ea _k1 k2 ->
|
|
|
|
not <$> retrieveexport ea k2
|
|
|
|
, check "remove export directory" $ \ea _k1 _k2 ->
|
2020-10-23 19:27:45 +00:00
|
|
|
runBool (removeexportdirectory ea)
|
2020-04-29 17:16:43 +00:00
|
|
|
, check "remove export directory that is already removed" $ \ea _k1 _k2 ->
|
2020-10-23 19:27:45 +00:00
|
|
|
runBool (removeexportdirectory ea)
|
2017-11-08 18:22:05 +00:00
|
|
|
-- renames are not tested because remotes do not need to support them
|
|
|
|
]
|
|
|
|
where
|
|
|
|
testexportdirectory = "testremote-export"
|
2019-12-05 18:36:43 +00:00
|
|
|
testexportlocation = mkExportLocation (toRawFilePath (testexportdirectory </> "location"))
|
2020-04-29 17:16:43 +00:00
|
|
|
check desc a = testCase desc $ do
|
2020-04-30 17:13:08 +00:00
|
|
|
let a' = mkr >>= \case
|
|
|
|
Just r -> do
|
|
|
|
let ea = Remote.exportActions r
|
|
|
|
k1 <- mkk1
|
|
|
|
k2 <- mkk2
|
|
|
|
a ea k1 k2
|
|
|
|
Nothing -> return True
|
2020-04-29 17:16:43 +00:00
|
|
|
runannex a' @? "failed"
|
|
|
|
storeexport ea k = do
|
2019-12-11 18:12:22 +00:00
|
|
|
loc <- fromRawFilePath <$> Annex.calcRepo (gitAnnexLocation k)
|
2017-11-08 18:22:05 +00:00
|
|
|
Remote.storeExport ea loc k testexportlocation nullMeterUpdate
|
2020-04-29 17:16:43 +00:00
|
|
|
retrieveexport ea k = withTmpFile "exported" $ \tmp h -> do
|
2017-11-08 18:22:05 +00:00
|
|
|
liftIO $ hClose h
|
2020-05-15 16:51:09 +00:00
|
|
|
tryNonAsync (Remote.retrieveExport ea k testexportlocation tmp nullMeterUpdate) >>= \case
|
|
|
|
Left _ -> return False
|
2022-05-09 16:25:04 +00:00
|
|
|
Right v -> verifyKeyContentPostRetrieval RetrievalAllKeysSecure AlwaysVerify v k (toRawFilePath tmp)
|
2020-04-29 17:16:43 +00:00
|
|
|
checkpresentexport ea k = Remote.checkPresentExport ea k testexportlocation
|
|
|
|
removeexport ea k = Remote.removeExport ea k testexportlocation
|
|
|
|
removeexportdirectory ea = case Remote.removeExportDirectory ea of
|
2019-12-05 18:36:43 +00:00
|
|
|
Just a -> a (mkExportDirectory (toRawFilePath testexportdirectory))
|
2020-05-15 18:32:45 +00:00
|
|
|
Nothing -> noop
|
2017-11-08 18:22:05 +00:00
|
|
|
|
2020-04-30 16:59:20 +00:00
|
|
|
testUnavailable :: RunAnnex -> Annex (Maybe Remote) -> Annex Key -> [TestTree]
|
2020-04-29 17:16:43 +00:00
|
|
|
testUnavailable runannex mkr mkk =
|
2020-05-14 18:08:09 +00:00
|
|
|
[ check isLeft "removeKey" $ \r k ->
|
toward SafeDropProof expiry checking
Added Maybe POSIXTime to SafeDropProof, which gets set when the proof is
based on a LockedCopy. If there are several LockedCopies, it uses the
closest expiry time. That is not optimal, it may be that the proof
expires based on one LockedCopy but another one has not expired. But
that seems unlikely to really happen, and anyway the user can just
re-run a drop if it fails due to expiry.
Pass the SafeDropProof to removeKey, which is responsible for checking
it for expiry in situations where that could be a problem. Which really
only means in Remote.Git.
Made Remote.Git check expiry when dropping from a local remote.
Checking expiry when dropping from a P2P remote is not yet implemented.
P2P.Protocol.remove has SafeDropProof plumbed through to it for that
purpose.
Fixing the remaining 2 build warnings should complete this work.
Note that the use of a POSIXTime here means that if the clock gets set
forward while git-annex is in the middle of a drop, it may say that
dropping took too long. That seems ok. Less ok is that if the clock gets
turned back a sufficient amount (eg 5 minutes), proof expiry won't be
noticed. It might be better to use the Monotonic clock, but that doesn't
advance when a laptop is suspended, and while there is the linux
Boottime clock, that is not available on other systems. Perhaps a
combination of POSIXTime and the Monotonic clock could detect laptop
suspension and also detect clock being turned back?
There is a potential future flag day where
p2pDefaultLockContentRetentionDuration is not assumed, but is probed
using the P2P protocol, and peers that don't support it can no longer
produce a LockedCopy. Until that happens, when git-annex is
communicating with older peers there is a risk of data loss when
a ssh connection closes during LOCKCONTENT.
2024-07-04 16:23:46 +00:00
|
|
|
Remote.removeKey r Nothing k
|
2020-05-13 18:03:00 +00:00
|
|
|
, check isLeft "storeKey" $ \r k ->
|
2024-07-01 14:42:27 +00:00
|
|
|
Remote.storeKey r k (AssociatedFile Nothing) Nothing nullMeterUpdate
|
2020-04-29 17:16:43 +00:00
|
|
|
, check (`notElem` [Right True, Right False]) "checkPresent" $ \r k ->
|
2014-08-10 18:52:58 +00:00
|
|
|
Remote.checkPresent r k
|
2020-04-29 17:16:43 +00:00
|
|
|
, check (== Right False) "retrieveKeyFile" $ \r k ->
|
disk free checking for unsized keys
Improve disk free space checking when transferring unsized keys to
local git remotes. Since the size of the object file is known, can
check that instead.
Getting unsized keys from local git remotes does not check the actual
object size. It would be harder to handle that direction because the size
check is run locally, before anything involving the remote is done. So it
doesn't know the size of the file on the remote.
Also, transferring unsized keys to other remotes, including ssh remotes and
p2p remotes don't do disk size checking for unsized keys. This would need a
change in protocol.
(It does seem like it would be possible to implement the same thing for
directory special remotes though.)
In some sense, it might be better to not ever do disk free checking for
unsized keys, than to do it only sometimes. A user might notice this
direction working and consider it a bug that the other direction does not.
On the other hand, disk reserve checking is not implemented for most
special remotes at all, and yet it is implemented for a few, which is also
inconsistent, but best effort. And so doing this best effort seems to make
some sense. Fundamentally, if the user wants the size to always be checked,
they should not use unsized keys.
Sponsored-by: Brock Spratlen on Patreon
2024-01-16 18:29:10 +00:00
|
|
|
logStatusAfter k $ getViaTmp (Remote.retrievalSecurityPolicy r) (RemoteVerify r) k (AssociatedFile Nothing) Nothing $ \dest ->
|
2021-08-17 16:41:36 +00:00
|
|
|
tryNonAsync (Remote.retrieveKeyFile r k (AssociatedFile Nothing) (fromRawFilePath dest) nullMeterUpdate (RemoteVerify r)) >>= \case
|
2020-05-13 21:05:56 +00:00
|
|
|
Right v -> return (True, v)
|
|
|
|
Left _ -> return (False, UnVerified)
|
|
|
|
, check (== Right False) "retrieveKeyFileCheap" $ \r k -> case Remote.retrieveKeyFileCheap r of
|
|
|
|
Nothing -> return False
|
disk free checking for unsized keys
Improve disk free space checking when transferring unsized keys to
local git remotes. Since the size of the object file is known, can
check that instead.
Getting unsized keys from local git remotes does not check the actual
object size. It would be harder to handle that direction because the size
check is run locally, before anything involving the remote is done. So it
doesn't know the size of the file on the remote.
Also, transferring unsized keys to other remotes, including ssh remotes and
p2p remotes don't do disk size checking for unsized keys. This would need a
change in protocol.
(It does seem like it would be possible to implement the same thing for
directory special remotes though.)
In some sense, it might be better to not ever do disk free checking for
unsized keys, than to do it only sometimes. A user might notice this
direction working and consider it a bug that the other direction does not.
On the other hand, disk reserve checking is not implemented for most
special remotes at all, and yet it is implemented for a few, which is also
inconsistent, but best effort. And so doing this best effort seems to make
some sense. Fundamentally, if the user wants the size to always be checked,
they should not use unsized keys.
Sponsored-by: Brock Spratlen on Patreon
2024-01-16 18:29:10 +00:00
|
|
|
Just a -> logStatusAfter k $ getViaTmp (Remote.retrievalSecurityPolicy r) (RemoteVerify r) k (AssociatedFile Nothing) Nothing $ \dest ->
|
2020-05-13 21:05:56 +00:00
|
|
|
unVerified $ isRight
|
2020-11-02 20:31:28 +00:00
|
|
|
<$> tryNonAsync (a k (AssociatedFile Nothing) (fromRawFilePath dest))
|
2014-08-10 18:52:58 +00:00
|
|
|
]
|
|
|
|
where
|
2020-04-30 16:59:20 +00:00
|
|
|
check checkval desc a = testCase desc $
|
|
|
|
join $ runannex $ mkr >>= \case
|
|
|
|
Just r -> do
|
|
|
|
k <- mkk
|
|
|
|
v <- either (Left . show) Right
|
|
|
|
<$> tryNonAsync (a r k)
|
|
|
|
return $ checkval v
|
|
|
|
@? ("(got: " ++ show v ++ ")")
|
|
|
|
Nothing -> return noop
|
2014-08-10 18:52:58 +00:00
|
|
|
|
2014-08-01 20:50:24 +00:00
|
|
|
cleanup :: [Remote] -> [Key] -> Bool -> CommandCleanup
|
2019-01-17 16:39:29 +00:00
|
|
|
cleanup rs ks ok
|
|
|
|
| all Remote.readonly rs = return ok
|
|
|
|
| otherwise = do
|
toward SafeDropProof expiry checking
Added Maybe POSIXTime to SafeDropProof, which gets set when the proof is
based on a LockedCopy. If there are several LockedCopies, it uses the
closest expiry time. That is not optimal, it may be that the proof
expires based on one LockedCopy but another one has not expired. But
that seems unlikely to really happen, and anyway the user can just
re-run a drop if it fails due to expiry.
Pass the SafeDropProof to removeKey, which is responsible for checking
it for expiry in situations where that could be a problem. Which really
only means in Remote.Git.
Made Remote.Git check expiry when dropping from a local remote.
Checking expiry when dropping from a P2P remote is not yet implemented.
P2P.Protocol.remove has SafeDropProof plumbed through to it for that
purpose.
Fixing the remaining 2 build warnings should complete this work.
Note that the use of a POSIXTime here means that if the clock gets set
forward while git-annex is in the middle of a drop, it may say that
dropping took too long. That seems ok. Less ok is that if the clock gets
turned back a sufficient amount (eg 5 minutes), proof expiry won't be
noticed. It might be better to use the Monotonic clock, but that doesn't
advance when a laptop is suspended, and while there is the linux
Boottime clock, that is not available on other systems. Perhaps a
combination of POSIXTime and the Monotonic clock could detect laptop
suspension and also detect clock being turned back?
There is a potential future flag day where
p2pDefaultLockContentRetentionDuration is not assumed, but is probed
using the P2P protocol, and peers that don't support it can no longer
produce a LockedCopy. Until that happens, when git-annex is
communicating with older peers there is a risk of data loss when
a ssh connection closes during LOCKCONTENT.
2024-07-04 16:23:46 +00:00
|
|
|
forM_ rs $ \r -> forM_ ks (Remote.removeKey r Nothing)
|
2020-07-25 15:54:34 +00:00
|
|
|
forM_ ks $ \k -> lockContentForRemoval k noop removeAnnex
|
2019-01-17 16:39:29 +00:00
|
|
|
return ok
|
2014-08-01 19:09:49 +00:00
|
|
|
|
2014-08-03 22:08:34 +00:00
|
|
|
chunkSizes :: Int -> Bool -> [Int]
|
|
|
|
chunkSizes base False =
|
2014-08-01 20:50:24 +00:00
|
|
|
[ 0 -- no chunking
|
|
|
|
, base `div` 100
|
|
|
|
, base `div` 1000
|
|
|
|
, base
|
|
|
|
]
|
2014-08-04 12:24:06 +00:00
|
|
|
chunkSizes _ True =
|
2014-08-03 22:08:34 +00:00
|
|
|
[ 0
|
|
|
|
]
|
2014-08-01 20:50:24 +00:00
|
|
|
|
2014-08-03 22:08:34 +00:00
|
|
|
keySizes :: Int -> Bool -> [Int]
|
|
|
|
keySizes base fast = filter want
|
2014-08-01 19:09:49 +00:00
|
|
|
[ 0 -- empty key is a special case when chunking
|
2014-08-01 20:50:24 +00:00
|
|
|
, base
|
|
|
|
, base + 1
|
|
|
|
, base - 1
|
|
|
|
, base * 2
|
2014-08-01 19:09:49 +00:00
|
|
|
]
|
2014-08-03 22:08:34 +00:00
|
|
|
where
|
|
|
|
want sz
|
|
|
|
| fast = sz <= base && sz > 0
|
|
|
|
| otherwise = sz > 0
|
2014-08-01 19:09:49 +00:00
|
|
|
|
|
|
|
randKey :: Int -> Annex Key
|
|
|
|
randKey sz = withTmpFile "randkey" $ \f h -> do
|
|
|
|
gen <- liftIO (newGenIO :: IO SystemRandom)
|
|
|
|
case genBytes sz gen of
|
2020-05-15 16:51:09 +00:00
|
|
|
Left e -> giveup $ "failed to generate random key: " ++ show e
|
2014-08-01 19:09:49 +00:00
|
|
|
Right (rand, _) -> liftIO $ B.hPut h rand
|
|
|
|
liftIO $ hClose h
|
|
|
|
let ks = KeySource
|
2020-02-21 13:34:59 +00:00
|
|
|
{ keyFilename = toRawFilePath f
|
|
|
|
, contentLocation = toRawFilePath f
|
2014-08-01 19:09:49 +00:00
|
|
|
, inodeCache = Nothing
|
|
|
|
}
|
2020-07-20 18:06:05 +00:00
|
|
|
k <- case Types.Backend.genKey Backend.Hash.testKeyBackend of
|
2020-05-15 16:51:09 +00:00
|
|
|
Just a -> a ks nullMeterUpdate
|
|
|
|
Nothing -> giveup "failed to generate random key (backend problem)"
|
2020-11-16 18:09:55 +00:00
|
|
|
_ <- moveAnnex k (AssociatedFile Nothing) (toRawFilePath f)
|
2014-08-01 19:09:49 +00:00
|
|
|
return k
|
2019-01-17 16:39:29 +00:00
|
|
|
|
2023-04-10 16:56:45 +00:00
|
|
|
getReadonlyKey :: Remote -> RawFilePath -> Annex Key
|
|
|
|
getReadonlyKey r f = do
|
|
|
|
qp <- coreQuotePath <$> Annex.getGitConfig
|
|
|
|
lookupKey f >>= \case
|
|
|
|
Nothing -> giveup $ decodeBS $ quote qp $ QuotedPath f <> " is not an annexed file"
|
|
|
|
Just k -> do
|
|
|
|
unlessM (inAnnex k) $
|
|
|
|
giveup $ decodeBS $ quote qp $ QuotedPath f <> " does not have its content locally present, cannot test it"
|
|
|
|
unlessM ((Remote.uuid r `elem`) <$> loggedLocations k) $
|
|
|
|
giveup $ decodeBS $ quote qp $ QuotedPath f <> " is not stored in the remote being tested, cannot test it"
|
|
|
|
return k
|
2020-10-23 19:27:45 +00:00
|
|
|
|
|
|
|
runBool :: Monad m => m () -> m Bool
|
|
|
|
runBool a = do
|
|
|
|
a
|
|
|
|
return True
|
|
|
|
|