| OLD | NEW |
| (Empty) |
| 1 # Copyright 2010 Google Inc. | |
| 2 # | |
| 3 # Licensed under the Apache License, Version 2.0 (the "License"); | |
| 4 # you may not use this file except in compliance with the License. | |
| 5 # You may obtain a copy of the License at | |
| 6 # | |
| 7 # http://www.apache.org/licenses/LICENSE-2.0 | |
| 8 # | |
| 9 # Unless required by applicable law or agreed to in writing, software | |
| 10 # distributed under the License is distributed on an "AS IS" BASIS, | |
| 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 12 # See the License for the specific language governing permissions and | |
| 13 # limitations under the License. | |
| 14 | |
| 15 """Implementation of gsutil commands.""" | |
| 16 | |
| 17 import ctypes | |
| 18 import datetime | |
| 19 import gzip | |
| 20 import mimetypes | |
| 21 import os | |
| 22 import platform | |
| 23 import re | |
| 24 import shutil | |
| 25 import signal | |
| 26 import sys | |
| 27 import tarfile | |
| 28 import tempfile | |
| 29 import time | |
| 30 import webbrowser | |
| 31 import xml.dom.minidom | |
| 32 import xml.sax.xmlreader | |
| 33 import boto | |
| 34 import boto.s3.connection | |
| 35 | |
| 36 from boto import handler | |
| 37 from boto.gs.resumable_upload_handler import ResumableUploadHandler | |
| 38 from boto.provider import Provider | |
| 39 from boto.pyami.config import BotoConfigLocations | |
| 40 from boto.s3.resumable_download_handler import ResumableDownloadHandler | |
| 41 from boto.storage_uri import BucketStorageUri | |
| 42 from exception import CommandException | |
| 43 from gslib.project_id import ProjectIdHandler | |
| 44 import wildcard_iterator | |
| 45 from wildcard_iterator import ContainsWildcard | |
| 46 from wildcard_iterator import ResultType | |
| 47 from wildcard_iterator import WildcardException | |
| 48 | |
| 49 GOOG_API_CONSOLE_URI = "http://code.google.com/apis/console" | |
| 50 | |
| 51 _HAVE_OAUTH2 = False | |
| 52 try: | |
| 53 from oauth2_plugin import oauth2_helper | |
| 54 _HAVE_OAUTH2 = True | |
| 55 except ImportError: | |
| 56 pass | |
| 57 | |
| 58 # Enum class for specifying listing style. | |
| 59 class ListingStyle(object): | |
| 60 SHORT = 'SHORT' | |
| 61 LONG = 'LONG' | |
| 62 LONG_LONG = 'LONG_LONG' | |
| 63 | |
| 64 | |
| 65 # Binary exponentiation strings. | |
| 66 EXP_STRINGS = [ | |
| 67 (0, 'B'), | |
| 68 (10, 'KB'), | |
| 69 (20, 'MB'), | |
| 70 (30, 'GB'), | |
| 71 (40, 'TB'), | |
| 72 (50, 'PB'), | |
| 73 ] | |
| 74 | |
| 75 ONE_MB = 1024*1024 | |
| 76 | |
| 77 SCOPE_FULL_CONTROL = 'https://www.googleapis.com/auth/devstorage.full_control' | |
| 78 SCOPE_READ_WRITE = 'https://www.googleapis.com/auth/devstorage.read_write' | |
| 79 SCOPE_READ_ONLY = 'https://www.googleapis.com/auth/devstorage.read_only' | |
| 80 | |
| 81 CONFIG_PRELUDE_CONTENT = """ | |
| 82 # This file contains credentials and other configuration information needed | |
| 83 # by the boto library, used by gsutil. You can edit this file (e.g., to add | |
| 84 # credentials) but be careful not to mis-edit any of the variable names (like | |
| 85 # "gs_access_key_id") or remove important markers (like the "[Credentials]" and | |
| 86 # "[Boto]" section delimeters). | |
| 87 # | |
| 88 """ | |
| 89 | |
| 90 CONFIG_BOTO_SECTION_CONTENT = """ | |
| 91 [Boto] | |
| 92 | |
| 93 # To use a proxy, edit and uncomment the proxy and proxy_port lines. If you | |
| 94 # need a user/password with this proxy, edit and uncomment those lines as well. | |
| 95 #proxy = <proxy host> | |
| 96 #proxy_port = <proxy port> | |
| 97 #proxy_user = <your proxy user name> | |
| 98 #proxy_pass = <your proxy password> | |
| 99 | |
| 100 # The following two options control the use of a secure transport for requests | |
| 101 # to S3 and Google Storage. It is highly recommended to set both options to | |
| 102 # True in production environments, especially when using OAuth2 bearer token | |
| 103 # authentication with Google Storage. | |
| 104 | |
| 105 # Set 'is_secure' to False to cause boto to connect using HTTP instead of the | |
| 106 # default HTTPS. This is useful if you want to capture/analyze traffic | |
| 107 # (e.g., with tcpdump). This option should always be set to True in production | |
| 108 # environments. | |
| 109 #is_secure = False | |
| 110 | |
| 111 # Set 'https_validate_certificates' to False to disable server certificate | |
| 112 # checking. This is useful if you want to capture/analyze traffic using an | |
| 113 # intercepting proxy. This option should always be set to True in production | |
| 114 # environments. | |
| 115 # In gsutil, the default for this option is True. *However*, the default for | |
| 116 # this option in the boto library itself is currently 'False'; it is therefore | |
| 117 # recommended to always set this option explicitly to True in configuration | |
| 118 # files. | |
| 119 https_validate_certificates = True | |
| 120 | |
| 121 # 'debug' controls the level of debug messages printed: 0 for none, 1 | |
| 122 # for basic boto debug, 2 for all boto debug plus HTTP requests/responses. | |
| 123 # Note: 'gsutil -d' sets debug to 2 for that one command run. | |
| 124 #debug = <0, 1, or 2> | |
| 125 | |
| 126 # 'num_retries' controls the number of retry attempts made when errors occur. | |
| 127 # The default is 5. Note: don't set this value to 0, as it will cause boto to | |
| 128 # fail when reusing HTTP connections. | |
| 129 #num_retries = <integer value> | |
| 130 """ | |
| 131 | |
| 132 CONFIG_INPUTLESS_GSUTIL_SECTION_CONTENT = """ | |
| 133 [GSUtil] | |
| 134 | |
| 135 # 'resumable_threshold' specifies the smallest file size [bytes] for which | |
| 136 # resumable Google Storage transfers are attempted. The default is 1048576 | |
| 137 # (1MB). | |
| 138 #resumable_threshold = 1048576 | |
| 139 | |
| 140 # 'resumable_tracker_dir' specifies the base location where resumable | |
| 141 # transfer tracker files are saved. By default they're in ~/.gsutil | |
| 142 #resumable_tracker_dir = <file path> | |
| 143 | |
| 144 # 'default_api_version' specifies the default Google Storage API version to | |
| 145 # use use. If not set below gsutil defaults to API version 1. | |
| 146 default_api_version = 2 | |
| 147 """ | |
| 148 | |
| 149 CONFIG_OAUTH2_CONFIG_CONTENT = """ | |
| 150 [OAuth2] | |
| 151 # This section specifies options used with OAuth2 authentication. | |
| 152 | |
| 153 # 'token_cache' specifies how the OAuth2 client should cache access tokens. | |
| 154 # Valid values are: | |
| 155 # 'in_memory': an in-memory cache is used. This is only useful if the boto | |
| 156 # client instance (and with it the OAuth2 plugin instance) persists | |
| 157 # across multiple requests. | |
| 158 # 'file_system' : access tokens will be cached in the file system, in files | |
| 159 # whose names include a key derived from the refresh token the access token | |
| 160 # based on. | |
| 161 # The default is 'file_system'. | |
| 162 #token_cache = file_system | |
| 163 #token_cache = in_memory | |
| 164 | |
| 165 # 'token_cache_path_pattern' specifies a path pattern for token cache files. | |
| 166 # This option is only relevant if token_cache = file_system. | |
| 167 # The value of this option should be a path, with place-holders '%(key)s' (which | |
| 168 # will be replaced with a key derived from the refresh token the cached access | |
| 169 # token was based on), and (optionally), %(uid)s (which will be replaced with | |
| 170 # the UID of the current user, if available via os.getuid()). | |
| 171 # Note that the config parser itself interpolates '%' placeholders, and hence | |
| 172 # the above placeholders need to be escaped as '%%(key)s'. | |
| 173 # The default value of this option is | |
| 174 # token_cache_path_pattern = <tmpdir>/oauth2client-tokencache.%%(uid)s.%%(key)s | |
| 175 # where <tmpdir> is the system-dependent default temp directory. | |
| 176 | |
| 177 # The following options specify the OAuth2 client identity and secret that is | |
| 178 # used when requesting and using OAuth2 tokens. If not specified, a default | |
| 179 # OAuth2 client for the gsutil tool is used; for uses of the boto library (with | |
| 180 # OAuth2 authentication plugin) in other client software, it is recommended to | |
| 181 # use a tool/client-specific OAuth2 client. For more information on OAuth2, see | |
| 182 # http://code.google.com/apis/accounts/docs/OAuth2.html | |
| 183 #client_id = <OAuth2 client id> | |
| 184 #client_secret = <OAuth2 client secret> | |
| 185 | |
| 186 # The following options specify the label and endpoint URIs for the OAUth2 | |
| 187 # authorization provider being used. Primarily useful for tool developers. | |
| 188 #provider_label = Google | |
| 189 #provider_authorization_uri = https://accounts.google.com/o/oauth2/auth | |
| 190 #provider_token_uri = https://accounts.google.com/o/oauth2/token | |
| 191 """ | |
| 192 | |
| 193 CONFIG_COMMAND_HELP = """ | |
| 194 Help on the gsutil config command: | |
| 195 gsutil [-D] config [OPTION] | |
| 196 | |
| 197 The gsutil config command obtains access credentials for Google Storage, and | |
| 198 writes a boto/gsutil configuration file with the obtained credentials. | |
| 199 | |
| 200 Unless specified otherwise, the configuration file is written to the default | |
| 201 config file path '%s'. If the default config file already exists, an attempt | |
| 202 is made to rename the existing file to a backup file '%s'; if that attempt | |
| 203 fails the command will exit. | |
| 204 | |
| 205 A different destination file can be specified with the -o <file> option (use | |
| 206 '-o -' to write the config to standard output). If the specified file already | |
| 207 exists, the command will fail. | |
| 208 | |
| 209 By default, gsutil config obtains OAuth2 tokens as follows (for background | |
| 210 on OAuth2, see http://code.google.com/apis/accounts/docs/OAuth2.html): | |
| 211 The command asks the user to open a web broswer to a URL for Google's | |
| 212 OAuth2 authorization page. In the browser, the user will be asked to sign | |
| 213 into the user's Google Account, unless already signed in. The user is then | |
| 214 prompted to authorize gsutil to access the user's Google Storage account | |
| 215 on the user's behalf. If the user approves the request, a verification | |
| 216 code is shown. The gsutil config command prompts for this verification | |
| 217 code, which is used to obtain an OAuth2 token that is written to the | |
| 218 configuration file. | |
| 219 | |
| 220 The -b option can be used to instruct gsutil config to launch a browser, | |
| 221 (using python's webbrowser module) to navigate to Google's OAuth2 | |
| 222 authorization page. Note that this will probably not work as expected | |
| 223 if you are running gsutil from an ssh window, or using gsutil on Windows. | |
| 224 | |
| 225 The -r, -w, -f options cause gsutil config to request a token with restricted | |
| 226 scope; the resulting token will be restricted to read-only operations, | |
| 227 read-write operation, or all operations (including getacl/setacl operations). | |
| 228 In addition, -s <scope> can be used to request additional (non-Google-Storage) | |
| 229 scopes. | |
| 230 | |
| 231 If no explicit scope option is given, -f (full control) is assumed by default. | |
| 232 | |
| 233 The -a option can be used to prompt for Google Storage access key and secret | |
| 234 instead. | |
| 235 | |
| 236 Options: | |
| 237 -h Print this help. | |
| 238 -a Prompt for Google Storage access key and secret instead of | |
| 239 obtaining an OAuth2 token. | |
| 240 -b Launch browser to obtain OAuth2 approval and project ID instead | |
| 241 of showing the URL and asking user to open the browser. | |
| 242 -D Print debug output. | |
| 243 -f Request token with full-control access (default). | |
| 244 -o <file> Write the configuration to <file> (use '-' for stdout) | |
| 245 -r Request token restricted to read-only access. | |
| 246 -s <scope> Request additional OAuth2 <scope>. | |
| 247 -w Request token restricted to read-write access. | |
| 248 | |
| 249 """ | |
| 250 | |
| 251 def MakeHumanReadable(num): | |
| 252 """Generates human readable string for a number. | |
| 253 | |
| 254 Args: | |
| 255 num: the number | |
| 256 | |
| 257 Returns: | |
| 258 A string form of the number using size abbreviations (KB, MB, etc.) | |
| 259 """ | |
| 260 i = 0 | |
| 261 while i+1 < len(EXP_STRINGS) and num >= (2 ** EXP_STRINGS[i+1][0]): | |
| 262 i += 1 | |
| 263 rounded_val = round(float(num) / 2 ** EXP_STRINGS[i][0], 2) | |
| 264 return '%s %s' % (rounded_val, EXP_STRINGS[i][1]) | |
| 265 | |
| 266 | |
| 267 def UriStrFor(iterated_uri, obj): | |
| 268 """Constructs a StorageUri string for the given iterated_uri and object. | |
| 269 | |
| 270 For example if we were iterating gs://*, obj could be an object in one | |
| 271 of the user's buckets enumerated by the ls command. | |
| 272 | |
| 273 Args: | |
| 274 iterated_uri: base StorageUri being iterated. | |
| 275 obj: object being listed. | |
| 276 | |
| 277 Returns: | |
| 278 URI string. | |
| 279 """ | |
| 280 return '%s://%s/%s' % (iterated_uri.scheme, obj.bucket.name, obj.name) | |
| 281 | |
| 282 | |
| 283 def OpenConfigFile(file_path): | |
| 284 """Creates and opens a configuration file for writing. | |
| 285 | |
| 286 The file is created with mode 0600, and attempts to open existing files will | |
| 287 fail (the latter is important to prevent symlink attacks). | |
| 288 | |
| 289 It is the caller's responsibility to close the file. | |
| 290 | |
| 291 Args: | |
| 292 file_path: Path of the file to be created. | |
| 293 | |
| 294 Returns: | |
| 295 A writable file object for the opened file. | |
| 296 | |
| 297 Raises: | |
| 298 CommandException: if an error occurred when opening the file (including when | |
| 299 the file already exists). | |
| 300 """ | |
| 301 flags = os.O_RDWR | os.O_CREAT | os.O_EXCL | |
| 302 # Accommodate Windows; stolen from python2.6/tempfile.py. | |
| 303 if hasattr(os, 'O_NOINHERIT'): | |
| 304 flags |= os.O_NOINHERIT | |
| 305 try: | |
| 306 fd = os.open(file_path, flags, 0600) | |
| 307 except (OSError, IOError), e: | |
| 308 raise CommandException("Failed to open %s for writing: %s" % | |
| 309 (file_path, e)) | |
| 310 return os.fdopen(fd, "w") | |
| 311 | |
| 312 | |
| 313 class Command(object): | |
| 314 """Class that contains all gsutil command code.""" | |
| 315 | |
| 316 def __init__(self, gsutil_bin_dir, boto_lib_dir, usage_string, | |
| 317 config_file_list, bucket_storage_uri_class=BucketStorageUri): | |
| 318 """Instantiates Command class. | |
| 319 | |
| 320 Args: | |
| 321 gsutil_bin_dir: bin dir from which gsutil is running. | |
| 322 boto_lib_dir: lib dir where boto runs. | |
| 323 usage_string: usage string to print when user makes command error. | |
| 324 config_file_list: config file list returned by GetBotoConfigFileList(). | |
| 325 bucket_storage_uri_class: Class to instantiate for cloud StorageUris. | |
| 326 Settable for testing/mocking. | |
| 327 """ | |
| 328 self.gsutil_bin_dir = gsutil_bin_dir | |
| 329 self.usage_string = usage_string | |
| 330 self.boto_lib_dir = boto_lib_dir | |
| 331 self.config_file_list = config_file_list | |
| 332 self.bucket_storage_uri_class = bucket_storage_uri_class | |
| 333 | |
| 334 config = boto.config | |
| 335 self.proj_id_handler = ProjectIdHandler() | |
| 336 | |
| 337 def OutputUsageAndExit(self): | |
| 338 sys.stderr.write(self.usage_string) | |
| 339 sys.exit(0) | |
| 340 | |
| 341 def StorageUri(self, uri_str, debug=0, validate=True): | |
| 342 """ | |
| 343 Helper to instantiate boto.StorageUri with gsutil default flag values. | |
| 344 Uses self.bucket_storage_uri_class to support mocking/testing. | |
| 345 | |
| 346 Args: | |
| 347 uri_str: StorageUri naming bucket + optional object. | |
| 348 debug: debug level to pass in to boto connection (range 0..3). | |
| 349 validate: Whether to check for bucket name validity. | |
| 350 | |
| 351 Returns: | |
| 352 boto.StorageUri for given uri_str. | |
| 353 | |
| 354 Raises: | |
| 355 InvalidUriError: if uri_str not valid. | |
| 356 """ | |
| 357 return boto.storage_uri( | |
| 358 uri_str, 'file', debug=debug, validate=validate, | |
| 359 bucket_storage_uri_class=self.bucket_storage_uri_class) | |
| 360 | |
| 361 def CmdWildcardIterator(self, uri_or_str, result_type=ResultType.URIS, | |
| 362 headers=None, debug=0): | |
| 363 """ | |
| 364 Helper to instantiate gslib.WildcardIterator, passing | |
| 365 self.bucket_storage_uri_class to support mocking/testing. | |
| 366 Args are same as gslib.WildcardIterator interface, but without the | |
| 367 bucket_storage_uri_class param (which is instead filled in from Command | |
| 368 class state). | |
| 369 """ | |
| 370 return wildcard_iterator.wildcard_iterator( | |
| 371 uri_or_str, self.proj_id_handler, result_type=result_type, | |
| 372 bucket_storage_uri_class=self.bucket_storage_uri_class, | |
| 373 headers=headers, debug=debug) | |
| 374 | |
| 375 def InsistUriNamesContainer(self, command, uri): | |
| 376 """Checks that URI names a directory or bucket. | |
| 377 | |
| 378 Args: | |
| 379 command: command being run | |
| 380 uri: StorageUri to check | |
| 381 | |
| 382 Raises: | |
| 383 CommandException: if errors encountered. | |
| 384 """ | |
| 385 if uri.names_singleton(): | |
| 386 raise CommandException('Destination StorageUri must name a bucket or ' | |
| 387 'directory for the\nmultiple source form of the ' | |
| 388 '"%s" command.' % command) | |
| 389 | |
| 390 def CatCommand(self, args, sub_opts=None, headers=None, debug=0): | |
| 391 """Implementation of cat command. | |
| 392 | |
| 393 Args: | |
| 394 args: command-line argument list. | |
| 395 sub_opts: list of command-specific options from getopt. | |
| 396 headers: dictionary containing optional HTTP headers to pass to boto. | |
| 397 debug: debug level to pass in to boto connection (range 0..3). | |
| 398 | |
| 399 Raises: | |
| 400 CommandException: if errors encountered. | |
| 401 """ | |
| 402 show_header = False | |
| 403 if sub_opts: | |
| 404 for o, unused_a in sub_opts: | |
| 405 if o == '-h': | |
| 406 show_header = True | |
| 407 | |
| 408 printed_one = False | |
| 409 for uri_str in args: | |
| 410 for uri in self.CmdWildcardIterator(uri_str, headers=headers, | |
| 411 debug=debug): | |
| 412 if not uri.object_name: | |
| 413 raise CommandException('"cat" command must specify objects.') | |
| 414 if show_header: | |
| 415 if printed_one: | |
| 416 print | |
| 417 print '==> %s <==' % uri.__str__() | |
| 418 printed_one = True | |
| 419 tmp_file = tempfile.TemporaryFile() | |
| 420 key = uri.get_key(False, headers) | |
| 421 key.get_file(tmp_file, headers) | |
| 422 tmp_file.seek(0) | |
| 423 while True: | |
| 424 # Use 8k buffer size. | |
| 425 data = tmp_file.read(8192) | |
| 426 if not data: | |
| 427 break | |
| 428 sys.stdout.write(data) | |
| 429 tmp_file.close() | |
| 430 | |
| 431 def SetAclCommand(self, args, unused_sub_opts=None, headers=None, debug=0): | |
| 432 """Implementation of setacl command. | |
| 433 | |
| 434 Args: | |
| 435 args: command-line argument list. | |
| 436 unused_sub_opts: list of command-specific options from getopt. | |
| 437 headers: dictionary containing optional HTTP headers to pass to boto. | |
| 438 debug: debug level to pass in to boto connection (range 0..3). | |
| 439 | |
| 440 Raises: | |
| 441 CommandException: if errors encountered. | |
| 442 """ | |
| 443 acl_arg = args[0] | |
| 444 uri_args = args[1:] | |
| 445 provider = None | |
| 446 first_uri = None | |
| 447 # Do a first pass over all matched objects to disallow multi-provider | |
| 448 # setacl requests, because there are differences in the ACL models. | |
| 449 for uri_str in uri_args: | |
| 450 for uri in self.CmdWildcardIterator(uri_str, headers=headers, | |
| 451 debug=debug): | |
| 452 if not provider: | |
| 453 provider = uri.scheme | |
| 454 elif uri.scheme != provider: | |
| 455 raise CommandException('"setacl" command spanning providers not ' | |
| 456 'allowed.') | |
| 457 if not first_uri: | |
| 458 first_uri = uri | |
| 459 | |
| 460 # Get ACL object from connection for the first URI, for interpreting the | |
| 461 # ACL. This won't fail because the main startup code insists on 1 arg | |
| 462 # for this command. | |
| 463 storage_uri = first_uri | |
| 464 acl_class = storage_uri.acl_class() | |
| 465 canned_acls = storage_uri.canned_acls() | |
| 466 | |
| 467 # Determine whether acl_arg names a file containing XML ACL text vs. the | |
| 468 # string name of a canned ACL. | |
| 469 if os.path.isfile(acl_arg): | |
| 470 acl_file = open(acl_arg, 'r') | |
| 471 acl_txt = acl_file.read() | |
| 472 acl_file.close() | |
| 473 acl_obj = acl_class() | |
| 474 h = handler.XmlHandler(acl_obj, storage_uri.get_bucket()) | |
| 475 try: | |
| 476 xml.sax.parseString(acl_txt, h) | |
| 477 except xml.sax._exceptions.SAXParseException, e: | |
| 478 raise CommandException('Requested ACL is invalid: %s at line %s, ' | |
| 479 'column %s' % (e.getMessage(), e.getLineNumber(), | |
| 480 e.getColumnNumber())) | |
| 481 acl_arg = acl_obj | |
| 482 else: | |
| 483 # No file exists, so expect a canned ACL string. | |
| 484 if acl_arg not in canned_acls: | |
| 485 raise CommandException('Invalid canned ACL "%s".' % acl_arg) | |
| 486 | |
| 487 # Now iterate over URIs and set the ACL on each. | |
| 488 for uri_str in uri_args: | |
| 489 for uri in self.CmdWildcardIterator(uri_str, headers=headers, | |
| 490 debug=debug): | |
| 491 print 'Setting ACL on %s...' % uri | |
| 492 uri.set_acl(acl_arg, uri.object_name, False, headers) | |
| 493 | |
| 494 def ExplainIfSudoNeeded(self, tf, dirs_to_remove): | |
| 495 """Explains what to do if sudo needed to update gsutil software. | |
| 496 | |
| 497 Happens if gsutil was previously installed by a different user (typically if | |
| 498 someone originally installed in a shared file system location, using sudo). | |
| 499 | |
| 500 Args: | |
| 501 tf: opened TarFile. | |
| 502 dirs_to_remove: list of directories to remove. | |
| 503 | |
| 504 Raises: | |
| 505 CommandException: if errors encountered. | |
| 506 """ | |
| 507 system = platform.system() | |
| 508 # If running under Windows we don't need (or have) sudo. | |
| 509 if system.lower().startswith('windows'): | |
| 510 return | |
| 511 | |
| 512 user_id = os.getuid() | |
| 513 if (os.stat(self.gsutil_bin_dir).st_uid == user_id and | |
| 514 os.stat(self.boto_lib_dir).st_uid == user_id): | |
| 515 return | |
| 516 | |
| 517 # Won't fail - this command runs after main startup code that insists on | |
| 518 # having a config file. | |
| 519 config_file = self.config_file_list | |
| 520 self.CleanUpUpdateCommand(tf, dirs_to_remove) | |
| 521 raise CommandException( | |
| 522 ('Since it was installed by a different user previously, you will need ' | |
| 523 'to update using the following commands.\nYou will be prompted for ' | |
| 524 'your password, and the install will run as "root". If you\'re unsure ' | |
| 525 'what this means please ask your system administrator for help:' | |
| 526 '\n\tchmod 644 %s\n\tsudo env BOTO_CONFIG=%s gsutil update' | |
| 527 '\n\tchmod 600 %s') % (config_file, config_file, config_file), | |
| 528 informational=True) | |
| 529 | |
| 530 # This list is checked during gsutil update by doing a lowercased | |
| 531 # slash-left-stripped check. For example "/Dev" would match the "dev" entry. | |
| 532 unsafe_update_dirs = [ | |
| 533 'applications', 'auto', 'bin', 'boot', 'desktop', 'dev', | |
| 534 'documents and settings', 'etc', 'export', 'home', 'kernel', 'lib', | |
| 535 'lib32', 'library', 'lost+found', 'mach_kernel', 'media', 'mnt', 'net', | |
| 536 'null', 'network', 'opt', 'private', 'proc', 'program files', 'python', | |
| 537 'root', 'sbin', 'scripts', 'srv', 'sys', 'system', 'tmp', 'users', 'usr', | |
| 538 'var', 'volumes', 'win', 'win32', 'windows', 'winnt', | |
| 539 ] | |
| 540 | |
| 541 def EnsureDirsSafeForUpdate(self, dirs): | |
| 542 """Throws Exception if any of dirs is known to be unsafe for gsutil update. | |
| 543 | |
| 544 This provides a fail-safe check to ensure we don't try to overwrite | |
| 545 or delete any important directories. (That shouldn't happen given the | |
| 546 way we construct tmp dirs, etc., but since the gsutil update cleanup | |
| 547 use shutil.rmtree() it's prudent to add extra checks.) | |
| 548 | |
| 549 Args: | |
| 550 dirs: list of directories to check. | |
| 551 | |
| 552 Raises: | |
| 553 CommandException: If unsafe directory encountered. | |
| 554 """ | |
| 555 for d in dirs: | |
| 556 if not d: | |
| 557 d = 'null' | |
| 558 if d.lstrip(os.sep).lower() in self.unsafe_update_dirs: | |
| 559 raise CommandException('EnsureDirsSafeForUpdate: encountered unsafe ' | |
| 560 'directory (%s); aborting update' % d) | |
| 561 | |
| 562 def CleanUpUpdateCommand(self, tf, dirs_to_remove): | |
| 563 """Cleans up temp files etc. from running update command. | |
| 564 | |
| 565 Args: | |
| 566 tf: opened TarFile. | |
| 567 dirs_to_remove: list of directories to remove. | |
| 568 | |
| 569 """ | |
| 570 tf.close() | |
| 571 self.EnsureDirsSafeForUpdate(dirs_to_remove) | |
| 572 for directory in dirs_to_remove: | |
| 573 shutil.rmtree(directory) | |
| 574 | |
| 575 def LoadVersionString(self): | |
| 576 """Loads version string for currently installed gsutil command. | |
| 577 | |
| 578 Returns: | |
| 579 Version string. | |
| 580 | |
| 581 Raises: | |
| 582 CommandException: if errors encountered. | |
| 583 """ | |
| 584 ver_file_path = self.gsutil_bin_dir + os.sep + 'VERSION' | |
| 585 if not os.path.isfile(ver_file_path): | |
| 586 raise CommandException( | |
| 587 '%s not found. Did you install the\ncomplete gsutil software after ' | |
| 588 'the gsutil "update" command was implemented?' % ver_file_path) | |
| 589 ver_file = open(ver_file_path, 'r') | |
| 590 installed_version_string = ver_file.read().rstrip('\n') | |
| 591 ver_file.close() | |
| 592 return installed_version_string | |
| 593 | |
| 594 def UpdateCommand(self, unused_args, sub_opts=None, headers=None, debug=0): | |
| 595 """Implementation of experimental update command. | |
| 596 | |
| 597 Args: | |
| 598 unused_args: command-line argument list. | |
| 599 sub_opts: list of command-specific options from getopt. | |
| 600 headers: dictionary containing optional HTTP headers to pass to boto. | |
| 601 debug: debug level to pass in to boto connection (range 0..3). | |
| 602 | |
| 603 Raises: | |
| 604 CommandException: if errors encountered. | |
| 605 """ | |
| 606 installed_version_string = self.LoadVersionString() | |
| 607 | |
| 608 dirs_to_remove = [] | |
| 609 # Retrieve gsutil tarball and check if it's newer than installed code. | |
| 610 # TODO: Store this version info as metadata on the tarball object and | |
| 611 # change this command's implementation to check that metadata instead of | |
| 612 # downloading the tarball to check the version info. | |
| 613 tmp_dir = tempfile.mkdtemp() | |
| 614 dirs_to_remove.append(tmp_dir) | |
| 615 os.chdir(tmp_dir) | |
| 616 print 'Checking for software update...' | |
| 617 self.CopyObjsCommand(['gs://pub/gsutil.tar.gz', 'file://gsutil.tar.gz'], [], | |
| 618 headers, debug) | |
| 619 tf = tarfile.open('gsutil.tar.gz') | |
| 620 tf.errorlevel = 1 # So fatal tarball unpack errors raise exceptions. | |
| 621 tf.extract('./gsutil/VERSION') | |
| 622 ver_file = open('gsutil/VERSION', 'r') | |
| 623 latest_version_string = ver_file.read().rstrip('\n') | |
| 624 ver_file.close() | |
| 625 | |
| 626 # The force_update option works around a problem with the way the | |
| 627 # first gsutil "update" command exploded the gsutil and boto directories, | |
| 628 # which didn't correctly install boto. People running that older code can | |
| 629 # run "gsutil update" (to update to the newer gsutil update code) followed | |
| 630 # by "gsutil update -f" (which will then update the boto code, even though | |
| 631 # the VERSION is already the latest version). | |
| 632 force_update = False | |
| 633 if sub_opts: | |
| 634 for o, unused_a in sub_opts: | |
| 635 if o == '-f': | |
| 636 force_update = True | |
| 637 if not force_update and installed_version_string == latest_version_string: | |
| 638 self.CleanUpUpdateCommand(tf, dirs_to_remove) | |
| 639 raise CommandException('You have the latest version of gsutil installed.', | |
| 640 informational=True) | |
| 641 | |
| 642 print(('This command will update to the "%s" version of\ngsutil at %s') % | |
| 643 (latest_version_string, self.gsutil_bin_dir)) | |
| 644 self.ExplainIfSudoNeeded(tf, dirs_to_remove) | |
| 645 | |
| 646 answer = raw_input('Proceed (Note: experimental command)? [y/N] ') | |
| 647 if not answer or answer.lower()[0] != 'y': | |
| 648 self.CleanUpUpdateCommand(tf, dirs_to_remove) | |
| 649 raise CommandException('Not running update.', informational=True) | |
| 650 | |
| 651 # Ignore keyboard interrupts during the update to reduce the chance someone | |
| 652 # hitting ^C leaves gsutil in a broken state. | |
| 653 signal.signal(signal.SIGINT, signal.SIG_IGN) | |
| 654 | |
| 655 # gsutil_bin_dir lists the path where the code should end up (like | |
| 656 # /usr/local/gsutil), which is one level down from the relative path in the | |
| 657 # tarball (since the latter creates files in ./gsutil). So, we need to | |
| 658 # extract at the parent directory level. | |
| 659 gsutil_bin_parent_dir = os.path.dirname(self.gsutil_bin_dir) | |
| 660 | |
| 661 # Extract tarball to a temporary directory in a sibling to gsutil_bin_dir. | |
| 662 old_dir = tempfile.mkdtemp(dir=gsutil_bin_parent_dir) | |
| 663 new_dir = tempfile.mkdtemp(dir=gsutil_bin_parent_dir) | |
| 664 dirs_to_remove.append(old_dir) | |
| 665 dirs_to_remove.append(new_dir) | |
| 666 self.EnsureDirsSafeForUpdate(dirs_to_remove) | |
| 667 try: | |
| 668 tf.extractall(path=new_dir) | |
| 669 except Exception, e: | |
| 670 self.CleanUpUpdateCommand(tf, dirs_to_remove) | |
| 671 raise CommandException('Update failed: %s.' % e) | |
| 672 | |
| 673 # Move old installation aside and new into place. | |
| 674 os.rename(self.gsutil_bin_dir, old_dir + os.sep + 'old') | |
| 675 os.rename(new_dir + os.sep + 'gsutil', self.gsutil_bin_dir) | |
| 676 self.CleanUpUpdateCommand(tf, dirs_to_remove) | |
| 677 signal.signal(signal.SIGINT, signal.SIG_DFL) | |
| 678 print 'Update complete.' | |
| 679 | |
| 680 def CheckForDirFileConflict(self, src_uri, dst_path): | |
| 681 """Checks whether copying src_uri into dst_path is not possible. | |
| 682 | |
| 683 This happens if a directory exists in local file system where a file | |
| 684 needs to go or vice versa. In that case we print an error message and | |
| 685 exits. Example: if the file "./x" exists and you try to do: | |
| 686 gsutil cp gs://mybucket/x/y . | |
| 687 the request can't succeed because it requires a directory where | |
| 688 the file x exists. | |
| 689 | |
| 690 Args: | |
| 691 src_uri: source StorageUri of copy | |
| 692 dst_path: destination path. | |
| 693 | |
| 694 Raises: | |
| 695 CommandException: if errors encountered. | |
| 696 """ | |
| 697 final_dir = os.path.dirname(dst_path) | |
| 698 if os.path.isfile(final_dir): | |
| 699 raise CommandException('Cannot retrieve %s because it a file exists ' | |
| 700 'where a directory needs to be created (%s).' % | |
| 701 (src_uri, final_dir)) | |
| 702 if os.path.isdir(dst_path): | |
| 703 raise CommandException('Cannot retrieve %s because a directory exists ' | |
| 704 '(%s) where the file needs to be created.' % | |
| 705 (src_uri, dst_path)) | |
| 706 | |
| 707 def GetAclCommand(self, args, unused_sub_opts=None, headers=None, debug=0): | |
| 708 """Implementation of getacl command. | |
| 709 | |
| 710 Args: | |
| 711 args: command-line argument list. | |
| 712 unused_sub_opts: list of command-specific options from getopt. | |
| 713 headers: dictionary containing optional HTTP headers to pass to boto. | |
| 714 debug: debug level to pass in to boto connection (range 0..3). | |
| 715 | |
| 716 Raises: | |
| 717 CommandException: if errors encountered. | |
| 718 """ | |
| 719 # Wildcarding is allowed but must resolve to just one object. | |
| 720 uris = list(self.CmdWildcardIterator(args[0], headers=headers, | |
| 721 debug=debug)) | |
| 722 if len(uris) != 1: | |
| 723 raise CommandException('Wildcards must resolve to exactly one object for ' | |
| 724 '"getacl" command.') | |
| 725 uri = uris[0] | |
| 726 if not uri.bucket_name: | |
| 727 raise CommandException('"getacl" command must specify a bucket or ' | |
| 728 'object.') | |
| 729 acl = uri.get_acl(False, headers) | |
| 730 # Pretty-print the XML to make it more easily human editable. | |
| 731 parsed_xml = xml.dom.minidom.parseString(acl.to_xml().encode('utf-8')) | |
| 732 print parsed_xml.toprettyxml(indent=' ') | |
| 733 | |
| 734 class FileCopyCallbackHandler(object): | |
| 735 """Outputs progress info for large copy requests.""" | |
| 736 | |
| 737 def __init__(self, upload): | |
| 738 if upload: | |
| 739 self.announce_text = 'Uploading' | |
| 740 else: | |
| 741 self.announce_text = 'Downloading' | |
| 742 | |
| 743 def call(self, total_bytes_transferred, total_size): | |
| 744 sys.stderr.write('%s: %s/%s \r' % ( | |
| 745 self.announce_text, | |
| 746 MakeHumanReadable(total_bytes_transferred), | |
| 747 MakeHumanReadable(total_size))) | |
| 748 if total_bytes_transferred == total_size: | |
| 749 sys.stderr.write('\n') | |
| 750 | |
| 751 def GetTransferHandlers(self, uri, key, file_size, upload): | |
| 752 """ | |
| 753 Selects upload/download and callback handlers. | |
| 754 | |
| 755 We use a callback handler that shows a simple textual progress indicator | |
| 756 if file_size is above the configurable threshold. | |
| 757 | |
| 758 We use a resumable transfer handler if file_size is >= the configurable | |
| 759 threshold and resumable transfers are supported by the given provider. | |
| 760 boto supports resumable downloads for all providers, but resumable | |
| 761 uploads are currently only supported by GS. | |
| 762 """ | |
| 763 config = boto.config | |
| 764 resumable_threshold = config.getint('GSUtil', 'resumable_threshold', ONE_MB) | |
| 765 if file_size >= resumable_threshold: | |
| 766 cb = self.FileCopyCallbackHandler(upload).call | |
| 767 num_cb = int(file_size / ONE_MB) | |
| 768 resumable_tracker_dir = config.get( | |
| 769 'GSUtil', 'resumable_tracker_dir', | |
| 770 os.path.expanduser('~' + os.sep + '.gsutil')) | |
| 771 if not os.path.exists(resumable_tracker_dir): | |
| 772 os.makedirs(resumable_tracker_dir) | |
| 773 if upload: | |
| 774 # Encode the src bucket and key into the tracker file name. | |
| 775 res_tracker_file_name = ( | |
| 776 re.sub('[/\\\\]', '_', 'resumable_upload__%s__%s.url' % | |
| 777 (key.bucket.name, key.name))) | |
| 778 else: | |
| 779 # Encode the fully-qualified src file name into the tracker file name. | |
| 780 res_tracker_file_name = ( | |
| 781 re.sub('[/\\\\]', '_', 'resumable_download__%s.etag' % | |
| 782 (os.path.realpath(uri.object_name)))) | |
| 783 tracker_file = '%s%s%s' % (resumable_tracker_dir, os.sep, | |
| 784 res_tracker_file_name) | |
| 785 if upload: | |
| 786 if uri.scheme == 'gs': | |
| 787 transfer_handler = ResumableUploadHandler(tracker_file) | |
| 788 else: | |
| 789 transfer_handler = None | |
| 790 else: | |
| 791 transfer_handler = ResumableDownloadHandler(tracker_file) | |
| 792 else: | |
| 793 transfer_handler = None | |
| 794 cb = None | |
| 795 num_cb = None | |
| 796 return (cb, num_cb, transfer_handler) | |
| 797 | |
| 798 def CopyObjToObjSameProvider(self, src_key, src_uri, dst_uri, headers): | |
| 799 # Do Object -> object copy within same provider (uses | |
| 800 # x-<provider>-copy-source metadata HTTP header to request copying at the | |
| 801 # server). (Note: boto does not currently provide a way to pass canned_acl | |
| 802 # when copying from object-to-object through x-<provider>-copy-source) | |
| 803 src_bucket = src_uri.get_bucket(False, headers) | |
| 804 dst_bucket = dst_uri.get_bucket(False, headers) | |
| 805 start_time = time.time() | |
| 806 dst_bucket.copy_key(dst_uri.object_name, src_bucket.name, | |
| 807 src_uri.object_name, headers) | |
| 808 end_time = time.time() | |
| 809 return (end_time - start_time, src_key.size) | |
| 810 | |
| 811 def CheckFreeSpace(self, path): | |
| 812 """Return path/drive free space (in bytes).""" | |
| 813 if platform.system() == 'Windows': | |
| 814 free_bytes = ctypes.c_ulonglong(0) | |
| 815 ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(path), None, | |
| 816 None, | |
| 817 ctypes.pointer(free_bytes)) | |
| 818 return free_bytes.value | |
| 819 else: | |
| 820 (_, f_frsize, _, _, f_bavail, _, _, _, _, _) = os.statvfs(path) | |
| 821 return f_frsize * f_bavail | |
| 822 | |
| 823 def PerformResumableUploadIfApplies(self, fp, dst_uri, headers, canned_acl): | |
| 824 """ | |
| 825 Performs resumable upload if supported by provider and file is above | |
| 826 threshold, else performs non-resumable upload. | |
| 827 | |
| 828 Returns (elapsed_time, bytes_transferred). | |
| 829 """ | |
| 830 start_time = time.time() | |
| 831 file_size = os.path.getsize(fp.name) | |
| 832 dst_key = dst_uri.new_key(False, headers) | |
| 833 (cb, num_cb, res_upload_handler) = self.GetTransferHandlers( | |
| 834 dst_uri, dst_key, file_size, True) | |
| 835 if dst_uri.scheme == 'gs': | |
| 836 # Resumable upload protocol is Google Storage-specific. | |
| 837 dst_key.set_contents_from_file(fp, headers, policy=canned_acl, | |
| 838 cb=cb, num_cb=num_cb, | |
| 839 res_upload_handler=res_upload_handler) | |
| 840 else: | |
| 841 dst_key.set_contents_from_file(fp, headers, policy=canned_acl, | |
| 842 cb=cb, num_cb=num_cb) | |
| 843 if res_upload_handler: | |
| 844 bytes_transferred = file_size - res_upload_handler.upload_start_point | |
| 845 else: | |
| 846 bytes_transferred = file_size | |
| 847 end_time = time.time() | |
| 848 return (end_time - start_time, bytes_transferred) | |
| 849 | |
| 850 def UploadFileToObject(self, sub_opts, src_key, src_uri, dst_uri, headers, | |
| 851 debug): | |
| 852 gzip_exts = [] | |
| 853 canned_acl = None | |
| 854 if sub_opts: | |
| 855 for o, a in sub_opts: | |
| 856 if o == '-a': | |
| 857 canned_acls = dst_uri.canned_acls() | |
| 858 if a not in canned_acls: | |
| 859 raise CommandException('Invalid canned ACL "%s".' % a) | |
| 860 canned_acl = a | |
| 861 elif o == '-t': | |
| 862 mimetype_tuple = mimetypes.guess_type(src_uri.object_name) | |
| 863 mime_type = mimetype_tuple[0] | |
| 864 content_encoding = mimetype_tuple[1] | |
| 865 if mime_type: | |
| 866 headers['Content-Type'] = mime_type | |
| 867 print '\t[Setting Content-Type=%s]' % mime_type | |
| 868 else: | |
| 869 print '\t[Unknown content type -> using application/octet stream]' | |
| 870 if content_encoding: | |
| 871 headers['Content-Encoding'] = content_encoding | |
| 872 elif o == '-z': | |
| 873 gzip_exts = a.split(',') | |
| 874 fname_parts = src_uri.object_name.split('.') | |
| 875 if len(fname_parts) > 1 and fname_parts[-1] in gzip_exts: | |
| 876 if debug: | |
| 877 print 'Compressing %s (to tmp)...' % src_key | |
| 878 gzip_tmp = tempfile.mkstemp() | |
| 879 gzip_path = gzip_tmp[1] | |
| 880 # Check for temp space. Assume the compressed object is at most 2x | |
| 881 # the size of the object (normally should compress to smaller than | |
| 882 # the object) | |
| 883 if self.CheckFreeSpace(gzip_path) < 2*int(os.path.getsize(src_key.name)): | |
| 884 raise CommandException('Inadequate temp space available to compress ' | |
| 885 '%s' % src_key.name) | |
| 886 the_gzip = gzip.open(gzip_path, 'wb') | |
| 887 the_gzip.writelines(src_key.fp) | |
| 888 the_gzip.close() | |
| 889 headers['Content-Encoding'] = 'gzip' | |
| 890 (elapsed_time, bytes_transferred) = self.PerformResumableUploadIfApplies( | |
| 891 open(gzip_path, 'rb'), dst_uri, headers, canned_acl) | |
| 892 os.unlink(gzip_path) | |
| 893 else: | |
| 894 (elapsed_time, bytes_transferred) = self.PerformResumableUploadIfApplies( | |
| 895 src_key.fp, dst_uri, headers, canned_acl) | |
| 896 return (elapsed_time, bytes_transferred) | |
| 897 | |
| 898 def DownloadObjectToFile(self, src_key, src_uri, dst_uri, headers, debug): | |
| 899 (cb, num_cb, res_download_handler) = self.GetTransferHandlers( | |
| 900 src_uri, src_key, src_key.size, False) | |
| 901 file_name = dst_uri.object_name | |
| 902 dir_name = os.path.dirname(file_name) | |
| 903 if dir_name and not os.path.exists(dir_name): | |
| 904 os.makedirs(dir_name) | |
| 905 # For gzipped objects not named *.gz download to a temp file and unzip. | |
| 906 if (hasattr(src_key, 'content_encoding') and | |
| 907 src_key.content_encoding == 'gzip' and | |
| 908 not file_name.endswith('.gz')): | |
| 909 # We can't use tempfile.mkstemp() here because we need a predictable | |
| 910 # filename for resumable downloads. | |
| 911 download_file_name = '%s_.gztmp' % file_name | |
| 912 need_to_unzip = True | |
| 913 else: | |
| 914 download_file_name = file_name | |
| 915 need_to_unzip = False | |
| 916 if res_download_handler: | |
| 917 fp = open(download_file_name, 'ab') | |
| 918 else: | |
| 919 fp = open(download_file_name, 'wb') | |
| 920 start_time = time.time() | |
| 921 src_key.get_contents_to_file(fp, headers, cb=cb, num_cb=num_cb, | |
| 922 res_download_handler=res_download_handler) | |
| 923 fp.close() | |
| 924 end_time = time.time() | |
| 925 if res_download_handler: | |
| 926 bytes_transferred = ( | |
| 927 src_key.size - res_download_handler.download_start_point) | |
| 928 else: | |
| 929 bytes_transferred = src_key.size | |
| 930 if need_to_unzip: | |
| 931 if debug: | |
| 932 print 'Uncompressing tmp to %s...' % file_name | |
| 933 # Downloaded gzipped file to a filename w/o .gz extension, so unzip. | |
| 934 f_in = gzip.open(download_file_name, 'rb') | |
| 935 f_out = open(file_name, 'wb') | |
| 936 f_out.writelines(f_in) | |
| 937 f_out.close(); | |
| 938 f_in.close(); | |
| 939 os.unlink(download_file_name) | |
| 940 return (end_time - start_time, bytes_transferred) | |
| 941 | |
| 942 def CopyFileToFile(self, src_key, dst_uri, headers): | |
| 943 dst_key = dst_uri.new_key(False, headers) | |
| 944 start_time = time.time() | |
| 945 dst_key.set_contents_from_file(src_key.fp, headers) | |
| 946 end_time = time.time() | |
| 947 return (end_time - start_time, os.path.getsize(src_key.fp.name)) | |
| 948 | |
| 949 def CopyObjToObjDiffProvider(self, sub_opts, src_key, src_uri, dst_uri, | |
| 950 headers, debug): | |
| 951 # We implement cross-provider object copy through a local temp file. | |
| 952 # Note that a downside of this approach is that killing the gsutil | |
| 953 # process partway through and then restarting will always repeat the | |
| 954 # download and upload, because the temp file name is different for each | |
| 955 # incarnation. (If however you just leave the process running and failures | |
| 956 # happen along the way, they will continue to restart and make progress | |
| 957 # as long as not too many failures happen in a row with no progress.) | |
| 958 tmp = tempfile.NamedTemporaryFile() | |
| 959 if self.CheckFreeSpace(tempfile.tempdir) < src_key.size: | |
| 960 raise CommandException('Inadequate temp space available to perform the ' | |
| 961 'requested copy') | |
| 962 start_time = time.time() | |
| 963 file_uri = self.StorageUri('file://%s' % tmp.name, debug=debug, | |
| 964 validate=False) | |
| 965 try: | |
| 966 self.DownloadObjectToFile(src_key, src_uri, file_uri, headers, debug) | |
| 967 self.UploadFileToObject(sub_opts, file_uri.get_key(), file_uri, dst_uri, | |
| 968 headers, debug) | |
| 969 finally: | |
| 970 tmp.close() | |
| 971 end_time = time.time() | |
| 972 return (end_time - start_time, src_key.size) | |
| 973 | |
| 974 def PerformCopy(self, src_uri, dst_uri, sub_opts=None, headers=None, debug=0): | |
| 975 """Helper method for CopyObjsCommand. | |
| 976 | |
| 977 Args: | |
| 978 src_uri: source StorageUri. | |
| 979 dst_uri: destination StorageUri. | |
| 980 sub_opts: list of command-specific options from getopt. | |
| 981 headers: dictionary containing optional HTTP headers to pass to boto. | |
| 982 debug: debug level to pass in to boto connection (range 0..3). | |
| 983 | |
| 984 Returns: | |
| 985 (elapsed_time, bytes_transferred) excluding overhead like initial HEAD. | |
| 986 | |
| 987 Raises: | |
| 988 CommandException: if errors encountered. | |
| 989 """ | |
| 990 # Make a copy of the input headers each time so we can set a different | |
| 991 # MIME type for each object. | |
| 992 if headers: | |
| 993 headers = headers.copy() | |
| 994 else: | |
| 995 headers = {} | |
| 996 | |
| 997 src_key = src_uri.get_key(False, headers) | |
| 998 if not src_key: | |
| 999 raise CommandException('"%s" does not exist.' % src_uri) | |
| 1000 | |
| 1001 # Separately handle cases to avoid extra file and network copying of | |
| 1002 # potentially very large files/objects. | |
| 1003 | |
| 1004 if src_uri.is_cloud_uri() and dst_uri.is_cloud_uri(): | |
| 1005 if src_uri.scheme == dst_uri.scheme: | |
| 1006 return self.CopyObjToObjSameProvider(src_key, src_uri, dst_uri, | |
| 1007 headers) | |
| 1008 else: | |
| 1009 return self.CopyObjToObjDiffProvider(sub_opts, src_key, src_uri, | |
| 1010 dst_uri, headers, debug) | |
| 1011 elif src_uri.is_file_uri() and dst_uri.is_cloud_uri(): | |
| 1012 return self.UploadFileToObject(sub_opts, src_key, src_uri, dst_uri, | |
| 1013 headers, debug) | |
| 1014 elif src_uri.is_cloud_uri() and dst_uri.is_file_uri(): | |
| 1015 return self.DownloadObjectToFile(src_key, src_uri, dst_uri, headers, | |
| 1016 debug) | |
| 1017 elif src_uri.is_file_uri() and dst_uri.is_file_uri(): | |
| 1018 return self.CopyFileToFile(src_key, dst_uri, headers) | |
| 1019 else: | |
| 1020 raise CommandException('Unexpected src/dest case') | |
| 1021 | |
| 1022 def ExpandWildcardsAndContainers(self, uri_strs, sub_opts=None, headers=None, | |
| 1023 debug=0): | |
| 1024 """Expands URI wildcarding, object-less bucket names, and directory names. | |
| 1025 | |
| 1026 Examples: | |
| 1027 Calling with uri_strs='gs://bucket' will enumerate all contained objects. | |
| 1028 Calling with uri_strs='file:///tmp' will enumerate all files under /tmp | |
| 1029 (or under any subdirectory). | |
| 1030 The previous example is equivalent to uri_strs='file:///tmp/*' | |
| 1031 and to uri_strs='file:///tmp/**' | |
| 1032 | |
| 1033 Args: | |
| 1034 uri_strs: URI strings needing expansion | |
| 1035 sub_opts: list of command-specific options from getopt. | |
| 1036 headers: dictionary containing optional HTTP headers to pass to boto. | |
| 1037 debug: debug level to pass in to boto connection (range 0..3). | |
| 1038 | |
| 1039 Returns: | |
| 1040 dict mapping StorageUri -> list of StorageUri, for each input uri_str. | |
| 1041 | |
| 1042 We build a dict of the expansion instead of using a generator to | |
| 1043 iterate incrementally because caller needs to know count before | |
| 1044 iterating and performing copy operations. | |
| 1045 """ | |
| 1046 # The algorithm we use is: | |
| 1047 # 1. Build a first level expanded list from uri_strs consisting of all | |
| 1048 # URIs that aren't file wildcards, plus expansions of the file wildcards. | |
| 1049 # 2. Build dict from above expanded list. | |
| 1050 # We do so that we can properly handle the following example: | |
| 1051 # gsutil cp file0 dir0 gs://bucket | |
| 1052 # where dir0 contains file1 and dir1/file2. | |
| 1053 # If we didn't do the first expansion, this cp command would end up | |
| 1054 # with this expansion: | |
| 1055 # {file://file0:[file://file0],file://dir0:[file://dir0/file1, | |
| 1056 # file://dir0/dir1/file2]} | |
| 1057 # instead of the (correct) expansion: | |
| 1058 # {file://file0:[file://file0],file://dir0/file1:[file://dir0/file1], | |
| 1059 # file://dir0/dir1:[file://dir0/dir1/file2]} | |
| 1060 # The latter expansion is needed so that in the "Copying..." loop of | |
| 1061 # CopyObjsCommand we know that dir0 was being copied, so we create an | |
| 1062 # object called gs://bucket/dir0/dir1/file2. (Otherwise it would look | |
| 1063 # like a single file was being copied, so we'd create an object called | |
| 1064 # gs://bucket/file2.) | |
| 1065 | |
| 1066 should_recurse = False | |
| 1067 if sub_opts: | |
| 1068 for o, unused_a in sub_opts: | |
| 1069 if o == '-r' or o == '-R': | |
| 1070 should_recurse = True | |
| 1071 | |
| 1072 # Step 1. | |
| 1073 uris_to_expand = [] | |
| 1074 for uri_str in uri_strs: | |
| 1075 uri = self.StorageUri(uri_str, debug=debug, validate=False) | |
| 1076 if uri.is_file_uri() and ContainsWildcard(uri_str): | |
| 1077 uris_to_expand.extend(list( | |
| 1078 self.CmdWildcardIterator(uri, headers=headers, debug=debug))) | |
| 1079 else: | |
| 1080 uris_to_expand.append(uri) | |
| 1081 | |
| 1082 # Step 2. | |
| 1083 result = {} | |
| 1084 for uri in uris_to_expand: | |
| 1085 if uri.names_container(): | |
| 1086 if not should_recurse: | |
| 1087 if uri.is_file_uri(): | |
| 1088 desc = 'directory' | |
| 1089 else: | |
| 1090 desc = 'bucket' | |
| 1091 print 'Omitting %s "%s".' % (desc, uri.uri) | |
| 1092 result[uri] = [] | |
| 1093 continue | |
| 1094 if uri.is_file_uri(): | |
| 1095 # dir -> convert to implicit recursive wildcard. | |
| 1096 uri_to_iter = '%s/**' % uri.uri | |
| 1097 else: | |
| 1098 # bucket -> convert to implicit wildcard. | |
| 1099 uri_to_iter = uri.clone_replace_name('*') | |
| 1100 else: | |
| 1101 uri_to_iter = uri | |
| 1102 result[uri] = list(self.CmdWildcardIterator( | |
| 1103 uri_to_iter, headers=headers, debug=debug)) | |
| 1104 return result | |
| 1105 | |
| 1106 def ErrorCheckCopyRequest(self, src_uri_expansion, dst_uri_str, headers, | |
| 1107 debug, command='cp'): | |
| 1108 """Checks copy request for problems, and builds needed base_dst_uri. | |
| 1109 | |
| 1110 base_dst_uri is the base uri to be used if it's a multi-object copy, e.g., | |
| 1111 the URI for the destination bucket. The actual dst_uri can then be | |
| 1112 constructed from the src_uri and this base_dst_uri. | |
| 1113 | |
| 1114 Args: | |
| 1115 src_uri_expansion: result from ExpandWildcardsAndContainers call. | |
| 1116 dst_uri_str: string representation of destination StorageUri. | |
| 1117 headers: dictionary containing optional HTTP headers to pass to boto. | |
| 1118 debug: flag indicating whether to include debug output | |
| 1119 command: name of command on behalf of which this call is running. | |
| 1120 | |
| 1121 Returns: | |
| 1122 (base_dst_uri to use for copy, bool indicator of multi-source request). | |
| 1123 | |
| 1124 Raises: | |
| 1125 CommandException: if errors found. | |
| 1126 """ | |
| 1127 for src_uri in src_uri_expansion: | |
| 1128 if src_uri.is_cloud_uri() and not src_uri.bucket_name: | |
| 1129 raise CommandException('Provider-only src_uri (%s)') | |
| 1130 | |
| 1131 if ContainsWildcard(dst_uri_str): | |
| 1132 matches = list(self.CmdWildcardIterator(dst_uri_str, headers=headers, | |
| 1133 debug=debug)) | |
| 1134 if len(matches) > 1: | |
| 1135 raise CommandException('Destination (%s) matches more than 1 URI' % | |
| 1136 dst_uri_str) | |
| 1137 base_dst_uri = matches[0] | |
| 1138 else: | |
| 1139 base_dst_uri = self.StorageUri(dst_uri_str, debug=debug) | |
| 1140 | |
| 1141 # Make sure entire expansion didn't result in nothing to copy. This can | |
| 1142 # happen if user request copying a directory w/o -r option, for example. | |
| 1143 have_work = False | |
| 1144 for v in src_uri_expansion.values(): | |
| 1145 if v: | |
| 1146 have_work = True | |
| 1147 break | |
| 1148 if not have_work: | |
| 1149 raise CommandException('Nothing to copy') | |
| 1150 | |
| 1151 # If multi-object copy request ensure base_dst_uri names a container. | |
| 1152 multi_src_request = (len(src_uri_expansion) > 1 or | |
| 1153 len(src_uri_expansion.values()[0]) > 1) | |
| 1154 if multi_src_request: | |
| 1155 self.InsistUriNamesContainer(command, base_dst_uri) | |
| 1156 | |
| 1157 # Ensure no src/dest pairs would overwrite src. Note that this is | |
| 1158 # more restrictive than the UNIX 'cp' command (which would, for example, | |
| 1159 # allow "mv * dir" and just skip the implied mv dir dir). We disallow such | |
| 1160 # partial completion operations in cloud copies because they are risky. | |
| 1161 for src_uri in iter(src_uri_expansion): | |
| 1162 for exp_src_uri in src_uri_expansion[src_uri]: | |
| 1163 new_dst_uri = self.ConstructDstUri(src_uri, exp_src_uri, base_dst_uri) | |
| 1164 if self.SrcDstSame(exp_src_uri, new_dst_uri): | |
| 1165 raise CommandException('cp: "%s" and "%s" are the same object - ' | |
| 1166 'abort.' % (exp_src_uri.uri, new_dst_uri.uri)) | |
| 1167 | |
| 1168 return (base_dst_uri, multi_src_request) | |
| 1169 | |
| 1170 def HandleMultiSrcCopyRequst(self, src_uri_expansion, dst_uri): | |
| 1171 """ | |
| 1172 Rewrites dst_uri and creates dest dir as needed, if this is a | |
| 1173 multi-source copy. | |
| 1174 | |
| 1175 Args: | |
| 1176 src_uri_expansion: result from ExpandWildcardsAndContainers call. | |
| 1177 dst_uri: uri constructed by ErrorCheckCopyRequest() call. | |
| 1178 | |
| 1179 Returns: | |
| 1180 dst_uri to use for copy. | |
| 1181 """ | |
| 1182 # If src_uri and dst_uri both name containers, handle | |
| 1183 # two cases to make copy command work like UNIX "cp -r" works: | |
| 1184 # a) if dst_uri names a non-existent directory, copy objects to a new | |
| 1185 # directory with the dst_uri name. In this case, | |
| 1186 # gsutil gs://bucket/a dir | |
| 1187 # should create dir/a. | |
| 1188 # b) if dst_uri names an existing directory, copy objects under that | |
| 1189 # directory. In this case, | |
| 1190 # gsutil gs://bucket/a dir | |
| 1191 # should create dir/bucket/a. | |
| 1192 src_uri_to_check = src_uri_expansion.keys()[0] | |
| 1193 if (src_uri_to_check.names_container() and dst_uri.names_container() and | |
| 1194 os.path.exists(dst_uri.object_name)): | |
| 1195 new_name = ('%s%s%s' % (dst_uri.object_name, os.sep, | |
| 1196 src_uri_to_check.bucket_name)).rstrip('/') | |
| 1197 dst_uri = dst_uri.clone_replace_name(new_name) | |
| 1198 # Create dest directory if needed. | |
| 1199 if dst_uri.is_file_uri() and not os.path.exists(dst_uri.object_name): | |
| 1200 os.makedirs(dst_uri.object_name) | |
| 1201 return dst_uri | |
| 1202 | |
| 1203 def SrcDstSame(self, src_uri, dst_uri): | |
| 1204 """Checks if src_uri and dst_uri represent same object. | |
| 1205 | |
| 1206 We don't handle anything about hard or symbolic links. | |
| 1207 | |
| 1208 Args: | |
| 1209 src_uri: source StorageUri. | |
| 1210 dst_uri: dest StorageUri. | |
| 1211 | |
| 1212 Returns: | |
| 1213 Bool indication. | |
| 1214 """ | |
| 1215 if src_uri.is_file_uri() and dst_uri.is_file_uri(): | |
| 1216 # Translate a/b/./c to a/b/c, so src=dst comparison below works. | |
| 1217 new_src_path = re.sub('%s+\.%s+' % (os.sep, os.sep), os.sep, | |
| 1218 src_uri.object_name) | |
| 1219 new_src_path = re.sub('^.%s+' % os.sep, '', new_src_path) | |
| 1220 new_dst_path = re.sub('%s+\.%s+' % (os.sep, os.sep), os.sep, | |
| 1221 dst_uri.object_name) | |
| 1222 new_dst_path = re.sub('^.%s+' % os.sep, '', new_dst_path) | |
| 1223 return (src_uri.clone_replace_name(new_src_path).uri == | |
| 1224 dst_uri.clone_replace_name(new_dst_path).uri) | |
| 1225 else: | |
| 1226 return src_uri.uri == dst_uri.uri | |
| 1227 | |
| 1228 def ConstructDstUri(self, src_uri, exp_src_uri, base_dst_uri): | |
| 1229 """Constructs a destination URI for CopyObjsCommand. | |
| 1230 | |
| 1231 Args: | |
| 1232 src_uri: src_uri to be copied. | |
| 1233 exp_src_uri: single URI from wildcard expansion of src_uri. | |
| 1234 base_dst_uri: uri constructed by ErrorCheckCopyRequest() call. | |
| 1235 | |
| 1236 Returns: | |
| 1237 dst_uri to use for copy. | |
| 1238 """ | |
| 1239 if base_dst_uri.names_container(): | |
| 1240 # To match naming semantics of UNIX 'cp' command, copying files | |
| 1241 # to buckets/dirs should result in objects/files named by just the | |
| 1242 # final filename component; while copying directories should result | |
| 1243 # in objects/files mirroring the directory hierarchy. Example of the | |
| 1244 # first case: | |
| 1245 # gsutil cp dir1/file1 gs://bucket | |
| 1246 # should create object gs://bucket/file1 | |
| 1247 # Example of the second case: | |
| 1248 # gsutil cp dir1/dir2 gs://bucket | |
| 1249 # should create object gs://bucket/dir2/file2 (assuming dir1/dir2 | |
| 1250 # contains file2). | |
| 1251 if src_uri.names_container(): | |
| 1252 dst_path_start = (src_uri.object_name.rstrip(os.sep) | |
| 1253 .rpartition(os.sep)[-1]) | |
| 1254 start_pos = exp_src_uri.object_name.find(dst_path_start) | |
| 1255 dst_key_name = exp_src_uri.object_name[start_pos:] | |
| 1256 else: | |
| 1257 # src is a file or object, so use final component of src name. | |
| 1258 dst_key_name = os.path.basename(exp_src_uri.object_name) | |
| 1259 if base_dst_uri.is_file_uri(): | |
| 1260 # dst names a directory, so append src obj name to dst obj name. | |
| 1261 dst_key_name = '%s%s%s' % (base_dst_uri.object_name, os.sep, | |
| 1262 dst_key_name) | |
| 1263 self.CheckForDirFileConflict(exp_src_uri, dst_key_name) | |
| 1264 else: | |
| 1265 # dest is an object or file: use dst obj name | |
| 1266 dst_key_name = base_dst_uri.object_name | |
| 1267 return base_dst_uri.clone_replace_name(dst_key_name) | |
| 1268 | |
| 1269 def CopyObjsCommand(self, args, sub_opts=None, headers=None, debug=0, | |
| 1270 command='cp'): | |
| 1271 """Implementation of cp command. | |
| 1272 | |
| 1273 Args: | |
| 1274 args: command-line argument list. | |
| 1275 sub_opts: list of command-specific options from getopt. | |
| 1276 headers: dictionary containing optional HTTP headers to pass to boto. | |
| 1277 debug: debug level to pass in to boto connection (range 0..3). | |
| 1278 command: name of command on behalf of which this call is running. | |
| 1279 | |
| 1280 Raises: | |
| 1281 CommandException: if errors encountered. | |
| 1282 """ | |
| 1283 # Expand wildcards and containers in source StorageUris. | |
| 1284 src_uri_expansion = self.ExpandWildcardsAndContainers( | |
| 1285 args[0:len(args)-1], sub_opts, headers, debug) | |
| 1286 | |
| 1287 # Check for various problems and determine base_dst_uri based for request. | |
| 1288 (base_dst_uri, multi_src_request) = self.ErrorCheckCopyRequest( | |
| 1289 src_uri_expansion, args[-1], headers, debug, command) | |
| 1290 # Rewrite base_dst_uri and create dest dir as needed for multi-source copy. | |
| 1291 if multi_src_request: | |
| 1292 base_dst_uri = self.HandleMultiSrcCopyRequst(src_uri_expansion, | |
| 1293 base_dst_uri) | |
| 1294 | |
| 1295 # Now iterate over expanded src URIs, and perform copy operations. | |
| 1296 total_elapsed_time = total_bytes_transferred = 0 | |
| 1297 for src_uri in iter(src_uri_expansion): | |
| 1298 for exp_src_uri in src_uri_expansion[src_uri]: | |
| 1299 print 'Copying %s...' % exp_src_uri | |
| 1300 dst_uri = self.ConstructDstUri(src_uri, exp_src_uri, base_dst_uri) | |
| 1301 (elapsed_time, bytes_transferred) = self.PerformCopy( | |
| 1302 exp_src_uri, dst_uri, sub_opts, headers, debug) | |
| 1303 total_elapsed_time += elapsed_time | |
| 1304 total_bytes_transferred += bytes_transferred | |
| 1305 if debug == 3: | |
| 1306 # Note that this only counts the actual GET and PUT bytes for the copy | |
| 1307 # - not any transfers for doing wildcard expansion, the initial HEAD | |
| 1308 # request boto performs when doing a bucket.get_key() operation, etc. | |
| 1309 if total_bytes_transferred != 0: | |
| 1310 print 'Total bytes copied=%d, total elapsed time=%5.3f secs (%sps)' % ( | |
| 1311 total_bytes_transferred, total_elapsed_time, | |
| 1312 MakeHumanReadable(float(total_bytes_transferred) / | |
| 1313 float(total_elapsed_time))) | |
| 1314 | |
| 1315 def HelpCommand(self, unused_args, unused_sub_opts=None, unused_headers=None, | |
| 1316 unused_debug=None): | |
| 1317 """Implementation of help command. | |
| 1318 | |
| 1319 Args: | |
| 1320 unused_args: command-line argument list. | |
| 1321 unused_sub_opts: list of command-specific options from getopt. | |
| 1322 unused_headers: dictionary containing optional HTTP headers to send. | |
| 1323 unused_debug: flag indicating whether to include debug output. | |
| 1324 """ | |
| 1325 self.OutputUsageAndExit() | |
| 1326 | |
| 1327 def VerCommand(self, unused_args, unused_sub_opts=None, unused_headers=None, | |
| 1328 unused_debug=None): | |
| 1329 """Implementation of ver command. | |
| 1330 | |
| 1331 Args: | |
| 1332 unused_args: command-line argument list. | |
| 1333 unused_sub_opts: list of command-specific options from getopt. | |
| 1334 unused_headers: dictionary containing optional HTTP headers to send. | |
| 1335 unused_debug: flag indicating whether to include debug output. | |
| 1336 """ | |
| 1337 config_ver = '' | |
| 1338 for path in BotoConfigLocations: | |
| 1339 try: | |
| 1340 f = open(path, 'r') | |
| 1341 while True: | |
| 1342 line = f.readline() | |
| 1343 if not line: | |
| 1344 break | |
| 1345 if line.find('was created by gsutil version') != -1: | |
| 1346 config_ver = ', config file version %s' % line.split('"')[-2] | |
| 1347 break | |
| 1348 # Only look at first first config file found in BotoConfigLocations. | |
| 1349 break | |
| 1350 except IOError: | |
| 1351 pass | |
| 1352 | |
| 1353 print 'gsutil version %s%s, python version %s' % ( | |
| 1354 self.LoadVersionString(), config_ver, sys.version) | |
| 1355 | |
| 1356 def PrintBucketInfo(self, bucket_uri, listing_style, headers=None, debug=0): | |
| 1357 """Print listing info for given bucket. | |
| 1358 | |
| 1359 Args: | |
| 1360 bucket_uri: StorageUri being listed. | |
| 1361 listing_style: ListingStyle enum describing type of output desired. | |
| 1362 headers: dictionary containing optional HTTP headers to pass to boto. | |
| 1363 debug: debug level to pass in to boto connection (range 0..3). | |
| 1364 | |
| 1365 Returns: | |
| 1366 Tuple (total objects, total bytes) in the bucket. | |
| 1367 """ | |
| 1368 bucket_objs = 0 | |
| 1369 bucket_bytes = 0 | |
| 1370 if listing_style == ListingStyle.SHORT: | |
| 1371 print bucket_uri | |
| 1372 else: | |
| 1373 try: | |
| 1374 for obj in self.CmdWildcardIterator( | |
| 1375 bucket_uri.clone_replace_name('*'), | |
| 1376 ResultType.KEYS, headers=headers, debug=debug): | |
| 1377 bucket_objs += 1 | |
| 1378 bucket_bytes += obj.size | |
| 1379 except WildcardException, e: | |
| 1380 # Ignore non-matching wildcards, to allow empty bucket listings. | |
| 1381 if e.reason.find('No matches') == -1: | |
| 1382 raise e | |
| 1383 if listing_style == ListingStyle.LONG: | |
| 1384 print '%s : %s objects, %s' % ( | |
| 1385 bucket_uri, bucket_objs, MakeHumanReadable(bucket_bytes)) | |
| 1386 else: # listing_style == ListingStyle.LONG_LONG: | |
| 1387 location_constraint = bucket_uri.get_location(validate=False, | |
| 1388 headers=headers) | |
| 1389 location_output = '' | |
| 1390 if location_constraint: | |
| 1391 location_output = '\n\tLocationConstraint: %s' % location_constraint | |
| 1392 self.proj_id_handler.FillInProjectHeaderIfNeeded( | |
| 1393 'get_acl', bucket_uri, headers) | |
| 1394 print '%s :\n\t%d objects, %s%s\n\tACL: %s' % ( | |
| 1395 bucket_uri, bucket_objs, MakeHumanReadable(bucket_bytes), | |
| 1396 location_output, bucket_uri.get_acl(False, headers)) | |
| 1397 return (bucket_objs, bucket_bytes) | |
| 1398 | |
| 1399 def PrintObjectInfo(self, iterated_uri, obj, listing_style, headers, debug): | |
| 1400 """Print listing info for given object. | |
| 1401 | |
| 1402 Args: | |
| 1403 iterated_uri: base StorageUri being listed (e.g., gs://abc/*). | |
| 1404 obj: object to be listed (or None if no associated object). | |
| 1405 listing_style: ListingStyle enum describing type of output desired. | |
| 1406 headers: dictionary containing optional HTTP headers to pass to boto. | |
| 1407 debug: flag indicating whether to include debug output | |
| 1408 | |
| 1409 Returns: | |
| 1410 Object length (if listing_style is one of the long listing formats). | |
| 1411 | |
| 1412 Raises: | |
| 1413 Exception: if calling bug encountered. | |
| 1414 """ | |
| 1415 if listing_style == ListingStyle.SHORT: | |
| 1416 print UriStrFor(iterated_uri, obj) | |
| 1417 return 0 | |
| 1418 elif listing_style == ListingStyle.LONG: | |
| 1419 # Exclude timestamp fractional secs (example: 2010-08-23T12:46:54.187Z). | |
| 1420 timestamp = obj.last_modified[:19].decode('utf8').encode('ascii') | |
| 1421 print '%10s %s %s' % (obj.size, timestamp, UriStrFor(iterated_uri, obj)) | |
| 1422 return obj.size | |
| 1423 elif listing_style == ListingStyle.LONG_LONG: | |
| 1424 uri_str = UriStrFor(iterated_uri, obj) | |
| 1425 print '%s:' % uri_str | |
| 1426 obj.open_read() | |
| 1427 print '\tObject size:\t%s' % obj.size | |
| 1428 print '\tLast mod:\t%s' % obj.last_modified | |
| 1429 if obj.cache_control: | |
| 1430 print '\tCache control:\t%s' % obj.cache_control | |
| 1431 print '\tMIME type:\t%s' % obj.content_type | |
| 1432 if obj.content_encoding: | |
| 1433 print '\tContent-Encoding:\t%s' % obj.content_encoding | |
| 1434 if obj.metadata: | |
| 1435 for name in obj.metadata: | |
| 1436 print '\tMetadata:\t%s = %s' % (name, obj.metadata[name]) | |
| 1437 print '\tEtag:\t%s' % obj.etag.strip('"\'') | |
| 1438 print '\tACL:\t%s' % ( | |
| 1439 self.StorageUri(uri_str, debug=debug).get_acl(False, headers)) | |
| 1440 return obj.size | |
| 1441 else: | |
| 1442 raise Exception('Unexpected ListingStyle(%s)' % listing_style) | |
| 1443 | |
| 1444 def ListCommand(self, args, sub_opts=None, headers=None, debug=0): | |
| 1445 """Implementation of ls command. | |
| 1446 | |
| 1447 Args: | |
| 1448 args: command-line argument list. | |
| 1449 sub_opts: list of command-specific options from getopt. | |
| 1450 headers: dictionary containing optional HTTP headers to pass to boto. | |
| 1451 debug: debug level to pass in to boto connection (range 0..3). | |
| 1452 """ | |
| 1453 listing_style = ListingStyle.SHORT | |
| 1454 get_bucket_info = False | |
| 1455 if sub_opts: | |
| 1456 for o, a in sub_opts: | |
| 1457 if o == '-b': | |
| 1458 get_bucket_info = True | |
| 1459 elif o == '-l': | |
| 1460 listing_style = ListingStyle.LONG | |
| 1461 elif o == '-L': | |
| 1462 listing_style = ListingStyle.LONG_LONG | |
| 1463 elif o == '-p': | |
| 1464 self.proj_id_handler.SetProjectId(a) | |
| 1465 | |
| 1466 if not args: | |
| 1467 # default to listing all gs buckets | |
| 1468 args = ['gs://'] | |
| 1469 | |
| 1470 total_objs = 0 | |
| 1471 total_bytes = 0 | |
| 1472 for uri_str in args: | |
| 1473 uri = self.StorageUri(uri_str, debug=debug, validate=False) | |
| 1474 self.proj_id_handler.FillInProjectHeaderIfNeeded('ls', uri, headers) | |
| 1475 | |
| 1476 if not uri.bucket_name: | |
| 1477 # Provider URI: add bucket wildcard to list buckets. | |
| 1478 for uri in self.CmdWildcardIterator('%s://*' % uri.scheme, | |
| 1479 headers=headers, debug=debug): | |
| 1480 (bucket_objs, bucket_bytes) = self.PrintBucketInfo(uri, listing_style, | |
| 1481 headers=headers, | |
| 1482 debug=debug) | |
| 1483 total_bytes += bucket_bytes | |
| 1484 total_objs += bucket_objs | |
| 1485 | |
| 1486 elif not uri.object_name: | |
| 1487 if get_bucket_info: | |
| 1488 # ls -b request on provider+bucket URI: List info about bucket(s). | |
| 1489 for uri in self.CmdWildcardIterator(uri, headers=headers, | |
| 1490 debug=debug): | |
| 1491 (bucket_objs, bucket_bytes) = self.PrintBucketInfo(uri, | |
| 1492 listing_style, | |
| 1493 headers=headers, | |
| 1494 debug=debug) | |
| 1495 total_bytes += bucket_bytes | |
| 1496 total_objs += bucket_objs | |
| 1497 else: | |
| 1498 # ls request on provider+bucket URI: List objects in the bucket(s). | |
| 1499 for obj in self.CmdWildcardIterator(uri.clone_replace_name('*'), | |
| 1500 ResultType.KEYS, headers=headers, | |
| 1501 debug=debug): | |
| 1502 total_bytes += self.PrintObjectInfo(uri, obj, listing_style, | |
| 1503 headers=headers, debug=debug) | |
| 1504 total_objs += 1 | |
| 1505 | |
| 1506 else: | |
| 1507 # Provider+bucket+object URI -> list the object(s). | |
| 1508 for obj in self.CmdWildcardIterator(uri, ResultType.KEYS, | |
| 1509 headers=headers, debug=debug): | |
| 1510 total_bytes += self.PrintObjectInfo(uri, obj, listing_style, | |
| 1511 headers=headers, debug=debug) | |
| 1512 total_objs += 1 | |
| 1513 if listing_style != ListingStyle.SHORT: | |
| 1514 print ('TOTAL: %d objects, %d bytes (%s)' % | |
| 1515 (total_objs, total_bytes, MakeHumanReadable(float(total_bytes)))) | |
| 1516 | |
| 1517 def MakeBucketsCommand(self, args, sub_opts=None, headers=None, debug=0): | |
| 1518 """Implementation of mb command. | |
| 1519 | |
| 1520 Args: | |
| 1521 args: command-line argument list. | |
| 1522 sub_opts: list of command-specific options from getopt. | |
| 1523 headers: dictionary containing optional HTTP headers to pass to boto. | |
| 1524 debug: debug level to pass in to boto connection (range 0..3). | |
| 1525 | |
| 1526 Raises: | |
| 1527 CommandException: if errors encountered. | |
| 1528 """ | |
| 1529 location = '' | |
| 1530 if sub_opts: | |
| 1531 for o, a in sub_opts: | |
| 1532 if o == '-l': | |
| 1533 location = a | |
| 1534 elif o == '-p': | |
| 1535 self.proj_id_handler.SetProjectId(a) | |
| 1536 | |
| 1537 if not headers: | |
| 1538 headers = {} | |
| 1539 else: | |
| 1540 headers = headers.copy() | |
| 1541 | |
| 1542 for bucket_uri_str in args: | |
| 1543 bucket_uri = self.StorageUri(bucket_uri_str, debug=debug) | |
| 1544 self.proj_id_handler.FillInProjectHeaderIfNeeded('mb', bucket_uri, headers
) | |
| 1545 print 'Creating %s...' % bucket_uri | |
| 1546 bucket_uri.create_bucket(headers=headers, location=location) | |
| 1547 | |
| 1548 def MoveObjsCommand(self, args, sub_opts=None, headers=None, debug=0): | |
| 1549 """Implementation of mv command. | |
| 1550 | |
| 1551 Note that there is no atomic rename operation - this command is simply | |
| 1552 a shorthand for 'cp' followed by 'rm'. | |
| 1553 | |
| 1554 Args: | |
| 1555 args: command-line argument list. | |
| 1556 sub_opts: list of command-specific options from getopt. | |
| 1557 headers: dictionary containing optional HTTP headers to pass to boto. | |
| 1558 debug: debug level to pass in to boto connection (range 0..3). | |
| 1559 | |
| 1560 Raises: | |
| 1561 CommandException: if errors encountered. | |
| 1562 """ | |
| 1563 # Refuse to delete a bucket or directory src URI (force users to explicitly | |
| 1564 # do that as a separate operation). | |
| 1565 src_uri_to_check = self.StorageUri(args[0], debug=debug, validate=False) | |
| 1566 if src_uri_to_check.names_container(): | |
| 1567 raise CommandException('Will not remove source buckets or directories. ' | |
| 1568 'You must separately copy and remove for that ' | |
| 1569 'purpose.') | |
| 1570 | |
| 1571 if len(args) > 2: | |
| 1572 self.InsistUriNamesContainer('mv', self.StorageUri(args[-1])) | |
| 1573 | |
| 1574 # Expand wildcards before calling CopyObjsCommand and RemoveObjsCommand, | |
| 1575 # to prevent the following problem: starting with a bucket containing | |
| 1576 # only the object gs://bucket/obj, say the user does: | |
| 1577 # gsutil mv gs://bucket/* gs://bucket/d.txt | |
| 1578 # If we didn't expand the wildcard first, the CopyObjsCommand would | |
| 1579 # first copy gs://bucket/obj to gs://bucket/d.txt, and the | |
| 1580 # RemoveObjsCommand would then remove that object. | |
| 1581 exp_arg_list = [] | |
| 1582 for uri_str in args: | |
| 1583 uri = self.StorageUri(uri_str, debug=debug, validate=False) | |
| 1584 if ContainsWildcard(uri_str): | |
| 1585 exp_arg_list.extend(str(u) for u in list( | |
| 1586 self.CmdWildcardIterator(uri, headers=headers, debug=debug))) | |
| 1587 else: | |
| 1588 exp_arg_list.append(uri.uri) | |
| 1589 | |
| 1590 self.CopyObjsCommand(exp_arg_list, sub_opts, headers, debug, 'mv') | |
| 1591 self.RemoveObjsCommand(exp_arg_list[0:-1], sub_opts, headers, debug) | |
| 1592 | |
| 1593 def RemoveBucketsCommand(self, args, unused_sub_opts=None, headers=None, | |
| 1594 debug=0): | |
| 1595 """Implementation of rb command. | |
| 1596 | |
| 1597 Args: | |
| 1598 args: command-line argument list. | |
| 1599 unused_sub_opts: list of command-specific options from getopt. | |
| 1600 headers: dictionary containing optional HTTP headers to pass to boto. | |
| 1601 debug: debug level to pass in to boto connection (range 0..3). | |
| 1602 | |
| 1603 Raises: | |
| 1604 CommandException: if errors encountered. | |
| 1605 """ | |
| 1606 # Expand bucket name wildcards, if any. | |
| 1607 for uri_str in args: | |
| 1608 for uri in self.CmdWildcardIterator(uri_str, headers=headers, | |
| 1609 debug=debug): | |
| 1610 if uri.object_name: | |
| 1611 raise CommandException('"rb" command requires a URI with no object ' | |
| 1612 'name') | |
| 1613 print 'Removing %s...' % uri | |
| 1614 uri.delete_bucket(headers) | |
| 1615 | |
| 1616 def RemoveObjsCommand(self, args, sub_opts=None, headers=None, debug=0): | |
| 1617 """Implementation of rm command. | |
| 1618 | |
| 1619 Args: | |
| 1620 args: command-line argument list. | |
| 1621 sub_opts: list of command-specific options from getopt. | |
| 1622 headers: dictionary containing optional HTTP headers to pass to boto. | |
| 1623 debug: debug level to pass in to boto connection (range 0..3). | |
| 1624 | |
| 1625 Raises: | |
| 1626 CommandException: if errors encountered. | |
| 1627 """ | |
| 1628 continue_on_error = False | |
| 1629 if sub_opts: | |
| 1630 for o, unused_a in sub_opts: | |
| 1631 if o == '-f': | |
| 1632 continue_on_error = True | |
| 1633 # Expand object name wildcards, if any. | |
| 1634 for uri_str in args: | |
| 1635 try: | |
| 1636 for uri in self.CmdWildcardIterator(uri_str, headers=headers, | |
| 1637 debug=debug): | |
| 1638 if uri.names_container(): | |
| 1639 if uri.is_cloud_uri(): | |
| 1640 # Before offering advice about how to do rm + rb, ensure those | |
| 1641 # commands won't fail because of bucket naming problems. | |
| 1642 boto.s3.connection.check_lowercase_bucketname(uri.bucket_name) | |
| 1643 uri_str = uri_str.rstrip('/\\') | |
| 1644 raise CommandException('"rm" command will not remove buckets. To ' | |
| 1645 'delete this/these bucket(s) do:\n\tgsutil rm
' | |
| 1646 '%s/*\n\tgsutil rb %s' % (uri_str, uri_str)) | |
| 1647 print 'Removing %s...' % uri | |
| 1648 uri.delete_key(validate=False, headers=headers) | |
| 1649 except Exception, e: | |
| 1650 if not continue_on_error: | |
| 1651 raise | |
| 1652 | |
| 1653 def WriteBotoConfigFile(self, config_file, use_oauth2=True, | |
| 1654 launch_browser=True, oauth2_scopes=[SCOPE_FULL_CONTROL]): | |
| 1655 """Creates a boto config file interactively. | |
| 1656 | |
| 1657 Needed credentials are obtained interactively, either by asking the user for | |
| 1658 access key and secret, or by walking the user through the OAuth2 approval | |
| 1659 flow. | |
| 1660 | |
| 1661 Args: | |
| 1662 config_file: file object to which the resulting config file will be | |
| 1663 written. | |
| 1664 use_oauth2: if True, walk user through OAuth2 approval flow and produce a | |
| 1665 config with an oauth2_refresh_token credential. If false, ask the | |
| 1666 user for access key and secret. | |
| 1667 launch_browser: in the OAuth2 approval flow, attempt to open a browser | |
| 1668 window and navigate to the approval URL. | |
| 1669 oauth2_scopes: a list of OAuth2 scopes to request authorization for, when | |
| 1670 using OAuth2. | |
| 1671 """ | |
| 1672 | |
| 1673 # Collect credentials | |
| 1674 provider_map = {'aws': 'aws', 'google': 'gs'} | |
| 1675 uri_map = {'aws': 's3', 'google': 'gs'} | |
| 1676 key_ids = {} | |
| 1677 sec_keys = {} | |
| 1678 if use_oauth2: | |
| 1679 oauth2_refresh_token = oauth2_helper.OAuth2ApprovalFlow( | |
| 1680 oauth2_helper.OAuth2ClientFromBotoConfig(boto.config), | |
| 1681 oauth2_scopes, launch_browser) | |
| 1682 else: | |
| 1683 got_creds = False | |
| 1684 for provider in provider_map: | |
| 1685 if provider == 'google': | |
| 1686 key_ids[provider] = raw_input('What is your %s access key ID? ' % | |
| 1687 provider) | |
| 1688 sec_keys[provider] = raw_input('What is your %s secret access key? ' % | |
| 1689 provider) | |
| 1690 got_creds = True | |
| 1691 if not key_ids[provider] or not sec_keys[provider]: | |
| 1692 raise CommandException( | |
| 1693 'Incomplete credentials provided. Please try again.') | |
| 1694 if not got_creds: | |
| 1695 raise CommandException('No credentials provided. Please try again.') | |
| 1696 | |
| 1697 # Write the config file prelude. | |
| 1698 config_file.write(CONFIG_PRELUDE_CONTENT) | |
| 1699 config_file.write( | |
| 1700 '# This file was created by gsutil version "%s"\n# at %s.\n' | |
| 1701 % (self.LoadVersionString(), | |
| 1702 datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'))) | |
| 1703 config_file.write('#\n# You can create additional configuration files by ' | |
| 1704 'running\n# gsutil config [options] [-o <config-file>]\n\n\n') | |
| 1705 | |
| 1706 # Write the config file Credentials section. | |
| 1707 config_file.write('[Credentials]\n\n') | |
| 1708 if use_oauth2: | |
| 1709 config_file.write('# Google OAuth2 credentials (for "gs://" URIs):\n') | |
| 1710 config_file.write('# The following OAuth2 token is authorized for ' | |
| 1711 'scope(s):\n') | |
| 1712 for scope in oauth2_scopes: | |
| 1713 config_file.write('# %s\n' % scope) | |
| 1714 config_file.write('gs_oauth2_refresh_token = %s\n\n' % | |
| 1715 oauth2_refresh_token.refresh_token) | |
| 1716 else: | |
| 1717 config_file.write('# To add Google OAuth2 credentials ("gs://" URIs), ' | |
| 1718 'edit and uncomment the\n# following line:\n' | |
| 1719 '#gs_oauth2_refresh_token = <your OAuth2 refresh token>\n\n') | |
| 1720 | |
| 1721 for provider in provider_map: | |
| 1722 key_prefix = provider_map[provider] | |
| 1723 uri_scheme = uri_map[provider] | |
| 1724 if provider in key_ids and provider in sec_keys: | |
| 1725 config_file.write('# %s credentials ("%s://" URIs):\n' % | |
| 1726 (provider, uri_scheme)) | |
| 1727 config_file.write('%s_access_key_id = %s\n' % | |
| 1728 (key_prefix, key_ids[provider])) | |
| 1729 config_file.write('%s_secret_access_key = %s\n' % | |
| 1730 (key_prefix, sec_keys[provider])) | |
| 1731 else: | |
| 1732 config_file.write('# To add %s credentials ("%s://" URIs), edit and ' | |
| 1733 'uncomment the\n# following two lines:\n' | |
| 1734 '#%s_access_key_id = <your %s access key ID>\n' | |
| 1735 '#%s_secret_access_key = <your %s secret access key>\n' % | |
| 1736 (provider, uri_scheme, key_prefix, provider, key_prefix, | |
| 1737 provider)) | |
| 1738 host_key = Provider.HostKeyMap[provider] | |
| 1739 config_file.write('# The ability to specify an alternate storage host ' | |
| 1740 'is primarily for cloud\n# storage service developers.\n' | |
| 1741 '#%s_host = <alternate storage host address>\n\n' % host_key) | |
| 1742 | |
| 1743 # Write the config file Boto section. | |
| 1744 config_file.write('%s\n' % CONFIG_BOTO_SECTION_CONTENT) | |
| 1745 | |
| 1746 # Write the config file GSUtil section that doesn't depend on user input. | |
| 1747 config_file.write(CONFIG_INPUTLESS_GSUTIL_SECTION_CONTENT) | |
| 1748 | |
| 1749 # Write the config file GSUtil section that includes the default | |
| 1750 # project ID input from the user. | |
| 1751 if launch_browser: | |
| 1752 sys.stdout.write( | |
| 1753 'Attempting to launch a browser to open the Google API console at ' | |
| 1754 'URL: %s\n\n' | |
| 1755 '[Note: due to a Python bug, you may see a spurious error message ' | |
| 1756 '"object is not\n callable [...] in [...] Popen.__del__" which can ' | |
| 1757 'be ignored.]\n\n' % GOOG_API_CONSOLE_URI) | |
| 1758 sys.stdout.write( | |
| 1759 'In your browser you should see the API Console. Click "Storage" and l
ook ' | |
| 1760 'for the value under "Identifying your project\n\n') | |
| 1761 if not webbrowser.open(GOOG_API_CONSOLE_URI, new=1, autoraise=True): | |
| 1762 sys.stdout.write( | |
| 1763 'Launching browser appears to have failed; please navigate a browser
' | |
| 1764 'to the following URL:\n%s\n' % GOOG_API_CONSOLE_URI) | |
| 1765 # Short delay; webbrowser.open on linux insists on printing out a message | |
| 1766 # which we don't want to run into the prompt for the auth code. | |
| 1767 time.sleep(2) | |
| 1768 else: | |
| 1769 sys.stdout.write( | |
| 1770 '\nPlease navigate your browser to %s,\nthen click "Services" on the ' | |
| 1771 'left side panel and ensure you have Storage' | |
| 1772 '\nactivated, then click "Storage" on the left side panel and find ' | |
| 1773 'the "x-goog-project-id" on that page.\n' % | |
| 1774 GOOG_API_CONSOLE_URI) | |
| 1775 default_project_id = raw_input('What is your project-id? ') | |
| 1776 project_id_section_prelude = """ | |
| 1777 # 'default_project_id' specifies the default Google Storage project ID to use | |
| 1778 # with the 'mb' and 'ls' commands. If defined it overrides the default value | |
| 1779 # you set in the API Console. Either of these defaults can be overridden | |
| 1780 # by specifying the -p option to the 'mb' and 'ls' commands. | |
| 1781 """ | |
| 1782 if default_project_id: | |
| 1783 config_file.write('%sdefault_project_id = %s\n\n\n' % | |
| 1784 (project_id_section_prelude, default_project_id)) | |
| 1785 else: | |
| 1786 sys.stderr.write('No default project ID entered. You will need to edit ' | |
| 1787 'the default_project_id value\nin your boto config file ' | |
| 1788 'before using "gsutil ls gs://" or "mb" commands' | |
| 1789 'with the default API version 2.\n') | |
| 1790 config_file.write('%s#default_project_id = <value>\n\n\n' % | |
| 1791 project_id_section_prelude) | |
| 1792 | |
| 1793 # Write the config file OAuth2 section. | |
| 1794 config_file.write(CONFIG_OAUTH2_CONFIG_CONTENT) | |
| 1795 | |
| 1796 def CreateConfigCommand(self, args_unused=[], sub_opts=[], | |
| 1797 headers_unused=None, debug=0): | |
| 1798 """Implementation of the 'config' command. | |
| 1799 | |
| 1800 Args: | |
| 1801 sub_opts: list of command-specific options from getopt. | |
| 1802 debug: debug level to pass in to boto connection (range 0..3). | |
| 1803 | |
| 1804 Raises: | |
| 1805 CommandException: if errors encountered. | |
| 1806 """ | |
| 1807 scopes = [] | |
| 1808 use_oauth2 = True | |
| 1809 launch_browser = False | |
| 1810 output_file_name = None | |
| 1811 for opt, opt_arg in sub_opts: | |
| 1812 if opt == '-h': | |
| 1813 sys.stderr.write(CONFIG_COMMAND_HELP) | |
| 1814 sys.exit(0) | |
| 1815 if opt == '-a': | |
| 1816 use_oauth2 = False | |
| 1817 if opt == '-b': | |
| 1818 launch_browser = True | |
| 1819 if opt == '-f': | |
| 1820 scopes.append(SCOPE_FULL_CONTROL) | |
| 1821 if opt == '-w': | |
| 1822 scopes.append(SCOPE_READ_WRITE) | |
| 1823 if opt == '-r': | |
| 1824 scopes.append(SCOPE_READ_ONLY) | |
| 1825 if opt == '-s': | |
| 1826 scopes.append(opt_arg) | |
| 1827 if opt == '-o': | |
| 1828 output_file_name = opt_arg | |
| 1829 | |
| 1830 if use_oauth2 and not _HAVE_OAUTH2: | |
| 1831 raise CommandException( | |
| 1832 "OAuth2 is only supported when running under Python 2.6 or later\n" | |
| 1833 "(unless additional dependencies are installed, " | |
| 1834 "see README for details);\n" | |
| 1835 "you are running Python %s.\nUse 'gsutil config -a' to create a " | |
| 1836 "config with Developer Key authentication credentials." % sys.version) | |
| 1837 | |
| 1838 if len(scopes) == 0: | |
| 1839 scopes.append(SCOPE_FULL_CONTROL) | |
| 1840 | |
| 1841 if output_file_name is None: | |
| 1842 # Use the default config file name, if it doesn't exist or can be moved | |
| 1843 # out of the way without clobbering an existing backup file. | |
| 1844 default_config_path = os.path.expanduser(os.path.join('~', '.boto')) | |
| 1845 if not os.path.exists(default_config_path): | |
| 1846 output_file_name = default_config_path | |
| 1847 else: | |
| 1848 default_config_path_bak = default_config_path + ".bak" | |
| 1849 if os.path.exists(default_config_path_bak): | |
| 1850 raise CommandException("Cannot back up existing config " | |
| 1851 "file '%s': backup file exists ('%s')." | |
| 1852 % (default_config_path, default_config_path_bak)) | |
| 1853 else: | |
| 1854 try: | |
| 1855 sys.stderr.write( | |
| 1856 "Backing up existing config file '%s' to '%s'...\n" | |
| 1857 % (default_config_path, default_config_path_bak)) | |
| 1858 os.rename(default_config_path, default_config_path_bak) | |
| 1859 except e: | |
| 1860 raise CommandException("Failed to back up existing config " | |
| 1861 "file ('%s' -> '%s'): %s." | |
| 1862 % (default_config_path, default_config_path_bak, e)) | |
| 1863 output_file_name = default_config_path | |
| 1864 | |
| 1865 if output_file_name == '-': | |
| 1866 output_file = sys.stdout | |
| 1867 else: | |
| 1868 output_file = OpenConfigFile(output_file_name) | |
| 1869 sys.stderr.write( | |
| 1870 'This script will create a boto config file at\n%s\ncontaining your ' | |
| 1871 'credentials, based on your responses to the following questions.\n\n' | |
| 1872 % output_file_name) | |
| 1873 | |
| 1874 try: | |
| 1875 self.WriteBotoConfigFile(output_file, use_oauth2=use_oauth2, | |
| 1876 launch_browser=launch_browser, oauth2_scopes=scopes) | |
| 1877 except Exception, e: | |
| 1878 # If an error occurred during config file creation, remove the invalid | |
| 1879 # config file. | |
| 1880 if output_file_name != '-': | |
| 1881 output_file.close() | |
| 1882 os.unlink(output_file_name) | |
| 1883 raise | |
| 1884 | |
| 1885 if output_file_name != '-': | |
| 1886 output_file.close() | |
| 1887 sys.stderr.write( | |
| 1888 '\nBoto config file "%s" created. If you need to use\na proxy to ' | |
| 1889 'access the Internet please see the instructions in that file.\n' | |
| 1890 % output_file_name) | |
| OLD | NEW |