File: Types.hs

package info (click to toggle)
git-annex 10.20251029-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 75,300 kB
  • sloc: haskell: 91,492; javascript: 9,103; sh: 1,593; makefile: 216; perl: 137; ansic: 44
file content (400 lines) | stat: -rw-r--r-- 12,087 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
{- P2P protocol over HTTP,
 - data types for servant not including the servant API
 -
 - Copyright 2024 Joey Hess <id@joeyh.name>
 -
 - Licensed under the GNU AGPL version 3 or higher.
 -}

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE CPP #-}

module P2P.Http.Types where

import Annex.Common
import qualified P2P.Protocol as P2P
import Utility.MonotonicClock

import Servant
import Data.Aeson hiding (Key)
import Text.Read (readMaybe)
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import qualified Data.ByteString as B
import Codec.Binary.Base64Url as B64
import Data.Char
import Control.DeepSeq
import GHC.Generics (Generic)

data V4 = V4 deriving (Show)
data V3 = V3 deriving (Show)
data V2 = V2 deriving (Show)
data V1 = V1 deriving (Show)
data V0 = V0 deriving (Show)

class APIVersion v where
	protocolVersion :: v -> P2P.ProtocolVersion

instance APIVersion V4 where protocolVersion _ = P2P.ProtocolVersion 4
instance APIVersion V3 where protocolVersion _ = P2P.ProtocolVersion 3
instance APIVersion V2 where protocolVersion _ = P2P.ProtocolVersion 2
instance APIVersion V1 where protocolVersion _ = P2P.ProtocolVersion 1
instance APIVersion V0 where protocolVersion _ = P2P.ProtocolVersion 0

-- Keys, UUIDs, and filenames can be base64 encoded since Servant uses 
-- Text and so needs UTF-8.
newtype B64Key = B64Key Key
	deriving (Show)

newtype B64FilePath = B64FilePath OsPath
	deriving (Show)

associatedFileToB64FilePath :: AssociatedFile -> Maybe B64FilePath
associatedFileToB64FilePath (AssociatedFile Nothing) = Nothing
associatedFileToB64FilePath (AssociatedFile (Just f)) = Just (B64FilePath f)

b64FilePathToAssociatedFile :: Maybe B64FilePath -> AssociatedFile
b64FilePathToAssociatedFile Nothing = AssociatedFile Nothing
b64FilePathToAssociatedFile (Just (B64FilePath f)) = AssociatedFile (Just f)

newtype B64UUID t = B64UUID { fromB64UUID :: UUID }
	deriving (Show, Ord, Eq, Generic, NFData)

encodeB64Text :: B.ByteString -> T.Text
encodeB64Text b = case TE.decodeUtf8' b of
	Right t
		| (snd <$> B.unsnoc b) == Just closebracket 
			&& (fst <$> B.uncons b) == Just openbracket ->
				b64wrapped
		| otherwise -> t
	Left _ -> b64wrapped
  where
	b64wrapped = TE.decodeUtf8 $ "[" <> B64.encode b <> "]"
	openbracket = fromIntegral (ord '[')
	closebracket = fromIntegral (ord ']')

decodeB64Text :: T.Text -> Either T.Text B.ByteString
decodeB64Text t = 
	case T.unsnoc t of
		Just (t', lastc) | lastc == ']' ->
			case T.uncons t' of
				Just (firstc, t'') | firstc == '[' ->
					case B64.decode (TE.encodeUtf8 t'') of
						Right b -> Right b
						Left _ -> Left "unable to base64 decode [] wrapped value"
				_ -> Right (TE.encodeUtf8 t)
		_ -> Right (TE.encodeUtf8 t)

-- Phantom types.
data ClientSide
data ServerSide
data Bypass
data Plus
data Lock

type LockID = B64UUID Lock

newtype DataLength = DataLength Integer
	deriving (Show)

newtype CheckPresentResult = CheckPresentResult Bool
	deriving (Show)

newtype RemoveResult = RemoveResult Bool
	deriving (Show)

data RemoveResultPlus = RemoveResultPlus Bool [B64UUID Plus]
	deriving (Show)

newtype GetTimestampResult = GetTimestampResult Timestamp
	deriving (Show)

newtype PutResult = PutResult Bool
	deriving (Eq, Show)

data PutResultPlus = PutResultPlus Bool [B64UUID Plus]
	deriving (Show)

data PutOffsetResult
	= PutOffsetResult Offset
	| PutOffsetResultAlreadyHave
	deriving (Show)

data PutOffsetResultPlus 
	= PutOffsetResultPlus Offset
	| PutOffsetResultAlreadyHavePlus [B64UUID Plus]
	deriving (Show, Generic, NFData)

newtype Offset = Offset P2P.Offset
	deriving (Show, Generic, NFData)

newtype Timestamp = Timestamp MonotonicTimestamp
	deriving (Show)

data LockResult = LockResult Bool (Maybe LockID)
	deriving (Show, Generic, NFData)

newtype UnlockRequest = UnlockRequest Bool
	deriving (Show, Generic, NFData)

-- Not using servant's built-in basic authentication support,
-- because whether authentication is needed depends on server
-- configuration.
data Auth = Auth B.ByteString B.ByteString
	deriving (Show, Generic, NFData, Eq, Ord)

instance ToHttpApiData Auth where
	toHeader (Auth u p) = "Basic " <> B64.encode (u <> ":" <> p)
#if MIN_VERSION_text(2,0,0)
	toUrlPiece = TE.decodeUtf8Lenient . toHeader
#else
	toUrlPiece = TE.decodeUtf8With (\_ _ -> Just '\xfffd') . toHeader
#endif

instance FromHttpApiData Auth where
	parseHeader h =
		let (b, rest) = B.break (isSpace . chr . fromIntegral) h
		in if map toLower (decodeBS b) == "basic"
			then case B64.decode (B.dropWhile (isSpace . chr . fromIntegral) rest) of
				Right v -> case B.split (fromIntegral (ord ':')) v of
					(u:ps) -> Right $
						Auth u (B.intercalate ":" ps)
					_ -> bad
				Left _ -> bad
			else bad
	  where
		bad = Left "invalid basic auth header"
	parseUrlPiece = parseHeader . encodeBS . T.unpack

newtype ConnectionKeepAlive = ConnectionKeepAlive T.Text

connectionKeepAlive :: ConnectionKeepAlive
connectionKeepAlive = ConnectionKeepAlive "Keep-Alive"

newtype KeepAlive = KeepAlive T.Text

keepAlive :: KeepAlive
keepAlive = KeepAlive "timeout=1200"

instance ToHttpApiData ConnectionKeepAlive where
	toUrlPiece (ConnectionKeepAlive t) = t

instance FromHttpApiData ConnectionKeepAlive where
	parseUrlPiece = Right . ConnectionKeepAlive

instance ToHttpApiData KeepAlive where
	toUrlPiece (KeepAlive t) = t

instance FromHttpApiData KeepAlive where
	parseUrlPiece = Right . KeepAlive

instance ToHttpApiData V4 where toUrlPiece _ = "v4"
instance ToHttpApiData V3 where toUrlPiece _ = "v3"
instance ToHttpApiData V2 where toUrlPiece _ = "v2"
instance ToHttpApiData V1 where toUrlPiece _ = "v1"
instance ToHttpApiData V0 where toUrlPiece _ = "v0"

instance FromHttpApiData V4 where parseUrlPiece = parseAPIVersion V4 "v4"
instance FromHttpApiData V3 where parseUrlPiece = parseAPIVersion V3 "v3"
instance FromHttpApiData V2 where parseUrlPiece = parseAPIVersion V2 "v2"
instance FromHttpApiData V1 where parseUrlPiece = parseAPIVersion V1 "v1"
instance FromHttpApiData V0 where parseUrlPiece = parseAPIVersion V0 "v0"

parseAPIVersion :: v -> T.Text -> T.Text -> Either T.Text v
parseAPIVersion v need t
	| t == need = Right v
	| otherwise = Left "bad version"

instance ToHttpApiData B64Key where
	toUrlPiece (B64Key k) = encodeB64Text (serializeKey' k)

instance FromHttpApiData B64Key where
	parseUrlPiece t = case decodeB64Text t of
		Right b -> maybe (Left "key parse error") (Right . B64Key)
			(deserializeKey' b)
		Left err -> Left err

instance ToHttpApiData (B64UUID t) where
	toUrlPiece (B64UUID u) = encodeB64Text (fromUUID u)

instance FromHttpApiData (B64UUID t) where
	parseUrlPiece t = case decodeB64Text t of
		Right b -> case toUUID b of
			u@(UUID _) -> Right (B64UUID u)
			NoUUID -> Left "empty UUID"
		Left err -> Left err

instance ToHttpApiData B64FilePath where
	toUrlPiece (B64FilePath f) = encodeB64Text (fromOsPath f)

instance FromHttpApiData B64FilePath where
	parseUrlPiece t = case decodeB64Text t of
		Right b -> Right (B64FilePath (toOsPath b))
		Left err -> Left err

instance ToHttpApiData Offset where
	toUrlPiece (Offset (P2P.Offset n)) = T.pack (show n)

instance FromHttpApiData Offset where
	parseUrlPiece t = case readMaybe (T.unpack t) of
		Nothing -> Left "offset parse error"
		Just n -> Right (Offset (P2P.Offset n))

instance ToHttpApiData Timestamp where
	toUrlPiece (Timestamp (MonotonicTimestamp n)) = T.pack (show n)

instance FromHttpApiData Timestamp where
	parseUrlPiece t = case readMaybe (T.unpack t) of
		Nothing -> Left "timestamp parse error"
		Just n -> Right (Timestamp (MonotonicTimestamp n))

instance ToHttpApiData DataLength where
	toUrlPiece (DataLength n) = T.pack (show n)

instance FromHttpApiData DataLength where
	parseUrlPiece t = case readMaybe (T.unpack t) of
		Nothing -> Left "X-git-annex-data-length parse error"
		Just n -> Right (DataLength n)

instance ToJSON PutResult where
	toJSON (PutResult b) =
		object ["stored" .= b]

instance FromJSON PutResult where
	parseJSON = withObject "PutResult" $ \v -> PutResult
		<$> v .: "stored"

instance ToJSON PutResultPlus where
	toJSON (PutResultPlus b us) = object
		[ "stored" .= b
		, "plusuuids" .= plusList us
		]

instance FromJSON PutResultPlus where
	parseJSON = withObject "PutResultPlus" $ \v -> PutResultPlus
		<$> v .: "stored"
		<*> v .: "plusuuids"

instance ToJSON CheckPresentResult where
	toJSON (CheckPresentResult b) = object
		["present" .= b]

instance FromJSON CheckPresentResult where
	parseJSON = withObject "CheckPresentResult" $ \v -> CheckPresentResult
		<$> v .: "present"

instance ToJSON RemoveResult where
	toJSON (RemoveResult b) = object
		["removed" .= b]

instance FromJSON RemoveResult where
	parseJSON = withObject "RemoveResult" $ \v -> RemoveResult
		<$> v .: "removed"

instance ToJSON RemoveResultPlus where
	toJSON (RemoveResultPlus b us) = object
		[ "removed" .= b
		, "plusuuids" .= plusList us
		]

instance FromJSON RemoveResultPlus where
	parseJSON = withObject "RemoveResultPlus" $ \v -> RemoveResultPlus
		<$> v .: "removed"
		<*> v .: "plusuuids"

instance ToJSON GetTimestampResult where
	toJSON (GetTimestampResult (Timestamp (MonotonicTimestamp t))) = object
		["timestamp" .= t]

instance FromJSON GetTimestampResult where
	parseJSON = withObject "GetTimestampResult" $ \v ->
		GetTimestampResult . Timestamp . MonotonicTimestamp
			<$> v .: "timestamp"

instance ToJSON PutOffsetResult where
	toJSON (PutOffsetResult (Offset (P2P.Offset o))) = object
		["offset" .= o]
	toJSON PutOffsetResultAlreadyHave = object
		["alreadyhave" .= True]

instance FromJSON PutOffsetResult where
	parseJSON = withObject "PutOffsetResult" $ \v ->
		(PutOffsetResult
			<$> (Offset . P2P.Offset <$> v .: "offset"))
		<|> (mkalreadyhave
			<$> (v .: "alreadyhave"))
	  where
		mkalreadyhave :: Bool -> PutOffsetResult
		mkalreadyhave _ = PutOffsetResultAlreadyHave

instance ToJSON PutOffsetResultPlus where
	toJSON (PutOffsetResultPlus (Offset (P2P.Offset o))) = object
		[ "offset" .= o ]
	toJSON (PutOffsetResultAlreadyHavePlus us) = object
		[ "alreadyhave" .= True
		, "plusuuids" .= plusList us
		]

instance FromJSON PutOffsetResultPlus where
	parseJSON = withObject "PutOffsetResultPlus" $ \v ->
		(PutOffsetResultPlus
			<$> (Offset . P2P.Offset <$> v .: "offset"))
		<|> (mkalreadyhave
			<$> (v .: "alreadyhave")
			<*> (v .: "plusuuids"))
	  where
		mkalreadyhave :: Bool -> [B64UUID Plus] -> PutOffsetResultPlus
		mkalreadyhave _ us = PutOffsetResultAlreadyHavePlus us

instance FromJSON (B64UUID t) where
	parseJSON (String t) = case decodeB64Text t of
		Right s -> pure (B64UUID (toUUID s))
		Left _ -> mempty
	parseJSON _ = mempty

instance ToJSON LockResult where
	toJSON (LockResult v (Just (B64UUID lck))) = object
		[ "locked" .= v
		, "lockid" .= encodeB64Text (fromUUID lck)
		]
	toJSON (LockResult v Nothing) = object
		[ "locked" .= v
		]

instance FromJSON LockResult where
	parseJSON = withObject "LockResult" $ \v -> LockResult
		<$> v .: "locked"
		<*> v .:? "lockid"

instance ToJSON UnlockRequest where
	toJSON (UnlockRequest v) = object
		["unlock" .= v]

instance FromJSON UnlockRequest where
	parseJSON = withObject "UnlockRequest" $ \v -> UnlockRequest
		<$> v .: "unlock"

plusList :: [B64UUID Plus] -> [String]
plusList = map (\(B64UUID u) -> fromUUID u)

class PlusClass plus unplus where
	dePlus :: plus -> unplus
	plus :: unplus -> plus

instance PlusClass RemoveResultPlus RemoveResult where
	dePlus (RemoveResultPlus b _) = RemoveResult b
	plus (RemoveResult b) = RemoveResultPlus b mempty

instance PlusClass PutResultPlus PutResult where
	dePlus (PutResultPlus b _) = PutResult b
	plus (PutResult b) = PutResultPlus b mempty

instance PlusClass PutOffsetResultPlus PutOffsetResult where
	dePlus (PutOffsetResultPlus o) = PutOffsetResult o
	dePlus (PutOffsetResultAlreadyHavePlus _) = PutOffsetResultAlreadyHave
	plus (PutOffsetResult o) = PutOffsetResultPlus o
	plus PutOffsetResultAlreadyHave = PutOffsetResultAlreadyHavePlus []