274 lines
9 KiB
Rust
274 lines
9 KiB
Rust
use crate::drive::s3_repository::S3Repository;
|
|
use chrono::{DateTime, Utc};
|
|
use std::sync::Arc;
|
|
|
|
use crate::core::shared::state::AppState;
|
|
|
|
use super::models::{Document, DocumentMetadata};
|
|
|
|
fn get_user_papers_path(user_identifier: &str) -> String {
|
|
let safe_id = user_identifier
|
|
.replace(['/', '\\', ':', '*', '?', '"', '<', '>', '|'], "_")
|
|
.to_lowercase();
|
|
format!("users/{}/papers", safe_id)
|
|
}
|
|
|
|
pub async fn save_document_to_drive(
|
|
state: &Arc<AppState>,
|
|
user_identifier: &str,
|
|
doc_id: &str,
|
|
title: &str,
|
|
content: &str,
|
|
is_named: bool,
|
|
) -> Result<String, String> {
|
|
let s3_client = state.drive.as_ref().ok_or("S3 service not available")?;
|
|
|
|
let base_path = get_user_papers_path(user_identifier);
|
|
let storage_type = if is_named { "named" } else { "current" };
|
|
|
|
let (doc_path, metadata_path) = if is_named {
|
|
let safe_title = title
|
|
.replace(['/', '\\', ':', '*', '?', '"', '<', '>', '|'], "_")
|
|
.to_lowercase()
|
|
.chars()
|
|
.take(50)
|
|
.collect::<String>();
|
|
(
|
|
format!("{}/{}/{}/document.md", base_path, storage_type, safe_title),
|
|
Some(format!(
|
|
"{}/{}/{}/metadata.json",
|
|
base_path, storage_type, safe_title
|
|
)),
|
|
)
|
|
} else {
|
|
(
|
|
format!("{}/{}/{}.md", base_path, storage_type, doc_id),
|
|
None,
|
|
)
|
|
};
|
|
|
|
s3_client
|
|
.put_object(
|
|
&state.bucket_name,
|
|
&doc_path,
|
|
content.as_bytes().to_vec(),
|
|
Some("text/markdown"),
|
|
)
|
|
.await
|
|
.map_err(|e| format!("Failed to save document: {}", e))?;
|
|
|
|
if let Some(meta_path) = metadata_path {
|
|
let metadata = serde_json::json!({
|
|
"id": doc_id,
|
|
"title": title,
|
|
"created_at": Utc::now().to_rfc3339(),
|
|
"updated_at": Utc::now().to_rfc3339(),
|
|
"word_count": content.split_whitespace().count()
|
|
});
|
|
|
|
s3_client
|
|
.put_object(
|
|
&state.bucket_name,
|
|
&meta_path,
|
|
metadata.to_string().into_bytes(),
|
|
Some("application/json"),
|
|
)
|
|
.await
|
|
.map_err(|e| format!("Failed to save metadata: {}", e))?;
|
|
}
|
|
|
|
Ok(doc_path)
|
|
}
|
|
|
|
pub async fn load_document_from_drive(
|
|
state: &Arc<AppState>,
|
|
user_identifier: &str,
|
|
doc_id: &str,
|
|
) -> Result<Option<Document>, String> {
|
|
let s3_client = state.drive.as_ref().ok_or("S3 service not available")?;
|
|
|
|
let base_path = get_user_papers_path(user_identifier);
|
|
|
|
let current_path = format!("{}/current/{}.md", base_path, doc_id);
|
|
|
|
if let Ok(bytes) = s3_client
|
|
.get_object(&state.bucket_name, ¤t_path)
|
|
.await
|
|
{
|
|
let content = String::from_utf8(bytes).map_err(|e| e.to_string())?;
|
|
|
|
let title = content
|
|
.lines()
|
|
.next()
|
|
.map(|l| l.trim_start_matches('#').trim())
|
|
.unwrap_or("Untitled")
|
|
.to_string();
|
|
|
|
return Ok(Some(Document {
|
|
id: doc_id.to_string(),
|
|
title,
|
|
content,
|
|
owner_id: user_identifier.to_string(),
|
|
storage_path: current_path,
|
|
created_at: Utc::now(),
|
|
updated_at: Utc::now(),
|
|
}));
|
|
}
|
|
|
|
Ok(None)
|
|
}
|
|
|
|
pub async fn list_documents_from_drive(
|
|
state: &Arc<AppState>,
|
|
user_identifier: &str,
|
|
) -> Result<Vec<DocumentMetadata>, String> {
|
|
let s3_client = state.drive.as_ref().ok_or("S3 service not available")?;
|
|
|
|
let base_path = get_user_papers_path(user_identifier);
|
|
let mut documents = Vec::new();
|
|
|
|
let current_prefix = format!("{}/current/", base_path);
|
|
if let Ok(result) = s3_client
|
|
.list_objects_v2()
|
|
.bucket(&state.bucket_name)
|
|
.prefix(¤t_prefix)
|
|
.send()
|
|
.await
|
|
{
|
|
for obj in result.contents() {
|
|
if let Some(key) = obj.key() {
|
|
if key.to_lowercase().ends_with(".md") {
|
|
let id = key
|
|
.trim_start_matches(¤t_prefix)
|
|
.trim_end_matches(".md")
|
|
.to_string();
|
|
|
|
documents.push(DocumentMetadata {
|
|
id: id.clone(),
|
|
title: format!("Untitled ({})", &id[..8.min(id.len())]),
|
|
owner_id: user_identifier.to_string(),
|
|
created_at: Utc::now(),
|
|
updated_at: obj
|
|
.last_modified()
|
|
.map(|t| {
|
|
DateTime::from_timestamp(t.secs(), t.subsec_nanos())
|
|
.unwrap_or_else(Utc::now)
|
|
})
|
|
.unwrap_or_else(Utc::now),
|
|
word_count: 0,
|
|
storage_type: "current".to_string(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let named_prefix = format!("{}/named/", base_path);
|
|
if let Ok(result) = s3_client
|
|
.list_objects_v2()
|
|
.bucket(&state.bucket_name)
|
|
.prefix(&named_prefix)
|
|
.delimiter("/")
|
|
.send()
|
|
.await
|
|
{
|
|
for prefix in result.common_prefixes() {
|
|
if let Some(folder) = prefix.prefix() {
|
|
let folder_name = folder
|
|
.trim_start_matches(&named_prefix)
|
|
.trim_end_matches('/');
|
|
|
|
let meta_key = format!("{}metadata.json", folder);
|
|
if let Ok(meta_result) = s3_client
|
|
.get_object()
|
|
.bucket(&state.bucket_name)
|
|
.key(&meta_key)
|
|
.send()
|
|
.await
|
|
{
|
|
if let Ok(bytes) = meta_result.body.collect().await {
|
|
if let Ok(meta_str) = String::from_utf8(bytes.into_bytes().to_vec()) {
|
|
if let Ok(meta) = serde_json::from_str::<serde_json::Value>(&meta_str) {
|
|
documents.push(DocumentMetadata {
|
|
id: meta["id"].as_str().unwrap_or(folder_name).to_string(),
|
|
title: meta["title"]
|
|
.as_str()
|
|
.unwrap_or(folder_name)
|
|
.to_string(),
|
|
owner_id: user_identifier.to_string(),
|
|
created_at: meta["created_at"]
|
|
.as_str()
|
|
.and_then(|s| DateTime::parse_from_rfc3339(s).ok())
|
|
.map(|d| d.with_timezone(&Utc))
|
|
.unwrap_or_else(Utc::now),
|
|
updated_at: meta["updated_at"]
|
|
.as_str()
|
|
.and_then(|s| DateTime::parse_from_rfc3339(s).ok())
|
|
.map(|d| d.with_timezone(&Utc))
|
|
.unwrap_or_else(Utc::now),
|
|
word_count: meta["word_count"].as_u64().unwrap_or(0) as usize,
|
|
storage_type: "named".to_string(),
|
|
});
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
documents.push(DocumentMetadata {
|
|
id: folder_name.to_string(),
|
|
title: folder_name.to_string(),
|
|
owner_id: user_identifier.to_string(),
|
|
created_at: Utc::now(),
|
|
updated_at: Utc::now(),
|
|
word_count: 0,
|
|
storage_type: "named".to_string(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
documents.sort_by(|a, b| b.updated_at.cmp(&a.updated_at));
|
|
|
|
Ok(documents)
|
|
}
|
|
|
|
pub async fn delete_document_from_drive(
|
|
state: &Arc<AppState>,
|
|
user_identifier: &str,
|
|
doc_id: &str,
|
|
) -> Result<(), String> {
|
|
let s3_client = state.drive.as_ref().ok_or("S3 service not available")?;
|
|
|
|
let base_path = get_user_papers_path(user_identifier);
|
|
|
|
let current_path = format!("{}/current/{}.md", base_path, doc_id);
|
|
let _ = s3_client
|
|
.delete_object()
|
|
.bucket(&state.bucket_name)
|
|
.key(¤t_path)
|
|
.send()
|
|
.await;
|
|
|
|
let named_prefix = format!("{}/named/{}/", base_path, doc_id);
|
|
if let Ok(result) = s3_client
|
|
.list_objects_v2()
|
|
.bucket(&state.bucket_name)
|
|
.prefix(&named_prefix)
|
|
.send()
|
|
.await
|
|
{
|
|
for obj in result.contents() {
|
|
if let Some(key) = obj.key() {
|
|
let _ = s3_client
|
|
.delete_object()
|
|
.bucket(&state.bucket_name)
|
|
.key(key)
|
|
.send()
|
|
.await;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|