vault-os/crates/vault-api/src/routes/files.rs
Harald Hoyer f820a72b04 Initial implementation of vault-os
Complete implementation across all 13 phases:

- vault-core: types, YAML frontmatter parsing, entity classification,
  filesystem ops, config, prompt composition, validation, search
- vault-watch: filesystem watcher with daemon write filtering, event
  classification
- vault-scheduler: cron engine, process executor, task runner with
  retry logic and concurrency limiting
- vault-api: Axum REST API (15 route modules), WebSocket with broadcast,
  AI assistant proxy, validation, templates
- Dashboard: React + TypeScript + Tailwind v4 with kanban, CodeMirror
  editor, dynamic view system, AI chat sidebar
- Nix flake with dev shell and NixOS module
- Graceful shutdown, inotify overflow recovery, tracing instrumentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 01:21:17 +01:00

126 lines
3.7 KiB
Rust

use crate::error::ApiError;
use crate::state::AppState;
use axum::extract::{Path, State};
use axum::routing::get;
use axum::{Json, Router};
use serde::Deserialize;
use serde_json::{json, Value};
use std::sync::Arc;
pub fn routes() -> Router<Arc<AppState>> {
Router::new()
.route("/files/{*path}", get(read_file).put(write_file).patch(patch_file).delete(delete_file))
}
async fn read_file(
State(state): State<Arc<AppState>>,
Path(path): Path<String>,
) -> Result<Json<Value>, ApiError> {
let file_path = state.vault_root.join(&path);
if !file_path.exists() {
return Err(ApiError::NotFound(format!("File '{}' not found", path)));
}
let content = std::fs::read_to_string(&file_path)
.map_err(|e| ApiError::Vault(vault_core::VaultError::io(e, &file_path)))?;
// Try to split frontmatter
if let Ok((yaml, body)) = vault_core::frontmatter::split_frontmatter(&content) {
let frontmatter: Value = serde_yaml::from_str(yaml).unwrap_or(Value::Null);
Ok(Json(json!({
"path": path,
"frontmatter": frontmatter,
"body": body,
})))
} else {
Ok(Json(json!({
"path": path,
"frontmatter": null,
"body": content,
})))
}
}
#[derive(Deserialize)]
struct WriteFileBody {
#[serde(default)]
frontmatter: Option<Value>,
#[serde(default)]
body: Option<String>,
#[serde(default)]
raw: Option<String>,
}
async fn write_file(
State(state): State<Arc<AppState>>,
Path(path): Path<String>,
Json(data): Json<WriteFileBody>,
) -> Result<Json<Value>, ApiError> {
let file_path = state.vault_root.join(&path);
if let Some(parent) = file_path.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| ApiError::Vault(vault_core::VaultError::io(e, parent)))?;
}
let content = if let Some(raw) = data.raw {
raw
} else {
let body = data.body.unwrap_or_default();
if let Some(fm) = data.frontmatter {
let yaml = serde_yaml::to_string(&fm)
.map_err(|e| ApiError::Internal(e.to_string()))?;
format!("---\n{}---\n{}", yaml, body)
} else {
body
}
};
state.write_filter.register(file_path.clone());
std::fs::write(&file_path, content)
.map_err(|e| ApiError::Vault(vault_core::VaultError::io(e, &file_path)))?;
Ok(Json(json!({ "status": "written", "path": path })))
}
async fn patch_file(
State(state): State<Arc<AppState>>,
Path(path): Path<String>,
Json(updates): Json<Value>,
) -> Result<Json<Value>, ApiError> {
let file_path = state.vault_root.join(&path);
if !file_path.exists() {
return Err(ApiError::NotFound(format!("File '{}' not found", path)));
}
let content = std::fs::read_to_string(&file_path)
.map_err(|e| ApiError::Vault(vault_core::VaultError::io(e, &file_path)))?;
let updated =
vault_core::frontmatter::update_frontmatter_fields(&content, &file_path, &updates)
.map_err(ApiError::Vault)?;
state.write_filter.register(file_path.clone());
std::fs::write(&file_path, updated)
.map_err(|e| ApiError::Vault(vault_core::VaultError::io(e, &file_path)))?;
Ok(Json(json!({ "status": "patched", "path": path })))
}
async fn delete_file(
State(state): State<Arc<AppState>>,
Path(path): Path<String>,
) -> Result<Json<Value>, ApiError> {
let file_path = state.vault_root.join(&path);
if !file_path.exists() {
return Err(ApiError::NotFound(format!("File '{}' not found", path)));
}
std::fs::remove_file(&file_path)
.map_err(|e| ApiError::Vault(vault_core::VaultError::io(e, &file_path)))?;
Ok(Json(json!({ "status": "deleted", "path": path })))
}