diff --git a/README.md b/README.md index e1398e8..2d4e3f9 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,11 @@ client.table_row_update(project, table_name, row_id, row_info) # Delete a row (only if you've already bought me a beer) client.table_row_delete(project, table_name, row_id) + +# Upload a file to an attachment field +image_path = "path/to/new_car.png" +image_column = "images" +client.upload_file(project, table, row_id, image_column, image_path) ``` ### Available filters diff --git a/nocodb/api.py b/nocodb/api.py index 296fa21..648f200 100644 --- a/nocodb/api.py +++ b/nocodb/api.py @@ -6,12 +6,14 @@ class NocoDBAPIUris(Enum): V1_DB_DATA_PREFIX = "api/v1/db/data/" V1_DB_META_PREFIX = "api/v1/db/meta/" + V1_DB_STORAGE_PREFIX = "api/v1/db/storage/" class NocoDBAPI: def __init__(self, base_uri: str): self.__base_data_uri = urljoin(base_uri + "/", NocoDBAPIUris.V1_DB_DATA_PREFIX.value) self.__base_meta_uri = urljoin(base_uri + "/", NocoDBAPIUris.V1_DB_META_PREFIX.value) + self.__base_storage_uri = urljoin(base_uri + "/", NocoDBAPIUris.V1_DB_STORAGE_PREFIX.value) def get_table_uri(self, project: NocoDBProject, table: str) -> str: return urljoin(self.__base_data_uri, "/".join( @@ -84,6 +86,24 @@ def get_project_tables_uri( "tables" ) )) + + def get_storage_upload_uri( + self, + ) -> str: + return urljoin(self.__base_storage_uri, "upload") + + def get_storage_upload_path( + self, project: NocoDBProject, table: str, column_id: str, + ) -> str: + """This Path/URL is used in the request body for uploading attachments.""" + return urljoin(f"{project.org_name}/", "/".join( + [ + project.project_name, + table, + column_id, + ] + )) + def get_table_meta_uri( self, tableId: str, operation: str = None, diff --git a/nocodb/infra/requests_client.py b/nocodb/infra/requests_client.py index 98e63b5..ea7266e 100644 --- a/nocodb/infra/requests_client.py +++ b/nocodb/infra/requests_client.py @@ -1,4 +1,5 @@ from typing import Optional + from ..nocodb import ( NocoDBClient, NocoDBProject, @@ -57,10 +58,10 @@ def table_row_create(self, project: NocoDBProject, table: str, body: dict) -> di "POST", self.__api_info.get_table_uri(project, table), json=body ).json() - def table_row_detail(self, project: NocoDBProject, table: str, row_id: int) -> dict: + def table_row_detail(self, project: NocoDBProject, table_id: str, row_id: int) -> dict: return self._request( "GET", - self.__api_info.get_row_detail_uri(project, table, row_id), + self.__api_info.get_row_detail_uri(project, table_id, row_id), ).json() def table_row_update( @@ -210,3 +211,39 @@ def table_column_set_primary( "POST", url=self.__api_info.get_column_uri(columnId, "primary"), ).json() + + def upload_file( + self, project: NocoDBProject, table_id: str, row_id: int, column_name: str, path: str + ) -> dict: + """Upload a file to an Attachment field.""" + content_type = self.__session.headers.get("Content-Type") + self.__session.headers.pop("Content-Type") + + # Upload the file to NocoDB's storage. + upload_result = self._request( + "POST", + url=self.__api_info.get_storage_upload_uri(), + data={"path": self.__api_info.get_storage_upload_path(project, table_id, column_name)}, + files={"file": open(path, "rb")} + ).json() + if len(upload_result) < 1: + raise ValueError("result of storage upload call doesn't contain the file info") + file_data = upload_result[0] + + # Link the uploaded file to the given attachment file. + result = self.table_row_update( + project, + table_id, + row_id, + { + column_name: [{ + "path": file_data["path"], + "title": file_data["title"], + "size": file_data["size"], + }] + } + ) + + self.__session.headers.update({"Content-Type": str(content_type)}) + return result + \ No newline at end of file diff --git a/nocodb/nocodb.py b/nocodb/nocodb.py index 3d7eb07..9628d2c 100644 --- a/nocodb/nocodb.py +++ b/nocodb/nocodb.py @@ -1,4 +1,5 @@ from abc import ABC, abstractmethod +from pathlib import Path from typing import Optional """ @@ -32,7 +33,7 @@ def get_header(self) -> dict: pass -class APIToken: +class APIToken(AuthToken): def __init__(self, token: str): self.__token = token @@ -40,7 +41,7 @@ def get_header(self) -> dict: return {"xc-token": self.__token} -class JWTAuthToken: +class JWTAuthToken(AuthToken): def __init__(self, token: str): self.__token = token @@ -173,3 +174,9 @@ def table_column_set_primary( self, columnId: str, ) -> dict: pass + + @abstractmethod + def upload_file( + self, project: NocoDBProject, table: str, row_id: int, column_id: str, file: Path + ): + pass