@@ -8,20 +8,32 @@ module Lib.GitHub
8
8
, githubGetFile
9
9
, githubGetCodeOwnersFile
10
10
, githubRepositoryExists
11
+
12
+ -- * Utilities for working with GitHub directories
13
+ , copyGithubDirectory
14
+ , writeToDirectory
15
+ , exitOnException
16
+ , githubStreamDirectoryContents
11
17
) where
12
18
13
19
import Control.Exception
14
20
( Exception
21
+ , SomeException
15
22
)
23
+ import Control.Monad.Fix (fix )
24
+ import Control.Monad.Trans.Except (ExceptT , runExceptT , throwE )
16
25
import Core.Types.Basic
17
26
( Commit (.. )
18
27
, Directory (.. )
19
28
, FileName (.. )
20
29
, Repository (.. )
21
30
, Username (.. )
22
31
)
32
+ import Data.ByteString (ByteString )
33
+ import Data.ByteString qualified as B
23
34
import Data.ByteString.Base64 qualified as B64
24
- import Data.Foldable (Foldable (.. ))
35
+ import Data.Foldable (Foldable (.. ), forM_ )
36
+ import Data.Function ((&) )
25
37
import Data.Text qualified as T
26
38
import Data.Text.Encoding qualified as T
27
39
import GitHub (Auth (.. ), FetchCount (.. ), github )
@@ -34,7 +46,22 @@ import Network.HTTP.Client
34
46
, Response (.. )
35
47
)
36
48
import Network.HTTP.Types (Status (.. ))
49
+ import Path
50
+ ( Abs
51
+ , Dir
52
+ , File
53
+ , Path
54
+ , Rel
55
+
56
+ , parseRelFile
57
+ , toFilePath
58
+ , (</>) , parseRelDir , parent
59
+ )
60
+ import Streaming
61
+ import Streaming.Prelude (yield )
62
+ import Streaming.Prelude qualified as S
37
63
import Text.JSON.Canonical
64
+ import System.Directory (createDirectoryIfMissing )
38
65
39
66
data GithubResponseError
40
67
= GithubResponseErrorRepositoryNotFound
@@ -179,6 +206,7 @@ data GetGithubFileFailure
179
206
| GetGithubFileUnsupportedEncoding String
180
207
| GetGithubFileOtherFailure FilePath String
181
208
| GetGithubFileCodeError GithubResponseStatusCodeError
209
+ | GithubPathParsingError String
182
210
deriving (Eq , Show )
183
211
184
212
instance Exception GetGithubFileFailure
@@ -237,3 +265,118 @@ githubGetFile auth (Repository owner repo) commitM (FileName filename) = do
237
265
where
238
266
owner' = N $ T. pack owner
239
267
repo' = N $ T. pack repo
268
+
269
+ githubStreamDirectoryContents
270
+ :: Auth
271
+ -> Repository
272
+ -> Maybe Commit
273
+ -> Path Rel Dir
274
+ -> Stream
275
+ (Of (Path Rel File , ByteString ))
276
+ (ExceptT GetGithubFileFailure IO )
277
+ ()
278
+ githubStreamDirectoryContents
279
+ auth
280
+ (Repository owner repo)
281
+ commitM
282
+ startDir =
283
+ ($ startDir) $ fix $ \ go dir -> do
284
+ -- Get the contents of the source directory
285
+ response <-
286
+ liftIO
287
+ $ github auth
288
+ $ GH. contentsForR
289
+ owner'
290
+ repo'
291
+ (T. dropEnd 1 $ T. pack $ toFilePath dir)
292
+ ((\ (Commit c) -> T. pack c) <$> commitM)
293
+ case response of
294
+ Left e -> do
295
+ res <- liftIO $ onStatusCodeOfException e $ \ c -> do
296
+ case c of
297
+ 404 ->
298
+ pure
299
+ . Just
300
+ $ GetGithubFileDirectoryNotFound
301
+ _ ->
302
+ pure
303
+ . Just
304
+ . GetGithubFileOtherFailure (toFilePath dir)
305
+ $ show e
306
+ case res of
307
+ Left err -> lift $ throwE $ GetGithubFileCodeError err
308
+ Right a -> lift $ throwE a
309
+ Right (GH. ContentFile contents) -> do
310
+ let content = GH. contentFileContent contents
311
+ ebytes <- case GH. contentFileEncoding contents of
312
+ " base64" ->
313
+ pure
314
+ . Right
315
+ . B64. decodeLenient
316
+ . T. encodeUtf8
317
+ $ content
318
+ enc ->
319
+ pure
320
+ . Left
321
+ . GetGithubFileUnsupportedEncoding
322
+ $ T. unpack enc
323
+ case ebytes of
324
+ Left err -> lift $ throwE err
325
+ Right t -> lift (dirToFile dir) >>= yield . (,t)
326
+ Right (GH. ContentDirectory vis) -> forM_ vis $ \ case
327
+ GH. ContentItem _ctype GH. ContentInfo {contentPath} -> do
328
+ dir' <-
329
+ lift
330
+ $ throwPathParsing
331
+ $ parseRelDir (T. unpack contentPath)
332
+ go dir'
333
+ where
334
+ owner' = N $ T. pack owner
335
+ repo' = N $ T. pack repo
336
+
337
+ dirToFile
338
+ :: Monad m => Path b t -> ExceptT GetGithubFileFailure m (Path Rel File )
339
+ dirToFile = throwPathParsing . parseRelFile . tailU . toFilePath
340
+ where
341
+ tailU = T. unpack . T. dropWhileEnd (== ' /' ) . T. dropWhile (== ' /' ) . T. pack
342
+
343
+ throwPathParsing
344
+ :: (Monad m )
345
+ => Either SomeException a
346
+ -> ExceptT GetGithubFileFailure m a
347
+ throwPathParsing f = case f of
348
+ Left err -> throwE . GithubPathParsingError . show $ err
349
+ Right file -> pure file
350
+
351
+ writeToDirectory
352
+ :: Path Abs Dir -> Stream (Of (Path Rel File , ByteString )) IO r -> IO r
353
+ writeToDirectory targetDir = S. mapM_ writeFile'
354
+ where
355
+ writeFile' (relPath, content) = do
356
+ let fullPath = targetDir </> relPath
357
+ createDirectoryIfMissing True (toFilePath $ parent fullPath)
358
+ B. writeFile (toFilePath fullPath) content
359
+
360
+ exitOnException
361
+ :: Stream (Of a ) (ExceptT e IO ) r -> Stream (Of a ) IO (Either e r )
362
+ exitOnException strm = effect $ do
363
+ x <- runExceptT $ S. next strm
364
+ pure $ case x of
365
+ Left e -> pure $ Left e
366
+ Right (Left r) -> pure $ Right r
367
+ Right (Right (a, rest)) -> do
368
+ yield a >> exitOnException rest
369
+
370
+ copyGithubDirectory
371
+ :: Auth
372
+ -> Repository
373
+ -> Maybe Commit
374
+ -> Path Rel Dir
375
+ -- ^ Source directory in the GitHub repository
376
+ -> Path Abs Dir
377
+ -- ^ Target directory on the local filesystem
378
+ -> IO (Either GetGithubFileFailure () )
379
+ copyGithubDirectory auth repo commitM srcDir targetDir =
380
+ githubStreamDirectoryContents auth repo commitM srcDir
381
+ & exitOnException
382
+ & writeToDirectory targetDir
0 commit comments