hstratus-drive-0.1.0.0: src-internal/Network/HStratus/Internal/Drive/Node.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE StrictData #-}
{-# OPTIONS_HADDOCK prune #-}
{- |
Module : Network.HStratus.Internal.Drive.Node
Copyright : (c) 2026 Tim Emiola
Maintainer : Tim Emiola <adetokunbo@emio.la>
SPDX-License-Identifier: BSD-3-Clause
Internal types for iCloud Drive nodes: identifiers, file and folder data.
-}
module Network.HStratus.Internal.Drive.Node
( -- * Node identifier
DriveNodeId (..)
, rootNodeId
-- * Node types
, DriveNode (..)
, FolderData (..)
, FileData (..)
, fileName
, nodeId
, nodeEtag
, folderDocId
-- * Node lookup
, matchFolderName
, selectFileNode
)
where
import Data.Int (Int64)
import Data.List (find)
import Data.Maybe (fromMaybe)
import Data.String (IsString (..))
import Data.Text (Text)
import qualified Data.Text as Text
import Data.Time (UTCTime)
-- | Stable identifier for a node in iCloud Drive (the @drivewsid@ field).
newtype DriveNodeId = DriveNodeId {unDriveNodeId :: Text}
deriving (Eq, Show)
instance IsString DriveNodeId where
fromString = DriveNodeId . Text.pack
-- | The node ID for the root of the main CloudDocs tree.
rootNodeId :: DriveNodeId
rootNodeId = DriveNodeId "FOLDER::com.apple.CloudDocs::root"
-- | A node in the iCloud Drive tree — either a folder or a file.
data DriveNode
= -- | a folder node with its metadata
DriveFolder FolderData
| -- | a file node with its metadata
DriveFile FileData
deriving (Eq, Show)
-- | Metadata for a folder node.
data FolderData = FolderData
{ fnId :: !DriveNodeId
-- ^ stable node identifier (@drivewsid@)
, fnEtag :: !Text
-- ^ version tag; required for rename and delete
, fnName :: !Text
-- ^ display name of the folder
, fnZone :: !Text
-- ^ CloudDocs zone (e.g. @com.apple.CloudDocs@)
, fnDateCreated :: !(Maybe UTCTime)
-- ^ creation timestamp; @Nothing@ when absent from the server response
}
deriving (Eq, Show)
-- | Metadata for a file node.
data FileData = FileData
{ fdId :: !DriveNodeId
-- ^ stable node identifier (@drivewsid@)
, fdDocId :: !Text
-- ^ document identifier (@docwsid@); used for download and upload
, fdEtag :: !Text
-- ^ version tag; required for rename and delete
, fdName :: !Text
-- ^ base file name (without extension)
, fdExtension :: !(Maybe Text)
-- ^ file extension, if present
, fdZone :: !Text
-- ^ CloudDocs zone (e.g. @com.apple.CloudDocs@)
, fdSize :: !(Maybe Int64)
-- ^ file size in bytes; @Nothing@ for zero-byte files
, fdDateCreated :: !(Maybe UTCTime)
-- ^ creation timestamp; @Nothing@ when absent from the server response
, fdDateModified :: !(Maybe UTCTime)
-- ^ last modification timestamp; @Nothing@ when absent from the server response
}
deriving (Eq, Show)
-- | The full display name of a file, with extension appended if present.
fileName :: FileData -> Text
fileName fd = case fdExtension fd of
Nothing -> fdName fd
Just ext -> fdName fd <> Text.pack "." <> ext
-- | True when the node is a folder whose display name equals the given text.
matchFolderName :: Text -> DriveNode -> Bool
matchFolderName name (DriveFolder fd) = fnName fd == name
matchFolderName _ (DriveFile _) = False
-- | Find the first node whose name matches, checking file and folder names.
selectFileNode :: Text -> [DriveNode] -> Maybe DriveNode
selectFileNode name = find matchesName
where
matchesName (DriveFile fd) = fileName fd == name
matchesName (DriveFolder fd) = fnName fd == name
-- | Extract the stable identifier from any node.
nodeId :: DriveNode -> DriveNodeId
nodeId (DriveFolder fd) = fnId fd
nodeId (DriveFile fd) = fdId fd
-- | Extract the version tag from any node.
nodeEtag :: DriveNode -> Text
nodeEtag (DriveFolder fd) = fnEtag fd
nodeEtag (DriveFile fd) = fdEtag fd
-- | Derive the @docwsid@ of a folder from its 'DriveNodeId' and zone.
folderDocId :: FolderData -> Text
folderDocId fd =
let DriveNodeId nid = fnId fd
prefix = "FOLDER::" <> fnZone fd <> "::"
in fromMaybe nid (Text.stripPrefix prefix nid)