Skip to content

Commit 49a04bf

Browse files
authored
Merge pull request #59 from nitrictech/feature/presign-urls
Add presigned url capability
2 parents 7430e52 + 9d870e2 commit 49a04bf

4 files changed

Lines changed: 83 additions & 2 deletions

File tree

examples/storage/sign_url_read.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# [START import]
2+
from nitric.api import Storage
3+
from nitric.api.storage import FileMode
4+
5+
# [END import]
6+
async def storage_sign_url_read():
7+
# [START snippet]
8+
# Construct a new storage client with default settings
9+
storage = Storage()
10+
11+
# Create a readonly presigned url for the file valid for the next 3600 seconds
12+
await storage.bucket("my-bucket").file("path/to/item").sign_url(mode=FileMode.READ, expiry=3600)
13+
14+
15+
# [END snippet]

examples/storage/sign_url_write.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# [START import]
2+
from nitric.api import Storage
3+
from nitric.api.storage import FileMode
4+
5+
# [END import]
6+
async def storage_sign_url_write():
7+
# [START snippet]
8+
# Construct a new storage client with default settings
9+
storage = Storage()
10+
11+
# Create a writable presigned url for the file valid for the next 3600 seconds
12+
await storage.bucket("my-bucket").file("path/to/item").sign_url(mode=FileMode.WRITE, expiry=3600)
13+
14+
15+
# [END snippet]

nitric/api/storage.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@
2020

2121
from grpclib import GRPCError
2222

23-
from nitric.api.exception import exception_from_grpc_error
23+
from nitric.api.exception import exception_from_grpc_error, InvalidArgumentException
2424
from nitric.utils import new_default_channel
25-
from nitricapi.nitric.storage.v1 import StorageServiceStub
25+
from nitricapi.nitric.storage.v1 import StorageServiceStub, StoragePreSignUrlRequestOperation
26+
from enum import Enum
2627

2728

2829
class Storage(object):
@@ -59,6 +60,22 @@ def file(self, key: str):
5960
return File(_storage=self._storage, _bucket=self.name, key=key)
6061

6162

63+
class FileMode(Enum):
64+
"""Definition of available operation modes for file signed URLs."""
65+
66+
READ = 0
67+
WRITE = 1
68+
69+
def to_request_operation(self) -> StoragePreSignUrlRequestOperation:
70+
"""Convert FileMode to a StoragePreSignUrlRequestOperation"""
71+
if self == FileMode.READ:
72+
return StoragePreSignUrlRequestOperation.READ
73+
elif self == FileMode.WRITE:
74+
return StoragePreSignUrlRequestOperation.WRITE
75+
else:
76+
raise InvalidArgumentException("Invalid FileMode")
77+
78+
6279
@dataclass(frozen=True, order=True)
6380
class File(object):
6481
"""A reference to a file in a bucket, used to perform operations on that file."""
@@ -92,3 +109,13 @@ async def delete(self):
92109
await self._storage._storage_stub.delete(bucket_name=self._bucket, key=self.key)
93110
except GRPCError as grpc_err:
94111
raise exception_from_grpc_error(grpc_err)
112+
113+
async def sign_url(self, mode: FileMode = FileMode.READ, expiry: int = 3600):
114+
"""Generated a signed url for reading or writing to a file"""
115+
116+
try:
117+
await self._storage._storage_stub.pre_sign_url(
118+
bucket_name=self._bucket, key=self.key, operation=mode.to_request_operation(), expiry=expiry
119+
)
120+
except GRPCError as grpc_err:
121+
raise exception_from_grpc_error(grpc_err)

tests/api/test_storage.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,22 @@ async def test_delete(self):
8383
assert mock_read.call_args.kwargs["bucket_name"] == "test-bucket"
8484
assert mock_read.call_args.kwargs["key"] == "test-file"
8585

86+
async def test_sign_url(self):
87+
mock_pre_sign_url = AsyncMock()
88+
mock_pre_sign_url.return_value = Object()
89+
90+
with patch("nitricapi.nitric.storage.v1.StorageServiceStub.pre_sign_url", mock_pre_sign_url):
91+
bucket = Storage().bucket("test-bucket")
92+
file = bucket.file("test-file")
93+
await file.sign_url()
94+
95+
# Check expected values were passed to Stub
96+
mock_pre_sign_url.assert_called_once()
97+
assert mock_pre_sign_url.call_args.kwargs["bucket_name"] == "test-bucket"
98+
assert mock_pre_sign_url.call_args.kwargs["key"] == "test-file"
99+
assert mock_pre_sign_url.call_args.kwargs["operation"] == 0
100+
assert mock_pre_sign_url.call_args.kwargs["expiry"] == 3600
101+
86102
async def test_write_error(self):
87103
mock_write = AsyncMock()
88104
mock_write.side_effect = GRPCError(Status.UNKNOWN, "test error")
@@ -106,3 +122,11 @@ async def test_delete_error(self):
106122
with patch("nitricapi.nitric.storage.v1.StorageServiceStub.delete", mock_delete):
107123
with pytest.raises(UnknownException) as e:
108124
await Storage().bucket("test-bucket").file("test-file").delete()
125+
126+
async def test_sign_url_error(self):
127+
mock_pre_sign_url = AsyncMock()
128+
mock_pre_sign_url.side_effect = GRPCError(Status.UNKNOWN, "test error")
129+
130+
with patch("nitricapi.nitric.storage.v1.StorageServiceStub.pre_sign_url", mock_pre_sign_url):
131+
with pytest.raises(UnknownException) as e:
132+
await Storage().bucket("test-bucket").file("test-file").sign_url()

0 commit comments

Comments
 (0)