@@ -18,6 +18,7 @@ import subprocess
1818import sys
1919import time
2020import platform
21+ PY2 = False
2122try :
2223 # python 3
2324 from urllib .parse import urlparse
2627 from urllib .error import HTTPError , URLError
2728 from urllib .request import urlopen
2829 from urllib .request import Request
30+ from urllib .request import HTTPRedirectHandler
31+ from urllib .request import build_opener
2932except ImportError :
3033 # python 2
34+ PY2 = True
3135 from urlparse import urlparse
3236 from urllib import quote as urlquote
3337 from urllib import urlencode
3438 from urllib2 import HTTPError , URLError
3539 from urllib2 import urlopen
3640 from urllib2 import Request
41+ from urllib2 import HTTPRedirectHandler
42+ from urllib2 import build_opener
3743
3844from github_backup import __version__
3945
@@ -303,6 +309,15 @@ def parse_args():
303309 parser .add_argument ('--keychain-account' ,
304310 dest = 'osx_keychain_item_account' ,
305311 help = 'OSX ONLY: account field of password item in OSX keychain that holds the personal access or OAuth token' )
312+ parser .add_argument ('--releases' ,
313+ action = 'store_true' ,
314+ dest = 'include_releases' ,
315+ help = 'include release information, not including assets or binaries'
316+ )
317+ parser .add_argument ('--assets' ,
318+ action = 'store_true' ,
319+ dest = 'include_assets' ,
320+ help = 'include assets alongside release information; only applies if including releases' )
306321 return parser .parse_args ()
307322
308323
@@ -532,6 +547,39 @@ def _request_url_error(template, retry_timeout):
532547 return False
533548
534549
550+ class S3HTTPRedirectHandler (HTTPRedirectHandler ):
551+ """
552+ A subclassed redirect handler for downloading Github assets from S3.
553+
554+ urllib will add the Authorization header to the redirected request to S3, which will result in a 400,
555+ so we should remove said header on redirect.
556+ """
557+ def redirect_request (self , req , fp , code , msg , headers , newurl ):
558+ if PY2 :
559+ # HTTPRedirectHandler is an old style class
560+ request = HTTPRedirectHandler .redirect_request (self , req , fp , code , msg , headers , newurl )
561+ else :
562+ request = super (S3HTTPRedirectHandler , self ).redirect_request (req , fp , code , msg , headers , newurl )
563+ del request .headers ['Authorization' ]
564+ return request
565+
566+
567+ def download_file (url , path , auth ):
568+ request = Request (url )
569+ request .add_header ('Accept' , 'application/octet-stream' )
570+ request .add_header ('Authorization' , 'Basic ' .encode ('ascii' ) + auth )
571+ opener = build_opener (S3HTTPRedirectHandler )
572+ response = opener .open (request )
573+
574+ chunk_size = 16 * 1024
575+ with open (path , 'wb' ) as f :
576+ while True :
577+ chunk = response .read (chunk_size )
578+ if not chunk :
579+ break
580+ f .write (chunk )
581+
582+
535583def get_authenticated_user (args ):
536584 template = 'https://{0}/user' .format (get_github_api_host (args ))
537585 data = retrieve_data (args , template , single_request = True )
@@ -699,6 +747,10 @@ def backup_repositories(args, output_directory, repositories):
699747 if args .include_hooks or args .include_everything :
700748 backup_hooks (args , repo_cwd , repository , repos_template )
701749
750+ if args .include_releases or args .include_everything :
751+ backup_releases (args , repo_cwd , repository , repos_template ,
752+ include_assets = args .include_assets or args .include_everything )
753+
702754 if args .incremental :
703755 open (last_update_path , 'w' ).write (last_update )
704756
@@ -880,6 +932,33 @@ def backup_hooks(args, repo_cwd, repository, repos_template):
880932 log_info ("Unable to read hooks, skipping" )
881933
882934
935+ def backup_releases (args , repo_cwd , repository , repos_template , include_assets = False ):
936+ repository_fullname = repository ['full_name' ]
937+
938+ # give release files somewhere to live & log intent
939+ release_cwd = os .path .join (repo_cwd , 'releases' )
940+ log_info ('Retrieving {0} releases' .format (repository_fullname ))
941+ mkdir_p (repo_cwd , release_cwd )
942+
943+ query_args = {}
944+
945+ release_template = '{0}/{1}/releases' .format (repos_template , repository_fullname )
946+ releases = retrieve_data (args , release_template , query_args = query_args )
947+
948+ # for each release, store it
949+ log_info ('Saving {0} releases to disk' .format (len (releases )))
950+ for release in releases :
951+ release_name = release ['tag_name' ]
952+ output_filepath = os .path .join (release_cwd , '{0}.json' .format (release_name ))
953+ with codecs .open (output_filepath , 'w+' , encoding = 'utf-8' ) as f :
954+ json_dump (release , f )
955+
956+ if include_assets :
957+ assets = retrieve_data (args , release ['assets_url' ])
958+ for asset in assets :
959+ download_file (asset ['url' ], os .path .join (release_cwd , asset ['name' ]), get_auth (args ))
960+
961+
883962def fetch_repository (name ,
884963 remote_url ,
885964 local_dir ,
0 commit comments