markdownfs / src /main.rs
Subramanya97's picture
Deploy from markdownfs@978f343
098a7fc
Raw
History Blame Contribute Delete
8.41 kB
use markdownfs::auth::session::Session;
use markdownfs::config::{CompatibilityTarget, Config};
use markdownfs::db::MarkdownDb;
use rustyline::error::ReadlineError;
use rustyline::DefaultEditor;
#[tokio::main]
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}"),
}
}