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.
- package/README.md +290 -0
- package/dist/git-dl.js +43 -0
- package/dist/git-dl.js.map +1 -0
- package/dist/package.json +138 -0
- package/dist/src/TaggedErrorVerifyingCause.js +27 -0
- package/dist/src/TaggedErrorVerifyingCause.js.map +1 -0
- package/dist/src/castToReadableStream.js +28 -0
- package/dist/src/castToReadableStream.js.map +1 -0
- package/dist/src/cli.js +8 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/commandLineParams.js +172 -0
- package/dist/src/commandLineParams.js.map +1 -0
- package/dist/src/commonErrors.js +60 -0
- package/dist/src/commonErrors.js.map +1 -0
- package/dist/src/configContext.js +12 -0
- package/dist/src/configContext.js.map +1 -0
- package/dist/src/downloadEntityFromRepo.js +21 -0
- package/dist/src/downloadEntityFromRepo.js.map +1 -0
- package/dist/src/errors.js +10 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/getPathContents/ParsedMetaInfoAboutPathContentsFromGitHubAPI.js +43 -0
- package/dist/src/getPathContents/ParsedMetaInfoAboutPathContentsFromGitHubAPI.js.map +1 -0
- package/dist/src/getPathContents/PathContentsMetaInfo.js +71 -0
- package/dist/src/getPathContents/PathContentsMetaInfo.js.map +1 -0
- package/dist/src/getPathContents/RawStreamOfRepoPathContentsFromGitHubAPI.js +4 -0
- package/dist/src/getPathContents/RawStreamOfRepoPathContentsFromGitHubAPI.js.map +1 -0
- package/dist/src/getPathContents/RepoPathContentsFromGitHubAPI.js +51 -0
- package/dist/src/getPathContents/RepoPathContentsFromGitHubAPI.js.map +1 -0
- package/dist/src/getPathContents/index.js +5 -0
- package/dist/src/getPathContents/index.js.map +1 -0
- package/dist/src/getPathContents/parseGitLFSObjectEither.js +70 -0
- package/dist/src/getPathContents/parseGitLFSObjectEither.js.map +1 -0
- package/dist/src/getReadableTarGzStreamOfRepoDirectory.js +39 -0
- package/dist/src/getReadableTarGzStreamOfRepoDirectory.js.map +1 -0
- package/dist/src/index.js +8 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/octokit.js +6 -0
- package/dist/src/octokit.js.map +1 -0
- package/dist/src/unpackRepoFolderTarGzStreamToFs.js +31 -0
- package/dist/src/unpackRepoFolderTarGzStreamToFs.js.map +1 -0
- package/dist/src/writeFileStreamToDestinationPath.js +22 -0
- package/dist/src/writeFileStreamToDestinationPath.js.map +1 -0
- package/dist-types/git-dl.d.ts +3 -0
- package/dist-types/git-dl.d.ts.map +1 -0
- package/dist-types/src/TaggedErrorVerifyingCause.d.ts +38 -0
- package/dist-types/src/TaggedErrorVerifyingCause.d.ts.map +1 -0
- package/dist-types/src/castToReadableStream.d.ts +11 -0
- package/dist-types/src/castToReadableStream.d.ts.map +1 -0
- package/dist-types/src/cli.d.ts +8 -0
- package/dist-types/src/cli.d.ts.map +1 -0
- package/dist-types/src/commandLineParams.d.ts +108 -0
- package/dist-types/src/commandLineParams.d.ts.map +1 -0
- package/dist-types/src/commonErrors.d.ts +60 -0
- package/dist-types/src/commonErrors.d.ts.map +1 -0
- package/dist-types/src/configContext.d.ts +44 -0
- package/dist-types/src/configContext.d.ts.map +1 -0
- package/dist-types/src/downloadEntityFromRepo.d.ts +11 -0
- package/dist-types/src/downloadEntityFromRepo.d.ts.map +1 -0
- package/dist-types/src/errors.d.ts +10 -0
- package/dist-types/src/errors.d.ts.map +1 -0
- package/dist-types/src/getPathContents/ParsedMetaInfoAboutPathContentsFromGitHubAPI.d.ts +141 -0
- package/dist-types/src/getPathContents/ParsedMetaInfoAboutPathContentsFromGitHubAPI.d.ts.map +1 -0
- package/dist-types/src/getPathContents/PathContentsMetaInfo.d.ts +66 -0
- package/dist-types/src/getPathContents/PathContentsMetaInfo.d.ts.map +1 -0
- package/dist-types/src/getPathContents/RawStreamOfRepoPathContentsFromGitHubAPI.d.ts +9 -0
- package/dist-types/src/getPathContents/RawStreamOfRepoPathContentsFromGitHubAPI.d.ts.map +1 -0
- package/dist-types/src/getPathContents/RepoPathContentsFromGitHubAPI.d.ts +81 -0
- package/dist-types/src/getPathContents/RepoPathContentsFromGitHubAPI.d.ts.map +1 -0
- package/dist-types/src/getPathContents/index.d.ts +5 -0
- package/dist-types/src/getPathContents/index.d.ts.map +1 -0
- package/dist-types/src/getPathContents/parseGitLFSObjectEither.d.ts +40 -0
- package/dist-types/src/getPathContents/parseGitLFSObjectEither.d.ts.map +1 -0
- package/dist-types/src/getReadableTarGzStreamOfRepoDirectory.d.ts +13 -0
- package/dist-types/src/getReadableTarGzStreamOfRepoDirectory.d.ts.map +1 -0
- package/dist-types/src/index.d.ts +9 -0
- package/dist-types/src/index.d.ts.map +1 -0
- package/dist-types/src/octokit.d.ts +8 -0
- package/dist-types/src/octokit.d.ts.map +1 -0
- package/dist-types/src/unpackRepoFolderTarGzStreamToFs.d.ts +16 -0
- package/dist-types/src/unpackRepoFolderTarGzStreamToFs.d.ts.map +1 -0
- package/dist-types/src/writeFileStreamToDestinationPath.d.ts +16 -0
- package/dist-types/src/writeFileStreamToDestinationPath.d.ts.map +1 -0
- package/package.json +139 -0
- package/src/TaggedErrorVerifyingCause.ts +142 -0
- package/src/castToReadableStream.ts +44 -0
- package/src/cli.ts +14 -0
- package/src/commandLineParams.ts +257 -0
- package/src/commonErrors.ts +139 -0
- package/src/configContext.ts +46 -0
- package/src/downloadEntityFromRepo.ts +86 -0
- package/src/errors.ts +24 -0
- package/src/getPathContents/ParsedMetaInfoAboutPathContentsFromGitHubAPI.ts +76 -0
- package/src/getPathContents/PathContentsMetaInfo.ts +85 -0
- package/src/getPathContents/RawStreamOfRepoPathContentsFromGitHubAPI.ts +6 -0
- package/src/getPathContents/RepoPathContentsFromGitHubAPI.ts +82 -0
- package/src/getPathContents/index.ts +7 -0
- package/src/getPathContents/parseGitLFSObjectEither.ts +143 -0
- package/src/getReadableTarGzStreamOfRepoDirectory.ts +65 -0
- package/src/index.ts +13 -0
- package/src/octokit.ts +15 -0
- package/src/unpackRepoFolderTarGzStreamToFs.ts +61 -0
- package/src/writeFileStreamToDestinationPath.ts +45 -0
- 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'
|