feat: add hono.file method
This commit is contained in:
@@ -4,21 +4,37 @@ import net.http
|
||||
import hono
|
||||
|
||||
fn main() {
|
||||
// ===== 1. 应用初始化 =====
|
||||
mut app := hono.Hono.new()
|
||||
|
||||
// ===== 2. 中间件配置 =====
|
||||
|
||||
// 日志中间件
|
||||
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(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())
|
||||
|
||||
// 静态文件服务 - 指定路径前缀
|
||||
// 资源文件服务(./static 目录)
|
||||
app.use(hono.serve_static_path('/assets', './static'))
|
||||
|
||||
// 静态文件服务 - 自定义配置
|
||||
// 上传文件服务(./uploads 目录)
|
||||
options := hono.StaticOptions{
|
||||
root: './uploads'
|
||||
path: '/files'
|
||||
@@ -34,12 +50,9 @@ fn main() {
|
||||
}
|
||||
app.use(hono.serve_static(options))
|
||||
|
||||
// 基本路由
|
||||
app.get('/hello', fn (mut c hono.Context) http.Response {
|
||||
return c.text('Hello, World!')
|
||||
})
|
||||
// ===== 4. 基础路由 =====
|
||||
|
||||
// 根路径
|
||||
// 根路径 - 欢迎页面
|
||||
app.get('/', fn (mut c hono.Context) http.Response {
|
||||
html_content := '<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
@@ -48,12 +61,14 @@ fn main() {
|
||||
<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; }
|
||||
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; }
|
||||
.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; }
|
||||
.file-section { background: #e8f5e8; border-left: 4px solid #4CAF50; }
|
||||
.route-section { background: #fff3e0; border-left: 4px solid #FF9800; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -95,7 +110,26 @@ fn main() {
|
||||
</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>
|
||||
<div class="endpoint">
|
||||
<strong>单参数:</strong> <a href="/users/123">/users/123</a>
|
||||
@@ -111,7 +145,7 @@ fn main() {
|
||||
<div class="section">
|
||||
<h2>✨ 特性</h2>
|
||||
<ul>
|
||||
<li class="feature">🚀 高性能路由系统</li>
|
||||
<li class="feature">🚀 高性能路由系统(Trie + Hybrid)</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">🎯 智能 Content-Type 检测</li>
|
||||
<li class="feature">🔧 洋葱模型中间件</li>
|
||||
<li class="feature">🔄 支持所有 HTTP 方法</li>
|
||||
<li class="feature">📊 路由性能统计</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -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\\"}"
|
||||
</pre>
|
||||
</div>
|
||||
</body>
|
||||
@@ -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')
|
||||
|
||||
+162
@@ -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
|
||||
}
|
||||
|
||||
+694
-23
@@ -3,48 +3,719 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>V-Hono 静态文件服务 - Public 目录</title>
|
||||
<title>V-Hono - 轻量级 V 语言 Web 框架</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
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;
|
||||
background: #fafafa;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.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;
|
||||
border-radius: 15px;
|
||||
backdrop-filter: blur(10px);
|
||||
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; }
|
||||
.feature {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
|
||||
.section h2 {
|
||||
color: #2c5aa0;
|
||||
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;
|
||||
margin: 10px 0;
|
||||
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>
|
||||
</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 class="header">
|
||||
<h1>🚀 V-Hono</h1>
|
||||
<p>轻量级 V 语言 Web 框架 - 功能完整展示</p>
|
||||
</div>
|
||||
|
||||
<p><a href="/" style="color: #fff; text-decoration: underline;">← 返回主页</a></p>
|
||||
<!-- 核心特性展示 -->
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
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>
|
||||
</html>
|
||||
@@ -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
@@ -1,9 +1,12 @@
|
||||
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 (/).
|
||||
This is a test file for v-hono Context.file functionality.
|
||||
|
||||
Features demonstrated:
|
||||
- Text file serving
|
||||
- Basic file serving
|
||||
- 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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user