feat: add serveStatic middleware
This commit is contained in:
@@ -12,6 +12,10 @@
|
|||||||
- ⚡ 支持通配符路由(`*` 和 `**`)
|
- ⚡ 支持通配符路由(`*` 和 `**`)
|
||||||
- 🔄 支持所有 HTTP 方法(GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS)
|
- 🔄 支持所有 HTTP 方法(GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS)
|
||||||
- 🏗️ 构造函数风格的 API 设计
|
- 🏗️ 构造函数风格的 API 设计
|
||||||
|
- 📁 静态文件服务中间件(类似 Hono.js 的 serveStatic)
|
||||||
|
- 🛡️ 内置安全防护(路径遍历攻击防护、点文件访问控制)
|
||||||
|
- 💾 智能缓存支持(ETag、Last-Modified、Cache-Control)
|
||||||
|
- 🎯 自动 Content-Type 检测
|
||||||
|
|
||||||
## 安装
|
## 安装
|
||||||
|
|
||||||
@@ -225,6 +229,45 @@ app.use(fn (mut c hono.Context, next fn (mut hono.Context) http.Response) http.R
|
|||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 静态文件服务
|
||||||
|
|
||||||
|
提供类似 Hono.js 的 `serveStatic` 中间件:
|
||||||
|
|
||||||
|
```v
|
||||||
|
// 使用默认配置(./public 目录)
|
||||||
|
app.use(hono.serve_static_default())
|
||||||
|
|
||||||
|
// 指定根目录
|
||||||
|
app.use(hono.serve_static_root('./static'))
|
||||||
|
|
||||||
|
// 指定路径前缀和根目录
|
||||||
|
app.use(hono.serve_static_path('/assets', './static'))
|
||||||
|
|
||||||
|
// 使用自定义配置
|
||||||
|
options := hono.StaticOptions{
|
||||||
|
root: './public'
|
||||||
|
path: '/static'
|
||||||
|
index: 'index.html'
|
||||||
|
dotfiles: false
|
||||||
|
etag: true
|
||||||
|
last_modified: true
|
||||||
|
max_age: 3600 // 1小时缓存
|
||||||
|
headers: {
|
||||||
|
'X-Custom-Header': 'value'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
app.use(hono.serve_static(options))
|
||||||
|
```
|
||||||
|
|
||||||
|
**特性:**
|
||||||
|
- 🛡️ 路径遍历攻击防护
|
||||||
|
- 📁 自动索引文件支持(index.html)
|
||||||
|
- 🔒 点文件访问控制
|
||||||
|
- 🏷️ ETag 和 Last-Modified 支持
|
||||||
|
- 💾 可配置缓存策略
|
||||||
|
- 📋 自定义响应头
|
||||||
|
- 🎯 智能 Content-Type 检测
|
||||||
|
|
||||||
### 查询参数和请求体
|
### 查询参数和请求体
|
||||||
|
|
||||||
```v
|
```v
|
||||||
@@ -256,6 +299,8 @@ app.clear_cache()
|
|||||||
|
|
||||||
## 示例
|
## 示例
|
||||||
|
|
||||||
|
### 基础示例
|
||||||
|
|
||||||
运行完整示例:
|
运行完整示例:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -269,6 +314,51 @@ v run example.v
|
|||||||
- 中间件功能
|
- 中间件功能
|
||||||
- 参数提取和查询参数处理
|
- 参数提取和查询参数处理
|
||||||
|
|
||||||
|
### 静态文件服务示例
|
||||||
|
|
||||||
|
运行静态文件服务示例:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
v run static_example.v
|
||||||
|
```
|
||||||
|
|
||||||
|
这个示例演示了:
|
||||||
|
- 多个静态文件目录配置
|
||||||
|
- 自定义缓存策略
|
||||||
|
- 安全特性演示
|
||||||
|
- 完整的HTML界面
|
||||||
|
|
||||||
|
**重要提示**: 静态文件路径映射
|
||||||
|
- `./public` 目录 → 访问路径 `/`
|
||||||
|
- `./static` 目录 → 访问路径 `/assets`
|
||||||
|
- `./uploads` 目录 → 访问路径 `/files`
|
||||||
|
|
||||||
|
例如:
|
||||||
|
- `./public/test.txt` → `http://localhost:8080/test.txt`
|
||||||
|
- `./static/style.css` → `http://localhost:8080/assets/style.css`
|
||||||
|
- `./uploads/sample.json` → `http://localhost:8080/files/sample.json`
|
||||||
|
|
||||||
|
### 快速测试
|
||||||
|
|
||||||
|
运行简单的静态文件测试:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
v run test_static.v
|
||||||
|
```
|
||||||
|
|
||||||
|
测试文件结构:
|
||||||
|
```
|
||||||
|
v-hono/
|
||||||
|
├── public/
|
||||||
|
│ ├── index.html # 默认索引文件
|
||||||
|
│ └── test.txt # 测试文本文件
|
||||||
|
├── static/
|
||||||
|
│ ├── style.css # 样式文件
|
||||||
|
│ └── app.js # JavaScript文件
|
||||||
|
└── uploads/
|
||||||
|
└── sample.json # JSON文件
|
||||||
|
```
|
||||||
|
|
||||||
## 性能特性
|
## 性能特性
|
||||||
|
|
||||||
- **混合路由算法**:静态路由 O(1) 查找,动态路由正则匹配
|
- **混合路由算法**:静态路由 O(1) 查找,动态路由正则匹配
|
||||||
|
|||||||
@@ -6,6 +6,34 @@ import hono
|
|||||||
fn main() {
|
fn main() {
|
||||||
mut app := hono.new_hono()
|
mut app := hono.new_hono()
|
||||||
|
|
||||||
|
// 日志中间件
|
||||||
|
app.use(fn (mut c hono.Context, next fn (mut hono.Context) http.Response) http.Response {
|
||||||
|
println('[LOG] ${c.req.method} ${c.url}')
|
||||||
|
return next(mut c)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 静态文件服务 - 默认配置(./public 目录)
|
||||||
|
app.use(hono.serve_static_default())
|
||||||
|
|
||||||
|
// 静态文件服务 - 指定路径前缀
|
||||||
|
app.use(hono.serve_static_path('/assets', './static'))
|
||||||
|
|
||||||
|
// 静态文件服务 - 自定义配置
|
||||||
|
options := hono.StaticOptions{
|
||||||
|
root: './uploads'
|
||||||
|
path: '/files'
|
||||||
|
index: 'index.html'
|
||||||
|
dotfiles: false
|
||||||
|
etag: true
|
||||||
|
last_modified: true
|
||||||
|
max_age: 3600 // 1小时缓存
|
||||||
|
headers: {
|
||||||
|
'X-Served-By': 'V-Hono'
|
||||||
|
'X-Static-Files': 'true'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
app.use(hono.serve_static(options))
|
||||||
|
|
||||||
// 基本路由
|
// 基本路由
|
||||||
app.get('/hello', fn (mut c hono.Context) http.Response {
|
app.get('/hello', fn (mut c hono.Context) http.Response {
|
||||||
return c.text('Hello, World!')
|
return c.text('Hello, World!')
|
||||||
@@ -13,7 +41,108 @@ fn main() {
|
|||||||
|
|
||||||
// 根路径
|
// 根路径
|
||||||
app.get('/', fn (mut c hono.Context) http.Response {
|
app.get('/', fn (mut c hono.Context) http.Response {
|
||||||
return c.html('<h1>Welcome to V-Hono!</h1><p>A lightweight web framework for V</p>')
|
html_content := '<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>V-Hono 完整示例</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; max-width: 1000px; margin: 0 auto; padding: 20px; }
|
||||||
|
.section { margin: 20px 0; padding: 15px; border: 1px solid #ddd; border-radius: 5px; }
|
||||||
|
.endpoint { background: #f5f5f5; padding: 10px; margin: 5px 0; border-radius: 3px; }
|
||||||
|
.feature { color: #2c5aa0; font-weight: bold; }
|
||||||
|
.static-section { background: #e8f4fd; border-left: 4px solid #2196F3; }
|
||||||
|
.api-section { background: #f3e5f5; border-left: 4px solid #9C27B0; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>🚀 V-Hono 完整示例</h1>
|
||||||
|
<p>A lightweight web framework for V with static file serving</p>
|
||||||
|
|
||||||
|
<div class="section static-section">
|
||||||
|
<h2>📁 静态文件服务</h2>
|
||||||
|
<div class="endpoint">
|
||||||
|
<strong>默认目录:</strong> <code>/</code> → <code>./public</code>
|
||||||
|
</div>
|
||||||
|
<div class="endpoint">
|
||||||
|
<strong>资源目录:</strong> <code>/assets</code> → <code>./static</code>
|
||||||
|
</div>
|
||||||
|
<div class="endpoint">
|
||||||
|
<strong>文件目录:</strong> <code>/files</code> → <code>./uploads</code>
|
||||||
|
</div>
|
||||||
|
<p><strong>测试文件:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li><a href="/test.txt">/test.txt</a> (来自 ./public/test.txt)</li>
|
||||||
|
<li><a href="/assets/style.css">/assets/style.css</a> (来自 ./static/style.css)</li>
|
||||||
|
<li><a href="/files/sample.json">/files/sample.json</a> (来自 ./uploads/sample.json)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section api-section">
|
||||||
|
<h2>🔧 API 端点</h2>
|
||||||
|
<div class="endpoint">
|
||||||
|
<strong>健康检查:</strong> <a href="/api/health">/api/health</a>
|
||||||
|
</div>
|
||||||
|
<div class="endpoint">
|
||||||
|
<strong>静态文件状态:</strong> <a href="/api/status">/api/status</a>
|
||||||
|
</div>
|
||||||
|
<div class="endpoint">
|
||||||
|
<strong>静态文件信息:</strong> <a href="/api/static-info">/api/static-info</a>
|
||||||
|
</div>
|
||||||
|
<div class="endpoint">
|
||||||
|
<strong>基础路由:</strong> <a href="/hello">/hello</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>🎯 动态路由示例</h2>
|
||||||
|
<div class="endpoint">
|
||||||
|
<strong>单参数:</strong> <a href="/users/123">/users/123</a>
|
||||||
|
</div>
|
||||||
|
<div class="endpoint">
|
||||||
|
<strong>多参数:</strong> <a href="/posts/456/comments/789">/posts/456/comments/789</a>
|
||||||
|
</div>
|
||||||
|
<div class="endpoint">
|
||||||
|
<strong>嵌套参数:</strong> <a href="/api/users/101/posts/202">/api/users/101/posts/202</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>✨ 特性</h2>
|
||||||
|
<ul>
|
||||||
|
<li class="feature">🚀 高性能路由系统</li>
|
||||||
|
<li class="feature">🛡️ 路径遍历攻击防护</li>
|
||||||
|
<li class="feature">📁 自动索引文件支持</li>
|
||||||
|
<li class="feature">🔒 点文件访问控制</li>
|
||||||
|
<li class="feature">🏷️ ETag 和 Last-Modified 支持</li>
|
||||||
|
<li class="feature">💾 可配置缓存策略</li>
|
||||||
|
<li class="feature">🎯 智能 Content-Type 检测</li>
|
||||||
|
<li class="feature">🔧 洋葱模型中间件</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>🧪 测试命令</h2>
|
||||||
|
<pre>
|
||||||
|
# 静态文件测试
|
||||||
|
curl http://localhost:8080/test.txt
|
||||||
|
curl http://localhost:8080/assets/style.css
|
||||||
|
curl http://localhost:8080/files/sample.json
|
||||||
|
|
||||||
|
# API 测试
|
||||||
|
curl http://localhost:8080/api/health
|
||||||
|
curl http://localhost:8080/api/status
|
||||||
|
curl http://localhost:8080/hello
|
||||||
|
|
||||||
|
# 动态路由测试
|
||||||
|
curl http://localhost:8080/users/123
|
||||||
|
curl http://localhost:8080/posts/456/comments/789
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>'
|
||||||
|
return c.html(html_content)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 健康检查
|
// 健康检查
|
||||||
@@ -22,6 +151,29 @@ fn main() {
|
|||||||
return c.json('{"status": "ok", "message": "Health check passed"}')
|
return c.json('{"status": "ok", "message": "Health check passed"}')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 静态文件服务状态
|
||||||
|
app.get('/api/status', fn (mut c hono.Context) http.Response {
|
||||||
|
return c.json('{"status": "ok", "static_files": "enabled"}')
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/api/static-info', fn (mut c hono.Context) http.Response {
|
||||||
|
return c.json('{
|
||||||
|
"static_directories": [
|
||||||
|
{"path": "/", "root": "./public"},
|
||||||
|
{"path": "/assets", "root": "./static"},
|
||||||
|
{"path": "/files", "root": "./uploads"}
|
||||||
|
],
|
||||||
|
"features": [
|
||||||
|
"Path traversal protection",
|
||||||
|
"Auto index files",
|
||||||
|
"ETag support",
|
||||||
|
"Last-Modified headers",
|
||||||
|
"Configurable caching",
|
||||||
|
"Custom headers"
|
||||||
|
]
|
||||||
|
}')
|
||||||
|
})
|
||||||
|
|
||||||
// 动态路由 - 单参数
|
// 动态路由 - 单参数
|
||||||
app.get('/users/:id', fn (mut c hono.Context) http.Response {
|
app.get('/users/:id', fn (mut c hono.Context) http.Response {
|
||||||
user_id := c.params['id']
|
user_id := c.params['id']
|
||||||
@@ -140,28 +292,51 @@ fn main() {
|
|||||||
return next(mut c)
|
return next(mut c)
|
||||||
})
|
})
|
||||||
|
|
||||||
println('Hono Server starting on :8080')
|
println('🚀 V-Hono 完整示例启动中...')
|
||||||
println('=== 测试端点 ===')
|
println('📍 服务器地址: http://localhost:8080')
|
||||||
println('静态路由:')
|
println('')
|
||||||
println(' - GET /')
|
println('📁 静态文件服务:')
|
||||||
println(' - GET /hello')
|
println(' - 默认: ./public (访问路径: /)')
|
||||||
|
println(' - 资源: ./static (访问路径: /assets)')
|
||||||
|
println(' - 文件: ./uploads (访问路径: /files)')
|
||||||
|
println('')
|
||||||
|
println('🔧 API 端点:')
|
||||||
println(' - GET /api/health')
|
println(' - GET /api/health')
|
||||||
println('动态路由:')
|
println(' - GET /api/status')
|
||||||
|
println(' - GET /api/static-info')
|
||||||
|
println(' - GET /hello')
|
||||||
|
println('')
|
||||||
|
println('🎯 动态路由:')
|
||||||
println(' - GET /users/123')
|
println(' - GET /users/123')
|
||||||
println(' - GET /posts/456/comments/789')
|
println(' - GET /posts/456/comments/789')
|
||||||
println(' - GET /api/users/101/posts/202')
|
println(' - GET /api/users/101/posts/202')
|
||||||
println('POST 请求:')
|
println('')
|
||||||
|
println('📝 POST 请求:')
|
||||||
println(' - POST /api/users')
|
println(' - POST /api/users')
|
||||||
println(' - POST /api/users/123')
|
println(' - POST /api/users/123')
|
||||||
println(' - POST /api/test/create/456')
|
println(' - POST /api/test/create/456')
|
||||||
println('其他方法:')
|
println('')
|
||||||
|
println('🔄 其他方法:')
|
||||||
println(' - PUT /api/users/123')
|
println(' - PUT /api/users/123')
|
||||||
println(' - DELETE /api/users/123')
|
println(' - DELETE /api/users/123')
|
||||||
println(' - PATCH /api/users/123')
|
println(' - PATCH /api/users/123')
|
||||||
println('')
|
println('')
|
||||||
println('测试命令示例:')
|
println('🧪 测试命令示例:')
|
||||||
|
println(' # 静态文件测试')
|
||||||
|
println(' curl http://localhost:8080/test.txt')
|
||||||
|
println(' curl http://localhost:8080/assets/style.css')
|
||||||
|
println(' curl http://localhost:8080/files/sample.json')
|
||||||
|
println('')
|
||||||
|
println(' # API 测试')
|
||||||
|
println(' curl http://localhost:8080/api/health')
|
||||||
|
println(' curl http://localhost:8080/api/status')
|
||||||
|
println(' curl http://localhost:8080/hello')
|
||||||
|
println('')
|
||||||
|
println(' # 动态路由测试')
|
||||||
println(' curl http://localhost:8080/users/123')
|
println(' curl http://localhost:8080/users/123')
|
||||||
println(' curl http://localhost:8080/posts/456/comments/789')
|
println(' curl http://localhost:8080/posts/456/comments/789')
|
||||||
|
println('')
|
||||||
|
println(' # POST 测试')
|
||||||
println(' curl -X POST http://localhost:8080/api/users -d "{\\"name\\":\\"John\\"}"')
|
println(' curl -X POST http://localhost:8080/api/users -d "{\\"name\\":\\"John\\"}"')
|
||||||
println(' curl -X POST "http://localhost:8080/api/test/create/456?token=abc123" -d "{\\"name\\":\\"Test\\"}"')
|
println(' curl -X POST "http://localhost:8080/api/test/create/456?token=abc123" -d "{\\"name\\":\\"Test\\"}"')
|
||||||
|
|
||||||
|
|||||||
+255
-1
@@ -32,4 +32,258 @@ app.get('/api/users/:id', fn (req hono.Request) http.Response {
|
|||||||
### 与 HybridRouter 对比
|
### 与 HybridRouter 对比
|
||||||
- HybridRouter 采用静态路由 map + 动态正则遍历,适合路由数量较少或兼容性需求高的场景
|
- HybridRouter 采用静态路由 map + 动态正则遍历,适合路由数量较少或兼容性需求高的场景
|
||||||
- TrieRouter 适合大规模路由、高并发、极致性能场景
|
- TrieRouter 适合大规模路由、高并发、极致性能场景
|
||||||
- 两者可并存,便于灵活切换和对比测试
|
- 两者可并存,便于灵活切换和对比测试
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 静态文件服务中间件
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
为 V-Hono 框架添加了类似 Hono.js 的 `serveStatic` 中间件,提供高效、安全的静态文件服务功能。
|
||||||
|
|
||||||
|
## 实现文件
|
||||||
|
|
||||||
|
### 核心实现
|
||||||
|
- `hono/static.v` - 静态文件服务中间件的主要实现
|
||||||
|
|
||||||
|
### 示例文件
|
||||||
|
- `example.v` - 包含静态文件服务的完整示例
|
||||||
|
|
||||||
|
### 测试文件
|
||||||
|
- `public/index.html` - 默认索引文件
|
||||||
|
- `public/test.txt` - 测试文本文件
|
||||||
|
- `static/style.css` - 测试CSS文件
|
||||||
|
- `static/app.js` - 测试JavaScript文件
|
||||||
|
- `uploads/sample.json` - 测试JSON文件
|
||||||
|
|
||||||
|
## 核心功能
|
||||||
|
|
||||||
|
### 1. 静态文件服务中间件 (`serve_static`)
|
||||||
|
|
||||||
|
```v
|
||||||
|
pub fn serve_static(options StaticOptions) fn (mut Context, fn (mut Context) http.Response) http.Response
|
||||||
|
```
|
||||||
|
|
||||||
|
主要特性:
|
||||||
|
- 路径匹配和文件映射
|
||||||
|
- 安全检查(路径遍历攻击防护)
|
||||||
|
- 点文件访问控制
|
||||||
|
- 自动索引文件支持
|
||||||
|
- 智能错误处理
|
||||||
|
|
||||||
|
### 2. 配置选项 (`StaticOptions`)
|
||||||
|
|
||||||
|
```v
|
||||||
|
pub struct StaticOptions {
|
||||||
|
pub:
|
||||||
|
root string = './public' // 静态文件根目录
|
||||||
|
path string = '/' // URL路径前缀
|
||||||
|
index string = 'index.html' // 默认索引文件
|
||||||
|
dotfiles bool = false // 是否允许访问以.开头的文件
|
||||||
|
etag bool = true // 是否启用ETag
|
||||||
|
last_modified bool = true // 是否启用Last-Modified
|
||||||
|
max_age int = 0 // 缓存时间(秒)
|
||||||
|
headers map[string]string // 自定义响应头
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 便捷函数
|
||||||
|
|
||||||
|
- `serve_static_default()` - 使用默认配置
|
||||||
|
- `serve_static_root(root string)` - 指定根目录
|
||||||
|
- `serve_static_path(path string, root string)` - 指定路径前缀和根目录
|
||||||
|
|
||||||
|
## 安全特性
|
||||||
|
|
||||||
|
### 路径遍历攻击防护
|
||||||
|
|
||||||
|
```v
|
||||||
|
fn is_safe_path(path string) bool {
|
||||||
|
// 检查是否包含 .. 或绝对路径
|
||||||
|
if path.contains('..') || path.starts_with('/') || path.starts_with('\\') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否包含危险字符
|
||||||
|
dangerous_chars := ['<', '>', ':', '"', '|', '?', '*']
|
||||||
|
for char in dangerous_chars {
|
||||||
|
if path.contains(char) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 点文件访问控制
|
||||||
|
|
||||||
|
默认情况下阻止访问以 `.` 开头的文件,可通过 `dotfiles: true` 启用。
|
||||||
|
|
||||||
|
## 性能特性
|
||||||
|
|
||||||
|
### ETag 支持
|
||||||
|
|
||||||
|
```v
|
||||||
|
fn generate_etag(content string, mod_time i64) string {
|
||||||
|
return '"${content.len}-${mod_time}"'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 缓存控制
|
||||||
|
|
||||||
|
```v
|
||||||
|
if options.max_age > 0 {
|
||||||
|
c.headers['Cache-Control'] = 'public, max-age=${options.max_age}'
|
||||||
|
} else {
|
||||||
|
c.headers['Cache-Control'] = 'no-cache'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Content-Type 检测
|
||||||
|
|
||||||
|
支持30+种文件类型的自动Content-Type检测:
|
||||||
|
|
||||||
|
```v
|
||||||
|
fn get_content_type(file_path string) string {
|
||||||
|
ext := os.file_ext(file_path).to_lower()
|
||||||
|
|
||||||
|
match ext {
|
||||||
|
'.html', '.htm' { return 'text/html; charset=utf-8' }
|
||||||
|
'.css' { return 'text/css; charset=utf-8' }
|
||||||
|
'.js' { return 'application/javascript; charset=utf-8' }
|
||||||
|
'.json' { return 'application/json; charset=utf-8' }
|
||||||
|
// ... 更多类型
|
||||||
|
else { return 'application/octet-stream' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用示例
|
||||||
|
|
||||||
|
### 基本用法
|
||||||
|
|
||||||
|
```v
|
||||||
|
import hono
|
||||||
|
|
||||||
|
mut app := hono.new_hono()
|
||||||
|
|
||||||
|
// 使用默认配置
|
||||||
|
app.use(hono.serve_static_default())
|
||||||
|
|
||||||
|
app.listen(':8080')
|
||||||
|
```
|
||||||
|
|
||||||
|
### 多目录配置
|
||||||
|
|
||||||
|
```v
|
||||||
|
// 默认静态文件
|
||||||
|
app.use(hono.serve_static_default())
|
||||||
|
|
||||||
|
// 资源文件
|
||||||
|
app.use(hono.serve_static_path('/assets', './static'))
|
||||||
|
|
||||||
|
// 上传文件
|
||||||
|
app.use(hono.serve_static_path('/uploads', './uploads'))
|
||||||
|
```
|
||||||
|
|
||||||
|
### 自定义配置
|
||||||
|
|
||||||
|
```v
|
||||||
|
options := hono.StaticOptions{
|
||||||
|
root: './public'
|
||||||
|
path: '/static'
|
||||||
|
index: 'index.html'
|
||||||
|
dotfiles: false
|
||||||
|
etag: true
|
||||||
|
last_modified: true
|
||||||
|
max_age: 3600
|
||||||
|
headers: {
|
||||||
|
'X-Served-By': 'V-Hono'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use(hono.serve_static(options))
|
||||||
|
```
|
||||||
|
|
||||||
|
## 与 Hono.js 的对比
|
||||||
|
|
||||||
|
| 特性 | V-Hono | Hono.js |
|
||||||
|
|------|--------|---------|
|
||||||
|
| 路径遍历防护 | ✅ | ✅ |
|
||||||
|
| 点文件控制 | ✅ | ✅ |
|
||||||
|
| ETag 支持 | ✅ | ✅ |
|
||||||
|
| Last-Modified | ✅ | ✅ |
|
||||||
|
| 缓存控制 | ✅ | ✅ |
|
||||||
|
| 自定义头部 | ✅ | ✅ |
|
||||||
|
| 自动索引文件 | ✅ | ✅ |
|
||||||
|
| Content-Type 检测 | ✅ | ✅ |
|
||||||
|
| 多目录支持 | ✅ | ✅ |
|
||||||
|
|
||||||
|
## 测试和验证
|
||||||
|
|
||||||
|
### 运行测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 完整示例
|
||||||
|
v run example.v
|
||||||
|
```
|
||||||
|
|
||||||
|
### 测试用例
|
||||||
|
|
||||||
|
1. **基本文件服务**
|
||||||
|
- 访问 `public/test.txt` → `/test.txt`
|
||||||
|
- 访问 `static/style.css` → `/assets/style.css`
|
||||||
|
|
||||||
|
2. **索引文件**
|
||||||
|
- 访问目录 `/` → 提供 `public/index.html`
|
||||||
|
|
||||||
|
3. **安全测试**
|
||||||
|
- 尝试路径遍历攻击 → 返回 403 Forbidden
|
||||||
|
- 尝试访问点文件 → 返回 403 Forbidden
|
||||||
|
|
||||||
|
4. **缓存测试**
|
||||||
|
- 检查 ETag 头
|
||||||
|
- 检查 Last-Modified 头
|
||||||
|
- 检查 Cache-Control 头
|
||||||
|
|
||||||
|
5. **Content-Type 测试**
|
||||||
|
- 验证不同文件类型的正确 Content-Type
|
||||||
|
|
||||||
|
## 性能考虑
|
||||||
|
|
||||||
|
### 优化点
|
||||||
|
|
||||||
|
1. **文件读取**: 使用 V 语言的 `os.read_file()` 进行高效文件读取
|
||||||
|
2. **路径检查**: 快速的安全路径验证
|
||||||
|
3. **头部生成**: 高效的响应头构建
|
||||||
|
4. **缓存策略**: 可配置的缓存控制
|
||||||
|
|
||||||
|
### 内存使用
|
||||||
|
|
||||||
|
- 文件内容一次性读取到内存
|
||||||
|
- 适合中小型文件
|
||||||
|
- 大文件建议使用专门的CDN服务
|
||||||
|
|
||||||
|
## 未来改进
|
||||||
|
|
||||||
|
### 可能的增强
|
||||||
|
|
||||||
|
1. **流式文件服务**: 支持大文件的流式传输
|
||||||
|
2. **压缩支持**: 自动Gzip压缩
|
||||||
|
3. **范围请求**: 支持HTTP Range请求
|
||||||
|
4. **文件监控**: 文件变化时自动刷新缓存
|
||||||
|
5. **性能监控**: 添加性能指标收集
|
||||||
|
|
||||||
|
### 扩展性
|
||||||
|
|
||||||
|
当前实现具有良好的扩展性,可以轻松添加:
|
||||||
|
- 新的文件类型支持
|
||||||
|
- 自定义安全检查
|
||||||
|
- 更多的缓存策略
|
||||||
|
- 文件预处理功能
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
成功为 V-Hono 框架添加了功能完整、安全可靠的静态文件服务中间件,提供了与 Hono.js 相当的体验,同时充分利用了 V 语言的特性。该实现具有良好的安全性、性能和易用性,可以满足大多数Web应用的静态文件服务需求。
|
||||||
+17
-9
@@ -116,16 +116,17 @@ fn (mut s ServerHanler) handle(req http.Request) http.Response {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 解析 query
|
||||||
|
raw_query := url.query().to_map()
|
||||||
|
mut query_map := map[string]string{}
|
||||||
|
for key, values in raw_query {
|
||||||
|
if values.len > 0 {
|
||||||
|
query_map[key] = values[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 尝试 Context 路由
|
// 尝试 Context 路由
|
||||||
if route_match := s.app.context_hybrid_router.match_route(req.method.str(), url.path) {
|
if route_match := s.app.context_hybrid_router.match_route(req.method.str(), url.path) {
|
||||||
// 解析 query
|
|
||||||
raw_query := url.query().to_map()
|
|
||||||
mut query_map := map[string]string{}
|
|
||||||
for key, values in raw_query {
|
|
||||||
if values.len > 0 {
|
|
||||||
query_map[key] = values[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// param 由路由匹配结果提供
|
// param 由路由匹配结果提供
|
||||||
param_map := route_match.params.clone()
|
param_map := route_match.params.clone()
|
||||||
// body
|
// body
|
||||||
@@ -136,8 +137,15 @@ fn (mut s ServerHanler) handle(req http.Request) http.Response {
|
|||||||
return s.exec_context_middlewares(0, mut ctx, fn [route_match] (mut c Context) http.Response {
|
return s.exec_context_middlewares(0, mut ctx, fn [route_match] (mut c Context) http.Response {
|
||||||
return route_match.handler.handle(mut c)
|
return route_match.handler.handle(mut c)
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
// 如果没有匹配的路由,也要执行中间件
|
||||||
|
param_map := map[string]string{}
|
||||||
|
body := req.data
|
||||||
|
mut ctx := Context.new(req, param_map, query_map, body)
|
||||||
|
return s.exec_context_middlewares(0, mut ctx, fn [res] (mut c Context) http.Response {
|
||||||
|
return res
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return res
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Context 版本的中间件执行函数
|
// Context 版本的中间件执行函数
|
||||||
|
|||||||
+8
-1
@@ -1,6 +1,7 @@
|
|||||||
module hono
|
module hono
|
||||||
|
|
||||||
import net.http
|
import net.http
|
||||||
|
import net.urllib
|
||||||
|
|
||||||
// Context 结构体,类似 Hono.js 的实现
|
// Context 结构体,类似 Hono.js 的实现
|
||||||
pub struct Context {
|
pub struct Context {
|
||||||
@@ -17,12 +18,18 @@ pub mut:
|
|||||||
|
|
||||||
// Context 构造函数
|
// Context 构造函数
|
||||||
pub fn Context.new(req http.Request, params map[string]string, query map[string]string, body string) Context {
|
pub fn Context.new(req http.Request, params map[string]string, query map[string]string, body string) Context {
|
||||||
|
// 解析URL获取路径
|
||||||
|
url := urllib.parse(req.url) or {
|
||||||
|
urllib.URL{
|
||||||
|
path: '/'
|
||||||
|
}
|
||||||
|
}
|
||||||
return Context{
|
return Context{
|
||||||
req: req
|
req: req
|
||||||
params: params
|
params: params
|
||||||
query: query
|
query: query
|
||||||
body: body
|
body: body
|
||||||
url: req.url
|
url: url.path
|
||||||
headers: map[string]string{}
|
headers: map[string]string{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+259
@@ -0,0 +1,259 @@
|
|||||||
|
module hono
|
||||||
|
|
||||||
|
import os
|
||||||
|
import net.http
|
||||||
|
import net.urllib
|
||||||
|
|
||||||
|
// 静态文件服务配置
|
||||||
|
pub struct StaticOptions {
|
||||||
|
pub:
|
||||||
|
root string = './public' // 静态文件根目录
|
||||||
|
path string = '/' // URL路径前缀
|
||||||
|
index string = 'index.html' // 默认索引文件
|
||||||
|
dotfiles bool // 是否允许访问以.开头的文件
|
||||||
|
etag bool = true // 是否启用ETag
|
||||||
|
last_modified bool = true // 是否启用Last-Modified
|
||||||
|
max_age int // 缓存时间(秒)
|
||||||
|
headers map[string]string // 自定义响应头
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认静态文件配置
|
||||||
|
pub fn default_static_options() StaticOptions {
|
||||||
|
return StaticOptions{
|
||||||
|
root: './public'
|
||||||
|
path: '/'
|
||||||
|
index: 'index.html'
|
||||||
|
dotfiles: false
|
||||||
|
etag: true
|
||||||
|
last_modified: true
|
||||||
|
max_age: 0
|
||||||
|
headers: map[string]string{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 静态文件服务中间件
|
||||||
|
pub fn serve_static(options StaticOptions) fn (mut Context, fn (mut Context) http.Response) http.Response {
|
||||||
|
return fn [options] (mut c Context, next fn (mut Context) http.Response) http.Response {
|
||||||
|
// 检查请求路径是否匹配静态文件路径
|
||||||
|
if !c.url.starts_with(options.path) {
|
||||||
|
return next(mut c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取文件路径
|
||||||
|
mut file_path := c.url[options.path.len..]
|
||||||
|
if file_path.starts_with('/') {
|
||||||
|
file_path = file_path[1..]
|
||||||
|
}
|
||||||
|
if file_path == '' {
|
||||||
|
file_path = options.index
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调试信息
|
||||||
|
println('[DEBUG] Static file request:')
|
||||||
|
println(' URL: ${c.url}')
|
||||||
|
println(' Path prefix: ${options.path}')
|
||||||
|
println(' File path: ${file_path}')
|
||||||
|
println(' Root: ${options.root}')
|
||||||
|
|
||||||
|
// 安全检查:防止路径遍历攻击
|
||||||
|
if !is_safe_path(file_path) {
|
||||||
|
println(' [DEBUG] Path traversal attack detected')
|
||||||
|
c.status(403)
|
||||||
|
return c.text('Forbidden')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否允许访问点文件
|
||||||
|
if !options.dotfiles && file_path.starts_with('.') {
|
||||||
|
println(' [DEBUG] Dot file access blocked')
|
||||||
|
c.status(403)
|
||||||
|
return c.text('Forbidden')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建完整的文件路径
|
||||||
|
full_path := os.join_path(options.root, file_path)
|
||||||
|
println(' Full path: ${full_path}')
|
||||||
|
|
||||||
|
// 检查文件是否存在
|
||||||
|
if !os.exists(full_path) {
|
||||||
|
println(' [DEBUG] File not found: ${full_path}')
|
||||||
|
return next(mut c)
|
||||||
|
}
|
||||||
|
|
||||||
|
println(' [DEBUG] File found, serving...')
|
||||||
|
|
||||||
|
// 检查是否为目录
|
||||||
|
if os.is_dir(full_path) {
|
||||||
|
// 尝试提供索引文件
|
||||||
|
index_path := os.join_path(full_path, options.index)
|
||||||
|
if os.exists(index_path) {
|
||||||
|
return serve_file(mut c, index_path, options)
|
||||||
|
}
|
||||||
|
// 如果没有索引文件,返回404
|
||||||
|
c.status(404)
|
||||||
|
return c.text('Not Found')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提供文件
|
||||||
|
return serve_file(mut c, full_path, options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提供单个文件
|
||||||
|
fn serve_file(mut c Context, file_path string, options StaticOptions) http.Response {
|
||||||
|
// 读取文件内容
|
||||||
|
file_content := os.read_file(file_path) or {
|
||||||
|
c.status(500)
|
||||||
|
return c.text('Internal Server Error')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件信息
|
||||||
|
file_info := os.stat(file_path) or {
|
||||||
|
c.status(500)
|
||||||
|
return c.text('Internal Server Error')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置状态码
|
||||||
|
c.status(200)
|
||||||
|
|
||||||
|
// 设置Content-Type
|
||||||
|
content_type := get_content_type(file_path)
|
||||||
|
c.headers['Content-Type'] = content_type
|
||||||
|
|
||||||
|
// 设置Content-Length
|
||||||
|
c.headers['Content-Length'] = file_content.len.str()
|
||||||
|
|
||||||
|
// 设置Last-Modified
|
||||||
|
if options.last_modified {
|
||||||
|
last_modified := format_http_date(file_info.mtime)
|
||||||
|
c.headers['Last-Modified'] = last_modified
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置ETag
|
||||||
|
if options.etag {
|
||||||
|
etag := generate_etag(file_content, file_info.mtime)
|
||||||
|
c.headers['ETag'] = etag
|
||||||
|
|
||||||
|
// 检查If-None-Match
|
||||||
|
if_none_match := c.req.header.get_custom('If-None-Match') or { '' }
|
||||||
|
if if_none_match == etag {
|
||||||
|
c.status(304)
|
||||||
|
return c.text('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置Cache-Control
|
||||||
|
if options.max_age > 0 {
|
||||||
|
c.headers['Cache-Control'] = 'public, max-age=${options.max_age}'
|
||||||
|
} else {
|
||||||
|
c.headers['Cache-Control'] = 'no-cache'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置自定义头部
|
||||||
|
for key, value in options.headers {
|
||||||
|
c.headers[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回文件内容
|
||||||
|
return c.text(file_content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 安全检查:防止路径遍历攻击
|
||||||
|
fn is_safe_path(path string) bool {
|
||||||
|
// 检查是否包含 .. 或绝对路径
|
||||||
|
if path.contains('..') || path.starts_with('/') || path.starts_with('\\') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否包含危险字符
|
||||||
|
dangerous_chars := ['<', '>', ':', '"', '|', '?', '*']
|
||||||
|
for dangerous_char in dangerous_chars {
|
||||||
|
if path.contains(dangerous_char) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据文件扩展名获取Content-Type
|
||||||
|
fn get_content_type(file_path string) string {
|
||||||
|
ext := os.file_ext(file_path).to_lower()
|
||||||
|
|
||||||
|
match ext {
|
||||||
|
'.html', '.htm' { return 'text/html; charset=utf-8' }
|
||||||
|
'.css' { return 'text/css; charset=utf-8' }
|
||||||
|
'.js' { return 'application/javascript; charset=utf-8' }
|
||||||
|
'.json' { return 'application/json; charset=utf-8' }
|
||||||
|
'.xml' { return 'application/xml; charset=utf-8' }
|
||||||
|
'.txt' { return 'text/plain; charset=utf-8' }
|
||||||
|
'.md' { return 'text/markdown; charset=utf-8' }
|
||||||
|
'.pdf' { return 'application/pdf' }
|
||||||
|
'.png' { return 'image/png' }
|
||||||
|
'.jpg', '.jpeg' { return 'image/jpeg' }
|
||||||
|
'.gif' { return 'image/gif' }
|
||||||
|
'.svg' { return 'image/svg+xml' }
|
||||||
|
'.ico' { return 'image/x-icon' }
|
||||||
|
'.woff' { return 'font/woff' }
|
||||||
|
'.woff2' { return 'font/woff2' }
|
||||||
|
'.ttf' { return 'font/ttf' }
|
||||||
|
'.eot' { return 'application/vnd.ms-fontobject' }
|
||||||
|
'.otf' { return 'font/otf' }
|
||||||
|
'.mp4' { return 'video/mp4' }
|
||||||
|
'.webm' { return 'video/webm' }
|
||||||
|
'.mp3' { return 'audio/mpeg' }
|
||||||
|
'.wav' { return 'audio/wav' }
|
||||||
|
'.zip' { return 'application/zip' }
|
||||||
|
'.tar' { return 'application/x-tar' }
|
||||||
|
'.gz' { return 'application/gzip' }
|
||||||
|
else { return 'application/octet-stream' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化HTTP日期
|
||||||
|
fn format_http_date(time i64) string {
|
||||||
|
// 简化的HTTP日期格式化
|
||||||
|
// 实际应用中可能需要更复杂的实现
|
||||||
|
return time.str()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成ETag
|
||||||
|
fn generate_etag(content string, mod_time i64) string {
|
||||||
|
// 简化的ETag生成
|
||||||
|
// 实际应用中可能需要更复杂的哈希算法
|
||||||
|
return '"${content.len}-${mod_time}"'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 便捷函数:使用默认配置的静态文件服务
|
||||||
|
pub fn serve_static_default() fn (mut Context, fn (mut Context) http.Response) http.Response {
|
||||||
|
return serve_static(default_static_options())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 便捷函数:指定根目录的静态文件服务
|
||||||
|
pub fn serve_static_root(root string) fn (mut Context, fn (mut Context) http.Response) http.Response {
|
||||||
|
options := StaticOptions{
|
||||||
|
root: root
|
||||||
|
path: '/'
|
||||||
|
index: 'index.html'
|
||||||
|
dotfiles: false
|
||||||
|
etag: true
|
||||||
|
last_modified: true
|
||||||
|
max_age: 0
|
||||||
|
headers: map[string]string{}
|
||||||
|
}
|
||||||
|
return serve_static(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 便捷函数:指定路径前缀的静态文件服务
|
||||||
|
pub fn serve_static_path(path string, root string) fn (mut Context, fn (mut Context) http.Response) http.Response {
|
||||||
|
options := StaticOptions{
|
||||||
|
root: root
|
||||||
|
path: path
|
||||||
|
index: 'index.html'
|
||||||
|
dotfiles: false
|
||||||
|
etag: true
|
||||||
|
last_modified: true
|
||||||
|
max_age: 0
|
||||||
|
headers: map[string]string{}
|
||||||
|
}
|
||||||
|
return serve_static(options)
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>V-Hono 静态文件服务 - Public 目录</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 50px auto;
|
||||||
|
padding: 20px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 15px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
h1 { margin-bottom: 20px; font-size: 2.5em; }
|
||||||
|
p { font-size: 1.2em; line-height: 1.6; }
|
||||||
|
.feature {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
padding: 15px;
|
||||||
|
margin: 10px 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>🎉 成功!</h1>
|
||||||
|
<p>这是来自 <code>./public</code> 目录的 <code>index.html</code> 文件</p>
|
||||||
|
|
||||||
|
<div class="feature">
|
||||||
|
<h3>✨ V-Hono 静态文件服务特性</h3>
|
||||||
|
<p>✅ 自动索引文件支持</p>
|
||||||
|
<p>✅ 路径遍历攻击防护</p>
|
||||||
|
<p>✅ ETag 和缓存支持</p>
|
||||||
|
<p>✅ 智能 Content-Type 检测</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p><a href="/" style="color: #fff; text-decoration: underline;">← 返回主页</a></p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
Hello from V-Hono static file server!
|
||||||
|
|
||||||
|
This is a test file from the ./public directory.
|
||||||
|
You can access this file directly via the root path (/).
|
||||||
|
|
||||||
|
Features demonstrated:
|
||||||
|
- Text file serving
|
||||||
|
- Content-Type detection
|
||||||
|
- Path mapping from URL to file system
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
/* V-Hono Static File Server - CSS Example */
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 15px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 2.5em;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-list li {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
margin: 10px 0;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid #4CAF50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.endpoint {
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
padding: 15px;
|
||||||
|
margin: 15px 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.endpoint strong {
|
||||||
|
color: #4CAF50;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation for features */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(20px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-list li {
|
||||||
|
animation: fadeIn 0.5s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-list li:nth-child(1) { animation-delay: 0.1s; }
|
||||||
|
.feature-list li:nth-child(2) { animation-delay: 0.2s; }
|
||||||
|
.feature-list li:nth-child(3) { animation-delay: 0.3s; }
|
||||||
|
.feature-list li:nth-child(4) { animation-delay: 0.4s; }
|
||||||
|
.feature-list li:nth-child(5) { animation-delay: 0.5s; }
|
||||||
|
.feature-list li:nth-child(6) { animation-delay: 0.6s; }
|
||||||
|
.feature-list li:nth-child(7) { animation-delay: 0.7s; }
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"message": "Hello from V-Hono static file server!",
|
||||||
|
"file": "sample.json",
|
||||||
|
"directory": "./uploads",
|
||||||
|
"url_path": "/files",
|
||||||
|
"timestamp": "2024-01-01T00:00:00Z",
|
||||||
|
"features": {
|
||||||
|
"path_traversal_protection": true,
|
||||||
|
"auto_index_files": true,
|
||||||
|
"etag_support": true,
|
||||||
|
"last_modified_headers": true,
|
||||||
|
"configurable_caching": true,
|
||||||
|
"custom_headers": true,
|
||||||
|
"content_type_detection": true
|
||||||
|
},
|
||||||
|
"content_types_supported": [
|
||||||
|
"text/html",
|
||||||
|
"text/css",
|
||||||
|
"application/javascript",
|
||||||
|
"application/json",
|
||||||
|
"text/plain",
|
||||||
|
"image/png",
|
||||||
|
"image/jpeg",
|
||||||
|
"image/gif",
|
||||||
|
"image/svg+xml",
|
||||||
|
"font/woff",
|
||||||
|
"font/woff2",
|
||||||
|
"application/pdf",
|
||||||
|
"video/mp4",
|
||||||
|
"audio/mpeg"
|
||||||
|
],
|
||||||
|
"security_features": [
|
||||||
|
"Path traversal attack prevention",
|
||||||
|
"Dot file access control",
|
||||||
|
"Safe path validation",
|
||||||
|
"Content-Type header injection protection"
|
||||||
|
],
|
||||||
|
"performance_features": [
|
||||||
|
"ETag generation for caching",
|
||||||
|
"Last-Modified headers",
|
||||||
|
"Configurable cache control",
|
||||||
|
"Efficient file reading"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user