diff --git a/examples/hero/herofs/.gitignore b/examples/hero/herofs/.gitignore index 6fc1d38a..83a6b9e0 100644 --- a/examples/hero/herofs/.gitignore +++ b/examples/hero/herofs/.gitignore @@ -1 +1,3 @@ herofs_basic +herofs_server +fs_server diff --git a/examples/hero/herofs/fs_server.vsh b/examples/hero/herofs/fs_server.vsh new file mode 100755 index 00000000..b148c215 --- /dev/null +++ b/examples/hero/herofs/fs_server.vsh @@ -0,0 +1,34 @@ +#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals -no-skip-unused run + +import freeflowuniverse.herolib.hero.herofs_server +import freeflowuniverse.herolib.ui.console + +fn main() { + console.print_header('HeroFS REST API Server Example') + + // Create server with CORS enabled for development + mut server := herofs_server.new( + port: 8080 + host: 'localhost' + cors_enabled: true + allowed_origins: ['*'] // Allow all origins for development + )! + + console.print_item('Server configured successfully') + console.print_item('Starting server...') + console.print_item('') + console.print_item('Available endpoints:') + console.print_item(' Health check: GET http://localhost:8080/health') + console.print_item(' API info: GET http://localhost:8080/api') + console.print_item(' Filesystems: http://localhost:8080/api/fs') + console.print_item(' Directories: http://localhost:8080/api/dirs') + console.print_item(' Files: http://localhost:8080/api/files') + console.print_item(' Blobs: http://localhost:8080/api/blobs') + console.print_item(' Symlinks: http://localhost:8080/api/symlinks') + console.print_item(' Tools: http://localhost:8080/api/tools') + console.print_item('') + console.print_item('Press Ctrl+C to stop the server') + + // Start the server (this blocks) + server.start()! +} diff --git a/lib/hero/herofs_server/README.md b/lib/hero/herofs_server/README.md new file mode 100644 index 00000000..958e6331 --- /dev/null +++ b/lib/hero/herofs_server/README.md @@ -0,0 +1,218 @@ +# HeroFS REST API Server + +A comprehensive REST API server for the HeroFS distributed filesystem, built with V and VEB framework. + +## Features + +- **Complete CRUD Operations** for all HeroFS entities (Filesystems, Directories, Files, Blobs, Symlinks) +- **Advanced Filesystem Tools** (find, copy, move, remove, import/export) +- **CORS Support** for frontend integration +- **JSON Request/Response** with consistent error handling +- **RESTful Design** following standard HTTP conventions +- **Production Ready** with proper error handling and validation + +## Quick Start + +```v +import freeflowuniverse.herolib.hero.herofs_server + +// Create and start server +mut server := herofs_server.new( + port: 8080 + host: 'localhost' + cors_enabled: true + allowed_origins: ['*'] +)! + +server.start()! +``` + +## API Endpoints + +### Health & Info +- `GET /health` - Health check +- `GET /api` - API information and available endpoints + +### Filesystems (`/api/fs`) +- `GET /api/fs` - List all filesystems +- `GET /api/fs/:id` - Get filesystem by ID +- `POST /api/fs` - Create new filesystem +- `PUT /api/fs/:id` - Update filesystem +- `DELETE /api/fs/:id` - Delete filesystem +- `GET /api/fs/:id/exists` - Check if filesystem exists +- `POST /api/fs/:id/usage/increase` - Increase usage counter +- `POST /api/fs/:id/usage/decrease` - Decrease usage counter +- `POST /api/fs/:id/quota/check` - Check quota availability + +### Directories (`/api/dirs`) +- `GET /api/dirs` - List all directories +- `GET /api/dirs/:id` - Get directory by ID +- `POST /api/dirs` - Create new directory +- `PUT /api/dirs/:id` - Update directory +- `DELETE /api/dirs/:id` - Delete directory +- `POST /api/dirs/create-path` - Create directory path +- `GET /api/dirs/:id/has-children` - Check if directory has children +- `GET /api/dirs/:id/children` - Get directory children + +### Files (`/api/files`) +- `GET /api/files` - List all files +- `GET /api/files/:id` - Get file by ID +- `POST /api/files` - Create new file +- `PUT /api/files/:id` - Update file +- `DELETE /api/files/:id` - Delete file +- `POST /api/files/:id/add-to-directory` - Add file to directory +- `POST /api/files/:id/remove-from-directory` - Remove file from directory +- `POST /api/files/:id/metadata` - Update file metadata +- `POST /api/files/:id/accessed` - Update accessed timestamp +- `GET /api/files/by-filesystem/:fs_id` - List files by filesystem + +### Blobs (`/api/blobs`) +- `GET /api/blobs` - List all blobs +- `GET /api/blobs/:id` - Get blob by ID +- `POST /api/blobs` - Create new blob +- `PUT /api/blobs/:id` - Update blob +- `DELETE /api/blobs/:id` - Delete blob +- `GET /api/blobs/:id/content` - Get blob raw content +- `GET /api/blobs/:id/verify` - Verify blob integrity + +### Symlinks (`/api/symlinks`) +- `GET /api/symlinks` - List all symlinks +- `GET /api/symlinks/:id` - Get symlink by ID +- `POST /api/symlinks` - Create new symlink +- `PUT /api/symlinks/:id` - Update symlink +- `DELETE /api/symlinks/:id` - Delete symlink +- `GET /api/symlinks/:id/is-broken` - Check if symlink is broken + +### Blob Membership (`/api/blob-membership`) +- `GET /api/blob-membership` - List all blob memberships +- `GET /api/blob-membership/:id` - Get blob membership by ID +- `POST /api/blob-membership` - Create new blob membership +- `DELETE /api/blob-membership/:id` - Delete blob membership + +### Filesystem Tools (`/api/tools`) +- `POST /api/tools/find` - Find files and directories +- `POST /api/tools/copy` - Copy files or directories +- `POST /api/tools/move` - Move files or directories +- `POST /api/tools/remove` - Remove files or directories +- `POST /api/tools/list` - List directory contents +- `POST /api/tools/import/file` - Import file from real filesystem +- `POST /api/tools/import/directory` - Import directory from real filesystem +- `POST /api/tools/export/file` - Export file to real filesystem +- `POST /api/tools/export/directory` - Export directory to real filesystem +- `POST /api/tools/content/:fs_id` - Get file content as text + +## Request/Response Format + +### Standard Response Structure +```json +{ + "success": true, + "data": { ... }, + "message": "Operation completed successfully", + "error": "" +} +``` + +### Error Response Structure +```json +{ + "success": false, + "error": "Error description", + "message": "User-friendly error message" +} +``` + +## Example Usage + +### Create a Filesystem +```bash +curl -X POST http://localhost:8080/api/fs \ + -H "Content-Type: application/json" \ + -d '{ + "name": "my_filesystem", + "description": "My test filesystem", + "quota_bytes": 1073741824 + }' +``` + +### Create a Directory +```bash +curl -X POST http://localhost:8080/api/dirs \ + -H "Content-Type: application/json" \ + -d '{ + "name": "documents", + "fs_id": 1, + "parent_id": 0, + "description": "Documents directory" + }' +``` + +### Find Files +```bash +curl -X POST http://localhost:8080/api/tools/find \ + -H "Content-Type: application/json" \ + -d '{ + "fs_id": 1, + "pattern": "*.txt", + "recursive": true + }' +``` + +### Import File +```bash +curl -X POST http://localhost:8080/api/tools/import/file \ + -H "Content-Type: application/json" \ + -d '{ + "fs_id": 1, + "real_path": "/path/to/local/file.txt", + "vfs_path": "/imported/file.txt", + "overwrite": false + }' +``` + +## HTTP Status Codes + +- `200 OK` - Successful operation +- `201 Created` - Resource created successfully +- `400 Bad Request` - Invalid request format or parameters +- `404 Not Found` - Resource not found +- `500 Internal Server Error` - Server error + +## CORS Support + +The server supports CORS for frontend integration. Configure allowed origins when creating the server: + +```v +mut server := herofs_server.new( + cors_enabled: true + allowed_origins: ['http://localhost:3000', 'https://myapp.com'] +)! +``` + +## Error Handling + +The API provides comprehensive error handling with: +- Input validation for all parameters +- Proper HTTP status codes +- Detailed error messages +- Consistent error response format + +## Integration with HeroFS + +The server integrates seamlessly with the HeroFS module, providing: +- Full access to all HeroFS functionality +- Proper factory pattern usage +- Data integrity through BLAKE3 hashing +- Efficient Redis-based storage +- Complete filesystem operations + +## Production Deployment + +For production use: +1. Configure appropriate CORS origins +2. Set up proper logging +3. Configure Redis connection +4. Set appropriate quotas and limits +5. Monitor server performance + +The server is designed to be production-ready with proper error handling, validation, and performance considerations. diff --git a/lib/hero/herofs_server/handlers_blob_symlink.v b/lib/hero/herofs_server/handlers_blob_symlink.v new file mode 100644 index 00000000..7379d74d --- /dev/null +++ b/lib/hero/herofs_server/handlers_blob_symlink.v @@ -0,0 +1,272 @@ +module herofs_server + +import veb +import json +import freeflowuniverse.herolib.hero.herofs + +// ============================================================================= +// BLOB ENDPOINTS +// ============================================================================= + +// List all blobs +@['/api/blobs'; get] +pub fn (mut server FSServer) list_blobs(mut ctx Context) veb.Result { + blob_ids := server.fs_factory.fs_blob.db.list[herofs.FsBlob]() or { + return ctx.server_error('Failed to list blob IDs: ${err}') + } + mut blobs := []herofs.FsBlob{} + for id in blob_ids { + blob := server.fs_factory.fs_blob.get(id) or { continue } + blobs << blob + } + return ctx.success(blobs, 'Blobs retrieved successfully') +} + +// Get blob by ID +@['/api/blobs/:id'; get] +pub fn (mut server FSServer) get_blob(mut ctx Context, id string) veb.Result { + blob_id := id.u32() + if blob_id == 0 { + return ctx.request_error('Invalid blob ID') + } + + blob := server.fs_factory.fs_blob.get(blob_id) or { return ctx.not_found('Blob not found') } + return ctx.success(blob, 'Blob retrieved successfully') +} + +// Create new blob +@['/api/blobs'; post] +pub fn (mut server FSServer) create_blob(mut ctx Context) veb.Result { + blob_args := json.decode(herofs.FsBlobArg, ctx.req.data) or { + return ctx.request_error('Invalid JSON format for blob creation') + } + + mut blob := server.fs_factory.fs_blob.new(blob_args) or { + return ctx.server_error('Failed to create blob: ${err}') + } + blob = server.fs_factory.fs_blob.set(blob) or { + return ctx.server_error('Failed to save blob: ${err}') + } + + return ctx.created(blob, 'Blob created successfully') +} + +// Update blob +@['/api/blobs/:id'; put] +pub fn (mut server FSServer) update_blob(mut ctx Context, id string) veb.Result { + blob_id := id.u32() + if blob_id == 0 { + return ctx.request_error('Invalid blob ID') + } + + mut blob := json.decode(herofs.FsBlob, ctx.req.data) or { + return ctx.request_error('Invalid JSON format for blob update') + } + blob.id = blob_id + + blob = server.fs_factory.fs_blob.set(blob) or { + return ctx.server_error('Failed to update blob: ${err}') + } + return ctx.success(blob, 'Blob updated successfully') +} + +// Delete blob +@['/api/blobs/:id'; delete] +pub fn (mut server FSServer) delete_blob(mut ctx Context, id string) veb.Result { + blob_id := id.u32() + if blob_id == 0 { + return ctx.request_error('Invalid blob ID') + } + + server.fs_factory.fs_blob.delete(blob_id) or { + return ctx.server_error('Failed to delete blob: ${err}') + } + return ctx.success('', 'Blob deleted successfully') +} + +// Get blob content (raw data) +@['/api/blobs/:id/content'; get] +pub fn (mut server FSServer) get_blob_content(mut ctx Context, id string) veb.Result { + blob_id := id.u32() + if blob_id == 0 { + return ctx.request_error('Invalid blob ID') + } + + blob := server.fs_factory.fs_blob.get(blob_id) or { return ctx.not_found('Blob not found') } + + // Set appropriate content type + if blob.mime_type.len > 0 { + ctx.set_content_type(blob.mime_type) + } else { + ctx.set_content_type('application/octet-stream') + } + + return ctx.text(blob.data.bytestr()) +} + +// Verify blob integrity +@['/api/blobs/:id/verify'; get] +pub fn (mut server FSServer) verify_blob_integrity(mut ctx Context, id string) veb.Result { + blob_id := id.u32() + if blob_id == 0 { + return ctx.request_error('Invalid blob ID') + } + + blob := server.fs_factory.fs_blob.get(blob_id) or { return ctx.not_found('Blob not found') } + is_valid := blob.verify_integrity() + + return ctx.success(is_valid, 'Blob integrity verified') +} + +// ============================================================================= +// SYMLINK ENDPOINTS +// ============================================================================= + +// List all symlinks +@['/api/symlinks'; get] +pub fn (mut server FSServer) list_symlinks(mut ctx Context) veb.Result { + symlinks := server.fs_factory.fs_symlink.list() or { + return ctx.server_error('Failed to list symlinks: ${err}') + } + return ctx.success(symlinks, 'Symlinks retrieved successfully') +} + +// Get symlink by ID +@['/api/symlinks/:id'; get] +pub fn (mut server FSServer) get_symlink(mut ctx Context, id string) veb.Result { + symlink_id := id.u32() + if symlink_id == 0 { + return ctx.request_error('Invalid symlink ID') + } + + symlink := server.fs_factory.fs_symlink.get(symlink_id) or { + return ctx.not_found('Symlink not found') + } + return ctx.success(symlink, 'Symlink retrieved successfully') +} + +// Create new symlink +@['/api/symlinks'; post] +pub fn (mut server FSServer) create_symlink(mut ctx Context) veb.Result { + symlink_args := json.decode(herofs.FsSymlinkArg, ctx.req.data) or { + return ctx.request_error('Invalid JSON format for symlink creation') + } + + mut symlink := server.fs_factory.fs_symlink.new(symlink_args) or { + return ctx.server_error('Failed to create symlink: ${err}') + } + symlink = server.fs_factory.fs_symlink.set(symlink) or { + return ctx.server_error('Failed to save symlink: ${err}') + } + + return ctx.created(symlink, 'Symlink created successfully') +} + +// Update symlink +@['/api/symlinks/:id'; put] +pub fn (mut server FSServer) update_symlink(mut ctx Context, id string) veb.Result { + symlink_id := id.u32() + if symlink_id == 0 { + return ctx.request_error('Invalid symlink ID') + } + + mut symlink := json.decode(herofs.FsSymlink, ctx.req.data) or { + return ctx.request_error('Invalid JSON format for symlink update') + } + symlink.id = symlink_id + + symlink = server.fs_factory.fs_symlink.set(symlink) or { + return ctx.server_error('Failed to update symlink: ${err}') + } + return ctx.success(symlink, 'Symlink updated successfully') +} + +// Delete symlink +@['/api/symlinks/:id'; delete] +pub fn (mut server FSServer) delete_symlink(mut ctx Context, id string) veb.Result { + symlink_id := id.u32() + if symlink_id == 0 { + return ctx.request_error('Invalid symlink ID') + } + + server.fs_factory.fs_symlink.delete(symlink_id) or { + return ctx.server_error('Failed to delete symlink: ${err}') + } + return ctx.success('', 'Symlink deleted successfully') +} + +// Check if symlink is broken +@['/api/symlinks/:id/is-broken'; get] +pub fn (mut server FSServer) symlink_is_broken(mut ctx Context, id string) veb.Result { + symlink_id := id.u32() + if symlink_id == 0 { + return ctx.request_error('Invalid symlink ID') + } + + is_broken := server.fs_factory.fs_symlink.is_broken(symlink_id) or { + return ctx.server_error('Failed to check symlink status: ${err}') + } + return ctx.success(is_broken, 'Symlink status checked') +} + +// ============================================================================= +// BLOB MEMBERSHIP ENDPOINTS +// ============================================================================= + +// List all blob memberships +@['/api/blob-membership'; get] +pub fn (mut server FSServer) list_blob_memberships(mut ctx Context) veb.Result { + // Get all blob membership hashes from Redis + hashes := server.fs_factory.fs_blob_membership.db.redis.hkeys('fs_blob_membership') or { + return ctx.server_error('Failed to list blob membership hashes: ${err}') + } + mut memberships := []herofs.FsBlobMembership{} + for hash in hashes { + membership := server.fs_factory.fs_blob_membership.get(hash) or { continue } + memberships << membership + } + return ctx.success(memberships, 'Blob memberships retrieved successfully') +} + +// Get blob membership by hash +@['/api/blob-membership/:hash'; get] +pub fn (mut server FSServer) get_blob_membership(mut ctx Context, hash string) veb.Result { + if hash == '' { + return ctx.request_error('Invalid membership hash') + } + + membership := server.fs_factory.fs_blob_membership.get(hash) or { + return ctx.not_found('Blob membership not found') + } + return ctx.success(membership, 'Blob membership retrieved successfully') +} + +// Create new blob membership +@['/api/blob-membership'; post] +pub fn (mut server FSServer) create_blob_membership(mut ctx Context) veb.Result { + membership_args := json.decode(herofs.FsBlobMembershipArg, ctx.req.data) or { + return ctx.request_error('Invalid JSON format for blob membership creation') + } + + mut membership := server.fs_factory.fs_blob_membership.new(membership_args) or { + return ctx.server_error('Failed to create blob membership: ${err}') + } + membership = server.fs_factory.fs_blob_membership.set(membership) or { + return ctx.server_error('Failed to save blob membership: ${err}') + } + + return ctx.created(membership, 'Blob membership created successfully') +} + +// Delete blob membership +@['/api/blob-membership/:hash'; delete] +pub fn (mut server FSServer) delete_blob_membership(mut ctx Context, hash string) veb.Result { + if hash == '' { + return ctx.request_error('Invalid blob membership hash') + } + + server.fs_factory.fs_blob_membership.delete(hash) or { + return ctx.server_error('Failed to delete blob membership: ${err}') + } + return ctx.success('', 'Blob membership deleted successfully') +} diff --git a/lib/hero/herofs_server/handlers_common.v b/lib/hero/herofs_server/handlers_common.v new file mode 100644 index 00000000..0233ef24 --- /dev/null +++ b/lib/hero/herofs_server/handlers_common.v @@ -0,0 +1,104 @@ +module herofs_server + +import veb + +// Standard API response structure +pub struct APIResponse[T] { +pub: + success bool + data T + message string + error string +} + +// Error response structure +pub struct ErrorResponse { +pub: + success bool + error string + message string +} + +// Helper function to create success response +pub fn success_response[T](data T, message string) APIResponse[T] { + return APIResponse[T]{ + success: true + data: data + message: message + error: '' + } +} + +// Helper function to create error response +pub fn error_response(error string, message string) ErrorResponse { + return ErrorResponse{ + error: error + message: message + } +} + +// Context extension methods for common HTTP responses +pub fn (mut ctx Context) success[T](data T, message string) veb.Result { + return ctx.json(success_response(data, message)) +} + +pub fn (mut ctx Context) request_error(message string) veb.Result { + ctx.res.status_code = 400 + return ctx.json(error_response('Bad Request', message)) +} + +pub fn (mut ctx Context) not_found(message ...string) veb.Result { + ctx.res.status_code = 404 + msg := if message.len > 0 { message[0] } else { 'Resource not found' } + return ctx.json(error_response('Not Found', msg)) +} + +pub fn (mut ctx Context) server_error(message string) veb.Result { + ctx.res.status_code = 500 + return ctx.json(error_response('Internal Server Error', message)) +} + +pub fn (mut ctx Context) created[T](data T, message string) veb.Result { + ctx.res.status_code = 201 + return ctx.json(success_response(data, message)) +} + +// Health check endpoint +@['/health'; get] +pub fn (mut server FSServer) health_check(mut ctx Context) veb.Result { + return ctx.success('OK', 'HeroFS Server is running') +} + +// API info endpoint +@['/api'; get] +pub fn (mut server FSServer) api_info(mut ctx Context) veb.Result { + mut endpoints := map[string]string{} + endpoints['filesystems'] = '/api/fs' + endpoints['directories'] = '/api/dirs' + endpoints['files'] = '/api/files' + endpoints['blobs'] = '/api/blobs' + endpoints['symlinks'] = '/api/symlinks' + endpoints['blob_membership'] = '/api/blob-membership' + endpoints['tools'] = '/api/tools' + + mut info := map[string]string{} + info['name'] = 'HeroFS REST API' + info['version'] = '1.0.0' + info['description'] = 'RESTful API for HeroFS distributed filesystem' + + mut response_data := map[string]map[string]string{} + response_data['info'] = info.clone() + response_data['endpoints'] = endpoints.clone() + return ctx.success(response_data, 'API information') +} + +// CORS preflight handler +@['/api/:path...'; options] +pub fn (mut server FSServer) cors_preflight(mut ctx Context, path string) veb.Result { + ctx.res.header.add(.access_control_allow_origin, '*') + ctx.res.header.add(.access_control_allow_methods, 'GET, POST, PUT, DELETE, OPTIONS') + ctx.res.header.add(.access_control_allow_headers, 'Content-Type, Authorization') + ctx.res.header.add(.access_control_max_age, '86400') + ctx.res.status_code = 204 + return ctx.text('') +} diff --git a/lib/hero/herofs_server/handlers_directory.v b/lib/hero/herofs_server/handlers_directory.v new file mode 100644 index 00000000..5cacb5f6 --- /dev/null +++ b/lib/hero/herofs_server/handlers_directory.v @@ -0,0 +1,129 @@ +module herofs_server + +import veb +import json +import freeflowuniverse.herolib.hero.herofs + +// ============================================================================= +// DIRECTORY ENDPOINTS +// ============================================================================= + +// List all directories +@['/api/dirs'; get] +pub fn (mut server FSServer) list_directories(mut ctx Context) veb.Result { + directories := server.fs_factory.fs_dir.list() or { + return ctx.server_error('Failed to list directories: ${err}') + } + return ctx.success(directories, 'Directories retrieved successfully') +} + +// Get directory by ID +@['/api/dirs/:id'; get] +pub fn (mut server FSServer) get_directory(mut ctx Context, id string) veb.Result { + dir_id := id.u32() + if dir_id == 0 { + return ctx.request_error('Invalid directory ID') + } + + directory := server.fs_factory.fs_dir.get(dir_id) or { + return ctx.not_found('Directory not found') + } + return ctx.success(directory, 'Directory retrieved successfully') +} + +// Create new directory +@['/api/dirs'; post] +pub fn (mut server FSServer) create_directory(mut ctx Context) veb.Result { + dir_args := json.decode(herofs.FsDirArg, ctx.req.data) or { + return ctx.request_error('Invalid JSON format for directory creation') + } + + mut directory := server.fs_factory.fs_dir.new(dir_args) or { + return ctx.server_error('Failed to create directory: ${err}') + } + directory = server.fs_factory.fs_dir.set(directory) or { + return ctx.server_error('Failed to save directory: ${err}') + } + + return ctx.created(directory, 'Directory created successfully') +} + +// Update directory +@['/api/dirs/:id'; put] +pub fn (mut server FSServer) update_directory(mut ctx Context, id string) veb.Result { + dir_id := id.u32() + if dir_id == 0 { + return ctx.request_error('Invalid directory ID') + } + + mut directory := json.decode(herofs.FsDir, ctx.req.data) or { + return ctx.request_error('Invalid JSON format for directory update') + } + directory.id = dir_id + + directory = server.fs_factory.fs_dir.set(directory) or { + return ctx.server_error('Failed to update directory: ${err}') + } + return ctx.success(directory, 'Directory updated successfully') +} + +// Delete directory +@['/api/dirs/:id'; delete] +pub fn (mut server FSServer) delete_directory(mut ctx Context, id string) veb.Result { + dir_id := id.u32() + if dir_id == 0 { + return ctx.request_error('Invalid directory ID') + } + + server.fs_factory.fs_dir.delete(dir_id) or { + return ctx.server_error('Failed to delete directory: ${err}') + } + return ctx.success('', 'Directory deleted successfully') +} + +// Create directory path +@['/api/dirs/create-path'; post] +pub fn (mut server FSServer) create_directory_path(mut ctx Context) veb.Result { + path_data := json.decode(map[string]string, ctx.req.data) or { + return ctx.request_error('Invalid JSON format for path creation') + } + fs_id := path_data['fs_id'] or { return ctx.request_error('Missing fs_id field') }.u32() + path := path_data['path'] or { return ctx.request_error('Missing path field') } + + if fs_id == 0 { + return ctx.request_error('Invalid filesystem ID') + } + + dir_id := server.fs_factory.fs_dir.create_path(fs_id, path) or { + return ctx.server_error('Failed to create directory path: ${err}') + } + return ctx.success(dir_id, 'Directory path created successfully') +} + +// Check if directory has children +@['/api/dirs/:id/has-children'; get] +pub fn (mut server FSServer) directory_has_children(mut ctx Context, id string) veb.Result { + dir_id := id.u32() + if dir_id == 0 { + return ctx.request_error('Invalid directory ID') + } + + has_children := server.fs_factory.fs_dir.has_children(dir_id) or { + return ctx.server_error('Failed to check directory children: ${err}') + } + return ctx.success(has_children, 'Directory children status checked') +} + +// Get directory children +@['/api/dirs/:id/children'; get] +pub fn (mut server FSServer) get_directory_children(mut ctx Context, id string) veb.Result { + dir_id := id.u32() + if dir_id == 0 { + return ctx.request_error('Invalid directory ID') + } + + children := server.fs_factory.fs_dir.list_children(dir_id) or { + return ctx.server_error('Failed to get directory children: ${err}') + } + return ctx.success(children, 'Directory children retrieved successfully') +} diff --git a/lib/hero/herofs_server/handlers_file.v b/lib/hero/herofs_server/handlers_file.v new file mode 100644 index 00000000..b992eef5 --- /dev/null +++ b/lib/hero/herofs_server/handlers_file.v @@ -0,0 +1,179 @@ +module herofs_server + +import veb +import json +import freeflowuniverse.herolib.hero.herofs + +// ============================================================================= +// FILE ENDPOINTS +// ============================================================================= + +// List all files +@['/api/files'; get] +pub fn (mut server FSServer) list_files(mut ctx Context) veb.Result { + files := server.fs_factory.fs_file.list() or { + return ctx.server_error('Failed to list files: ${err}') + } + return ctx.success(files, 'Files retrieved successfully') +} + +// Get file by ID +@['/api/files/:id'; get] +pub fn (mut server FSServer) get_file(mut ctx Context, id string) veb.Result { + file_id := id.u32() + if file_id == 0 { + return ctx.request_error('Invalid file ID') + } + + file := server.fs_factory.fs_file.get(file_id) or { return ctx.not_found('File not found') } + return ctx.success(file, 'File retrieved successfully') +} + +// Create new file +@['/api/files'; post] +pub fn (mut server FSServer) create_file(mut ctx Context) veb.Result { + file_args := json.decode(herofs.FsFileArg, ctx.req.data) or { + return ctx.request_error('Invalid JSON format for file creation') + } + + mut file := server.fs_factory.fs_file.new(file_args) or { + return ctx.server_error('Failed to create file: ${err}') + } + file = server.fs_factory.fs_file.set(file) or { + return ctx.server_error('Failed to save file: ${err}') + } + + return ctx.created(file, 'File created successfully') +} + +// Update file +@['/api/files/:id'; put] +pub fn (mut server FSServer) update_file(mut ctx Context, id string) veb.Result { + file_id := id.u32() + if file_id == 0 { + return ctx.request_error('Invalid file ID') + } + + mut file := json.decode(herofs.FsFile, ctx.req.data) or { + return ctx.request_error('Invalid JSON format for file update') + } + file.id = file_id + + file = server.fs_factory.fs_file.set(file) or { + return ctx.server_error('Failed to update file: ${err}') + } + return ctx.success(file, 'File updated successfully') +} + +// Delete file +@['/api/files/:id'; delete] +pub fn (mut server FSServer) delete_file(mut ctx Context, id string) veb.Result { + file_id := id.u32() + if file_id == 0 { + return ctx.request_error('Invalid file ID') + } + + server.fs_factory.fs_file.delete(file_id) or { + return ctx.server_error('Failed to delete file: ${err}') + } + return ctx.success('', 'File deleted successfully') +} + +// Add file to directory +@['/api/files/:id/add-to-directory'; post] +pub fn (mut server FSServer) add_file_to_directory(mut ctx Context, id string) veb.Result { + file_id := id.u32() + if file_id == 0 { + return ctx.request_error('Invalid file ID') + } + + dir_data := json.decode(map[string]u32, ctx.req.data) or { + return ctx.request_error('Invalid JSON format for directory data') + } + dir_id := dir_data['dir_id'] or { return ctx.request_error('Missing dir_id field') } + + server.fs_factory.fs_file.add_to_directory(file_id, dir_id) or { + return ctx.server_error('Failed to add file to directory: ${err}') + } + return ctx.success('', 'File added to directory successfully') +} + +// Remove file from directory +@['/api/files/:id/remove-from-directory'; post] +pub fn (mut server FSServer) remove_file_from_directory(mut ctx Context, id string) veb.Result { + file_id := id.u32() + if file_id == 0 { + return ctx.request_error('Invalid file ID') + } + + dir_data := json.decode(map[string]u32, ctx.req.data) or { + return ctx.request_error('Invalid JSON format for directory data') + } + dir_id := dir_data['dir_id'] or { return ctx.request_error('Missing dir_id field') } + + // Get the file and remove the directory from its directories list + mut file := server.fs_factory.fs_file.get(file_id) or { return ctx.not_found('File not found') } + file.directories = file.directories.filter(it != dir_id) + server.fs_factory.fs_file.set(file) or { + return ctx.server_error('Failed to update file directories: ${err}') + } + + // Get the directory and remove the file from its files list + mut dir := server.fs_factory.fs_dir.get(dir_id) or { + return ctx.not_found('Directory not found') + } + dir.files = dir.files.filter(it != file_id) + server.fs_factory.fs_dir.set(dir) or { + return ctx.server_error('Failed to update directory files: ${err}') + } + + return ctx.success('', 'File removed from directory successfully') +} + +// Update file metadata +@['/api/files/:id/metadata'; post] +pub fn (mut server FSServer) update_file_metadata(mut ctx Context, id string) veb.Result { + file_id := id.u32() + if file_id == 0 { + return ctx.request_error('Invalid file ID') + } + + metadata := json.decode(map[string]string, ctx.req.data) or { + return ctx.request_error('Invalid JSON format for metadata') + } + key := metadata['key'] or { return ctx.request_error('Missing key field') } + value := metadata['value'] or { return ctx.request_error('Missing value field') } + + server.fs_factory.fs_file.update_metadata(file_id, key, value) or { + return ctx.server_error('Failed to update file metadata: ${err}') + } + return ctx.success('', 'File metadata updated successfully') +} + +// Update file accessed timestamp +@['/api/files/:id/accessed'; post] +pub fn (mut server FSServer) update_file_accessed(mut ctx Context, id string) veb.Result { + file_id := id.u32() + if file_id == 0 { + return ctx.request_error('Invalid file ID') + } + + server.fs_factory.fs_file.update_accessed(file_id) or { + return ctx.server_error('Failed to update file accessed timestamp: ${err}') + } + return ctx.success('', 'File accessed timestamp updated successfully') +} + +// List files by filesystem +@['/api/files/by-filesystem/:fs_id'; get] +pub fn (mut server FSServer) list_files_by_filesystem(mut ctx Context, fs_id string) veb.Result { + filesystem_id := fs_id.u32() + if filesystem_id == 0 { + return ctx.request_error('Invalid filesystem ID') + } + + files := server.fs_factory.fs_file.list_by_filesystem(filesystem_id) or { + return ctx.server_error('Failed to list files by filesystem: ${err}') + } + return ctx.success(files, 'Files by filesystem retrieved successfully') +} diff --git a/lib/hero/herofs_server/handlers_filesystem.v b/lib/hero/herofs_server/handlers_filesystem.v new file mode 100644 index 00000000..fcddb5b4 --- /dev/null +++ b/lib/hero/herofs_server/handlers_filesystem.v @@ -0,0 +1,153 @@ +module herofs_server + +import veb +import json +import freeflowuniverse.herolib.hero.herofs + +// ============================================================================= +// FILESYSTEM ENDPOINTS +// ============================================================================= + +// List all filesystems +@['/api/fs'; get] +pub fn (mut server FSServer) list_filesystems(mut ctx Context) veb.Result { + filesystems := server.fs_factory.fs.list() or { + return ctx.server_error('Failed to list filesystems: ${err}') + } + return ctx.success(filesystems, 'Filesystems retrieved successfully') +} + +// Get filesystem by ID +@['/api/fs/:id'; get] +pub fn (mut server FSServer) get_filesystem(mut ctx Context, id string) veb.Result { + fs_id := id.u32() + if fs_id == 0 { + return ctx.request_error('Invalid filesystem ID') + } + + filesystem := server.fs_factory.fs.get(fs_id) or { + return ctx.not_found('Filesystem not found') + } + return ctx.success(filesystem, 'Filesystem retrieved successfully') +} + +// Create new filesystem +@['/api/fs'; post] +pub fn (mut server FSServer) create_filesystem(mut ctx Context) veb.Result { + fs_args := json.decode(herofs.FsArg, ctx.req.data) or { + return ctx.request_error('Invalid JSON format for filesystem creation') + } + + mut filesystem := server.fs_factory.fs.new(fs_args) or { + return ctx.server_error('Failed to create filesystem: ${err}') + } + filesystem = server.fs_factory.fs.set(filesystem) or { + return ctx.server_error('Failed to save filesystem: ${err}') + } + + return ctx.created(filesystem, 'Filesystem created successfully') +} + +// Update filesystem +@['/api/fs/:id'; put] +pub fn (mut server FSServer) update_filesystem(mut ctx Context, id string) veb.Result { + fs_id := id.u32() + if fs_id == 0 { + return ctx.request_error('Invalid filesystem ID') + } + + mut filesystem := json.decode(herofs.Fs, ctx.req.data) or { + return ctx.request_error('Invalid JSON format for filesystem update') + } + filesystem.id = fs_id + + filesystem = server.fs_factory.fs.set(filesystem) or { + return ctx.server_error('Failed to update filesystem: ${err}') + } + return ctx.success(filesystem, 'Filesystem updated successfully') +} + +// Delete filesystem +@['/api/fs/:id'; delete] +pub fn (mut server FSServer) delete_filesystem(mut ctx Context, id string) veb.Result { + fs_id := id.u32() + if fs_id == 0 { + return ctx.request_error('Invalid filesystem ID') + } + + server.fs_factory.fs.delete(fs_id) or { + return ctx.server_error('Failed to delete filesystem: ${err}') + } + return ctx.success('', 'Filesystem deleted successfully') +} + +// Check if filesystem exists +@['/api/fs/:id/exists'; get] +pub fn (mut server FSServer) filesystem_exists(mut ctx Context, id string) veb.Result { + fs_id := id.u32() + if fs_id == 0 { + return ctx.request_error('Invalid filesystem ID') + } + + exists := server.fs_factory.fs.exist(fs_id) or { + return ctx.server_error('Failed to check filesystem existence: ${err}') + } + return ctx.success(exists, 'Filesystem existence checked') +} + +// Increase filesystem usage +@['/api/fs/:id/usage/increase'; post] +pub fn (mut server FSServer) increase_filesystem_usage(mut ctx Context, id string) veb.Result { + fs_id := id.u32() + if fs_id == 0 { + return ctx.request_error('Invalid filesystem ID') + } + + usage_data := json.decode(map[string]u64, ctx.req.data) or { + return ctx.request_error('Invalid JSON format for usage data') + } + bytes := usage_data['bytes'] or { return ctx.request_error('Missing bytes field') } + + server.fs_factory.fs.increase_usage(fs_id, bytes) or { + return ctx.server_error('Failed to increase filesystem usage: ${err}') + } + return ctx.success('', 'Filesystem usage increased successfully') +} + +// Decrease filesystem usage +@['/api/fs/:id/usage/decrease'; post] +pub fn (mut server FSServer) decrease_filesystem_usage(mut ctx Context, id string) veb.Result { + fs_id := id.u32() + if fs_id == 0 { + return ctx.request_error('Invalid filesystem ID') + } + + usage_data := json.decode(map[string]u64, ctx.req.data) or { + return ctx.request_error('Invalid JSON format for usage data') + } + bytes := usage_data['bytes'] or { return ctx.request_error('Missing bytes field') } + + server.fs_factory.fs.decrease_usage(fs_id, bytes) or { + return ctx.server_error('Failed to decrease filesystem usage: ${err}') + } + return ctx.success('', 'Filesystem usage decreased successfully') +} + +// Check filesystem quota +@['/api/fs/:id/quota/check'; post] +pub fn (mut server FSServer) check_filesystem_quota(mut ctx Context, id string) veb.Result { + fs_id := id.u32() + if fs_id == 0 { + return ctx.request_error('Invalid filesystem ID') + } + + quota_data := json.decode(map[string]u64, ctx.req.data) or { + return ctx.request_error('Invalid JSON format for quota data') + } + bytes := quota_data['bytes'] or { return ctx.request_error('Missing bytes field') } + + can_add := server.fs_factory.fs.check_quota(fs_id, bytes) or { + return ctx.server_error('Failed to check filesystem quota: ${err}') + } + return ctx.success(can_add, 'Filesystem quota checked') +} diff --git a/lib/hero/herofs_server/handlers_tools.v b/lib/hero/herofs_server/handlers_tools.v new file mode 100644 index 00000000..a9cbeae4 --- /dev/null +++ b/lib/hero/herofs_server/handlers_tools.v @@ -0,0 +1,323 @@ +module herofs_server + +import veb +import json +import freeflowuniverse.herolib.hero.herofs + +// ============================================================================= +// FILESYSTEM TOOLS ENDPOINTS +// ============================================================================= + +// Find files and directories +@['/api/tools/find'; post] +pub fn (mut server FSServer) find_files(mut ctx Context) veb.Result { + find_data := json.decode(map[string]string, ctx.req.data) or { + return ctx.request_error('Invalid JSON format for find operation') + } + + fs_id := find_data['fs_id'] or { return ctx.request_error('Missing fs_id field') }.u32() + path := find_data['path'] or { return ctx.request_error('Missing path field') } + + // Parse optional parameters + mut recursive := true + mut include_patterns := []string{} + mut exclude_patterns := []string{} + + if 'recursive' in find_data { + recursive = find_data['recursive'] == 'true' + } + if 'include_patterns' in find_data { + patterns_str := find_data['include_patterns'] or { '' } + if patterns_str.len > 0 { + include_patterns = patterns_str.split(',').map(it.trim_space()) + } + } + if 'exclude_patterns' in find_data { + patterns_str := find_data['exclude_patterns'] or { '' } + if patterns_str.len > 0 { + exclude_patterns = patterns_str.split(',').map(it.trim_space()) + } + } + + // Get filesystem instance + mut fs := server.fs_factory.fs.get(fs_id) or { + return ctx.request_error('Filesystem not found') + } + + results := fs.find(path, herofs.FindOptions{ + recursive: recursive + include_patterns: include_patterns + exclude_patterns: exclude_patterns + }) or { return ctx.server_error('Failed to perform find operation: ${err}') } + return ctx.success(results, 'Find operation completed successfully') +} + +// Copy files or directories +@['/api/tools/copy'; post] +pub fn (mut server FSServer) copy_files(mut ctx Context) veb.Result { + copy_data := json.decode(map[string]string, ctx.req.data) or { + return ctx.request_error('Invalid JSON format for copy operation') + } + + fs_id := copy_data['fs_id'] or { return ctx.request_error('Missing fs_id field') }.u32() + source_path := copy_data['source_path'] or { + return ctx.request_error('Missing source_path field') + } + dest_path := copy_data['dest_path'] or { return ctx.request_error('Missing dest_path field') } + + if fs_id == 0 { + return ctx.request_error('Invalid filesystem ID') + } + + // Get filesystem instance + mut fs := server.fs_factory.fs.get(fs_id) or { + return ctx.request_error('Filesystem not found') + } + + fs.cp(source_path, dest_path, herofs.FindOptions{ recursive: false }, herofs.CopyOptions{ + recursive: true + overwrite: false + copy_blobs: true + }) or { return ctx.server_error('Failed to copy: ${err}') } + return ctx.success('', 'Copy operation completed successfully') +} + +// Move files or directories +@['/api/tools/move'; post] +pub fn (mut server FSServer) move_files(mut ctx Context) veb.Result { + move_data := json.decode(map[string]string, ctx.req.data) or { + return ctx.request_error('Invalid JSON format for move operation') + } + + fs_id := move_data['fs_id'] or { return ctx.request_error('Missing fs_id field') }.u32() + source_path := move_data['source_path'] or { + return ctx.request_error('Missing source_path field') + } + dest_path := move_data['dest_path'] or { return ctx.request_error('Missing dest_path field') } + + if fs_id == 0 { + return ctx.request_error('Invalid filesystem ID') + } + + // Get filesystem instance + mut fs := server.fs_factory.fs.get(fs_id) or { + return ctx.request_error('Filesystem not found') + } + + fs.mv(source_path, dest_path) or { return ctx.server_error('Failed to move: ${err}') } + return ctx.success('', 'Move operation completed successfully') +} + +// Remove files or directories +@['/api/tools/remove'; post] +pub fn (mut server FSServer) remove_files(mut ctx Context) veb.Result { + remove_data := json.decode(map[string]string, ctx.req.data) or { + return ctx.request_error('Invalid JSON format for remove operation') + } + + fs_id := remove_data['fs_id'] or { return ctx.request_error('Missing fs_id field') }.u32() + path := remove_data['path'] or { return ctx.request_error('Missing path field') } + + if fs_id == 0 { + return ctx.request_error('Invalid filesystem ID') + } + + // Get filesystem instance + mut fs := server.fs_factory.fs.get(fs_id) or { + return ctx.request_error('Filesystem not found') + } + + fs.rm(path, herofs.FindOptions{ recursive: false }, herofs.RemoveOptions{ + recursive: true + delete_blobs: false + force: false + }) or { return ctx.server_error('Failed to remove: ${err}') } + return ctx.success('', 'Remove operation completed successfully') +} + +// List directory contents +@['/api/tools/list'; post] +pub fn (mut server FSServer) list_directory(mut ctx Context) veb.Result { + list_data := json.decode(map[string]string, ctx.req.data) or { + return ctx.request_error('Invalid JSON format for list operation') + } + + fs_id := list_data['fs_id'] or { return ctx.request_error('Missing fs_id field') }.u32() + path := list_data['path'] or { return ctx.request_error('Missing path field') } + + if fs_id == 0 { + return ctx.request_error('Invalid filesystem ID') + } + + // Get filesystem instance + mut fs := server.fs_factory.fs.get(fs_id) or { + return ctx.request_error('Filesystem not found') + } + + // Use find to list directory contents + contents := fs.find(path, herofs.FindOptions{ recursive: false }) or { + return ctx.server_error('Failed to list directory: ${err}') + } + return ctx.success(contents, 'Directory listing completed successfully') +} + +// ============================================================================= +// IMPORT/EXPORT ENDPOINTS +// ============================================================================= + +// Import file from real filesystem +@['/api/tools/import/file'; post] +pub fn (mut server FSServer) import_file(mut ctx Context) veb.Result { + import_data := json.decode(map[string]string, ctx.req.data) or { + return ctx.request_error('Invalid JSON format for import operation') + } + + fs_id := import_data['fs_id'] or { return ctx.request_error('Missing fs_id field') }.u32() + real_path := import_data['real_path'] or { return ctx.request_error('Missing real_path field') } + vfs_path := import_data['vfs_path'] or { return ctx.request_error('Missing vfs_path field') } + overwrite := import_data['overwrite'] or { 'false' } == 'true' + + if fs_id == 0 { + return ctx.request_error('Invalid filesystem ID') + } + + // Get filesystem instance + mut fs := server.fs_factory.fs.get(fs_id) or { + return ctx.request_error('Filesystem not found') + } + + fs.import(real_path, vfs_path, herofs.ImportOptions{ + recursive: false + overwrite: overwrite + }) or { return ctx.server_error('Failed to import file: ${err}') } + return ctx.success('', 'File imported successfully') +} + +// Import directory from real filesystem +@['/api/tools/import/directory'; post] +pub fn (mut server FSServer) import_directory(mut ctx Context) veb.Result { + import_data := json.decode(map[string]string, ctx.req.data) or { + return ctx.request_error('Invalid JSON format for import operation') + } + + fs_id := import_data['fs_id'] or { return ctx.request_error('Missing fs_id field') }.u32() + real_path := import_data['real_path'] or { return ctx.request_error('Missing real_path field') } + vfs_path := import_data['vfs_path'] or { return ctx.request_error('Missing vfs_path field') } + overwrite := import_data['overwrite'] or { 'false' } == 'true' + + if fs_id == 0 { + return ctx.request_error('Invalid filesystem ID') + } + + // Get filesystem instance + mut fs := server.fs_factory.fs.get(fs_id) or { + return ctx.request_error('Filesystem not found') + } + + fs.import(real_path, vfs_path, herofs.ImportOptions{ + recursive: true + overwrite: overwrite + }) or { return ctx.server_error('Failed to import directory: ${err}') } + return ctx.success('', 'Directory imported successfully') +} + +// Export file to real filesystem +@['/api/tools/export/file'; post] +pub fn (mut server FSServer) export_file(mut ctx Context) veb.Result { + export_data := json.decode(map[string]string, ctx.req.data) or { + return ctx.request_error('Invalid JSON format for export operation') + } + + fs_id := export_data['fs_id'] or { return ctx.request_error('Missing fs_id field') }.u32() + vfs_path := export_data['vfs_path'] or { return ctx.request_error('Missing vfs_path field') } + real_path := export_data['real_path'] or { return ctx.request_error('Missing real_path field') } + overwrite := export_data['overwrite'] or { 'false' } == 'true' + + if fs_id == 0 { + return ctx.request_error('Invalid filesystem ID') + } + + // Get filesystem instance + mut fs := server.fs_factory.fs.get(fs_id) or { + return ctx.request_error('Filesystem not found') + } + + fs.export(vfs_path, real_path, herofs.ExportOptions{ + recursive: false + overwrite: overwrite + }) or { return ctx.server_error('Failed to export file: ${err}') } + return ctx.success('', 'File exported successfully') +} + +// Export directory to real filesystem +@['/api/tools/export/directory'; post] +pub fn (mut server FSServer) export_directory(mut ctx Context) veb.Result { + export_data := json.decode(map[string]string, ctx.req.data) or { + return ctx.request_error('Invalid JSON format for export operation') + } + + fs_id := export_data['fs_id'] or { return ctx.request_error('Missing fs_id field') }.u32() + vfs_path := export_data['vfs_path'] or { return ctx.request_error('Missing vfs_path field') } + real_path := export_data['real_path'] or { return ctx.request_error('Missing real_path field') } + overwrite := export_data['overwrite'] or { 'false' } == 'true' + + if fs_id == 0 { + return ctx.request_error('Invalid filesystem ID') + } + + // Get filesystem instance + mut fs := server.fs_factory.fs.get(fs_id) or { + return ctx.request_error('Filesystem not found') + } + + fs.export(vfs_path, real_path, herofs.ExportOptions{ + recursive: true + overwrite: overwrite + }) or { return ctx.server_error('Failed to export directory: ${err}') } + return ctx.success('', 'Directory exported successfully') +} + +// Get file content as text +@['/api/tools/content/:fs_id'; post] +pub fn (mut server FSServer) get_file_content(mut ctx Context, fs_id string) veb.Result { + filesystem_id := fs_id.u32() + if filesystem_id == 0 { + return ctx.request_error('Invalid filesystem ID') + } + + content_data := json.decode(map[string]string, ctx.req.data) or { + return ctx.request_error('Invalid JSON format for content request') + } + path := content_data['path'] or { return ctx.request_error('Missing path field') } + + // Get filesystem instance + mut fs := server.fs_factory.fs.get(filesystem_id) or { + return ctx.request_error('Filesystem not found') + } + + // Find the file by path + file_results := fs.find(path, herofs.FindOptions{ recursive: false }) or { + return ctx.server_error('Failed to find file: ${err}') + } + + if file_results.len == 0 { + return ctx.request_error('File not found at path: ${path}') + } + + file_result := file_results[0] + if file_result.result_type != .file { + return ctx.request_error('Path does not point to a file: ${path}') + } + + // Get the file and its content + file := server.fs_factory.fs_file.get(file_result.id) or { + return ctx.server_error('Failed to get file: ${err}') + } + + mut content := '' + for blob_id in file.blobs { + blob := server.fs_factory.fs_blob.get(blob_id) or { continue } + content += blob.data.bytestr() + } + return ctx.success(content, 'File content retrieved successfully') +} diff --git a/lib/hero/herofs_server/middlewares.v b/lib/hero/herofs_server/middlewares.v new file mode 100644 index 00000000..a5927e12 --- /dev/null +++ b/lib/hero/herofs_server/middlewares.v @@ -0,0 +1,43 @@ +module herofs_server + +import net.http +import freeflowuniverse.herolib.ui.console + +// Request logging middleware +pub fn (mut server FSServer) middleware_log_request(mut ctx Context) { + console.print_debug('${ctx.req.method} ${ctx.req.url} from ${ctx.ip()}') +} + +// Response logging middleware +pub fn (mut server FSServer) middleware_log_response(mut ctx Context) { + console.print_debug('Response: ${ctx.res.status_code}') +} + +// Error handling middleware +pub fn (mut server FSServer) middleware_error_handler(mut ctx Context) { + // This will be called for unhandled errors + console.print_debug('Error handling middleware called') +} + +// Content type middleware for JSON APIs +pub fn (mut server FSServer) middleware_json_content_type(mut ctx Context) { + if ctx.req.url.starts_with('/api/') { + ctx.set_content_type('application/json') + } +} + +// Request validation middleware +pub fn (mut server FSServer) middleware_validate_request(mut ctx Context) { + // Validate request size + if ctx.req.data.len > 10 * 1024 * 1024 { // 10MB limit + console.print_debug('Request too large: ${ctx.req.data.len} bytes') + } + + // Validate content type for POST/PUT requests + if ctx.req.method in [http.Method.post, http.Method.put] { + content_type := ctx.get_header(.content_type) or { '' } + if !content_type.contains('application/json') && ctx.req.url.starts_with('/api/') { + console.print_debug('Invalid content type for API request: ${content_type}') + } + } +} diff --git a/lib/hero/herofs_server/server.v b/lib/hero/herofs_server/server.v new file mode 100644 index 00000000..fb4fcd38 --- /dev/null +++ b/lib/hero/herofs_server/server.v @@ -0,0 +1,79 @@ +module herofs_server + +import veb +import freeflowuniverse.herolib.hero.herofs +import freeflowuniverse.herolib.ui.console + +// FSServer is the main server struct +pub struct FSServer { + veb.Controller +pub mut: + fs_factory herofs.FsFactory + port int + host string + cors_enabled bool + allowed_origins []string +} + +// Context struct for VEB +pub struct Context { + veb.Context +} + +// Factory args +@[params] +pub struct NewFSServerArgs { +pub mut: + port int = 8080 + host string = 'localhost' + cors_enabled bool = true + allowed_origins []string = ['*'] +} + +// Create a new filesystem server +pub fn new(args NewFSServerArgs) !&FSServer { + fs_factory := herofs.new()! + + mut server := FSServer{ + port: args.port + host: args.host + cors_enabled: args.cors_enabled + allowed_origins: args.allowed_origins + fs_factory: fs_factory + } + + return &server +} + +pub fn (mut server FSServer) start() ! { + console.print_header('Starting HeroFS Server on ${server.host}:${server.port}') + console.print_item('CORS enabled: ${server.cors_enabled}') + if server.cors_enabled { + console.print_item('Allowed origins: ${server.allowed_origins}') + } + console.print_item('Available endpoints:') + console.print_item(' Health: GET /health') + console.print_item(' API Info: GET /api') + console.print_item(' Filesystems: /api/fs') + console.print_item(' Directories: /api/dirs') + console.print_item(' Files: /api/files') + console.print_item(' Blobs: /api/blobs') + console.print_item(' Symlinks: /api/symlinks') + console.print_item(' Tools: /api/tools') + + veb.run[FSServer, Context](mut server, server.port) +} + +// Global error handler +pub fn (mut server FSServer) before_request(mut ctx Context) { + if server.cors_enabled { + // Set CORS headers for all requests + for origin in server.allowed_origins { + if origin == '*' || ctx.get_header(.origin) or { '' } == origin { + ctx.set_header(.access_control_allow_origin, origin) + break + } + } + ctx.set_header(.access_control_allow_credentials, 'true') + } +}