github-backup 0.51.2__py3-none-any.whl → 0.52.0__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/github_backup.py +98 -58
- {github_backup-0.51.2.dist-info → github_backup-0.52.0.dist-info}/METADATA +1 -1
- github_backup-0.52.0.dist-info/RECORD +9 -0
- github_backup-0.51.2.dist-info/RECORD +0 -9
- {github_backup-0.51.2.data → github_backup-0.52.0.data}/scripts/github-backup +0 -0
- {github_backup-0.51.2.dist-info → github_backup-0.52.0.dist-info}/WHEEL +0 -0
- {github_backup-0.51.2.dist-info → github_backup-0.52.0.dist-info}/licenses/LICENSE.txt +0 -0
- {github_backup-0.51.2.dist-info → github_backup-0.52.0.dist-info}/top_level.txt +0 -0
- {github_backup-0.51.2.dist-info → github_backup-0.52.0.dist-info}/zip-safe +0 -0
github_backup/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.
|
|
1
|
+
__version__ = "0.52.0"
|
github_backup/github_backup.py
CHANGED
|
@@ -37,6 +37,15 @@ FNULL = open(os.devnull, "w")
|
|
|
37
37
|
FILE_URI_PREFIX = "file://"
|
|
38
38
|
logger = logging.getLogger(__name__)
|
|
39
39
|
|
|
40
|
+
|
|
41
|
+
class RepositoryUnavailableError(Exception):
|
|
42
|
+
"""Raised when a repository is unavailable due to legal reasons (e.g., DMCA takedown)."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, message, dmca_url=None):
|
|
45
|
+
super().__init__(message)
|
|
46
|
+
self.dmca_url = dmca_url
|
|
47
|
+
|
|
48
|
+
|
|
40
49
|
# Setup SSL context with fallback chain
|
|
41
50
|
https_ctx = ssl.create_default_context()
|
|
42
51
|
if https_ctx.get_ca_certs():
|
|
@@ -592,27 +601,39 @@ def retrieve_data_gen(args, template, query_args=None, single_request=False):
|
|
|
592
601
|
auth = get_auth(args, encode=not args.as_app)
|
|
593
602
|
query_args = get_query_args(query_args)
|
|
594
603
|
per_page = 100
|
|
595
|
-
|
|
604
|
+
next_url = None
|
|
596
605
|
|
|
597
606
|
while True:
|
|
598
607
|
if single_request:
|
|
599
|
-
|
|
608
|
+
request_per_page = None
|
|
600
609
|
else:
|
|
601
|
-
|
|
602
|
-
request_page, request_per_page = page, per_page
|
|
610
|
+
request_per_page = per_page
|
|
603
611
|
|
|
604
612
|
request = _construct_request(
|
|
605
613
|
request_per_page,
|
|
606
|
-
request_page,
|
|
607
614
|
query_args,
|
|
608
|
-
template,
|
|
615
|
+
next_url or template,
|
|
609
616
|
auth,
|
|
610
617
|
as_app=args.as_app,
|
|
611
618
|
fine=True if args.token_fine is not None else False,
|
|
612
619
|
) # noqa
|
|
613
|
-
r, errors = _get_response(request, auth, template)
|
|
620
|
+
r, errors = _get_response(request, auth, next_url or template)
|
|
614
621
|
|
|
615
622
|
status_code = int(r.getcode())
|
|
623
|
+
|
|
624
|
+
# Handle DMCA takedown (HTTP 451) - raise exception to skip entire repository
|
|
625
|
+
if status_code == 451:
|
|
626
|
+
dmca_url = None
|
|
627
|
+
try:
|
|
628
|
+
response_data = json.loads(r.read().decode("utf-8"))
|
|
629
|
+
dmca_url = response_data.get("block", {}).get("html_url")
|
|
630
|
+
except Exception:
|
|
631
|
+
pass
|
|
632
|
+
raise RepositoryUnavailableError(
|
|
633
|
+
"Repository unavailable due to legal reasons (HTTP 451)",
|
|
634
|
+
dmca_url=dmca_url
|
|
635
|
+
)
|
|
636
|
+
|
|
616
637
|
# Check if we got correct data
|
|
617
638
|
try:
|
|
618
639
|
response = json.loads(r.read().decode("utf-8"))
|
|
@@ -644,15 +665,14 @@ def retrieve_data_gen(args, template, query_args=None, single_request=False):
|
|
|
644
665
|
retries += 1
|
|
645
666
|
time.sleep(5)
|
|
646
667
|
request = _construct_request(
|
|
647
|
-
|
|
648
|
-
page,
|
|
668
|
+
request_per_page,
|
|
649
669
|
query_args,
|
|
650
|
-
template,
|
|
670
|
+
next_url or template,
|
|
651
671
|
auth,
|
|
652
672
|
as_app=args.as_app,
|
|
653
673
|
fine=True if args.token_fine is not None else False,
|
|
654
674
|
) # noqa
|
|
655
|
-
r, errors = _get_response(request, auth, template)
|
|
675
|
+
r, errors = _get_response(request, auth, next_url or template)
|
|
656
676
|
|
|
657
677
|
status_code = int(r.getcode())
|
|
658
678
|
try:
|
|
@@ -682,7 +702,16 @@ def retrieve_data_gen(args, template, query_args=None, single_request=False):
|
|
|
682
702
|
if type(response) is list:
|
|
683
703
|
for resp in response:
|
|
684
704
|
yield resp
|
|
685
|
-
|
|
705
|
+
# Parse Link header for next page URL (cursor-based pagination)
|
|
706
|
+
link_header = r.headers.get("Link", "")
|
|
707
|
+
next_url = None
|
|
708
|
+
if link_header:
|
|
709
|
+
# Parse Link header: <https://api.github.com/...?per_page=100&after=cursor>; rel="next"
|
|
710
|
+
for link in link_header.split(","):
|
|
711
|
+
if 'rel="next"' in link:
|
|
712
|
+
next_url = link[link.find("<") + 1:link.find(">")]
|
|
713
|
+
break
|
|
714
|
+
if not next_url:
|
|
686
715
|
break
|
|
687
716
|
elif type(response) is dict and single_request:
|
|
688
717
|
yield response
|
|
@@ -735,22 +764,27 @@ def _get_response(request, auth, template):
|
|
|
735
764
|
|
|
736
765
|
|
|
737
766
|
def _construct_request(
|
|
738
|
-
per_page,
|
|
767
|
+
per_page, query_args, template, auth, as_app=None, fine=False
|
|
739
768
|
):
|
|
740
|
-
|
|
741
|
-
if
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
if query_args:
|
|
746
|
-
all_query_args.update(query_args)
|
|
747
|
-
|
|
748
|
-
request_url = template
|
|
749
|
-
if all_query_args:
|
|
750
|
-
querystring = urlencode(all_query_args)
|
|
751
|
-
request_url = template + "?" + querystring
|
|
769
|
+
# If template is already a full URL with query params (from Link header), use it directly
|
|
770
|
+
if "?" in template and template.startswith("http"):
|
|
771
|
+
request_url = template
|
|
772
|
+
# Extract query string for logging
|
|
773
|
+
querystring = template.split("?", 1)[1]
|
|
752
774
|
else:
|
|
753
|
-
|
|
775
|
+
# Build URL with query parameters
|
|
776
|
+
all_query_args = {}
|
|
777
|
+
if per_page:
|
|
778
|
+
all_query_args["per_page"] = per_page
|
|
779
|
+
if query_args:
|
|
780
|
+
all_query_args.update(query_args)
|
|
781
|
+
|
|
782
|
+
request_url = template
|
|
783
|
+
if all_query_args:
|
|
784
|
+
querystring = urlencode(all_query_args)
|
|
785
|
+
request_url = template + "?" + querystring
|
|
786
|
+
else:
|
|
787
|
+
querystring = ""
|
|
754
788
|
|
|
755
789
|
request = Request(request_url)
|
|
756
790
|
if auth is not None:
|
|
@@ -766,7 +800,7 @@ def _construct_request(
|
|
|
766
800
|
"Accept", "application/vnd.github.machine-man-preview+json"
|
|
767
801
|
)
|
|
768
802
|
|
|
769
|
-
log_url = template
|
|
803
|
+
log_url = template if "?" not in template else template.split("?")[0]
|
|
770
804
|
if querystring:
|
|
771
805
|
log_url += "?" + querystring
|
|
772
806
|
logger.info("Requesting {}".format(log_url))
|
|
@@ -843,8 +877,7 @@ def download_file(url, path, auth, as_app=False, fine=False):
|
|
|
843
877
|
return
|
|
844
878
|
|
|
845
879
|
request = _construct_request(
|
|
846
|
-
per_page=
|
|
847
|
-
page=1,
|
|
880
|
+
per_page=None,
|
|
848
881
|
query_args={},
|
|
849
882
|
template=url,
|
|
850
883
|
auth=auth,
|
|
@@ -1657,40 +1690,47 @@ def backup_repositories(args, output_directory, repositories):
|
|
|
1657
1690
|
|
|
1658
1691
|
continue # don't try to back anything else for a gist; it doesn't exist
|
|
1659
1692
|
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1693
|
+
try:
|
|
1694
|
+
download_wiki = args.include_wiki or args.include_everything
|
|
1695
|
+
if repository["has_wiki"] and download_wiki:
|
|
1696
|
+
fetch_repository(
|
|
1697
|
+
repository["name"],
|
|
1698
|
+
repo_url.replace(".git", ".wiki.git"),
|
|
1699
|
+
os.path.join(repo_cwd, "wiki"),
|
|
1700
|
+
skip_existing=args.skip_existing,
|
|
1701
|
+
bare_clone=args.bare_clone,
|
|
1702
|
+
lfs_clone=args.lfs_clone,
|
|
1703
|
+
no_prune=args.no_prune,
|
|
1704
|
+
)
|
|
1705
|
+
if args.include_issues or args.include_everything:
|
|
1706
|
+
backup_issues(args, repo_cwd, repository, repos_template)
|
|
1673
1707
|
|
|
1674
|
-
|
|
1675
|
-
|
|
1708
|
+
if args.include_pulls or args.include_everything:
|
|
1709
|
+
backup_pulls(args, repo_cwd, repository, repos_template)
|
|
1676
1710
|
|
|
1677
|
-
|
|
1678
|
-
|
|
1711
|
+
if args.include_milestones or args.include_everything:
|
|
1712
|
+
backup_milestones(args, repo_cwd, repository, repos_template)
|
|
1679
1713
|
|
|
1680
|
-
|
|
1681
|
-
|
|
1714
|
+
if args.include_labels or args.include_everything:
|
|
1715
|
+
backup_labels(args, repo_cwd, repository, repos_template)
|
|
1682
1716
|
|
|
1683
|
-
|
|
1684
|
-
|
|
1717
|
+
if args.include_hooks or args.include_everything:
|
|
1718
|
+
backup_hooks(args, repo_cwd, repository, repos_template)
|
|
1685
1719
|
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1720
|
+
if args.include_releases or args.include_everything:
|
|
1721
|
+
backup_releases(
|
|
1722
|
+
args,
|
|
1723
|
+
repo_cwd,
|
|
1724
|
+
repository,
|
|
1725
|
+
repos_template,
|
|
1726
|
+
include_assets=args.include_assets or args.include_everything,
|
|
1727
|
+
)
|
|
1728
|
+
except RepositoryUnavailableError as e:
|
|
1729
|
+
logger.warning(f"Repository {repository['full_name']} is unavailable (HTTP 451)")
|
|
1730
|
+
if e.dmca_url:
|
|
1731
|
+
logger.warning(f"DMCA notice: {e.dmca_url}")
|
|
1732
|
+
logger.info(f"Skipping remaining resources for {repository['full_name']}")
|
|
1733
|
+
continue
|
|
1694
1734
|
|
|
1695
1735
|
if args.incremental:
|
|
1696
1736
|
if last_update == "0000-00-00T00:00:00Z":
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
github_backup/__init__.py,sha256=uXy9jtvBCb6cwZyNng8d7tehPtkx2Xcdr7Lm3SEzEF8,23
|
|
2
|
+
github_backup/github_backup.py,sha256=eo8P69vIgeP_ntzY4DdGrzFnEKPIoqBrgBX3Dw_2lIo,77487
|
|
3
|
+
github_backup-0.52.0.data/scripts/github-backup,sha256=I79TNA67phytRmI9nm_LlD9h2zauXFOsOn9tYyAy9pU,1546
|
|
4
|
+
github_backup-0.52.0.dist-info/licenses/LICENSE.txt,sha256=ML-pLyn1qtAKo_DHMcQ8-p7k3f8cp-IR-mf5B49R2rk,1085
|
|
5
|
+
github_backup-0.52.0.dist-info/METADATA,sha256=92rNj52N87cGNzBGRJYCyaotJIEgZ5ZgZ-DUugpuvlI,21544
|
|
6
|
+
github_backup-0.52.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
+
github_backup-0.52.0.dist-info/top_level.txt,sha256=Za6fKRAlxZ0b3ZBH_osPqklLe14LetdNAkvAQANT2AA,14
|
|
8
|
+
github_backup-0.52.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
9
|
+
github_backup-0.52.0.dist-info/RECORD,,
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
github_backup/__init__.py,sha256=2AuLzuHMBBPVOM-m0Woty11pTO9ExwPKWRWtj1LvS-g,23
|
|
2
|
-
github_backup/github_backup.py,sha256=ousskr9hBtfKgAh1x78vnSefQhPGqa9K2fLxWLUKdyY,75428
|
|
3
|
-
github_backup-0.51.2.data/scripts/github-backup,sha256=I79TNA67phytRmI9nm_LlD9h2zauXFOsOn9tYyAy9pU,1546
|
|
4
|
-
github_backup-0.51.2.dist-info/licenses/LICENSE.txt,sha256=ML-pLyn1qtAKo_DHMcQ8-p7k3f8cp-IR-mf5B49R2rk,1085
|
|
5
|
-
github_backup-0.51.2.dist-info/METADATA,sha256=zoLPP4sNYwAsnFUXTZR1K1NB5J-i2xQidc9DZ3hbkfk,21544
|
|
6
|
-
github_backup-0.51.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
-
github_backup-0.51.2.dist-info/top_level.txt,sha256=Za6fKRAlxZ0b3ZBH_osPqklLe14LetdNAkvAQANT2AA,14
|
|
8
|
-
github_backup-0.51.2.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
9
|
-
github_backup-0.51.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|