|
| 1 | +from datetime import timedelta |
| 2 | +from os.path import split |
| 3 | + |
| 4 | +from google.cloud.storage.blob import Blob |
| 5 | + |
| 6 | + |
| 7 | +def upload_blob( |
| 8 | + instance, |
| 9 | + field_name, |
| 10 | + local_path, |
| 11 | + destination_path=None, |
| 12 | + attributes=None, |
| 13 | + allow_overwrite=False, |
| 14 | + existing_path=None, |
| 15 | +): |
| 16 | + """Upload a file to the cloud store, using the instance and field name to determine the store details |
| 17 | +
|
| 18 | + You might use this utility to upload fixtue files for integration tests, as part of |
| 19 | + a migration, or as part of a function creating local files. The field's own logic for generating paths |
| 20 | + is used by default, although this can be overridden. |
| 21 | +
|
| 22 | + Returns the field value so you can use this to construct instances directly: |
| 23 | +
|
| 24 | + # Directly set the instance field without processing the blob ingress |
| 25 | + with override_settings(GCP_STORAGE_OVERRIDE_BLOBFIELD_VALUE=True): |
| 26 | + instance.field_name = upload_blob(...) |
| 27 | +
|
| 28 | + :param django.db.Model instance: An instance of a django Model which has a BlobField |
| 29 | + :param str field_name: The name of the BlobField attribute on the instance |
| 30 | + :param str local_path: The path to the file to upload |
| 31 | + :param str destination_path: The path to upload the file to. If None, the remote path will be generated |
| 32 | + from the BlobField. If setting this value, take care to override the value of the field on the instance |
| 33 | + so it's path matches; this is not updated for you. |
| 34 | + :param dict attributes: A dictionary of attributes to set on the blob eg content type |
| 35 | + :param bool allow_overwrite: If true, allows existing blobs at the path to be overwritten. If destination_path is not given, this is provided to the get_destination_path callback (and may be overridden by that callback per its specification) |
| 36 | + :param str existing_path: If destination_path is None, this is provided to the get_destination_path callback to simulate behaviour where there is an existing path |
| 37 | + """ |
| 38 | + # Get the field (which |
| 39 | + field = instance._meta.get_field(field_name) |
| 40 | + if destination_path is None: |
| 41 | + destination_path, allow_overwrite = field.get_destination_path( |
| 42 | + instance, |
| 43 | + original_name=local_path, |
| 44 | + attributes=attributes, |
| 45 | + allow_overwrite=allow_overwrite, |
| 46 | + existing_path=existing_path, |
| 47 | + bucket=field.storage.bucket, |
| 48 | + ) |
| 49 | + |
| 50 | + # If not allowing overwrite, set generation matching constraints to prevent it |
| 51 | + if_generation_match = None if allow_overwrite else 0 |
| 52 | + |
| 53 | + # Attributes must be a dict by default |
| 54 | + attributes = attributes or {} |
| 55 | + |
| 56 | + # Upload the file |
| 57 | + Blob(destination_path, bucket=field.storage.bucket).upload_from_filename( |
| 58 | + local_path, if_generation_match=if_generation_match, **attributes |
| 59 | + ) |
| 60 | + |
| 61 | + # Return the field value |
| 62 | + return {"path": destination_path} |
| 63 | + |
| 64 | + |
| 65 | +def get_path(instance, field_name): |
| 66 | + """Get the path of the blob in the object store""" |
| 67 | + field_value = getattr(instance, field_name) |
| 68 | + return field_value.get("path", None) if field_value is not None else None |
| 69 | + |
| 70 | + |
| 71 | +def get_blob(instance, field_name): |
| 72 | + """Get a blob from a model instance containing a BlobField |
| 73 | +
|
| 74 | + This allows you to download the blob to a local file. For example: |
| 75 | +
|
| 76 | + ```py |
| 77 | + blob = get_blob(mymodel, "my_field_name") |
| 78 | +
|
| 79 | + logger.info("Downloading file %s from bucket %s", blob.name, blob.bucket.name) |
| 80 | +
|
| 81 | + # Download the blob to the temporary directory |
| 82 | + blob_file_name = os.path.split(blob.name)[-1] |
| 83 | + blob.download_to_filename(blob_file_name) |
| 84 | + ``` |
| 85 | +
|
| 86 | + :param django.db.Model instance: An instance of a django Model which has a BlobField |
| 87 | + :param str field_name: The name of the BlobField attribute on the instance |
| 88 | + """ |
| 89 | + path = get_path(instance, field_name) |
| 90 | + if path is not None: |
| 91 | + field = instance._meta.get_field(field_name) |
| 92 | + return field.storage.bucket.blob(path) |
| 93 | + |
| 94 | + |
| 95 | +def get_blob_name(instance, field_name): |
| 96 | + """Get the name of the blob including its extension (absent any path) |
| 97 | +
|
| 98 | + The name is the object path absent any folder prefixes, |
| 99 | + eg if blob is located at path `mystuff/1234/myfile.txt` the name |
| 100 | + is `myfile.txt` |
| 101 | + """ |
| 102 | + path = get_path(instance, field_name) |
| 103 | + if path is not None: |
| 104 | + return split(path)[-1] |
| 105 | + |
| 106 | + |
| 107 | +def get_signed_url(instance, field_name, expiration=None): |
| 108 | + """Get a signed URL to the blob for the given model field name |
| 109 | + :param str field_name: Name of the model field (which should be a BlobField) |
| 110 | + :param Union[datetime.datetime|datetime.timedelta|None] expiration: Expiration date or duration for the URL. If None, duration defaults to 24hrs. |
| 111 | + :return str: Signed URL of the blob |
| 112 | + """ |
| 113 | + expiration = expiration or timedelta(hours=24) |
| 114 | + return get_blob(instance, field_name).generate_signed_url(expiration=expiration) |
| 115 | + |
| 116 | + |
| 117 | +class BlobFieldModel: |
| 118 | + """Mixin to a model to provide extra utility methods for processing of blobs""" |
| 119 | + |
| 120 | + def get_blob(self, field_name): |
| 121 | + """Get a blob object for the given model field name""" |
| 122 | + return get_blob(self, field_name) |
| 123 | + |
| 124 | + def get_blob_name(self, field_name): |
| 125 | + """Get blob name for the given model field name""" |
| 126 | + return get_blob_name(self, field_name) |
| 127 | + |
| 128 | + def get_path(self, field_name): |
| 129 | + """Get the path of the blob in the object store for the given model field name""" |
| 130 | + return get_path(self, field_name) |
| 131 | + |
| 132 | + def get_signed_url(self, field_name, expiration=None): |
| 133 | + """Get a signed URL to the blob for the given model field name""" |
| 134 | + return get_signed_url(self, field_name, expiration) |
0 commit comments