From 47b08d34a651e9771c3c3e5469f6406888297989 Mon Sep 17 00:00:00 2001 From: Hu Yueh-Wei Date: Wed, 25 Dec 2024 15:20:27 +0800 Subject: [PATCH] feat: add dir-list tman designer endpoint --- core/src/ten_manager/Cargo.lock | 81 ++++--- core/src/ten_manager/Cargo.toml | 4 + .../ten_manager/src/designer/dir_list/mod.rs | 227 ++++++++++++++++++ core/src/ten_manager/src/designer/mod.rs | 5 + core/src/ten_rust/Cargo.lock | 24 +- 5 files changed, 289 insertions(+), 52 deletions(-) create mode 100644 core/src/ten_manager/src/designer/dir_list/mod.rs diff --git a/core/src/ten_manager/Cargo.lock b/core/src/ten_manager/Cargo.lock index de9d384737..08da713cf8 100644 --- a/core/src/ten_manager/Cargo.lock +++ b/core/src/ten_manager/Cargo.lock @@ -369,9 +369,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.94" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" +checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" [[package]] name = "arbitrary" @@ -534,9 +534,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.4" +version = "1.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9157bbaa6b165880c27a4293a474c91cdcf265cc68cc829bf10be0964a391caf" +checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e" dependencies = [ "shlex", ] @@ -1177,11 +1177,11 @@ checksum = "9958ab3ce3170c061a27679916bd9b969eceeb5e8b120438e6751d0987655c42" [[package]] name = "home" -version = "0.5.9" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1249,9 +1249,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.5.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f" +checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" dependencies = [ "bytes", "futures-channel", @@ -1268,9 +1268,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.3" +version = "0.27.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" dependencies = [ "futures-util", "http 1.2.0", @@ -1460,9 +1460,9 @@ dependencies = [ [[package]] name = "impl-more" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aae21c3177a27788957044151cc2800043d127acaa460a47ebb9b84dfa2c6aa0" +checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" [[package]] name = "indexmap" @@ -1662,9 +1662,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.168" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libloading" @@ -1810,9 +1810,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" dependencies = [ "adler2", ] @@ -2043,9 +2043,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.5" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] @@ -2110,7 +2110,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" dependencies = [ "memchr", - "thiserror 2.0.7", + "thiserror 2.0.9", "ucd-trie", ] @@ -2234,7 +2234,7 @@ dependencies = [ "rustc-hash 2.1.0", "rustls", "socket2", - "thiserror 2.0.7", + "thiserror 2.0.9", "tokio", "tracing", ] @@ -2253,7 +2253,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.7", + "thiserror 2.0.9", "tinyvec", "tracing", "web-time", @@ -2261,9 +2261,9 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.8" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52cd4b1eff68bf27940dd39811292c49e007f4d0b4c357358dc9b0197be6b527" +checksum = "1c40286217b4ba3a71d644d752e6a0b71f13f1b6a2c5311acfcbe0c2418ed904" dependencies = [ "cfg_aliases", "libc", @@ -2641,9 +2641,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.133" +version = "1.0.134" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d" dependencies = [ "indexmap", "itoa", @@ -2837,9 +2837,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.90" +version = "2.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" +checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035" dependencies = [ "proc-macro2", "quote", @@ -2919,6 +2919,7 @@ dependencies = [ "ten_rust", "tokio", "url", + "urlencoding", "walkdir", "webbrowser", "zip", @@ -2962,11 +2963,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.7" +version = "2.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93605438cbd668185516ab499d589afb7ee1859ea3d5fc8f6b0755e1c7443767" +checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc" dependencies = [ - "thiserror-impl 2.0.7", + "thiserror-impl 2.0.9", ] [[package]] @@ -2982,9 +2983,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.7" +version = "2.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d8749b4531af2117677a5fcd12b1348a3fe2b81e36e61ffeac5c4aa3273e36" +checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" dependencies = [ "proc-macro2", "quote", @@ -3044,9 +3045,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" dependencies = [ "tinyvec_macros", ] @@ -3149,9 +3150,9 @@ checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "unicase" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" @@ -3790,9 +3791,9 @@ dependencies = [ [[package]] name = "zip" -version = "2.2.1" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d52293fc86ea7cf13971b3bb81eb21683636e7ae24c729cdaf1b7c4157a352" +checksum = "ae9c1ea7b3a5e1f4b922ff856a129881167511563dc219869afe3787fc0c1a45" dependencies = [ "arbitrary", "crc32fast", @@ -3801,7 +3802,7 @@ dependencies = [ "flate2", "indexmap", "memchr", - "thiserror 2.0.7", + "thiserror 2.0.9", "time", "zopfli", ] diff --git a/core/src/ten_manager/Cargo.toml b/core/src/ten_manager/Cargo.toml index d9ad5f5abe..2e8cc0de95 100644 --- a/core/src/ten_manager/Cargo.toml +++ b/core/src/ten_manager/Cargo.toml @@ -60,3 +60,7 @@ mimalloc = "0.1" [target."cfg(unix)".dependencies.clingo] version = "0.8" features = ["static-linking"] + +[dev-dependencies] +tempfile = "3.5" +urlencoding = "2.1" diff --git a/core/src/ten_manager/src/designer/dir_list/mod.rs b/core/src/ten_manager/src/designer/dir_list/mod.rs new file mode 100644 index 0000000000..0d13dd46fa --- /dev/null +++ b/core/src/ten_manager/src/designer/dir_list/mod.rs @@ -0,0 +1,227 @@ +// +// Copyright © 2024 Agora +// This file is part of TEN Framework, an open source project. +// Licensed under the Apache License, Version 2.0, with certain conditions. +// Refer to the "LICENSE" file in the root directory for more information. +// +use std::{ + fs, + path::Path, + sync::{Arc, RwLock}, +}; + +use actix_web::{web, HttpResponse, Responder}; +use serde::{Deserialize, Serialize}; + +use super::{ + response::{ApiResponse, Status}, + DesignerState, +}; + +#[derive(Serialize, Deserialize, Debug)] +pub struct FsEntry { + pub name: String, + pub path: String, + pub is_dir: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct DirListResponse { + pub entries: Vec, +} + +pub async fn list_dir( + path: web::Path, + _state: web::Data>>, +) -> impl Responder { + let path_str = path.into_inner(); + let path_obj = Path::new(&path_str); + + // If the path does not exist, return 404. + if !path_obj.exists() { + let response = ApiResponse { + status: Status::Fail, + data: (), + meta: None, + }; + return HttpResponse::NotFound().json(response); + } + + // Collect the returned results. + let mut entries = Vec::new(); + + if path_obj.is_file() { + // If it is a file, return only the file information. + if let Some(file_name) = path_obj.file_name() { + entries.push(FsEntry { + name: file_name.to_string_lossy().to_string(), + path: path_str.clone(), + is_dir: false, + }); + } + } else if path_obj.is_dir() { + // If it is a folder, list its immediate items. + if let Ok(dir_entries) = fs::read_dir(path_obj) { + for entry in dir_entries.flatten() { + let file_type = entry.file_type(); + if let Ok(ft) = file_type { + let file_name = + entry.file_name().to_string_lossy().to_string(); + let full_path = entry.path().to_string_lossy().to_string(); + entries.push(FsEntry { + name: file_name, + path: full_path, + is_dir: ft.is_dir(), + }); + } + } + } + } + + let response = ApiResponse { + status: Status::Ok, + data: DirListResponse { entries }, + meta: None, + }; + HttpResponse::Ok().json(response) +} + +#[cfg(test)] +mod tests { + use crate::config::TmanConfig; + + use super::*; + use actix_web::{test, App}; + use std::fs::{self, File}; + use std::io::Write; + use tempfile::tempdir; + use urlencoding::encode; + + #[actix_web::test] + async fn test_list_dir_with_file_path() { + // Create a temporary directory. + let dir = tempdir().unwrap(); + let file_path = dir.path().join("test_file.txt"); + let mut file = File::create(&file_path).unwrap(); + writeln!(file, "Hello, world!").unwrap(); + + // Initialize DesignerState. + let state = web::Data::new(Arc::new(RwLock::new(DesignerState { + base_dir: None, + all_pkgs: None, + tman_config: TmanConfig::default(), + }))); + + // Configure the `list_dir` route. + let app = test::init_service(App::new().app_data(state.clone()).route( + "/api/designer/v1/dir-list/{path}", + web::get().to(list_dir), + )) + .await; + + // Construct the request. + let req_path = file_path.to_string_lossy().to_string(); + let encoded_path = encode(&req_path); + let req = test::TestRequest::get() + .uri(&format!("/api/designer/v1/dir-list/{}", encoded_path)) + .to_request(); + let resp = test::call_service(&app, req).await; + + assert!(resp.status().is_success()); + + let body = test::read_body(resp).await; + let response: ApiResponse = + serde_json::from_slice(&body).unwrap(); + + assert_eq!(response.status, Status::Ok); + assert_eq!(response.data.entries.len(), 1); + let entry = &response.data.entries[0]; + assert_eq!(entry.name, "test_file.txt"); + assert_eq!(entry.path, req_path); + assert!(!entry.is_dir); + } + + #[actix_web::test] + async fn test_list_dir_with_directory_path() { + // Create a temporary directory. + let dir = tempdir().unwrap(); + let sub_dir = dir.path().join("sub_dir"); + fs::create_dir(&sub_dir).unwrap(); + let file1 = dir.path().join("file1.txt"); + let file2 = sub_dir.join("file2.txt"); + let mut f1 = File::create(&file1).unwrap(); + writeln!(f1, "File 1").unwrap(); + let mut f2 = File::create(&file2).unwrap(); + writeln!(f2, "File 2").unwrap(); + + // Initialize DesignerState. + let state = web::Data::new(Arc::new(RwLock::new(DesignerState { + base_dir: None, + all_pkgs: None, + tman_config: TmanConfig::default(), + }))); + + // Configure the `list_dir` route. + let app = test::init_service(App::new().app_data(state.clone()).route( + "/api/designer/v1/dir-list/{path}", + web::get().to(list_dir), + )) + .await; + + // Construct the request. + let req_path = dir.path().to_string_lossy().to_string(); + let encoded_path = encode(&req_path); + let req = test::TestRequest::get() + .uri(&format!("/api/designer/v1/dir-list/{}", encoded_path)) + .to_request(); + let resp = test::call_service(&app, req).await; + + assert!(resp.status().is_success()); + + let body = test::read_body(resp).await; + let response: ApiResponse = + serde_json::from_slice(&body).unwrap(); + + assert_eq!(response.status, Status::Ok); + assert_eq!(response.data.entries.len(), 2); + + let mut entries = response.data.entries.iter().collect::>(); + entries.sort_by_key(|e| &e.name); + assert_eq!(entries[0].name, "file1.txt"); + assert_eq!(entries[0].path, file1.to_string_lossy()); + assert!(!entries[0].is_dir); + assert_eq!(entries[1].name, "sub_dir"); + assert_eq!(entries[1].path, sub_dir.to_string_lossy()); + assert!(entries[1].is_dir); + } + + #[actix_web::test] + async fn test_list_dir_with_non_existing_path() { + let state = web::Data::new(Arc::new(RwLock::new(DesignerState { + base_dir: None, + all_pkgs: None, + tman_config: TmanConfig::default(), + }))); + + let app = test::init_service(App::new().app_data(state.clone()).route( + "/api/designer/v1/dir-list/{path}", + web::get().to(list_dir), + )) + .await; + + // Construct an invalid path. + let non_existing_path = "/path/to/nonexistent".to_string(); + let encoded_path = encode(&non_existing_path); + let req = test::TestRequest::get() + .uri(&format!("/api/designer/v1/dir-list/{}", encoded_path)) + .to_request(); + let resp = test::call_service(&app, req).await; + + assert_eq!(resp.status(), actix_web::http::StatusCode::NOT_FOUND); + + let body = test::read_body(resp).await; + let response: ApiResponse<()> = serde_json::from_slice(&body).unwrap(); + + assert_eq!(response.status, Status::Fail); + } +} diff --git a/core/src/ten_manager/src/designer/mod.rs b/core/src/ten_manager/src/designer/mod.rs index 69dd870e80..02b3954f72 100644 --- a/core/src/ten_manager/src/designer/mod.rs +++ b/core/src/ten_manager/src/designer/mod.rs @@ -7,6 +7,7 @@ mod addons; mod base_dir; mod common; +mod dir_list; mod file_content; pub mod frontend; mod get_all_pkgs; @@ -99,5 +100,9 @@ pub fn configure_routes( "/api/designer/v1/base-dir", web::put().to(base_dir::set_base_dir), ) + .route( + "/api/designer/v1/dir-list/{path}", + web::get().to(dir_list::list_dir), + ) .route("/ws/terminal", web::get().to(ws_terminal)); } diff --git a/core/src/ten_rust/Cargo.lock b/core/src/ten_rust/Cargo.lock index 8f33203a67..d7bbc2d4c4 100644 --- a/core/src/ten_rust/Cargo.lock +++ b/core/src/ten_rust/Cargo.lock @@ -27,9 +27,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.94" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" +checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" [[package]] name = "autocfg" @@ -480,9 +480,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.168" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libloading" @@ -831,9 +831,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.133" +version = "1.0.134" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d" dependencies = [ "indexmap", "itoa", @@ -873,9 +873,9 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "syn" -version = "2.0.90" +version = "2.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" +checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035" dependencies = [ "proc-macro2", "quote", @@ -926,18 +926,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.7" +version = "2.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93605438cbd668185516ab499d589afb7ee1859ea3d5fc8f6b0755e1c7443767" +checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.7" +version = "2.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d8749b4531af2117677a5fcd12b1348a3fe2b81e36e61ffeac5c4aa3273e36" +checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" dependencies = [ "proc-macro2", "quote",