post3 0.1.0

Pluggable S3-compatible object storage — core library with PostgreSQL and filesystem backends
Documentation
use sqlx::{PgPool, Postgres, Transaction};
use uuid::Uuid;

use crate::error::Post3Error;
use crate::models::ObjectRow;

pub struct ObjectsRepository<'a> {
    db: &'a PgPool,
}

impl<'a> ObjectsRepository<'a> {
    pub fn new(db: &'a PgPool) -> Self {
        Self { db }
    }

    pub async fn insert_in_tx(
        tx: &mut Transaction<'_, Postgres>,
        bucket_id: Uuid,
        key: &str,
        size: i64,
        etag: &str,
        content_type: &str,
    ) -> Result<ObjectRow, Post3Error> {
        // Delete existing (cascades to blocks + metadata)
        sqlx::query("DELETE FROM objects WHERE bucket_id = $1 AND key = $2")
            .bind(bucket_id)
            .bind(key)
            .execute(&mut **tx)
            .await?;

        let row = sqlx::query_as::<_, ObjectRow>(
            "INSERT INTO objects (bucket_id, key, size, etag, content_type) \
             VALUES ($1, $2, $3, $4, $5) RETURNING *",
        )
        .bind(bucket_id)
        .bind(key)
        .bind(size)
        .bind(etag)
        .bind(content_type)
        .fetch_one(&mut **tx)
        .await?;

        Ok(row)
    }

    pub async fn get(
        &self,
        bucket_id: Uuid,
        key: &str,
    ) -> Result<Option<ObjectRow>, Post3Error> {
        let row = sqlx::query_as::<_, ObjectRow>(
            "SELECT * FROM objects WHERE bucket_id = $1 AND key = $2",
        )
        .bind(bucket_id)
        .bind(key)
        .fetch_optional(self.db)
        .await?;

        Ok(row)
    }

    pub async fn delete(
        &self,
        bucket_id: Uuid,
        key: &str,
    ) -> Result<bool, Post3Error> {
        let result =
            sqlx::query("DELETE FROM objects WHERE bucket_id = $1 AND key = $2")
                .bind(bucket_id)
                .bind(key)
                .execute(self.db)
                .await?;

        Ok(result.rows_affected() > 0)
    }

    pub async fn list(
        &self,
        bucket_id: Uuid,
        prefix: Option<&str>,
        start_after: Option<&str>,
        max_keys: i64,
    ) -> Result<Vec<ObjectRow>, Post3Error> {
        let rows = match (prefix, start_after) {
            (Some(pfx), Some(after)) => {
                let pattern = format!("{pfx}%");
                sqlx::query_as::<_, ObjectRow>(
                    "SELECT * FROM objects \
                     WHERE bucket_id = $1 AND key LIKE $2 AND key > $3 \
                     ORDER BY key ASC LIMIT $4",
                )
                .bind(bucket_id)
                .bind(pattern)
                .bind(after)
                .bind(max_keys)
                .fetch_all(self.db)
                .await?
            }
            (Some(pfx), None) => {
                let pattern = format!("{pfx}%");
                sqlx::query_as::<_, ObjectRow>(
                    "SELECT * FROM objects \
                     WHERE bucket_id = $1 AND key LIKE $2 \
                     ORDER BY key ASC LIMIT $3",
                )
                .bind(bucket_id)
                .bind(pattern)
                .bind(max_keys)
                .fetch_all(self.db)
                .await?
            }
            (None, Some(after)) => {
                sqlx::query_as::<_, ObjectRow>(
                    "SELECT * FROM objects \
                     WHERE bucket_id = $1 AND key > $2 \
                     ORDER BY key ASC LIMIT $3",
                )
                .bind(bucket_id)
                .bind(after)
                .bind(max_keys)
                .fetch_all(self.db)
                .await?
            }
            (None, None) => {
                sqlx::query_as::<_, ObjectRow>(
                    "SELECT * FROM objects \
                     WHERE bucket_id = $1 \
                     ORDER BY key ASC LIMIT $2",
                )
                .bind(bucket_id)
                .bind(max_keys)
                .fetch_all(self.db)
                .await?
            }
        };

        Ok(rows)
    }
}