github-backup 0.60.0__py3-none-any.whl → 0.61.1__py3-none-any.whl
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.
- github_backup/__init__.py +1 -1
- github_backup/cli.py +10 -0
- github_backup/github_backup.py +143 -7
- {github_backup-0.60.0.dist-info → github_backup-0.61.1.dist-info}/METADATA +10 -6
- github_backup-0.61.1.dist-info/RECORD +11 -0
- github_backup-0.60.0.dist-info/RECORD +0 -11
- {github_backup-0.60.0.dist-info → github_backup-0.61.1.dist-info}/WHEEL +0 -0
- {github_backup-0.60.0.dist-info → github_backup-0.61.1.dist-info}/entry_points.txt +0 -0
- {github_backup-0.60.0.dist-info → github_backup-0.61.1.dist-info}/licenses/LICENSE.txt +0 -0
- {github_backup-0.60.0.dist-info → github_backup-0.61.1.dist-info}/top_level.txt +0 -0
- {github_backup-0.60.0.dist-info → github_backup-0.61.1.dist-info}/zip-safe +0 -0
github_backup/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.
|
|
1
|
+
__version__ = "0.61.1"
|
github_backup/cli.py
CHANGED
|
@@ -46,6 +46,16 @@ def main():
|
|
|
46
46
|
"Use -t/--token or -f/--token-fine to authenticate."
|
|
47
47
|
)
|
|
48
48
|
|
|
49
|
+
# Issue #477: Fine-grained PATs cannot download all attachment types from
|
|
50
|
+
# private repos. Image attachments will be retried via Markdown API workaround.
|
|
51
|
+
if args.include_attachments and args.token_fine:
|
|
52
|
+
logger.warning(
|
|
53
|
+
"Using --attachments with fine-grained token. Due to GitHub platform "
|
|
54
|
+
"limitations, file attachments (PDFs, etc.) from private repos may fail. "
|
|
55
|
+
"Image attachments will be retried via workaround. For full attachment "
|
|
56
|
+
"support, use --token-classic instead."
|
|
57
|
+
)
|
|
58
|
+
|
|
49
59
|
if args.quiet:
|
|
50
60
|
logger.setLevel(logging.WARNING)
|
|
51
61
|
|
github_backup/github_backup.py
CHANGED
|
@@ -310,6 +310,12 @@ def parse_args(args=None):
|
|
|
310
310
|
dest="include_milestones",
|
|
311
311
|
help="include milestones in backup",
|
|
312
312
|
)
|
|
313
|
+
parser.add_argument(
|
|
314
|
+
"--security-advisories",
|
|
315
|
+
action="store_true",
|
|
316
|
+
dest="include_security_advisories",
|
|
317
|
+
help="include security advisories in backup",
|
|
318
|
+
)
|
|
313
319
|
parser.add_argument(
|
|
314
320
|
"--repositories",
|
|
315
321
|
action="store_true",
|
|
@@ -1056,6 +1062,65 @@ def download_attachment_file(url, path, auth, as_app=False, fine=False):
|
|
|
1056
1062
|
return metadata
|
|
1057
1063
|
|
|
1058
1064
|
|
|
1065
|
+
def get_jwt_signed_url_via_markdown_api(url, token, repo_context):
|
|
1066
|
+
"""Convert a user-attachments/assets URL to a JWT-signed URL via Markdown API.
|
|
1067
|
+
|
|
1068
|
+
GitHub's Markdown API renders image URLs and returns HTML containing
|
|
1069
|
+
JWT-signed private-user-images.githubusercontent.com URLs that work
|
|
1070
|
+
without token authentication.
|
|
1071
|
+
|
|
1072
|
+
This is a workaround for issue #477 where fine-grained PATs cannot
|
|
1073
|
+
download user-attachments URLs from private repos directly.
|
|
1074
|
+
|
|
1075
|
+
Limitations:
|
|
1076
|
+
- Only works for /assets/ URLs (images)
|
|
1077
|
+
- Does NOT work for /files/ URLs (PDFs, text files, etc.)
|
|
1078
|
+
- JWT URLs expire after ~5 minutes
|
|
1079
|
+
|
|
1080
|
+
Args:
|
|
1081
|
+
url: The github.com/user-attachments/assets/UUID URL
|
|
1082
|
+
token: Raw fine-grained PAT (github_pat_...)
|
|
1083
|
+
repo_context: Repository context as "owner/repo"
|
|
1084
|
+
|
|
1085
|
+
Returns:
|
|
1086
|
+
str: JWT-signed URL from private-user-images.githubusercontent.com
|
|
1087
|
+
None: If conversion fails
|
|
1088
|
+
"""
|
|
1089
|
+
|
|
1090
|
+
try:
|
|
1091
|
+
payload = json.dumps(
|
|
1092
|
+
{"text": f"", "mode": "gfm", "context": repo_context}
|
|
1093
|
+
).encode("utf-8")
|
|
1094
|
+
|
|
1095
|
+
request = Request("https://api.github.com/markdown", data=payload, method="POST")
|
|
1096
|
+
request.add_header("Authorization", f"token {token}")
|
|
1097
|
+
request.add_header("Content-Type", "application/json")
|
|
1098
|
+
request.add_header("Accept", "application/vnd.github+json")
|
|
1099
|
+
|
|
1100
|
+
html = urlopen(request, timeout=30).read().decode("utf-8")
|
|
1101
|
+
|
|
1102
|
+
# Parse JWT-signed URL from HTML response
|
|
1103
|
+
# Format: <img src="https://private-user-images.githubusercontent.com/...?jwt=..." ...>
|
|
1104
|
+
if match := re.search(
|
|
1105
|
+
r'src="(https://private-user-images\.githubusercontent\.com/[^"]+)"', html
|
|
1106
|
+
):
|
|
1107
|
+
jwt_url = match.group(1)
|
|
1108
|
+
logger.debug("Converted attachment URL to JWT-signed URL via Markdown API")
|
|
1109
|
+
return jwt_url
|
|
1110
|
+
|
|
1111
|
+
logger.debug("Markdown API response did not contain JWT-signed URL")
|
|
1112
|
+
return None
|
|
1113
|
+
|
|
1114
|
+
except HTTPError as e:
|
|
1115
|
+
logger.debug(
|
|
1116
|
+
"Markdown API request failed with HTTP {0}: {1}".format(e.code, e.reason)
|
|
1117
|
+
)
|
|
1118
|
+
return None
|
|
1119
|
+
except Exception as e:
|
|
1120
|
+
logger.debug("Markdown API request failed: {0}".format(str(e)))
|
|
1121
|
+
return None
|
|
1122
|
+
|
|
1123
|
+
|
|
1059
1124
|
def extract_attachment_urls(item_data, issue_number=None, repository_full_name=None):
|
|
1060
1125
|
"""Extract GitHub-hosted attachment URLs from issue/PR body and comments.
|
|
1061
1126
|
|
|
@@ -1409,15 +1474,46 @@ def download_attachments(
|
|
|
1409
1474
|
filename = get_attachment_filename(url)
|
|
1410
1475
|
filepath = os.path.join(attachments_dir, filename)
|
|
1411
1476
|
|
|
1412
|
-
#
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1477
|
+
# Issue #477: Fine-grained PATs cannot download user-attachments/assets
|
|
1478
|
+
# from private repos directly (404). Use Markdown API workaround to get
|
|
1479
|
+
# a JWT-signed URL. Only works for /assets/ (images), not /files/.
|
|
1480
|
+
needs_jwt = (
|
|
1481
|
+
args.token_fine is not None
|
|
1482
|
+
and repository.get("private", False)
|
|
1483
|
+
and "github.com/user-attachments/assets/" in url
|
|
1419
1484
|
)
|
|
1420
1485
|
|
|
1486
|
+
if not needs_jwt:
|
|
1487
|
+
# NORMAL download path
|
|
1488
|
+
metadata = download_attachment_file(
|
|
1489
|
+
url,
|
|
1490
|
+
filepath,
|
|
1491
|
+
get_auth(args, encode=not args.as_app),
|
|
1492
|
+
as_app=args.as_app,
|
|
1493
|
+
fine=args.token_fine is not None,
|
|
1494
|
+
)
|
|
1495
|
+
elif jwt_url := get_jwt_signed_url_via_markdown_api(
|
|
1496
|
+
url, args.token_fine, repository["full_name"]
|
|
1497
|
+
):
|
|
1498
|
+
# JWT needed and extracted, download via JWT
|
|
1499
|
+
metadata = download_attachment_file(
|
|
1500
|
+
jwt_url, filepath, auth=None, as_app=False, fine=False
|
|
1501
|
+
)
|
|
1502
|
+
metadata["url"] = url # Apply back the original URL
|
|
1503
|
+
metadata["jwt_workaround"] = True
|
|
1504
|
+
else:
|
|
1505
|
+
# Markdown API workaround failed - skip download we know will fail
|
|
1506
|
+
metadata = {
|
|
1507
|
+
"url": url,
|
|
1508
|
+
"success": False,
|
|
1509
|
+
"skipped_at": datetime.now(timezone.utc).isoformat(),
|
|
1510
|
+
"error": "Fine-grained token cannot download private repo attachments. "
|
|
1511
|
+
"Markdown API workaround failed. Use --token-classic instead.",
|
|
1512
|
+
}
|
|
1513
|
+
logger.warning(
|
|
1514
|
+
"Skipping attachment {0}: {1}".format(url, metadata["error"])
|
|
1515
|
+
)
|
|
1516
|
+
|
|
1421
1517
|
# If download succeeded but we got an extension from Content-Disposition,
|
|
1422
1518
|
# we may need to rename the file to add the extension
|
|
1423
1519
|
if metadata["success"] and metadata.get("original_filename"):
|
|
@@ -1718,6 +1814,9 @@ def backup_repositories(args, output_directory, repositories):
|
|
|
1718
1814
|
if args.include_milestones or args.include_everything:
|
|
1719
1815
|
backup_milestones(args, repo_cwd, repository, repos_template)
|
|
1720
1816
|
|
|
1817
|
+
if args.include_security_advisories or args.include_everything:
|
|
1818
|
+
backup_security_advisories(args, repo_cwd, repository, repos_template)
|
|
1819
|
+
|
|
1721
1820
|
if args.include_labels or args.include_everything:
|
|
1722
1821
|
backup_labels(args, repo_cwd, repository, repos_template)
|
|
1723
1822
|
|
|
@@ -1934,6 +2033,43 @@ def backup_milestones(args, repo_cwd, repository, repos_template):
|
|
|
1934
2033
|
)
|
|
1935
2034
|
|
|
1936
2035
|
|
|
2036
|
+
def backup_security_advisories(args, repo_cwd, repository, repos_template):
|
|
2037
|
+
advisory_cwd = os.path.join(repo_cwd, "security-advisories")
|
|
2038
|
+
if args.skip_existing and os.path.isdir(advisory_cwd):
|
|
2039
|
+
return
|
|
2040
|
+
|
|
2041
|
+
logger.info("Retrieving {0} security advisories".format(repository["full_name"]))
|
|
2042
|
+
mkdir_p(repo_cwd, advisory_cwd)
|
|
2043
|
+
|
|
2044
|
+
template = "{0}/{1}/security-advisories".format(
|
|
2045
|
+
repos_template, repository["full_name"]
|
|
2046
|
+
)
|
|
2047
|
+
|
|
2048
|
+
_advisories = retrieve_data(args, template)
|
|
2049
|
+
|
|
2050
|
+
advisories = {}
|
|
2051
|
+
for advisory in _advisories:
|
|
2052
|
+
advisories[advisory["ghsa_id"]] = advisory
|
|
2053
|
+
|
|
2054
|
+
written_count = 0
|
|
2055
|
+
for ghsa_id, advisory in list(advisories.items()):
|
|
2056
|
+
advisory_file = "{0}/{1}.json".format(advisory_cwd, ghsa_id)
|
|
2057
|
+
if json_dump_if_changed(advisory, advisory_file):
|
|
2058
|
+
written_count += 1
|
|
2059
|
+
|
|
2060
|
+
total = len(advisories)
|
|
2061
|
+
if written_count == total:
|
|
2062
|
+
logger.info("Saved {0} security advisories to disk".format(total))
|
|
2063
|
+
elif written_count == 0:
|
|
2064
|
+
logger.info("{0} security advisories unchanged, skipped write".format(total))
|
|
2065
|
+
else:
|
|
2066
|
+
logger.info(
|
|
2067
|
+
"Saved {0} of {1} security advisories to disk ({2} unchanged)".format(
|
|
2068
|
+
written_count, total, total - written_count
|
|
2069
|
+
)
|
|
2070
|
+
)
|
|
2071
|
+
|
|
2072
|
+
|
|
1937
2073
|
def backup_labels(args, repo_cwd, repository, repos_template):
|
|
1938
2074
|
label_cwd = os.path.join(repo_cwd, "labels")
|
|
1939
2075
|
output_file = "{0}/labels.json".format(label_cwd)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: github-backup
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.61.1
|
|
4
4
|
Summary: backup a github user or organization
|
|
5
5
|
Home-page: http://github.com/josegonzalez/python-github-backup
|
|
6
6
|
Author: Jose Diaz-Gonzalez
|
|
@@ -73,9 +73,9 @@ CLI Help output::
|
|
|
73
73
|
[--watched] [--followers] [--following] [--all]
|
|
74
74
|
[--issues] [--issue-comments] [--issue-events] [--pulls]
|
|
75
75
|
[--pull-comments] [--pull-commits] [--pull-details]
|
|
76
|
-
[--labels] [--hooks] [--milestones] [--
|
|
77
|
-
[--bare] [--no-prune] [--lfs] [--wikis]
|
|
78
|
-
[--starred-gists] [--skip-archived] [--skip-existing]
|
|
76
|
+
[--labels] [--hooks] [--milestones] [--security-advisories]
|
|
77
|
+
[--repositories] [--bare] [--no-prune] [--lfs] [--wikis]
|
|
78
|
+
[--gists] [--starred-gists] [--skip-archived] [--skip-existing]
|
|
79
79
|
[-L [LANGUAGES ...]] [-N NAME_REGEX] [-H GITHUB_HOST]
|
|
80
80
|
[-O] [-R REPOSITORY] [-P] [-F] [--prefer-ssh] [-v]
|
|
81
81
|
[--keychain-name OSX_KEYCHAIN_ITEM_NAME]
|
|
@@ -85,7 +85,7 @@ CLI Help output::
|
|
|
85
85
|
[--skip-assets-on [SKIP_ASSETS_ON ...]] [--attachments]
|
|
86
86
|
[--throttle-limit THROTTLE_LIMIT]
|
|
87
87
|
[--throttle-pause THROTTLE_PAUSE]
|
|
88
|
-
[--exclude [EXCLUDE ...]]
|
|
88
|
+
[--exclude [EXCLUDE ...]] [--retries MAX_RETRIES]
|
|
89
89
|
USER
|
|
90
90
|
|
|
91
91
|
Backup a github account
|
|
@@ -131,6 +131,8 @@ CLI Help output::
|
|
|
131
131
|
--hooks include hooks in backup (works only when
|
|
132
132
|
authenticated)
|
|
133
133
|
--milestones include milestones in backup
|
|
134
|
+
--security-advisories
|
|
135
|
+
include security advisories in backup
|
|
134
136
|
--repositories include repository clone in backup
|
|
135
137
|
--bare clone bare repositories
|
|
136
138
|
--no-prune disable prune option for git fetch
|
|
@@ -309,6 +311,8 @@ The tool automatically extracts file extensions from HTTP headers to ensure file
|
|
|
309
311
|
|
|
310
312
|
**Repository filtering** for repo files/assets handles renamed and transferred repositories gracefully. URLs are included if they either match the current repository name directly, or redirect to it (e.g., ``willmcgugan/rich`` redirects to ``Textualize/rich`` after transfer).
|
|
311
313
|
|
|
314
|
+
**Fine-grained token limitation:** Due to a GitHub platform limitation, fine-grained personal access tokens (``github_pat_...``) cannot download attachments from private repositories directly. This affects both ``/assets/`` (images) and ``/files/`` (documents) URLs. The tool implements a workaround for image attachments using GitHub's Markdown API, which converts URLs to temporary JWT-signed URLs that can be downloaded. However, this workaround only works for images - document attachments (PDFs, text files, etc.) will fail with 404 errors when using fine-grained tokens on private repos. For full attachment support on private repositories, use a classic token (``-t``) instead of a fine-grained token (``-f``). See `#477 <https://github.com/josegonzalez/python-github-backup/issues/477>`_ for details.
|
|
315
|
+
|
|
312
316
|
|
|
313
317
|
Run in Docker container
|
|
314
318
|
-----------------------
|
|
@@ -431,7 +435,7 @@ Quietly and incrementally backup useful Github user data (public and private rep
|
|
|
431
435
|
export FINE_ACCESS_TOKEN=SOME-GITHUB-TOKEN
|
|
432
436
|
GH_USER=YOUR-GITHUB-USER
|
|
433
437
|
|
|
434
|
-
github-backup -f $FINE_ACCESS_TOKEN --prefer-ssh -o ~/github-backup/ -l error -P -i --all-starred --starred --watched --followers --following --issues --issue-comments --issue-events --pulls --pull-comments --pull-commits --labels --milestones --repositories --wikis --releases --assets --attachments --pull-details --gists --starred-gists $GH_USER
|
|
438
|
+
github-backup -f $FINE_ACCESS_TOKEN --prefer-ssh -o ~/github-backup/ -l error -P -i --all-starred --starred --watched --followers --following --issues --issue-comments --issue-events --pulls --pull-comments --pull-commits --labels --milestones --security-advisories --repositories --wikis --releases --assets --attachments --pull-details --gists --starred-gists $GH_USER
|
|
435
439
|
|
|
436
440
|
Debug an error/block or incomplete backup into a temporary directory. Omit "incremental" to fill a previous incomplete backup. ::
|
|
437
441
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
github_backup/__init__.py,sha256=3UxN07YtETzDhpq7DYM8JY8aOjsIEIETvTVen65rK2k,23
|
|
2
|
+
github_backup/__main__.py,sha256=3yorCiC63maaxbjdtaeAs-G-yAAxKSO3mprhGfa1Fho,271
|
|
3
|
+
github_backup/cli.py,sha256=gPYU0CviuHB19y8RHhcD0p8xHS3UcJbBIK6HVfWazTc,2902
|
|
4
|
+
github_backup/github_backup.py,sha256=ApN6orMb-BlJJVI_PTZZkzteEzuZBhTySydlRWU-Bi0,87911
|
|
5
|
+
github_backup-0.61.1.dist-info/licenses/LICENSE.txt,sha256=ML-pLyn1qtAKo_DHMcQ8-p7k3f8cp-IR-mf5B49R2rk,1085
|
|
6
|
+
github_backup-0.61.1.dist-info/METADATA,sha256=-iHVrHGbT0z0R9VqXbltR7pRxDV18aOnfFdofA73K3s,27503
|
|
7
|
+
github_backup-0.61.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
8
|
+
github_backup-0.61.1.dist-info/entry_points.txt,sha256=cLPKK7MqC77QVb6HnT2mnyL_f-ciavJG_Y9vVHlCF68,57
|
|
9
|
+
github_backup-0.61.1.dist-info/top_level.txt,sha256=Za6fKRAlxZ0b3ZBH_osPqklLe14LetdNAkvAQANT2AA,14
|
|
10
|
+
github_backup-0.61.1.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
11
|
+
github_backup-0.61.1.dist-info/RECORD,,
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
github_backup/__init__.py,sha256=XCDU0PzRSGNchH5cFRqniZdytnPgXmacSgzeC0BxXiI,23
|
|
2
|
-
github_backup/__main__.py,sha256=3yorCiC63maaxbjdtaeAs-G-yAAxKSO3mprhGfa1Fho,271
|
|
3
|
-
github_backup/cli.py,sha256=ifnOne5AaEI7laofgIq3geURJtvbA-bzxSQgLcmZgd0,2345
|
|
4
|
-
github_backup/github_backup.py,sha256=DJ1mFWELLAZBbM8UXmsHOxin3OBGFIFZk_SXQdhv_7w,82562
|
|
5
|
-
github_backup-0.60.0.dist-info/licenses/LICENSE.txt,sha256=ML-pLyn1qtAKo_DHMcQ8-p7k3f8cp-IR-mf5B49R2rk,1085
|
|
6
|
-
github_backup-0.60.0.dist-info/METADATA,sha256=Lwu1h4UYm6z8OUq9iieqgS_7ITlsB-oQujHB0h302PM,26529
|
|
7
|
-
github_backup-0.60.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
8
|
-
github_backup-0.60.0.dist-info/entry_points.txt,sha256=cLPKK7MqC77QVb6HnT2mnyL_f-ciavJG_Y9vVHlCF68,57
|
|
9
|
-
github_backup-0.60.0.dist-info/top_level.txt,sha256=Za6fKRAlxZ0b3ZBH_osPqklLe14LetdNAkvAQANT2AA,14
|
|
10
|
-
github_backup-0.60.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
11
|
-
github_backup-0.60.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|