github-backup 0.61.0__py3-none-any.whl → 0.61.2__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 +110 -11
- {github_backup-0.61.0.dist-info → github_backup-0.61.2.dist-info}/METADATA +14 -1
- github_backup-0.61.2.dist-info/RECORD +11 -0
- github_backup-0.61.0.dist-info/RECORD +0 -11
- {github_backup-0.61.0.dist-info → github_backup-0.61.2.dist-info}/WHEEL +0 -0
- {github_backup-0.61.0.dist-info → github_backup-0.61.2.dist-info}/entry_points.txt +0 -0
- {github_backup-0.61.0.dist-info → github_backup-0.61.2.dist-info}/licenses/LICENSE.txt +0 -0
- {github_backup-0.61.0.dist-info → github_backup-0.61.2.dist-info}/top_level.txt +0 -0
- {github_backup-0.61.0.dist-info → github_backup-0.61.2.dist-info}/zip-safe +0 -0
github_backup/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.61.
|
|
1
|
+
__version__ = "0.61.2"
|
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
|
@@ -1062,6 +1062,65 @@ def download_attachment_file(url, path, auth, as_app=False, fine=False):
|
|
|
1062
1062
|
return metadata
|
|
1063
1063
|
|
|
1064
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
|
+
|
|
1065
1124
|
def extract_attachment_urls(item_data, issue_number=None, repository_full_name=None):
|
|
1066
1125
|
"""Extract GitHub-hosted attachment URLs from issue/PR body and comments.
|
|
1067
1126
|
|
|
@@ -1415,15 +1474,46 @@ def download_attachments(
|
|
|
1415
1474
|
filename = get_attachment_filename(url)
|
|
1416
1475
|
filepath = os.path.join(attachments_dir, filename)
|
|
1417
1476
|
|
|
1418
|
-
#
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
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
|
|
1425
1484
|
)
|
|
1426
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
|
+
|
|
1427
1517
|
# If download succeeded but we got an extension from Content-Disposition,
|
|
1428
1518
|
# we may need to rename the file to add the extension
|
|
1429
1519
|
if metadata["success"] and metadata.get("original_filename"):
|
|
@@ -1724,7 +1814,7 @@ def backup_repositories(args, output_directory, repositories):
|
|
|
1724
1814
|
if args.include_milestones or args.include_everything:
|
|
1725
1815
|
backup_milestones(args, repo_cwd, repository, repos_template)
|
|
1726
1816
|
|
|
1727
|
-
if args.include_security_advisories or args.include_everything:
|
|
1817
|
+
if args.include_security_advisories or (args.include_everything and not repository["Private"]):
|
|
1728
1818
|
backup_security_advisories(args, repo_cwd, repository, repos_template)
|
|
1729
1819
|
|
|
1730
1820
|
if args.include_labels or args.include_everything:
|
|
@@ -1949,11 +2039,20 @@ def backup_security_advisories(args, repo_cwd, repository, repos_template):
|
|
|
1949
2039
|
return
|
|
1950
2040
|
|
|
1951
2041
|
logger.info("Retrieving {0} security advisories".format(repository["full_name"]))
|
|
1952
|
-
mkdir_p(repo_cwd, advisory_cwd)
|
|
1953
2042
|
|
|
1954
|
-
template = "{0}/{1}/security-advisories".format(
|
|
2043
|
+
template = "{0}/{1}/security-advisories".format(
|
|
2044
|
+
repos_template, repository["full_name"]
|
|
2045
|
+
)
|
|
1955
2046
|
|
|
1956
|
-
|
|
2047
|
+
try:
|
|
2048
|
+
_advisories = retrieve_data(args, template)
|
|
2049
|
+
except Exception as e:
|
|
2050
|
+
if "404" in str(e):
|
|
2051
|
+
logger.info("Security advisories are not available for this repository, skipping")
|
|
2052
|
+
return
|
|
2053
|
+
raise
|
|
2054
|
+
|
|
2055
|
+
mkdir_p(repo_cwd, advisory_cwd)
|
|
1957
2056
|
|
|
1958
2057
|
advisories = {}
|
|
1959
2058
|
for advisory in _advisories:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: github-backup
|
|
3
|
-
Version: 0.61.
|
|
3
|
+
Version: 0.61.2
|
|
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
|
|
@@ -311,6 +311,19 @@ The tool automatically extracts file extensions from HTTP headers to ensure file
|
|
|
311
311
|
|
|
312
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).
|
|
313
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
|
+
|
|
316
|
+
|
|
317
|
+
About security advisories
|
|
318
|
+
-------------------------
|
|
319
|
+
|
|
320
|
+
GitHub security advisories are only available in public repositories. GitHub does not provide the respective API endpoint for private repositories.
|
|
321
|
+
|
|
322
|
+
Therefore the logic is implemented as follows:
|
|
323
|
+
- Security advisories are included in the `--all` option.
|
|
324
|
+
- If only the `--all` option was provided, backups of security advisories are skipped for private repositories.
|
|
325
|
+
- If the `--security-advisories` option is provided (on its own or in addition to `--all`), a backup of security advisories is attempted for all repositories, with graceful handling if the GitHub API doesn't return any.
|
|
326
|
+
|
|
314
327
|
|
|
315
328
|
Run in Docker container
|
|
316
329
|
-----------------------
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
github_backup/__init__.py,sha256=-WIJ_hJF1yvHvtvBO1JNouMIkdi1zl3hnbvBpOOoJXM,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=UJwLQ3NU4TVxmviFVRGX9bC7NPUR-hE3zjpmzEYnzUI,88140
|
|
5
|
+
github_backup-0.61.2.dist-info/licenses/LICENSE.txt,sha256=ML-pLyn1qtAKo_DHMcQ8-p7k3f8cp-IR-mf5B49R2rk,1085
|
|
6
|
+
github_backup-0.61.2.dist-info/METADATA,sha256=TAUA8FnuarPk__xgEBOd5OaNKdV0QPxIFBSxneg0p7s,28144
|
|
7
|
+
github_backup-0.61.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
8
|
+
github_backup-0.61.2.dist-info/entry_points.txt,sha256=cLPKK7MqC77QVb6HnT2mnyL_f-ciavJG_Y9vVHlCF68,57
|
|
9
|
+
github_backup-0.61.2.dist-info/top_level.txt,sha256=Za6fKRAlxZ0b3ZBH_osPqklLe14LetdNAkvAQANT2AA,14
|
|
10
|
+
github_backup-0.61.2.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
11
|
+
github_backup-0.61.2.dist-info/RECORD,,
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
github_backup/__init__.py,sha256=57kaeWxmyt5jDwUTomJhqevhkw_IXJwindDD018ZEtY,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=G3zwRZjwsg2UqY6sQvIG78VyuyrY5mFEeizPZgQe9SM,84189
|
|
5
|
-
github_backup-0.61.0.dist-info/licenses/LICENSE.txt,sha256=ML-pLyn1qtAKo_DHMcQ8-p7k3f8cp-IR-mf5B49R2rk,1085
|
|
6
|
-
github_backup-0.61.0.dist-info/METADATA,sha256=yHsvlCYXWXH3ccO4YWXFsD9tyz1omV5-O31f5u5gmAc,26693
|
|
7
|
-
github_backup-0.61.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
8
|
-
github_backup-0.61.0.dist-info/entry_points.txt,sha256=cLPKK7MqC77QVb6HnT2mnyL_f-ciavJG_Y9vVHlCF68,57
|
|
9
|
-
github_backup-0.61.0.dist-info/top_level.txt,sha256=Za6fKRAlxZ0b3ZBH_osPqklLe14LetdNAkvAQANT2AA,14
|
|
10
|
-
github_backup-0.61.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
11
|
-
github_backup-0.61.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|