Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 #!/usr/bin/python | 1 #!/usr/bin/python |
| 2 | 2 |
| 3 # pylint: disable=C0301 | 3 # pylint: disable=C0301 |
| 4 """ | 4 """ |
| 5 Copyright 2014 Google Inc. | 5 Copyright 2014 Google Inc. |
| 6 | 6 |
| 7 Use of this source code is governed by a BSD-style license that can be | 7 Use of this source code is governed by a BSD-style license that can be |
| 8 found in the LICENSE file. | 8 found in the LICENSE file. |
| 9 | 9 |
| 10 Utilities for accessing Google Cloud Storage, using the boto library (wrapper | 10 Utilities for accessing Google Cloud Storage, using the boto library (wrapper |
| (...skipping 18 matching lines...) Expand all Loading... | |
| 29 # Imports from third-party code | 29 # Imports from third-party code |
| 30 TRUNK_DIRECTORY = os.path.abspath(os.path.join( | 30 TRUNK_DIRECTORY = os.path.abspath(os.path.join( |
| 31 os.path.dirname(__file__), os.pardir, os.pardir)) | 31 os.path.dirname(__file__), os.pardir, os.pardir)) |
| 32 for import_subdir in ['boto']: | 32 for import_subdir in ['boto']: |
| 33 import_dirpath = os.path.join( | 33 import_dirpath = os.path.join( |
| 34 TRUNK_DIRECTORY, 'third_party', 'externals', import_subdir) | 34 TRUNK_DIRECTORY, 'third_party', 'externals', import_subdir) |
| 35 if import_dirpath not in sys.path: | 35 if import_dirpath not in sys.path: |
| 36 # We need to insert at the beginning of the path, to make sure that our | 36 # We need to insert at the beginning of the path, to make sure that our |
| 37 # imported versions are favored over others that might be in the path. | 37 # imported versions are favored over others that might be in the path. |
| 38 sys.path.insert(0, import_dirpath) | 38 sys.path.insert(0, import_dirpath) |
| 39 from boto.exception import BotoServerError | |
| 39 from boto.gs import acl | 40 from boto.gs import acl |
| 40 from boto.gs.bucket import Bucket | 41 from boto.gs.bucket import Bucket |
| 41 from boto.gs.connection import GSConnection | 42 from boto.gs.connection import GSConnection |
| 42 from boto.gs.key import Key | 43 from boto.gs.key import Key |
| 43 from boto.s3.bucketlistresultset import BucketListResultSet | 44 from boto.s3.bucketlistresultset import BucketListResultSet |
| 44 from boto.s3.connection import SubdomainCallingFormat | 45 from boto.s3.connection import SubdomainCallingFormat |
| 45 from boto.s3.prefix import Prefix | 46 from boto.s3.prefix import Prefix |
| 46 | 47 |
| 47 # Permissions that may be set on each file in Google Storage. | 48 # "Canned" ACLs that provide a "base coat" of permissions for each file in |
| 48 # See SupportedPermissions in | 49 # Google Storage. See CannedACLStrings in |
| 50 # https://github.com/boto/boto/blob/develop/boto/gs/acl.py | |
| 51 CANNED_ACL_AUTHENTICATED_READ = 'authenticated-read' | |
| 52 CANNED_ACL_BUCKET_OWNER_FULL_CONTROL = 'bucket-owner-full-control' | |
| 53 CANNED_ACL_BUCKET_OWNER_READ = 'bucket-owner-read' | |
| 54 CANNED_ACL_PRIVATE = 'private' | |
| 55 CANNED_ACL_PROJECT_PRIVATE = 'project-private' | |
| 56 CANNED_ACL_PUBLIC_READ = 'public-read' | |
| 57 CANNED_ACL_PUBLIC_READ_WRITE = 'public-read-write' | |
|
rmistry
2014/07/18 16:55:07
Nit:
Change the above to only have one space befor
epoger
2014/07/18 17:36:19
Aligned 'em all.
| |
| 58 | |
| 59 # "Fine-grained" permissions that may be set per user/group on each file in | |
| 60 # Google Storage. See SupportedPermissions in | |
| 49 # https://github.com/boto/boto/blob/develop/boto/gs/acl.py | 61 # https://github.com/boto/boto/blob/develop/boto/gs/acl.py |
| 50 PERMISSION_NONE = None | 62 PERMISSION_NONE = None |
| 51 PERMISSION_OWNER = 'FULL_CONTROL' | 63 PERMISSION_OWNER = 'FULL_CONTROL' |
| 52 PERMISSION_READ = 'READ' | 64 PERMISSION_READ = 'READ' |
| 53 PERMISSION_WRITE = 'WRITE' | 65 PERMISSION_WRITE = 'WRITE' |
| 54 | 66 |
| 55 # Types of identifiers we can use to set ACLs. | 67 # Types of identifiers we can use to set "fine-grained" ACLs. |
| 56 ID_TYPE_GROUP_BY_DOMAIN = acl.GROUP_BY_DOMAIN | 68 ID_TYPE_GROUP_BY_DOMAIN = acl.GROUP_BY_DOMAIN |
| 57 ID_TYPE_GROUP_BY_EMAIL = acl.GROUP_BY_EMAIL | 69 ID_TYPE_GROUP_BY_EMAIL = acl.GROUP_BY_EMAIL |
| 58 ID_TYPE_GROUP_BY_ID = acl.GROUP_BY_ID | 70 ID_TYPE_GROUP_BY_ID = acl.GROUP_BY_ID |
| 59 ID_TYPE_USER_BY_EMAIL = acl.USER_BY_EMAIL | 71 ID_TYPE_USER_BY_EMAIL = acl.USER_BY_EMAIL |
| 60 ID_TYPE_USER_BY_ID = acl.USER_BY_ID | 72 ID_TYPE_USER_BY_ID = acl.USER_BY_ID |
| 61 | 73 |
| 62 # Which field we get/set in ACL entries, depending on ID_TYPE. | 74 # Which field we get/set in ACL entries, depending on ID_TYPE. |
| 63 FIELD_BY_ID_TYPE = { | 75 FIELD_BY_ID_TYPE = { |
| 64 ID_TYPE_GROUP_BY_DOMAIN: 'domain', | 76 ID_TYPE_GROUP_BY_DOMAIN: 'domain', |
| 65 ID_TYPE_GROUP_BY_EMAIL: 'email_address', | 77 ID_TYPE_GROUP_BY_EMAIL: 'email_address', |
| (...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 113 def delete_file(self, bucket, path): | 125 def delete_file(self, bucket, path): |
| 114 """Delete a single file within a GS bucket. | 126 """Delete a single file within a GS bucket. |
| 115 | 127 |
| 116 TODO(epoger): what if bucket or path does not exist? Should probably raise | 128 TODO(epoger): what if bucket or path does not exist? Should probably raise |
| 117 an exception. Implement, and add a test to exercise this. | 129 an exception. Implement, and add a test to exercise this. |
| 118 | 130 |
| 119 Params: | 131 Params: |
| 120 bucket: GS bucket to delete a file from | 132 bucket: GS bucket to delete a file from |
| 121 path: full path (Posix-style) of the file within the bucket to delete | 133 path: full path (Posix-style) of the file within the bucket to delete |
| 122 """ | 134 """ |
| 123 conn = self._create_connection() | 135 b = self._connect_to_bucket(bucket_name=bucket) |
| 124 b = conn.get_bucket(bucket_name=bucket) | |
| 125 item = Key(b) | 136 item = Key(b) |
| 126 item.key = path | 137 item.key = path |
| 127 item.delete() | 138 try: |
| 139 item.delete() | |
| 140 except BotoServerError, e: | |
|
epoger
2014/07/18 15:48:19
While I was in here, improved various error messag
| |
| 141 e.body = (repr(e.body) + | |
| 142 ' while deleting bucket=%s, path=%s' % (bucket, path)) | |
| 143 raise | |
| 128 | 144 |
| 129 def upload_file(self, source_path, dest_bucket, dest_path): | 145 def upload_file(self, source_path, dest_bucket, dest_path, |
| 146 canned_acl=CANNED_ACL_PRIVATE, fine_grained_acl_list=None): | |
|
epoger
2014/07/18 15:48:19
I didn't add the http_header_lines param we had in
| |
| 130 """Upload contents of a local file to Google Storage. | 147 """Upload contents of a local file to Google Storage. |
| 131 | 148 |
| 132 TODO(epoger): Add the extra parameters provided by upload_file() within | 149 TODO(epoger): Add the only_if_modified param provided by upload_file() in |
| 133 https://github.com/google/skia-buildbot/blob/master/slave/skia_slave_scripts /utils/old_gs_utils.py , | 150 https://github.com/google/skia-buildbot/blob/master/slave/skia_slave_scripts /utils/old_gs_utils.py , |
| 134 so we can replace that function with this one. | 151 so we can replace that function with this one. |
| 135 | 152 |
| 136 params: | 153 params: |
| 137 source_path: full path (local-OS-style) on local disk to read from | 154 source_path: full path (local-OS-style) on local disk to read from |
| 138 dest_bucket: GCS bucket to copy the file to | 155 dest_bucket: GCS bucket to copy the file to |
| 139 dest_path: full path (Posix-style) within that bucket | 156 dest_path: full path (Posix-style) within that bucket |
| 157 canned_acl: which predefined ACL to apply to the file on Google Storage; | |
| 158 must be one of the CANNED_ACL_* constants defined above. | |
| 159 TODO(epoger): add unittests for this param, although it seems to work | |
| 160 in my manual testing | |
|
rmistry
2014/07/18 16:55:07
This should be allowed to be None because if all y
epoger
2014/07/18 17:36:19
I agree, sort of. Searching for "predefined" in h
| |
| 161 fine_grained_acl_list: list of (id_type, id_value, permission) tuples | |
| 162 to apply to the uploaded file, or None if canned_acl is sufficient | |
| 140 """ | 163 """ |
| 141 conn = self._create_connection() | 164 b = self._connect_to_bucket(bucket_name=dest_bucket) |
| 142 b = conn.get_bucket(bucket_name=dest_bucket) | |
| 143 item = Key(b) | 165 item = Key(b) |
| 144 item.key = dest_path | 166 item.key = dest_path |
| 145 item.set_contents_from_filename(filename=source_path) | 167 try: |
| 168 item.set_contents_from_filename(filename=source_path, policy=canned_acl) | |
| 169 except BotoServerError, e: | |
| 170 e.body = (repr(e.body) + | |
| 171 ' while uploading source_path=%s to bucket=%s, path=%s' % ( | |
| 172 source_path, dest_bucket, item.key)) | |
| 173 raise | |
| 174 # TODO(epoger): This may be inefficient, because it calls | |
| 175 # _connect_to_bucket() again. Depending on how expensive that | |
| 176 # call is, we may want to optimize this. | |
| 177 for (id_type, id_value, permission) in fine_grained_acl_list or []: | |
| 178 self.set_acl( | |
| 179 bucket=dest_bucket, path=item.key, | |
| 180 id_type=id_type, id_value=id_value, permission=permission) | |
| 181 | |
| 182 def upload_dir_contents(self, source_dir, dest_bucket, dest_dir, | |
| 183 canned_acl=CANNED_ACL_PRIVATE, | |
| 184 fine_grained_acl_list=None): | |
| 185 """Recursively upload contents of a local directory to Google Storage. | |
| 186 | |
| 187 params: | |
| 188 source_dir: full path (local-OS-style) on local disk of directory to copy | |
| 189 contents of | |
| 190 dest_bucket: GCS bucket to copy the files into | |
| 191 dest_dir: full path (Posix-style) within that bucket; write the files into | |
| 192 this directory | |
| 193 canned_acl: which predefined ACL to apply to the files on Google Storage; | |
| 194 must be one of the CANNED_ACL_* constants defined above. | |
| 195 TODO(epoger): add unittests for this param, although it seems to work | |
| 196 in my manual testing | |
| 197 fine_grained_acl_list: list of (id_type, id_value, permission) tuples | |
| 198 to apply to every file uploaded, or None if canned_acl is sufficient | |
| 199 TODO(epoger): add unittests for this param, although it seems to work | |
| 200 in my manual testing | |
| 201 | |
| 202 The copy operates as a "merge with overwrite": any files in source_dir will | |
| 203 be "overlaid" on top of the existing content in dest_dir. Existing files | |
| 204 with the same names will be overwritten. | |
| 205 | |
| 206 TODO(epoger): Upload multiple files simultaneously to reduce latency. | |
|
epoger
2014/07/18 15:48:18
All these TODOs copied in from https://skia.google
| |
| 207 | |
| 208 TODO(epoger): Add a "noclobber" mode that will not upload any files would | |
| 209 overwrite existing files in Google Storage. | |
| 210 | |
| 211 TODO(epoger): Consider adding a do_compress parameter that would compress | |
| 212 the file using gzip before upload, and add a "Content-Encoding:gzip" header | |
| 213 so that HTTP downloads of the file would be unzipped automatically. | |
| 214 See https://developers.google.com/storage/docs/gsutil/addlhelp/ | |
| 215 WorkingWithObjectMetadata#content-encoding | |
| 216 """ | |
| 217 b = self._connect_to_bucket(bucket_name=dest_bucket) | |
| 218 for filename in sorted(os.listdir(source_dir)): | |
| 219 local_path = os.path.join(source_dir, filename) | |
| 220 if os.path.isdir(local_path): | |
| 221 self.upload_dir_contents( # recurse | |
| 222 source_dir=local_path, dest_bucket=dest_bucket, | |
| 223 dest_dir=posixpath.join(dest_dir, filename), | |
| 224 canned_acl=canned_acl) | |
| 225 else: | |
| 226 item = Key(b) | |
| 227 item.key = posixpath.join(dest_dir, filename) | |
| 228 try: | |
| 229 item.set_contents_from_filename( | |
| 230 filename=local_path, policy=canned_acl) | |
| 231 except BotoServerError, e: | |
| 232 e.body = (repr(e.body) + | |
| 233 ' while uploading local_path=%s to bucket=%s, path=%s' % ( | |
| 234 local_path, dest_bucket, item.key)) | |
| 235 raise | |
| 236 # TODO(epoger): This may be inefficient, because it calls | |
| 237 # _connect_to_bucket() for every file. Depending on how expensive that | |
| 238 # call is, we may want to optimize this. | |
| 239 for (id_type, id_value, permission) in fine_grained_acl_list or []: | |
| 240 self.set_acl( | |
| 241 bucket=dest_bucket, path=item.key, | |
| 242 id_type=id_type, id_value=id_value, permission=permission) | |
| 146 | 243 |
| 147 def download_file(self, source_bucket, source_path, dest_path, | 244 def download_file(self, source_bucket, source_path, dest_path, |
| 148 create_subdirs_if_needed=False): | 245 create_subdirs_if_needed=False): |
| 149 """Downloads a single file from Google Cloud Storage to local disk. | 246 """Downloads a single file from Google Cloud Storage to local disk. |
| 150 | 247 |
| 151 Args: | 248 Args: |
| 152 source_bucket: GCS bucket to download the file from | 249 source_bucket: GCS bucket to download the file from |
| 153 source_path: full path (Posix-style) within that bucket | 250 source_path: full path (Posix-style) within that bucket |
| 154 dest_path: full path (local-OS-style) on local disk to copy the file to | 251 dest_path: full path (local-OS-style) on local disk to copy the file to |
| 155 create_subdirs_if_needed: boolean; whether to create subdirectories as | 252 create_subdirs_if_needed: boolean; whether to create subdirectories as |
| 156 needed to create dest_path | 253 needed to create dest_path |
| 157 """ | 254 """ |
| 158 conn = self._create_connection() | 255 b = self._connect_to_bucket(bucket_name=source_bucket) |
| 159 b = conn.get_bucket(bucket_name=source_bucket) | |
| 160 item = Key(b) | 256 item = Key(b) |
| 161 item.key = source_path | 257 item.key = source_path |
| 162 if create_subdirs_if_needed: | 258 if create_subdirs_if_needed: |
| 163 _makedirs_if_needed(os.path.dirname(dest_path)) | 259 _makedirs_if_needed(os.path.dirname(dest_path)) |
| 164 with open(dest_path, 'w') as f: | 260 with open(dest_path, 'w') as f: |
| 165 item.get_contents_to_file(fp=f) | 261 try: |
| 262 item.get_contents_to_file(fp=f) | |
| 263 except BotoServerError, e: | |
| 264 e.body = (repr(e.body) + | |
| 265 ' while downloading bucket=%s, path=%s to local_path=%s' % ( | |
| 266 source_bucket, source_path, dest_path)) | |
| 267 raise | |
| 268 | |
| 269 def download_dir_contents(self, source_bucket, source_dir, dest_dir): | |
| 270 """Recursively download contents of a Google Storage directory to local disk | |
| 271 | |
| 272 params: | |
| 273 source_bucket: GCS bucket to copy the files from | |
| 274 source_dir: full path (Posix-style) within that bucket; read the files | |
| 275 from this directory | |
| 276 dest_dir: full path (local-OS-style) on local disk of directory to copy | |
| 277 the files into | |
| 278 | |
| 279 The copy operates as a "merge with overwrite": any files in source_dir will | |
| 280 be "overlaid" on top of the existing content in dest_dir. Existing files | |
| 281 with the same names will be overwritten. | |
| 282 | |
| 283 TODO(epoger): Download multiple files simultaneously to reduce latency. | |
| 284 """ | |
| 285 _makedirs_if_needed(dest_dir) | |
| 286 b = self._connect_to_bucket(bucket_name=source_bucket) | |
| 287 (dirs, files) = self.list_bucket_contents( | |
| 288 bucket=source_bucket, subdir=source_dir) | |
| 289 | |
| 290 for filename in files: | |
| 291 item = Key(b) | |
| 292 item.key = posixpath.join(source_dir, filename) | |
| 293 dest_path = os.path.join(dest_dir, filename) | |
| 294 with open(dest_path, 'w') as f: | |
| 295 try: | |
| 296 item.get_contents_to_file(fp=f) | |
| 297 except BotoServerError, e: | |
| 298 e.body = (repr(e.body) + | |
| 299 ' while downloading bucket=%s, path=%s to local_path=%s' % ( | |
| 300 source_bucket, item.key, dest_path)) | |
| 301 raise | |
| 302 | |
| 303 for dirname in dirs: | |
| 304 self.download_dir_contents( # recurse | |
| 305 source_bucket=source_bucket, | |
| 306 source_dir=posixpath.join(source_dir, dirname), | |
| 307 dest_dir=os.path.join(dest_dir, dirname)) | |
| 166 | 308 |
| 167 def get_acl(self, bucket, path, id_type, id_value): | 309 def get_acl(self, bucket, path, id_type, id_value): |
| 168 """Retrieve partial access permissions on a single file in Google Storage. | 310 """Retrieve partial access permissions on a single file in Google Storage. |
| 169 | 311 |
| 170 Various users who match this id_type/id_value pair may have access rights | 312 Various users who match this id_type/id_value pair may have access rights |
| 171 other than that returned by this call, if they have been granted those | 313 other than that returned by this call, if they have been granted those |
| 172 rights based on *other* id_types (e.g., perhaps they have group access | 314 rights based on *other* id_types (e.g., perhaps they have group access |
| 173 rights, beyond their individual access rights). | 315 rights, beyond their individual access rights). |
| 174 | 316 |
| 317 TODO(epoger): What if the remote file does not exist? This should probably | |
| 318 raise an exception in that case. | |
| 319 | |
| 175 Params: | 320 Params: |
| 176 bucket: GS bucket | 321 bucket: GS bucket |
| 177 path: full path (Posix-style) to the file within that bucket | 322 path: full path (Posix-style) to the file within that bucket |
| 178 id_type: must be one of the ID_TYPE_* constants defined above | 323 id_type: must be one of the ID_TYPE_* constants defined above |
| 179 id_value: get permissions for users whose id_type field contains this | 324 id_value: get permissions for users whose id_type field contains this |
| 180 value | 325 value |
| 181 | 326 |
| 182 Returns: the PERMISSION_* constant which has been set for users matching | 327 Returns: the PERMISSION_* constant which has been set for users matching |
| 183 this id_type/id_value, on this file; or PERMISSION_NONE if no such | 328 this id_type/id_value, on this file; or PERMISSION_NONE if no such |
| 184 permissions have been set. | 329 permissions have been set. |
| 185 """ | 330 """ |
| 186 field = FIELD_BY_ID_TYPE[id_type] | 331 field = FIELD_BY_ID_TYPE[id_type] |
| 187 conn = self._create_connection() | 332 b = self._connect_to_bucket(bucket_name=bucket) |
| 188 b = conn.get_bucket(bucket_name=bucket) | |
| 189 acls = b.get_acl(key_name=path) | 333 acls = b.get_acl(key_name=path) |
| 190 matching_entries = [entry for entry in acls.entries.entry_list | 334 matching_entries = [entry for entry in acls.entries.entry_list |
| 191 if (entry.scope.type == id_type) and | 335 if (entry.scope.type == id_type) and |
| 192 (getattr(entry.scope, field) == id_value)] | 336 (getattr(entry.scope, field) == id_value)] |
| 193 if matching_entries: | 337 if matching_entries: |
| 194 assert len(matching_entries) == 1, '%d == 1' % len(matching_entries) | 338 assert len(matching_entries) == 1, '%d == 1' % len(matching_entries) |
| 195 return matching_entries[0].permission | 339 return matching_entries[0].permission |
| 196 else: | 340 else: |
| 197 return PERMISSION_NONE | 341 return PERMISSION_NONE |
| 198 | 342 |
| 199 def set_acl(self, bucket, path, id_type, id_value, permission): | 343 def set_acl(self, bucket, path, id_type, id_value, permission): |
| 200 """Set partial access permissions on a single file in Google Storage. | 344 """Set partial access permissions on a single file in Google Storage. |
| 201 | 345 |
| 202 Note that a single set_acl() call will not guarantee what access rights any | 346 Note that a single set_acl() call will not guarantee what access rights any |
| 203 given user will have on a given file, because permissions are additive. | 347 given user will have on a given file, because permissions are additive. |
| 204 (E.g., if you set READ permission for a group, but a member of that group | 348 (E.g., if you set READ permission for a group, but a member of that group |
| 205 already has WRITE permission, that member will still have WRITE permission.) | 349 already has WRITE permission, that member will still have WRITE permission.) |
| 206 TODO(epoger): Do we know that for sure? I *think* that's how it works... | 350 TODO(epoger): Do we know that for sure? I *think* that's how it works... |
| 207 | 351 |
| 208 If there is already a permission set on this file for this id_type/id_value | 352 If there is already a permission set on this file for this id_type/id_value |
| 209 combination, this call will overwrite it. | 353 combination, this call will overwrite it. |
| 210 | 354 |
| 355 TODO(epoger): What if the remote file does not exist? This should probably | |
| 356 raise an exception in that case. | |
| 357 | |
| 211 Params: | 358 Params: |
| 212 bucket: GS bucket | 359 bucket: GS bucket |
| 213 path: full path (Posix-style) to the file within that bucket | 360 path: full path (Posix-style) to the file within that bucket |
| 214 id_type: must be one of the ID_TYPE_* constants defined above | 361 id_type: must be one of the ID_TYPE_* constants defined above |
| 215 id_value: add permission for users whose id_type field contains this value | 362 id_value: add permission for users whose id_type field contains this value |
| 216 permission: permission to add for users matching id_type/id_value; | 363 permission: permission to add for users matching id_type/id_value; |
| 217 must be one of the PERMISSION_* constants defined above. | 364 must be one of the PERMISSION_* constants defined above. |
| 218 If PERMISSION_NONE, then any permissions will be granted to this | 365 If PERMISSION_NONE, then any permissions will be granted to this |
| 219 particular id_type/id_value will be removed... but, given that | 366 particular id_type/id_value will be removed... but, given that |
| 220 permissions are additive, specific users may still have access rights | 367 permissions are additive, specific users may still have access rights |
| 221 based on permissions given to *other* id_type/id_value pairs. | 368 based on permissions given to *other* id_type/id_value pairs. |
| 222 | 369 |
| 223 Example Code: | 370 Example Code: |
| 224 bucket = 'gs://bucket-name' | 371 bucket = 'gs://bucket-name' |
| 225 path = 'path/to/file' | 372 path = 'path/to/file' |
| 226 id_type = ID_TYPE_USER_BY_EMAIL | 373 id_type = ID_TYPE_USER_BY_EMAIL |
| 227 id_value = 'epoger@google.com' | 374 id_value = 'epoger@google.com' |
| 228 set_acl(bucket, path, id_type, id_value, PERMISSION_READ) | 375 set_acl(bucket, path, id_type, id_value, PERMISSION_READ) |
| 229 assert PERMISSION_READ == get_acl(bucket, path, id_type, id_value) | 376 assert PERMISSION_READ == get_acl(bucket, path, id_type, id_value) |
| 230 set_acl(bucket, path, id_type, id_value, PERMISSION_WRITE) | 377 set_acl(bucket, path, id_type, id_value, PERMISSION_WRITE) |
| 231 assert PERMISSION_WRITE == get_acl(bucket, path, id_type, id_value) | 378 assert PERMISSION_WRITE == get_acl(bucket, path, id_type, id_value) |
| 232 """ | 379 """ |
| 233 field = FIELD_BY_ID_TYPE[id_type] | 380 field = FIELD_BY_ID_TYPE[id_type] |
| 234 conn = self._create_connection() | 381 b = self._connect_to_bucket(bucket_name=bucket) |
| 235 b = conn.get_bucket(bucket_name=bucket) | |
| 236 acls = b.get_acl(key_name=path) | 382 acls = b.get_acl(key_name=path) |
| 237 | 383 |
| 238 # Remove any existing entries that refer to the same id_type/id_value, | 384 # Remove any existing entries that refer to the same id_type/id_value, |
| 239 # because the API will fail if we try to set more than one. | 385 # because the API will fail if we try to set more than one. |
| 240 matching_entries = [entry for entry in acls.entries.entry_list | 386 matching_entries = [entry for entry in acls.entries.entry_list |
| 241 if (entry.scope.type == id_type) and | 387 if (entry.scope.type == id_type) and |
| 242 (getattr(entry.scope, field) == id_value)] | 388 (getattr(entry.scope, field) == id_value)] |
| 243 if matching_entries: | 389 if matching_entries: |
| 244 assert len(matching_entries) == 1, '%d == 1' % len(matching_entries) | 390 assert len(matching_entries) == 1, '%d == 1' % len(matching_entries) |
| 245 acls.entries.entry_list.remove(matching_entries[0]) | 391 acls.entries.entry_list.remove(matching_entries[0]) |
| 246 | 392 |
| 247 # Add a new entry to the ACLs. | 393 # Add a new entry to the ACLs. |
| 248 if permission != PERMISSION_NONE: | 394 if permission != PERMISSION_NONE: |
| 249 args = {'type': id_type, 'permission': permission} | 395 args = {'type': id_type, 'permission': permission} |
| 250 args[field] = id_value | 396 args[field] = id_value |
| 251 new_entry = acl.Entry(**args) | 397 new_entry = acl.Entry(**args) |
| 252 acls.entries.entry_list.append(new_entry) | 398 acls.entries.entry_list.append(new_entry) |
| 253 | 399 |
| 254 # Finally, write back the modified ACLs. | 400 # Finally, write back the modified ACLs. |
| 255 b.set_acl(acl_or_str=acls, key_name=path) | 401 b.set_acl(acl_or_str=acls, key_name=path) |
| 256 | 402 |
| 257 def list_bucket_contents(self, bucket, subdir=None): | 403 def list_bucket_contents(self, bucket, subdir=None): |
| 258 """Returns files in the Google Storage bucket as a (dirs, files) tuple. | 404 """Returns files in the Google Storage bucket as a (dirs, files) tuple. |
| 259 | 405 |
| 406 TODO(epoger): This should raise an exception if subdir does not exist in | |
| 407 Google Storage; right now, it just returns empty contents. | |
| 408 | |
| 260 Args: | 409 Args: |
| 261 bucket: name of the Google Storage bucket | 410 bucket: name of the Google Storage bucket |
| 262 subdir: directory within the bucket to list, or None for root directory | 411 subdir: directory within the bucket to list, or None for root directory |
| 263 """ | 412 """ |
| 264 # The GS command relies on the prefix (if any) ending with a slash. | 413 # The GS command relies on the prefix (if any) ending with a slash. |
| 265 prefix = subdir or '' | 414 prefix = subdir or '' |
| 266 if prefix and not prefix.endswith('/'): | 415 if prefix and not prefix.endswith('/'): |
| 267 prefix += '/' | 416 prefix += '/' |
| 268 prefix_length = len(prefix) if prefix else 0 | 417 prefix_length = len(prefix) if prefix else 0 |
| 269 | 418 |
| 270 conn = self._create_connection() | 419 b = self._connect_to_bucket(bucket_name=bucket) |
| 271 b = conn.get_bucket(bucket_name=bucket) | |
| 272 lister = BucketListResultSet(bucket=b, prefix=prefix, delimiter='/') | 420 lister = BucketListResultSet(bucket=b, prefix=prefix, delimiter='/') |
| 273 dirs = [] | 421 dirs = [] |
| 274 files = [] | 422 files = [] |
| 275 for item in lister: | 423 for item in lister: |
| 276 t = type(item) | 424 t = type(item) |
| 277 if t is Key: | 425 if t is Key: |
| 278 files.append(item.key[prefix_length:]) | 426 files.append(item.key[prefix_length:]) |
| 279 elif t is Prefix: | 427 elif t is Prefix: |
| 280 dirs.append(item.name[prefix_length:-1]) | 428 dirs.append(item.name[prefix_length:-1]) |
| 281 return (dirs, files) | 429 return (dirs, files) |
| 282 | 430 |
| 431 def _connect_to_bucket(self, bucket_name): | |
| 432 """Returns a Bucket object we can use to access a particular bucket in GS. | |
| 433 | |
| 434 Params: | |
| 435 bucket_name: name of the bucket (e.g., 'chromium-skia-gm') | |
| 436 """ | |
| 437 try: | |
| 438 return self._create_connection().get_bucket(bucket_name=bucket_name) | |
| 439 except BotoServerError, e: | |
| 440 e.body = repr(e.body) + ' while connecting to bucket=%s' % bucket_name | |
| 441 raise | |
| 442 | |
| 283 def _create_connection(self): | 443 def _create_connection(self): |
| 284 """Returns a GSConnection object we can use to access Google Storage.""" | 444 """Returns a GSConnection object we can use to access Google Storage.""" |
| 285 if self._gs_access_key_id: | 445 if self._gs_access_key_id: |
| 286 return GSConnection( | 446 return GSConnection( |
| 287 gs_access_key_id=self._gs_access_key_id, | 447 gs_access_key_id=self._gs_access_key_id, |
| 288 gs_secret_access_key=self._gs_secret_access_key) | 448 gs_secret_access_key=self._gs_secret_access_key) |
| 289 else: | 449 else: |
| 290 return AnonymousGSConnection() | 450 return AnonymousGSConnection() |
| 291 | 451 |
| 292 def _config_file_as_dict(filepath): | 452 def _config_file_as_dict(filepath): |
| (...skipping 49 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 342 Do you have a ~/.boto file that provides the credentials needed to read | 502 Do you have a ~/.boto file that provides the credentials needed to read |
| 343 and write gs://chromium-skia-gm ? | 503 and write gs://chromium-skia-gm ? |
| 344 """ | 504 """ |
| 345 raise | 505 raise |
| 346 | 506 |
| 347 bucket = 'chromium-skia-gm' | 507 bucket = 'chromium-skia-gm' |
| 348 remote_dir = 'gs_utils_test/%d' % random.randint(0, sys.maxint) | 508 remote_dir = 'gs_utils_test/%d' % random.randint(0, sys.maxint) |
| 349 subdir = 'subdir' | 509 subdir = 'subdir' |
| 350 filenames_to_upload = ['file1', 'file2'] | 510 filenames_to_upload = ['file1', 'file2'] |
| 351 | 511 |
| 352 # Upload test files to Google Storage. | 512 # Upload test files to Google Storage, checking that their fine-grained |
| 513 # ACLs were set correctly. | |
| 514 id_type = ID_TYPE_GROUP_BY_DOMAIN | |
| 515 id_value = 'chromium.org' | |
| 516 set_permission = PERMISSION_READ | |
| 353 local_src_dir = tempfile.mkdtemp() | 517 local_src_dir = tempfile.mkdtemp() |
| 354 os.mkdir(os.path.join(local_src_dir, subdir)) | 518 os.mkdir(os.path.join(local_src_dir, subdir)) |
| 355 try: | 519 try: |
| 356 for filename in filenames_to_upload: | 520 for filename in filenames_to_upload: |
| 357 with open(os.path.join(local_src_dir, subdir, filename), 'w') as f: | 521 with open(os.path.join(local_src_dir, subdir, filename), 'w') as f: |
| 358 f.write('contents of %s\n' % filename) | 522 f.write('contents of %s\n' % filename) |
| 359 gs.upload_file(source_path=os.path.join(local_src_dir, subdir, filename), | 523 dest_path = posixpath.join(remote_dir, subdir, filename) |
| 360 dest_bucket=bucket, | 524 gs.upload_file( |
| 361 dest_path=posixpath.join(remote_dir, subdir, filename)) | 525 source_path=os.path.join(local_src_dir, subdir, filename), |
| 526 dest_bucket=bucket, dest_path=dest_path, | |
| 527 fine_grained_acl_list=[(id_type, id_value, set_permission)]) | |
| 528 got_permission = gs.get_acl(bucket=bucket, path=dest_path, | |
| 529 id_type=id_type, id_value=id_value) | |
| 530 assert got_permission == set_permission, '%s == %s' % ( | |
| 531 got_permission, set_permission) | |
| 362 finally: | 532 finally: |
| 363 shutil.rmtree(local_src_dir) | 533 shutil.rmtree(local_src_dir) |
| 364 | 534 |
| 365 # Get a list of the files we uploaded to Google Storage. | 535 # Get a list of the files we uploaded to Google Storage. |
| 366 (dirs, files) = gs.list_bucket_contents( | 536 (dirs, files) = gs.list_bucket_contents( |
| 367 bucket=bucket, subdir=remote_dir) | 537 bucket=bucket, subdir=remote_dir) |
| 368 assert dirs == [subdir], '%s == [%s]' % (dirs, subdir) | 538 assert dirs == [subdir], '%s == [%s]' % (dirs, subdir) |
| 369 assert files == [], '%s == []' % files | 539 assert files == [], '%s == []' % files |
| 370 (dirs, files) = gs.list_bucket_contents( | 540 (dirs, files) = gs.list_bucket_contents( |
| 371 bucket=bucket, subdir=posixpath.join(remote_dir, subdir)) | 541 bucket=bucket, subdir=posixpath.join(remote_dir, subdir)) |
| (...skipping 55 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 427 gs.delete_file(bucket=bucket, | 597 gs.delete_file(bucket=bucket, |
| 428 path=posixpath.join(remote_dir, subdir, filename)) | 598 path=posixpath.join(remote_dir, subdir, filename)) |
| 429 | 599 |
| 430 # Confirm that we deleted all the files we uploaded to Google Storage. | 600 # Confirm that we deleted all the files we uploaded to Google Storage. |
| 431 (dirs, files) = gs.list_bucket_contents( | 601 (dirs, files) = gs.list_bucket_contents( |
| 432 bucket=bucket, subdir=posixpath.join(remote_dir, subdir)) | 602 bucket=bucket, subdir=posixpath.join(remote_dir, subdir)) |
| 433 assert dirs == [], '%s == []' % dirs | 603 assert dirs == [], '%s == []' % dirs |
| 434 assert files == [], '%s == []' % files | 604 assert files == [], '%s == []' % files |
| 435 | 605 |
| 436 | 606 |
| 607 def _test_dir_upload_and_download(): | |
| 608 """Test upload_dir_contents() and download_dir_contents().""" | |
| 609 try: | |
| 610 gs = GSUtils(boto_file_path=os.path.expanduser(os.path.join('~','.boto'))) | |
| 611 except: | |
| 612 print """ | |
| 613 Failed to instantiate GSUtils object with default .boto file path. | |
| 614 Do you have a ~/.boto file that provides the credentials needed to read | |
| 615 and write gs://chromium-skia-gm ? | |
| 616 """ | |
| 617 raise | |
| 618 | |
| 619 bucket = 'chromium-skia-gm' | |
| 620 remote_dir = 'gs_utils_test/%d' % random.randint(0, sys.maxint) | |
| 621 subdir = 'subdir' | |
| 622 filenames = ['file1', 'file2'] | |
| 623 | |
| 624 # Create directory tree on local disk, and upload it. | |
| 625 local_src_dir = tempfile.mkdtemp() | |
| 626 os.mkdir(os.path.join(local_src_dir, subdir)) | |
| 627 try: | |
| 628 for filename in filenames: | |
| 629 with open(os.path.join(local_src_dir, subdir, filename), 'w') as f: | |
| 630 f.write('contents of %s\n' % filename) | |
| 631 gs.upload_dir_contents(source_dir=local_src_dir, dest_bucket=bucket, | |
| 632 dest_dir=remote_dir) | |
| 633 finally: | |
| 634 shutil.rmtree(local_src_dir) | |
| 635 | |
| 636 # Validate the list of the files we uploaded to Google Storage. | |
| 637 (dirs, files) = gs.list_bucket_contents( | |
| 638 bucket=bucket, subdir=remote_dir) | |
| 639 assert dirs == [subdir], '%s == [%s]' % (dirs, subdir) | |
| 640 assert files == [], '%s == []' % files | |
| 641 (dirs, files) = gs.list_bucket_contents( | |
| 642 bucket=bucket, subdir=posixpath.join(remote_dir, subdir)) | |
| 643 assert dirs == [], '%s == []' % dirs | |
| 644 assert files == filenames, '%s == %s' % (files, filenames) | |
| 645 | |
| 646 # Download the directory tree we just uploaded, make sure its contents | |
| 647 # are what we expect, and then delete the tree in Google Storage. | |
| 648 local_dest_dir = tempfile.mkdtemp() | |
| 649 try: | |
| 650 gs.download_dir_contents(source_bucket=bucket, source_dir=remote_dir, | |
| 651 dest_dir=local_dest_dir) | |
| 652 for filename in filenames: | |
| 653 with open(os.path.join(local_dest_dir, subdir, filename)) as f: | |
| 654 file_contents = f.read() | |
| 655 assert file_contents == 'contents of %s\n' % filename, ( | |
| 656 '%s == "contents of %s\n"' % (file_contents, filename)) | |
| 657 finally: | |
| 658 shutil.rmtree(local_dest_dir) | |
| 659 for filename in filenames: | |
| 660 gs.delete_file(bucket=bucket, | |
| 661 path=posixpath.join(remote_dir, subdir, filename)) | |
| 662 | |
| 663 | |
| 437 # TODO(epoger): How should we exercise these self-tests? | 664 # TODO(epoger): How should we exercise these self-tests? |
| 438 # See http://skbug.com/2751 | 665 # See http://skbug.com/2751 |
| 439 if __name__ == '__main__': | 666 if __name__ == '__main__': |
| 440 _test_public_read() | 667 _test_public_read() |
| 441 _test_authenticated_round_trip() | 668 _test_authenticated_round_trip() |
| 669 _test_dir_upload_and_download() | |
| 442 # TODO(epoger): Add _test_unauthenticated_access() to make sure we raise | 670 # TODO(epoger): Add _test_unauthenticated_access() to make sure we raise |
| 443 # an exception when we try to access without needed credentials. | 671 # an exception when we try to access without needed credentials. |
| OLD | NEW |