2026-07-19 23:57:21 +09:00
|
|
|
module Main (main) where
|
|
|
|
|
|
|
|
|
|
import Control.Concurrent (threadDelay)
|
|
|
|
|
import Control.Concurrent.Async (concurrently)
|
|
|
|
|
import Control.Exception (SomeException, try)
|
|
|
|
|
import Control.Monad (replicateM_, unless)
|
|
|
|
|
import Crypto.Hash
|
|
|
|
|
( Digest
|
|
|
|
|
, SHA256
|
|
|
|
|
, hashFinalize
|
|
|
|
|
, hashInit
|
|
|
|
|
, hashUpdate
|
|
|
|
|
)
|
|
|
|
|
import qualified Data.ByteArray as BA
|
|
|
|
|
import qualified Data.ByteString as BS
|
|
|
|
|
import qualified Data.ByteString.Base16 as B16
|
|
|
|
|
import qualified Data.ByteString.Char8 as B8
|
|
|
|
|
import System.Directory (getFileSize)
|
|
|
|
|
import System.Environment (getArgs)
|
|
|
|
|
import System.Exit (ExitCode (..))
|
|
|
|
|
import System.IO
|
|
|
|
|
( IOMode (..)
|
|
|
|
|
, hClose
|
|
|
|
|
, withBinaryFile
|
|
|
|
|
)
|
|
|
|
|
import System.IO.Temp
|
|
|
|
|
( withSystemTempDirectory
|
|
|
|
|
, withSystemTempFile
|
|
|
|
|
)
|
|
|
|
|
import System.Process (readProcessWithExitCode)
|
|
|
|
|
import Text.Read (readMaybe)
|
|
|
|
|
import Nuthatch.Probe.Assert
|
|
|
|
|
import Nuthatch.Probe.Curl
|
2026-07-20 23:06:01 +09:00
|
|
|
import Nuthatch.Probe.Http
|
2026-07-19 23:57:21 +09:00
|
|
|
import Nuthatch.Probe.Oidc
|
|
|
|
|
import Nuthatch.Probe.ProductClient
|
|
|
|
|
|
|
|
|
|
main :: IO ()
|
|
|
|
|
main =
|
|
|
|
|
withSystemTempDirectory "nuthatch-streaming-probe" $ \directory -> do
|
2026-07-20 23:06:01 +09:00
|
|
|
context <- newProbeContext
|
2026-07-19 23:57:21 +09:00
|
|
|
user <- requireEnv "NUTHATCH_OIDC_USER_A"
|
|
|
|
|
session <- login context False user "streaming-a"
|
|
|
|
|
getArgs >>= \case
|
|
|
|
|
["large"] -> verifyLarge context session directory
|
|
|
|
|
["throttle"] -> verifyThrottle context session directory
|
|
|
|
|
["refill"] -> verifyRefill context session directory
|
|
|
|
|
["unavailable"] -> verifyUnavailable context session
|
|
|
|
|
_ -> fail "expected large, throttle, refill, or unavailable mode"
|
|
|
|
|
|
|
|
|
|
verifyLarge :: ProbeContext -> Session -> FilePath -> IO ()
|
|
|
|
|
verifyLarge context session directory = do
|
|
|
|
|
largeBytes <- readEnvInteger "NUTHATCH_STREAMING_LARGE_BYTES"
|
|
|
|
|
overLimit <- readEnvInteger "NUTHATCH_STREAMING_OVER_LIMIT_BYTES"
|
|
|
|
|
let largePath = directory <> "/large.bin"
|
|
|
|
|
downloadPath = directory <> "/large-download.bin"
|
|
|
|
|
writeZeros largePath largeBytes
|
|
|
|
|
largeDigest <- hashFile largePath
|
|
|
|
|
uploaded <- uploadFileDeclared
|
|
|
|
|
context session "streaming-large.bin"
|
|
|
|
|
"70000000-0000-4000-8000-000000000001"
|
2026-07-20 23:06:01 +09:00
|
|
|
largeBytes largeDigest largePath defaultRequestOptions
|
2026-07-19 23:57:21 +09:00
|
|
|
requireSucceeded "large streaming upload" uploaded
|
2026-07-20 23:06:01 +09:00
|
|
|
downloaded <- downloadTo
|
|
|
|
|
(probeClient context)
|
|
|
|
|
defaultRequestOptions
|
|
|
|
|
{ optionCookies = Just (sessionCookies session) }
|
|
|
|
|
downloadPath
|
|
|
|
|
(probeBase context
|
|
|
|
|
<> "/api/download?root=personal&path=streaming-large.bin")
|
|
|
|
|
assertEqual "large streaming download status" 200 (probeStatus downloaded)
|
2026-07-19 23:57:21 +09:00
|
|
|
hashFile downloadPath >>= assertEqual "large download digest" largeDigest
|
|
|
|
|
let tiny = "x"
|
|
|
|
|
mapM_
|
|
|
|
|
(\label -> do
|
|
|
|
|
replay <- uploadBytesDeclared
|
|
|
|
|
context session "streaming-large.bin"
|
|
|
|
|
"70000000-0000-4000-8000-000000000007"
|
2026-07-20 23:06:01 +09:00
|
|
|
largeBytes largeDigest tiny defaultRequestOptions
|
2026-07-19 23:57:21 +09:00
|
|
|
requireSucceeded ("prior/desired observation " <> label) replay)
|
|
|
|
|
["first", "replay"]
|
|
|
|
|
sizeMismatch <- uploadBytesDeclared
|
|
|
|
|
context session "size-mismatch.bin"
|
|
|
|
|
"70000000-0000-4000-8000-000000000002"
|
2026-07-20 23:06:01 +09:00
|
|
|
2 (sha256Hex tiny) tiny defaultRequestOptions
|
2026-07-19 23:57:21 +09:00
|
|
|
requireError "size mismatch" 400 "size_mismatch" sizeMismatch
|
|
|
|
|
identityMismatch <- uploadBytesDeclared
|
|
|
|
|
context session "identity-mismatch.bin"
|
|
|
|
|
"70000000-0000-4000-8000-000000000003"
|
2026-07-20 23:06:01 +09:00
|
|
|
1 (BS.replicate 64 48) tiny defaultRequestOptions
|
2026-07-19 23:57:21 +09:00
|
|
|
requireError "content identity mismatch"
|
|
|
|
|
400 "content_identity_mismatch" identityMismatch
|
|
|
|
|
tooLarge <- uploadFileDeclared
|
|
|
|
|
context session "too-large.bin"
|
|
|
|
|
"70000000-0000-4000-8000-000000000004"
|
|
|
|
|
overLimit (sha256Hex tiny) largePath
|
2026-07-20 23:06:01 +09:00
|
|
|
defaultRequestOptions { optionTimeoutMicros = Just 5000000 }
|
2026-07-19 23:57:21 +09:00
|
|
|
requireError "object size limit" 413 "object_too_large" tooLarge
|
|
|
|
|
aborted <- interruptedUpload
|
|
|
|
|
context session "aborted-upload.bin"
|
|
|
|
|
"70000000-0000-4000-8000-000000000005"
|
|
|
|
|
largeBytes largeDigest largePath
|
|
|
|
|
assert "aborted upload unexpectedly completed" (not aborted)
|
|
|
|
|
missing <- download context session "personal" "aborted-upload.bin"
|
2026-07-20 23:06:01 +09:00
|
|
|
assertEqual "aborted upload visibility" 404 (probeStatus missing)
|
2026-07-19 23:57:21 +09:00
|
|
|
replayAfterAbort <- uploadBytesDeclared
|
|
|
|
|
context session "aborted-upload.bin"
|
|
|
|
|
"70000000-0000-4000-8000-000000000005"
|
2026-07-20 23:06:01 +09:00
|
|
|
largeBytes largeDigest tiny defaultRequestOptions
|
|
|
|
|
assertEqual "aborted upload replay status" 200 (probeStatus replayAfterAbort)
|
2026-07-19 23:57:21 +09:00
|
|
|
assert "aborted upload replay lost active or failed identity" $
|
2026-07-20 23:06:01 +09:00
|
|
|
any (`BS.isInfixOf` probeBody replayAfterAbort)
|
2026-07-19 23:57:21 +09:00
|
|
|
["\"result\":\"pending\"", "\"result\":\"failed\""]
|
|
|
|
|
abortedOverwrite <- interruptedUpload
|
|
|
|
|
context session "streaming-large.bin"
|
|
|
|
|
"70000000-0000-4000-8000-000000000006"
|
|
|
|
|
largeBytes (BS.replicate 64 48) largePath
|
|
|
|
|
assert "aborted overwrite unexpectedly completed" (not abortedOverwrite)
|
|
|
|
|
preservedPath <- pure (directory <> "/preserved.bin")
|
2026-07-20 23:06:01 +09:00
|
|
|
preserved <- downloadTo
|
|
|
|
|
(probeClient context)
|
|
|
|
|
defaultRequestOptions
|
|
|
|
|
{ optionCookies = Just (sessionCookies session) }
|
|
|
|
|
preservedPath
|
|
|
|
|
(probeBase context
|
|
|
|
|
<> "/api/download?root=personal&path=streaming-large.bin")
|
|
|
|
|
assertEqual "preserved object status" 200 (probeStatus preserved)
|
2026-07-19 23:57:21 +09:00
|
|
|
hashFile preservedPath >>= assertEqual "preserved object digest" largeDigest
|
|
|
|
|
let partialPath = directory <> "/partial-download.bin"
|
|
|
|
|
partial <- interruptedDownload context session partialPath
|
|
|
|
|
assert "aborted download unexpectedly completed" (not partial)
|
|
|
|
|
partialBytes <- getFileSize partialPath
|
|
|
|
|
assert "aborted download emitted no bytes" (partialBytes > 0)
|
|
|
|
|
assert "aborted download emitted the complete object" $
|
|
|
|
|
partialBytes < largeBytes
|
2026-07-20 23:06:01 +09:00
|
|
|
ready <- get (probeClient context) (probeBase context <> "/readyz")
|
2026-07-19 23:57:21 +09:00
|
|
|
requireStatus "readiness after aborts" 200 ready
|
2026-07-20 23:06:01 +09:00
|
|
|
assertContains "readiness after aborts" "\"ready\":true" (probeBody ready)
|
2026-07-19 23:57:21 +09:00
|
|
|
putStrLn "Large streaming and abort behavior passed"
|
|
|
|
|
|
|
|
|
|
verifyThrottle :: ProbeContext -> Session -> FilePath -> IO ()
|
|
|
|
|
verifyThrottle context session directory = do
|
|
|
|
|
userB <- requireEnv "NUTHATCH_OIDC_USER_B"
|
|
|
|
|
sessionB <- login context False userB "streaming-b"
|
|
|
|
|
let jointPath = directory <> "/joint-upload.bin"
|
|
|
|
|
throttlePath = directory <> "/throttle-upload.bin"
|
|
|
|
|
writeZeros jointPath 65536
|
|
|
|
|
jointDigest <- hashFile jointPath
|
2026-07-20 23:06:01 +09:00
|
|
|
joint <- curlUploadFileDeclared
|
2026-07-19 23:57:21 +09:00
|
|
|
context session "joint-throttle.bin"
|
|
|
|
|
"70000000-0000-4000-8000-000000000013"
|
2026-07-20 23:06:01 +09:00
|
|
|
65536 jointDigest jointPath
|
2026-07-19 23:57:21 +09:00
|
|
|
requireSucceeded "joint throttle setup" joint
|
|
|
|
|
writeZeros throttlePath 1048576
|
|
|
|
|
uploadDigest <- hashFile throttlePath
|
2026-07-20 23:06:01 +09:00
|
|
|
throttled <- curlUploadFileDeclared
|
2026-07-19 23:57:21 +09:00
|
|
|
context sessionB "throttled-upload.bin"
|
|
|
|
|
"71000000-0000-4000-8000-000000000001"
|
2026-07-20 23:06:01 +09:00
|
|
|
1048576 uploadDigest throttlePath
|
2026-07-19 23:57:21 +09:00
|
|
|
requireError "incremental upload throttle"
|
|
|
|
|
429 "bandwidth_exceeded" throttled
|
2026-07-20 23:06:01 +09:00
|
|
|
prestart <- curlUploadBytesDeclared
|
2026-07-19 23:57:21 +09:00
|
|
|
context sessionB "prestart.bin"
|
|
|
|
|
"71000000-0000-4000-8000-000000000002"
|
2026-08-15 21:04:55 +09:00
|
|
|
65536 uploadDigest "x"
|
2026-07-20 23:06:01 +09:00
|
|
|
assertEqual
|
|
|
|
|
("pre-start upload throttle: " <> B8.unpack (probeBody prestart))
|
|
|
|
|
429
|
|
|
|
|
(probeStatus prestart)
|
2026-07-19 23:57:21 +09:00
|
|
|
let firstPath = directory <> "/parallel-a"
|
|
|
|
|
secondPath = directory <> "/parallel-b"
|
2026-07-20 23:06:01 +09:00
|
|
|
downloadAttempt path = do
|
|
|
|
|
cookies <- cookieHeader (sessionCookies session)
|
|
|
|
|
try
|
|
|
|
|
(curlToFile path
|
|
|
|
|
[ "--header", "Cookie: " <> B8.unpack cookies
|
|
|
|
|
, probeBase context
|
|
|
|
|
<> "/api/download?root=personal&path=streaming-large.bin"
|
|
|
|
|
])
|
|
|
|
|
:: IO (Either SomeException ProbeResponse)
|
2026-07-20 00:35:38 +09:00
|
|
|
BS.writeFile firstPath BS.empty
|
|
|
|
|
BS.writeFile secondPath BS.empty
|
2026-07-20 23:06:01 +09:00
|
|
|
(first, second) <-
|
|
|
|
|
concurrently (downloadAttempt firstPath) (downloadAttempt secondPath)
|
2026-07-19 23:57:21 +09:00
|
|
|
assert "first throttled download completed" (isLeft first)
|
|
|
|
|
assert "second throttled download completed" (isLeft second)
|
|
|
|
|
firstBytes <- getFileSize firstPath
|
|
|
|
|
secondBytes <- getFileSize secondPath
|
|
|
|
|
burst <- readEnvInteger "NUTHATCH_STREAMING_THROTTLE_BURST_BYTES"
|
|
|
|
|
let moved = firstBytes + secondBytes
|
|
|
|
|
jointLimit = burst - 65536 + 4096
|
|
|
|
|
assert "parallel throttled downloads emitted no bytes" (moved > 0)
|
|
|
|
|
assert "parallel downloads exceeded their joint token budget" $
|
|
|
|
|
moved <= jointLimit
|
|
|
|
|
exhausted <- download context session "personal" "streaming-large.bin"
|
|
|
|
|
requireStatus "post-exhaustion download" 429 exhausted
|
|
|
|
|
putStrLn "Shared incremental throttle behavior passed"
|
|
|
|
|
|
|
|
|
|
verifyRefill :: ProbeContext -> Session -> FilePath -> IO ()
|
|
|
|
|
verifyRefill context session directory = do
|
|
|
|
|
let bodyPath = directory <> "/refill-upload.bin"
|
2026-08-15 21:51:56 +09:00
|
|
|
refillPath = directory <> "/refill-byte.bin"
|
2026-07-19 23:57:21 +09:00
|
|
|
writeZeros bodyPath 65536
|
2026-08-15 21:51:56 +09:00
|
|
|
BS.writeFile refillPath "x"
|
2026-07-19 23:57:21 +09:00
|
|
|
digest <- hashFile bodyPath
|
2026-08-15 21:51:56 +09:00
|
|
|
refillDigest <- hashFile refillPath
|
2026-07-20 23:06:01 +09:00
|
|
|
initial <- curlUploadFileDeclared
|
2026-07-19 23:57:21 +09:00
|
|
|
context session "refill-initial.bin"
|
|
|
|
|
"72000000-0000-4000-8000-000000000001"
|
2026-07-20 23:06:01 +09:00
|
|
|
65536 digest bodyPath
|
2026-07-19 23:57:21 +09:00
|
|
|
requireSucceeded "initial token use" initial
|
2026-07-20 23:06:01 +09:00
|
|
|
exhausted <- curlUploadFileDeclared
|
2026-07-19 23:57:21 +09:00
|
|
|
context session "refill-exhausted.bin"
|
|
|
|
|
"72000000-0000-4000-8000-000000000003"
|
2026-07-20 23:06:01 +09:00
|
|
|
65536 digest bodyPath
|
2026-07-19 23:57:21 +09:00
|
|
|
requireStatus "exhausted token bucket" 429 exhausted
|
|
|
|
|
threadDelay 2000000
|
2026-07-20 23:06:01 +09:00
|
|
|
refilled <- curlUploadFileDeclared
|
2026-07-19 23:57:21 +09:00
|
|
|
context session "refill-refilled.bin"
|
|
|
|
|
"72000000-0000-4000-8000-000000000002"
|
2026-08-15 21:51:56 +09:00
|
|
|
1 refillDigest refillPath
|
2026-07-19 23:57:21 +09:00
|
|
|
requireSucceeded "refilled token bucket" refilled
|
|
|
|
|
putStrLn "Token-bucket refill behavior passed"
|
|
|
|
|
|
|
|
|
|
verifyUnavailable :: ProbeContext -> Session -> IO ()
|
|
|
|
|
verifyUnavailable context session = do
|
|
|
|
|
listing <- browse context session "?root=personal"
|
|
|
|
|
requireStatus "browse without Redis" 200 listing
|
2026-07-20 23:06:01 +09:00
|
|
|
assertContains "browse without Redis" "\"path\":" (probeBody listing)
|
2026-07-19 23:57:21 +09:00
|
|
|
let path =
|
2026-07-20 23:06:01 +09:00
|
|
|
if "cross-user.txt" `BS.isInfixOf` probeBody listing
|
2026-07-19 23:57:21 +09:00
|
|
|
then "cross-user.txt"
|
|
|
|
|
else "streaming-large.bin"
|
|
|
|
|
query =
|
|
|
|
|
if path == "cross-user.txt" then "CROSS-USER" else "STREAMING-LARGE"
|
|
|
|
|
searched <- browse context session ("?root=personal&q=" <> query)
|
2026-07-20 23:06:01 +09:00
|
|
|
assertContains "search without Redis" (B8.pack path) (probeBody searched)
|
2026-07-19 23:57:21 +09:00
|
|
|
unavailableUpload <- upload context session "personal" "unavailable.bin"
|
|
|
|
|
"72000000-0000-4000-8000-000000000001" "x"
|
|
|
|
|
requireError "upload without Redis"
|
|
|
|
|
503 "throttle_unavailable" unavailableUpload
|
|
|
|
|
unavailableDownload <-
|
|
|
|
|
retryTransport 4 (download context session "personal" path)
|
|
|
|
|
requireError "download without Redis"
|
|
|
|
|
503 "throttle_unavailable" unavailableDownload
|
|
|
|
|
putStrLn "Redis-unavailable transfer behavior passed"
|
|
|
|
|
|
|
|
|
|
retryTransport :: Int -> IO value -> IO value
|
|
|
|
|
retryTransport attempts action = do
|
|
|
|
|
result <- try action
|
|
|
|
|
case result of
|
|
|
|
|
Right value -> pure value
|
|
|
|
|
Left exception
|
|
|
|
|
| attempts > 1 -> do
|
|
|
|
|
threadDelay 100000
|
|
|
|
|
retryTransport (attempts - 1) action
|
|
|
|
|
| otherwise -> fail (show (exception :: SomeException))
|
|
|
|
|
|
|
|
|
|
uploadFileDeclared
|
|
|
|
|
:: ProbeContext
|
|
|
|
|
-> Session
|
|
|
|
|
-> String
|
|
|
|
|
-> String
|
|
|
|
|
-> Integer
|
|
|
|
|
-> BS.ByteString
|
|
|
|
|
-> FilePath
|
2026-07-20 23:06:01 +09:00
|
|
|
-> RequestOptions
|
|
|
|
|
-> IO ProbeResponse
|
|
|
|
|
uploadFileDeclared context session path operationId size digest bodyPath options =
|
|
|
|
|
postWith
|
|
|
|
|
(probeClient context)
|
|
|
|
|
options
|
|
|
|
|
{ optionCookies = Just (sessionCookies session)
|
|
|
|
|
, optionHeaders =
|
|
|
|
|
[ ("Content-Type", "application/octet-stream")
|
|
|
|
|
, ("X-Declared-Size", B8.pack (show size))
|
|
|
|
|
, ("X-Content-SHA256", digest)
|
|
|
|
|
, ("X-CSRF-Token", sessionCsrf session)
|
|
|
|
|
] <> optionHeaders options
|
|
|
|
|
, optionBody = FileBody bodyPath
|
|
|
|
|
}
|
|
|
|
|
(uploadUrl context path operationId)
|
2026-07-19 23:57:21 +09:00
|
|
|
|
|
|
|
|
uploadBytesDeclared
|
|
|
|
|
:: ProbeContext
|
|
|
|
|
-> Session
|
|
|
|
|
-> String
|
|
|
|
|
-> String
|
|
|
|
|
-> Integer
|
|
|
|
|
-> BS.ByteString
|
|
|
|
|
-> BS.ByteString
|
2026-07-20 23:06:01 +09:00
|
|
|
-> RequestOptions
|
|
|
|
|
-> IO ProbeResponse
|
|
|
|
|
uploadBytesDeclared context session path operationId size digest body options =
|
|
|
|
|
postWith
|
|
|
|
|
(probeClient context)
|
|
|
|
|
options
|
|
|
|
|
{ optionCookies = Just (sessionCookies session)
|
|
|
|
|
, optionHeaders =
|
|
|
|
|
[ ("Content-Type", "application/octet-stream")
|
|
|
|
|
, ("X-Declared-Size", B8.pack (show size))
|
|
|
|
|
, ("X-Content-SHA256", digest)
|
|
|
|
|
, ("X-CSRF-Token", sessionCsrf session)
|
|
|
|
|
] <> optionHeaders options
|
|
|
|
|
, optionBody = ByteBody body
|
|
|
|
|
}
|
|
|
|
|
(uploadUrl context path operationId)
|
2026-07-19 23:57:21 +09:00
|
|
|
|
|
|
|
|
uploadArguments
|
|
|
|
|
:: ProbeContext
|
|
|
|
|
-> Session
|
|
|
|
|
-> String
|
|
|
|
|
-> String
|
|
|
|
|
-> Integer
|
|
|
|
|
-> BS.ByteString
|
2026-07-20 23:06:01 +09:00
|
|
|
-> IO [String]
|
|
|
|
|
uploadArguments context session path operationId size digest = do
|
|
|
|
|
cookies <- cookieHeader (sessionCookies session)
|
|
|
|
|
pure
|
|
|
|
|
[ "--header", "Cookie: " <> B8.unpack cookies
|
|
|
|
|
, "--header", "Content-Type: application/octet-stream"
|
|
|
|
|
, "--header", "X-Declared-Size: " <> show size
|
|
|
|
|
, "--header", "X-Content-SHA256: " <> B8.unpack digest
|
|
|
|
|
, "--header", "X-CSRF-Token: " <> B8.unpack (sessionCsrf session)
|
|
|
|
|
, probeBase context
|
|
|
|
|
<> "/api/upload?root=personal&path="
|
|
|
|
|
<> path
|
|
|
|
|
<> "&operation_id="
|
|
|
|
|
<> operationId
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
curlUploadFileDeclared
|
|
|
|
|
:: ProbeContext
|
|
|
|
|
-> Session
|
|
|
|
|
-> String
|
|
|
|
|
-> String
|
|
|
|
|
-> Integer
|
|
|
|
|
-> BS.ByteString
|
|
|
|
|
-> FilePath
|
|
|
|
|
-> IO ProbeResponse
|
|
|
|
|
curlUploadFileDeclared context session path operationId size digest bodyPath = do
|
|
|
|
|
arguments <- uploadArguments context session path operationId size digest
|
|
|
|
|
curl (arguments <> ["--data-binary", "@" <> bodyPath])
|
|
|
|
|
|
|
|
|
|
curlUploadBytesDeclared
|
|
|
|
|
:: ProbeContext
|
|
|
|
|
-> Session
|
|
|
|
|
-> String
|
|
|
|
|
-> String
|
|
|
|
|
-> Integer
|
|
|
|
|
-> BS.ByteString
|
|
|
|
|
-> BS.ByteString
|
|
|
|
|
-> IO ProbeResponse
|
|
|
|
|
curlUploadBytesDeclared context session path operationId size digest body =
|
|
|
|
|
withSystemTempFile "nuthatch-streaming-body" $ \bodyPath handle -> do
|
|
|
|
|
BS.hPut handle body
|
|
|
|
|
hClose handle
|
|
|
|
|
curlUploadFileDeclared
|
|
|
|
|
context session path operationId size digest bodyPath
|
2026-07-19 23:57:21 +09:00
|
|
|
|
|
|
|
|
interruptedUpload
|
|
|
|
|
:: ProbeContext
|
|
|
|
|
-> Session
|
|
|
|
|
-> String
|
|
|
|
|
-> String
|
|
|
|
|
-> Integer
|
|
|
|
|
-> BS.ByteString
|
|
|
|
|
-> FilePath
|
|
|
|
|
-> IO Bool
|
|
|
|
|
interruptedUpload context session path operationId size digest bodyPath = do
|
2026-07-20 23:06:01 +09:00
|
|
|
common <- uploadArguments context session path operationId size digest
|
2026-07-19 23:57:21 +09:00
|
|
|
let arguments =
|
|
|
|
|
[ "--insecure"
|
|
|
|
|
, "--silent"
|
|
|
|
|
, "--show-error"
|
2026-08-15 21:04:55 +09:00
|
|
|
, "--header", "Expect:"
|
|
|
|
|
, "--max-time", "2"
|
2026-07-19 23:57:21 +09:00
|
|
|
, "--limit-rate", "64k"
|
|
|
|
|
, "--request", "POST"
|
|
|
|
|
, "--upload-file", bodyPath
|
|
|
|
|
]
|
2026-07-20 23:06:01 +09:00
|
|
|
<> common
|
2026-07-19 23:57:21 +09:00
|
|
|
(exitCode, _, _) <- readProcessWithExitCode "curl" arguments ""
|
|
|
|
|
pure (exitCode == ExitSuccess)
|
|
|
|
|
|
|
|
|
|
interruptedDownload
|
|
|
|
|
:: ProbeContext
|
|
|
|
|
-> Session
|
|
|
|
|
-> FilePath
|
|
|
|
|
-> IO Bool
|
|
|
|
|
interruptedDownload context session outputPath = do
|
2026-07-20 23:06:01 +09:00
|
|
|
cookies <- cookieHeader (sessionCookies session)
|
2026-07-19 23:57:21 +09:00
|
|
|
let arguments =
|
|
|
|
|
[ "--insecure"
|
|
|
|
|
, "--silent"
|
|
|
|
|
, "--show-error"
|
2026-08-15 21:04:55 +09:00
|
|
|
, "--max-time", "2"
|
2026-07-19 23:57:21 +09:00
|
|
|
, "--limit-rate", "64k"
|
2026-07-20 23:06:01 +09:00
|
|
|
, "--header", "Cookie: " <> B8.unpack cookies
|
2026-07-19 23:57:21 +09:00
|
|
|
, "--output", outputPath
|
|
|
|
|
, probeBase context
|
|
|
|
|
<> "/api/download?root=personal&path=streaming-large.bin"
|
|
|
|
|
]
|
|
|
|
|
(exitCode, _, _) <- readProcessWithExitCode "curl" arguments ""
|
|
|
|
|
pure (exitCode == ExitSuccess)
|
|
|
|
|
|
2026-07-20 23:06:01 +09:00
|
|
|
uploadUrl :: ProbeContext -> String -> String -> String
|
|
|
|
|
uploadUrl context path operationId =
|
|
|
|
|
probeBase context
|
|
|
|
|
<> "/api/upload?root=personal&path="
|
|
|
|
|
<> path
|
|
|
|
|
<> "&operation_id="
|
|
|
|
|
<> operationId
|
|
|
|
|
|
2026-07-19 23:57:21 +09:00
|
|
|
writeZeros :: FilePath -> Integer -> IO ()
|
|
|
|
|
writeZeros path byteCount =
|
|
|
|
|
withBinaryFile path WriteMode $ \handle -> do
|
|
|
|
|
let chunk = BS.replicate 1048576 0
|
|
|
|
|
(fullChunks, remainder) = byteCount `divMod` 1048576
|
|
|
|
|
replicateM_ (fromIntegral fullChunks) (BS.hPut handle chunk)
|
|
|
|
|
unless (remainder == 0) $
|
|
|
|
|
BS.hPut handle (BS.take (fromIntegral remainder) chunk)
|
|
|
|
|
|
|
|
|
|
hashFile :: FilePath -> IO BS.ByteString
|
|
|
|
|
hashFile path =
|
|
|
|
|
withBinaryFile path ReadMode $ \handle ->
|
|
|
|
|
go handle hashInit
|
|
|
|
|
where
|
|
|
|
|
go handle context = do
|
|
|
|
|
chunk <- BS.hGetSome handle 1048576
|
|
|
|
|
if BS.null chunk
|
|
|
|
|
then
|
|
|
|
|
pure
|
|
|
|
|
(B16.encode
|
|
|
|
|
(BA.convert (hashFinalize context :: Digest SHA256)))
|
|
|
|
|
else do
|
|
|
|
|
let updated = hashUpdate context chunk
|
|
|
|
|
updated `seq` go handle updated
|
|
|
|
|
|
|
|
|
|
readEnvInteger :: String -> IO Integer
|
|
|
|
|
readEnvInteger name = do
|
|
|
|
|
value <- requireEnv name
|
|
|
|
|
maybe (fail ("invalid " <> name)) pure (readMaybe value)
|
|
|
|
|
|
2026-07-20 23:06:01 +09:00
|
|
|
requireSucceeded :: String -> ProbeResponse -> IO ()
|
2026-07-19 23:57:21 +09:00
|
|
|
requireSucceeded label response = do
|
|
|
|
|
requireStatus label 200 response
|
2026-07-20 23:06:01 +09:00
|
|
|
assertContains label "\"result\":\"succeeded\"" (probeBody response)
|
2026-07-19 23:57:21 +09:00
|
|
|
|
2026-07-20 23:06:01 +09:00
|
|
|
requireError :: String -> Int -> BS.ByteString -> ProbeResponse -> IO ()
|
2026-07-19 23:57:21 +09:00
|
|
|
requireError label status code response = do
|
|
|
|
|
requireStatus label status response
|
2026-07-20 23:06:01 +09:00
|
|
|
assertContains label ("\"error\":\"" <> code <> "\"") (probeBody response)
|
2026-07-19 23:57:21 +09:00
|
|
|
|
2026-07-20 23:06:01 +09:00
|
|
|
requireStatus :: String -> Int -> ProbeResponse -> IO ()
|
2026-07-19 23:57:21 +09:00
|
|
|
requireStatus label expected response =
|
2026-07-20 23:06:01 +09:00
|
|
|
assertEqual (label <> " status") expected (probeStatus response)
|
2026-07-19 23:57:21 +09:00
|
|
|
|
|
|
|
|
isLeft :: Either left right -> Bool
|
|
|
|
|
isLeft (Left _) = True
|
|
|
|
|
isLeft (Right _) = False
|