Spaces:
Running
Running
| use markdownfs::auth::session::Session; | |
| use markdownfs::config::{CompatibilityTarget, Config}; | |
| use markdownfs::db::MarkdownDb; | |
| use rustyline::error::ReadlineError; | |
| use rustyline::DefaultEditor; | |
| async fn main() { | |
| let config = Config::from_env(); | |
| let db = match MarkdownDb::open(config) { | |
| Ok(db) => { | |
| let commits = db.commit_count().await; | |
| let objects = db.object_count().await; | |
| if commits > 0 { | |
| println!( | |
| "markdownfs v{} — Loaded from disk ({commits} commits, {objects} objects)", | |
| env!("CARGO_PKG_VERSION") | |
| ); | |
| } else { | |
| println!( | |
| "markdownfs v{} — Markdown Virtual File System", | |
| env!("CARGO_PKG_VERSION") | |
| ); | |
| } | |
| db | |
| } | |
| Err(e) => { | |
| eprintln!("Warning: failed to load state: {e}"); | |
| eprintln!("Starting fresh.\n"); | |
| MarkdownDb::open_memory() | |
| } | |
| }; | |
| let _save_handle = db.spawn_auto_save(); | |
| let mut rl = DefaultEditor::new().expect("failed to initialize readline"); | |
| let history_path = std::env::var("HOME") | |
| .ok() | |
| .map(|h| format!("{h}/.markdownfs_history")); | |
| if let Some(ref path) = history_path { | |
| let _ = rl.load_history(path); | |
| } | |
| let mut session = login_flow(&db, &mut rl).await; | |
| println!("\nType 'help' for available commands, 'exit' to quit.\n"); | |
| loop { | |
| let pwd = db.pwd().await; | |
| let home_prefix = format!("/home/{}", session.username); | |
| let display_pwd = if pwd == home_prefix { | |
| "~".to_string() | |
| } else if let Some(rest) = pwd.strip_prefix(&home_prefix) { | |
| format!("~{rest}") | |
| } else { | |
| pwd | |
| }; | |
| let prompt = format!("{}@markdownfs:{display_pwd} $ ", session.username); | |
| match rl.readline(&prompt) { | |
| Ok(line) => { | |
| let line = line.trim(); | |
| if line.is_empty() { | |
| continue; | |
| } | |
| let _ = rl.add_history_entry(line); | |
| match line { | |
| "exit" | "quit" => break, | |
| _ if line.starts_with("edit ") => { | |
| handle_edit(line, &db, &mut rl, &session).await; | |
| } | |
| _ => match db.execute_command(line, &mut session).await { | |
| Ok(output) => { | |
| if !output.is_empty() { | |
| print!("{output}"); | |
| } | |
| } | |
| Err(e) => eprintln!("{e}"), | |
| }, | |
| } | |
| } | |
| Err(ReadlineError::Interrupted) => { | |
| println!("^C"); | |
| } | |
| Err(ReadlineError::Eof) => break, | |
| Err(e) => { | |
| eprintln!("readline error: {e}"); | |
| break; | |
| } | |
| } | |
| } | |
| match db.save().await { | |
| Ok(()) => { | |
| let info = db.persist_info(); | |
| match info.location { | |
| Some(dir) => println!("State saved via {} to {}/", info.backend, dir.display()), | |
| None => println!("State saved via {}", info.backend), | |
| } | |
| } | |
| Err(e) => eprintln!("Warning: failed to save state: {e}"), | |
| } | |
| if let Some(ref path) = history_path { | |
| let _ = rl.save_history(path); | |
| } | |
| println!("Goodbye!"); | |
| } | |
| async fn login_flow(db: &MarkdownDb, rl: &mut DefaultEditor) -> Session { | |
| let has_users = db.has_users().await; | |
| if !has_users { | |
| println!("\nWelcome! Let's set up your account."); | |
| loop { | |
| match rl.readline("Admin username: ") { | |
| Ok(name) => { | |
| let name = name.trim().to_string(); | |
| if name.is_empty() { | |
| eprintln!("Username cannot be empty."); | |
| continue; | |
| } | |
| match db.create_admin(&name).await { | |
| Ok(session) => { | |
| db.cd_to_home(&name).await; | |
| println!( | |
| "\nCreated admin '{}' (uid={}, groups=[{}, wheel])", | |
| session.username, session.uid, session.username | |
| ); | |
| println!("Home directory: /home/{}", session.username); | |
| return session; | |
| } | |
| Err(e) => eprintln!("{e}"), | |
| } | |
| } | |
| Err(ReadlineError::Interrupted | ReadlineError::Eof) => { | |
| return Session::root(); | |
| } | |
| Err(e) => { | |
| eprintln!("readline error: {e}"); | |
| return Session::root(); | |
| } | |
| } | |
| } | |
| } else { | |
| loop { | |
| match rl.readline("Login as: ") { | |
| Ok(name) => { | |
| let name = name.trim().to_string(); | |
| if name.is_empty() { | |
| continue; | |
| } | |
| match db.login(&name).await { | |
| Ok(session) => { | |
| db.cd_to_home(&name).await; | |
| println!( | |
| "Logged in as '{}' (uid={}, gid={})", | |
| session.username, session.uid, session.gid | |
| ); | |
| return session; | |
| } | |
| Err(_) => eprintln!("Unknown user: {name}"), | |
| } | |
| } | |
| Err(ReadlineError::Interrupted | ReadlineError::Eof) => { | |
| return Session::root(); | |
| } | |
| Err(e) => { | |
| eprintln!("readline error: {e}"); | |
| return Session::root(); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| async fn handle_edit( | |
| line: &str, | |
| db: &MarkdownDb, | |
| rl: &mut DefaultEditor, | |
| session: &Session, | |
| ) { | |
| let path = line.strip_prefix("edit ").unwrap().trim(); | |
| if path.is_empty() { | |
| eprintln!("markdownfs: edit: missing file path"); | |
| return; | |
| } | |
| if db.config().compatibility_target != CompatibilityTarget::Posix && !path.ends_with(".md") { | |
| eprintln!("markdownfs: only .md files are supported: '{path}'"); | |
| return; | |
| } | |
| if let Ok(content) = db.cat(path).await { | |
| let text = String::from_utf8_lossy(&content); | |
| if !text.is_empty() { | |
| println!("--- Current content of {path} ---"); | |
| for (i, line) in text.lines().enumerate() { | |
| println!("{:>4} | {line}", i + 1); | |
| } | |
| println!("--- End ---"); | |
| } | |
| } | |
| println!("Enter new content (type EOF on a blank line to finish, CANCEL to abort):"); | |
| let mut content = String::new(); | |
| let mut line_num = 1; | |
| loop { | |
| match rl.readline(&format!("{line_num:>4} | ")) { | |
| Ok(input) => { | |
| if input.trim() == "EOF" { | |
| break; | |
| } | |
| if input.trim() == "CANCEL" { | |
| println!("Edit cancelled."); | |
| return; | |
| } | |
| content.push_str(&input); | |
| content.push('\n'); | |
| line_num += 1; | |
| } | |
| Err(ReadlineError::Interrupted) => { | |
| println!("\nEdit cancelled."); | |
| return; | |
| } | |
| Err(ReadlineError::Eof) => break, | |
| Err(e) => { | |
| eprintln!("readline error: {e}"); | |
| return; | |
| } | |
| } | |
| } | |
| if content.ends_with('\n') { | |
| content.pop(); | |
| } | |
| if db.stat(path).await.is_err() { | |
| if let Err(e) = db.touch(path, session.uid, session.gid).await { | |
| eprintln!("{e}"); | |
| return; | |
| } | |
| } | |
| if let Err(e) = db.write_file(path, content.into_bytes()).await { | |
| eprintln!("{e}"); | |
| return; | |
| } | |
| match db.commit(&format!("edit {path}"), &session.username).await { | |
| Ok(hash) => println!("[{hash}] edit {path}"), | |
| Err(e) => eprintln!("auto-commit failed: {e}"), | |
| } | |
| } | |