gsutil.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. #!/usr/bin/env python
  2. # Copyright 2014 The Chromium Authors. All rights reserved.
  3. # Use of this source code is governed by a BSD-style license that can be
  4. # found in the LICENSE file.
  5. """Run a pinned gsutil."""
  6. import argparse
  7. import base64
  8. import contextlib
  9. import hashlib
  10. import json
  11. import os
  12. import shutil
  13. import subprocess
  14. import sys
  15. import tempfile
  16. import time
  17. import urllib2
  18. import zipfile
  19. GSUTIL_URL = 'https://storage.googleapis.com/pub/'
  20. API_URL = 'https://www.googleapis.com/storage/v1/b/pub/o/'
  21. THIS_DIR = os.path.dirname(os.path.abspath(__file__))
  22. DEFAULT_BIN_DIR = os.path.join(THIS_DIR, 'external_bin', 'gsutil')
  23. DEFAULT_FALLBACK_GSUTIL = os.path.join(
  24. THIS_DIR, 'third_party', 'gsutil', 'gsutil')
  25. IS_WINDOWS = os.name == 'nt'
  26. class InvalidGsutilError(Exception):
  27. pass
  28. def download_gsutil(version, target_dir):
  29. """Downloads gsutil into the target_dir."""
  30. filename = 'gsutil_%s.zip' % version
  31. target_filename = os.path.join(target_dir, filename)
  32. # Check if the target exists already.
  33. if os.path.exists(target_filename):
  34. md5_calc = hashlib.md5()
  35. with open(target_filename, 'rb') as f:
  36. while True:
  37. buf = f.read(4096)
  38. if not buf:
  39. break
  40. md5_calc.update(buf)
  41. local_md5 = md5_calc.hexdigest()
  42. metadata_url = '%s%s' % (API_URL, filename)
  43. metadata = json.load(urllib2.urlopen(metadata_url))
  44. remote_md5 = base64.b64decode(metadata['md5Hash'])
  45. if local_md5 == remote_md5:
  46. return target_filename
  47. os.remove(target_filename)
  48. # Do the download.
  49. url = '%s%s' % (GSUTIL_URL, filename)
  50. u = urllib2.urlopen(url)
  51. with open(target_filename, 'wb') as f:
  52. while True:
  53. buf = u.read(4096)
  54. if not buf:
  55. break
  56. f.write(buf)
  57. return target_filename
  58. @contextlib.contextmanager
  59. def temporary_directory(base):
  60. tmpdir = tempfile.mkdtemp(prefix='gsutil_py', dir=base)
  61. try:
  62. yield tmpdir
  63. finally:
  64. if os.path.isdir(tmpdir):
  65. shutil.rmtree(tmpdir)
  66. def ensure_gsutil(version, target, clean):
  67. bin_dir = os.path.join(target, 'gsutil_%s' % version)
  68. gsutil_bin = os.path.join(bin_dir, 'gsutil', 'gsutil')
  69. gsutil_flag = os.path.join(bin_dir, 'gsutil', 'install.flag')
  70. # We assume that if gsutil_flag exists, then we have a good version
  71. # of the gsutil package.
  72. if not clean and os.path.isfile(gsutil_flag):
  73. # Everything is awesome! we're all done here.
  74. return gsutil_bin
  75. if not os.path.exists(target):
  76. os.makedirs(target)
  77. with temporary_directory(target) as instance_dir:
  78. # Clean up if we're redownloading a corrupted gsutil.
  79. cleanup_path = os.path.join(instance_dir, 'clean')
  80. try:
  81. os.rename(bin_dir, cleanup_path)
  82. except (OSError, IOError):
  83. cleanup_path = None
  84. if cleanup_path:
  85. shutil.rmtree(cleanup_path)
  86. download_dir = os.path.join(instance_dir, 'download')
  87. target_zip_filename = download_gsutil(version, instance_dir)
  88. with zipfile.ZipFile(target_zip_filename, 'r') as target_zip:
  89. target_zip.extractall(download_dir)
  90. try:
  91. os.rename(download_dir, bin_dir)
  92. except (OSError, IOError):
  93. # Something else did this in parallel.
  94. pass
  95. # Final check that the gsutil bin exists. This should never fail.
  96. if not os.path.isfile(gsutil_bin):
  97. raise InvalidGsutilError()
  98. # Drop a flag file.
  99. with open(gsutil_flag, 'w') as f:
  100. f.write('This flag file is dropped by gsutil.py')
  101. return gsutil_bin
  102. def run_gsutil(force_version, fallback, target, args, clean=False):
  103. if force_version:
  104. gsutil_bin = ensure_gsutil(force_version, target, clean)
  105. else:
  106. gsutil_bin = fallback
  107. disable_update = ['-o', 'GSUtil:software_update_check_period=0']
  108. if sys.platform == 'cygwin':
  109. # This script requires Windows Python, so invoke with depot_tools'
  110. # Python.
  111. def winpath(path):
  112. return subprocess.check_output(['cygpath', '-w', path]).strip()
  113. cmd = ['python.bat', winpath(__file__)]
  114. cmd.extend(args)
  115. sys.exit(subprocess.call(cmd))
  116. assert sys.platform != 'cygwin'
  117. # Run "gsutil" through "vpython". We need to do this because on GCE instances,
  118. # expectations are made about Python having access to "google-compute-engine"
  119. # and "boto" packages that are not met with non-system Python (e.g., bundles).
  120. cmd = [
  121. 'vpython',
  122. '-vpython-spec', os.path.join(THIS_DIR, 'gsutil.vpython'),
  123. '--',
  124. gsutil_bin
  125. ] + disable_update + args
  126. return subprocess.call(cmd, shell=IS_WINDOWS)
  127. def parse_args():
  128. bin_dir = os.environ.get('DEPOT_TOOLS_GSUTIL_BIN_DIR', DEFAULT_BIN_DIR)
  129. parser = argparse.ArgumentParser()
  130. parser.add_argument('--force-version', default='4.13')
  131. parser.add_argument('--clean', action='store_true',
  132. help='Clear any existing gsutil package, forcing a new download.')
  133. parser.add_argument('--fallback', default=DEFAULT_FALLBACK_GSUTIL)
  134. parser.add_argument('--target', default=bin_dir,
  135. help='The target directory to download/store a gsutil version in. '
  136. '(default is %(default)s).')
  137. parser.add_argument('args', nargs=argparse.REMAINDER)
  138. args, extras = parser.parse_known_args()
  139. if args.args and args.args[0] == '--':
  140. args.args.pop(0)
  141. if extras:
  142. args.args = extras + args.args
  143. return args
  144. def main():
  145. args = parse_args()
  146. return run_gsutil(args.force_version, args.fallback, args.target, args.args,
  147. clean=args.clean)
  148. if __name__ == '__main__':
  149. sys.exit(main())