| OLD | NEW |
| (Empty) | |
| 1 # Copyright 2011 Google Inc. |
| 2 # Copyright 2011, Nexenta Systems Inc. |
| 3 # |
| 4 # Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 # you may not use this file except in compliance with the License. |
| 6 # You may obtain a copy of the License at |
| 7 # |
| 8 # http://www.apache.org/licenses/LICENSE-2.0 |
| 9 # |
| 10 # Unless required by applicable law or agreed to in writing, software |
| 11 # distributed under the License is distributed on an "AS IS" BASIS, |
| 12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 # See the License for the specific language governing permissions and |
| 14 # limitations under the License. |
| 15 |
| 16 import boto |
| 17 import ctypes |
| 18 import errno |
| 19 import gzip |
| 20 import hashlib |
| 21 import mimetypes |
| 22 import os |
| 23 import platform |
| 24 import re |
| 25 import subprocess |
| 26 import sys |
| 27 import tempfile |
| 28 import threading |
| 29 import time |
| 30 |
| 31 from boto.gs.resumable_upload_handler import ResumableUploadHandler |
| 32 from boto.s3.resumable_download_handler import ResumableDownloadHandler |
| 33 from gslib.bucket_listing_ref import BucketListingRef |
| 34 from gslib.command import Command |
| 35 from gslib.command import COMMAND_NAME |
| 36 from gslib.command import COMMAND_NAME_ALIASES |
| 37 from gslib.command import CONFIG_REQUIRED |
| 38 from gslib.command import FILE_URIS_OK |
| 39 from gslib.command import MAX_ARGS |
| 40 from gslib.command import MIN_ARGS |
| 41 from gslib.command import PROVIDER_URIS_OK |
| 42 from gslib.command import SUPPORTED_SUB_ARGS |
| 43 from gslib.command import URIS_START_ARG |
| 44 from gslib.exception import CommandException |
| 45 from gslib.help_provider import HELP_NAME |
| 46 from gslib.help_provider import HELP_NAME_ALIASES |
| 47 from gslib.help_provider import HELP_ONE_LINE_SUMMARY |
| 48 from gslib.help_provider import HELP_TEXT |
| 49 from gslib.help_provider import HelpType |
| 50 from gslib.help_provider import HELP_TYPE |
| 51 from gslib.util import MakeHumanReadable |
| 52 from gslib.util import NO_MAX |
| 53 from gslib.util import ONE_MB |
| 54 from gslib.wildcard_iterator import ContainsWildcard |
| 55 |
| 56 _detailed_help_text = (""" |
| 57 <B>SYNOPSIS</B> |
| 58 gsutil cp [-a canned_acl] [-e] [-p] [-z ext1,ext2,...] src_uri dst_uri |
| 59 - or - |
| 60 gsutil cp [-a canned_acl] [-e] [-p] [-R] [-z extensions] uri... dst_uri |
| 61 |
| 62 |
| 63 <B>DESCRIPTION</B> |
| 64 The gsutil cp command allows you to copy data between your local file |
| 65 system and the cloud, copy data within the cloud, and copy data between |
| 66 cloud storage providers. For example, to copy all text files from the |
| 67 local directory to a bucket you could do: |
| 68 |
| 69 gsutil cp *.txt gs://my_bucket |
| 70 |
| 71 Similarly, you can download text files from a bucket by doing: |
| 72 |
| 73 gsutil cp gs://my_bucket/*.txt . |
| 74 |
| 75 If you want to copy an entire directory tree you need to use the -R option: |
| 76 |
| 77 gsutil cp -R dir gs://my_bucket |
| 78 |
| 79 If you have a large number of files to upload you might want to use the |
| 80 gsutil -m option, to perform a parallel (multi-threaded/multi-processing) |
| 81 copy: |
| 82 |
| 83 gsutil -m cp -R dir gs://my_bucket |
| 84 |
| 85 |
| 86 <B>HOW NAMES ARE CONSTRUCTED</B> |
| 87 The gsutil cp command strives to name objects in a way consistent with how |
| 88 UNIX cp works, which causes names to be constructed varying ways depending on |
| 89 whether you're performing a recursive directory copy or copying individually |
| 90 named objects; and whether you're copying to an existing or non-existent |
| 91 directory. |
| 92 |
| 93 When performing recursive directory copies, object names are constructed |
| 94 that mirror the source directory structure starting at the point of |
| 95 recursive processing. For example, the command: |
| 96 |
| 97 gsutil cp -R dir1/dir2 gs://my_bucket |
| 98 |
| 99 will create objects named like gs://my_bucket/dir2/a/b/c, assuming |
| 100 dir1/dir2 contains the file a/b/c. |
| 101 |
| 102 In contrast, copying individually named files will result in objects named |
| 103 by the final path component of the source files. For example, the command: |
| 104 |
| 105 gsutil cp dir1/dir2/** gs://my_bucket |
| 106 |
| 107 will create objects named like gs://my_bucket/c. |
| 108 |
| 109 The same rules apply for downloads: recursive copies of buckets and |
| 110 bucket subdirectories produce mirrored filename structure, while copying |
| 111 individually (or wildcard) named objects produce flatly named files. |
| 112 |
| 113 Note that in the above example the '**' wildcard matches all names |
| 114 anywhere under dir. The wildcard '*' will match just one level deep |
| 115 names. For more details see 'gsutil help wildcards'. |
| 116 |
| 117 There's an additional wrinkle when working with subdirectories: the resulting |
| 118 names depend on whether the destination subdirectory exists. For example, |
| 119 if gs://my_bucket/subdir exists as a subdirectory, the command: |
| 120 |
| 121 gsutil cp -R dir1/dir2 gs://my_bucket/subdir |
| 122 |
| 123 will create objects named like gs://my_bucket/subdir/dir2/a/b/c. In contrast, |
| 124 if gs://my_bucket/subdir does not exist, this same gsutil cp command will |
| 125 create objects named like gs://my_bucket/subdir/a/b/c. |
| 126 |
| 127 |
| 128 <B>COPYING TO/FROM SUBDIRECTORIES; DISTRIBUTING TRANSFERS ACROSS MACHINES</B> |
| 129 You can use gsutil to copy to and from subdirectories by using a command like: |
| 130 |
| 131 gsutil cp -R dir gs://my_bucket/data |
| 132 |
| 133 This will cause dir and all of its files and nested subdirectories to be |
| 134 copied under the specified destination, resulting in objects with names like |
| 135 gs://my_bucket/data/dir/a/b/c. Similarly you can download from bucket |
| 136 subdirectories by using a command like: |
| 137 |
| 138 gsutil cp -R gs://my_bucket/data dir |
| 139 |
| 140 This will cause everything nested under gs://my_bucket/data dir to be |
| 141 downloaded to files, resulting in files with names like dir/data/a/b/c. |
| 142 |
| 143 Copying subdirectories is useful if you want to add data to an existing |
| 144 bucket directory structure over time. It's also useful if you want |
| 145 to parallelize uploads and downloads across multiple machines (often |
| 146 reducing overall transfer time compared with simply running gsutil -m |
| 147 cp on one machine). For example, if your bucket contains this structure: |
| 148 |
| 149 gs://my_bucket/data/result_set_01/ |
| 150 gs://my_bucket/data/result_set_02/ |
| 151 ... |
| 152 gs://my_bucket/data/result_set_99/ |
| 153 |
| 154 you could perform concurrent downloads across 3 machines by running these |
| 155 commands on each machine, respectively: |
| 156 |
| 157 gsutil cp -R gs://my_bucket/data/result_set_[0-3]* dir |
| 158 gsutil cp -R gs://my_bucket/data/result_set_[4-6]* dir |
| 159 gsutil cp -R gs://my_bucket/data/result_set_[7-9]* dir |
| 160 |
| 161 Note that dir could be a local directory on each machine, or it could |
| 162 be a directory mounted off of a shared file server; whether the latter |
| 163 performs acceptably may depend on a number of things, so we recommend |
| 164 you experiment and find out what works best for you. |
| 165 |
| 166 |
| 167 <B>COPYING IN THE CLOUD AND METADATA PRESERVATION</B> |
| 168 If both the source and destination URI are cloud URIs from the same |
| 169 provider, gsutil copies data "in the cloud" (i.e., without downloading |
| 170 to and uploading from the machine where you run gsutil). In addition to |
| 171 the performance and cost advantages of doing this, copying in the cloud |
| 172 preserves metadata (like Content-Type and Cache-Control). In contrast, |
| 173 when you download data from the cloud it ends up in a file, which has |
| 174 no associated metadata. Thus, unless you have some way to hold on to |
| 175 or re-create that metadata, downloading to a file will not retain the |
| 176 metadata. |
| 177 |
| 178 Note that by default, the gsutil cp command does not copy the object |
| 179 ACL to the new object, and instead will use the default bucket ACL (see |
| 180 "gsutil help setdefacl"). You can override this behavior with the -p |
| 181 option (see OPTIONS below). |
| 182 |
| 183 |
| 184 <B>RESUMABLE TRANSFERS</B> |
| 185 gsutil automatically uses the Google Cloud Storage resumable upload |
| 186 feature whenever you use the cp command to upload an object that is larger |
| 187 than 1 MB. You do not need to specify any special command line options |
| 188 to make this happen. If your upload is interrupted you can restart the |
| 189 upload by running the same cp command that you ran to start the upload. |
| 190 |
| 191 Similarly, gsutil automatically performs resumable downloads (using HTTP |
| 192 standard Range GET operations) whenever you use the cp command to download an |
| 193 object larger than 1 MB. |
| 194 |
| 195 Resumable uploads and downloads store some state information in a file named |
| 196 by the file being uploaded (or object being downloaded) in ~/.gsutil. If you |
| 197 attempt to resume a transfer from a machine with a different directory, the |
| 198 transfer will start over from scratch. |
| 199 |
| 200 See also "gsutil help prod" for details on using resumable transfers |
| 201 in production. |
| 202 |
| 203 |
| 204 <B>STREAMING TRANSFERS</B> |
| 205 Use '-' in place of src_uri or dst_uri to perform a streaming |
| 206 transfer. For example: |
| 207 long_running_computation | gsutil cp - gs://my_bucket/obj |
| 208 |
| 209 Streaming transfers do not support resumable uploads/downloads. |
| 210 |
| 211 |
| 212 <B>OPTIONS</B> |
| 213 -a Sets named canned_acl when uploaded objects created. See |
| 214 'gsutil help acls' for further details. |
| 215 |
| 216 -e Exclude symlinks. When specified, symbolic links will not be |
| 217 copied. |
| 218 |
| 219 -p Causes ACL to be preserved when copying in the cloud. Note that |
| 220 this option has performance and cost implications, because it |
| 221 is essentially performing three requests (getacl, cp, setacl). |
| 222 (The performance issue can be mitigated to some degree by |
| 223 using gsutil -m cp to cause parallel copying.) |
| 224 |
| 225 -R, -r Causes directories, buckets, and bucket subdirectories to be |
| 226 copied recursively. If you neglect to use this option for |
| 227 an upload, gsutil will copy any files it finds and skip any |
| 228 directories. Similarly, neglecting to specify -R for a download |
| 229 will cause gsutil to copy any objects at the current bucket |
| 230 directory level, and skip any subdirectories. |
| 231 |
| 232 -t DEPRECATED. This option used to be used to request setting |
| 233 Content-Type based on file extension and/or content, which is |
| 234 now the default behavior. The -t option is left in place for |
| 235 now to avoid breaking existing scripts. It will be removed at |
| 236 a future date. |
| 237 |
| 238 -z 'txt,html' Compresses file uploads with the given extensions. |
| 239 If you are uploading a large file with compressible content, |
| 240 such as a .js, .css, or .html file, you can gzip-compress the |
| 241 file during the upload process by specifying the -z <extensions> |
| 242 option. Compressing data before upload saves on usage charges |
| 243 because you are uploading a smaller amount of data. |
| 244 |
| 245 When you specify the -z option, the data from your files is |
| 246 compressed before it is uploaded, but your actual files are left |
| 247 uncompressed on the local disk. The uploaded objects retain the |
| 248 original content type and name as the original files but are given |
| 249 a Content-Encoding header with the value "gzip" to indicate that |
| 250 the object data stored compressed on the Google Cloud Storage |
| 251 servers. |
| 252 |
| 253 The -z option is most useful in combination with Content-Type |
| 254 recognition (see "gsutil help metadata"). For example, the |
| 255 following command: |
| 256 |
| 257 gsutil cp -z html -a public-read cattypes.html gs://mycats |
| 258 |
| 259 will do all of the following: |
| 260 - Upload as the object gs://mycats/cattypes.html (cp command) |
| 261 - Set the Content-Type to text/html (based on file extension) |
| 262 - Compress the data in the file cattypes.html (-z option) |
| 263 - Set the Content-Encoding to gzip (-z option) |
| 264 - Set the ACL to public-read (-a option) |
| 265 - If a user tries to view cattypes.html in a browser, the |
| 266 browser will know to uncompress the data based on the |
| 267 Content-Encoding header, and to render it as HTML based on |
| 268 the Content-Type header. |
| 269 """) |
| 270 |
| 271 |
| 272 class CpCommand(Command): |
| 273 """Implementation of gsutil cp command.""" |
| 274 |
| 275 # Set default Content-Type type. |
| 276 DEFAULT_CONTENT_TYPE = 'application/octet-stream' |
| 277 USE_MAGICFILE = boto.config.getbool('GSUtil', 'use_magicfile', False) |
| 278 |
| 279 # Command specification (processed by parent class). |
| 280 command_spec = { |
| 281 # Name of command. |
| 282 COMMAND_NAME : 'cp', |
| 283 # List of command name aliases. |
| 284 COMMAND_NAME_ALIASES : ['copy'], |
| 285 # Min number of args required by this command. |
| 286 MIN_ARGS : 2, |
| 287 # Max number of args required by this command, or NO_MAX. |
| 288 MAX_ARGS : NO_MAX, |
| 289 # Getopt-style string specifying acceptable sub args. |
| 290 # -t is deprecated but leave intact for now to avoid breakage. |
| 291 SUPPORTED_SUB_ARGS : 'a:eMprRtz:', |
| 292 # True if file URIs acceptable for this command. |
| 293 FILE_URIS_OK : True, |
| 294 # True if provider-only URIs acceptable for this command. |
| 295 PROVIDER_URIS_OK : False, |
| 296 # Index in args of first URI arg. |
| 297 URIS_START_ARG : 0, |
| 298 # True if must configure gsutil before running command. |
| 299 CONFIG_REQUIRED : True, |
| 300 } |
| 301 help_spec = { |
| 302 # Name of command or auxiliary help info for which this help applies. |
| 303 HELP_NAME : 'cp', |
| 304 # List of help name aliases. |
| 305 HELP_NAME_ALIASES : ['copy'], |
| 306 # Type of help: |
| 307 HELP_TYPE : HelpType.COMMAND_HELP, |
| 308 # One line summary of this help. |
| 309 HELP_ONE_LINE_SUMMARY : 'Copy files and objects', |
| 310 # The full help text. |
| 311 HELP_TEXT : _detailed_help_text, |
| 312 } |
| 313 |
| 314 def _CheckFinalMd5(self, key, file_name): |
| 315 """ |
| 316 Checks that etag from server agrees with md5 computed after the |
| 317 download completes. This is important, since the download could |
| 318 have spanned a number of hours and multiple processes (e.g., |
| 319 gsutil runs), and the user could change some of the file and not |
| 320 realize they have inconsistent data. |
| 321 """ |
| 322 # Open file in binary mode to avoid surprises in Windows. |
| 323 fp = open(file_name, 'rb') |
| 324 try: |
| 325 file_md5 = key.compute_md5(fp)[0] |
| 326 finally: |
| 327 fp.close() |
| 328 obj_md5 = key.etag.strip('"\'') |
| 329 if self.debug: |
| 330 print 'Checking file md5 against etag. (%s/%s)' % (file_md5, obj_md5) |
| 331 if file_md5 != obj_md5: |
| 332 # Checksums don't match - remove file and raise exception. |
| 333 os.unlink(file_name) |
| 334 raise CommandException( |
| 335 'File changed during download: md5 signature doesn\'t match ' |
| 336 'etag (incorrect downloaded file deleted)') |
| 337 |
| 338 def _CheckForDirFileConflict(self, exp_src_uri, dst_uri): |
| 339 """Checks whether copying exp_src_uri into dst_uri is not possible. |
| 340 |
| 341 This happens if a directory exists in local file system where a file |
| 342 needs to go or vice versa. In that case we print an error message and |
| 343 exits. Example: if the file "./x" exists and you try to do: |
| 344 gsutil cp gs://mybucket/x/y . |
| 345 the request can't succeed because it requires a directory where |
| 346 the file x exists. |
| 347 |
| 348 Note that we don't enforce any corresponding restrictions for buckets, |
| 349 because the flat namespace semantics for buckets doesn't prohibit such |
| 350 cases the way hierarchical file systems do. For example, if a bucket |
| 351 contains an object called gs://bucket/dir and then you run the command: |
| 352 gsutil cp file1 file2 gs://bucket/dir |
| 353 you'll end up with objects gs://bucket/dir, gs://bucket/dir/file1, and |
| 354 gs://bucket/dir/file2. |
| 355 |
| 356 Args: |
| 357 exp_src_uri: Expanded source StorageUri of copy. |
| 358 dst_uri: Destination URI. |
| 359 |
| 360 Raises: |
| 361 CommandException: if errors encountered. |
| 362 """ |
| 363 if dst_uri.is_cloud_uri(): |
| 364 # The problem can only happen for file destination URIs. |
| 365 return |
| 366 dst_path = dst_uri.object_name |
| 367 final_dir = os.path.dirname(dst_path) |
| 368 if os.path.isfile(final_dir): |
| 369 raise CommandException('Cannot retrieve %s because a file exists ' |
| 370 'where a directory needs to be created (%s).' % |
| 371 (exp_src_uri, final_dir)) |
| 372 if os.path.isdir(dst_path): |
| 373 raise CommandException('Cannot retrieve %s because a directory exists ' |
| 374 '(%s) where the file needs to be created.' % |
| 375 (exp_src_uri, dst_path)) |
| 376 |
| 377 def _InsistDstUriNamesContainer(self, exp_dst_uri, |
| 378 have_existing_dst_container, command_name): |
| 379 """ |
| 380 Raises an exception if URI doesn't name a directory, bucket, or bucket |
| 381 subdir, with special exception for cp -R (see comments below). |
| 382 |
| 383 Args: |
| 384 exp_dst_uri: Wildcard-expanding dst_uri. |
| 385 have_existing_dst_container: bool indicator of whether exp_dst_uri |
| 386 names a container (directory, bucket, or bucket subdir). |
| 387 command_name: Name of command making call. May not be the same as |
| 388 self.command_name in the case of commands implemented atop other |
| 389 commands (like mv command). |
| 390 |
| 391 Raises: |
| 392 CommandException: if the URI being checked does not name a container. |
| 393 """ |
| 394 if exp_dst_uri.is_file_uri(): |
| 395 ok = exp_dst_uri.names_directory() |
| 396 else: |
| 397 if have_existing_dst_container: |
| 398 ok = True |
| 399 else: |
| 400 # It's ok to specify a non-existing bucket subdir if this is a |
| 401 # recursive copy, such as: |
| 402 # gsutil cp -R dir gs://bucket/abc |
| 403 # where there is no existing subdir gs://bucket/abc. |
| 404 ok = self.recursion_requested and exp_dst_uri.names_object() |
| 405 if not ok: |
| 406 raise CommandException('Destination URI must name a directory, bucket, ' |
| 407 'or bucket\nsubdirectory for the multiple ' |
| 408 'source form of the %s command.' % command_name) |
| 409 |
| 410 class _FileCopyCallbackHandler(object): |
| 411 """Outputs progress info for large copy requests.""" |
| 412 |
| 413 def __init__(self, upload): |
| 414 if upload: |
| 415 self.announce_text = 'Uploading' |
| 416 else: |
| 417 self.announce_text = 'Downloading' |
| 418 |
| 419 def call(self, total_bytes_transferred, total_size): |
| 420 sys.stderr.write('%s: %s/%s \r' % ( |
| 421 self.announce_text, |
| 422 MakeHumanReadable(total_bytes_transferred), |
| 423 MakeHumanReadable(total_size))) |
| 424 if total_bytes_transferred == total_size: |
| 425 sys.stderr.write('\n') |
| 426 |
| 427 class _StreamCopyCallbackHandler(object): |
| 428 """Outputs progress info for Stream copy to cloud. |
| 429 Total Size of the stream is not known, so we output |
| 430 only the bytes transferred. |
| 431 """ |
| 432 |
| 433 def call(self, total_bytes_transferred, total_size): |
| 434 sys.stderr.write('Uploading: %s \r' % ( |
| 435 MakeHumanReadable(total_bytes_transferred))) |
| 436 if total_size and total_bytes_transferred == total_size: |
| 437 sys.stderr.write('\n') |
| 438 |
| 439 def _GetTransferHandlers(self, uri, key, file_size, upload): |
| 440 """ |
| 441 Selects upload/download and callback handlers. |
| 442 |
| 443 We use a callback handler that shows a simple textual progress indicator |
| 444 if file_size is above the configurable threshold. |
| 445 |
| 446 We use a resumable transfer handler if file_size is >= the configurable |
| 447 threshold and resumable transfers are supported by the given provider. |
| 448 boto supports resumable downloads for all providers, but resumable |
| 449 uploads are currently only supported by GS. |
| 450 """ |
| 451 config = boto.config |
| 452 resumable_threshold = config.getint('GSUtil', 'resumable_threshold', ONE_MB) |
| 453 if file_size >= resumable_threshold: |
| 454 cb = self._FileCopyCallbackHandler(upload).call |
| 455 num_cb = int(file_size / ONE_MB) |
| 456 resumable_tracker_dir = config.get( |
| 457 'GSUtil', 'resumable_tracker_dir', |
| 458 os.path.expanduser('~' + os.sep + '.gsutil')) |
| 459 if not os.path.exists(resumable_tracker_dir): |
| 460 os.makedirs(resumable_tracker_dir) |
| 461 if upload: |
| 462 # Encode the src bucket and key into the tracker file name. |
| 463 res_tracker_file_name = ( |
| 464 re.sub('[/\\\\]', '_', 'resumable_upload__%s__%s.url' % |
| 465 (key.bucket.name, key.name))) |
| 466 else: |
| 467 # Encode the fully-qualified src file name into the tracker file name. |
| 468 res_tracker_file_name = ( |
| 469 re.sub('[/\\\\]', '_', 'resumable_download__%s.etag' % |
| 470 (os.path.realpath(uri.object_name)))) |
| 471 |
| 472 res_tracker_file_name = _hash_filename(res_tracker_file_name) |
| 473 tracker_file = '%s%s%s' % (resumable_tracker_dir, os.sep, |
| 474 res_tracker_file_name) |
| 475 if upload: |
| 476 if uri.scheme == 'gs': |
| 477 transfer_handler = ResumableUploadHandler(tracker_file) |
| 478 else: |
| 479 transfer_handler = None |
| 480 else: |
| 481 transfer_handler = ResumableDownloadHandler(tracker_file) |
| 482 else: |
| 483 transfer_handler = None |
| 484 cb = None |
| 485 num_cb = None |
| 486 return (cb, num_cb, transfer_handler) |
| 487 |
| 488 # We pass the headers explicitly to this call instead of using self.headers |
| 489 # so we can set different metadata (like Content-Type type) for each object. |
| 490 def _CopyObjToObjSameProvider(self, src_key, src_uri, dst_uri, headers): |
| 491 # Do Object -> object copy within same provider (uses |
| 492 # x-<provider>-copy-source metadata HTTP header to request copying at the |
| 493 # server). |
| 494 src_bucket = src_uri.get_bucket(False, headers) |
| 495 dst_bucket = dst_uri.get_bucket(False, headers) |
| 496 preserve_acl = False |
| 497 if self.sub_opts: |
| 498 for o, a in self.sub_opts: |
| 499 if o == '-p': |
| 500 preserve_acl = True |
| 501 start_time = time.time() |
| 502 # Pass headers in headers param not metadata param, so boto will copy |
| 503 # existing key's metadata and just set the additional headers specified |
| 504 # in the headers param (rather than using the headers to override existing |
| 505 # metadata). In particular this allows us to copy the existing key's |
| 506 # Content-Type and other metadata users need while still being able to |
| 507 # set headers the API needs (like x-goog-project-id). |
| 508 dst_bucket.copy_key(dst_uri.object_name, src_bucket.name, |
| 509 src_uri.object_name, preserve_acl=preserve_acl, |
| 510 headers=headers) |
| 511 end_time = time.time() |
| 512 return (end_time - start_time, src_key.size) |
| 513 |
| 514 def _CheckFreeSpace(self, path): |
| 515 """Return path/drive free space (in bytes).""" |
| 516 if platform.system() == 'Windows': |
| 517 free_bytes = ctypes.c_ulonglong(0) |
| 518 ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(path), None, |
| 519 None, |
| 520 ctypes.pointer(free_bytes)) |
| 521 return free_bytes.value |
| 522 else: |
| 523 (_, f_frsize, _, _, f_bavail, _, _, _, _, _) = os.statvfs(path) |
| 524 return f_frsize * f_bavail |
| 525 |
| 526 def _PerformResumableUploadIfApplies(self, fp, dst_uri, canned_acl, headers): |
| 527 """ |
| 528 Performs resumable upload if supported by provider and file is above |
| 529 threshold, else performs non-resumable upload. |
| 530 |
| 531 Returns (elapsed_time, bytes_transferred). |
| 532 """ |
| 533 start_time = time.time() |
| 534 file_size = os.path.getsize(fp.name) |
| 535 dst_key = dst_uri.new_key(False, headers) |
| 536 (cb, num_cb, res_upload_handler) = self._GetTransferHandlers( |
| 537 dst_uri, dst_key, file_size, True) |
| 538 if dst_uri.scheme == 'gs': |
| 539 # Resumable upload protocol is Google Cloud Storage-specific. |
| 540 dst_key.set_contents_from_file(fp, headers, policy=canned_acl, |
| 541 cb=cb, num_cb=num_cb, |
| 542 res_upload_handler=res_upload_handler) |
| 543 else: |
| 544 dst_key.set_contents_from_file(fp, headers, policy=canned_acl, |
| 545 cb=cb, num_cb=num_cb) |
| 546 if res_upload_handler: |
| 547 bytes_transferred = file_size - res_upload_handler.upload_start_point |
| 548 else: |
| 549 bytes_transferred = file_size |
| 550 end_time = time.time() |
| 551 return (end_time - start_time, bytes_transferred) |
| 552 |
| 553 def _PerformStreamUpload(self, fp, dst_uri, headers, canned_acl=None): |
| 554 """ |
| 555 Performs Stream upload to cloud. |
| 556 |
| 557 Args: |
| 558 fp: The file whose contents to upload. |
| 559 dst_uri: Destination StorageUri. |
| 560 headers: A copy of the headers dictionary. |
| 561 canned_acl: Optional canned ACL to set on the object. |
| 562 |
| 563 Returns (elapsed_time, bytes_transferred). |
| 564 """ |
| 565 start_time = time.time() |
| 566 dst_key = dst_uri.new_key(False, headers) |
| 567 |
| 568 cb = self._StreamCopyCallbackHandler().call |
| 569 dst_key.set_contents_from_stream(fp, headers, policy=canned_acl, cb=cb) |
| 570 try: |
| 571 bytes_transferred = fp.tell() |
| 572 except: |
| 573 bytes_transferred = 0 |
| 574 |
| 575 end_time = time.time() |
| 576 return (end_time - start_time, bytes_transferred) |
| 577 |
| 578 def _GetContentType(self, object_name): |
| 579 # Streams (denoted by '-') are expected to be 'application/octet-stream' |
| 580 # and 'file' would partially consume them. |
| 581 if not object_name == '-': |
| 582 if self.USE_MAGICFILE: |
| 583 p = subprocess.Popen(['file', '--mime-type', object_name], |
| 584 stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
| 585 output, error = p.communicate() |
| 586 if p.returncode != 0 or error: |
| 587 raise CommandException( |
| 588 'Encountered error running "file --mime-type %s" (returncode=%d).' |
| 589 '\n%s' % (object_name, p.returncode, error)) |
| 590 # Parse output by removing line delimiter and splitting on last ": ". |
| 591 mime_type = output.rstrip().rpartition(': ')[2] |
| 592 if mime_type: |
| 593 return mime_type |
| 594 else: |
| 595 return mimetypes.guess_type(object_name)[0] |
| 596 return self.DEFAULT_CONTENT_TYPE |
| 597 |
| 598 def _UploadFileToObject(self, src_key, src_uri, dst_uri, headers): |
| 599 """Helper method for uploading a local file to an object. |
| 600 |
| 601 Args: |
| 602 src_key: Source StorageUri. Must be a file URI. |
| 603 src_uri: Source StorageUri. |
| 604 dst_uri: Destination StorageUri. |
| 605 headers: The headers dictionary. |
| 606 Returns: |
| 607 (elapsed_time, bytes_transferred) excluding overhead like initial HEAD. |
| 608 |
| 609 Raises: |
| 610 CommandException: if errors encountered. |
| 611 """ |
| 612 gzip_exts = [] |
| 613 canned_acl = None |
| 614 # Previously, the -t option was used to request automatic content |
| 615 # type detection, however, whether -t was specified for not, content |
| 616 # detection was being done. To repair this problem while preserving |
| 617 # backward compatibilty, the -t option has been deprecated and content |
| 618 # type detection is now enabled by default unless the Content-Type |
| 619 # header is explicitly specified via the -h option. |
| 620 if self.sub_opts: |
| 621 for o, a in self.sub_opts: |
| 622 if o == '-a': |
| 623 canned_acls = dst_uri.canned_acls() |
| 624 if a not in canned_acls: |
| 625 raise CommandException('Invalid canned ACL "%s".' % a) |
| 626 canned_acl = a |
| 627 elif o == '-t': |
| 628 print 'Warning: -t is deprecated. Content type detection is ' + ( |
| 629 'enabled by default,\nunless inhibited by specifying ') + ( |
| 630 'a Content-Type header via the -h option.') |
| 631 elif o == '-z': |
| 632 gzip_exts = a.split(',') |
| 633 |
| 634 if 'Content-Type' in headers: |
| 635 # Process Content-Type header. If specified via -h option with empty |
| 636 # string (i.e. -h "Content-Type:") set header to None, which will |
| 637 # inhibit boto from sending the CT header. Otherwise, boto will pass |
| 638 # through the user specified CT header. |
| 639 if not headers['Content-Type']: |
| 640 headers['Content-Type'] = None |
| 641 else: |
| 642 # If no CT header was specified via the -h option, we do auto-content |
| 643 # detection and use the results to formulate the Content-Type. |
| 644 mime_type = self._GetContentType(src_uri.object_name) |
| 645 if mime_type: |
| 646 headers['Content-Type'] = mime_type |
| 647 print '\t[Setting Content-Type=%s]' % mime_type |
| 648 else: |
| 649 print '\t[Unknown content type -> using %s]' % self.DEFAULT_CONTENT_TYPE |
| 650 |
| 651 fname_parts = src_uri.object_name.split('.') |
| 652 if len(fname_parts) > 1 and fname_parts[-1] in gzip_exts: |
| 653 if self.debug: |
| 654 print 'Compressing %s (to tmp)...' % src_key |
| 655 gzip_tmp = tempfile.mkstemp() |
| 656 gzip_path = gzip_tmp[1] |
| 657 # Check for temp space. Assume the compressed object is at most 2x |
| 658 # the size of the object (normally should compress to smaller than |
| 659 # the object) |
| 660 if self._CheckFreeSpace(gzip_path) < 2*int(os.path.getsize(src_key.name)): |
| 661 raise CommandException('Inadequate temp space available to compress ' |
| 662 '%s' % src_key.name) |
| 663 gzip_fp = gzip.open(gzip_path, 'wb') |
| 664 try: |
| 665 gzip_fp.writelines(src_key.fp) |
| 666 finally: |
| 667 gzip_fp.close() |
| 668 headers['Content-Encoding'] = 'gzip' |
| 669 gzip_fp = open(gzip_path, 'rb') |
| 670 try: |
| 671 (elapsed_time, bytes_transferred) = ( |
| 672 self._PerformResumableUploadIfApplies(gzip_fp, dst_uri, |
| 673 canned_acl, headers)) |
| 674 finally: |
| 675 gzip_fp.close() |
| 676 os.unlink(gzip_path) |
| 677 elif (src_key.is_stream() |
| 678 and dst_uri.get_provider().supports_chunked_transfer()): |
| 679 (elapsed_time, bytes_transferred) = self._PerformStreamUpload( |
| 680 src_key.fp, dst_uri, headers, canned_acl) |
| 681 else: |
| 682 if src_key.is_stream(): |
| 683 # For Providers that doesn't support chunked Transfers |
| 684 tmp = tempfile.NamedTemporaryFile() |
| 685 file_uri = self.suri_builder.StorageUri('file://%s' % tmp.name) |
| 686 try: |
| 687 file_uri.new_key(False, headers).set_contents_from_file( |
| 688 src_key.fp, headers) |
| 689 src_key = file_uri.get_key() |
| 690 finally: |
| 691 file_uri.close() |
| 692 try: |
| 693 (elapsed_time, bytes_transferred) = ( |
| 694 self._PerformResumableUploadIfApplies(src_key.fp, dst_uri, |
| 695 canned_acl, headers)) |
| 696 finally: |
| 697 if src_key.is_stream(): |
| 698 tmp.close() |
| 699 else: |
| 700 src_key.close() |
| 701 |
| 702 return (elapsed_time, bytes_transferred) |
| 703 |
| 704 def _DownloadObjectToFile(self, src_key, src_uri, dst_uri, headers): |
| 705 (cb, num_cb, res_download_handler) = self._GetTransferHandlers( |
| 706 src_uri, src_key, src_key.size, False) |
| 707 file_name = dst_uri.object_name |
| 708 dir_name = os.path.dirname(file_name) |
| 709 if dir_name and not os.path.exists(dir_name): |
| 710 # Do dir creation in try block so can ignore case where dir already |
| 711 # exists. This is needed to avoid a race condition when running gsutil |
| 712 # -m cp. |
| 713 try: |
| 714 os.makedirs(dir_name) |
| 715 except OSError, e: |
| 716 if e.errno != errno.EEXIST: |
| 717 raise |
| 718 # For gzipped objects not named *.gz download to a temp file and unzip. |
| 719 if (hasattr(src_key, 'content_encoding') |
| 720 and src_key.content_encoding == 'gzip' |
| 721 and not file_name.endswith('.gz')): |
| 722 # We can't use tempfile.mkstemp() here because we need a predictable |
| 723 # filename for resumable downloads. |
| 724 download_file_name = '%s_.gztmp' % file_name |
| 725 need_to_unzip = True |
| 726 else: |
| 727 download_file_name = file_name |
| 728 need_to_unzip = False |
| 729 fp = None |
| 730 try: |
| 731 if res_download_handler: |
| 732 fp = open(download_file_name, 'ab') |
| 733 else: |
| 734 fp = open(download_file_name, 'wb') |
| 735 start_time = time.time() |
| 736 src_key.get_contents_to_file(fp, headers, cb=cb, num_cb=num_cb, |
| 737 res_download_handler=res_download_handler) |
| 738 # If a custom test method is defined, call it here. For the copy command, |
| 739 # test methods are expected to take one argument: an open file pointer, |
| 740 # and are used to perturb the open file during download to exercise |
| 741 # download error detection. |
| 742 if self.test_method: |
| 743 self.test_method(fp) |
| 744 end_time = time.time() |
| 745 finally: |
| 746 if fp: |
| 747 fp.close() |
| 748 |
| 749 # Verify downloaded file checksum matched source object's checksum. |
| 750 self._CheckFinalMd5(src_key, download_file_name) |
| 751 |
| 752 if res_download_handler: |
| 753 bytes_transferred = ( |
| 754 src_key.size - res_download_handler.download_start_point) |
| 755 else: |
| 756 bytes_transferred = src_key.size |
| 757 if need_to_unzip: |
| 758 if self.debug: |
| 759 sys.stderr.write('Uncompressing tmp to %s...\n' % file_name) |
| 760 # Downloaded gzipped file to a filename w/o .gz extension, so unzip. |
| 761 f_in = gzip.open(download_file_name, 'rb') |
| 762 f_out = open(file_name, 'wb') |
| 763 try: |
| 764 f_out.writelines(f_in) |
| 765 finally: |
| 766 f_out.close() |
| 767 f_in.close() |
| 768 os.unlink(download_file_name) |
| 769 return (end_time - start_time, bytes_transferred) |
| 770 |
| 771 def _PerformDownloadToStream(self, src_key, src_uri, str_fp, headers): |
| 772 (cb, num_cb, res_download_handler) = self._GetTransferHandlers( |
| 773 src_uri, src_key, src_key.size, False) |
| 774 start_time = time.time() |
| 775 src_key.get_contents_to_file(str_fp, headers, cb=cb, num_cb=num_cb) |
| 776 end_time = time.time() |
| 777 bytes_transferred = src_key.size |
| 778 end_time = time.time() |
| 779 return (end_time - start_time, bytes_transferred) |
| 780 |
| 781 def _CopyFileToFile(self, src_key, dst_uri, headers): |
| 782 dst_key = dst_uri.new_key(False, headers) |
| 783 start_time = time.time() |
| 784 dst_key.set_contents_from_file(src_key.fp, headers) |
| 785 end_time = time.time() |
| 786 return (end_time - start_time, os.path.getsize(src_key.fp.name)) |
| 787 |
| 788 def _CopyObjToObjDiffProvider(self, src_key, src_uri, dst_uri, headers): |
| 789 # If destination is GS, We can avoid the local copying through a local file |
| 790 # as GS supports chunked transfer. |
| 791 if dst_uri.scheme == 'gs': |
| 792 canned_acls = None |
| 793 if self.sub_opts: |
| 794 for o, a in self.sub_opts: |
| 795 if o == '-a': |
| 796 canned_acls = dst_uri.canned_acls() |
| 797 if a not in canned_acls: |
| 798 raise CommandException('Invalid canned ACL "%s".' % a) |
| 799 canned_acl = a |
| 800 elif o == '-p': |
| 801 # We don't attempt to preserve ACLs across providers because |
| 802 # GCS and S3 support different ACLs. |
| 803 raise NotImplementedError('Cross-provider cp -p not supported') |
| 804 elif o == '-t': |
| 805 mime_type = self._GetContentType(src_uri.object_name) |
| 806 if mime_type: |
| 807 headers['Content-Type'] = mime_type |
| 808 print '\t[Setting Content-Type=%s]' % mime_type |
| 809 else: |
| 810 print '\t[Unknown content type -> using application/octet stream]' |
| 811 |
| 812 # TODO: This _PerformStreamUpload call passes in a Key for fp |
| 813 # param, relying on Python "duck typing" (the fact that the lower-level |
| 814 # methods that expect an fp only happen to call fp methods that are |
| 815 # defined and semantically equivalent to those defined on src_key). This |
| 816 # should be replaced by a class that wraps an fp interface around the |
| 817 # Key, throwing 'not implemented' for methods (like seek) that aren't |
| 818 # implemented by non-file Keys. |
| 819 return self._PerformStreamUpload(src_key, dst_uri, headers, canned_acls) |
| 820 |
| 821 # If destination is not GS, We implement object copy through a local |
| 822 # temp file. Note that a downside of this approach is that killing the |
| 823 # gsutil process partway through and then restarting will always repeat the |
| 824 # download and upload, because the temp file name is different for each |
| 825 # incarnation. (If however you just leave the process running and failures |
| 826 # happen along the way, they will continue to restart and make progress |
| 827 # as long as not too many failures happen in a row with no progress.) |
| 828 tmp = tempfile.NamedTemporaryFile() |
| 829 if self._CheckFreeSpace(tempfile.tempdir) < src_key.size: |
| 830 raise CommandException('Inadequate temp space available to perform the ' |
| 831 'requested copy') |
| 832 start_time = time.time() |
| 833 file_uri = self.suri_builder.StorageUri('file://%s' % tmp.name) |
| 834 try: |
| 835 self._DownloadObjectToFile(src_key, src_uri, file_uri, headers) |
| 836 self._UploadFileToObject(file_uri.get_key(), file_uri, dst_uri, headers) |
| 837 finally: |
| 838 tmp.close() |
| 839 end_time = time.time() |
| 840 return (end_time - start_time, src_key.size) |
| 841 |
| 842 def _PerformCopy(self, src_uri, dst_uri): |
| 843 """Performs copy from src_uri to dst_uri, handling various special cases. |
| 844 |
| 845 Args: |
| 846 src_uri: Source StorageUri. |
| 847 dst_uri: Destination StorageUri. |
| 848 |
| 849 Returns: |
| 850 (elapsed_time, bytes_transferred) excluding overhead like initial HEAD. |
| 851 |
| 852 Raises: |
| 853 CommandException: if errors encountered. |
| 854 """ |
| 855 # Make a copy of the input headers each time so we can set a different |
| 856 # MIME type for each object. |
| 857 if self.headers: |
| 858 headers = self.headers.copy() |
| 859 else: |
| 860 headers = {} |
| 861 |
| 862 src_key = src_uri.get_key(False, headers) |
| 863 if not src_key: |
| 864 raise CommandException('"%s" does not exist.' % src_uri) |
| 865 |
| 866 # Separately handle cases to avoid extra file and network copying of |
| 867 # potentially very large files/objects. |
| 868 |
| 869 if src_uri.is_cloud_uri() and dst_uri.is_cloud_uri(): |
| 870 if src_uri.scheme == dst_uri.scheme: |
| 871 return self._CopyObjToObjSameProvider(src_key, src_uri, dst_uri, |
| 872 headers) |
| 873 else: |
| 874 return self._CopyObjToObjDiffProvider(src_key, src_uri, dst_uri, |
| 875 headers) |
| 876 elif src_uri.is_file_uri() and dst_uri.is_cloud_uri(): |
| 877 return self._UploadFileToObject(src_key, src_uri, dst_uri, headers) |
| 878 elif src_uri.is_cloud_uri() and dst_uri.is_file_uri(): |
| 879 return self._DownloadObjectToFile(src_key, src_uri, dst_uri, headers) |
| 880 elif src_uri.is_file_uri() and dst_uri.is_file_uri(): |
| 881 return self._CopyFileToFile(src_key, dst_uri, headers) |
| 882 else: |
| 883 raise CommandException('Unexpected src/dest case') |
| 884 |
| 885 def _ExpandDstUri(self, src_uri_expansion, dst_uri_str): |
| 886 """ |
| 887 Expands wildcard if present in dst_uri_str. |
| 888 |
| 889 Args: |
| 890 src_uri_expansion: gslib.name_expansion.NameExpansionResult. |
| 891 dst_uri_str: String representation of requested dst_uri. |
| 892 |
| 893 Returns: |
| 894 (exp_dst_uri, have_existing_dst_container) |
| 895 where have_existing_dst_container is a bool indicating whether |
| 896 exp_dst_uri names an existing directory, bucket, or bucket subdirectory. |
| 897 |
| 898 Raises: |
| 899 CommandException: if dst_uri_str matched more than 1 URI. |
| 900 """ |
| 901 dst_uri = self.suri_builder.StorageUri(dst_uri_str) |
| 902 |
| 903 # Handle wildcarded dst_uri case. |
| 904 if ContainsWildcard(dst_uri): |
| 905 blr_expansion = list(self.exp_handler.WildcardIterator(dst_uri)) |
| 906 if len(blr_expansion) != 1: |
| 907 raise CommandException('Destination (%s) must match exactly 1 URI' % |
| 908 dst_uri_str) |
| 909 blr = blr_expansion[0] |
| 910 uri = blr.GetUri() |
| 911 if uri.is_cloud_uri(): |
| 912 return (uri, uri.names_bucket() or blr.HasPrefix()) |
| 913 else: |
| 914 return (uri, uri.names_directory()) |
| 915 |
| 916 # Handle non-wildcarded dst_uri: |
| 917 if dst_uri.is_file_uri(): |
| 918 return (dst_uri, dst_uri.names_directory()) |
| 919 if dst_uri.names_bucket(): |
| 920 return (dst_uri, True) |
| 921 # For object URIs we need to do a wildcard expansion with |
| 922 # dst_uri + "*" and then find if there's a Prefix matching dst_uri. |
| 923 blr_expansion = list(self.exp_handler.WildcardIterator( |
| 924 '%s*' % dst_uri_str.rstrip(dst_uri.delim))) |
| 925 for blr in blr_expansion: |
| 926 if (blr.GetRStrippedUriString() == dst_uri_str.rstrip(dst_uri.delim)): |
| 927 return (dst_uri, blr.HasPrefix()) |
| 928 return (dst_uri, False) |
| 929 |
| 930 def _ConstructDstUri(self, src_uri, exp_src_uri, |
| 931 src_uri_names_container, src_uri_expands_to_multi, |
| 932 have_multiple_srcs, exp_dst_uri, |
| 933 have_existing_dest_subdir): |
| 934 """ |
| 935 Constructs the destination URI for a given exp_src_uri/exp_dst_uri pair, |
| 936 using context-dependent naming rules intended to mimic UNIX cp semantics. |
| 937 |
| 938 Args: |
| 939 src_uri: src_uri to be copied. |
| 940 exp_src_uri: Single StorageUri from wildcard expansion of src_uri. |
| 941 src_uri_names_container: True if src_uri names a container (including the |
| 942 case of a wildcard-named bucket subdir (like gs://bucket/abc, |
| 943 where gs://bucket/abc/* matched some objects). Note that this is |
| 944 additional semantics tha src_uri.names_container() doesn't understand |
| 945 because the latter only understands StorageUris, not wildcards. |
| 946 src_uri_expands_to_multi: True if src_uri expanded to multiple URIs. |
| 947 have_multiple_srcs: True if this is a multi-source request. This can be |
| 948 true if src_uri wildcard-expanded to multiple URIs or if there were |
| 949 multiple source URIs in the request. |
| 950 exp_dst_uri: the expanded StorageUri requested for the cp destination. |
| 951 Final written path is constructed from this plus a context-dependent |
| 952 variant of src_uri. |
| 953 have_existing_dest_subdir: bool indicator whether dest is an existing |
| 954 subdirectory. |
| 955 |
| 956 Returns: |
| 957 StorageUri to use for copy. |
| 958 |
| 959 Raises: |
| 960 CommandException if destination object name not specified for |
| 961 source and source is a stream. |
| 962 """ |
| 963 if self._ShouldTreatDstUriAsSingleton( |
| 964 have_multiple_srcs, have_existing_dest_subdir, exp_dst_uri): |
| 965 # We're copying one file or object to one file or object. |
| 966 return exp_dst_uri |
| 967 |
| 968 if not self.recursion_requested and not have_multiple_srcs: |
| 969 # We're copying one file or object to a subdirectory. Append final comp |
| 970 # of exp_src_uri to exp_dest_uri. |
| 971 src_final_comp = exp_src_uri.object_name.rpartition(src_uri.delim)[-1] |
| 972 return self.suri_builder.StorageUri('%s%s%s' % ( |
| 973 exp_dst_uri.uri.rstrip(exp_dst_uri.delim), exp_dst_uri.delim, |
| 974 src_final_comp)) |
| 975 |
| 976 # Else we're copying multiple sources to a directory, bucket, or a bucket |
| 977 # "sub-directory". |
| 978 |
| 979 # Ensure exp_dst_uri ends in delim char if we're doing a multi-src copy or |
| 980 # a copy to a directory. (The check for copying to a directory needs |
| 981 # special-case handling so that the command: |
| 982 # gsutil cp gs://bucket/obj dir |
| 983 # will turn into file://dir/ instead of file://dir -- the latter would cause |
| 984 # the file "dirobj" to be created.) |
| 985 # Note: need to check have_multiple_srcs or src_uri.names_container() |
| 986 # because src_uri could be a bucket containing a single object, named |
| 987 # as gs://bucket. |
| 988 if ((have_multiple_srcs or src_uri.names_container() |
| 989 or os.path.isdir(exp_dst_uri.object_name)) |
| 990 and not exp_dst_uri.uri.endswith(exp_dst_uri.delim)): |
| 991 exp_dst_uri = exp_dst_uri.clone_replace_name( |
| 992 '%s%s' % (exp_dst_uri.object_name, exp_dst_uri.delim) |
| 993 ) |
| 994 |
| 995 # There are 3 cases for copying multiple sources to a dir/bucket/bucket |
| 996 # subdir needed to match the naming semantics of the UNIX cp command: |
| 997 # 1. For the "mv -R" command, people expect renaming to occur at the |
| 998 # level of the src subdir, vs appending that subdir beneath |
| 999 # the dst subdir like is done for copying. For example: |
| 1000 # gsutil -m rm -R gs://bucket |
| 1001 # gsutil -m cp -R cloudreader gs://bucket |
| 1002 # gsutil -m cp -R cloudauth gs://bucket/subdir1 |
| 1003 # gsutil -m mv -R gs://bucket/subdir1 gs://bucket/subdir2 |
| 1004 # would (if using cp semantics) end up with paths like: |
| 1005 # gs://bucket/subdir2/subdir1/cloudauth/.svn/all-wcprops |
| 1006 # whereas people expect: |
| 1007 # gs://bucket/subdir2/cloudauth/.svn/all-wcprops |
| 1008 # 2. Copying from directories, buckets, or bucket subdirs should result in |
| 1009 # objects/files mirroring the source directory hierarchy. For example: |
| 1010 # gsutil cp dir1/dir2 gs://bucket |
| 1011 # should create the object gs://bucket/dir2/file2, assuming dir1/dir2 |
| 1012 # contains file2). |
| 1013 # To be consistent with UNIX cp behavior, there's one more wrinkle when |
| 1014 # working with subdirs: The resulting object names depend on whether the |
| 1015 # destination subdirectory exists. For example, if gs://bucket/subdir |
| 1016 # exists, the command: |
| 1017 # gsutil cp -R dir1/dir2 gs://bucket/subdir |
| 1018 # should create objects named like gs://bucket/subdir/dir2/a/b/c. In |
| 1019 # contrast, if gs://bucket/subdir does not exist, this same command |
| 1020 # should create objects named like gs://bucket/subdir/a/b/c. |
| 1021 # 3. Copying individual files or objects to dirs, buckets or bucket subdirs |
| 1022 # should result in objects/files named by the final source file name |
| 1023 # component. Example: |
| 1024 # gsutil cp dir1/*.txt gs://bucket |
| 1025 # should create the objects gs://bucket/f1.txt and gs://bucket/f2.txt, |
| 1026 # assuming dir1 contains f1.txt and f2.txt. |
| 1027 |
| 1028 if (self.mv_naming_semantics and self.recursion_requested |
| 1029 and src_uri_expands_to_multi): |
| 1030 # Case 1. Handle naming semantics for recursive bucket subdir mv. |
| 1031 # Here we want to line up the src_uri against its expansion, to find |
| 1032 # the base to build the new name. For example, starting with: |
| 1033 # gsutil mv -R gs://bucket/abcd gs://bucket/xyz |
| 1034 # and exp_src_uri being gs://bucket/abcd/123 |
| 1035 # we want exp_src_uri_tail to be /123 |
| 1036 # Note: mv.py code disallows wildcard specification of source URI. |
| 1037 exp_src_uri_tail = exp_src_uri.uri[len(src_uri.uri):] |
| 1038 dst_key_name = '%s/%s' % (exp_dst_uri.object_name.rstrip('/'), |
| 1039 exp_src_uri_tail.strip('/')) |
| 1040 return exp_dst_uri.clone_replace_name(dst_key_name) |
| 1041 |
| 1042 if src_uri_names_container and not exp_dst_uri.names_file(): |
| 1043 # Case 2. Build dst_key_name from subpath of exp_src_uri past |
| 1044 # where src_uri ends. For example, for src_uri=gs://bucket/ and |
| 1045 # exp_src_uri=gs://bucket/src_subdir/obj, dst_key_name should be |
| 1046 # src_subdir/obj. |
| 1047 src_uri_path_sans_final_dir = _GetPathBeforeFinalDir(src_uri) |
| 1048 dst_key_name = exp_src_uri.uri[ |
| 1049 len(src_uri_path_sans_final_dir):].lstrip(src_uri.delim) |
| 1050 # Handle case where dst_uri is a non-existent subdir. |
| 1051 if not have_existing_dest_subdir: |
| 1052 dst_key_name = dst_key_name.partition(exp_dst_uri.delim)[-1] |
| 1053 # Handle special case where src_uri was a directory named with '.' or |
| 1054 # './', so that running a command like: |
| 1055 # gsutil cp -r . gs://dest |
| 1056 # will produce obj names of the form gs://dest/abc instead of |
| 1057 # gs://dest/./abc. |
| 1058 if dst_key_name.startswith('./'): |
| 1059 dst_key_name = dst_key_name[2:] |
| 1060 |
| 1061 else: |
| 1062 # Case 3. |
| 1063 if exp_src_uri.is_stream(): |
| 1064 raise CommandException('Destination object name needed when ' |
| 1065 'source is a stream') |
| 1066 dst_key_name = exp_src_uri.object_name.rpartition(src_uri.delim)[-1] |
| 1067 |
| 1068 if (exp_dst_uri.is_file_uri() |
| 1069 or self._ShouldTreatDstUriAsBucketSubDir( |
| 1070 have_multiple_srcs, exp_dst_uri)): |
| 1071 dst_key_name = '%s%s' % (exp_dst_uri.object_name, dst_key_name) |
| 1072 |
| 1073 return exp_dst_uri.clone_replace_name(dst_key_name) |
| 1074 |
| 1075 def _FixWindowsNaming(self, src_uri, dst_uri): |
| 1076 """ |
| 1077 Rewrites the destination URI built by _ConstructDstUri() to translate |
| 1078 Windows pathnames to cloud pathnames if neeeded. |
| 1079 |
| 1080 Args: |
| 1081 src_uri: src_uri to be copied. |
| 1082 dst_uri: the destination URI built by _ConstructDstUri(). |
| 1083 |
| 1084 Returns: |
| 1085 StorageUri to use for copy. |
| 1086 """ |
| 1087 if (src_uri.is_file_uri() and src_uri.delim == '\\' |
| 1088 and dst_uri.is_cloud_uri()): |
| 1089 trans_uri_str = re.sub(r'\\', '/', dst_uri.uri) |
| 1090 dst_uri = self.suri_builder.StorageUri(trans_uri_str) |
| 1091 return dst_uri |
| 1092 |
| 1093 # Command entry point. |
| 1094 def RunCommand(self): |
| 1095 |
| 1096 # Inner funcs. |
| 1097 def _CopyExceptionHandler(e): |
| 1098 """Simple exception handler to allow post-completion status.""" |
| 1099 self.THREADED_LOGGER.error(str(e)) |
| 1100 self.copy_failure_count += 1 |
| 1101 |
| 1102 def _CopyFunc(src_uri, exp_src_uri, src_uri_names_container, |
| 1103 src_uri_expands_to_multi, have_multiple_srcs, |
| 1104 have_existing_dest_subdir): |
| 1105 """Worker function for performing the actual copy.""" |
| 1106 if exp_src_uri.is_file_uri() and exp_src_uri.is_stream(): |
| 1107 sys.stderr.write("Copying from <STDIN>...\n") |
| 1108 else: |
| 1109 self.THREADED_LOGGER.info('Copying %s...', exp_src_uri) |
| 1110 dst_uri = self._ConstructDstUri(src_uri, exp_src_uri, |
| 1111 src_uri_names_container, |
| 1112 src_uri_expands_to_multi, |
| 1113 have_multiple_srcs, exp_dst_uri, |
| 1114 have_existing_dest_subdir) |
| 1115 dst_uri = self._FixWindowsNaming(src_uri, dst_uri) |
| 1116 |
| 1117 self._CheckForDirFileConflict(exp_src_uri, dst_uri) |
| 1118 if self._SrcDstSame(exp_src_uri, dst_uri): |
| 1119 raise CommandException('cp: "%s" and "%s" are the same file - ' |
| 1120 'abort.' % (exp_src_uri, dst_uri)) |
| 1121 |
| 1122 (elapsed_time, bytes_transferred) = self._PerformCopy(exp_src_uri, |
| 1123 dst_uri) |
| 1124 stats_lock.acquire() |
| 1125 self.total_elapsed_time += elapsed_time |
| 1126 self.total_bytes_transferred += bytes_transferred |
| 1127 stats_lock.release() |
| 1128 |
| 1129 # Start of RunCommand code. |
| 1130 self._ParseArgs() |
| 1131 |
| 1132 self.total_elapsed_time = self.total_bytes_transferred = 0 |
| 1133 if self.args[-1] == '-' or self.args[-1] == 'file://-': |
| 1134 self._HandleStreamingDownload() |
| 1135 return |
| 1136 |
| 1137 src_uri_expansion = self.exp_handler.ExpandWildcardsAndContainers( |
| 1138 self.args[0:len(self.args)-1], self.recursion_requested) |
| 1139 (exp_dst_uri, have_existing_dst_container) = self._ExpandDstUri( |
| 1140 src_uri_expansion, self.args[-1]) |
| 1141 |
| 1142 self._SanityCheckRequest(src_uri_expansion, exp_dst_uri, |
| 1143 have_existing_dst_container) |
| 1144 |
| 1145 # Use a lock to ensure accurate statistics in the face of |
| 1146 # multi-threading/multi-processing. |
| 1147 stats_lock = threading.Lock() |
| 1148 |
| 1149 # Tracks if any copies failed. |
| 1150 self.copy_failure_count = 0 |
| 1151 |
| 1152 # Start the clock. |
| 1153 start_time = time.time() |
| 1154 |
| 1155 # Tuple of attributes to share/manage across multiple processes in |
| 1156 # parallel (-m) mode. |
| 1157 shared_attrs = ('copy_failure_count', 'total_bytes_transferred') |
| 1158 |
| 1159 # Perform copy requests in parallel (-m) mode, if requested, using |
| 1160 # configured number of parallel processes and threads. Otherwise, |
| 1161 # perform requests with sequential function calls in current process. |
| 1162 self.Apply(_CopyFunc, src_uri_expansion, _CopyExceptionHandler, |
| 1163 have_existing_dst_container, shared_attrs) |
| 1164 if self.debug: |
| 1165 print 'total_bytes_transferred:' + str(self.total_bytes_transferred) |
| 1166 |
| 1167 end_time = time.time() |
| 1168 self.total_elapsed_time = end_time - start_time |
| 1169 |
| 1170 if self.debug == 3: |
| 1171 # Note that this only counts the actual GET and PUT bytes for the copy |
| 1172 # - not any transfers for doing wildcard expansion, the initial HEAD |
| 1173 # request boto performs when doing a bucket.get_key() operation, etc. |
| 1174 if self.total_bytes_transferred != 0: |
| 1175 sys.stderr.write( |
| 1176 'Total bytes copied=%d, total elapsed time=%5.3f secs (%sps)\n' % ( |
| 1177 self.total_bytes_transferred, self.total_elapsed_time, |
| 1178 MakeHumanReadable(float(self.total_bytes_transferred) / |
| 1179 float(self.total_elapsed_time)))) |
| 1180 if self.copy_failure_count: |
| 1181 plural_str = '' |
| 1182 if self.copy_failure_count > 1: |
| 1183 plural_str = 's' |
| 1184 raise CommandException('%d file%s/object%s could not be transferred.' % ( |
| 1185 self.copy_failure_count, plural_str, plural_str)) |
| 1186 |
| 1187 # test specification, see definition of test_steps in base class for |
| 1188 # details on how to populate these fields |
| 1189 test_steps = [ |
| 1190 # (test name, cmd line, ret code, (result_file, expect_file)) |
| 1191 ('upload', 'gsutil cp $F1 gs://$B1/$O1', 0, None), |
| 1192 ('download', 'gsutil cp gs://$B1/$O1 $F9', 0, ('$F9', '$F1')), |
| 1193 ('stream upload', 'cat $F1 | gsutil cp - gs://$B1/$O1', 0, None), |
| 1194 ('check stream upload', 'gsutil cp gs://$B1/$O1 $F9', 0, ('$F9', '$F1')), |
| 1195 # Clean up if we got interupted. |
| 1196 ('remove test files', |
| 1197 'rm -f test.mp3 test_mp3.mime test.gif test_gif.mime test.foo', |
| 1198 0, None), |
| 1199 ('setup mp3 file', 'cp gslib/test_data/test.mp3 test.mp3', 0, None), |
| 1200 ('setup mp3 mime', 'echo audio/mpeg >test_mp3.mime', 0, None), |
| 1201 ('setup gif file', 'cp gslib/test_data/test.gif test.gif', 0, None), |
| 1202 ('setup gif mime', 'echo image/gif >test_gif.mime', 0, None), |
| 1203 # TODO: we don't need test.app and test.bin anymore if |
| 1204 # USE_MAGICFILE=True. Implement a way to test both with and without using |
| 1205 # magic file. |
| 1206 #('setup app file', 'echo application/octet-stream >test.app', 0, None), |
| 1207 ('setup foo file', 'echo foo/bar >test.foo', 0, None), |
| 1208 ('upload mp3', 'gsutil cp test.mp3 gs://$B1/$O1', 0, None), |
| 1209 ('verify mp3', 'gsutil ls -L gs://$B1/$O1 | grep MIME | cut -f3 >$F1', |
| 1210 0, ('$F1', 'test_mp3.mime')), |
| 1211 ('upload gif', 'gsutil cp test.gif gs://$B1/$O1', 0, None), |
| 1212 ('verify gif', 'gsutil ls -L gs://$B1/$O1 | grep MIME | cut -f3 >$F1', |
| 1213 0, ('$F1', 'test_gif.mime')), |
| 1214 # TODO: The commented-out /noCT test below fails with USE_MAGICFILE=True. |
| 1215 ('upload mp3/noCT', |
| 1216 'gsutil -h "Content-Type:" cp test.mp3 gs://$B1/$O1', 0, None), |
| 1217 ('verify mp3/noCT', 'gsutil ls -L gs://$B1/$O1 | grep MIME | cut -f3 >$F1', |
| 1218 0, ('$F1', 'test_mp3.mime')), |
| 1219 ('upload gif/noCT', |
| 1220 'gsutil -h "Content-Type:" cp test.gif gs://$B1/$O1', 0, None), |
| 1221 ('verify gif/noCT', 'gsutil ls -L gs://$B1/$O1 | grep MIME | cut -f3 >$F1', |
| 1222 0, ('$F1', 'test_gif.mime')), |
| 1223 #('upload foo/noCT', 'gsutil -h "Content-Type:" cp test.foo gs://$B1/$O1', |
| 1224 # 0, None), |
| 1225 #('verify foo/noCT', 'gsutil ls -L gs://$B1/$O1 | grep MIME | cut -f3 >$F1', |
| 1226 # 0, ('$F1', 'test_bin.mime')), |
| 1227 ('upload mp3/-h gif', |
| 1228 'gsutil -h "Content-Type:image/gif" cp test.mp3 gs://$B1/$O1', 0, None), |
| 1229 ('verify mp3/-h gif', |
| 1230 'gsutil ls -L gs://$B1/$O1 | grep MIME | cut -f3 >$F1', |
| 1231 0, ('$F1', 'test_gif.mime')), |
| 1232 ('upload gif/-h gif', |
| 1233 'gsutil -h "Content-Type:image/gif" cp test.gif gs://$B1/$O1', 0, None), |
| 1234 ('verify gif/-h gif', |
| 1235 'gsutil ls -L gs://$B1/$O1 | grep MIME | cut -f3 >$F1', |
| 1236 0, ('$F1', 'test_gif.mime')), |
| 1237 ('upload foo/-h gif', |
| 1238 'gsutil -h "Content-Type: image/gif" cp test.foo gs://$B1/$O1', 0, None), |
| 1239 ('verify foo/-h gif', |
| 1240 'gsutil ls -L gs://$B1/$O1 | grep MIME | cut -f3 >$F1', |
| 1241 0, ('$F1', 'test_gif.mime')), |
| 1242 ('remove test files', |
| 1243 'rm -f test.mp3 test_mp3.mime test.gif test_gif.mime test.foo', |
| 1244 0, None), |
| 1245 ] |
| 1246 |
| 1247 def _ParseArgs(self): |
| 1248 self.mv_naming_semantics = False |
| 1249 self.exclude_symlinks = False |
| 1250 # self.recursion_requested initialized in command.py (so can be checked |
| 1251 # in parent class for all commands). |
| 1252 if self.sub_opts: |
| 1253 for o, unused_a in self.sub_opts: |
| 1254 if o == '-e': |
| 1255 self.exclude_symlinks = True |
| 1256 if o == '-M': |
| 1257 # Note that we signal to the cp command to use the alternate naming |
| 1258 # semantics by passing the undocumented (for internal use) -m option |
| 1259 # when running the cp command from mv.py. These semantics only apply |
| 1260 # for mv -R applied to bucket subdirs. |
| 1261 self.mv_naming_semantics = True |
| 1262 elif o == '-r' or o == '-R': |
| 1263 self.recursion_requested = True |
| 1264 |
| 1265 def _SanityCheckRequest(self, src_uri_expansion, exp_dst_uri, |
| 1266 have_existing_dst_container): |
| 1267 if src_uri_expansion.IsEmpty(): |
| 1268 raise CommandException('No URIs matched') |
| 1269 for src_uri in src_uri_expansion.GetSrcUris(): |
| 1270 if src_uri.names_provider(): |
| 1271 raise CommandException('Provider-only src_uri (%s)') |
| 1272 if src_uri_expansion.IsMultiSrcRequest(): |
| 1273 self._InsistDstUriNamesContainer(exp_dst_uri, have_existing_dst_container, |
| 1274 self.command_name) |
| 1275 if (exp_dst_uri.is_file_uri() |
| 1276 and not os.path.exists(exp_dst_uri.object_name)): |
| 1277 os.makedirs(exp_dst_uri.object_name) |
| 1278 |
| 1279 def _HandleStreamingDownload(self): |
| 1280 # Destination is <STDOUT>. Manipulate sys.stdout so as to redirect all |
| 1281 # debug messages to <STDERR>. |
| 1282 stdout_fp = sys.stdout |
| 1283 sys.stdout = sys.stderr |
| 1284 did_some_work = False |
| 1285 for uri_str in self.args[0:len(self.args)-1]: |
| 1286 for uri in self.exp_handler.WildcardIterator(uri_str).IterUris(): |
| 1287 if not uri.names_object(): |
| 1288 raise CommandException('Destination Stream requires that ' |
| 1289 'source URI %s should represent an object!') |
| 1290 did_some_work = True |
| 1291 key = uri.get_key(False, self.headers) |
| 1292 (elapsed_time, bytes_transferred) = self._PerformDownloadToStream( |
| 1293 key, uri, stdout_fp, self.headers) |
| 1294 self.total_elapsed_time += elapsed_time |
| 1295 self.total_bytes_transferred += bytes_transferred |
| 1296 if not did_some_work: |
| 1297 raise CommandException('No URIs matched') |
| 1298 if self.debug == 3: |
| 1299 if self.total_bytes_transferred != 0: |
| 1300 sys.stderr.write( |
| 1301 'Total bytes copied=%d, total elapsed time=%5.3f secs (%sps)\n' % |
| 1302 (self.total_bytes_transferred, self.total_elapsed_time, |
| 1303 MakeHumanReadable(float(self.total_bytes_transferred) / |
| 1304 float(self.total_elapsed_time)))) |
| 1305 |
| 1306 def _SrcDstSame(self, src_uri, dst_uri): |
| 1307 """Checks if src_uri and dst_uri represent the same object or file. |
| 1308 |
| 1309 We don't handle anything about hard or symbolic links. |
| 1310 |
| 1311 Args: |
| 1312 src_uri: Source StorageUri. |
| 1313 dst_uri: Destination StorageUri. |
| 1314 |
| 1315 Returns: |
| 1316 Bool indicator. |
| 1317 """ |
| 1318 if src_uri.is_file_uri() and dst_uri.is_file_uri(): |
| 1319 # Translate a/b/./c to a/b/c, so src=dst comparison below works. |
| 1320 new_src_path = re.sub('%s+\.%s+' % (os.sep, os.sep), os.sep, |
| 1321 src_uri.object_name) |
| 1322 new_src_path = re.sub('^.%s+' % os.sep, '', new_src_path) |
| 1323 new_dst_path = re.sub('%s+\.%s+' % (os.sep, os.sep), os.sep, |
| 1324 dst_uri.object_name) |
| 1325 new_dst_path = re.sub('^.%s+' % os.sep, '', new_dst_path) |
| 1326 return (src_uri.clone_replace_name(new_src_path).uri == |
| 1327 dst_uri.clone_replace_name(new_dst_path).uri) |
| 1328 else: |
| 1329 # TODO: There are cases where copying from src to dst with the same |
| 1330 # object makes sense, namely, for setting metadata on an object. At some |
| 1331 # point if we offer a command to do so, add a parameter to the current |
| 1332 # function to allow this check to be overridden. Note that we want this |
| 1333 # check to prevent a user from blowing away data using the mv command, |
| 1334 # with a command like: |
| 1335 # gsutil mv -R gs://bucket/abc/* gs://bucket/abc |
| 1336 return src_uri.uri == dst_uri.uri |
| 1337 |
| 1338 def _ShouldTreatDstUriAsBucketSubDir(self, have_multiple_srcs, dst_uri): |
| 1339 """ |
| 1340 Checks whether dst_uri should be treated as a bucket "sub-directory". The |
| 1341 decision about whether something constitutes a bucket "sub-directory" |
| 1342 depends on whether there are multiple sources in this request. For |
| 1343 example, when running the command: |
| 1344 gsutil cp file gs://bucket/abc |
| 1345 gs://bucket/abc names an object; in contrast, when running the command: |
| 1346 gsutil cp file1 file2 gs://bucket/abc |
| 1347 gs://bucket/abc names a bucket "sub-directory". |
| 1348 |
| 1349 Note that we don't disallow naming a bucket "sub-directory" where there's |
| 1350 already an object at that URI. For example it's legitimate (albeit |
| 1351 confusing) to have an object called gs://bucket/dir and |
| 1352 then run the command |
| 1353 gsutil cp file1 file2 gs://bucket/dir |
| 1354 Doing so will end up with objects gs://bucket/dir, gs://bucket/dir/file1, |
| 1355 and gs://bucket/dir/file2. |
| 1356 |
| 1357 Args: |
| 1358 have_multiple_srcs: Bool indicator of whether this is a multi-source |
| 1359 operation. |
| 1360 dst_uri: StorageUri to check. |
| 1361 |
| 1362 Returns: |
| 1363 bool indicator. |
| 1364 """ |
| 1365 return (self.recursion_requested and have_multiple_srcs |
| 1366 and dst_uri.is_cloud_uri()) |
| 1367 |
| 1368 def _ShouldTreatDstUriAsSingleton(self, have_multiple_srcs, |
| 1369 have_existing_dest_subdir, dst_uri): |
| 1370 """ |
| 1371 Checks that dst_uri names a singleton (file or object) after |
| 1372 dir/wildcard expansion. The decision is more nuanced than simply |
| 1373 dst_uri.names_singleton()) because of the possibility that an object path |
| 1374 might name a bucket sub-directory. |
| 1375 |
| 1376 Args: |
| 1377 have_multiple_srcs: Bool indicator of whether this is a multi-source |
| 1378 operation. |
| 1379 have_existing_dest_subdir: bool indicator whether dest is an existing |
| 1380 subdirectory. |
| 1381 dst_uri: StorageUri to check. |
| 1382 |
| 1383 Returns: |
| 1384 bool indicator. |
| 1385 """ |
| 1386 if have_multiple_srcs: |
| 1387 # Only a file meets the criteria in this case. |
| 1388 return dst_uri.names_file() |
| 1389 return not have_existing_dest_subdir and dst_uri.names_singleton() |
| 1390 |
| 1391 |
| 1392 def _GetPathBeforeFinalDir(uri): |
| 1393 """ |
| 1394 Returns the part of the path before the final directory component for the |
| 1395 given URI, handling cases for file system directories, bucket, and bucket |
| 1396 subdirectories. Example: for gs://bucket/dir/ we'll return 'gs://bucket'. |
| 1397 |
| 1398 Args: |
| 1399 uri: StorageUri. |
| 1400 |
| 1401 Returns: |
| 1402 String name of above-described path, sans final path separator. |
| 1403 """ |
| 1404 sep = uri.delim |
| 1405 assert not uri.names_file() |
| 1406 if uri.names_directory(): |
| 1407 return uri.uri.rstrip(sep).rpartition(sep)[0] |
| 1408 if uri.names_bucket(): |
| 1409 return '%s://' % uri.scheme |
| 1410 # Else it names a bucket subdir. |
| 1411 return uri.uri.rstrip(sep).rpartition(sep)[0] |
| 1412 |
| 1413 def _hash_filename(filename): |
| 1414 """ |
| 1415 Apply a hash function (SHA1) to shorten the passed file name. In order |
| 1416 to minimize the risk of collisions, we include the epoch time (with |
| 1417 microsecond graularity). The complete spec for the hashed file name is |
| 1418 as follows: |
| 1419 |
| 1420 TRACKER_<hash>_<timestamp>_<trailing> |
| 1421 |
| 1422 where hash is a SHA1 hash on the original file name, timestamp is a |
| 1423 microsecond granularity current time stamp and trailing is the last |
| 1424 16 chars from the original file name. Max file name lengths vary by |
| 1425 operating system so the goal of this function is to ensure the hashed |
| 1426 version takes less than 100 characters. |
| 1427 |
| 1428 Args: |
| 1429 filename: file name to be hashed. |
| 1430 |
| 1431 Returns: |
| 1432 shorter, hashed version of passed file name |
| 1433 """ |
| 1434 m = hashlib.sha1(filename) |
| 1435 hashed_name = ("TRACKER_" + m.hexdigest() + ('.%.6f' % time.time()) + |
| 1436 '.' + filename[-16:]) |
| 1437 return hashed_name |
| OLD | NEW |