Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,128 @@ def upload_file(self, file: UploadFile, storage_service: Inject[StorageService])

See [Sample Project](https://github.com/python-ellar/ellar-storage/tree/master/samples)

## Some Quick Cloud Setup

### Google Cloud Storage

- For a service Account

```python
from ellar.common import Module
from ellar.core import ModuleBase
from ellar_storage import StorageModule, get_driver, Provider

@Module(modules=[
StorageModule.setup(
files={
# For a service Account
"driver": get_driver(Provider.GOOGLE_STORAGE),
"options": {
"key": "client_email",
"secret": "private_key",
"...": "..."
},
},
)
])
class ApplicationModule(ModuleBase):
pass
```
- Installed Application
```python
from ellar.common import Module
from ellar.core import ModuleBase
from ellar_storage import StorageModule, get_driver, Provider

@Module(modules=[
StorageModule.setup(
files={
# For a service Account
"driver": get_driver(Provider.GOOGLE_STORAGE),
"options": {
"key": "client_id",
"secret": "client_secret",
"...": "..."
},
},
)
])
class ApplicationModule(ModuleBase):
pass
```

- GCE instance
```python
from ellar.common import Module
from ellar.core import ModuleBase
from ellar_storage import StorageModule, get_driver, Provider

@Module(modules=[
StorageModule.setup(
files={
# For a service Account
"driver": get_driver(Provider.GOOGLE_STORAGE),
"options": {
"key": "GOOG0123456789ABCXYZ",
"secret": "key_secret",
"...": "..."
},
},
)
])
class ApplicationModule(ModuleBase):
pass
```
See [GCS](https://libcloud.readthedocs.io/en/stable/storage/drivers/google_storage.html?highlight=Google%20Cloud%20Storage#api-docs)

### AWS S3

```python
from ellar.common import Module
from ellar.core import ModuleBase
from ellar_storage import StorageModule, get_driver, Provider

@Module(modules=[
StorageModule.setup(
files={
"driver": get_driver(Provider.S3),
"options": {
"key": "api key",
"secret": "api secret key"
},
},
)
])
class ApplicationModule(ModuleBase):
pass
```

#### Specifying canned ACL when uploading an object
If you want to specify custom ACL when uploading an object,
you can do so by passing `extra` argument with the acl attribute to the `save` or `save_content` methods.

Valid values for this attribute are:

- private (default)
- public-read
- public-read-write
- authenticated-read
- bucket-owner-read
- bucket-owner-full-control

For example
```python
from ellar.common import UploadFile, Inject, post
from ellar_storage import StorageService

@post('/upload')
def upload_file(self, file: UploadFile, storage_service: Inject[StorageService]):
extra = {"content_type": "application/octet-stream", "acl": "public-read"}
stored_file = storage_service.save(file=file, extra=extra)

return {"message": f"{stored_file.filename} saved"}
```

## API Reference

### StorageService
Expand Down
2 changes: 1 addition & 1 deletion ellar_storage/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Storage Module for Ellar"""

__version__ = "0.1.1"
__version__ = "0.1.2"

from .module import StorageModule
from .providers import Provider, get_driver
Expand Down
2 changes: 1 addition & 1 deletion ellar_storage/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class _ContainerOptions(t.TypedDict):

class _StorageSetupKey(t.TypedDict):
driver: t.Type[StorageDriver]
options: _ContainerOptions
options: t.Union[_ContainerOptions, t.Dict[str, t.Any]]


@Module()
Expand Down
9 changes: 8 additions & 1 deletion ellar_storage/schemas.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import typing as t

from ellar.pydantic import model_validator
from ellar.pydantic import field_validator, model_validator
from pydantic import BaseModel

from ellar_storage.storage import StorageDriver
Expand All @@ -10,6 +10,13 @@ class _StorageSetupItem(BaseModel):
driver: t.Type[StorageDriver]
options: t.Dict[str, t.Any] = {}

@field_validator("options", mode="before")
def pre_options_validate(cls, value: t.Dict) -> t.Any:
if "key" not in value:
raise ValueError("Driver Options must have a `key` option ")

return value


class StorageSetup(BaseModel):
# default storage name that must exist in `storages`
Expand Down
8 changes: 8 additions & 0 deletions ellar_storage/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,15 @@ def save(
self,
file: UploadFile,
upload_storage: t.Optional[str] = None,
extra: t.Optional[t.Dict[str, t.Any]] = None,
) -> StoredFile:
extra_ = extra or {}
extra_.setdefault(
"meta_data",
{"content_type": file.content_type, "filename": file.filename},
)
extra_.setdefault("content_type", file.content_type)

return self.save_content(
name=file.filename or str(uuid.uuid4())[10],
content=file.file,
Expand Down
12 changes: 12 additions & 0 deletions tests/test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,15 @@ def test_storage_fails_for_invalid_default_storage():
},
default="files",
)


def test_storage_fails_for_invalid_driver_option():
with pytest.raises(ValueError, match="Driver Options must have a `key` option"):
StorageSetup(
storages={
"cloud": {
"driver": get_driver(Provider.GOOGLE_STORAGE),
"options": {"secret": "key_secret"},
}
},
)