packages feed

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)