big dev changes

This commit is contained in:
Mateusz Gruszczyński
2026-07-21 10:11:57 +02:00
parent 4bb92a343b
commit 0b4b1753a0
23 changed files with 690 additions and 51 deletions
+98 -5
View File
@@ -53,6 +53,10 @@ pub struct CreateNoteRequest {
name: String,
#[serde(default)]
password: Option<String>,
#[serde(default)]
protect: bool,
#[serde(default)]
created_by: Option<String>,
}
#[derive(Debug, Deserialize)]
@@ -84,6 +88,8 @@ pub struct NoteListItem {
created_at: String,
updated_at: String,
url: String,
protected: bool,
created_by: Option<String>,
}
#[derive(Debug, Serialize)]
@@ -93,6 +99,7 @@ pub struct NoteInfo {
slug: String,
title: String,
protected: bool,
note_protected: bool,
created_at: String,
updated_at: String,
}
@@ -141,6 +148,8 @@ pub async fn open_workspace(
title: note.title,
created_at: db::normalize_timestamp(&note.created_at),
updated_at: db::normalize_timestamp(&note.updated_at),
protected: note.protected,
created_by: note.created_by,
})
.collect();
@@ -163,7 +172,8 @@ pub async fn create_note(
}
let slug = unique_note_slug(&state, workspace.id, &base).await?;
let note = db::create_note(&state.db, workspace.id, &slug, title).await?;
let created_by = payload.created_by.as_deref().map(str::trim).filter(|v| !v.is_empty()).map(|v| v.chars().take(40).collect::<String>());
let note = db::create_note(&state.db, workspace.id, &slug, title, payload.protect, created_by.as_deref()).await?;
Ok((
StatusCode::CREATED,
Json(NoteListItem {
@@ -172,6 +182,8 @@ pub async fn create_note(
title: note.title,
created_at: db::normalize_timestamp(&note.created_at),
updated_at: db::normalize_timestamp(&note.updated_at),
protected: note.protected,
created_by: note.created_by,
}),
))
}
@@ -193,6 +205,7 @@ pub async fn note_info(
slug: note.slug,
title: note.title,
protected: workspace.password_hash.is_some(),
note_protected: note.protected,
created_at: db::normalize_timestamp(&note.created_at),
updated_at: db::normalize_timestamp(&note.updated_at),
}))
@@ -547,8 +560,30 @@ pub async fn upload_pad_file(
stored = format!("{stem}-{}{}", db::random_suffix(6), ext);
path = dir.join(&stored);
}
tokio::fs::write(&path, bytes).await.map_err(|_| ApiError::internal("Failed to save the file"))?;
Ok(Json(serde_json::json!({"name": stored, "url": format!("/f/{}/{}", file_token, stored)})))
tokio::fs::write(&path, &bytes).await.map_err(|_| ApiError::internal("Failed to save the file"))?;
let url = format!("/f/{}/{}", file_token, stored);
let mime = mime_guess::from_path(&stored).first_or_octet_stream().to_string();
db::register_pad_file(&state.db, pad.id, &stored, &url, &mime, bytes.len() as i64).await?;
Ok(Json(serde_json::json!({"name": stored, "url": url})))
}
pub async fn pad_files(
State(state): State<SharedState>,
Path(slug): Path<String>,
Json(payload): Json<PasswordRequest>,
) -> Result<Json<Vec<db::NoteFile>>, ApiError> {
let pad = authorized_pad(&state, &slug, payload.password.as_deref()).await?;
let mut files = db::list_pad_files(&state.db, pad.id).await?;
for file in &mut files {
let attached = pad.content.contains(&file.url);
if attached != file.is_attached {
db::set_pad_file_attached(&state.db, file.id, attached).await?;
file.is_attached = attached;
file.detached_at = if attached { None } else { Some(chrono::Utc::now().to_rfc3339()) };
}
file.created_at = db::normalize_timestamp(&file.created_at);
}
Ok(Json(files))
}
pub async fn upload_note_file(
@@ -586,11 +621,69 @@ pub async fn upload_note_file(
stored = format!("{stem}-{}{}", db::random_suffix(6), ext);
path = dir.join(&stored);
}
tokio::fs::write(&path, bytes).await.map_err(|_| ApiError::internal("Failed to save the file"))?;
Ok(Json(serde_json::json!({"name": stored, "url": format!("/f/{}/{}", file_token, stored)})))
tokio::fs::write(&path, &bytes).await.map_err(|_| ApiError::internal("Failed to save the file"))?;
let url = format!("/f/{}/{}", file_token, stored);
let mime = mime_guess::from_path(&stored).first_or_octet_stream().to_string();
db::register_note_file(&state.db, note.id, &stored, &url, &mime, bytes.len() as i64).await?;
Ok(Json(serde_json::json!({"name": stored, "url": url})))
}
pub async fn delete_note(
State(state): State<SharedState>,
Path((workspace_slug, note_slug)): Path<(String, String)>,
Json(payload): Json<PasswordRequest>,
) -> Result<Json<serde_json::Value>, ApiError> {
let (_workspace, note) = authorized_note(&state, &workspace_slug, &note_slug, payload.password.as_deref()).await?;
if note.protected { return Err(ApiError::bad_request("This note is protected and cannot be deleted")); }
db::delete_note(&state.db, note.id).await?;
Ok(Json(serde_json::json!({"ok": true})))
}
pub async fn note_files(
State(state): State<SharedState>,
Path((workspace_slug, note_slug)): Path<(String, String)>,
Json(payload): Json<PasswordRequest>,
) -> Result<Json<Vec<db::NoteFile>>, ApiError> {
let (_workspace, note) = authorized_note(&state, &workspace_slug, &note_slug, payload.password.as_deref()).await?;
let mut files = db::list_note_files(&state.db, note.id).await?;
for file in &mut files {
let attached = note.content.contains(&file.url);
if attached != file.is_attached {
db::set_note_file_attached(&state.db, file.id, attached).await?;
file.is_attached = attached;
file.detached_at = if attached { None } else { Some(chrono::Utc::now().to_rfc3339()) };
}
file.created_at = db::normalize_timestamp(&file.created_at);
}
Ok(Json(files))
}
pub async fn delete_note_file(
State(state): State<SharedState>,
Path((workspace_slug, note_slug, file_id)): Path<(String, String, i64)>,
Json(payload): Json<PasswordRequest>,
) -> Result<Json<serde_json::Value>, ApiError> {
let (workspace, note) = authorized_note(&state, &workspace_slug, &note_slug, payload.password.as_deref()).await?;
if workspace.password_hash.is_none() || payload.password.as_deref().unwrap_or_default().is_empty() {
return Err(ApiError::unauthorized());
}
let file = db::find_note_file(&state.db, note.id, file_id).await?
.ok_or_else(ApiError::not_found_file)?;
let relative = file.url.trim_start_matches('/').split('/').collect::<Vec<_>>();
if relative.len() == 3 && relative[0] == "f" {
let directory = format!("{}_{}", note.id, relative[1]);
let path = std::path::Path::new(&state.files_dir).join("notes").join(directory).join(sanitize_filename(relative[2]));
match tokio::fs::remove_file(&path).await {
Ok(()) => {},
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {},
Err(_) => return Err(ApiError::internal("Failed to delete the file")),
}
}
db::delete_note_file(&state.db, note.id, file_id).await?;
Ok(Json(serde_json::json!({"ok": true})))
}
pub async fn download_file(
State(state): State<SharedState>,
Path((token, filename)): Path<(String, String)>,