Skip to content

Storage adapters

The core of file-keeper's flexibility lies in its storage adapters. Adapters encapsulate the logic for interacting with a specific storage system, allowing file-keeper to remain agnostic to the underlying implementation. To create a custom adapter, you'll need to define a class that inherits from Storage and implements its services.

Steps to create a custom adapter

Define a Settings class

Create a dataclass to hold the configuration options for your storage adapter. This class should inherit from Settings. This allows file-keeper to handle validation and default values.

Example

from dataclasses import dataclass
import file_keeper as fk

@dataclass
class MyStorageSettings(fk.Settings):
    api_key: str = ""
    endpoint: str = ""

Extend the Storage class

Create a class that inherits from Storage and sets the SettingsFactory class attribute to your settings class. It also sets UploaderFactory and ReaderFactory in the same way. The implementation will follow soon.

Example

...

class MyStorage(fk.Storage):
    settings: MyStorageSettings

    SettingsFactory = MyStorageSettings
    UploaderFactory = MyUploader
    ReaderFactory = MyReader

Implement Uploader and Reader services

Create classes for UploaderFactory and ReaderFactory that inherit from Uploader and Reader respectively. These classes will contain the logic for uploading and reading files to and from your storage system.

Make sure to add CREATE capability to Uploader and STREAM capability to Reader. Otherwise storage will pretend that these services do not support these operations

Example

class MyUploader(fk.Uploader):

    capabilities = fk.Capability.CREATE

    def upload(self, location: fk.Location, upload: fk.Upload, extras: dict[str, Any]) -> fk.FileData:
        # Implement your upload logic here
        reader = upload.hashing_reader()
        for chunk in reader:
            # send fragment to storage

        return fk.FileData(location, upload.size, upload.content_type, reader.get_hash())


class MyReader(fk.Reader):

    capabilities = fk.Capability.STREAM

    def stream(self, data: fk.FileData, extras: dict[str, Any]) -> Iterable[bytes]:
        # Implement your streaming logic here
        for chunk in file_stream:
            yield chunk

Register the adapter

If you are going to use custom adapter only inside a single script, you can register it directly using adapters registry:

fk.adapters.register("local", MyStorage)

If you are writing a library that will be used accross multiple project it's better to register storage using entrypoints of the python package.

Use the register_adapters hook to register your adapter. This makes it available as a type inside make_storage.

@fk.hookimpl
def register_adapters(registry: fk.Registry[type[fk.Storage]]):
    registry.register("local", MyStorage)

Initialize the adapter

Now you can use your custom adapter:

storage = fk.make_storage("local", {
    "adapter": "local",
    "api_key": "123",
    "endpoint": "https://example.local",
})

This is a basic example, but it demonstrates the fundamental principles of creating a custom storage adapter. You can extend this example to support more complex features and integrate with a wider range of storage systems.