mirror of
https://github.com/perstarkse/minne.git
synced 2026-04-25 10:18:38 +02:00
wip surreal
This commit is contained in:
@@ -4,17 +4,22 @@ use mime_guess::from_path;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
|
use surrealdb::RecordId;
|
||||||
use std::{
|
use std::{
|
||||||
io::{BufReader, Read},
|
io::{BufReader, Read},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
use tempfile::NamedTempFile;
|
use tempfile::NamedTempFile;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tracing::info;
|
use tracing::{debug, info};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::redis::client::{RedisClient, RedisClientTrait};
|
use crate::{redis::client::{RedisClient, RedisClientTrait}, surrealdb::SurrealDbClient};
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct Record {
|
||||||
|
id: RecordId,
|
||||||
|
}
|
||||||
|
|
||||||
/// Represents metadata and storage information for a file.
|
/// Represents metadata and storage information for a file.
|
||||||
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
|
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
|
||||||
@@ -40,6 +45,9 @@ pub enum FileError {
|
|||||||
#[error("Unsupported MIME type: {0}")]
|
#[error("Unsupported MIME type: {0}")]
|
||||||
UnsupportedMime(String),
|
UnsupportedMime(String),
|
||||||
|
|
||||||
|
#[error("SurrealDB error: {0}")]
|
||||||
|
SurrealError(#[from] surrealdb::Error),
|
||||||
|
|
||||||
#[error("Redis error: {0}")]
|
#[error("Redis error: {0}")]
|
||||||
RedisError(#[from] crate::redis::client::RedisError),
|
RedisError(#[from] crate::redis::client::RedisError),
|
||||||
|
|
||||||
@@ -86,6 +94,7 @@ impl IntoResponse for FileError {
|
|||||||
FileError::PersistError(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Failed to persist file"),
|
FileError::PersistError(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Failed to persist file"),
|
||||||
FileError::SerializationError(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Serialization error"),
|
FileError::SerializationError(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Serialization error"),
|
||||||
FileError::DeserializationError(_) => (StatusCode::BAD_REQUEST, "Deserialization error"),
|
FileError::DeserializationError(_) => (StatusCode::BAD_REQUEST, "Deserialization error"),
|
||||||
|
FileError::SurrealError(_) =>(StatusCode::INTERNAL_SERVER_ERROR, "Serialization error"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let body = Json(json!({
|
let body = Json(json!({
|
||||||
@@ -97,14 +106,66 @@ impl IntoResponse for FileError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FileInfo {
|
impl FileInfo {
|
||||||
/// Creates a new `FileInfo` instance from uploaded field data.
|
// /// Creates a new `FileInfo` instance from uploaded field data.
|
||||||
///
|
// ///
|
||||||
/// # Arguments
|
// /// # Arguments
|
||||||
/// * `field_data` - The uploaded file data.
|
// /// * `field_data` - The uploaded file data.
|
||||||
///
|
// ///
|
||||||
/// # Returns
|
// /// # Returns
|
||||||
/// * `Result<FileInfo, FileError>` - The created `FileInfo` or an error.
|
// /// * `Result<FileInfo, FileError>` - The created `FileInfo` or an error.
|
||||||
pub async fn new(field_data: FieldData<NamedTempFile>, redis_client: &RedisClient) -> Result<FileInfo, FileError> {
|
// pub async fn new(field_data: FieldData<NamedTempFile>, redis_client: &RedisClient) -> Result<FileInfo, FileError> {
|
||||||
|
// let file = field_data.contents; // NamedTempFile
|
||||||
|
// let metadata = field_data.metadata;
|
||||||
|
|
||||||
|
// // Extract file name from metadata
|
||||||
|
// let file_name = metadata.file_name.ok_or(FileError::MissingFileName)?;
|
||||||
|
// info!("File name: {:?}", file_name);
|
||||||
|
|
||||||
|
// // Calculate SHA256 hash of the file
|
||||||
|
// let sha = Self::get_sha(&file).await?;
|
||||||
|
// info!("SHA256: {:?}", sha);
|
||||||
|
|
||||||
|
// // Check if SHA exists in Redis
|
||||||
|
// if let Some(existing_file_info) = redis_client.get_file_info_by_sha(&sha).await? {
|
||||||
|
// info!("Duplicate file detected with SHA256: {}", sha);
|
||||||
|
// return Ok(existing_file_info);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Generate a new UUID
|
||||||
|
// let uuid = Uuid::new_v4();
|
||||||
|
// info!("UUID: {:?}", uuid);
|
||||||
|
|
||||||
|
// // Sanitize file name
|
||||||
|
// let sanitized_file_name = sanitize_file_name(&file_name);
|
||||||
|
// info!("Sanitized file name: {:?}", sanitized_file_name);
|
||||||
|
|
||||||
|
// // Persist the file to the filesystem
|
||||||
|
// let persisted_path = Self::persist_file(&uuid, file, &sanitized_file_name).await?;
|
||||||
|
|
||||||
|
// // Guess the MIME type
|
||||||
|
// let mime_type = Self::guess_mime_type(&persisted_path);
|
||||||
|
// info!("Mime type: {:?}", mime_type);
|
||||||
|
|
||||||
|
// // Construct the FileInfo object
|
||||||
|
// let file_info = FileInfo {
|
||||||
|
// uuid: uuid.to_string(),
|
||||||
|
// sha256: sha.clone(),
|
||||||
|
// path: persisted_path.to_string_lossy().to_string(),
|
||||||
|
// mime_type,
|
||||||
|
// };
|
||||||
|
|
||||||
|
// // Store FileInfo in Redis with SHA256 as key
|
||||||
|
// redis_client.set_file_info(&sha, &file_info).await?;
|
||||||
|
|
||||||
|
// // Map UUID to SHA256 in Redis
|
||||||
|
// redis_client.set_sha_uuid_mapping(&uuid, &sha).await?;
|
||||||
|
|
||||||
|
// Ok(file_info)
|
||||||
|
// }
|
||||||
|
pub async fn new(
|
||||||
|
field_data: FieldData<NamedTempFile>,
|
||||||
|
db_client: &SurrealDbClient,
|
||||||
|
) -> Result<FileInfo, FileError> {
|
||||||
let file = field_data.contents; // NamedTempFile
|
let file = field_data.contents; // NamedTempFile
|
||||||
let metadata = field_data.metadata;
|
let metadata = field_data.metadata;
|
||||||
|
|
||||||
@@ -116,8 +177,8 @@ impl FileInfo {
|
|||||||
let sha = Self::get_sha(&file).await?;
|
let sha = Self::get_sha(&file).await?;
|
||||||
info!("SHA256: {:?}", sha);
|
info!("SHA256: {:?}", sha);
|
||||||
|
|
||||||
// Check if SHA exists in Redis
|
// Check if SHA exists in SurrealDB
|
||||||
if let Some(existing_file_info) = redis_client.get_file_info_by_sha(&sha).await? {
|
if let Some(existing_file_info) = Self::get_by_sha(&sha, db_client).await? {
|
||||||
info!("Duplicate file detected with SHA256: {}", sha);
|
info!("Duplicate file detected with SHA256: {}", sha);
|
||||||
return Ok(existing_file_info);
|
return Ok(existing_file_info);
|
||||||
}
|
}
|
||||||
@@ -145,15 +206,13 @@ impl FileInfo {
|
|||||||
mime_type,
|
mime_type,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store FileInfo in Redis with SHA256 as key
|
// Store FileInfo in SurrealDB
|
||||||
redis_client.set_file_info(&sha, &file_info).await?;
|
Self::create_record(&file_info, db_client).await?;
|
||||||
|
|
||||||
// Map UUID to SHA256 in Redis
|
|
||||||
redis_client.set_sha_uuid_mapping(&uuid, &sha).await?;
|
|
||||||
|
|
||||||
Ok(file_info)
|
Ok(file_info)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Retrieves `FileInfo` based on UUID.
|
/// Retrieves `FileInfo` based on UUID.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
@@ -334,6 +393,84 @@ impl FileInfo {
|
|||||||
.first_or(mime::APPLICATION_OCTET_STREAM)
|
.first_or(mime::APPLICATION_OCTET_STREAM)
|
||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Creates a new record in SurrealDB for the given `FileInfo`.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `file_info` - The `FileInfo` to store.
|
||||||
|
/// * `db_client` - Reference to the SurrealDbClient.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Result<(), FileError>` - Empty result or an error.
|
||||||
|
async fn create_record(file_info: &FileInfo, db_client: &SurrealDbClient) -> Result<(), FileError> {
|
||||||
|
// Define the table and primary key
|
||||||
|
|
||||||
|
// Create the record
|
||||||
|
let _created: Option<Record> = db_client
|
||||||
|
.client
|
||||||
|
.create(("file", &file_info.uuid ))
|
||||||
|
.content(file_info.clone())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
debug!("{:?}",_created);
|
||||||
|
|
||||||
|
info!("Created FileInfo record with SHA256: {}", file_info.sha256);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves a `FileInfo` by UUID.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `uuid` - The UUID string.
|
||||||
|
/// * `db_client` - Reference to the SurrealDbClient.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Result<Option<FileInfo>, FileError>` - The `FileInfo` or `None` if not found.
|
||||||
|
async fn get_by_uuid(uuid: &str, db_client: &SurrealDbClient) -> Result<Option<FileInfo>, FileError> {
|
||||||
|
let query = format!("SELECT * FROM file WHERE uuid = '{}'", uuid);
|
||||||
|
let response: Vec<FileInfo> = db_client.client.query(query).await?.take(0)?;
|
||||||
|
|
||||||
|
Ok(response.into_iter().next())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves a `FileInfo` by SHA256.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `sha256` - The SHA256 hash string.
|
||||||
|
/// * `db_client` - Reference to the SurrealDbClient.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Result<Option<FileInfo>, FileError>` - The `FileInfo` or `None` if not found.
|
||||||
|
async fn get_by_sha(sha256: &str, db_client: &SurrealDbClient) -> Result<Option<FileInfo>, FileError> {
|
||||||
|
let query = format!("SELECT * FROM file WHERE sha256 = '{}'", sha256);
|
||||||
|
let response: Vec<FileInfo> = db_client.client.query(query).await?.take(0)?;
|
||||||
|
|
||||||
|
Ok(response.into_iter().next())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deletes a `FileInfo` record by SHA256.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `sha256` - The SHA256 hash string.
|
||||||
|
/// * `db_client` - Reference to the SurrealDbClient.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Result<(), FileError>` - Empty result or an error.
|
||||||
|
async fn delete_record(sha256: &str, db_client: &SurrealDbClient) -> Result<(), FileError> {
|
||||||
|
let table = "file";
|
||||||
|
let primary_key = sha256;
|
||||||
|
|
||||||
|
let _created: Option<Record> = db_client
|
||||||
|
.client
|
||||||
|
.delete((table, primary_key))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
info!("Deleted FileInfo record with SHA256: {}", sha256);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sanitizes the file name to prevent security vulnerabilities like directory traversal.
|
/// Sanitizes the file name to prevent security vulnerabilities like directory traversal.
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ mod tests {
|
|||||||
|
|
||||||
let test_sha = "dummysha256hash".to_string();
|
let test_sha = "dummysha256hash".to_string();
|
||||||
let test_file_info = FileInfo {
|
let test_file_info = FileInfo {
|
||||||
uuid: Uuid::new_v4(),
|
uuid: Uuid::new_v4().to_string(),
|
||||||
sha256: test_sha.clone(),
|
sha256: test_sha.clone(),
|
||||||
path: "/path/to/file".to_string(),
|
path: "/path/to/file".to_string(),
|
||||||
mime_type: "text/plain".to_string(),
|
mime_type: "text/plain".to_string(),
|
||||||
@@ -222,7 +222,7 @@ mod tests {
|
|||||||
|
|
||||||
let test_sha = "dummysha256hash".to_string();
|
let test_sha = "dummysha256hash".to_string();
|
||||||
let test_file_info = FileInfo {
|
let test_file_info = FileInfo {
|
||||||
uuid: Uuid::new_v4(),
|
uuid: Uuid::new_v4().to_string(),
|
||||||
sha256: test_sha.clone(),
|
sha256: test_sha.clone(),
|
||||||
path: "/path/to/file".to_string(),
|
path: "/path/to/file".to_string(),
|
||||||
mime_type: "text/plain".to_string(),
|
mime_type: "text/plain".to_string(),
|
||||||
|
|||||||
@@ -32,8 +32,9 @@ pub async fn upload_handler(
|
|||||||
// Initialize a new RedisClient instance
|
// Initialize a new RedisClient instance
|
||||||
let redis_client = RedisClient::new("redis://127.0.0.1/");
|
let redis_client = RedisClient::new("redis://127.0.0.1/");
|
||||||
|
|
||||||
|
let database = SurrealDbClient::new().await.map_err(|e| FileError::PersistError(e.to_string())).unwrap();
|
||||||
// Process the file upload
|
// Process the file upload
|
||||||
let file_info = FileInfo::new(input.file, &redis_client).await?;
|
let file_info = FileInfo::new(input.file, &database).await?;
|
||||||
|
|
||||||
// Prepare the response JSON
|
// Prepare the response JSON
|
||||||
let response = json!({
|
let response = json!({
|
||||||
@@ -45,9 +46,6 @@ pub async fn upload_handler(
|
|||||||
|
|
||||||
info!("File uploaded successfully: {:?}", file_info);
|
info!("File uploaded successfully: {:?}", file_info);
|
||||||
|
|
||||||
let database = SurrealDbClient::new().await.map_err(|e| FileError::PersistError(e.to_string())).unwrap();
|
|
||||||
|
|
||||||
set_file_info(database.client, &file_info.sha256, file_info.clone()).await.unwrap();
|
|
||||||
|
|
||||||
// Return the response with HTTP 200
|
// Return the response with HTTP 200
|
||||||
Ok((axum::http::StatusCode::OK, Json(response)))
|
Ok((axum::http::StatusCode::OK, Json(response)))
|
||||||
|
|||||||
Reference in New Issue
Block a user