Skip to content

Commit 1b12273

Browse files
committed
Add external file listing, asset/attachment folder APIs, and refactor CreateFolderPath
- Add vxcore_folder_list_external API to list unindexed files/folders in bundled notebooks - Add FolderManager::GetPublicAssetsFolder, GetPublicAttachmentsFolder, GetAssetsFolder, GetAttachmentsFolder for resolving asset/attachment paths - Move CreateFolderPath from Notebook to FolderManager for better cohesion - Add IsSingleName utility to check if path has no separators - Skip assets/attachments folders when listing external nodes - Extract kMetadataFolderName constant in BundledNotebook
1 parent 3cdf2e9 commit 1b12273

16 files changed

+582
-45
lines changed

include/vxcore/vxcore.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,16 @@ VXCORE_API VxCoreError vxcore_folder_list_children(VxCoreContextHandle context,
108108
const char *notebook_id,
109109
const char *folder_path,
110110
char **out_children_json);
111+
112+
// List external (unindexed) nodes in a folder.
113+
// External nodes exist on filesystem but are not tracked in metadata.
114+
// folder_path: Path relative to notebook root ("" or "." for root)
115+
// Output JSON: {"files": [...], "folders": [...]}
116+
// Each entry contains only "name" field (no ID since not indexed).
117+
VXCORE_API VxCoreError vxcore_folder_list_external(VxCoreContextHandle context,
118+
const char *notebook_id,
119+
const char *folder_path,
120+
char **out_external_json);
111121
// ============ File Operations ============
112122
VXCORE_API VxCoreError vxcore_file_create(VxCoreContextHandle context, const char *notebook_id,
113123
const char *folder_path, const char *file_name,

src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ set(VXCORE_SOURCES
2424
core/notebook_manager.cpp
2525
core/folder.cpp
2626
core/bundled_folder_manager.cpp
27+
core/folder_manager.cpp
2728
core/raw_folder_manager.cpp
2829
db/db_manager.cpp
2930
db/file_db.cpp

src/api/vxcore_folder_api.cpp

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ VXCORE_API VxCoreError vxcore_folder_create_path(VxCoreContextHandle context,
6363
}
6464

6565
std::string folder_id;
66-
VxCoreError error = notebook->CreateFolderPath(folder_path, folder_id);
66+
VxCoreError error = notebook->GetFolderManager()->CreateFolderPath(folder_path, folder_id);
6767
if (error != VXCORE_OK) {
6868
return error;
6969
}
@@ -248,6 +248,70 @@ VXCORE_API VxCoreError vxcore_folder_list_children(VxCoreContextHandle context,
248248
}
249249
}
250250

251+
VXCORE_API VxCoreError vxcore_folder_list_external(VxCoreContextHandle context,
252+
const char *notebook_id,
253+
const char *folder_path,
254+
char **out_external_json) {
255+
if (!context || !notebook_id || !out_external_json) {
256+
return VXCORE_ERR_INVALID_PARAM;
257+
}
258+
259+
vxcore::VxCoreContext *ctx = reinterpret_cast<vxcore::VxCoreContext *>(context);
260+
261+
try {
262+
vxcore::Notebook *notebook = ctx->notebook_manager->GetNotebook(notebook_id);
263+
if (!notebook) {
264+
ctx->last_error = "Notebook not found";
265+
return VXCORE_ERR_NOT_FOUND;
266+
}
267+
268+
vxcore::FolderManager *folder_manager = notebook->GetFolderManager();
269+
if (!folder_manager) {
270+
ctx->last_error = "FolderManager not available";
271+
return VXCORE_ERR_INVALID_STATE;
272+
}
273+
274+
std::string path = folder_path ? folder_path : ".";
275+
vxcore::FolderManager::FolderContents contents;
276+
277+
VxCoreError error = folder_manager->ListExternalNodes(path, contents);
278+
if (error != VXCORE_OK) {
279+
return error;
280+
}
281+
282+
// Build JSON response
283+
nlohmann::json result;
284+
nlohmann::json files_json = nlohmann::json::array();
285+
nlohmann::json folders_json = nlohmann::json::array();
286+
287+
for (const auto &file : contents.files) {
288+
// External files only have name (no ID)
289+
nlohmann::json file_json;
290+
file_json["name"] = file.name;
291+
file_json["type"] = "file";
292+
files_json.push_back(file_json);
293+
}
294+
295+
for (const auto &folder : contents.folders) {
296+
// External folders only have name (no ID)
297+
nlohmann::json folder_json;
298+
folder_json["name"] = folder.name;
299+
folder_json["type"] = "folder";
300+
folders_json.push_back(folder_json);
301+
}
302+
303+
result["files"] = files_json;
304+
result["folders"] = folders_json;
305+
306+
*out_external_json = vxcore_strdup(result.dump().c_str());
307+
return VXCORE_OK;
308+
} catch (const std::exception &e) {
309+
ctx->last_error = std::string("Exception: ") + e.what();
310+
return VXCORE_ERR_UNKNOWN;
311+
}
312+
}
313+
314+
251315
VXCORE_API VxCoreError vxcore_file_import(VxCoreContextHandle context, const char *notebook_id,
252316
const char *folder_path, const char *external_file_path,
253317
char **out_file_id) {

src/core/bundled_folder_manager.cpp

Lines changed: 97 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
#include <unordered_set>
1010

1111
#include "metadata_store.h"
12-
#include "notebook.h"
12+
#include "bundled_notebook.h"
1313
#include "utils/file_utils.h"
1414
#include "utils/logger.h"
1515
#include "utils/utils.h"
@@ -1870,10 +1870,6 @@ VxCoreError BundledFolderManager::ImportFolder(const std::string &dest_folder_pa
18701870
continue;
18711871
}
18721872
if (entry.is_directory()) {
1873-
// Skip vx_notebook metadata folder
1874-
if (entry_name == "vx_notebook") {
1875-
continue;
1876-
}
18771873
copy_filtered(entry.path(), dest / entry_name);
18781874
} else if (entry.is_regular_file()) {
18791875
// Apply suffix filter if allowlist is specified
@@ -1923,14 +1919,15 @@ VxCoreError BundledFolderManager::ImportFolder(const std::string &dest_folder_pa
19231919
std::function<VxCoreError(const std::string &, FolderConfig &)> index_contents =
19241920
[&](const std::string &rel_path, FolderConfig &config) -> VxCoreError {
19251921
std::string abs_path = GetContentPath(rel_path);
1922+
const bool is_root = (rel_path.empty() || rel_path == ".");
19261923

19271924
try {
19281925
for (const auto &entry : fs::directory_iterator(abs_path)) {
19291926
std::string entry_name = entry.path().filename().string();
19301927

19311928
if (entry.is_directory()) {
1932-
// Skip hidden folders and vx_notebook metadata folder
1933-
if (entry_name.empty() || entry_name[0] == '.' || entry_name == "vx_notebook") {
1929+
if (entry_name.empty() || entry_name[0] == '.' ||
1930+
(is_root && entry_name == BundledNotebook::kMetadataFolderName)) {
19341931
continue;
19351932
}
19361933

@@ -2230,4 +2227,97 @@ VxCoreError BundledFolderManager::UnindexNode(const std::string &node_path) {
22302227
VXCORE_LOG_WARN("UnindexNode: Node not found in metadata: %s", clean_path.c_str());
22312228
return VXCORE_ERR_NOT_FOUND;
22322229
}
2230+
2231+
VxCoreError BundledFolderManager::ListExternalNodes(const std::string &folder_path,
2232+
FolderContents &out_contents) {
2233+
const auto clean_path = GetCleanRelativePath(folder_path);
2234+
VXCORE_LOG_INFO("ListExternalNodes: path=%s", clean_path.c_str());
2235+
2236+
// Clear output
2237+
out_contents.files.clear();
2238+
out_contents.folders.clear();
2239+
2240+
// Get folder config (indexed items)
2241+
FolderConfig *config = nullptr;
2242+
VxCoreError error = GetFolderConfig(clean_path, &config);
2243+
if (error != VXCORE_OK) {
2244+
VXCORE_LOG_ERROR("ListExternalNodes: Failed to get folder config: path=%s, error=%d",
2245+
clean_path.c_str(), error);
2246+
return error;
2247+
}
2248+
2249+
// Build sets of indexed items for fast lookup
2250+
std::set<std::string> indexed_files;
2251+
std::set<std::string> indexed_folders;
2252+
for (const auto &file : config->files) {
2253+
indexed_files.insert(file.name);
2254+
}
2255+
for (const auto &folder_name : config->folders) {
2256+
indexed_folders.insert(folder_name);
2257+
}
2258+
2259+
// Get filesystem content path
2260+
std::string content_path = GetContentPath(clean_path);
2261+
if (!fs::exists(content_path)) {
2262+
VXCORE_LOG_WARN("ListExternalNodes: Content path does not exist: %s", content_path.c_str());
2263+
return VXCORE_ERR_NOT_FOUND;
2264+
}
2265+
2266+
// Check if assets/attachments folders should be skipped (only if single name)
2267+
const NotebookConfig &nb_config = notebook_->GetConfig();
2268+
const bool need_check_assets_folder = IsSingleName(nb_config.assets_folder);
2269+
const bool need_check_attachments_folder = IsSingleName(nb_config.attachments_folder);
2270+
2271+
const bool is_root = clean_path.empty() || clean_path == ".";
2272+
2273+
// Scan filesystem and find unindexed items
2274+
try {
2275+
for (const auto &entry : fs::directory_iterator(content_path)) {
2276+
std::string entry_name = entry.path().filename().string();
2277+
2278+
// Skip hidden files/folders (starting with '.')
2279+
if (entry_name.empty() || entry_name[0] == '.') {
2280+
continue;
2281+
}
2282+
2283+
// Skip metadata folder (only at root)
2284+
if (is_root && entry_name == BundledNotebook::kMetadataFolderName) {
2285+
continue;
2286+
}
2287+
2288+
// Skip assets and attachments folders
2289+
if ((need_check_assets_folder && entry_name == nb_config.assets_folder) ||
2290+
(need_check_attachments_folder && entry_name == nb_config.attachments_folder)) {
2291+
continue;
2292+
}
2293+
2294+
if (entry.is_directory()) {
2295+
// Check if folder is NOT indexed
2296+
if (indexed_folders.find(entry_name) == indexed_folders.end()) {
2297+
// External folder - create a minimal FolderRecord
2298+
FolderRecord record;
2299+
record.name = entry_name;
2300+
// No ID since it's not indexed
2301+
out_contents.folders.push_back(record);
2302+
}
2303+
} else if (entry.is_regular_file()) {
2304+
// Check if file is NOT indexed
2305+
if (indexed_files.find(entry_name) == indexed_files.end()) {
2306+
// External file - create a minimal FileRecord
2307+
FileRecord record;
2308+
record.name = entry_name;
2309+
// No ID since it's not indexed
2310+
out_contents.files.push_back(record);
2311+
}
2312+
}
2313+
}
2314+
} catch (const std::exception &e) {
2315+
VXCORE_LOG_ERROR("ListExternalNodes: Failed to iterate directory: %s", e.what());
2316+
return VXCORE_ERR_IO;
2317+
}
2318+
2319+
VXCORE_LOG_INFO("ListExternalNodes: Found %zu external files, %zu external folders",
2320+
out_contents.files.size(), out_contents.folders.size());
2321+
return VXCORE_OK;
2322+
}
22332323
} // namespace vxcore

src/core/bundled_folder_manager.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ class BundledFolderManager : public FolderManager {
9393

9494
VxCoreError UnindexNode(const std::string &node_path) override;
9595

96+
VxCoreError ListExternalNodes(const std::string &folder_path,
97+
FolderContents &out_contents) override;
98+
9699
// Syncs the MetadataStore from config files (vx.json)
97100
// Called on notebook open to rebuild cache from ground truth
98101
// Returns VXCORE_OK on success

src/core/bundled_notebook.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
namespace vxcore {
1111

12+
const char *BundledNotebook::kMetadataFolderName = "vx_notebook";
13+
1214
BundledNotebook::BundledNotebook(const std::string &local_data_folder,
1315
const std::string &root_folder)
1416
: Notebook(local_data_folder, root_folder, NotebookType::Bundled) {
@@ -127,7 +129,7 @@ VxCoreError BundledNotebook::Open(const std::string &local_data_folder,
127129
}
128130

129131
std::string BundledNotebook::GetMetadataFolder() const {
130-
return ConcatenatePaths(root_folder_, "vx_notebook");
132+
return ConcatenatePaths(root_folder_, kMetadataFolderName);
131133
}
132134

133135
std::string BundledNotebook::GetConfigPath() const {

src/core/bundled_notebook.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ class BundledNotebook : public Notebook {
2222
std::string GetRecycleBinPath() const override;
2323
VxCoreError EmptyRecycleBin() override;
2424

25+
static const char *kMetadataFolderName;
26+
2527
private:
2628
BundledNotebook(const std::string &local_data_folder, const std::string &root_folder);
2729

src/core/folder_manager.cpp

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
#include "folder_manager.h"
2+
3+
#include <filesystem>
4+
5+
namespace vxcore {
6+
7+
VxCoreError FolderManager::CreateFolderPath(const std::string &folder_path,
8+
std::string &out_folder_id) {
9+
if (folder_path.empty()) {
10+
return VXCORE_ERR_INVALID_PARAM;
11+
}
12+
13+
std::vector<std::string> path_components = SplitPathComponents(folder_path);
14+
if (path_components.empty()) {
15+
return VXCORE_ERR_INVALID_PARAM;
16+
}
17+
18+
std::string current_parent = ".";
19+
std::string folder_id;
20+
21+
for (size_t i = 0; i < path_components.size(); ++i) {
22+
const std::string &folder_name = path_components[i];
23+
if (folder_name.empty()) {
24+
return VXCORE_ERR_INVALID_PARAM;
25+
}
26+
27+
VxCoreError err = CreateFolder(current_parent, folder_name, folder_id);
28+
if (err != VXCORE_OK && err != VXCORE_ERR_ALREADY_EXISTS) {
29+
return err;
30+
}
31+
32+
current_parent = ConcatenatePaths(current_parent, folder_name);
33+
}
34+
35+
out_folder_id = folder_id;
36+
return VXCORE_OK;
37+
}
38+
39+
std::string FolderManager::GetPublicAssetsFolder(const std::string &file_path) const {
40+
const NotebookConfig &config = notebook_->GetConfig();
41+
const std::string &assets_folder = config.assets_folder;
42+
43+
// If absolute path, return as-is
44+
if (!IsRelativePath(assets_folder)) {
45+
return CleanPath(assets_folder);
46+
}
47+
48+
// Get parent folder of the file
49+
auto [parent_path, _] = SplitPath(file_path);
50+
std::string abs_parent = notebook_->GetAbsolutePath(parent_path);
51+
52+
// Resolve relative to file's parent folder
53+
return CleanFsPath(std::filesystem::path(abs_parent) / assets_folder);
54+
}
55+
56+
std::string FolderManager::GetPublicAttachmentsFolder(const std::string &file_path) const {
57+
const NotebookConfig &config = notebook_->GetConfig();
58+
const std::string &attachments_folder = config.attachments_folder;
59+
60+
// If absolute path, return as-is
61+
if (!IsRelativePath(attachments_folder)) {
62+
return CleanPath(attachments_folder);
63+
}
64+
65+
// Get parent folder of the file
66+
auto [parent_path, _] = SplitPath(file_path);
67+
std::string abs_parent = notebook_->GetAbsolutePath(parent_path);
68+
69+
// Resolve relative to file's parent folder
70+
return CleanFsPath(std::filesystem::path(abs_parent) / attachments_folder);
71+
}
72+
73+
std::string FolderManager::GetAssetsFolder(const std::string &file_path) {
74+
// Get file's UUID
75+
const FileRecord *record = nullptr;
76+
VxCoreError err = GetFileInfo(file_path, &record);
77+
if (err != VXCORE_OK || !record) {
78+
return "";
79+
}
80+
81+
std::string public_folder = GetPublicAssetsFolder(file_path);
82+
return ConcatenatePaths(public_folder, record->id);
83+
}
84+
85+
std::string FolderManager::GetAttachmentsFolder(const std::string &file_path) {
86+
// Get file's UUID
87+
const FileRecord *record = nullptr;
88+
VxCoreError err = GetFileInfo(file_path, &record);
89+
if (err != VXCORE_OK || !record) {
90+
return "";
91+
}
92+
93+
std::string public_folder = GetPublicAttachmentsFolder(file_path);
94+
return ConcatenatePaths(public_folder, record->id);
95+
}
96+
97+
} // namespace vxcore

0 commit comments

Comments
 (0)