git-dlp 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.md +290 -0
  2. package/dist/git-dl.js +43 -0
  3. package/dist/git-dl.js.map +1 -0
  4. package/dist/package.json +138 -0
  5. package/dist/src/TaggedErrorVerifyingCause.js +27 -0
  6. package/dist/src/TaggedErrorVerifyingCause.js.map +1 -0
  7. package/dist/src/castToReadableStream.js +28 -0
  8. package/dist/src/castToReadableStream.js.map +1 -0
  9. package/dist/src/cli.js +8 -0
  10. package/dist/src/cli.js.map +1 -0
  11. package/dist/src/commandLineParams.js +172 -0
  12. package/dist/src/commandLineParams.js.map +1 -0
  13. package/dist/src/commonErrors.js +60 -0
  14. package/dist/src/commonErrors.js.map +1 -0
  15. package/dist/src/configContext.js +12 -0
  16. package/dist/src/configContext.js.map +1 -0
  17. package/dist/src/downloadEntityFromRepo.js +21 -0
  18. package/dist/src/downloadEntityFromRepo.js.map +1 -0
  19. package/dist/src/errors.js +10 -0
  20. package/dist/src/errors.js.map +1 -0
  21. package/dist/src/getPathContents/ParsedMetaInfoAboutPathContentsFromGitHubAPI.js +43 -0
  22. package/dist/src/getPathContents/ParsedMetaInfoAboutPathContentsFromGitHubAPI.js.map +1 -0
  23. package/dist/src/getPathContents/PathContentsMetaInfo.js +71 -0
  24. package/dist/src/getPathContents/PathContentsMetaInfo.js.map +1 -0
  25. package/dist/src/getPathContents/RawStreamOfRepoPathContentsFromGitHubAPI.js +4 -0
  26. package/dist/src/getPathContents/RawStreamOfRepoPathContentsFromGitHubAPI.js.map +1 -0
  27. package/dist/src/getPathContents/RepoPathContentsFromGitHubAPI.js +51 -0
  28. package/dist/src/getPathContents/RepoPathContentsFromGitHubAPI.js.map +1 -0
  29. package/dist/src/getPathContents/index.js +5 -0
  30. package/dist/src/getPathContents/index.js.map +1 -0
  31. package/dist/src/getPathContents/parseGitLFSObjectEither.js +70 -0
  32. package/dist/src/getPathContents/parseGitLFSObjectEither.js.map +1 -0
  33. package/dist/src/getReadableTarGzStreamOfRepoDirectory.js +39 -0
  34. package/dist/src/getReadableTarGzStreamOfRepoDirectory.js.map +1 -0
  35. package/dist/src/index.js +8 -0
  36. package/dist/src/index.js.map +1 -0
  37. package/dist/src/octokit.js +6 -0
  38. package/dist/src/octokit.js.map +1 -0
  39. package/dist/src/unpackRepoFolderTarGzStreamToFs.js +31 -0
  40. package/dist/src/unpackRepoFolderTarGzStreamToFs.js.map +1 -0
  41. package/dist/src/writeFileStreamToDestinationPath.js +22 -0
  42. package/dist/src/writeFileStreamToDestinationPath.js.map +1 -0
  43. package/dist-types/git-dl.d.ts +3 -0
  44. package/dist-types/git-dl.d.ts.map +1 -0
  45. package/dist-types/src/TaggedErrorVerifyingCause.d.ts +38 -0
  46. package/dist-types/src/TaggedErrorVerifyingCause.d.ts.map +1 -0
  47. package/dist-types/src/castToReadableStream.d.ts +11 -0
  48. package/dist-types/src/castToReadableStream.d.ts.map +1 -0
  49. package/dist-types/src/cli.d.ts +8 -0
  50. package/dist-types/src/cli.d.ts.map +1 -0
  51. package/dist-types/src/commandLineParams.d.ts +108 -0
  52. package/dist-types/src/commandLineParams.d.ts.map +1 -0
  53. package/dist-types/src/commonErrors.d.ts +60 -0
  54. package/dist-types/src/commonErrors.d.ts.map +1 -0
  55. package/dist-types/src/configContext.d.ts +44 -0
  56. package/dist-types/src/configContext.d.ts.map +1 -0
  57. package/dist-types/src/downloadEntityFromRepo.d.ts +11 -0
  58. package/dist-types/src/downloadEntityFromRepo.d.ts.map +1 -0
  59. package/dist-types/src/errors.d.ts +10 -0
  60. package/dist-types/src/errors.d.ts.map +1 -0
  61. package/dist-types/src/getPathContents/ParsedMetaInfoAboutPathContentsFromGitHubAPI.d.ts +141 -0
  62. package/dist-types/src/getPathContents/ParsedMetaInfoAboutPathContentsFromGitHubAPI.d.ts.map +1 -0
  63. package/dist-types/src/getPathContents/PathContentsMetaInfo.d.ts +66 -0
  64. package/dist-types/src/getPathContents/PathContentsMetaInfo.d.ts.map +1 -0
  65. package/dist-types/src/getPathContents/RawStreamOfRepoPathContentsFromGitHubAPI.d.ts +9 -0
  66. package/dist-types/src/getPathContents/RawStreamOfRepoPathContentsFromGitHubAPI.d.ts.map +1 -0
  67. package/dist-types/src/getPathContents/RepoPathContentsFromGitHubAPI.d.ts +81 -0
  68. package/dist-types/src/getPathContents/RepoPathContentsFromGitHubAPI.d.ts.map +1 -0
  69. package/dist-types/src/getPathContents/index.d.ts +5 -0
  70. package/dist-types/src/getPathContents/index.d.ts.map +1 -0
  71. package/dist-types/src/getPathContents/parseGitLFSObjectEither.d.ts +40 -0
  72. package/dist-types/src/getPathContents/parseGitLFSObjectEither.d.ts.map +1 -0
  73. package/dist-types/src/getReadableTarGzStreamOfRepoDirectory.d.ts +13 -0
  74. package/dist-types/src/getReadableTarGzStreamOfRepoDirectory.d.ts.map +1 -0
  75. package/dist-types/src/index.d.ts +9 -0
  76. package/dist-types/src/index.d.ts.map +1 -0
  77. package/dist-types/src/octokit.d.ts +8 -0
  78. package/dist-types/src/octokit.d.ts.map +1 -0
  79. package/dist-types/src/unpackRepoFolderTarGzStreamToFs.d.ts +16 -0
  80. package/dist-types/src/unpackRepoFolderTarGzStreamToFs.d.ts.map +1 -0
  81. package/dist-types/src/writeFileStreamToDestinationPath.d.ts +16 -0
  82. package/dist-types/src/writeFileStreamToDestinationPath.d.ts.map +1 -0
  83. package/package.json +139 -0
  84. package/src/TaggedErrorVerifyingCause.ts +142 -0
  85. package/src/castToReadableStream.ts +44 -0
  86. package/src/cli.ts +14 -0
  87. package/src/commandLineParams.ts +257 -0
  88. package/src/commonErrors.ts +139 -0
  89. package/src/configContext.ts +46 -0
  90. package/src/downloadEntityFromRepo.ts +86 -0
  91. package/src/errors.ts +24 -0
  92. package/src/getPathContents/ParsedMetaInfoAboutPathContentsFromGitHubAPI.ts +76 -0
  93. package/src/getPathContents/PathContentsMetaInfo.ts +85 -0
  94. package/src/getPathContents/RawStreamOfRepoPathContentsFromGitHubAPI.ts +6 -0
  95. package/src/getPathContents/RepoPathContentsFromGitHubAPI.ts +82 -0
  96. package/src/getPathContents/index.ts +7 -0
  97. package/src/getPathContents/parseGitLFSObjectEither.ts +143 -0
  98. package/src/getReadableTarGzStreamOfRepoDirectory.ts +65 -0
  99. package/src/index.ts +13 -0
  100. package/src/octokit.ts +15 -0
  101. package/src/unpackRepoFolderTarGzStreamToFs.ts +61 -0
  102. package/src/writeFileStreamToDestinationPath.ts +45 -0
  103. package/template.env +13 -0
@@ -0,0 +1,85 @@
1
+ import * as Effect from 'effect/Effect'
2
+
3
+ import { CastToReadableStream } from '../castToReadableStream.ts'
4
+ import { ParsedMetaInfoAboutPathContentsFromGitHubAPI } from './ParsedMetaInfoAboutPathContentsFromGitHubAPI.ts'
5
+ import { parseGitLFSObjectEither } from './parseGitLFSObjectEither.ts'
6
+
7
+ export const PathContentsMetaInfo = Effect.gen(function* () {
8
+ const response = yield* ParsedMetaInfoAboutPathContentsFromGitHubAPI
9
+
10
+ const { type, name, path, size } = response
11
+
12
+ if (type === 'dir') {
13
+ const { entries, sha: treeSha } = response
14
+
15
+ if (name && path)
16
+ return {
17
+ type,
18
+ name,
19
+ path,
20
+ treeSha,
21
+ entries,
22
+ meta: 'This nested directory can be downloaded as a git tree',
23
+ } as const
24
+
25
+ return {
26
+ type,
27
+ treeSha,
28
+ entries,
29
+ meta: 'This root directory of the repo can be downloaded as a git tree',
30
+ } as const
31
+ }
32
+
33
+ // This is quite forgiving implementation. I had the choice to throw
34
+ // errors whenever GitHub's API didn't follow its documentation. I even
35
+ // did that initially, but when I looked at the resulting mess, I changed
36
+ // my mind not to. For example, I could throw errors in the following
37
+ // cases:
38
+
39
+ // 1. When the size is between 1 MB and 100 MB per documentation, I
40
+ // should never receive data. Instead, I should receive an empty
41
+ // "content" field and an "encoding" field equal to "none." If this
42
+ // promise was broken, I could have thrown an error. Instead, if I
43
+ // receive a 50 MB file with the correct encoding, I will parse and
44
+ // return it.
45
+ // 2. Per the documentation, all files larger than 100 MB should be put
46
+ // into Git LFS storage. If I receive a 110 MB file inlined, I'll not
47
+ // fail; I'll parse and return it.
48
+ // 3. Per documentation when size less than 1MB it MUST be inlined. If it
49
+ // wasn't inlined I could have thrown an error, but instead, I just
50
+ // returned an object representing the message "It's a blob, download
51
+ // it elsewhere"
52
+ // 4. Per documentation files larger than 100 mb must be in a git LFS
53
+ // storage and it's assumed that git LFS annotation will be provided.
54
+ // But if it's not provided, instead of throwing an error, I returned
55
+ // an object representing the message "It's a blob, download it
56
+ // elsewhere"
57
+
58
+ // In the end it leads to much lower complexity with a ton of IFs removed
59
+
60
+ const { content, encoding, sha: blobSha, ...restFileObjectFields } = response
61
+ const base = { ...restFileObjectFields, blobSha }
62
+
63
+ if (encoding === 'none')
64
+ return { ...base, meta: 'This file can be downloaded as a blob' } as const
65
+
66
+ const contentAsBuffer = Buffer.from(content, encoding)
67
+
68
+ const potentialGitLFSObject = yield* parseGitLFSObjectEither({
69
+ contentAsBuffer,
70
+ expectedContentSize: size,
71
+ })
72
+
73
+ if (typeof potentialGitLFSObject === 'object')
74
+ return {
75
+ ...base,
76
+ ...potentialGitLFSObject,
77
+ meta: 'This file can be downloaded as a git-LFS object',
78
+ } as const
79
+
80
+ return {
81
+ ...base,
82
+ contentStream: CastToReadableStream(Effect.succeed(contentAsBuffer)),
83
+ meta: 'This file is small enough that GitHub API decided to inline it',
84
+ } as const
85
+ })
@@ -0,0 +1,6 @@
1
+ import { CastToReadableStream } from '../castToReadableStream.ts'
2
+ import { RepoPathContentsFromGitHubAPI } from './RepoPathContentsFromGitHubAPI.ts'
3
+
4
+ export const RawStreamOfRepoPathContentsFromGitHubAPI = CastToReadableStream(
5
+ RepoPathContentsFromGitHubAPI('raw', true),
6
+ )
@@ -0,0 +1,82 @@
1
+ import { RequestError } from '@octokit/request-error'
2
+ import type { OctokitResponse } from '@octokit/types'
3
+
4
+ import * as Cause from 'effect/Cause'
5
+ import * as Effect from 'effect/Effect'
6
+
7
+ import {
8
+ GitHubApiNoCommitFoundForGitRefError,
9
+ GitHubApiRepoIsEmptyError,
10
+ GitHubApiThingNotExistsOrYouDontHaveAccessError,
11
+ parseCommonGitHubApiErrors,
12
+ } from '../commonErrors.ts'
13
+ import { InputConfigTag } from '../configContext.ts'
14
+ import { OctokitTag } from '../octokit.ts'
15
+
16
+ // TODO make better typesignature so that it actually reflects what's returned:
17
+ // stream or parsed body use params as a generic. (but generally fuck github API
18
+ // and their fucking octokit)
19
+ export const RepoPathContentsFromGitHubAPI = Effect.fn(
20
+ 'getRepoPathContentsFromGitHubAPI',
21
+ )(function* (format: 'object' | 'raw', streamBody?: boolean) {
22
+ const octokit = yield* OctokitTag
23
+
24
+ const { gitRef, pathToEntityInRepo, repo } = yield* InputConfigTag
25
+ // TODO: improve sitation on what's default with git ref. Better set null
26
+ // instead of empty string? to signify it's not present actually. Also should
27
+ // note that HEAD could mean something different when ref is not specified.
28
+ // Should consider if we should fallback to HEAD or just not specifiying the
29
+ // property at all.
30
+
31
+ return yield* Effect.tryPromise({
32
+ try: signal =>
33
+ octokit.request('GET /repos/{owner}/{repo}/contents/{path}', {
34
+ owner: repo.owner,
35
+ repo: repo.name,
36
+ path: pathToEntityInRepo,
37
+ ...(gitRef && { ref: gitRef }),
38
+ request: {
39
+ signal,
40
+ parseSuccessResponseBody: !streamBody,
41
+ },
42
+ mediaType: { format },
43
+ headers: {
44
+ 'X-GitHub-Api-Version': '2022-11-28',
45
+ },
46
+ }),
47
+ catch: error => {
48
+ if (!(error instanceof RequestError))
49
+ return new Cause.UnknownException(
50
+ error,
51
+ 'Failed to request contents at the path inside GitHub repo',
52
+ )
53
+
54
+ const potentialErrorMessage = (error.response as ResponseWithError)?.data
55
+ ?.message
56
+
57
+ if (error.status === 404 && potentialErrorMessage)
58
+ return parseNotFoundErrors(potentialErrorMessage, error, gitRef)
59
+
60
+ return parseCommonGitHubApiErrors(error)
61
+ },
62
+ })
63
+ })
64
+
65
+ const parseNotFoundErrors = (
66
+ potentialErrorMessage: string,
67
+ error: RequestError,
68
+ gitRef: string,
69
+ ) => {
70
+ if (potentialErrorMessage === 'This repository is empty.')
71
+ return new GitHubApiRepoIsEmptyError(error)
72
+
73
+ if (gitRef && potentialErrorMessage.startsWith('No commit found for the ref'))
74
+ return new GitHubApiNoCommitFoundForGitRefError(error, { gitRef })
75
+
76
+ return new GitHubApiThingNotExistsOrYouDontHaveAccessError(error)
77
+ }
78
+
79
+ type ResponseWithError = OctokitResponse<
80
+ { message?: string } | undefined,
81
+ number
82
+ >
@@ -0,0 +1,7 @@
1
+ export { FailedToParseResponseFromRepoPathContentsMetaInfoAPIError } from './ParsedMetaInfoAboutPathContentsFromGitHubAPI.ts'
2
+ export { PathContentsMetaInfo } from './PathContentsMetaInfo.ts'
3
+ export {
4
+ FailedToParseGitLFSInfoError,
5
+ InconsistentExpectedAndRealContentSizeError,
6
+ } from './parseGitLFSObjectEither.ts'
7
+ export { RawStreamOfRepoPathContentsFromGitHubAPI } from './RawStreamOfRepoPathContentsFromGitHubAPI.ts'
@@ -0,0 +1,143 @@
1
+ import { outdent } from 'outdent'
2
+
3
+ import * as Either from 'effect/Either'
4
+ import * as ParseResult from 'effect/ParseResult'
5
+ import * as Schema from 'effect/Schema'
6
+
7
+ import {
8
+ buildTaggedErrorClassVerifyingCause,
9
+ type TaggedErrorClass,
10
+ } from '../TaggedErrorVerifyingCause.ts'
11
+
12
+ export const parseGitLFSObjectEither = ({
13
+ contentAsBuffer,
14
+ expectedContentSize,
15
+ }: {
16
+ contentAsBuffer: Buffer<ArrayBuffer>
17
+ expectedContentSize: number
18
+ }) =>
19
+ Either.gen(function* () {
20
+ // gitLFS info usually is no longer than MAX_GIT_LFS_INFO_SIZE bytes
21
+ const contentAsString = contentAsBuffer
22
+ .subarray(0, MAX_GIT_LFS_INFO_SIZE)
23
+ .toString('utf8')
24
+
25
+ const parsingResult = Either.mapLeft(
26
+ decodeGitLFSInfoSchema(contentAsString.match(gitLFSInfoRegexp)?.groups),
27
+ cause =>
28
+ new FailedToParseGitLFSInfoError(cause, {
29
+ partOfContentThatCouldBeGitLFSInfo: contentAsString,
30
+ }),
31
+ )
32
+
33
+ const matchedByRegexpAndParsedByEffectSchema = Either.isRight(parsingResult)
34
+ const doesSizeFromGitLFSInfoAlignWithExpectedContentSize =
35
+ Either.isRight(parsingResult) &&
36
+ parsingResult.right.size === expectedContentSize
37
+
38
+ const shouldFailIfItIsNotGitLFS =
39
+ contentAsBuffer.byteLength !== expectedContentSize
40
+
41
+ const isThisAGitLFSObject =
42
+ matchedByRegexpAndParsedByEffectSchema &&
43
+ doesSizeFromGitLFSInfoAlignWithExpectedContentSize
44
+
45
+ if (isThisAGitLFSObject)
46
+ return {
47
+ gitLFSObjectIdSha256: parsingResult.right.oidSha256,
48
+ gitLFSVersion: parsingResult.right.version,
49
+ } as const
50
+
51
+ if (shouldFailIfItIsNotGitLFS)
52
+ return yield* Either.left(
53
+ new InconsistentExpectedAndRealContentSizeError({
54
+ actual: contentAsBuffer.byteLength,
55
+ expected: expectedContentSize,
56
+ gitLFSInfo: parsingResult,
57
+ }),
58
+ )
59
+
60
+ return 'This is not a git LFS object' as const
61
+ })
62
+
63
+ // there are some responses that look like
64
+ // `version https://git-lfs.github.com/spec/v1
65
+ // oid sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
66
+ // size 128
67
+ // `
68
+ // and the only variable thing in it is the size at the end, and I assume
69
+ // that supported file size is not greater than 100 GB
70
+ const MAX_GIT_LFS_INFO_SIZE = 137
71
+ // Don't add regexp /g modifier, it breaks match groups
72
+ const gitLFSInfoRegexp =
73
+ /^version (?<version>https:\/\/git-lfs\.github\.com\/spec\/v1)\noid sha256:(?<oidSha256>[0-9a-f]{64})\nsize (?<size>[1-9]\d{0,11})\n$/m
74
+
75
+ const GitLFSInfoSchema = Schema.Struct({
76
+ version: Schema.NonEmptyTrimmedString,
77
+ oidSha256: Schema.NonEmptyTrimmedString,
78
+ size: Schema.NumberFromString,
79
+ })
80
+
81
+ const decodeGitLFSInfoSchema = Schema.decodeUnknownEither(GitLFSInfoSchema, {
82
+ exact: true,
83
+ })
84
+
85
+ // Extracting to a separate type is required by JSR, so that consumers of the
86
+ // library will have much faster type inference
87
+ export type FailedToParseGitLFSInfoErrorClass = TaggedErrorClass<{
88
+ ErrorName: 'FailedToParseGitLFSInfoError'
89
+ ExpectedCauseClass: typeof ParseResult.ParseError
90
+ DynamicContext: { partOfContentThatCouldBeGitLFSInfo: string }
91
+ }>
92
+
93
+ const _1: FailedToParseGitLFSInfoErrorClass =
94
+ buildTaggedErrorClassVerifyingCause<{
95
+ partOfContentThatCouldBeGitLFSInfo: string
96
+ }>()(
97
+ 'FailedToParseGitLFSInfoError',
98
+ `Failed to parse git LFS announcement`,
99
+ ParseResult.ParseError,
100
+ )
101
+
102
+ export class FailedToParseGitLFSInfoError extends _1 {}
103
+
104
+ type InconsistentSizesDynamicContext = {
105
+ actual: number
106
+ expected: number
107
+ gitLFSInfo: Either.Either<
108
+ Readonly<{
109
+ version: string
110
+ oidSha256: string
111
+ size: number
112
+ }>,
113
+ InstanceType<FailedToParseGitLFSInfoErrorClass>
114
+ >
115
+ }
116
+
117
+ // Extracting to a separate type is required by JSR, so that consumers of the
118
+ // library will have much faster type inference
119
+
120
+ const _2: TaggedErrorClass<{
121
+ ErrorName: 'InconsistentExpectedAndRealContentSizeError'
122
+ StaticContext: { comment: string }
123
+ DynamicContext: InconsistentSizesDynamicContext
124
+ }> = buildTaggedErrorClassVerifyingCause<InconsistentSizesDynamicContext>()(
125
+ 'InconsistentExpectedAndRealContentSizeError',
126
+ ctx =>
127
+ `Got file with size ${ctx.actual} bytes while expecting ${ctx.expected} bytes`,
128
+ void 0,
129
+ {
130
+ comment: outdent({ newline: ' ' })`
131
+ If we weren't successful in parsing it as git LFS object
132
+ announcement using RegExp and Effect.Schema, we just do a basic size
133
+ consistency check. The check implements the second marker of it
134
+ being a Git LFS object as a backup to checking does "content" look
135
+ like a Git LFS object. If GitHub API's "size" field is different
136
+ from actual size of "content" field, it means either our schema with
137
+ regexp fucked up, or GitHub API did. If it doesn't throw, it means
138
+ there's no reason to assume it's a Git LFS object.
139
+ `,
140
+ },
141
+ )
142
+
143
+ export class InconsistentExpectedAndRealContentSizeError extends _2 {}
@@ -0,0 +1,65 @@
1
+ import { RequestError } from '@octokit/request-error'
2
+
3
+ import * as Cause from 'effect/Cause'
4
+ import * as Effect from 'effect/Effect'
5
+ import * as EFunction from 'effect/Function'
6
+
7
+ import { CastToReadableStream } from './castToReadableStream.ts'
8
+ import {
9
+ GitHubApiGeneralUserError,
10
+ parseCommonGitHubApiErrors,
11
+ } from './commonErrors.ts'
12
+ import { InputConfigTag } from './configContext.ts'
13
+ import { OctokitTag } from './octokit.ts'
14
+
15
+ export const getReadableTarGzStreamOfRepoDirectory = (
16
+ gitRefWhichWillBeUsedToIdentifyGitTree?: string,
17
+ ) =>
18
+ EFunction.pipe(
19
+ requestTarballFromGitHubAPI(gitRefWhichWillBeUsedToIdentifyGitTree),
20
+ Effect.map(({ data }) => data),
21
+ CastToReadableStream,
22
+ )
23
+
24
+ // TODO: better return type signature so that we actually show what's returned
25
+ // buffer, stream or something else. Octokit sucks
26
+ const requestTarballFromGitHubAPI = (
27
+ gitRefWhichWillBeUsedToIdentifyGitTree = '',
28
+ ) =>
29
+ Effect.gen(function* () {
30
+ const octokit = yield* OctokitTag
31
+
32
+ const {
33
+ repo: { owner, name },
34
+ } = yield* InputConfigTag
35
+
36
+ return yield* Effect.tryPromise({
37
+ try: signal =>
38
+ octokit.request('GET /repos/{owner}/{repo}/tarball/{ref}', {
39
+ owner,
40
+ repo: name,
41
+ ref: gitRefWhichWillBeUsedToIdentifyGitTree,
42
+ request: {
43
+ signal,
44
+ parseSuccessResponseBody: false,
45
+ },
46
+ headers: {
47
+ 'X-GitHub-Api-Version': '2022-11-28',
48
+ },
49
+ }),
50
+ catch: error => {
51
+ if (!(error instanceof RequestError))
52
+ return new Cause.UnknownException(
53
+ error,
54
+ 'Failed to request .tar.gz file from GitHub API',
55
+ )
56
+
57
+ if (error.status === 400)
58
+ return new GitHubApiGeneralUserError(error, {
59
+ notes: 'Error happened probably because you asked for empty repo',
60
+ })
61
+
62
+ return parseCommonGitHubApiErrors(error)
63
+ },
64
+ })
65
+ })
package/src/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @module
3
+ */
4
+
5
+ export * from './cli.ts'
6
+ export type {
7
+ InputConfig,
8
+ OutputConfig,
9
+ SingleTargetConfig,
10
+ } from './configContext.ts'
11
+ export { downloadEntityFromRepo } from './downloadEntityFromRepo.ts'
12
+ export * from './errors.ts'
13
+ export { OctokitLayer } from './octokit.ts'
package/src/octokit.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { Octokit, type OctokitOptions } from '@octokit/core'
2
+
3
+ import * as Context from 'effect/Context'
4
+ import * as Layer from 'effect/Layer'
5
+
6
+ // Extracting to a separate type is required by JSR, so that consumers of the
7
+ // library will have much faster type inference
8
+ type OctokitTag = Context.Tag<Octokit, Octokit>
9
+
10
+ export const OctokitTag: OctokitTag = Context.GenericTag<Octokit>('OctokitTag')
11
+
12
+ export const OctokitLayer: (
13
+ options?: OctokitOptions,
14
+ ) => Layer.Layer<Octokit, never, never> = (options?: OctokitOptions) =>
15
+ Layer.succeed(OctokitTag, OctokitTag.of(new Octokit(options)))
@@ -0,0 +1,61 @@
1
+ import type { Readable } from 'node:stream'
2
+ import { pipeline } from 'node:stream/promises'
3
+ import { createGunzip } from 'node:zlib'
4
+
5
+ import { extract } from 'tar-fs'
6
+
7
+ import * as Effect from 'effect/Effect'
8
+
9
+ import { OutputConfigTag } from './configContext.ts'
10
+ import {
11
+ buildTaggedErrorClassVerifyingCause,
12
+ type TaggedErrorClass,
13
+ } from './TaggedErrorVerifyingCause.ts'
14
+
15
+ // TODO: Use this maybe for tar.gz unpacking?
16
+ // https://github.com/leonitousconforti/eftar
17
+ // TODO: Or implement my own wrapper around node:zlib?
18
+
19
+ export const unpackRepoFolderTarGzStreamToFs = <E, R>(
20
+ self: Effect.Effect<Readable, E, R>,
21
+ ) =>
22
+ Effect.gen(function* () {
23
+ const tarGzStream = yield* self
24
+
25
+ const {
26
+ localPathAtWhichEntityFromRepoWillBeAvailable:
27
+ pathToLocalDirWhichWillHaveContentsOfRepoDir,
28
+ } = yield* OutputConfigTag
29
+
30
+ yield* Effect.tryPromise({
31
+ try: signal =>
32
+ pipeline(
33
+ tarGzStream,
34
+ createGunzip(),
35
+ extract(pathToLocalDirWhichWillHaveContentsOfRepoDir, {
36
+ map: header => {
37
+ // GitHub creates archive with nested dir inside which has all
38
+ // the files we need, so we remove this dir's name from the
39
+ // beginning
40
+ header.name = header.name.replace(/^[^/]*\/(.*)/, '$1')
41
+ return header
42
+ },
43
+ }),
44
+ { signal },
45
+ ),
46
+ catch: cause =>
47
+ new FailedToUnpackRepoFolderTarGzStreamToFsError({ cause }),
48
+ })
49
+ })
50
+
51
+ // Extracting to a separate type is required by JSR, so that consumers of the
52
+ // library will have much faster type inference
53
+ const _1: TaggedErrorClass<{
54
+ ErrorName: 'FailedToUnpackRepoFolderTarGzStreamToFsError'
55
+ DynamicContext: { cause: unknown }
56
+ }> = buildTaggedErrorClassVerifyingCause<{ cause: unknown }>()(
57
+ 'FailedToUnpackRepoFolderTarGzStreamToFsError',
58
+ 'Error: Failed to unpack to fs received from GitHub .tar.gz stream of repo folder contents',
59
+ )
60
+
61
+ export class FailedToUnpackRepoFolderTarGzStreamToFsError extends _1 {}
@@ -0,0 +1,45 @@
1
+ import { createWriteStream } from 'node:fs'
2
+ import type { Readable } from 'node:stream'
3
+ import { pipeline } from 'node:stream/promises'
4
+
5
+ import * as Effect from 'effect/Effect'
6
+
7
+ import { OutputConfigTag } from './configContext.ts'
8
+ import {
9
+ buildTaggedErrorClassVerifyingCause,
10
+ type TaggedErrorClass,
11
+ } from './TaggedErrorVerifyingCause.ts'
12
+
13
+ // TODO: use effect platform streams
14
+
15
+ export const writeFileStreamToDestinationPath = <E, R>(
16
+ self: Effect.Effect<Readable, E, R>,
17
+ ) =>
18
+ Effect.gen(function* () {
19
+ const fileStream = yield* self
20
+
21
+ const {
22
+ localPathAtWhichEntityFromRepoWillBeAvailable: localDownloadedFilePath,
23
+ } = yield* OutputConfigTag
24
+
25
+ yield* Effect.tryPromise({
26
+ try: signal =>
27
+ pipeline(fileStream, createWriteStream(localDownloadedFilePath), {
28
+ signal,
29
+ }),
30
+ catch: cause =>
31
+ new FailedToWriteFileStreamToDestinationPathError({ cause }),
32
+ })
33
+ })
34
+
35
+ // Extracting to a separate type is required by JSR, so that consumers of the
36
+ // library will have much faster type inference
37
+ const _1: TaggedErrorClass<{
38
+ ErrorName: 'FailedToWriteFileStreamToDestinationPathError'
39
+ DynamicContext: { cause: unknown }
40
+ }> = buildTaggedErrorClassVerifyingCause<{ cause: unknown }>()(
41
+ 'FailedToWriteFileStreamToDestinationPathError',
42
+ 'Error: Failed to write file stream to destination path',
43
+ )
44
+
45
+ export class FailedToWriteFileStreamToDestinationPathError extends _1 {}
package/template.env ADDED
@@ -0,0 +1,13 @@
1
+ # Get here: https://github.com/settings/tokens/new?description=Read%20repo%20contents%20access%20to%20fetch-github-folder&scopes=public_repo&default_expires_at=none
2
+ GITHUB_ACCESS_TOKEN='ghp_1234567890abcdefghij'
3
+
4
+ REPO_OWNER='apache'
5
+
6
+ REPO_NAME='superset'
7
+
8
+ PATH_TO_ENTITY_IN_REPO='docker'
9
+
10
+ # If commented, default branch in repo will be used
11
+ # GIT_REF='main'
12
+
13
+ DESTINATION_PATH='/tmp/tmp.pPuwKB2gSZ/docker'