bucketstore.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. """
  2. bucketstore module
  3. """
  4. import io
  5. import os
  6. import os.path
  7. import boto3
  8. import botocore
  9. from typing import BinaryIO, Callable, List, Union
  10. AWS_DEFAULT_REGION = "us-east-1"
  11. class S3Key:
  12. """An Amazon S3 Key"""
  13. def __init__(self, bucket: "S3Bucket", name: str) -> None:
  14. """constructor"""
  15. super(S3Key, self).__init__()
  16. self.bucket = bucket
  17. self.name = name
  18. def __repr__(self) -> str:
  19. """str representation of an s3key"""
  20. return "<S3Key name={0!r} bucket={1!r}>".format(self.name, self.bucket.name)
  21. def __len__(self) -> int:
  22. """returns the size of the s3 object of this key in bytes"""
  23. return self.size()
  24. @property
  25. def _boto_object(self): # type: ignore
  26. """the underlying boto3 s3 key object"""
  27. return self.bucket._boto_s3.Object(self.bucket.name, self.name)
  28. def get(self) -> str:
  29. """Gets the value of the key."""
  30. return self._boto_object.get()["Body"].read()
  31. def download(self, file: Union[str, BinaryIO], callback: Callable = None) -> None:
  32. """download the key to the given path or file object"""
  33. if self.name not in self.bucket:
  34. raise Exception("this key does not exist!")
  35. _download = self.bucket._boto_s3.meta.client.download_fileobj
  36. if isinstance(file, str):
  37. with open(file, "wb") as data:
  38. _download(self.bucket.name, self.name, data, Callback=callback)
  39. elif isinstance(file, io.IOBase):
  40. _download(self.bucket.name, self.name, file, Callback=callback)
  41. def upload(self, file: Union[str, BinaryIO], callback: Callable = None) -> None:
  42. """upload the file or file obj at the given path to this key"""
  43. _upload = self.bucket._boto_s3.meta.client.upload_fileobj
  44. if isinstance(file, str):
  45. if not os.path.isfile(file):
  46. raise Exception("file does not exist!")
  47. with open(file, "rb") as data:
  48. _upload(data, self.bucket.name, self.name, Callback=callback)
  49. elif isinstance(file, io.IOBase):
  50. _upload(file, self.bucket.name, self.name, Callback=callback)
  51. def size(self) -> int:
  52. """get the size of this object in s3"""
  53. total = 0
  54. for key in self.bucket._boto_bucket.objects.filter(Prefix=self.name):
  55. total += key.size
  56. return total
  57. def set(self, value: str, metadata: dict = None, content_type: str = "") -> dict:
  58. """Sets the key to the given value."""
  59. if not metadata:
  60. metadata = {}
  61. return self._boto_object.put(
  62. Body=value, Metadata=metadata, ContentType=content_type
  63. )
  64. def rename(self, new_name: str) -> None:
  65. """renames the key to a given new name"""
  66. # copy the item to avoid pulling and pushing
  67. self.bucket._boto_s3.Object(self.bucket.name, new_name).copy_from(
  68. CopySource="{}/{}".format(self.bucket.name, self.name)
  69. )
  70. # Delete the current key.
  71. self.delete()
  72. # Set the new name.
  73. self.name = new_name
  74. def delete(self,) -> dict:
  75. """Deletes the key."""
  76. return self._boto_object.delete()
  77. @property
  78. def is_public(self) -> bool:
  79. """returns True if the public-read ACL is set for the Key."""
  80. for grant in self._boto_object.Acl().grants:
  81. if "AllUsers" in grant["Grantee"].get("URI", ""):
  82. if grant["Permission"] == "READ":
  83. return True
  84. return False
  85. def make_public(self) -> dict:
  86. """sets the 'public-read' ACL for the key."""
  87. if not self.is_public:
  88. return self._boto_object.Acl().put(ACL="public-read")
  89. return {}
  90. @property
  91. def meta(self) -> dict:
  92. """returns the metadata for the key."""
  93. return self._boto_object.get()["Metadata"]
  94. @meta.setter
  95. def meta(self, value: dict) -> None:
  96. """sets the metadata for the key."""
  97. self.set(self.get(), value)
  98. @property
  99. def url(self) -> str:
  100. """returns the public URL for the given key."""
  101. if self.is_public:
  102. return "{0}/{1}/{2}".format(
  103. self.bucket._boto_s3.meta.client.meta.endpoint_url,
  104. self.bucket.name,
  105. self.name,
  106. )
  107. raise ValueError(
  108. "{0!r} does not have the public-read ACL set. "
  109. "Use the make_public() method to allow for "
  110. "public URL sharing.".format(self.name)
  111. )
  112. def temp_url(self, duration: int = 120) -> str:
  113. """returns a temporary URL for the given key."""
  114. return self.bucket._boto_s3.meta.client.generate_presigned_url(
  115. "get_object",
  116. Params={"Bucket": self.bucket.name, "Key": self.name},
  117. ExpiresIn=duration,
  118. )
  119. class S3Bucket:
  120. """An Amazon S3 Bucket."""
  121. def __init__(self, name: str, create: bool = False, region: str = "") -> None:
  122. super(S3Bucket, self).__init__()
  123. self.name = name
  124. self.region = region or os.getenv("AWS_DEFAULT_REGION", AWS_DEFAULT_REGION)
  125. self._boto_s3 = boto3.resource("s3", self.region)
  126. self._boto_bucket = self._boto_s3.Bucket(self.name)
  127. # Check if the bucket exists.
  128. if not self._boto_s3.Bucket(self.name) in self._boto_s3.buckets.all():
  129. if create:
  130. # Create the bucket.
  131. self._boto_s3.create_bucket(Bucket=self.name)
  132. else:
  133. raise ValueError("The bucket {0!r} doesn't exist!".format(self.name))
  134. def __getitem__(self, key: str) -> str:
  135. """allows for accessing keys with the array syntax"""
  136. return self.get(key)
  137. def __setitem__(self, key: str, value: str) -> dict:
  138. """allows for setting/uploading keys with the array syntax"""
  139. return self.set(key, value)
  140. def __delitem__(self, key: str) -> dict:
  141. """allow for deletion of keys via the del operator"""
  142. return self.delete(key)
  143. def __contains__(self, item: str) -> bool:
  144. """allows for use of the in keyword on the bucket object"""
  145. try:
  146. self._boto_s3.Object(self.name, item).load()
  147. return True
  148. except botocore.exceptions.ClientError as exception:
  149. if exception.response["Error"]["Code"] == "404":
  150. # The object does not exist.
  151. return False
  152. raise # pragma: no cover
  153. def list(self) -> List:
  154. """returns a list of keys in the bucket."""
  155. return [k.key for k in self._boto_bucket.objects.all()]
  156. @property
  157. def is_public(self) -> bool:
  158. """returns True if the public-read ACL is set for the bucket."""
  159. for grant in self._boto_bucket.Acl().grants:
  160. if "AllUsers" in grant["Grantee"].get("URI", ""):
  161. if grant["Permission"] == "READ":
  162. return True
  163. return False
  164. def make_public(self) -> dict:
  165. """Makes the bucket public-readable."""
  166. return self._boto_bucket.Acl().put(ACL="public-read")
  167. def key(self, key: str) -> S3Key:
  168. """returns a given key from the bucket."""
  169. return S3Key(self, key)
  170. def all(self) -> List[S3Key]:
  171. """returns all keys in the bucket."""
  172. return [self.key(k) for k in self.list()]
  173. def get(self, key: str) -> str:
  174. """get the contents of the given key"""
  175. selected_key = self.key(key)
  176. return selected_key.get()
  177. def set(
  178. self, key: str, value: str, metadata: dict = None, content_type: str = ""
  179. ) -> dict:
  180. """creates/edits a key in the s3 bucket"""
  181. if not metadata:
  182. metadata = {}
  183. new_key = self.key(key)
  184. return new_key.set(value, metadata, content_type)
  185. def delete(self, key: str = None) -> dict:
  186. """Deletes the given key, or the whole bucket."""
  187. # Delete the whole bucket.
  188. if key is None:
  189. # Delete everything in the bucket.
  190. for each_key in self.all():
  191. each_key.delete()
  192. # Delete the bucket.
  193. return self._boto_bucket.delete()
  194. # If a key was passed, delete they key.
  195. k = self.key(key)
  196. return k.delete()
  197. def __repr__(self) -> str:
  198. """representation of an s3bucket object"""
  199. return "<S3Bucket name={0!r}>".format(self.name)
  200. def list() -> List[str]: # pylint: disable=redefined-builtin
  201. """lists buckets, by name."""
  202. s3_resource = boto3.resource("s3")
  203. return [bucket.name for bucket in s3_resource.buckets.all()]
  204. def get(bucket_name: str, create: bool = False) -> S3Bucket:
  205. """get an s3bucket object by name"""
  206. return S3Bucket(bucket_name, create=create)
  207. def login(
  208. access_key_id: str, secret_access_key: str, region: str = AWS_DEFAULT_REGION
  209. ) -> None:
  210. """sets environment variables for boto3."""
  211. os.environ["AWS_ACCESS_KEY_ID"] = access_key_id
  212. os.environ["AWS_SECRET_ACCESS_KEY"] = secret_access_key
  213. os.environ["AWS_DEFAULT_REGION"] = region