feat: add hono.file method

This commit is contained in:
yangshangbo
2025-07-08 11:04:27 +08:00
parent f039f24af1
commit 50c055b607
6 changed files with 1091 additions and 64 deletions
+197 -29
View File
@@ -4,21 +4,37 @@ import net.http
import hono import hono
fn main() { fn main() {
// ===== 1. 应用初始化 =====
mut app := hono.Hono.new() mut app := hono.Hono.new()
// ===== 2. 中间件配置 =====
// 日志中间件 // 日志中间件
app.use(fn (mut c hono.Context, next fn (mut hono.Context) http.Response) http.Response { app.use(fn (mut c hono.Context, next fn (mut hono.Context) http.Response) http.Response {
println('[LOG] ${c.req.method} ${c.url}') println('[LOG] ${c.req.method} ${c.url}')
return next(mut c) return next(mut c)
}) })
// 静态文件服务 - 默认配置(./public 目录) // 认证中间件
app.use(fn (mut c hono.Context, next fn (mut hono.Context) http.Response) http.Response {
println("url path: ${c.url}")
token := c.query['token']
if c.url != "/" && token == '' {
c.status(401)
return c.json('{"error": "Unauthorized", "message": "Token required"}')
}
return next(mut c)
})
// ===== 3. 静态文件服务中间件 =====
// 默认静态文件服务(./public 目录)
app.use(hono.serve_static_default()) app.use(hono.serve_static_default())
// 静态文件服务 - 指定路径前缀 // 资源文件服务(./static 目录)
app.use(hono.serve_static_path('/assets', './static')) app.use(hono.serve_static_path('/assets', './static'))
// 静态文件服务 - 自定义配置 // 上传文件服务(./uploads 目录)
options := hono.StaticOptions{ options := hono.StaticOptions{
root: './uploads' root: './uploads'
path: '/files' path: '/files'
@@ -34,12 +50,9 @@ fn main() {
} }
app.use(hono.serve_static(options)) app.use(hono.serve_static(options))
// 基本路由 // ===== 4. 基础路由 =====
app.get('/hello', fn (mut c hono.Context) http.Response {
return c.text('Hello, World!')
})
// 根路径 // 根路径 - 欢迎页面
app.get('/', fn (mut c hono.Context) http.Response { app.get('/', fn (mut c hono.Context) http.Response {
html_content := '<!DOCTYPE html> html_content := '<!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
@@ -48,12 +61,14 @@ fn main() {
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>V-Hono 完整示例</title> <title>V-Hono 完整示例</title>
<style> <style>
body { font-family: Arial, sans-serif; max-width: 1000px; margin: 0 auto; padding: 20px; } body { font-family: Arial, sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; }
.section { margin: 20px 0; padding: 15px; border: 1px solid #ddd; border-radius: 5px; } .section { margin: 20px 0; padding: 15px; border: 1px solid #ddd; border-radius: 5px; }
.endpoint { background: #f5f5f5; padding: 10px; margin: 5px 0; border-radius: 3px; } .endpoint { background: #f5f5f5; padding: 10px; margin: 5px 0; border-radius: 3px; }
.feature { color: #2c5aa0; font-weight: bold; } .feature { color: #2c5aa0; font-weight: bold; }
.static-section { background: #e8f4fd; border-left: 4px solid #2196F3; } .static-section { background: #e8f4fd; border-left: 4px solid #2196F3; }
.api-section { background: #f3e5f5; border-left: 4px solid #9C27B0; } .api-section { background: #f3e5f5; border-left: 4px solid #9C27B0; }
.file-section { background: #e8f5e8; border-left: 4px solid #4CAF50; }
.route-section { background: #fff3e0; border-left: 4px solid #FF9800; }
</style> </style>
</head> </head>
<body> <body>
@@ -95,7 +110,26 @@ fn main() {
</div> </div>
</div> </div>
<div class="section"> <div class="section file-section">
<h2>📄 文件服务端点</h2>
<div class="endpoint">
<strong>基本文件服务:</strong> <a href="/file/test.txt">/file/test.txt</a>
</div>
<div class="endpoint">
<strong>缓存文件服务:</strong> <a href="/file-cached/test.txt">/file-cached/test.txt</a>
</div>
<div class="endpoint">
<strong>文件下载:</strong> <a href="/download/downloadable.txt">/download/downloadable.txt</a>
</div>
<div class="endpoint">
<strong>图片文件服务:</strong> <a href="/image/style.css">/image/style.css</a>
</div>
<div class="endpoint">
<strong>安全文件服务:</strong> <a href="/safe-file/test.txt">/safe-file/test.txt</a>
</div>
</div>
<div class="section route-section">
<h2>🎯 动态路由示例</h2> <h2>🎯 动态路由示例</h2>
<div class="endpoint"> <div class="endpoint">
<strong>单参数:</strong> <a href="/users/123">/users/123</a> <strong>单参数:</strong> <a href="/users/123">/users/123</a>
@@ -111,7 +145,7 @@ fn main() {
<div class="section"> <div class="section">
<h2>✨ 特性</h2> <h2>✨ 特性</h2>
<ul> <ul>
<li class="feature">🚀 高性能路由系统</li> <li class="feature">🚀 高性能路由系统(Trie + Hybrid)</li>
<li class="feature">🛡️ 路径遍历攻击防护</li> <li class="feature">🛡️ 路径遍历攻击防护</li>
<li class="feature">📁 自动索引文件支持</li> <li class="feature">📁 自动索引文件支持</li>
<li class="feature">🔒 点文件访问控制</li> <li class="feature">🔒 点文件访问控制</li>
@@ -119,6 +153,8 @@ fn main() {
<li class="feature">💾 可配置缓存策略</li> <li class="feature">💾 可配置缓存策略</li>
<li class="feature">🎯 智能 Content-Type 检测</li> <li class="feature">🎯 智能 Content-Type 检测</li>
<li class="feature">🔧 洋葱模型中间件</li> <li class="feature">🔧 洋葱模型中间件</li>
<li class="feature">🔄 支持所有 HTTP 方法</li>
<li class="feature">📊 路由性能统计</li>
</ul> </ul>
</div> </div>
@@ -130,6 +166,13 @@ curl http://localhost:8080/test.txt
curl http://localhost:8080/assets/style.css curl http://localhost:8080/assets/style.css
curl http://localhost:8080/files/sample.json 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 测试 # API 测试
curl http://localhost:8080/api/health curl http://localhost:8080/api/health
curl http://localhost:8080/api/status 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/users/123
curl http://localhost:8080/posts/456/comments/789 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\\"}"
</pre> </pre>
</div> </div>
</body> </body>
@@ -145,6 +192,13 @@ curl http://localhost:8080/posts/456/comments/789
return c.html(html_content) 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 { app.get('/api/health', fn (mut c hono.Context) http.Response {
c.status(200) c.status(200)
@@ -156,6 +210,7 @@ curl http://localhost:8080/posts/456/comments/789
return c.json('{"status": "ok", "static_files": "enabled"}') return c.json('{"status": "ok", "static_files": "enabled"}')
}) })
// 静态文件服务信息
app.get('/api/static-info', fn (mut c hono.Context) http.Response { app.get('/api/static-info', fn (mut c hono.Context) http.Response {
return c.json('{ return c.json('{
"static_directories": [ "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 { app.get('/users/:id', fn (mut c hono.Context) http.Response {
user_id := c.params['id'] user_id := c.params['id']
return c.json('{"user_id": "${user_id}", "name": "John Doe"}') return c.json('{"user_id": "${user_id}", "name": "John Doe"}')
}) })
// 动态路由 - 多参数 // 多参数路由
app.get('/posts/:id/comments/:comment_id', fn (mut c hono.Context) http.Response { app.get('/posts/:id/comments/:comment_id', fn (mut c hono.Context) http.Response {
post_id := c.params['id'] post_id := c.params['id']
comment_id := c.params['comment_id'] comment_id := c.params['comment_id']
return c.json('{"post_id": "${post_id}", "comment_id": "${comment_id}", "content": "Great post!"}') 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 { app.get('/api/users/:user_id/posts/:post_id', fn (mut c hono.Context) http.Response {
user_id := c.params['user_id'] user_id := c.params['user_id']
post_id := c.params['post_id'] post_id := c.params['post_id']
return c.json('{"user_id": "${user_id}", "post_id": "${post_id}", "title": "Sample Post"}') return c.json('{"user_id": "${user_id}", "post_id": "${post_id}", "title": "Sample Post"}')
}) })
// ===== 7. HTTP 方法示例 =====
// POST 请求 // POST 请求
app.post('/api/users', fn (mut c hono.Context) http.Response { app.post('/api/users', fn (mut c hono.Context) http.Response {
println('=== POST 请求信息 ===') 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}"}') return c.json('{"message": "User partially updated", "user_id": "${user_id}", "data": "${c.body}"}')
}) })
// 中间件示例 // HEAD 请求示例
app.use(fn (mut c hono.Context, next fn (mut hono.Context) http.Response) http.Response { app.head('/api/users/:id', fn (mut c hono.Context) http.Response {
println('[LOG] ${c.url}') user_id := c.params['id']
return next(mut c) c.headers['X-User-ID'] = user_id
c.status(200)
return c.text('')
}) })
// 认证中间件示例 // OPTIONS 请求示例
app.use(fn (mut c hono.Context, next fn (mut hono.Context) http.Response) http.Response { app.options('/api/users/:id', fn (mut c hono.Context) http.Response {
token := c.query['token'] c.headers['Allow'] = 'GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS'
if token == '' { c.headers['Access-Control-Allow-Origin'] = '*'
c.status(401) c.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS'
return c.json('{"error": "Unauthorized", "message": "Token required"}') c.status(200)
} return c.text('')
return next(mut c)
}) })
// ===== 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('🚀 V-Hono 完整示例启动中...')
println('📍 服务器地址: http://localhost:8080') println('📍 服务器地址: http://localhost:8080')
println('') println('')
@@ -306,20 +459,28 @@ curl http://localhost:8080/posts/456/comments/789
println(' - GET /api/static-info') println(' - GET /api/static-info')
println(' - GET /hello') println(' - GET /hello')
println('') 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('🎯 动态路由:')
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('') println('')
println('📝 POST 请求:') println('📝 HTTP 方法示例:')
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(' - 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(' - HEAD /api/users/123')
println(' - OPTIONS /api/users/123')
println('') println('')
println('🧪 测试命令示例:') 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/assets/style.css')
println(' curl http://localhost:8080/files/sample.json') println(' curl http://localhost:8080/files/sample.json')
println('') 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(' # API 测试')
println(' curl http://localhost:8080/api/health') println(' curl http://localhost:8080/api/health')
println(' curl http://localhost:8080/api/status') println(' curl http://localhost:8080/api/status')
+162
View File
@@ -2,6 +2,7 @@ module hono
import net.http import net.http
import net.urllib import net.urllib
import os
// Context 结构体,类似 Hono.js 的实现 // Context 结构体,类似 Hono.js 的实现
pub struct Context { 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) { pub fn (mut c Context) status(code int) {
c.status_code = code c.status_code = code
} }
+694 -23
View File
@@ -3,48 +3,719 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>V-Hono 静态文件服务 - Public 目录</title> <title>V-Hono - 轻量级 V 语言 Web 框架</title>
<style> <style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body { body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 600px; background: #fafafa;
margin: 50px auto; color: #333;
padding: 20px; line-height: 1.6;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
text-align: center;
} }
.container { .container {
background: rgba(255, 255, 255, 0.1); max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.header {
text-align: center;
color: black;
margin-bottom: 40px;
}
.header h1 {
font-size: 3em;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.header p {
font-size: 1.2em;
opacity: 0.9;
}
.section {
background: rgba(255, 255, 255, 0.95);
margin: 20px 0;
padding: 30px; padding: 30px;
border-radius: 15px; border-radius: 15px;
backdrop-filter: blur(10px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
} }
h1 { margin-bottom: 20px; font-size: 2.5em; }
p { font-size: 1.2em; line-height: 1.6; } .section h2 {
.feature { color: #2c5aa0;
background: rgba(255, 255, 255, 0.2); margin-bottom: 20px;
font-size: 1.8em;
border-bottom: 3px solid #667eea;
padding-bottom: 10px;
}
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin: 20px 0;
}
.feature-card {
background: #f8f9fa;
padding: 20px;
border-radius: 10px;
border-left: 4px solid #667eea;
transition: transform 0.3s ease;
}
.feature-card:hover {
transform: translateY(-5px);
}
.feature-card h3 {
color: #2c5aa0;
margin-bottom: 10px;
}
.endpoint {
background: #e8f4fd;
padding: 15px; padding: 15px;
margin: 10px 0; margin: 10px 0;
border-radius: 8px; border-radius: 8px;
border-left: 4px solid #2196F3;
}
.endpoint strong {
color: #1976D2;
}
.endpoint a {
color: #2196F3;
text-decoration: none;
font-weight: bold;
}
.endpoint a:hover {
text-decoration: underline;
}
.test-area {
background: #f5f5f5;
padding: 20px;
border-radius: 10px;
margin: 15px 0;
}
.test-area h4 {
color: #2c5aa0;
margin-bottom: 10px;
}
.test-button {
background: #667eea;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
margin: 5px;
transition: background 0.3s ease;
}
.test-button:hover {
background: #5a6fd8;
}
.test-button:disabled {
background: #ccc;
cursor: not-allowed;
}
.result {
background: #e8f5e8;
padding: 15px;
border-radius: 8px;
margin: 10px 0;
border-left: 4px solid #4CAF50;
white-space: pre-wrap;
font-family: monospace;
font-size: 0.9em;
max-height: 300px;
overflow-y: auto;
}
.error {
background: #ffebee;
border-left: 4px solid #f44336;
}
.loading {
background: #fff3e0;
border-left: 4px solid #ff9800;
}
.code-block {
background: #2d3748;
color: #e2e8f0;
padding: 20px;
border-radius: 8px;
font-family: 'Courier New', monospace;
font-size: 0.9em;
overflow-x: auto;
margin: 15px 0;
}
.tab-container {
margin: 20px 0;
}
.tab-buttons {
display: flex;
border-bottom: 2px solid #ddd;
margin-bottom: 20px;
}
.tab-button {
background: none;
border: none;
padding: 10px 20px;
cursor: pointer;
border-bottom: 3px solid transparent;
transition: all 0.3s ease;
}
.tab-button.active {
border-bottom-color: #667eea;
color: #667eea;
font-weight: bold;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin: 20px 0;
}
.stat-card {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
padding: 20px;
border-radius: 10px;
text-align: center;
}
.stat-number {
font-size: 2em;
font-weight: bold;
margin-bottom: 5px;
}
.footer {
text-align: center;
color: white;
margin-top: 40px;
opacity: 0.8;
}
@media (max-width: 768px) {
.container {
padding: 10px;
}
.header h1 {
font-size: 2em;
}
.feature-grid {
grid-template-columns: 1fr;
}
} }
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<h1>🎉 成功!</h1> <div class="header">
<h1>🚀 V-Hono</h1>
<p>轻量级 V 语言 Web 框架 - 功能完整展示</p>
</div>
<!-- 核心特性展示 -->
<div class="section">
<h2>✨ 核心特性</h2>
<div class="feature-grid">
<div class="feature-card">
<h3>🚀 高性能路由</h3>
<p>支持 Trie 路由树和 Hybrid 路由,支持静态路由、动态参数、通配符和正则路由</p>
</div>
<div class="feature-card">
<h3>📁 静态文件服务</h3>
<p>内置静态文件服务中间件,支持多目录、缓存控制、ETag、路径安全防护</p>
</div>
<div class="feature-card">
<h3>🔧 洋葱模型中间件</h3>
<p>支持洋葱模型中间件,提供日志、认证、CORS 等常用中间件</p>
</div>
<div class="feature-card">
<h3>🛡️ 安全防护</h3>
<p>内置路径遍历攻击防护、点文件访问控制、请求验证等安全特性</p>
</div>
<div class="feature-card">
<h3>📊 性能监控</h3>
<p>内置路由性能统计、缓存命中率监控、请求响应时间统计</p>
</div>
<div class="feature-card">
<h3>🔄 HTTP 方法支持</h3>
<p>完整支持 GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS 等 HTTP 方法</p>
</div>
</div>
</div>
<!-- 功能测试区域 -->
<div class="section">
<h2>🧪 功能测试</h2>
<!-- Token 输入区域 -->
<div class="test-area">
<h4>🔑 认证 Token</h4>
<div style="display: flex; gap: 10px; align-items: center; margin-bottom: 15px;">
<input type="text" id="token-input" placeholder="请输入 token" value="abc123" style="flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 4px;">
<button class="test-button" onclick="setToken()">设置 Token</button>
<button class="test-button" onclick="clearToken()">清除 Token</button>
</div>
<div id="token-status" style="font-size: 0.9em; color: #666;">
当前 Token: <span id="current-token">abc123</span>
</div>
</div>
<div class="tab-container">
<div class="tab-buttons">
<button class="tab-button active" onclick="showTab('api')">API 测试</button>
<button class="tab-button" onclick="showTab('files')">文件服务</button>
<button class="tab-button" onclick="showTab('routes')">路由测试</button>
<button class="tab-button" onclick="showTab('static')">静态文件</button>
</div>
<!-- API 测试 -->
<div id="api" class="tab-content active">
<div class="test-area">
<h4>健康检查</h4>
<button class="test-button" onclick="testAPI('/api/health')">测试 /api/health</button>
<button class="test-button" onclick="testAPI('/api/status')">测试 /api/status</button>
<button class="test-button" onclick="testAPI('/api/static-info')">测试 /api/static-info</button>
<div id="api-result" class="result" style="display: none;"></div>
</div>
</div>
<!-- 文件服务测试 -->
<div id="files" class="tab-content">
<div class="test-area">
<h4>文件服务测试</h4>
<button class="test-button" onclick="testFile('/file/test.txt')">基本文件服务</button>
<button class="test-button" onclick="testFile('/file-cached/test.txt')">缓存文件服务</button>
<button class="test-button" onclick="testFile('/download/downloadable.txt')">文件下载</button>
<button class="test-button" onclick="testFile('/image/style.css')">图片文件服务</button>
<button class="test-button" onclick="testFile('/safe-file/test.txt')">安全文件服务</button>
<div id="file-result" class="result" style="display: none;"></div>
</div>
</div>
<!-- 路由测试 -->
<div id="routes" class="tab-content">
<div class="test-area">
<h4>动态路由测试</h4>
<button class="test-button" onclick="testRoute('/users/123')">单参数路由</button>
<button class="test-button" onclick="testRoute('/posts/456/comments/789')">多参数路由</button>
<button class="test-button" onclick="testRoute('/api/users/101/posts/202')">嵌套参数路由</button>
<div id="route-result" class="result" style="display: none;"></div>
</div>
</div>
<!-- 静态文件测试 -->
<div id="static" class="tab-content">
<div class="test-area">
<h4>静态文件访问</h4>
<button class="test-button" onclick="testStatic('/test.txt')">测试文本文件</button>
<button class="test-button" onclick="testStatic('/assets/style.css')">测试CSS文件</button>
<button class="test-button" onclick="testStatic('/files/sample.json')">测试JSON文件</button>
<div id="static-result" class="result" style="display: none;"></div>
</div>
</div>
</div>
</div>
<!-- 可用端点 -->
<div class="section">
<h2>📋 可用端点</h2>
<h3>🔧 API 端点</h3>
<div class="endpoint">
<strong>健康检查:</strong> <a href="/api/health" target="_blank">/api/health</a>
</div>
<div class="endpoint">
<strong>静态文件状态:</strong> <a href="/api/status" target="_blank">/api/status</a>
</div>
<div class="endpoint">
<strong>静态文件信息:</strong> <a href="/api/static-info" target="_blank">/api/static-info</a>
</div>
<div class="endpoint">
<strong>基础路由:</strong> <a href="/hello" target="_blank">/hello</a>
</div>
<h3>📄 文件服务端点</h3>
<div class="endpoint">
<strong>基本文件服务:</strong> <a href="/file/test.txt" target="_blank">/file/test.txt</a>
</div>
<div class="endpoint">
<strong>缓存文件服务:</strong> <a href="/file-cached/test.txt" target="_blank">/file-cached/test.txt</a>
</div>
<div class="endpoint">
<strong>文件下载:</strong> <a href="/download/downloadable.txt" target="_blank">/download/downloadable.txt</a>
</div>
<div class="endpoint">
<strong>图片文件服务:</strong> <a href="/image/style.css" target="_blank">/image/style.css</a>
</div>
<div class="endpoint">
<strong>安全文件服务:</strong> <a href="/safe-file/test.txt" target="_blank">/safe-file/test.txt</a>
</div>
<h3>🎯 动态路由</h3>
<div class="endpoint">
<strong>单参数:</strong> <a href="/users/123" target="_blank">/users/123</a>
</div>
<div class="endpoint">
<strong>多参数:</strong> <a href="/posts/456/comments/789" target="_blank">/posts/456/comments/789</a>
</div>
<div class="endpoint">
<strong>嵌套参数:</strong> <a href="/api/users/101/posts/202" target="_blank">/api/users/101/posts/202</a>
</div>
<h3>📁 静态文件目录</h3>
<div class="endpoint">
<strong>默认目录:</strong> <a href="/test.txt" target="_blank">/test.txt</a> (来自 ./public)
</div>
<div class="endpoint">
<strong>资源目录:</strong> <a href="/assets/style.css" target="_blank">/assets/style.css</a> (来自 ./static)
</div>
<div class="endpoint">
<strong>文件目录:</strong> <a href="/files/sample.json" target="_blank">/files/sample.json</a> (来自 ./uploads)
</div>
</div>
<!-- 性能统计 -->
<div class="section">
<h2>📊 性能统计</h2>
<div class="stats">
<div class="stat-card">
<div class="stat-number" id="request-count">0</div>
<div>总请求数</div>
</div>
<div class="stat-card">
<div class="stat-number" id="success-rate">100%</div>
<div>成功率</div>
</div>
<div class="stat-card">
<div class="stat-number" id="avg-response">0ms</div>
<div>平均响应时间</div>
</div>
<div class="stat-card">
<div class="stat-number" id="cache-hit">0%</div>
<div>缓存命中率</div>
</div>
</div>
</div>
<!-- 代码示例 -->
<div class="section">
<h2>💻 代码示例</h2>
<div class="code-block">
// 基本路由设置
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')
</div>
</div>
<div class="footer">
<p>🎉 V-Hono 静态文件服务正常运行中</p>
<p>这是来自 <code>./public</code> 目录的 <code>index.html</code> 文件</p> <p>这是来自 <code>./public</code> 目录的 <code>index.html</code> 文件</p>
</div>
<div class="feature">
<h3>✨ V-Hono 静态文件服务特性</h3>
<p>✅ 自动索引文件支持</p>
<p>✅ 路径遍历攻击防护</p>
<p>✅ ETag 和缓存支持</p>
<p>✅ 智能 Content-Type 检测</p>
</div> </div>
<p><a href="/" style="color: #fff; text-decoration: underline;">← 返回主页</a></p> <script>
</div> let requestCount = 0;
let successCount = 0;
let totalResponseTime = 0;
let currentToken = 'abc123'; // 默认 token
// Token 管理函数
function setToken() {
const tokenInput = document.getElementById('token-input');
const newToken = tokenInput.value.trim();
if (newToken) {
currentToken = newToken;
document.getElementById('current-token').textContent = currentToken;
showMessage('Token 已更新', 'success');
} else {
showMessage('请输入有效的 Token', 'error');
}
}
function clearToken() {
currentToken = '';
document.getElementById('token-input').value = '';
document.getElementById('current-token').textContent = '未设置';
showMessage('Token 已清除', 'warning');
}
function showMessage(message, type) {
const messageDiv = document.createElement('div');
messageDiv.textContent = message;
messageDiv.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 10px 20px;
border-radius: 5px;
color: white;
font-weight: bold;
z-index: 1000;
animation: slideIn 0.3s ease;
`;
switch(type) {
case 'success':
messageDiv.style.background = '#4CAF50';
break;
case 'error':
messageDiv.style.background = '#f44336';
break;
case 'warning':
messageDiv.style.background = '#ff9800';
break;
}
document.body.appendChild(messageDiv);
setTimeout(() => {
messageDiv.style.animation = 'slideOut 0.3s ease';
setTimeout(() => document.body.removeChild(messageDiv), 300);
}, 2000);
}
// 添加 CSS 动画
const style = document.createElement('style');
style.textContent = `
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOut {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
`;
document.head.appendChild(style);
function showTab(tabName) {
// 隐藏所有标签内容
const tabContents = document.querySelectorAll('.tab-content');
tabContents.forEach(content => content.classList.remove('active'));
// 移除所有按钮的active类
const tabButtons = document.querySelectorAll('.tab-button');
tabButtons.forEach(button => button.classList.remove('active'));
// 显示选中的标签内容
document.getElementById(tabName).classList.add('active');
event.target.classList.add('active');
}
async function testAPI(endpoint) {
const resultDiv = document.getElementById('api-result');
resultDiv.style.display = 'block';
resultDiv.className = 'result loading';
resultDiv.textContent = '测试中...';
const startTime = Date.now();
try {
// 构建带 token 的 URL
const url = new URL(endpoint, window.location.origin);
if (currentToken) {
url.searchParams.set('token', currentToken);
}
const response = await fetch(url.toString());
const data = await response.text();
const responseTime = Date.now() - startTime;
resultDiv.className = 'result';
resultDiv.textContent = `✅ 成功 (${responseTime}ms)\n状态码: ${response.status}\n请求URL: ${url.toString()}\n响应数据:\n${data}`;
updateStats(true, responseTime);
} catch (error) {
resultDiv.className = 'result error';
resultDiv.textContent = `❌ 错误\n${error.message}`;
updateStats(false, 0);
}
}
async function testFile(endpoint) {
const resultDiv = document.getElementById('file-result');
resultDiv.style.display = 'block';
resultDiv.className = 'result loading';
resultDiv.textContent = '测试中...';
const startTime = Date.now();
try {
// 构建带 token 的 URL
const url = new URL(endpoint, window.location.origin);
if (currentToken) {
url.searchParams.set('token', currentToken);
}
const response = await fetch(url.toString());
const data = await response.text();
const responseTime = Date.now() - startTime;
resultDiv.className = 'result';
resultDiv.textContent = `✅ 成功 (${responseTime}ms)\n状态码: ${response.status}\n请求URL: ${url.toString()}\n文件大小: ${data.length} 字符\n内容预览:\n${data.substring(0, 200)}${data.length > 200 ? '...' : ''}`;
updateStats(true, responseTime);
} catch (error) {
resultDiv.className = 'result error';
resultDiv.textContent = `❌ 错误\n${error.message}`;
updateStats(false, 0);
}
}
async function testRoute(endpoint) {
const resultDiv = document.getElementById('route-result');
resultDiv.style.display = 'block';
resultDiv.className = 'result loading';
resultDiv.textContent = '测试中...';
const startTime = Date.now();
try {
// 构建带 token 的 URL
const url = new URL(endpoint, window.location.origin);
if (currentToken) {
url.searchParams.set('token', currentToken);
}
const response = await fetch(url.toString());
const data = await response.text();
const responseTime = Date.now() - startTime;
resultDiv.className = 'result';
resultDiv.textContent = `✅ 成功 (${responseTime}ms)\n状态码: ${response.status}\n请求URL: ${url.toString()}\n路由参数解析:\n${data}`;
updateStats(true, responseTime);
} catch (error) {
resultDiv.className = 'result error';
resultDiv.textContent = `❌ 错误\n${error.message}`;
updateStats(false, 0);
}
}
async function testStatic(endpoint) {
const resultDiv = document.getElementById('static-result');
resultDiv.style.display = 'block';
resultDiv.className = 'result loading';
resultDiv.textContent = '测试中...';
const startTime = Date.now();
try {
// 构建带 token 的 URL
const url = new URL(endpoint, window.location.origin);
if (currentToken) {
url.searchParams.set('token', currentToken);
}
const response = await fetch(url.toString());
const data = await response.text();
const responseTime = Date.now() - startTime;
resultDiv.className = 'result';
resultDiv.textContent = `✅ 成功 (${responseTime}ms)\n状态码: ${response.status}\n请求URL: ${url.toString()}\n文件类型: ${response.headers.get('content-type')}\n文件大小: ${data.length} 字符`;
updateStats(true, responseTime);
} catch (error) {
resultDiv.className = 'result error';
resultDiv.textContent = `❌ 错误\n${error.message}`;
updateStats(false, 0);
}
}
function updateStats(success, responseTime) {
requestCount++;
if (success) {
successCount++;
totalResponseTime += responseTime;
}
const successRate = Math.round((successCount / requestCount) * 100);
const avgResponse = requestCount > 0 ? Math.round(totalResponseTime / requestCount) : 0;
const cacheHit = Math.round(Math.random() * 30 + 70); // 模拟缓存命中率
document.getElementById('request-count').textContent = requestCount;
document.getElementById('success-rate').textContent = successRate + '%';
document.getElementById('avg-response').textContent = avgResponse + 'ms';
document.getElementById('cache-hit').textContent = cacheHit + '%';
}
// 页面加载时自动测试健康检查
window.addEventListener('load', function() {
setTimeout(() => {
testAPI('/api/health');
}, 1000);
});
// 添加键盘事件支持
document.getElementById('token-input').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
setToken();
}
});
</script>
</body> </body>
</html> </html>
+15
View File
@@ -0,0 +1,15 @@
{
"message": "Hello from v-hono Context.file!",
"features": [
"File serving with automatic Content-Type detection",
"Security validation against path traversal",
"Caching support with ETag and Last-Modified",
"Custom headers and options",
"Error handling for missing files"
],
"example": {
"endpoint": "/file/sample.json",
"content_type": "application/json",
"cached": true
}
}
+9 -6
View File
@@ -1,9 +1,12 @@
Hello from V-Hono static file server! This is a test file for v-hono Context.file functionality.
This is a test file from the ./public directory.
You can access this file directly via the root path (/).
Features demonstrated: Features demonstrated:
- Text file serving - Basic file serving
- Content-Type detection - Content-Type detection
- Path mapping from URL to file system - Security validation
- Caching options
This file can be accessed via:
- GET /file/test.txt
- GET /file-cached/test.txt
- GET /safe-file/test.txt
+8
View File
@@ -0,0 +1,8 @@
This is a downloadable file.
This file demonstrates the download functionality with:
- Content-Disposition: attachment header
- Custom Content-Type
- File download behavior
Access this file via: GET /download/downloadable.txt