🎯 动态路由示例
单参数: /users/123
@@ -111,7 +145,7 @@ fn main() {
✨ 特性
- - 🚀 高性能路由系统
+ - 🚀 高性能路由系统(Trie + Hybrid)
- 🛡️ 路径遍历攻击防护
- 📁 自动索引文件支持
- 🔒 点文件访问控制
@@ -119,6 +153,8 @@ fn main() {
- 💾 可配置缓存策略
- 🎯 智能 Content-Type 检测
- 🔧 洋葱模型中间件
+ - 🔄 支持所有 HTTP 方法
+ - 📊 路由性能统计
@@ -130,6 +166,13 @@ curl http://localhost:8080/test.txt
curl http://localhost:8080/assets/style.css
curl http://localhost:8080/files/sample.json
+# 文件服务测试
+curl http://localhost:8080/file/test.txt
+curl http://localhost:8080/file-cached/test.txt
+curl http://localhost:8080/download/downloadable.txt
+curl http://localhost:8080/image/style.css
+curl http://localhost:8080/safe-file/test.txt
+
# API 测试
curl http://localhost:8080/api/health
curl http://localhost:8080/api/status
@@ -138,6 +181,10 @@ curl http://localhost:8080/hello
# 动态路由测试
curl http://localhost:8080/users/123
curl http://localhost:8080/posts/456/comments/789
+
+# POST 测试
+curl -X POST http://localhost:8080/api/users -d "{\\"name\\":\\"John\\"}"
+curl -X POST "http://localhost:8080/api/test/create/456?token=abc123" -d "{\\"name\\":\\"Test\\"}"
@@ -145,6 +192,13 @@ curl http://localhost:8080/posts/456/comments/789
return c.html(html_content)
})
+ // 基础路由
+ app.get('/hello', fn (mut c hono.Context) http.Response {
+ return c.text('Hello, World!')
+ })
+
+ // ===== 5. API 路由 =====
+
// 健康检查
app.get('/api/health', fn (mut c hono.Context) http.Response {
c.status(200)
@@ -156,6 +210,7 @@ curl http://localhost:8080/posts/456/comments/789
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": [
@@ -174,26 +229,30 @@ curl http://localhost:8080/posts/456/comments/789
}')
})
- // 动态路由 - 单参数
+ // ===== 6. 动态路由 =====
+
+ // 单参数路由
app.get('/users/:id', fn (mut c hono.Context) http.Response {
user_id := c.params['id']
return c.json('{"user_id": "${user_id}", "name": "John Doe"}')
})
- // 动态路由 - 多参数
+ // 多参数路由
app.get('/posts/:id/comments/:comment_id', fn (mut c hono.Context) http.Response {
post_id := c.params['id']
comment_id := c.params['comment_id']
return c.json('{"post_id": "${post_id}", "comment_id": "${comment_id}", "content": "Great post!"}')
})
- // 动态路由 - 嵌套参数
+ // 嵌套参数路由
app.get('/api/users/:user_id/posts/:post_id', fn (mut c hono.Context) http.Response {
user_id := c.params['user_id']
post_id := c.params['post_id']
return c.json('{"user_id": "${user_id}", "post_id": "${post_id}", "title": "Sample Post"}')
})
+ // ===== 7. HTTP 方法示例 =====
+
// POST 请求
app.post('/api/users', fn (mut c hono.Context) http.Response {
println('=== POST 请求信息 ===')
@@ -276,22 +335,116 @@ curl http://localhost:8080/posts/456/comments/789
return c.json('{"message": "User partially updated", "user_id": "${user_id}", "data": "${c.body}"}')
})
- // 中间件示例
- app.use(fn (mut c hono.Context, next fn (mut hono.Context) http.Response) http.Response {
- println('[LOG] ${c.url}')
- return next(mut c)
+ // HEAD 请求示例
+ app.head('/api/users/:id', fn (mut c hono.Context) http.Response {
+ user_id := c.params['id']
+ c.headers['X-User-ID'] = user_id
+ c.status(200)
+ return c.text('')
})
- // 认证中间件示例
- app.use(fn (mut c hono.Context, next fn (mut hono.Context) http.Response) http.Response {
- token := c.query['token']
- if token == '' {
- c.status(401)
- return c.json('{"error": "Unauthorized", "message": "Token required"}')
- }
- return next(mut c)
+ // OPTIONS 请求示例
+ app.options('/api/users/:id', fn (mut c hono.Context) http.Response {
+ c.headers['Allow'] = 'GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS'
+ c.headers['Access-Control-Allow-Origin'] = '*'
+ c.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS'
+ c.status(200)
+ return c.text('')
})
+ // ===== 8. 文件服务功能 =====
+
+ // 基本文件服务示例
+ app.get('/file/:filename', fn (mut c hono.Context) http.Response {
+ filename := c.params['filename']
+ file_path := 'public/${filename}'
+ return c.file(file_path)
+ })
+
+ // 带选项的文件服务示例
+ app.get('/file-cached/:filename', fn (mut c hono.Context) http.Response {
+ filename := c.params['filename']
+ file_path := 'public/${filename}'
+
+ options := hono.FileOptions{
+ max_age: 3600 // 缓存1小时
+ headers: {
+ 'X-Served-By': 'v-hono'
+ }
+ }
+
+ return c.file_with_options(file_path, options)
+ })
+
+ // 自定义Content-Type的文件服务
+ app.get('/download/:filename', fn (mut c hono.Context) http.Response {
+ filename := c.params['filename']
+ file_path := 'uploads/${filename}'
+
+ options := hono.FileOptions{
+ content_type: 'application/octet-stream'
+ headers: {
+ 'Content-Disposition': 'attachment; filename="${filename}"'
+ }
+ }
+
+ return c.file_with_options(file_path, options)
+ })
+
+ // 图片文件服务示例
+ app.get('/image/:filename', fn (mut c hono.Context) http.Response {
+ filename := c.params['filename']
+ file_path := 'static/${filename}'
+
+ options := hono.FileOptions{
+ max_age: 86400 // 缓存24小时
+ headers: {
+ 'X-Image-Type': 'static'
+ }
+ }
+
+ return c.file_with_options(file_path, options)
+ })
+
+ // 禁用缓存的文件服务
+ app.get('/dynamic/:filename', fn (mut c hono.Context) http.Response {
+ filename := c.params['filename']
+ file_path := 'dynamic/${filename}'
+
+ options := hono.FileOptions{
+ no_cache: true
+ headers: {
+ 'X-Dynamic': 'true'
+ }
+ }
+
+ return c.file_with_options(file_path, options)
+ })
+
+ // ===== 9. 错误处理示例 =====
+
+ // 安全文件服务
+ app.get('/safe-file/:filename', fn (mut c hono.Context) http.Response {
+ filename := c.params['filename']
+
+ // 安全检查:只允许访问特定目录
+ if filename.contains('..') || filename.starts_with('/') {
+ c.status(403)
+ return c.text('Access denied')
+ }
+
+ file_path := 'public/${filename}'
+ return c.file(file_path)
+ })
+
+ // 404 错误处理
+ app.get('/**', fn (mut c hono.Context) http.Response {
+ c.status(404)
+ return c.json('{"error": "Not Found", "message": "The requested resource was not found"}')
+ })
+
+ // ===== 10. 服务器启动 =====
+
println('🚀 V-Hono 完整示例启动中...')
println('📍 服务器地址: http://localhost:8080')
println('')
@@ -306,20 +459,28 @@ curl http://localhost:8080/posts/456/comments/789
println(' - GET /api/static-info')
println(' - GET /hello')
println('')
+ println('📄 文件服务端点:')
+ println(' - GET /file/:filename - 基本文件服务')
+ println(' - GET /file-cached/:filename - 缓存文件服务')
+ println(' - GET /download/:filename - 文件下载')
+ println(' - GET /image/:filename - 图片文件服务')
+ println(' - GET /dynamic/:filename - 动态文件服务')
+ println(' - GET /safe-file/:filename - 安全文件服务')
+ println('')
println('🎯 动态路由:')
println(' - GET /users/123')
println(' - GET /posts/456/comments/789')
println(' - GET /api/users/101/posts/202')
println('')
- println('📝 POST 请求:')
+ println('📝 HTTP 方法示例:')
println(' - POST /api/users')
println(' - POST /api/users/123')
println(' - POST /api/test/create/456')
- println('')
- println('🔄 其他方法:')
println(' - PUT /api/users/123')
println(' - DELETE /api/users/123')
println(' - PATCH /api/users/123')
+ println(' - HEAD /api/users/123')
+ println(' - OPTIONS /api/users/123')
println('')
println('🧪 测试命令示例:')
println(' # 静态文件测试')
@@ -327,6 +488,13 @@ curl http://localhost:8080/posts/456/comments/789
println(' curl http://localhost:8080/assets/style.css')
println(' curl http://localhost:8080/files/sample.json')
println('')
+ println(' # 文件服务测试')
+ println(' curl http://localhost:8080/file/test.txt')
+ println(' curl http://localhost:8080/file-cached/test.txt')
+ println(' curl http://localhost:8080/download/downloadable.txt')
+ println(' curl http://localhost:8080/image/style.css')
+ println(' curl http://localhost:8080/safe-file/test.txt')
+ println('')
println(' # API 测试')
println(' curl http://localhost:8080/api/health')
println(' curl http://localhost:8080/api/status')
diff --git a/hono/request.v b/hono/request.v
index 8b8c57d..43644c9 100644
--- a/hono/request.v
+++ b/hono/request.v
@@ -2,6 +2,7 @@ module hono
import net.http
import net.urllib
+import os
// Context 结构体,类似 Hono.js 的实现
pub struct Context {
@@ -74,6 +75,167 @@ pub fn (mut c Context) html(data string) http.Response {
}
}
+// file method - serve a file directly from the context
+pub fn (mut c Context) file(file_path string) http.Response {
+ return c.file_with_options(file_path, FileOptions{})
+}
+
+// file_with_options method - serve a file with custom options
+pub fn (mut c Context) file_with_options(file_path string, options FileOptions) http.Response {
+ // 安全检查:防止路径遍历攻击
+ if !is_safe_file_path(file_path) {
+ c.status(403)
+ return c.text('Forbidden')
+ }
+
+ // 检查文件是否存在
+ if !os.exists(file_path) {
+ c.status(404)
+ return c.text('File Not Found')
+ }
+
+ // 检查是否为目录
+ if os.is_dir(file_path) {
+ c.status(400)
+ return c.text('Cannot serve directory')
+ }
+
+ // 读取文件内容
+ 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')
+ }
+
+ // 设置状态码
+ if options.status_code > 0 {
+ c.status(options.status_code)
+ } else {
+ c.status(200)
+ }
+
+ // 设置Content-Type
+ if options.content_type != '' {
+ c.headers['Content-Type'] = options.content_type
+ } else {
+ content_type := get_file_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_file_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 if options.no_cache {
+ c.headers['Cache-Control'] = 'no-cache'
+ }
+
+ // 设置自定义头部
+ for key, value in options.headers {
+ c.headers[key] = value
+ }
+
+ // 返回文件内容
+ return c.text(file_content)
+}
+
+// FileOptions struct for configuring file serving
+pub struct FileOptions {
+pub:
+ status_code int = 0 // 自定义状态码,0表示使用默认200
+ content_type string = '' // 自定义Content-Type
+ last_modified bool = true // 是否设置Last-Modified头
+ etag bool = true // 是否设置ETag头
+ max_age int = 0 // 缓存时间(秒)
+ no_cache bool = false // 是否禁用缓存
+ headers map[string]string // 自定义响应头
+}
+
+// 安全检查:防止路径遍历攻击
+fn is_safe_file_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_file_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' }
+ }
+}
+
+// 生成ETag
+fn generate_file_etag(content string, mod_time i64) string {
+ // 简化的ETag生成
+ // 实际应用中可能需要更复杂的哈希算法
+ return '"${content.len}-${mod_time}"'
+}
+
pub fn (mut c Context) status(code int) {
c.status_code = code
}
diff --git a/public/index.html b/public/index.html
index c367637..93930b8 100644
--- a/public/index.html
+++ b/public/index.html
@@ -3,48 +3,719 @@
-
V-Hono 静态文件服务 - Public 目录
+
V-Hono - 轻量级 V 语言 Web 框架
-
🎉 成功!
-
这是来自 ./public 目录的 index.html 文件
-
-
-
✨ V-Hono 静态文件服务特性
-
✅ 自动索引文件支持
-
✅ 路径遍历攻击防护
-
✅ ETag 和缓存支持
-
✅ 智能 Content-Type 检测
+
+
+
+
+
✨ 核心特性
+
+
+
🚀 高性能路由
+
支持 Trie 路由树和 Hybrid 路由,支持静态路由、动态参数、通配符和正则路由
+
+
+
📁 静态文件服务
+
内置静态文件服务中间件,支持多目录、缓存控制、ETag、路径安全防护
+
+
+
🔧 洋葱模型中间件
+
支持洋葱模型中间件,提供日志、认证、CORS 等常用中间件
+
+
+
🛡️ 安全防护
+
内置路径遍历攻击防护、点文件访问控制、请求验证等安全特性
+
+
+
📊 性能监控
+
内置路由性能统计、缓存命中率监控、请求响应时间统计
+
+
+
🔄 HTTP 方法支持
+
完整支持 GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS 等 HTTP 方法
+
+
+
+
+
+
+
🧪 功能测试
+
+
+
+
🔑 认证 Token
+
+
+
+
+
+
+ 当前 Token: abc123
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
健康检查
+
+
+
+
+
+
+
+
+
+
+
文件服务测试
+
+
+
+
+
+
+
+
+
+
+
+
+
动态路由测试
+
+
+
+
+
+
+
+
+
+
+
静态文件访问
+
+
+
+
+
+
+
+
+
+
+
+
📋 可用端点
+
+
🔧 API 端点
+
+
+
+
+
+
📄 文件服务端点
+
+
+
+
+
+
+
🎯 动态路由
+
+
+
+
+
📁 静态文件目录
+
+
+
+
+
+
+
+
+
+
+
💻 代码示例
+
+// 基本路由设置
+mut app := hono.Hono.new()
+
+// 静态文件服务
+app.use(hono.serve_static_default())
+app.use(hono.serve_static_path('/assets', './static'))
+
+// 基础路由
+app.get('/hello', fn (mut c hono.Context) http.Response {
+ return c.text('Hello, World!')
+})
+
+// 动态路由
+app.get('/users/:id', fn (mut c hono.Context) http.Response {
+ user_id := c.params['id']
+ return c.json('{"user_id": "${user_id}"}')
+})
+
+// 文件服务
+app.get('/file/:filename', fn (mut c hono.Context) http.Response {
+ filename := c.params['filename']
+ return c.file('public/${filename}')
+})
+
+// 启动服务器
+app.listen(':8080')
+
+
+
+
-
-
← 返回主页
+
+