diff --git a/jupiter/callisto/src/mod.rs b/jupiter/callisto/src/mod.rs index 4129d80c5..8fd7b8cb2 100644 --- a/jupiter/callisto/src/mod.rs +++ b/jupiter/callisto/src/mod.rs @@ -1,6 +1,6 @@ //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.14 -pub mod entity_ext; +pub mod entity_ext; pub mod prelude; pub mod access_token; diff --git a/jupiter/src/migration/m20250725_103004_add_note.rs b/jupiter/src/migration/m20250725_103004_add_note.rs index d30029c9a..4f07a7586 100644 --- a/jupiter/src/migration/m20250725_103004_add_note.rs +++ b/jupiter/src/migration/m20250725_103004_add_note.rs @@ -73,14 +73,23 @@ impl MigrationTrait for Migration { .col(ColumnDef::new(Notes::OriginalProjectId).big_unsigned()) .col(ColumnDef::new(Notes::OriginalPostId).big_unsigned()) .col(ColumnDef::new(Notes::OriginalDigestId).big_unsigned()) - .col(ColumnDef::new(Notes::Visibility).integer().not_null().default(0)) + .col( + ColumnDef::new(Notes::Visibility) + .integer() + .not_null() + .default(0), + ) .col( ColumnDef::new(Notes::NonMemberViewsCount) .integer() .not_null() .default(0), ) - .col(ColumnDef::new(Notes::ResolvedCommentsCount).integer().default(0_i32)) + .col( + ColumnDef::new(Notes::ResolvedCommentsCount) + .integer() + .default(0_i32), + ) .col(ColumnDef::new(Notes::ProjectId).big_unsigned()) .col(ColumnDef::new(Notes::LastActivityAt).date_time()) .col(ColumnDef::new(Notes::ContentUpdatedAt).date_time()) diff --git a/jupiter/src/storage/note_storage.rs b/jupiter/src/storage/note_storage.rs index 2022d6385..6bd95b4cd 100644 --- a/jupiter/src/storage/note_storage.rs +++ b/jupiter/src/storage/note_storage.rs @@ -48,9 +48,9 @@ impl NoteStorage { let save_note = note_active_model.insert(self.get_connection()).await; match save_note { Ok(model) => Ok(Some(model)), - Err(e) => Err(MegaError::with_message(format!( - "Failed to save note: {e}", - ))), + Err(e) => Err(MegaError::with_message( + format!("Failed to save note: {e}",), + )), } } diff --git a/libra/src/command/add.rs b/libra/src/command/add.rs index 2b76c5add..c286b65f3 100644 --- a/libra/src/command/add.rs +++ b/libra/src/command/add.rs @@ -1,6 +1,6 @@ use crate::command::status; use crate::utils::object_ext::BlobExt; -use clap::{Parser}; +use clap::Parser; use mercury::internal::index::{Index, IndexEntry}; use mercury::internal::object::blob::Blob; use std::path::{Path, PathBuf}; @@ -80,7 +80,9 @@ pub async fn execute(args: AddArgs) { .modified .into_iter() .filter(|p| { - let s = p.to_str().unwrap_or_else(|| { panic!("path {:?} is not valid UTF-8", p.display()) }); + let s = p + .to_str() + .unwrap_or_else(|| panic!("path {:?} is not valid UTF-8", p.display())); index.tracked(s, 0) }) .collect(); diff --git a/libra/src/command/remove.rs b/libra/src/command/remove.rs index 4d954090a..e3a06f2be 100644 --- a/libra/src/command/remove.rs +++ b/libra/src/command/remove.rs @@ -31,12 +31,12 @@ pub fn execute(args: RemoveArgs) -> Result<(), GitError> { } let idx_file = path::index(); let mut index = Index::load(&idx_file)?; - + // check if pathspec is all in index (skip if force is enabled) if !args.force && !validate_pathspec(&args.pathspec, &index) { return Ok(()); } - + let dirs = get_dirs(&args.pathspec, &index, args.force); if !dirs.is_empty() && !args.recursive { println!( @@ -49,7 +49,7 @@ pub fn execute(args: RemoveArgs) -> Result<(), GitError> { for path_str in args.pathspec.iter() { let path = PathBuf::from(path_str); let path_wd = path.to_workdir().to_string_or_panic(); - + if dirs.contains(path_str) { // dir let removed = index.remove_dir_files(&path_wd); @@ -75,7 +75,7 @@ pub fn execute(args: RemoveArgs) -> Result<(), GitError> { index.remove(&path_wd, 0); println!("rm '{}'", path_wd.bright_green()); } - + if !args.cached { fs::remove_file(&path)?; } @@ -113,7 +113,7 @@ fn get_dirs(pathspec: &[String], index: &Index, force: bool) -> Vec { for path_str in pathspec.iter() { let path = PathBuf::from(path_str); let path_wd = path.to_workdir().to_string_or_panic(); - + if force { // In force mode, check if the path exists and is a directory if path.exists() && path.is_dir() { diff --git a/libra/tests/command/fetch_test.rs b/libra/tests/command/fetch_test.rs index 5cb255bef..f8b9cfda2 100644 --- a/libra/tests/command/fetch_test.rs +++ b/libra/tests/command/fetch_test.rs @@ -10,7 +10,10 @@ fn init_temp_repo() -> TempDir { let temp_path = temp_dir.path(); eprintln!("Temporary directory created at: {temp_path:?}"); - assert!(temp_path.is_dir(), "Temporary path is not a valid directory"); + assert!( + temp_path.is_dir(), + "Temporary path is not a valid directory" + ); let output = Command::new(env!("CARGO_BIN_EXE_libra")) .current_dir(temp_path) @@ -41,7 +44,12 @@ async fn test_fetch_invalid_remote() { eprintln!("Adding invalid remote: https://invalid-url.example/repo.git"); let remote_output = TokioCommand::new(env!("CARGO_BIN_EXE_libra")) .current_dir(temp_path) - .args(["remote", "add", "origin", "https://invalid-url.example/repo.git"]) + .args([ + "remote", + "add", + "origin", + "https://invalid-url.example/repo.git", + ]) .output() .await .expect("Failed to add remote"); @@ -85,7 +93,6 @@ async fn test_fetch_invalid_remote() { } // Command completed within timeout Ok(Ok(output)) => { - eprintln!("Fetch completed (status: {:?})", output.status); assert!( !output.status.success(), @@ -101,7 +108,6 @@ async fn test_fetch_invalid_remote() { } // Failed to start the command Ok(Err(e)) => { - panic!("Failed to run 'libra fetch' command: {e}"); } } diff --git a/libra/tests/command/lfs_test.rs b/libra/tests/command/lfs_test.rs index 2e61d1bc1..49cfa9fde 100644 --- a/libra/tests/command/lfs_test.rs +++ b/libra/tests/command/lfs_test.rs @@ -9,7 +9,10 @@ fn init_temp_repo() -> TempDir { // Variables can be used directly in the `format!` string // FIX: Removed {:?} and added variable directly with formatting println!("Temporary directory created at: {temp_path:?}"); - assert!(temp_path.is_dir(), "Temporary path is not a valid directory"); + assert!( + temp_path.is_dir(), + "Temporary path is not a valid directory" + ); // Using env!("CARGO_BIN_EXE_libra") to get the path to the libra executable let output = Command::new(env!("CARGO_BIN_EXE_libra")) diff --git a/libra/tests/command/merge_test.rs b/libra/tests/command/merge_test.rs index 22564decb..997e17491 100644 --- a/libra/tests/command/merge_test.rs +++ b/libra/tests/command/merge_test.rs @@ -7,7 +7,10 @@ fn init_temp_repo() -> TempDir { let temp_path = temp_dir.path(); println!("Temporary directory created at: {temp_path:?}"); - assert!(temp_path.is_dir(), "Temporary path is not a valid directory"); + assert!( + temp_path.is_dir(), + "Temporary path is not a valid directory" + ); let output = Command::new(env!("CARGO_BIN_EXE_libra")) .current_dir(temp_path) @@ -38,7 +41,7 @@ async fn test_merge_fast_forward() { .args(["branch", "feature"]) .output() .expect("Failed to create branch"); - + Command::new(env!("CARGO_BIN_EXE_libra")) .current_dir(temp_path) .args(["checkout", "feature"]) @@ -62,7 +65,7 @@ async fn test_merge_fast_forward() { .expect("Failed to commit"); // Switch back to the main branch and perform fast-forward merge - + Command::new(env!("CARGO_BIN_EXE_libra")) .current_dir(temp_path) .args(["checkout", "main"]) @@ -81,7 +84,6 @@ async fn test_merge_fast_forward() { ); } - #[tokio::test] /// Test merging a remote branch async fn test_merge_remote_branch() { @@ -110,7 +112,6 @@ async fn test_merge_remote_branch() { ); } - #[tokio::test] /// Test merging branches with no common ancestor async fn test_merge_no_common_ancestor() { @@ -184,4 +185,3 @@ async fn test_merge_no_common_ancestor() { String::from_utf8_lossy(&merge_output.stderr) ); } - diff --git a/libra/tests/command/remove_test.rs b/libra/tests/command/remove_test.rs index c1649165e..412720728 100644 --- a/libra/tests/command/remove_test.rs +++ b/libra/tests/command/remove_test.rs @@ -1,15 +1,15 @@ use libra::command::{add, commit, init, remove}; use libra::utils::test; use serial_test::serial; -use tempfile::tempdir; use std::path::Path; +use tempfile::tempdir; async fn test_remove_file() { println!("\n\x1b[1mTest remove file functionality.\x1b[0m"); // Create a test file test::ensure_file("test_file.txt", Some("test content")); - + // Add file to index let add_args = add::AddArgs { all: false, @@ -40,7 +40,7 @@ async fn test_remove_cached_file() { // Create a test file test::ensure_file("cached_file.txt", Some("cached content")); - + // Add file to index let add_args = add::AddArgs { all: false, @@ -73,7 +73,7 @@ async fn test_remove_directory() { test::ensure_file("test_dir/file1.txt", Some("file1 content")); test::ensure_file("test_dir/file2.txt", Some("file2 content")); test::ensure_file("test_dir/subdir/file3.txt", Some("file3 content")); - + // Add all files to index let add_args = add::AddArgs { all: true, @@ -105,7 +105,7 @@ async fn test_remove_directory_without_recursive() { // Create directory structure test::ensure_file("test_dir2/file1.txt", Some("file1 content")); test::ensure_file("test_dir2/file2.txt", Some("file2 content")); - + // Add all files to index let add_args = add::AddArgs { all: true, @@ -197,7 +197,7 @@ async fn test_remove_multiple_files() { test::ensure_file("file1.txt", Some("content1")); test::ensure_file("file2.txt", Some("content2")); test::ensure_file("file3.txt", Some("content3")); - + // Add files to index let add_args = add::AddArgs { all: true, @@ -212,7 +212,11 @@ async fn test_remove_multiple_files() { // Remove multiple files let remove_args = remove::RemoveArgs { - pathspec: vec!["file1.txt".to_string(), "file2.txt".to_string(), "file3.txt".to_string()], + pathspec: vec![ + "file1.txt".to_string(), + "file2.txt".to_string(), + "file3.txt".to_string(), + ], cached: false, recursive: false, force: false, @@ -283,7 +287,9 @@ async fn test_remove_command() { repo_directory: temp_path.path().to_str().unwrap().to_string(), quiet: false, }; - init::init(init_args).await.expect("Error initializing repository"); + init::init(init_args) + .await + .expect("Error initializing repository"); // Create initial commit let commit_args = commit::CommitArgs { diff --git a/libra/tests/command/revert_test.rs b/libra/tests/command/revert_test.rs index 26185cc8e..c2dc9fc35 100644 --- a/libra/tests/command/revert_test.rs +++ b/libra/tests/command/revert_test.rs @@ -30,7 +30,7 @@ async fn test_basic_revert() { verbose: false, dry_run: false, ignore_errors: false, - refresh : false, + refresh: false, }) .await; commit::execute(CommitArgs { diff --git a/mercury/src/internal/index.rs b/mercury/src/internal/index.rs index 1d30621c7..1f4b62068 100644 --- a/mercury/src/internal/index.rs +++ b/mercury/src/internal/index.rs @@ -342,15 +342,24 @@ impl Index { pub fn refresh(&mut self, file: impl AsRef, workdir: &Path) -> Result { let path = file.as_ref(); - let name = path.to_str() + let name = path + .to_str() .ok_or(GitError::InvalidPathError(format!("{path:?}")))?; if let Some(entry) = self.entries.get_mut(&(name.to_string(), 0)) { let abs_path = workdir.join(path); let meta = fs::symlink_metadata(&abs_path)?; // Try creation time; on error, warn and use modification time (or now) - let new_ctime = Time::from_system_time(Self::time_or_now("creation time", &abs_path, meta.created())); - let new_mtime = Time::from_system_time(Self::time_or_now("modification time", &abs_path, meta.modified())); + let new_ctime = Time::from_system_time(Self::time_or_now( + "creation time", + &abs_path, + meta.created(), + )); + let new_mtime = Time::from_system_time(Self::time_or_now( + "modification time", + &abs_path, + meta.modified(), + )); let new_size = meta.len() as u32; // re-calculate SHA1 @@ -358,17 +367,17 @@ impl Index { let mut hasher = Sha1::new(); io::copy(&mut file, &mut hasher)?; let new_hash = SHA1::from_bytes(&hasher.finalize()); - + // refresh index if entry.ctime != new_ctime || entry.mtime != new_mtime - || entry.size != new_size - || entry.hash != new_hash + || entry.size != new_size + || entry.hash != new_hash { entry.ctime = new_ctime; entry.mtime = new_mtime; - entry.size = new_size; - entry.hash = new_hash; + entry.size = new_size; + entry.hash = new_hash; return Ok(true); } } @@ -376,15 +385,15 @@ impl Index { } /// Try to get a timestamp, logging on error, and finally falling back to now. - fn time_or_now( - what: &str, - path: &Path, - res: io::Result, - ) -> SystemTime { + fn time_or_now(what: &str, path: &Path, res: io::Result) -> SystemTime { match res { Ok(ts) => ts, Err(e) => { - eprintln!("warning: failed to get {what} for {path:?}: {e}; using SystemTime::now()", what = what, path = path.display()); + eprintln!( + "warning: failed to get {what} for {path:?}: {e}; using SystemTime::now()", + what = what, + path = path.display() + ); SystemTime::now() } } diff --git a/mono/src/api/api_router.rs b/mono/src/api/api_router.rs index 2ffcbd053..231f1aa2c 100644 --- a/mono/src/api/api_router.rs +++ b/mono/src/api/api_router.rs @@ -20,7 +20,8 @@ use common::model::CommonResult; use utoipa_axum::{router::OpenApiRouter, routes}; use crate::api::{ - conversation::conv_router, issue::issue_router, label::label_router, mr::mr_router, notes::note_router, user::user_router, MonoApiServiceState + conversation::conv_router, issue::issue_router, label::label_router, mr::mr_router, + notes::note_router, user::user_router, MonoApiServiceState, }; use crate::{api::error::ApiError, server::https_server::GIT_TAG}; diff --git a/mono/src/api/mod.rs b/mono/src/api/mod.rs index 66ddddfd8..e048c5094 100644 --- a/mono/src/api/mod.rs +++ b/mono/src/api/mod.rs @@ -9,6 +9,7 @@ use oauth2::{ }; use std::path::Path; +use crate::api::oauth::campsite_store::CampsiteApiStore; use ceres::{ api_service::{ import_api_service::ImportApiService, mono_api_service::MonoApiService, ApiHandler, @@ -16,12 +17,11 @@ use ceres::{ protocol::repo::Repo, }; use common::errors::ProtocolError; +use jupiter::storage::note_storage::NoteStorage; use jupiter::storage::{ conversation_storage::ConversationStorage, issue_storage::IssueStorage, mr_storage::MrStorage, user_storage::UserStorage, Storage, }; -use jupiter::storage::note_storage::NoteStorage; -use crate::api::oauth::campsite_store::CampsiteApiStore; pub mod api_common; pub mod api_router; @@ -31,8 +31,8 @@ pub mod issue; pub mod label; pub mod lfs; pub mod mr; -pub mod oauth; pub mod notes; +pub mod oauth; pub mod user; pub type GithubClient< diff --git a/orion/.env b/orion/.env index 984e59819..ad2700e68 100644 --- a/orion/.env +++ b/orion/.env @@ -1,2 +1,4 @@ -BUCK_PROJECT_ROOT="/home/bean/projects/buck2" -SERVER_WS="ws://localhost:8004/ws" \ No newline at end of file +BUCK_PROJECT_ROOT="/tmp/megadir/mount" +SERVER_WS="ws://orion.gitmega.com/ws" +SELECT_TASK_COUNT="30" +INITIAL_POLL_INTERVAL_SECS="2" \ No newline at end of file diff --git a/orion/Cargo.toml b/orion/Cargo.toml index 05baaa35b..0775c4b46 100644 --- a/orion/Cargo.toml +++ b/orion/Cargo.toml @@ -17,4 +17,4 @@ dotenvy = { workspace = true } tokio-tungstenite = { workspace = true } tungstenite = { workspace = true } serde_json = { workspace = true } -reqwest.workspace = true +reqwest = { workspace = true, features = ["json"] } diff --git a/orion/src/buck_controller.rs b/orion/src/buck_controller.rs index a3d02dfe5..e6bf003e9 100644 --- a/orion/src/buck_controller.rs +++ b/orion/src/buck_controller.rs @@ -1,46 +1,108 @@ use crate::ws::WSMessage; use once_cell::sync::Lazy; -use serde_json::json; +use serde_json::{json, Value}; use std::io; use std::process::{ExitStatus, Stdio}; use tokio::io::AsyncBufReadExt; use tokio::process::Command; use tokio::sync::mpsc::UnboundedSender; +use tokio::time::{sleep, Duration}; static PROJECT_ROOT: Lazy = Lazy::new(|| std::env::var("BUCK_PROJECT_ROOT").expect("BUCK_PROJECT_ROOT must be set")); -/// Sends a filesystem mount request to the specified API endpoint +/// Sends a filesystem mount request to the specified API endpoint and waits for completion /// Parameters: /// - repo: Repository path to mount /// - mr: Merge request number -/// Returns: Result containing the response body string on success, or an error on failure -pub async fn mount_fs(repo: &str, mr: &str) -> Result { +/// Returns: Result containing success status on completion, or an error on failure +pub async fn mount_fs(repo: &str, mr: &str) -> Result> { // Create HTTP client let client = reqwest::Client::new(); - // Construct JSON request payload - let payload = json!({ + // Step 1: Send mount request to get request_id + let mount_payload = json!({ "path": repo, "mr": mr, }); - // Send POST request - let res = client + let mount_res = client .post("http://localhost:2725/api/fs/mount") .header("Content-Type", "application/json") - .body(payload.to_string()) + .body(mount_payload.to_string()) .send() .await?; - // Print status code - println!("Mount request status: {}", res.status()); + println!("Mount request status: {}", mount_res.status()); - // Get and return response body - let body = res.text().await?; - println!("Mount response body: {body}"); + let mount_body: Value = mount_res.json().await?; + println!("Mount response: {mount_body}"); - Ok(body) + // Extract request_id from response + let request_id = mount_body + .get("request_id") + .and_then(|v| v.as_str()) + .ok_or("Missing request_id in mount response")? + .to_string(); + + // Check if mount request was successful + if mount_body.get("status").and_then(|v| v.as_str()) != Some("Success") { + return Err("Mount request failed".into()); + } + + println!("Mount request initiated with request_id: {request_id}"); + + // Step 2: Poll for completion + let max_attempts = std::env::var("SELECT_TASK_COUNT").unwrap_or("30".into()); + let max_attempts: u64 = max_attempts.parse().unwrap_or(30); + let initial_poll_interval_secs = + std::env::var("INITIAL_POLL_INTERVAL_SECS").unwrap_or("10".into()); + let max_poll_interval_secs = 120; // Maximum backoff interval: 2 minutes + let mut poll_interval = initial_poll_interval_secs.parse::().unwrap_or(10); + for _attempt in 1..=max_attempts { + // Wait before checking status + sleep(Duration::from_secs(poll_interval)).await; + // Exponential backoff: double interval, up to max_poll_interval_secs + poll_interval = std::cmp::min(poll_interval * 2, max_poll_interval_secs); + + let select_url = format!("http://localhost:2725/api/fs/select/{request_id}"); + let select_res = client.get(&select_url).send().await?; + + let select_body: Value = select_res.json().await?; + println!("Select response: {select_body}"); + + // Check overall status + if select_body.get("status").and_then(|v| v.as_str()) != Some("Success") { + return Err("Select request failed".into()); + } + + // Check task status + match select_body.get("task_status").and_then(|v| v.as_str()) { + Some("finished") => { + println!("Mount task completed successfully"); + return Ok(true); + } + Some("error") => { + let message = select_body + .get("message") + .and_then(|v| v.as_str()) + .unwrap_or("Unknown error"); + return Err(format!("Mount task failed: {message}").into()); + } + Some("fetching") => { + println!("Mount task still in progress (fetching)..."); + continue; + } + Some(other_status) => { + println!("Mount task status: {other_status}"); + continue; + } + None => { + return Err("Missing task_status in select response".into()); + } + } + } + Err("Mount operation timed out".into()) } pub async fn build( @@ -55,7 +117,20 @@ pub async fn build( // Prepare the command to run // Note: `args` is a list of additional arguments to pass to the `buck - let _ = mount_fs(&repo, &mr).await; + // Mount filesystem before building + match mount_fs(&repo, &mr).await { + Ok(true) => { + tracing::info!("Filesystem mounted successfully for repo: {}", repo); + } + Ok(false) => { + tracing::error!("Filesystem mount failed for repo: {}", repo); + return Err(io::Error::other("Filesystem mount failed")); + } + Err(e) => { + tracing::error!("Error mounting filesystem for repo {}: {}", repo, e); + return Err(io::Error::other(format!("Filesystem mount error: {e}"))); + } + } let mut cmd = Command::new("buck2"); let cmd = cmd diff --git a/scorpio/scorpio.toml b/scorpio/scorpio.toml index 3bf61a142..83469d7ae 100644 --- a/scorpio/scorpio.toml +++ b/scorpio/scorpio.toml @@ -6,5 +6,5 @@ git_email = "admin@mega.org" workspace = "/tmp/megadir/mount" base_url = "http://git.gitmega.com" dicfuse_readable = "true" -load_dir_depth = "3" -fetch_file_thread = "10" \ No newline at end of file +load_dir_depth = "1" +fetch_file_thread = "10" diff --git a/scorpio/src/daemon/mod.rs b/scorpio/src/daemon/mod.rs index a62da7832..ff28cf7a3 100644 --- a/scorpio/src/daemon/mod.rs +++ b/scorpio/src/daemon/mod.rs @@ -5,29 +5,42 @@ use crate::fuse::MegaFuse; use crate::manager::fetch::fetch; use crate::manager::{mr, ScorpioManager, WorkDir}; use crate::util::{config, GPath}; -use axum::extract::State; +use axum::extract::{Path, State}; use axum::routing::{get, post}; use axum::Router; +use dashmap::DashMap; use mercury::hash::SHA1; use serde::{Deserialize, Serialize}; use tokio::sync::Mutex; +use uuid::Uuid; mod git; const SUCCESS: &str = "Success"; const FAIL: &str = "Fail"; -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, Clone)] struct MountRequest { path: String, mr: Option, // mr is the mount request, used for buck2 temp mount. } +/// Response structure for mount requests. +/// Returns immediately with a request ID for tracking the async operation. #[derive(Debug, Deserialize, Serialize)] struct MountResponse { - status: String, - mount: MountInfo, - message: String, + status: String, // Operation status: "Success" or "Fail" + request_id: String, // Unique ID for tracking the mount task + message: String, // Human-readable status message +} +/// Mount task structure, used to track asynchronous mount operations. +/// Each task represents a mount request that can be executed in the background. +#[derive(Debug, Deserialize, Serialize, Clone)] +struct MountStatus { + request_id: String, // Unique identifier for the mount request + status: String, // Current task status: "fetching", "finished", or "error" + mount_info: MountRequest, // Original mount request containing path and mr info + result: Option, // Mount result populated when task completes successfully } -#[derive(Debug, Deserialize, Serialize, Default)] +#[derive(Debug, Deserialize, Serialize, Default, Clone)] struct MountInfo { hash: String, path: String, @@ -71,20 +84,35 @@ struct ConfigRequest { mount_path: Option, store_path: Option, } + +/// Response structure for mount task status queries. +/// Provides current task status and mount information when available. +#[derive(Debug, Deserialize, Serialize)] +struct SelectResponse { + status: String, // API call status: "Success" or "Fail" + task_status: String, // Task status: "fetching", "finished", "error", or "not_found" + mount: Option, // Mount information available when task is finished + message: String, // Human-readable status message +} +/// Application state shared across all request handlers. +/// Contains shared resources and task tracking for the daemon. #[derive(Clone)] struct ScoState { - fuse: Arc, - manager: Arc>, + fuse: Arc, // Shared FUSE filesystem interface + manager: Arc>, // Shared workspace manager + tasks: Arc>, // Thread-safe storage for async mount tasks } #[allow(unused)] pub async fn daemon_main(fuse: Arc, manager: ScorpioManager) { let inner = ScoState { fuse, manager: Arc::new(Mutex::new(manager)), + tasks: Arc::new(DashMap::new()), // Initialize empty task tracking map }; let mut app = Router::new() .route("/api/fs/mount", post(mount_handler)) .route("/api/fs/mpoint", get(mounts_handler)) + .route("/api/fs/select/{request_id}", get(select_handler)) .route("/api/fs/umount", post(umount_handler)) .route("/api/config", get(config_handler)) .route("/api/config", post(update_config_handler)) @@ -103,17 +131,71 @@ pub async fn daemon_main(fuse: Arc, manager: ScorpioManager) { axum::serve(listener, app).await.unwrap() } -/// Mount a dictionary by path , like "/path/to/dic" or "path/to/dic" +/// Asynchronous mount handler for clients. +/// Initiates a mount operation in the background and returns immediately with a tracking ID. +/// This allows clients to start multiple mount operations concurrently and check their status. async fn mount_handler( State(state): State, req: axum::Json, ) -> axum::Json { - // transform by GPath , is case of wrong format. + // Generate a unique request ID for tracking this mount operation + let request_id = Uuid::new_v4().to_string(); + + // Create initial task record with "fetching" status + let mount_status = MountStatus { + request_id: request_id.clone(), + status: "fetching".to_string(), + mount_info: req.0.clone(), + result: None, + }; + + // Store the task in the shared task map for status tracking + state.tasks.insert(request_id.clone(), mount_status); + + // Spawn background task to perform the actual mount operation + let state_clone = state.clone(); + let req_clone = req.0.clone(); + let request_id_clone = request_id.clone(); + + tokio::spawn(async move { + // Clone state to avoid ownership issues in the async task + let state_for_task = state_clone.clone(); + let mount_result = perform_mount_task(state_for_task, req_clone).await; + + // Update the task status based on mount operation result + if let Some(mut task) = state_clone.tasks.get_mut(&request_id_clone) { + match mount_result { + Ok(mount_info) => { + task.status = "finished".to_string(); + task.result = Some(mount_info); + } + Err(_) => { + task.status = "error".to_string(); + // Could add error details here in the future + } + } + } + }); + + // Return immediately with the request ID for client tracking + axum::Json(MountResponse { + status: SUCCESS.to_string(), + request_id, + message: "Mount task started successfully".to_string(), + }) +} + +/// Helper function to perform the actual mount operation. +/// This function contains the core mounting logic extracted from the original mount handler. +/// It handles both temporary mounts (for buck2) and regular mounts with proper error handling. +async fn perform_mount_task(state: ScoState, req: MountRequest) -> Result { + // Normalize the path format using GPath utility let mono_path = GPath::from(req.path.clone()).to_string(); - // bool to indicate if it is a temp path for buck2. + // Determine if this is a temporary mount for buck2 workflow let mut temp_mount = false; - // get inode by this path . + + // Try to get existing inode, or create temporary point if path doesn't exist let inode = match state.fuse.get_inode(&mono_path).await { Ok(a) => a, Err(_) => { @@ -124,29 +206,24 @@ async fn mount_handler( .store .add_temp_point(&mono_path) .await - .unwrap() + .map_err(|e| format!("Failed to add temp point: {e}"))? } }; - // return fail if this inode is mounted. + // Check if the target is already mounted to prevent conflicts if state.fuse.is_mount(inode).await { - return axum::Json(MountResponse { - status: FAIL.into(), - mount: MountInfo::default(), - message: "The target is mounted.".to_string(), - }); + return Err("The target is already mounted".to_string()); } + // Acquire manager lock and check for existing checkouts let mut ml = state.manager.lock().await; if let Err(mounted_path) = ml.check_before_mount(&mono_path) { - return axum::Json(MountResponse { - status: FAIL.into(), - mount: MountInfo::default(), - message: format!("The {mounted_path} is already check-out "), - }); + return Err(format!("The {mounted_path} is already check-out")); } + let store_path = config::store_path(); - // if it is a temp mount , mount it & return the hash and path. + + // Handle temporary mount case (typically for buck2) if temp_mount { let temp_hash = { let hasher = SHA1::new(mono_path.as_bytes()); @@ -154,64 +231,96 @@ async fn mount_handler( }; let store_path = PathBuf::from(store_path).join(&temp_hash); - let _ = state.fuse.overlay_mount(inode, store_path, false).await; + + // Perform the actual overlay mount + state + .fuse + .overlay_mount(inode, store_path, false) + .await + .map_err(|e| format!("Failed to overlay mount: {e}"))?; + let mount_info = MountInfo { hash: temp_hash.clone(), path: mono_path.clone(), inode, }; + + // Update manager's work directory list ml.works.push(WorkDir { path: mono_path, node: inode, hash: temp_hash, }); let _ = ml.to_toml("config.toml"); - return axum::Json(MountResponse { - status: SUCCESS.into(), - mount: mount_info, - message: "Directory mounted successfully".to_string(), - }); + + return Ok(mount_info); } - // fetch the dictionary node info from mono. - let work_dir = fetch(&mut ml, inode, mono_path).await.unwrap(); + // Handle regular mount case - fetch repository information + let work_dir = fetch(&mut ml, inode, mono_path) + .await + .map_err(|e| format!("Failed to fetch: {e}"))?; let store_path = PathBuf::from(store_path).join(&work_dir.hash); + + // Handle merge request (MR) layer if provided if let Some(m) = &req.mr { let mr_store_path = PathBuf::from(&store_path).join("mr"); - // if mr is provided, we need to fetch the mr info from mono. if let Err(e) = mr::build_mr_layer(m, mr_store_path).await { - return axum::Json(MountResponse { - status: FAIL.into(), - mount: MountInfo::default(), - message: format!("Failed to build mr layer: {e}"), - }); + return Err(format!("Failed to build mr layer: {e}")); } } - // checkout / mount this dictionary. - if let Err(e) = state + // Perform the final overlay mount with MR layer if applicable + state .fuse .overlay_mount(inode, store_path, req.mr.is_some()) .await - { - return axum::Json(MountResponse { - status: FAIL.into(), - mount: MountInfo::default(), - message: format!("Mount process error: {e}."), - }); - } + .map_err(|e| format!("Mount process error: {e}"))?; let mount_info = MountInfo { hash: work_dir.hash, path: work_dir.path, inode, }; - axum::Json(MountResponse { - status: SUCCESS.into(), - mount: mount_info, - message: "Directory mounted successfully".to_string(), - }) + + Ok(mount_info) } + +/// Query handler for mount task status. +/// Allows clients to check the progress of their asynchronous mount operations. +/// Requires a valid request_id as URL path parameter. +/// Automatically cleans up completed tasks from memory. +async fn select_handler( + State(state): State, + Path(request_id): Path, +) -> axum::Json { + // Search by request_id (now provided as URL path parameter) + if let Some(task) = state.tasks.get(&request_id) { + let response = SelectResponse { + status: SUCCESS.to_string(), + task_status: task.status.clone(), + mount: task.result.clone(), + message: "Task found".to_string(), + }; + + // Clean up completed tasks from memory to prevent memory leaks + if task.status == "finished" || task.status == "error" { + println!("{:?} now be removed", task.request_id); + drop(task); // Release the reference before removing + state.tasks.remove(&request_id); + } + + axum::Json(response) + } else { + axum::Json(SelectResponse { + status: FAIL.to_string(), + task_status: "not_found".to_string(), + mount: None, + message: "Task not found".to_string(), + }) + } +} + /// Get all mounted dictionary . async fn mounts_handler(State(state): State) -> axum::Json { let manager = state.manager.lock().await;