add file browser component and widget

This commit is contained in:
Timur Gordon
2025-08-05 15:02:23 +02:00
parent 4e43c21b72
commit ba43a82db0
95 changed files with 17840 additions and 423 deletions

135
examples/README.md Normal file
View File

@@ -0,0 +1,135 @@
# Framework Examples
This directory contains examples demonstrating how to use the Hero Framework components.
## WebSocket Manager Demo
The `ws_manager_demo.rs` example demonstrates the complete usage of the `WsManager` for connecting to multiple WebSocket servers, handling authentication, and executing scripts.
### Prerequisites
1. **Start Hero WebSocket Server(s)**:
```bash
# Terminal 1 - Start first server
cd ../hero/interfaces/websocket/server
cargo run --example circle_auth_demo -- --port 3030
# Terminal 2 - Start second server (optional)
cargo run --example circle_auth_demo -- --port 3031
```
2. **Run the Example**:
```bash
# From the framework directory
cargo run --example ws_manager_demo
```
### What the Demo Shows
The example demonstrates:
- **Multi-server Connection**: Connecting to multiple WebSocket servers simultaneously
- **Authentication**: Using private keys for server authentication
- **Script Execution**: Running JavaScript/Rhai scripts on connected servers
- **Connection Management**: Connecting, disconnecting, and reconnecting to specific servers
- **Status Monitoring**: Checking connection and authentication status
- **Runtime Management**: Adding new connections dynamically
- **Error Handling**: Graceful handling of connection and execution errors
### Key Code Patterns
#### Creating and Connecting
```rust
let manager = WsManager::builder()
.private_key("your_private_key_hex".to_string())
.add_server_url("ws://localhost:3030".to_string())
.add_server_url("ws://localhost:3031".to_string())
.build();
manager.connect().await?;
```
#### Executing Scripts on Specific Servers
```rust
if let Some(result) = manager.with_client("ws://localhost:3030", |client| {
client.play("console.log('Hello World!');".to_string())
}).await {
match result {
Ok(output) => println!("Script output: {}", output),
Err(e) => eprintln!("Script error: {}", e),
}
}
```
#### Working with All Connected Clients
```rust
manager.with_all_clients(|clients| {
for (url, client) in clients {
if client.is_connected() {
// Use the client for operations
println!("Connected to: {}", url);
}
}
});
```
#### Connection Management
```rust
// Disconnect from specific server
manager.disconnect_from_server("ws://localhost:3031").await?;
// Reconnect
manager.connect_to_server("ws://localhost:3031").await?;
// Add new connection at runtime
manager.add_connection(
"ws://localhost:3032".to_string(),
Some(private_key.to_string())
);
```
### Expected Output
When running the demo with servers available, you should see output like:
```
🚀 Starting WebSocket Manager Demo
📡 Connecting to WebSocket servers...
✅ Successfully initiated connections
🔍 Checking connection status...
ws://localhost:3030 -> Connected
ws://localhost:3031 -> Connected
🎯 Executing script on specific server...
📤 Sending script to ws://localhost:3030
✅ Script output: Script executed successfully
🌐 Executing operations on all connected clients...
📤 Pinging ws://localhost:3030
📤 Pinging ws://localhost:3031
✅ ws://localhost:3030 responded: pong from 2024-01-15T10:30:45.123Z
✅ ws://localhost:3031 responded: pong from 2024-01-15T10:30:45.124Z
🎉 WebSocket Manager Demo completed!
```
## Website Example
The `website` directory contains a complete Yew-based web application demonstrating the framework's browser capabilities, including:
- WebSocket client management in WASM
- Browser-based authentication
- Interactive script execution
- Real-time connection monitoring
See `website/README.md` for details on running the web application.
## Tips for Development
1. **Logging**: Set `RUST_LOG=debug` for detailed connection logs
2. **Testing**: Use multiple terminal windows to run servers on different ports
3. **Authentication**: In production, load private keys from secure storage, not hardcoded strings
4. **Error Handling**: The examples show basic error handling patterns - extend as needed for production use
## Common Issues
- **Connection Refused**: Make sure the WebSocket servers are running before starting the demo
- **Authentication Failures**: Verify that the private key format is correct (64-character hex string)
- **Port Conflicts**: Use different ports if the default ones (3030, 3031) are already in use

2995
examples/file_browser_demo/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,27 @@
[package]
name = "file_browser_demo"
version = "0.1.0"
edition = "2021"
# Make this package independent from the parent workspace
[workspace]
[dependencies]
framework = { path = "../.." }
yew = { version = "0.21", features = ["csr"] }
yew-router = "0.18"
wasm-bindgen = "0.2"
web-sys = "0.3"
js-sys = "0.3"
wasm-bindgen-futures = "0.4"
gloo = "0.11"
console_error_panic_hook = "0.1"
wee_alloc = "0.4"
[dependencies.getrandom]
version = "0.2"
features = ["js"]
[[bin]]
name = "file_browser_demo"
path = "src/main.rs"

View File

@@ -0,0 +1,273 @@
# File Browser Demo
A comprehensive file browser component built with Yew (Rust) and compiled to WebAssembly, featuring Uppy.js integration for resumable file uploads via the TUS protocol.
## Features
- 📁 **File System Browser**: Navigate directories, view files with metadata
- ⬆️ **Resumable Uploads**: TUS protocol support via Uppy.js for reliable file uploads
- ⬇️ **File Downloads**: Direct download with progress tracking
- 🗂️ **Directory Management**: Create and delete directories
- 🗑️ **File Management**: Delete files with confirmation
- 📊 **Progress Tracking**: Real-time upload progress with visual indicators
- 🎨 **Modern UI**: Bootstrap-styled responsive interface
- 🚀 **WebAssembly**: High-performance Rust code compiled to WASM
## Architecture
### Component Structure
```
FileBrowser
├── FileBrowserConfig (Properties)
├── FileBrowserMsg (Messages)
├── API Functions (HTTP calls to backend)
└── Uppy.js Integration (JavaScript interop)
```
### Key Components
1. **FileBrowser**: Main Yew component with file listing, navigation, and upload UI
2. **FileBrowserConfig**: Configuration struct for customizing the widget
3. **API Layer**: Async functions for backend communication using web_sys::fetch
4. **Uppy Integration**: JavaScript interop for TUS resumable uploads
## Configuration Options
The `FileBrowserConfig` struct allows extensive customization:
```rust
FileBrowserConfig {
base_endpoint: "/files".to_string(), // Backend API endpoint
max_file_size: 100 * 1024 * 1024, // Max file size (100MB)
chunk_size: 1024 * 1024, // Download chunk size (1MB)
initial_path: "".to_string(), // Starting directory
show_upload: true, // Enable upload functionality
show_download: true, // Enable download functionality
show_delete: true, // Enable delete functionality
show_create_dir: true, // Enable directory creation
css_classes: "container-fluid".to_string(), // Custom CSS classes
theme: "light".to_string(), // Uppy theme (light/dark)
}
```
## Backend Compatibility
The file browser component is designed to work with the Python Flask backend from `src/files.py`. It expects the following API endpoints:
- `GET /files/list/{path}` - List directory contents
- `POST /files/upload` - TUS resumable upload endpoint
- `GET /files/download/{path}` - Download files
- `POST /files/dirs/{path}` - Create directories
- `DELETE /files/delete/{path}` - Delete files/directories
### Mock Server
For testing and development, this demo includes a Rust-based mock server that implements the same API as the Python backend:
**Location:** `mock-server/`
**Features:**
- Full API compatibility with `src/files.py`
- Sample files and directories for testing
- CORS enabled for frontend development
- Lightweight and fast
- No external dependencies beyond Rust
**Manual Usage:**
```bash
# Start just the mock server
./run-mock-server.sh
# Or run manually
cd mock-server
cargo run --release
```
## Quick Start
### Prerequisites
- Rust (latest stable version)
- `trunk` for building and serving WASM applications:
```bash
cargo install trunk
```
- `wasm32-unknown-unknown` target:
```bash
rustup target add wasm32-unknown-unknown
```
### Easy Demo Launch
The quickest way to see the file browser in action:
```bash
./run-demo.sh
```
This script will:
1. Build and start the Rust mock server on `http://localhost:3001`
2. Build and serve the WASM demo on `http://localhost:8080`
3. Automatically open your browser to the demo
4. Handle cleanup when you press Ctrl+C
## Prerequisites
1. **Rust and Trunk**:
```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
cargo install trunk
```
2. **Backend Server**: The Python Flask backend from the knowledgecenter project
## Building and Running
### 1. Build the WASM Application
```bash
# From the file_browser_demo directory
./build.sh
```
This will:
- Build the Rust code to WebAssembly using Trunk
- Generate optimized WASM and JavaScript files
- Output files to the `dist/` directory
### 2. Start the Backend Server
```bash
# From the knowledgecenter directory
cd /path/to/knowledgecenter
python -m flask run
```
Make sure CORS is configured to allow requests from your frontend origin.
### 3. Serve the Frontend
**Development Mode:**
```bash
# From the file_browser_demo directory
trunk serve
```
**Production Mode:**
```bash
# From the file_browser_demo directory
./serve.sh
```
### 4. Open in Browser
Trunk will automatically open `http://127.0.0.1:8080` in your web browser.
## Usage as a Widget
The file browser can be used as a reusable widget in other Yew applications:
```rust
use framework::components::{FileBrowser, FileBrowserConfig};
#[function_component(MyApp)]
fn my_app() -> Html {
let config = FileBrowserConfig {
base_endpoint: "/api/files".to_string(),
initial_path: "documents".to_string(),
theme: "dark".to_string(),
..Default::default()
};
html! {
<div class="my-app">
<FileBrowser ..config />
</div>
}
}
```
## Customization
### Styling
The component uses Bootstrap classes and can be customized via:
1. **CSS Classes**: Pass custom classes via `css_classes` in config
2. **Theme**: Set Uppy theme to "light" or "dark"
3. **Custom CSS**: Override the default styles in your application
### Functionality
Enable/disable features via configuration:
```rust
FileBrowserConfig {
show_upload: false, // Hide upload functionality
show_delete: false, // Hide delete buttons
show_create_dir: false, // Hide directory creation
// ... other options
}
```
## Development
### Project Structure
```
file_browser_demo/
├── Cargo.toml # Rust dependencies
├── build.sh # Build script
├── index.html # HTML template with Uppy.js
├── src/
│ └── main.rs # Main Yew application
└── README.md # This file
```
### Key Dependencies
- **yew**: Rust web framework
- **wasm-bindgen**: Rust/JavaScript interop
- **web-sys**: Web API bindings
- **serde**: Serialization for API communication
- **js-sys**: JavaScript value manipulation
### JavaScript Dependencies
- **Uppy.js**: File upload library with TUS support
- **Bootstrap**: UI framework
- **Bootstrap Icons**: Icon set
## Troubleshooting
### WASM Module Loading Issues
1. Ensure files are served over HTTP (not file://)
2. Check browser console for detailed error messages
3. Verify WASM files are generated in `pkg/` directory
### Upload Issues
1. Check backend server is running and accessible
2. Verify CORS configuration allows your frontend origin
3. Ensure TUS endpoints are properly implemented in backend
### Build Issues
1. Update Rust toolchain: `rustup update`
2. Clear cargo cache: `cargo clean`
3. Reinstall wasm-pack if needed
## Browser Support
- Chrome/Chromium 80+
- Firefox 72+
- Safari 13.1+
- Edge 80+
WebAssembly and modern JavaScript features are required.
## License
This demo is part of the Hero Framework project. See the main project for licensing information.

View File

@@ -0,0 +1,31 @@
[build]
target = "index.html"
dist = "dist"
[serve]
address = "127.0.0.1"
port = 8080
open = true
[tools]
# Aggressive WASM optimization with wasm-opt
wasm-opt = [
"-Os", # Optimize for size
"--enable-mutable-globals",
"--enable-sign-ext",
"--enable-nontrapping-float-to-int",
"--enable-bulk-memory",
"--strip-debug", # Remove debug info
"--strip-producers", # Remove producer info
"--dce", # Dead code elimination
"--vacuum", # Remove unused code
"--merge-blocks", # Merge basic blocks
"--precompute", # Precompute expressions
"--precompute-propagate", # Propagate precomputed values
"--remove-unused-names", # Remove unused function names
"--simplify-locals", # Simplify local variables
"--coalesce-locals", # Coalesce local variables
"--reorder-locals", # Reorder locals for better compression
"--flatten", # Flatten control flow
"--rereloop", # Optimize loops
]

View File

@@ -0,0 +1,17 @@
#!/bin/bash
echo "Building File Browser Demo with Trunk..."
# Check if trunk is installed
if ! command -v trunk &> /dev/null; then
echo "Trunk is not installed. Installing..."
cargo install trunk
fi
# Build for development
echo "Building for development..."
trunk build
echo "Build complete! Files are in the 'dist' directory."
echo "To serve locally, run: trunk serve"
echo "To build for production, run: trunk build --release"

View File

@@ -0,0 +1,201 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>File Browser Demo - Yew + Uppy.js + TUS</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
<!-- Uppy CSS -->
<link href="https://releases.transloadit.com/uppy/v4.13.3/uppy.min.css" rel="stylesheet">
<style>
body {
background-color: #f8f9fa;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.app {
min-height: 100vh;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
.file-browser {
padding: 1.5rem;
max-height: 80vh;
display: flex;
flex-direction: column;
}
.file-browser-toolbar {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border: 1px solid #dee2e6;
border-radius: 0.375rem;
flex-shrink: 0;
}
.file-browser-items {
flex: 1;
overflow-y: auto;
max-height: 500px;
}
.file-browser-items .table {
margin-bottom: 0;
}
.file-browser-items .table th {
background-color: #f8f9fa;
border-top: none;
font-weight: 600;
color: #495057;
}
.file-browser-items .table td {
vertical-align: middle;
}
.file-browser-items .table tbody tr:hover {
background-color: #f8f9fa;
}
.file-browser-upload .card {
border: 2px dashed #dee2e6;
background: #f8f9fa;
}
.file-browser-upload .card-header {
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
border-bottom: 1px solid #90caf9;
}
/* Uppy Dashboard Styling */
.uppy-Dashboard {
border: none !important;
background: transparent !important;
}
.uppy-Dashboard-inner {
border: 2px dashed #007bff !important;
border-radius: 0.5rem !important;
background: linear-gradient(135deg, #f8f9ff 0%, #e6f3ff 100%) !important;
}
.uppy-Dashboard-dropFilesHereHint {
color: #007bff !important;
font-weight: 500 !important;
}
/* Progress bars */
.progress {
height: 0.5rem;
background-color: #e9ecef;
}
.progress-bar {
background: linear-gradient(90deg, #007bff 0%, #0056b3 100%);
}
/* Breadcrumb styling */
.file-browser-breadcrumb .breadcrumb {
background: linear-gradient(135deg, #fff 0%, #f8f9fa 100%);
border: 1px solid #dee2e6;
border-radius: 0.375rem;
padding: 0.75rem 1rem;
}
.file-browser-breadcrumb .breadcrumb-item a {
color: #007bff;
text-decoration: none;
}
.file-browser-breadcrumb .breadcrumb-item a:hover {
color: #0056b3;
text-decoration: underline;
}
/* Modal styling */
.modal-content {
border: none;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
.modal-header {
background: linear-gradient(135deg, #007bff 0%, #0056b3 100%);
color: white;
border-bottom: none;
}
.modal-header .btn-close {
filter: invert(1);
}
/* Button styling */
.btn-outline-primary:hover {
transform: translateY(-1px);
box-shadow: 0 0.125rem 0.25rem rgba(0, 123, 255, 0.25);
}
.btn-outline-success:hover {
transform: translateY(-1px);
box-shadow: 0 0.125rem 0.25rem rgba(40, 167, 69, 0.25);
}
.btn-outline-danger:hover {
transform: translateY(-1px);
box-shadow: 0 0.125rem 0.25rem rgba(220, 53, 69, 0.25);
}
/* Loading spinner */
.spinner-border {
color: #007bff;
}
/* Footer */
footer {
margin-top: auto;
}
footer a {
color: #007bff !important;
}
footer a:hover {
color: #0056b3 !important;
}
</style>
</head>
<body>
<div id="app"></div>
<!-- Trunk will automatically inject the WASM loading script here -->
<link data-trunk rel="rust" data-bin="file_browser_demo" />
<!-- Uppy.js (ES Modules) -->
<script type="module">
// Import Uppy modules and make them globally available
import { Uppy, Dashboard, Tus, GoogleDrive } from "https://releases.transloadit.com/uppy/v4.13.3/uppy.min.mjs";
// Make Uppy available globally for the Rust/WASM code
window.Uppy = Uppy;
window.Uppy.Dashboard = Dashboard;
window.Uppy.Tus = Tus;
window.Uppy.GoogleDrive = GoogleDrive;
console.log("Uppy.js loaded and available globally");
</script>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,25 @@
[package]
name = "file-browser-mock-server"
version = "0.1.0"
edition = "2021"
[workspace]
[[bin]]
name = "mock-server"
path = "src/main.rs"
[dependencies]
axum = "0.7"
tokio = { version = "1.0", features = ["full"] }
tower-http = { version = "0.5", features = ["cors"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
uuid = { version = "1.0", features = ["v4"] }
chrono = { version = "0.4", features = ["serde"] }
tracing = "0.1"
tracing-subscriber = "0.3"
anyhow = "1.0"
clap = { version = "4.0", features = ["derive"] }
walkdir = "2.3"
base64 = "0.21"

View File

@@ -0,0 +1,3 @@
# File Browser Demo
This is a sample file for testing the file browser component.

View File

@@ -0,0 +1 @@
Sample notes file content.

View File

@@ -0,0 +1,3 @@
# Sample Report
This is a sample markdown report.

View File

@@ -0,0 +1 @@
Placeholder for image files.

View File

@@ -0,0 +1 @@
{"name": "sample-project", "version": "1.0.0"}

View File

@@ -0,0 +1,3 @@
# Project 1
Sample project documentation.

View File

@@ -0,0 +1 @@
This is a sample text file.

View File

@@ -0,0 +1,3 @@
# File Browser Demo
This is a sample file for testing the file browser component.

View File

@@ -0,0 +1,59 @@
# Design
## Overview
This document outlines a system design that satisfies the specified requirements for decentralized backend ownership. It describes how to implement core capabilities like isolation, delegation, and open logic control — without introducing tight coupling or central dependencies.
## Design Principles
### 1. **Contextual Execution**
- Define a runtime model where each peer context is a named environment.
- Execution is scoped to a context, and all operations are resolved within it.
**Implementation Strategy:**
- Use a unified worker engine that can load and execute within a namespaced peer context.
- Contexts are mounted via a virtual filesystem abstraction, one directory per peer.
### 2. **Logical Isolation via Filesystem Namespacing**
- Each peer's execution environment is backed by a namespaced root directory.
- All storage operations are relative to that root.
**Advantages:**
- Easy enforcement of data boundaries
- Works across shared processes
### 3. **Script-Based Delegated Execution**
- Scripts are the unit of cross-peer interaction.
- A script includes the `caller` (originating peer), parameters, and logic.
**Design Feature:**
- A script sent to another peer is evaluated with both `caller` and `target` contexts available to the runtime.
- Target peer decides whether to accept and how to interpret it.
### 4. **Policy-Driven Acceptance**
- Each context has policies determining:
- Which peers may send scripts
- Which actions are allowed
**Example:** Policies written as declarative access control rules, tied to peer IDs, namespaces, or capabilities.
### 5. **Open, Modifiable Logic**
- Use an embedded domain-specific language (e.g. Rhai) that allows:
- Peer owners to define and inspect their logic
- Script modules to be composed, extended, or overridden
### 6. **Worker Multiplexing**
- Use a single worker binary that can handle one or many peer contexts.
- The context is dynamically determined at runtime.
**Design Note:**
- All workers enforce namespacing, even when only one peer is active per process.
- Supports both isolated (1 peer per worker) and shared (many peers per worker) deployments.
## Optional Enhancements
- Pluggable transport layer (WebSocket, HTTP/2, NATS, etc.)
- Pluggable storage backends for namespace-mounting (FS, S3, SQLite, etc.)
- Declarative schema binding between DSL and structured data
This design enables decentralized application runtime control while supporting a scalable and secure execution model.

View File

@@ -0,0 +1 @@
Sample notes file content.

View File

@@ -0,0 +1,3 @@
# Sample Report
This is a sample markdown report.

View File

@@ -0,0 +1 @@
Placeholder for image files.

View File

@@ -0,0 +1 @@
{"name": "sample-project", "version": "1.0.0"}

View File

@@ -0,0 +1,59 @@
# Design
## Overview
This document outlines a system design that satisfies the specified requirements for decentralized backend ownership. It describes how to implement core capabilities like isolation, delegation, and open logic control — without introducing tight coupling or central dependencies.
## Design Principles
### 1. **Contextual Execution**
- Define a runtime model where each peer context is a named environment.
- Execution is scoped to a context, and all operations are resolved within it.
**Implementation Strategy:**
- Use a unified worker engine that can load and execute within a namespaced peer context.
- Contexts are mounted via a virtual filesystem abstraction, one directory per peer.
### 2. **Logical Isolation via Filesystem Namespacing**
- Each peer's execution environment is backed by a namespaced root directory.
- All storage operations are relative to that root.
**Advantages:**
- Easy enforcement of data boundaries
- Works across shared processes
### 3. **Script-Based Delegated Execution**
- Scripts are the unit of cross-peer interaction.
- A script includes the `caller` (originating peer), parameters, and logic.
**Design Feature:**
- A script sent to another peer is evaluated with both `caller` and `target` contexts available to the runtime.
- Target peer decides whether to accept and how to interpret it.
### 4. **Policy-Driven Acceptance**
- Each context has policies determining:
- Which peers may send scripts
- Which actions are allowed
**Example:** Policies written as declarative access control rules, tied to peer IDs, namespaces, or capabilities.
### 5. **Open, Modifiable Logic**
- Use an embedded domain-specific language (e.g. Rhai) that allows:
- Peer owners to define and inspect their logic
- Script modules to be composed, extended, or overridden
### 6. **Worker Multiplexing**
- Use a single worker binary that can handle one or many peer contexts.
- The context is dynamically determined at runtime.
**Design Note:**
- All workers enforce namespacing, even when only one peer is active per process.
- Supports both isolated (1 peer per worker) and shared (many peers per worker) deployments.
## Optional Enhancements
- Pluggable transport layer (WebSocket, HTTP/2, NATS, etc.)
- Pluggable storage backends for namespace-mounting (FS, S3, SQLite, etc.)
- Declarative schema binding between DSL and structured data
This design enables decentralized application runtime control while supporting a scalable and secure execution model.

View File

@@ -0,0 +1,3 @@
# Project 1
Sample project documentation.

View File

@@ -0,0 +1,50 @@
# System Requirements Specification
## Objective
To define the core requirements for a system that fulfills the goals of decentralized backend ownership — enabling individuals and organizations to control, operate, and interact through their own backend environments without relying on centralized infrastructure.
## Functional Requirements
### 1. **Isolated Execution Contexts**
- Each user or peer must operate within a distinct, logically isolated execution context.
- Contexts must not be able to interfere with each other's state or runtime.
### 2. **Cross-Context Communication**
- Peers must be able to initiate interactions with other peers.
- Communication must include origin metadata (who initiated it), and be authorized by the target context.
### 3. **Delegated Execution**
- A peer must be able to send code or instructions to another peer for execution, under the recipient's policies.
- The recipient must treat the execution as contextualized by the caller, but constrained by its own local rules.
### 4. **Ownership of Logic and Data**
- Users must be able to inspect, modify, and extend the logic that governs their backend.
- Data storage and access policies must be under the control of the peer.
### 5. **Composability and Modifiability**
- System behavior must be defined by open, composable modules or scripts.
- Users must be able to override default behavior or extend it with minimal coupling.
## Non-Functional Requirements
### 6. **Security and Isolation**
- Scripts or instructions from external peers must be sandboxed and policy-checked.
- Each execution context must enforce boundaries between data and logic.
### 7. **Resilience and Redundancy**
- Failure of one peer or node must not impact others.
- Communication must be asynchronous and fault-tolerant.
### 8. **Portability**
- A peers logic and data must be portable across environments and host infrastructure.
- No assumption of persistent centralized hosting.
### 9. **Transparency**
- All logic must be auditable by its owner.
- Communications between peers must be observable and traceable.
### 10. **Scalability**
- The system must support large numbers of peer contexts, potentially hosted on shared infrastructure without compromising logical separation.
These requirements define the baseline for any system that claims to decentralize backend control and empower users to operate their own programmable, connected environments.

View File

@@ -0,0 +1,34 @@
# Rethinking Backend Ownership
## Motivation
Modern applications are powered by backends that run on infrastructure and systems controlled by centralized entities. Whether it's social platforms, collaboration tools, or data-driven apps, the backend is almost always a black box — hosted, maintained, and operated by someone else.
This has profound implications:
- **Loss of autonomy:** Users are locked out of the logic, rules, and data structures that govern their digital experience.
- **Opaque control:** Application behavior can change without the users consent — and often without visibility.
- **Vendor lock-in:** Switching providers or migrating data is often non-trivial, risky, or impossible.
- **Security and privacy risks:** Centralized backends present single points of failure and attack.
In this model, users are not participants in their computing environment — they are clients of someone else's backend.
## The Vision
The purpose of this initiative is to invert that dynamic. We aim to establish a paradigm where users and organizations **own and control their own backend logic and data**, without sacrificing connectivity, collaboration, or scalability.
This means:
- **Local authority:** Each user or organization should have full control over how their backend behaves — what code runs, what data is stored, and who can access it.
- **Portable and interoperable:** Ownership must not mean isolation. User-owned backends should be able to interact with one another on equal footing.
- **Transparent logic:** Application behavior should be visible, inspectable, and modifiable by the user.
- **Delegation, not dependence:** Users should be able to cooperate and interact by delegating execution to each other — not by relying on a central server.
## What We Stand For
- **Agency:** You control your digital environment.
- **Decentralization:** No central chokepoint for computation or data.
- **Modularity:** Users compose their backend behavior, not inherit it from a monolith.
- **Resilience:** Systems should degrade gracefully, fail independently, and recover without central orchestration.
This is about building a more equitable and open computing model — one where the backend serves you, not the other way around.

View File

@@ -0,0 +1 @@
This is a sample text file.

View File

@@ -0,0 +1,50 @@
# System Requirements Specification
## Objective
To define the core requirements for a system that fulfills the goals of decentralized backend ownership — enabling individuals and organizations to control, operate, and interact through their own backend environments without relying on centralized infrastructure.
## Functional Requirements
### 1. **Isolated Execution Contexts**
- Each user or peer must operate within a distinct, logically isolated execution context.
- Contexts must not be able to interfere with each other's state or runtime.
### 2. **Cross-Context Communication**
- Peers must be able to initiate interactions with other peers.
- Communication must include origin metadata (who initiated it), and be authorized by the target context.
### 3. **Delegated Execution**
- A peer must be able to send code or instructions to another peer for execution, under the recipient's policies.
- The recipient must treat the execution as contextualized by the caller, but constrained by its own local rules.
### 4. **Ownership of Logic and Data**
- Users must be able to inspect, modify, and extend the logic that governs their backend.
- Data storage and access policies must be under the control of the peer.
### 5. **Composability and Modifiability**
- System behavior must be defined by open, composable modules or scripts.
- Users must be able to override default behavior or extend it with minimal coupling.
## Non-Functional Requirements
### 6. **Security and Isolation**
- Scripts or instructions from external peers must be sandboxed and policy-checked.
- Each execution context must enforce boundaries between data and logic.
### 7. **Resilience and Redundancy**
- Failure of one peer or node must not impact others.
- Communication must be asynchronous and fault-tolerant.
### 8. **Portability**
- A peers logic and data must be portable across environments and host infrastructure.
- No assumption of persistent centralized hosting.
### 9. **Transparency**
- All logic must be auditable by its owner.
- Communications between peers must be observable and traceable.
### 10. **Scalability**
- The system must support large numbers of peer contexts, potentially hosted on shared infrastructure without compromising logical separation.
These requirements define the baseline for any system that claims to decentralize backend control and empower users to operate their own programmable, connected environments.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,34 @@
# Rethinking Backend Ownership
## Motivation
Modern applications are powered by backends that run on infrastructure and systems controlled by centralized entities. Whether it's social platforms, collaboration tools, or data-driven apps, the backend is almost always a black box — hosted, maintained, and operated by someone else.
This has profound implications:
- **Loss of autonomy:** Users are locked out of the logic, rules, and data structures that govern their digital experience.
- **Opaque control:** Application behavior can change without the users consent — and often without visibility.
- **Vendor lock-in:** Switching providers or migrating data is often non-trivial, risky, or impossible.
- **Security and privacy risks:** Centralized backends present single points of failure and attack.
In this model, users are not participants in their computing environment — they are clients of someone else's backend.
## The Vision
The purpose of this initiative is to invert that dynamic. We aim to establish a paradigm where users and organizations **own and control their own backend logic and data**, without sacrificing connectivity, collaboration, or scalability.
This means:
- **Local authority:** Each user or organization should have full control over how their backend behaves — what code runs, what data is stored, and who can access it.
- **Portable and interoperable:** Ownership must not mean isolation. User-owned backends should be able to interact with one another on equal footing.
- **Transparent logic:** Application behavior should be visible, inspectable, and modifiable by the user.
- **Delegation, not dependence:** Users should be able to cooperate and interact by delegating execution to each other — not by relying on a central server.
## What We Stand For
- **Agency:** You control your digital environment.
- **Decentralization:** No central chokepoint for computation or data.
- **Modularity:** Users compose their backend behavior, not inherit it from a monolith.
- **Resilience:** Systems should degrade gracefully, fail independently, and recover without central orchestration.
This is about building a more equitable and open computing model — one where the backend serves you, not the other way around.

View File

@@ -0,0 +1,565 @@
use axum::{
extract::{DefaultBodyLimit, Path, Query},
http::{HeaderMap, StatusCode},
response::{IntoResponse, Json, Response},
routing::{delete, get, post},
Router,
};
use walkdir::WalkDir;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
fs,
path::{Path as StdPath, PathBuf},
sync::{Arc, Mutex},
};
use tower_http::cors::CorsLayer;
use tracing::{info, warn};
/// File/Directory item information
#[derive(Debug, Serialize, Deserialize)]
struct FileItem {
name: String,
path: String,
is_directory: bool,
size: Option<u64>,
modified: Option<String>,
hash: Option<String>,
}
/// API response for directory listing
#[derive(Debug, Serialize)]
struct ListResponse {
contents: Vec<FileItem>,
}
/// API response for errors
#[derive(Debug, Serialize)]
struct ErrorResponse {
error: String,
}
/// API response for success messages
#[derive(Debug, Serialize)]
struct SuccessResponse {
message: String,
}
/// Query parameters for listing
#[derive(Debug, Deserialize)]
struct ListQuery {
recursive: Option<bool>,
}
/// Mock server state
#[derive(Clone)]
struct AppState {
base_dir: PathBuf,
// Simple upload tracking: upload_id -> (filename, file_path)
uploads: Arc<Mutex<HashMap<String, (String, PathBuf)>>>,
}
impl AppState {
fn new() -> anyhow::Result<Self> {
let base_dir = PathBuf::from("./mock_files");
// Create base directory if it doesn't exist
fs::create_dir_all(&base_dir)?;
// Create some sample files and directories
create_sample_files(&base_dir)?;
Ok(AppState {
base_dir,
uploads: Arc::new(Mutex::new(HashMap::new())),
})
}
/// Get a safe path within the base directory
fn get_safe_path(&self, user_path: &str) -> Option<PathBuf> {
let user_path = if user_path.is_empty() || user_path == "." {
"".to_string()
} else {
user_path.to_string()
};
// Normalize path and prevent directory traversal
let normalized = user_path.replace("..", "").replace("//", "/");
let safe_path = self.base_dir.join(normalized);
// Ensure the path is within base directory
if safe_path.starts_with(&self.base_dir) {
Some(safe_path)
} else {
None
}
}
}
/// Create sample files and directories for demo
fn create_sample_files(base_dir: &StdPath) -> anyhow::Result<()> {
let sample_dirs = ["documents", "images", "projects"];
let sample_files = [
("README.md", "# File Browser Demo\n\nThis is a sample file for testing the file browser component."),
("sample.txt", "This is a sample text file."),
("documents/report.md", "# Sample Report\n\nThis is a sample markdown report."),
("documents/notes.txt", "Sample notes file content."),
("images/placeholder.txt", "Placeholder for image files."),
("projects/project1.md", "# Project 1\n\nSample project documentation."),
("projects/config.json", r#"{"name": "sample-project", "version": "1.0.0"}"#),
];
// Create sample directories
for dir in &sample_dirs {
let dir_path = base_dir.join(dir);
fs::create_dir_all(dir_path)?;
}
// Create sample files
for (file_path, content) in &sample_files {
let full_path = base_dir.join(file_path);
if let Some(parent) = full_path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(full_path, content)?;
}
Ok(())
}
/// Convert file metadata to FileItem
fn file_to_item(path: &StdPath, base_dir: &StdPath) -> anyhow::Result<FileItem> {
let metadata = fs::metadata(path)?;
let name = path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown")
.to_string();
let relative_path = path.strip_prefix(base_dir)
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|_| name.clone());
let modified = metadata.modified()
.ok()
.and_then(|time| DateTime::<Utc>::from(time).format("%Y-%m-%d %H:%M:%S").to_string().into());
Ok(FileItem {
name,
path: relative_path,
is_directory: metadata.is_dir(),
size: if metadata.is_file() { Some(metadata.len()) } else { None },
modified,
hash: None,
})
}
/// List directory contents (root)
/// GET /files/list/
async fn list_root_directory(
Query(params): Query<ListQuery>,
axum::extract::State(state): axum::extract::State<AppState>,
) -> impl IntoResponse {
list_directory_impl("".to_string(), params, state).await
}
/// List directory contents with path
/// GET /files/list/<path>
async fn list_directory(
Path(path): Path<String>,
Query(params): Query<ListQuery>,
axum::extract::State(state): axum::extract::State<AppState>,
) -> impl IntoResponse {
list_directory_impl(path, params, state).await
}
/// Internal implementation for directory listing
async fn list_directory_impl(
path: String,
params: ListQuery,
state: AppState,
) -> impl IntoResponse {
let safe_path = match state.get_safe_path(&path) {
Some(p) => p,
None => {
return (
StatusCode::BAD_REQUEST,
Json(ErrorResponse { error: "Invalid path".to_string() }),
).into_response();
}
};
if !safe_path.exists() || !safe_path.is_dir() {
return (
StatusCode::NOT_FOUND,
Json(ErrorResponse { error: "Directory not found".to_string() }),
).into_response();
}
let mut contents = Vec::new();
if params.recursive.unwrap_or(false) {
// Recursive listing
for entry in WalkDir::new(&safe_path) {
if let Ok(entry) = entry {
if entry.path() != safe_path {
if let Ok(item) = file_to_item(entry.path(), &state.base_dir) {
contents.push(item);
}
}
}
}
} else {
// Non-recursive listing
if let Ok(entries) = fs::read_dir(&safe_path) {
for entry in entries.flatten() {
if let Ok(item) = file_to_item(&entry.path(), &state.base_dir) {
contents.push(item);
}
}
}
}
// Sort: directories first, then files, both alphabetically
contents.sort_by(|a, b| {
match (a.is_directory, b.is_directory) {
(true, false) => std::cmp::Ordering::Less,
(false, true) => std::cmp::Ordering::Greater,
_ => a.name.cmp(&b.name),
}
});
Json(ListResponse { contents }).into_response()
}
/// Create directory
/// POST /files/dirs/<path>
async fn create_directory(
Path(path): Path<String>,
axum::extract::State(state): axum::extract::State<AppState>,
) -> Response {
let safe_path = match state.get_safe_path(&path) {
Some(p) => p,
None => {
return (
StatusCode::BAD_REQUEST,
Json(ErrorResponse { error: "Invalid path".to_string() }),
).into_response();
}
};
match fs::create_dir_all(&safe_path) {
Ok(_) => {
info!("Created directory: {:?}", safe_path);
(
StatusCode::OK,
Json(SuccessResponse { message: "Directory created successfully".to_string() }),
).into_response()
}
Err(e) => {
warn!("Failed to create directory {:?}: {}", safe_path, e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse { error: "Failed to create directory".to_string() }),
).into_response()
}
}
}
/// Delete file or directory
/// DELETE /files/delete/<path>
async fn delete_item(
Path(path): Path<String>,
axum::extract::State(state): axum::extract::State<AppState>,
) -> Response {
let safe_path = match state.get_safe_path(&path) {
Some(p) => p,
None => {
return (
StatusCode::BAD_REQUEST,
Json(ErrorResponse { error: "Invalid path".to_string() }),
).into_response();
}
};
if !safe_path.exists() {
return (
StatusCode::NOT_FOUND,
Json(ErrorResponse { error: "File or directory not found".to_string() }),
).into_response();
}
let result = if safe_path.is_dir() {
fs::remove_dir_all(&safe_path)
} else {
fs::remove_file(&safe_path)
};
match result {
Ok(_) => {
info!("Deleted: {:?}", safe_path);
(
StatusCode::OK,
Json(SuccessResponse { message: "Deleted successfully".to_string() }),
).into_response()
}
Err(e) => {
warn!("Failed to delete {:?}: {}", safe_path, e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse { error: "Failed to delete".to_string() }),
).into_response()
}
}
}
/// Handle TUS upload creation
/// POST /files/upload
/// POST /files/upload/<path> (for specific directory)
async fn create_upload(
headers: HeaderMap,
axum::extract::State(state): axum::extract::State<AppState>,
) -> impl IntoResponse {
create_upload_impl(headers, state, None).await
}
/// Handle TUS upload creation with path
/// POST /files/upload/<path>
async fn create_upload_with_path(
Path(path): Path<String>,
headers: HeaderMap,
axum::extract::State(state): axum::extract::State<AppState>,
) -> impl IntoResponse {
create_upload_impl(headers, state, Some(path)).await
}
/// Internal implementation for upload creation
async fn create_upload_impl(
headers: HeaderMap,
state: AppState,
target_path: Option<String>,
) -> impl IntoResponse {
let upload_id = uuid::Uuid::new_v4().to_string();
// Get filename from Upload-Metadata header (base64 encoded)
// TUS format: "filename <base64-encoded-filename>,type <base64-encoded-type>"
let filename = headers
.get("upload-metadata")
.and_then(|v| v.to_str().ok())
.and_then(|metadata| {
info!("Upload metadata received: {}", metadata);
// Parse TUS metadata format: "filename <base64>,type <base64>"
for pair in metadata.split(',') {
let parts: Vec<&str> = pair.trim().split_whitespace().collect();
if parts.len() == 2 && parts[0] == "filename" {
use base64::Engine;
if let Ok(decoded_bytes) = base64::engine::general_purpose::STANDARD.decode(parts[1]) {
if let Ok(decoded_filename) = String::from_utf8(decoded_bytes) {
info!("Extracted filename: {}", decoded_filename);
return Some(decoded_filename);
}
}
}
}
None
})
.unwrap_or_else(|| {
warn!("Could not extract filename from metadata, using fallback: upload_{}", upload_id);
format!("upload_{}", upload_id)
});
// Determine target directory - use provided path or current directory
let target_dir = if let Some(path) = target_path {
if path.is_empty() {
state.base_dir.clone()
} else {
state.base_dir.join(&path)
}
} else {
state.base_dir.clone()
};
// Create target directory if it doesn't exist
if let Err(e) = fs::create_dir_all(&target_dir) {
warn!("Failed to create target directory: {}", e);
}
// Store upload metadata with preserved filename
let upload_path = target_dir.join(&filename);
// Store the upload info for later use
if let Ok(mut uploads) = state.uploads.lock() {
uploads.insert(upload_id.clone(), (filename.clone(), upload_path));
}
let mut response_headers = HeaderMap::new();
response_headers.insert("Location", format!("/files/upload/{}", upload_id).parse().unwrap());
response_headers.insert("Tus-Resumable", "1.0.0".parse().unwrap());
info!("Created upload with ID: {} for file: {}", upload_id, filename);
(StatusCode::CREATED, response_headers, "")
}
/// Handle TUS upload data
/// PATCH /files/upload/<upload_id>
async fn tus_upload_chunk(
Path(upload_id): Path<String>,
axum::extract::State(state): axum::extract::State<AppState>,
_headers: HeaderMap,
body: axum::body::Bytes,
) -> impl IntoResponse {
// Get upload info from tracking
let upload_info = {
if let Ok(uploads) = state.uploads.lock() {
uploads.get(&upload_id).cloned()
} else {
None
}
};
let (filename, file_path) = match upload_info {
Some(info) => info,
None => {
warn!("Upload ID not found: {}", upload_id);
return (StatusCode::NOT_FOUND, HeaderMap::new(), "").into_response();
}
};
// Write the file data to disk
match std::fs::write(&file_path, &body) {
Ok(_) => {
info!("Successfully saved file: {} ({} bytes)", filename, body.len());
// Clean up upload tracking
if let Ok(mut uploads) = state.uploads.lock() {
uploads.remove(&upload_id);
}
let mut response_headers = HeaderMap::new();
response_headers.insert("Tus-Resumable", "1.0.0".parse().unwrap());
response_headers.insert("Upload-Offset", body.len().to_string().parse().unwrap());
(StatusCode::NO_CONTENT, response_headers, "").into_response()
}
Err(e) => {
warn!("Failed to save file {}: {}", filename, e);
(StatusCode::INTERNAL_SERVER_ERROR, HeaderMap::new(), "").into_response()
}
}
}
/// Download file
/// GET /files/download/<path>
async fn download_file(
Path(path): Path<String>,
axum::extract::State(state): axum::extract::State<AppState>,
) -> impl IntoResponse {
let safe_path = match state.get_safe_path(&path) {
Some(p) => p,
None => {
return (
StatusCode::BAD_REQUEST,
Json(ErrorResponse { error: "Invalid path".to_string() }),
).into_response();
}
};
if !safe_path.exists() || safe_path.is_dir() {
return (
StatusCode::NOT_FOUND,
Json(ErrorResponse { error: "File not found".to_string() }),
).into_response();
}
match fs::read(&safe_path) {
Ok(contents) => {
let mut headers = HeaderMap::new();
headers.insert(
"Content-Disposition",
format!("attachment; filename=\"{}\"",
safe_path.file_name().unwrap_or_default().to_string_lossy())
.parse().unwrap()
);
(StatusCode::OK, headers, contents).into_response()
}
Err(e) => {
warn!("Failed to read file {:?}: {}", safe_path, e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse { error: "Failed to read file".to_string() }),
).into_response()
}
}
}
/// Health check endpoint
async fn health_check() -> impl IntoResponse {
Json(serde_json::json!({
"status": "ok",
"message": "Mock file server is running"
}))
}
/// Root endpoint with API info
async fn root() -> impl IntoResponse {
Json(serde_json::json!({
"name": "Mock File Server",
"description": "A Rust mock server for testing the file browser component",
"endpoints": {
"GET /files/list/<path>": "List directory contents",
"POST /files/dirs/<path>": "Create directory",
"DELETE /files/delete/<path>": "Delete file/directory",
"POST /files/upload": "Upload file (TUS protocol)",
"PATCH /files/upload/<id>": "Upload file chunk",
"GET /files/download/<path>": "Download file",
"GET /health": "Health check"
}
}))
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Initialize tracing
tracing_subscriber::fmt::init();
// Initialize app state
let state = AppState::new()?;
info!("Base directory: {:?}", state.base_dir);
// Build the router
let app = Router::new()
.route("/", get(root))
.route("/health", get(health_check))
.route("/files/list/*path", get(list_directory))
.route("/files/list/", get(list_root_directory))
.route("/files/dirs/*path", post(create_directory))
.route("/files/delete/*path", delete(delete_item))
.route("/files/upload", post(create_upload))
.route("/files/upload/to/*path", post(create_upload_with_path))
.route("/files/upload/:upload_id", axum::routing::patch(tus_upload_chunk))
.route("/files/download/*path", get(download_file))
.layer(DefaultBodyLimit::max(500 * 1024 * 1024)) // 500MB limit for large file uploads
.layer(CorsLayer::permissive())
.with_state(state);
// Start the server
let port = std::env::var("PORT").unwrap_or_else(|_| "3001".to_string());
let addr = format!("0.0.0.0:{}", port);
info!("🚀 Mock File Server starting on http://{}", addr);
info!("📋 Available endpoints:");
info!(" GET /files/list/<path> - List directory contents");
info!(" POST /files/dirs/<path> - Create directory");
info!(" DELETE /files/delete/<path> - Delete file/directory");
info!(" POST /files/upload - Upload file (TUS)");
info!(" GET /files/download/<path> - Download file");
info!(" GET /health - Health check");
let listener = tokio::net::TcpListener::bind(&addr).await?;
axum::serve(listener, app).await?;
Ok(())
}

View File

@@ -0,0 +1,21 @@
{
"name": "file-browser-demo-mock-server",
"version": "1.0.0",
"description": "Mock server for file browser demo",
"main": "mock-server.js",
"scripts": {
"start": "node mock-server.js",
"dev": "nodemon mock-server.js"
},
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"multer": "^1.4.5-lts.1"
},
"devDependencies": {
"nodemon": "^3.0.1"
},
"keywords": ["file-browser", "mock-server", "demo"],
"author": "Herocode Framework",
"license": "MIT"
}

View File

@@ -0,0 +1,107 @@
#!/bin/bash
# Run File Browser Demo Script
# This script starts both the mock server and the WASM demo
set -e
echo "🎯 File Browser Demo Launcher"
echo "=============================="
echo ""
# Function to cleanup background processes
cleanup() {
echo ""
echo "🧹 Cleaning up..."
if [ ! -z "$MOCK_SERVER_PID" ]; then
kill $MOCK_SERVER_PID 2>/dev/null || true
echo "✅ Mock server stopped"
fi
if [ ! -z "$TRUNK_PID" ]; then
kill $TRUNK_PID 2>/dev/null || true
echo "✅ Trunk dev server stopped"
fi
exit 0
}
# Set up signal handlers
trap cleanup SIGINT SIGTERM
# Check dependencies
echo "🔍 Checking dependencies..."
if ! command -v cargo &> /dev/null; then
echo "❌ Error: Rust/Cargo is not installed"
echo "Please install Rust from https://rustup.rs/"
exit 1
fi
if ! command -v trunk &> /dev/null; then
echo "❌ Error: Trunk is not installed"
echo "Please install Trunk: cargo install trunk"
exit 1
fi
echo "✅ Dependencies OK"
echo ""
# Start mock server in background
echo "🚀 Starting mock server..."
cd "$(dirname "$0")/mock-server"
cargo build --release
cargo run --release &
MOCK_SERVER_PID=$!
cd ..
# Wait a moment for server to start
sleep 2
# Check if mock server is running
if ! curl -s http://localhost:3001/health > /dev/null; then
echo "❌ Error: Mock server failed to start"
cleanup
exit 1
fi
echo "✅ Mock server running on http://localhost:3001"
echo ""
# Start trunk dev server
echo "🌐 Starting WASM demo..."
echo "Building and serving on http://localhost:8080"
echo ""
echo "📋 Demo Features:"
echo " • File/directory listing with navigation"
echo " • Create new directories"
echo " • Upload files with progress tracking"
echo " • Download files"
echo " • Delete files and directories"
echo " • Responsive Bootstrap UI"
echo ""
echo "💡 Use Ctrl+C to stop both servers"
echo ""
trunk serve --port 8080 &
TRUNK_PID=$!
# Wait for trunk to start
sleep 3
# Open browser (optional)
if command -v open &> /dev/null; then
echo "🌍 Opening browser..."
open http://localhost:8080
elif command -v xdg-open &> /dev/null; then
echo "🌍 Opening browser..."
xdg-open http://localhost:8080
fi
echo ""
echo "🎉 Demo is ready!"
echo " 📱 Frontend: http://localhost:8080"
echo " 🔧 Backend: http://localhost:3001"
echo ""
echo "Press Ctrl+C to stop all servers"
# Wait for user to stop
wait

View File

@@ -0,0 +1,39 @@
#!/bin/bash
# Run Mock Server Script
# This script starts the Rust mock server for testing the file browser component
set -e
echo "🚀 Starting Mock File Server..."
echo "📁 This server provides the same API as src/files.py for testing"
echo ""
# Change to mock server directory
cd "$(dirname "$0")/mock-server"
# Check if Rust is installed
if ! command -v cargo &> /dev/null; then
echo "❌ Error: Rust/Cargo is not installed"
echo "Please install Rust from https://rustup.rs/"
exit 1
fi
# Build and run the mock server
echo "🔨 Building mock server..."
cargo build --release
echo "🌐 Starting server on http://localhost:3001"
echo "📋 Available endpoints:"
echo " GET /files/list/<path> - List directory contents"
echo " POST /files/dirs/<path> - Create directory"
echo " DELETE /files/delete/<path> - Delete file/directory"
echo " POST /files/upload - Upload file (TUS)"
echo " GET /files/download/<path> - Download file"
echo " GET /health - Health check"
echo ""
echo "💡 Use Ctrl+C to stop the server"
echo ""
# Run the server
cargo run --release

View File

@@ -0,0 +1,31 @@
#!/bin/bash
# serve.sh - Build optimized WASM and serve with Trunk
set -e
echo "🔧 Building optimized WASM bundle..."
trunk build --release
echo "📦 Checking bundle sizes..."
if [ -d "dist" ]; then
echo "Bundle sizes:"
find dist -name "*.wasm" -exec ls -lh {} \; | awk '{print " WASM: " $5 " - " $9}'
find dist -name "*.js" -exec ls -lh {} \; | awk '{print " JS: " $5 " - " $9}'
find dist -name "*.css" -exec ls -lh {} \; | awk '{print " CSS: " $5 " - " $9}'
echo ""
fi
echo "🚀 Starting Trunk development server..."
echo "📍 Server will be available at: http://127.0.0.1:8080"
echo ""
echo "💡 Tips:"
echo " - Make sure your Flask backend is running on http://127.0.0.1:5000"
echo " - Check CORS configuration in your backend"
echo " - Upload files will use TUS protocol for resumable uploads"
echo ""
echo "⏹️ Press Ctrl+C to stop the server"
echo ""
# Start Trunk serve
trunk serve --release

View File

@@ -0,0 +1,87 @@
use yew::prelude::*;
use framework::components::{FileBrowser, FileBrowserConfig};
#[function_component(FileBrowserPage)]
fn file_browser_page() -> Html {
let config = FileBrowserConfig {
base_endpoint: "http://localhost:3001/files".to_string(),
max_file_size: 100 * 1024 * 1024, // 100MB
chunk_size: 1024 * 1024, // 1MB
initial_path: "".to_string(),
show_upload: true,
show_download: true,
show_delete: true,
show_create_dir: true,
css_classes: "container-fluid".to_string(),
theme: "light".to_string(),
};
html! {
<div class="app">
<nav class="navbar navbar-expand-lg navbar-dark bg-primary mb-4">
<div class="container-fluid">
<span class="navbar-brand mb-0 h1">
<i class="bi bi-folder2-open"></i>
{" File Browser Demo"}
</span>
<div class="navbar-text">
{"Powered by Uppy.js & TUS Protocol"}
</div>
</div>
</nav>
<main class="container-fluid">
<div class="row">
<div class="col-12">
<div class="card shadow">
<div class="card-header bg-light">
<h5 class="card-title mb-0">
<i class="bi bi-hdd-stack"></i>
{" File System Browser"}
</h5>
<small class="text-muted">
{"Browse, upload, download, and manage files with resumable uploads"}
</small>
</div>
<div class="card-body p-0">
<FileBrowser ..config />
</div>
</div>
</div>
</div>
<footer class="mt-5 py-4 bg-light text-center text-muted">
<div class="container">
<p class="mb-1">
{"File Browser Component Demo - Built with "}
<a href="https://yew.rs" target="_blank" class="text-decoration-none">{"Yew"}</a>
{", "}
<a href="https://uppy.io" target="_blank" class="text-decoration-none">{"Uppy.js"}</a>
{", and "}
<a href="https://tus.io" target="_blank" class="text-decoration-none">{"TUS Protocol"}</a>
</p>
<small>{"Compiled to WebAssembly for maximum performance"}</small>
</div>
</footer>
</main>
</div>
}
}
#[function_component(App)]
fn app() -> Html {
html! { <FileBrowserPage /> }
}
fn main() {
// Set up panic hook for better error messages in development
#[cfg(feature = "console_error_panic_hook")]
console_error_panic_hook::set_once();
// Use wee_alloc as the global allocator for smaller WASM binary size
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
yew::Renderer::<App>::new().render();
}

0
examples/website/build.sh Normal file → Executable file
View File

View File

@@ -6,7 +6,8 @@ use crate::router::{Route, switch};
use crate::console::{expose_to_console, log_console_examples};
pub struct App {
ws_manager: WsManager,
#[cfg(not(target_arch = "wasm32"))]
ws_manager: WsManager,
}
pub enum AppMsg {
@@ -18,25 +19,36 @@ impl Component for App {
type Properties = ();
fn create(_ctx: &Context<Self>) -> Self {
let ws_manager = WsManager::builder()
.add_server_url("ws://localhost:8080".to_string())
.add_server_url("ws://localhost:8081".to_string())
.add_server_url("ws://localhost:8443/ws".to_string())
.build();
#[cfg(not(target_arch = "wasm32"))]
{
let ws_manager = WsManager::builder()
.add_server_url("ws://localhost:8080".to_string())
.add_server_url("ws://localhost:8081".to_string())
.add_server_url("ws://localhost:8443/ws".to_string())
.build();
// Expose WebSocket manager to browser console
expose_to_console(ws_manager.clone());
log_console_examples();
// Expose WebSocket manager to browser console
expose_to_console(ws_manager.clone());
log_console_examples();
// Clone the manager to move it into the async block
let manager_clone = ws_manager.clone();
spawn_local(async move {
if let Err(e) = manager_clone.connect().await {
log::error!("Failed to connect WebSocket manager: {:?}", e);
}
});
// Clone the manager to move it into the async block
let manager_clone = ws_manager.clone();
spawn_local(async move {
if let Err(e) = manager_clone.connect().await {
log::error!("Failed to connect WebSocket manager: {:?}", e);
}
});
Self { ws_manager }
Self { ws_manager }
}
#[cfg(target_arch = "wasm32")]
{
// For WASM builds, just log console examples
log_console_examples();
Self {}
}
}
fn update(&mut self, _ctx: &Context<Self>, _msg: Self::Message) -> bool {
@@ -44,9 +56,17 @@ impl Component for App {
}
fn view(&self, _ctx: &Context<Self>) -> Html {
let ws_manager_for_switch = self.ws_manager.clone();
#[cfg(not(target_arch = "wasm32"))]
let switch_render = {
let ws_manager_for_switch = self.ws_manager.clone();
Callback::from(move |route: Route| {
switch(route, ws_manager_for_switch.clone())
})
};
#[cfg(target_arch = "wasm32")]
let switch_render = Callback::from(move |route: Route| {
switch(route, ws_manager_for_switch.clone())
switch(route)
});
html! {

View File

@@ -0,0 +1,208 @@
# FileBrowser Widget
A WebAssembly-based file browser widget that can be embedded in any web application.
## Features
- File and directory browsing
- File upload with progress tracking (using TUS protocol)
- File download
- Directory creation and deletion
- File editing (markdown with live preview, text files)
## Running the Example
1. **Start a local server** (required for WASM):
```bash
python3 -m http.server 8081
# or
npx serve .
```
2. **Start the mock backend** (in another terminal):
```bash
cd ../file_browser_demo
cargo run --bin mock_server
```
3. **Open the example**:
- Navigate to `http://localhost:8081`
- The widget will load with a configuration panel
- Try different settings and see them applied in real-time
## Key Features Demonstrated
### Runtime Configuration
The example shows how to configure the widget at runtime without rebuilding:
```javascript
// Create base configuration
const config = create_default_config('http://localhost:3001/files');
// Apply runtime settings using corrected method names
config.setTheme('light'); // Theme selection
config.setMaxFileSize(100 * 1024 * 1024); // 100MB limit
config.setShowUpload(true); // Enable upload
config.setShowDownload(true); // Enable download
config.setShowDelete(false); // Disable delete
config.setInitialPath('documents/'); // Start in documents folder
// Create widget with configuration
const widget = create_file_browser_widget('container-id', config);
```
### Dynamic Reconfiguration
The widget can be recreated with new settings:
```javascript
function updateWidget() {
// Destroy existing widget
if (currentWidget) {
currentWidget.destroy();
}
// Create new widget with updated config
const newConfig = create_default_config(newEndpoint);
newConfig.setTheme(selectedTheme);
currentWidget = create_file_browser_widget('container', newConfig);
}
```
### Error Handling
The example includes comprehensive error handling:
- WASM initialization errors
- Browser compatibility checks
- Widget creation failures
- Network connectivity issues
## Widget API Reference
### Core Functions
```javascript
// Initialize WASM module (call once)
await init();
// Create default configuration
const config = create_default_config(baseEndpoint);
// Create widget instance
const widget = create_file_browser_widget(containerId, config);
// Utility functions
const version = get_version();
const isCompatible = check_browser_compatibility();
```
### Configuration Methods
```javascript
config.setTheme(theme); // 'light' | 'dark'
config.setMaxFileSize(bytes); // Number in bytes
config.setShowUpload(enabled); // Boolean
config.setShowDownload(enabled); // Boolean
config.setShowDelete(enabled); // Boolean
config.setCssClasses(classes); // String of CSS classes
config.setInitialPath(path); // String path
```
### Widget Handle Methods
```javascript
widget.destroy(); // Clean up widget
// Note: Currently no update method - recreate widget for config changes
```
## Advanced Usage
### Custom Styling
```javascript
config.setCssClasses('my-custom-theme dark-mode');
```
### Multiple Widgets
```javascript
const widget1 = create_file_browser_widget('container1', config1);
const widget2 = create_file_browser_widget('container2', config2);
```
### Integration with Frameworks
**React:**
```jsx
function FileBrowserComponent({ endpoint }) {
const containerRef = useRef();
const widgetRef = useRef();
useEffect(() => {
async function initWidget() {
await init();
const config = create_default_config(endpoint);
widgetRef.current = create_file_browser_widget(
containerRef.current,
config
);
}
initWidget();
return () => widgetRef.current?.destroy();
}, [endpoint]);
return <div ref={containerRef} />;
}
```
**Vue:**
```vue
<template>
<div ref="container"></div>
</template>
<script>
export default {
async mounted() {
await init();
const config = create_default_config(this.endpoint);
this.widget = create_file_browser_widget(this.$refs.container, config);
},
beforeUnmount() {
this.widget?.destroy();
}
}
</script>
```
## Troubleshooting
### Common Issues
1. **"config.setTheme is not a function"**
- Ensure you're using the latest widget build
- Check that WASM module is properly initialized
2. **Widget not appearing**
- Verify container element exists
- Check browser console for errors
- Ensure WASM files are served correctly
3. **Backend connection errors**
- Verify backend is running on specified endpoint
- Check CORS configuration
- Ensure all required API endpoints are implemented
### Debug Mode
```javascript
// Enable debug logging
console.log('Widget version:', get_version());
console.log('Browser compatible:', check_browser_compatibility());
```
## Performance Notes
- **Initial Load**: ~368KB total (WASM + JS)
- **Runtime Memory**: ~2-5MB depending on file list size
- **Startup Time**: ~100-300ms on modern browsers
- **File Operations**: Near-native performance via WASM
The widget is optimized for production use with minimal overhead.

View File

@@ -0,0 +1,125 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FileBrowser Widget Example</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
<style>
body { padding: 20px; }
.widget-container {
border: 2px dashed #dee2e6;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
}
</style>
</head>
<body>
<div class="container">
<h1>FileBrowser Widget Example</h1>
<p>This demonstrates how to embed the FileBrowser widget in your website.</p>
<div class="widget-container">
<h3>File Browser Widget</h3>
<div id="file-browser-widget"></div>
</div>
<div class="mt-4">
<h4>Configuration</h4>
<div class="row">
<div class="col-md-6">
<label for="endpoint" class="form-label">Base Endpoint:</label>
<input type="text" id="endpoint" class="form-control" value="http://localhost:3001/files">
</div>
<div class="col-md-6">
<label for="theme" class="form-label">Theme:</label>
<select id="theme" class="form-select">
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</div>
</div>
<div class="row mt-3">
<div class="col-md-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="show-upload" checked>
<label class="form-check-label" for="show-upload">Show Upload</label>
</div>
</div>
<div class="col-md-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="show-download" checked>
<label class="form-check-label" for="show-download">Show Download</label>
</div>
</div>
<div class="col-md-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="show-delete" checked>
<label class="form-check-label" for="show-delete">Show Delete</label>
</div>
</div>
</div>
<button id="recreate-widget" class="btn btn-primary mt-3">Recreate Widget</button>
</div>
</div>
<script type="module">
import init, {
create_file_browser_widget,
create_default_config,
check_browser_compatibility,
get_version
} from './file_browser_widget.js';
let currentWidget = null;
async function initWidget() {
await init();
console.log('FileBrowser Widget version:', get_version());
if (!check_browser_compatibility()) {
alert('Your browser is not compatible with this widget');
return;
}
createWidget();
}
function createWidget() {
// Destroy existing widget
if (currentWidget) {
currentWidget.destroy();
currentWidget = null;
}
// Clear container
const container = document.getElementById('file-browser-widget');
container.innerHTML = '';
// Get configuration from form
const config = create_default_config(document.getElementById('endpoint').value);
config.set_theme(document.getElementById('theme').value);
config.set_show_upload(document.getElementById('show-upload').checked);
config.set_show_download(document.getElementById('show-download').checked);
config.set_show_delete(document.getElementById('show-delete').checked);
try {
currentWidget = create_file_browser_widget('file-browser-widget', config);
console.log('Widget created successfully');
} catch (error) {
console.error('Failed to create widget:', error);
container.innerHTML = `<div class="alert alert-danger">Failed to create widget: ${error}</div>`;
}
}
// Event listeners
document.getElementById('recreate-widget').addEventListener('click', createWidget);
// Initialize when page loads
initWidget();
</script>
</body>
</html>

View File

@@ -0,0 +1,112 @@
/* tslint:disable */
/* eslint-disable */
export function main(): void;
/**
* Create and mount a FileBrowser widget to the specified DOM element
*/
export function create_file_browser_widget(container_id: string, config: JSWidgetConfig): FileBrowserWidgetHandle;
/**
* Create and mount a FileBrowser widget to a specific DOM element
*/
export function create_file_browser_widget_on_element(element: Element, config: JSWidgetConfig): FileBrowserWidgetHandle;
/**
* Utility function to create a default configuration
*/
export function create_default_config(base_endpoint: string): JSWidgetConfig;
/**
* Get version information
*/
export function get_version(): string;
/**
* Check if the widget is compatible with the current browser
*/
export function check_browser_compatibility(): boolean;
/**
* Handle for managing the widget instance
*/
export class FileBrowserWidgetHandle {
private constructor();
free(): void;
/**
* Destroy the widget instance
*/
destroy(): void;
/**
* Update the widget configuration
*/
update_config(_config: JSWidgetConfig): void;
}
/**
* JavaScript-compatible configuration wrapper
*/
export class JSWidgetConfig {
free(): void;
constructor(base_endpoint: string);
setMaxFileSize(size: bigint): void;
setShowUpload(show: boolean): void;
setShowDownload(show: boolean): void;
setShowDelete(show: boolean): void;
setTheme(theme: string): void;
setCssClasses(classes: string): void;
setInitialPath(path: string): void;
}
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
export interface InitOutput {
readonly memory: WebAssembly.Memory;
readonly main: () => void;
readonly __wbg_jswidgetconfig_free: (a: number, b: number) => void;
readonly jswidgetconfig_new: (a: number, b: number) => number;
readonly jswidgetconfig_setMaxFileSize: (a: number, b: bigint) => void;
readonly jswidgetconfig_setShowUpload: (a: number, b: number) => void;
readonly jswidgetconfig_setShowDownload: (a: number, b: number) => void;
readonly jswidgetconfig_setShowDelete: (a: number, b: number) => void;
readonly jswidgetconfig_setTheme: (a: number, b: number, c: number) => void;
readonly jswidgetconfig_setCssClasses: (a: number, b: number, c: number) => void;
readonly jswidgetconfig_setInitialPath: (a: number, b: number, c: number) => void;
readonly __wbg_filebrowserwidgethandle_free: (a: number, b: number) => void;
readonly filebrowserwidgethandle_destroy: (a: number) => void;
readonly filebrowserwidgethandle_update_config: (a: number, b: number) => void;
readonly create_file_browser_widget: (a: number, b: number, c: number) => [number, number, number];
readonly create_file_browser_widget_on_element: (a: any, b: number) => [number, number, number];
readonly create_default_config: (a: number, b: number) => number;
readonly get_version: () => [number, number];
readonly check_browser_compatibility: () => number;
readonly __wbindgen_exn_store: (a: number) => void;
readonly __externref_table_alloc: () => number;
readonly __wbindgen_export_2: WebAssembly.Table;
readonly __wbindgen_malloc: (a: number, b: number) => number;
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
readonly __externref_drop_slice: (a: number, b: number) => void;
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
readonly __wbindgen_export_7: WebAssembly.Table;
readonly __externref_table_dealloc: (a: number) => void;
readonly _dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h126b2208f8e42866: (a: number, b: number) => void;
readonly closure28_externref_shim: (a: number, b: number, c: any, d: any) => void;
readonly closure25_externref_shim: (a: number, b: number, c: any, d: any, e: any) => void;
readonly closure52_externref_shim: (a: number, b: number, c: any) => void;
readonly closure62_externref_shim: (a: number, b: number, c: any) => void;
readonly __wbindgen_start: () => void;
}
export type SyncInitInput = BufferSource | WebAssembly.Module;
/**
* Instantiates the given `module`, which can either be bytes or
* a precompiled `WebAssembly.Module`.
*
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
*
* @returns {InitOutput}
*/
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
/**
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
* for everything else, calls `WebAssembly.instantiate` directly.
*
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
*
* @returns {Promise<InitOutput>}
*/
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -0,0 +1,262 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FileBrowser Widget Example</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
<script src="./uppy.min.js"></script>
<link href="./uppy.min.css" rel="stylesheet">
<style>
body {
padding: 20px;
background-color: #f8f9fa;
}
.widget-container {
border: 2px dashed #dee2e6;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
background: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.config-panel {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.status-indicator {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 8px;
}
.status-success { background-color: #28a745; }
.status-error { background-color: #dc3545; }
.status-loading { background-color: #ffc107; }
</style>
</head>
<body>
<div class="container-fluid">
<div class="row">
<div class="col-12">
<h1 class="mb-4">
<i class="bi bi-folder2-open"></i>
FileBrowser Widget Example
</h1>
<p class="lead">This demonstrates how to embed the FileBrowser widget in your website with runtime configuration.</p>
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="config-panel">
<h4>
<i class="bi bi-gear"></i>
Configuration
</h4>
<div class="mb-3">
<label for="endpoint" class="form-label">Base Endpoint:</label>
<input type="text" id="endpoint" class="form-control" value="http://localhost:3001/files">
<div class="form-text">Backend API endpoint for file operations</div>
</div>
<div class="mb-3">
<label for="theme" class="form-label">Theme:</label>
<select id="theme" class="form-select">
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</div>
<div class="mb-3">
<label for="max-file-size" class="form-label">Max File Size (MB):</label>
<input type="number" id="max-file-size" class="form-control" value="100" min="1" max="1000">
</div>
<div class="mb-3">
<label class="form-label">Features:</label>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="show-upload" checked>
<label class="form-check-label" for="show-upload">Show Upload</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="show-download" checked>
<label class="form-check-label" for="show-download">Show Download</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="show-delete" checked>
<label class="form-check-label" for="show-delete">Show Delete</label>
</div>
</div>
<div class="mb-3">
<label for="initial-path" class="form-label">Initial Path:</label>
<input type="text" id="initial-path" class="form-control" placeholder="e.g., documents/">
</div>
<button id="recreate-widget" class="btn btn-primary w-100">
<i class="bi bi-arrow-clockwise"></i>
Apply Configuration
</button>
<div class="mt-3">
<div id="status" class="small">
<span class="status-indicator status-loading"></span>
<span id="status-text">Initializing...</span>
</div>
</div>
</div>
<div class="config-panel">
<h5>
<i class="bi bi-info-circle"></i>
Widget Info
</h5>
<div class="small">
<div><strong>Version:</strong> <span id="widget-version">Loading...</span></div>
<div><strong>Browser Compatible:</strong> <span id="browser-compat">Checking...</span></div>
<div><strong>WASM Size:</strong> ~329KB</div>
<div><strong>JS Size:</strong> ~39KB</div>
</div>
</div>
</div>
<div class="col-md-8">
<div class="widget-container">
<h3>
<i class="bi bi-hdd-stack"></i>
File Browser Widget
</h3>
<p class="text-muted mb-3">The widget will appear below once initialized:</p>
<div id="file-browser-widget"></div>
</div>
</div>
</div>
</div>
<script type="module">
import init, {
create_file_browser_widget,
create_default_config,
check_browser_compatibility,
get_version
} from './file_browser_widget.js';
let currentWidget = null;
let isInitialized = false;
function updateStatus(text, type = 'loading') {
const statusElement = document.getElementById('status-text');
const indicatorElement = document.querySelector('.status-indicator');
statusElement.textContent = text;
indicatorElement.className = `status-indicator status-${type}`;
}
async function initWidget() {
try {
updateStatus('Loading WASM module...', 'loading');
await init();
updateStatus('Checking compatibility...', 'loading');
const version = get_version();
const isCompatible = check_browser_compatibility();
document.getElementById('widget-version').textContent = version;
document.getElementById('browser-compat').textContent = isCompatible ? 'Yes ✓' : 'No ✗';
if (!isCompatible) {
updateStatus('Browser not compatible', 'error');
document.getElementById('file-browser-widget').innerHTML =
'<div class="alert alert-danger">Your browser is not compatible with this widget</div>';
return;
}
isInitialized = true;
updateStatus('Ready', 'success');
createWidget();
} catch (error) {
console.error('Failed to initialize widget:', error);
updateStatus(`Initialization failed: ${error.message}`, 'error');
document.getElementById('file-browser-widget').innerHTML =
`<div class="alert alert-danger">Failed to initialize: ${error.message}</div>`;
}
}
function createWidget() {
if (!isInitialized) {
updateStatus('Widget not initialized', 'error');
return;
}
try {
updateStatus('Creating widget...', 'loading');
// Destroy existing widget
if (currentWidget) {
currentWidget.destroy();
currentWidget = null;
}
// Clear container
const container = document.getElementById('file-browser-widget');
container.innerHTML = '';
// Get configuration from form
const config = create_default_config(document.getElementById('endpoint').value);
// Apply configuration using the corrected method names
config.setTheme(document.getElementById('theme').value);
config.setMaxFileSize(parseInt(document.getElementById('max-file-size').value) * 1024 * 1024);
config.setShowUpload(document.getElementById('show-upload').checked);
config.setShowDownload(document.getElementById('show-download').checked);
config.setShowDelete(document.getElementById('show-delete').checked);
const initialPath = document.getElementById('initial-path').value.trim();
if (initialPath) {
config.setInitialPath(initialPath);
}
// Create widget
currentWidget = create_file_browser_widget('file-browser-widget', config);
updateStatus('Widget ready', 'success');
} catch (error) {
console.error('Failed to create widget:', error);
updateStatus(`Widget creation failed: ${error.message}`, 'error');
document.getElementById('file-browser-widget').innerHTML =
`<div class="alert alert-danger">Failed to create widget: ${error.message}</div>`;
}
}
// Event listeners
document.getElementById('recreate-widget').addEventListener('click', createWidget);
// Auto-recreate on configuration changes
['endpoint', 'theme', 'max-file-size', 'show-upload', 'show-download', 'show-delete', 'initial-path'].forEach(id => {
const element = document.getElementById(id);
if (element.type === 'checkbox') {
element.addEventListener('change', () => {
if (isInitialized) createWidget();
});
} else {
element.addEventListener('input', () => {
if (isInitialized) {
clearTimeout(element.debounceTimer);
element.debounceTimer = setTimeout(createWidget, 500);
}
});
}
});
// Initialize when page loads
initWidget();
</script>
</body>
</html>

11
examples/widget_example/uppy.min.css vendored Normal file

File diff suppressed because one or more lines are too long

69
examples/widget_example/uppy.min.js vendored Normal file

File diff suppressed because one or more lines are too long

178
examples/ws_manager_demo.rs Normal file
View File

@@ -0,0 +1,178 @@
//! WebSocket Manager Demo
//!
//! This example demonstrates how to use the WsManager to connect to multiple
//! WebSocket servers, authenticate, and execute scripts on connected clients.
//!
//! # Prerequisites
//!
//! 1. Start one or more Hero WebSocket servers:
//! ```bash
//! # Terminal 1
//! cd hero/interfaces/websocket/server
//! cargo run --example circle_auth_demo -- --port 3030
//!
//! # Terminal 2 (optional)
//! cargo run --example circle_auth_demo -- --port 3031
//! ```
//!
//! 2. Run this example:
//! ```bash
//! cd framework
//! cargo run --example ws_manager_demo
//! ```
use framework::WsManager;
use log::{info, warn, error};
use std::time::Duration;
use tokio::time::sleep;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize logging
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
info!("🚀 Starting WebSocket Manager Demo");
// Example private key (in real usage, load from secure storage)
let private_key = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
// Create a manager with multiple servers
let manager = WsManager::builder()
.private_key(private_key.to_string())
.add_server_url("ws://localhost:3030".to_string())
.add_server_url("ws://localhost:3031".to_string())
.build();
info!("📡 Connecting to WebSocket servers...");
// Connect to all configured servers
match manager.connect().await {
Ok(()) => info!("✅ Successfully initiated connections"),
Err(e) => {
error!("❌ Failed to connect: {}", e);
return Err(e.into());
}
}
// Give connections time to establish
sleep(Duration::from_secs(2)).await;
// Check connection status
info!("🔍 Checking connection status...");
let statuses = manager.get_all_connection_statuses();
for (url, status) in &statuses {
info!(" {} -> {}", url, status);
}
// Demonstrate script execution on a specific server
info!("🎯 Executing script on specific server...");
let script = r#"
console.log("Hello from WebSocket Manager!");
return "Script executed successfully";
"#;
if let Some(future) = manager.with_client("ws://localhost:3030", |client| {
info!(" 📤 Sending script to ws://localhost:3030");
client.play(script.to_string())
}) {
let result = future.await;
match result {
Ok(output) => info!(" ✅ Script output: {}", output),
Err(e) => warn!(" ⚠️ Script error: {}", e),
}
} else {
warn!(" ❌ Server ws://localhost:3030 is not connected");
}
// Demonstrate operations on all connected clients
info!("🌐 Executing operations on all connected clients...");
let ping_script = "return 'pong from ' + new Date().toISOString();";
// Get list of connected URLs first
let connected_urls = manager.get_connected_urls();
for url in connected_urls {
info!(" 📤 Pinging {}", url);
if let Some(future) = manager.with_client(&url, |client| {
client.play(ping_script.to_string())
}) {
match future.await {
Ok(output) => info!(" ✅ {} responded: {}", url, output),
Err(e) => warn!(" ⚠️ {} error: {}", url, e),
}
} else {
warn!(" ❌ {} is not connected", url);
}
}
// Wait for async operations to complete
sleep(Duration::from_secs(3)).await;
// Demonstrate authentication status check
info!("🔐 Checking authentication status...");
// Note: For complex async operations, you may need to handle them differently
// due to Rust's lifetime constraints with closures and futures
info!(" 💡 Authentication check would be done here in a real application");
info!(" 💡 Use manager.with_client() to access client methods like whoami()");
// Demonstrate connection management
info!("🔧 Testing connection management...");
// Disconnect from a specific server
info!(" 🔌 Disconnecting from ws://localhost:3031...");
if let Err(e) = manager.disconnect_from_server("ws://localhost:3031").await {
warn!(" ⚠️ Disconnect error: {}", e);
} else {
info!(" ✅ Disconnected successfully");
}
// Check status after disconnect
sleep(Duration::from_secs(1)).await;
let new_statuses = manager.get_all_connection_statuses();
for (url, status) in &new_statuses {
info!(" {} -> {}", url, status);
}
// Reconnect
info!(" 🔌 Reconnecting to ws://localhost:3031...");
if let Err(e) = manager.connect_to_server("ws://localhost:3031").await {
warn!(" ⚠️ Reconnect error: {}", e);
} else {
info!(" ✅ Reconnected successfully");
sleep(Duration::from_secs(1)).await;
}
// Final status check
info!("📊 Final connection status:");
let final_statuses = manager.get_all_connection_statuses();
for (url, status) in &final_statuses {
info!(" {} -> {}", url, status);
}
// Demonstrate adding a connection at runtime
info!(" Adding new connection at runtime...");
manager.add_connection(
"ws://localhost:3032".to_string(),
Some(private_key.to_string())
);
// Try to connect to the new server (will fail if server isn't running)
if let Err(e) = manager.connect_to_server("ws://localhost:3032").await {
warn!(" ⚠️ Could not connect to ws://localhost:3032: {}", e);
warn!(" 💡 Start a server on port 3032 to test this feature");
} else {
info!(" ✅ Connected to new server ws://localhost:3032");
}
info!("🎉 WebSocket Manager Demo completed!");
info!("💡 Key takeaways:");
info!(" - Use WsManager::builder() to configure multiple servers");
info!(" - Call connect() to establish all connections");
info!(" - Use with_client() for operations on specific servers");
info!(" - Use with_all_clients() for bulk operations");
info!(" - Connections are managed automatically with keep-alive");
Ok(())
}