add file browser component and widget
This commit is contained in:
2995
examples/file_browser_demo/Cargo.lock
generated
Normal file
2995
examples/file_browser_demo/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
examples/file_browser_demo/Cargo.toml
Normal file
27
examples/file_browser_demo/Cargo.toml
Normal 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"
|
273
examples/file_browser_demo/README.md
Normal file
273
examples/file_browser_demo/README.md
Normal 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.
|
31
examples/file_browser_demo/Trunk.toml
Normal file
31
examples/file_browser_demo/Trunk.toml
Normal 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
|
||||
]
|
17
examples/file_browser_demo/build.sh
Executable file
17
examples/file_browser_demo/build.sh
Executable 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"
|
201
examples/file_browser_demo/index.html
Normal file
201
examples/file_browser_demo/index.html
Normal 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>
|
1279
examples/file_browser_demo/mock-server/Cargo.lock
generated
Normal file
1279
examples/file_browser_demo/mock-server/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
examples/file_browser_demo/mock-server/Cargo.toml
Normal file
25
examples/file_browser_demo/mock-server/Cargo.toml
Normal 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"
|
@@ -0,0 +1,3 @@
|
||||
# File Browser Demo
|
||||
|
||||
This is a sample file for testing the file browser component.
|
@@ -0,0 +1 @@
|
||||
Sample notes file content.
|
@@ -0,0 +1,3 @@
|
||||
# Sample Report
|
||||
|
||||
This is a sample markdown report.
|
@@ -0,0 +1 @@
|
||||
Placeholder for image files.
|
@@ -0,0 +1 @@
|
||||
{"name": "sample-project", "version": "1.0.0"}
|
@@ -0,0 +1,3 @@
|
||||
# Project 1
|
||||
|
||||
Sample project documentation.
|
@@ -0,0 +1 @@
|
||||
This is a sample text file.
|
@@ -0,0 +1,3 @@
|
||||
# File Browser Demo
|
||||
|
||||
This is a sample file for testing the file browser component.
|
Binary file not shown.
Binary file not shown.
59
examples/file_browser_demo/mock-server/mock_files/design.md
Normal file
59
examples/file_browser_demo/mock-server/mock_files/design.md
Normal 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.
|
@@ -0,0 +1 @@
|
||||
Sample notes file content.
|
@@ -0,0 +1,3 @@
|
||||
# Sample Report
|
||||
|
||||
This is a sample markdown report.
|
Binary file not shown.
@@ -0,0 +1 @@
|
||||
Placeholder for image files.
|
@@ -0,0 +1 @@
|
||||
{"name": "sample-project", "version": "1.0.0"}
|
@@ -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.
|
@@ -0,0 +1,3 @@
|
||||
# Project 1
|
||||
|
||||
Sample project documentation.
|
@@ -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 peer’s 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.
|
@@ -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 user’s 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.
|
@@ -0,0 +1 @@
|
||||
This is a sample text 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 peer’s 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
34
examples/file_browser_demo/mock-server/mock_files/why.md
Normal file
34
examples/file_browser_demo/mock-server/mock_files/why.md
Normal 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 user’s 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.
|
565
examples/file_browser_demo/mock-server/src/main.rs
Normal file
565
examples/file_browser_demo/mock-server/src/main.rs
Normal 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(())
|
||||
}
|
21
examples/file_browser_demo/package.json
Normal file
21
examples/file_browser_demo/package.json
Normal 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"
|
||||
}
|
107
examples/file_browser_demo/run-demo.sh
Executable file
107
examples/file_browser_demo/run-demo.sh
Executable 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
|
39
examples/file_browser_demo/run-mock-server.sh
Executable file
39
examples/file_browser_demo/run-mock-server.sh
Executable 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
|
31
examples/file_browser_demo/serve.sh
Executable file
31
examples/file_browser_demo/serve.sh
Executable 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
|
87
examples/file_browser_demo/src/main.rs
Normal file
87
examples/file_browser_demo/src/main.rs
Normal 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();
|
||||
}
|
Reference in New Issue
Block a user