hstratus-drive-0.1.0.0: src-internal/Network/HStratus/Internal/Drive/NodeData.hs
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_HADDOCK prune #-}
{- |
Module : Network.HStratus.Internal.Drive.NodeData
Copyright : (c) 2026 Tim Emiola
Maintainer : Tim Emiola <adetokunbo@emio.la>
SPDX-License-Identifier: BSD-3-Clause
JSON parsers for iCloud Drive API responses: node metadata, download URLs, and upload receipts.
-}
module Network.HStratus.Internal.Drive.NodeData
( parseNodeResponse
, parseChildrenResponse
, parseDownloadUrl
, UploadReceipt (..)
, parseUploadTokenResponse
, parseUploadReceiptResponse
)
where
import Data.Aeson
( Object
, Value
, withArray
, withObject
, (.:)
, (.:?)
)
import Data.Aeson.Types (Parser)
import Data.Functor ((<&>))
import Data.Int (Int64)
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as Text
import Data.Time (UTCTime, ZonedTime, zonedTimeToUTC)
import Data.Time.Format.ISO8601 (iso8601ParseM)
import qualified Data.Vector as V
import Network.HStratus.Internal.Drive.Node
( DriveNode (..)
, DriveNodeId (..)
, FileData (..)
, FolderData (..)
)
{- | Parse the first element of a @retrieveItemDetailsInFolders@ response as a
@DriveNode@.
-}
parseNodeResponse :: Value -> Parser DriveNode
parseNodeResponse = withArray "node response" $ \arr ->
case V.toList arr of
[] -> fail "retrieveItemDetailsInFolders: empty response array"
(v : _) -> parseNode v
{- | Parse the children from the first element of a
@retrieveItemDetailsInFolders@ response.
-}
parseChildrenResponse :: Value -> Parser [DriveNode]
parseChildrenResponse = withArray "children response" $ \arr ->
case V.toList arr of
[] -> fail "retrieveItemDetailsInFolders: empty response array"
(v : _) -> withObject "folder" parseItems v
-- | Extract the download URL from a @download/by_id@ response.
parseDownloadUrl :: Value -> Parser Text
parseDownloadUrl = withObject "download response" $ \o -> do
dataToken <- o .:? "data_token"
pkgToken <- o .:? "package_token"
case (dataToken, pkgToken) of
(Just dt, _) -> withObject "data_token" (.: "url") dt
(_, Just pt) -> withObject "package_token" (.: "url") pt
_other -> fail "download response: neither data_token nor package_token found"
parseNode :: Value -> Parser DriveNode
parseNode = withObject "DriveNode" $ \o -> do
nodeType <- o .: "type" :: Parser Text
case nodeType of
"FILE" -> DriveFile <$> parseFileData o
"FOLDER" -> DriveFolder <$> parseFolderData o
"APP_LIBRARY" -> DriveFolder <$> parseFolderData o
other -> fail $ "DriveNode: unknown node type: " <> Text.unpack other
parseFolderData :: Object -> Parser FolderData
parseFolderData o =
(FolderData . DriveNodeId <$> (o .: "drivewsid"))
<*> o .: "etag"
<*> o .: "name"
<*> o .: "zone"
<*> (o .:? "dateCreated" >>= traverse parseTimestamp)
parseFileData :: Object -> Parser FileData
parseFileData o =
(FileData . DriveNodeId <$> (o .: "drivewsid"))
<*> o .: "docwsid"
<*> o .: "etag"
<*> o .: "name"
<*> o .:? "extension"
<*> o .: "zone"
<*> o .:? "size"
<*> (o .:? "dateCreated" >>= traverse parseTimestamp)
<*> (o .:? "dateModified" >>= traverse parseTimestamp)
parseItems :: Object -> Parser [DriveNode]
parseItems o = do
items <- (o .:? "items") <&> fromMaybe []
mapM parseNode items
-- | Checksum metadata returned after uploading file content (step 2 of upload).
data UploadReceipt = UploadReceipt
{ urFileChecksum :: !Text
-- ^ SHA-256 checksum of the uploaded file
, urWrappingKey :: !Text
-- ^ encryption wrapping key returned by the upload endpoint
, urReferenceChecksum :: !Text
-- ^ reference checksum used in the commit body
, urSize :: !Int64
-- ^ byte size of the uploaded content
, urReceipt :: !(Maybe Text)
-- ^ opaque receipt token; @Nothing@ when absent from the server response
}
-- | Parse the @upload/web@ response to extract @(document_id, upload_url)@.
parseUploadTokenResponse :: Value -> Parser (Text, Text)
parseUploadTokenResponse = withArray "upload token response" $ \arr ->
case V.toList arr of
[] -> fail "upload token response: empty array"
(v : _) ->
withObject "upload token" (\o -> (,) <$> o .: "document_id" <*> o .: "url") v
-- | Parse the multipart-upload response body to extract 'UploadReceipt'.
parseUploadReceiptResponse :: Value -> Parser UploadReceipt
parseUploadReceiptResponse = withObject "upload receipt response" $ \o -> do
sf <- o .: "singleFile"
withObject "singleFile" parseReceiptFields sf
parseReceiptFields :: Object -> Parser UploadReceipt
parseReceiptFields o =
UploadReceipt
<$> o .: "fileChecksum"
<*> o .: "wrappingKey"
<*> o .: "referenceChecksum"
<*> o .: "size"
<*> o .:? "receipt"
-- | Parse an ISO 8601 timestamp in either UTC (@Z@) or offset (@±HH:MM@) form.
parseTimestamp :: Text -> Parser UTCTime
parseTimestamp t =
let s = Text.unpack t
in case (iso8601ParseM s :: Maybe UTCTime) of
Just ut -> pure ut
Nothing -> case (iso8601ParseM s :: Maybe ZonedTime) of
Just zt -> pure (zonedTimeToUTC zt)
Nothing -> fail $ "invalid ISO 8601 timestamp: " <> s