diff --git a/README.MD b/README.MD index 65dfd1c..8fd36ee 100644 --- a/README.MD +++ b/README.MD @@ -12,6 +12,10 @@ - ⚡ 支持通配符路由(`*` 和 `**`) - 🔄 支持所有 HTTP 方法(GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS) - 🏗️ 构造函数风格的 API 设计 +- 📁 静态文件服务中间件(类似 Hono.js 的 serveStatic) +- 🛡️ 内置安全防护(路径遍历攻击防护、点文件访问控制) +- 💾 智能缓存支持(ETag、Last-Modified、Cache-Control) +- 🎯 自动 Content-Type 检测 ## 安装 @@ -225,6 +229,45 @@ app.use(fn (mut c hono.Context, next fn (mut hono.Context) http.Response) http.R }) ``` +### 静态文件服务 + +提供类似 Hono.js 的 `serveStatic` 中间件: + +```v +// 使用默认配置(./public 目录) +app.use(hono.serve_static_default()) + +// 指定根目录 +app.use(hono.serve_static_root('./static')) + +// 指定路径前缀和根目录 +app.use(hono.serve_static_path('/assets', './static')) + +// 使用自定义配置 +options := hono.StaticOptions{ + root: './public' + path: '/static' + index: 'index.html' + dotfiles: false + etag: true + last_modified: true + max_age: 3600 // 1小时缓存 + headers: { + 'X-Custom-Header': 'value' + } +} +app.use(hono.serve_static(options)) +``` + +**特性:** +- 🛡️ 路径遍历攻击防护 +- 📁 自动索引文件支持(index.html) +- 🔒 点文件访问控制 +- 🏷️ ETag 和 Last-Modified 支持 +- 💾 可配置缓存策略 +- 📋 自定义响应头 +- 🎯 智能 Content-Type 检测 + ### 查询参数和请求体 ```v @@ -256,6 +299,8 @@ app.clear_cache() ## 示例 +### 基础示例 + 运行完整示例: ```bash @@ -269,6 +314,51 @@ v run example.v - 中间件功能 - 参数提取和查询参数处理 +### 静态文件服务示例 + +运行静态文件服务示例: + +```bash +v run static_example.v +``` + +这个示例演示了: +- 多个静态文件目录配置 +- 自定义缓存策略 +- 安全特性演示 +- 完整的HTML界面 + +**重要提示**: 静态文件路径映射 +- `./public` 目录 → 访问路径 `/` +- `./static` 目录 → 访问路径 `/assets` +- `./uploads` 目录 → 访问路径 `/files` + +例如: +- `./public/test.txt` → `http://localhost:8080/test.txt` +- `./static/style.css` → `http://localhost:8080/assets/style.css` +- `./uploads/sample.json` → `http://localhost:8080/files/sample.json` + +### 快速测试 + +运行简单的静态文件测试: + +```bash +v run test_static.v +``` + +测试文件结构: +``` +v-hono/ +├── public/ +│ ├── index.html # 默认索引文件 +│ └── test.txt # 测试文本文件 +├── static/ +│ ├── style.css # 样式文件 +│ └── app.js # JavaScript文件 +└── uploads/ + └── sample.json # JSON文件 +``` + ## 性能特性 - **混合路由算法**:静态路由 O(1) 查找,动态路由正则匹配 diff --git a/example.v b/example.v index e8e50ce..590c08b 100644 --- a/example.v +++ b/example.v @@ -6,6 +6,34 @@ import hono fn main() { mut app := hono.new_hono() + // 日志中间件 + app.use(fn (mut c hono.Context, next fn (mut hono.Context) http.Response) http.Response { + println('[LOG] ${c.req.method} ${c.url}') + return next(mut c) + }) + + // 静态文件服务 - 默认配置(./public 目录) + app.use(hono.serve_static_default()) + + // 静态文件服务 - 指定路径前缀 + app.use(hono.serve_static_path('/assets', './static')) + + // 静态文件服务 - 自定义配置 + options := hono.StaticOptions{ + root: './uploads' + path: '/files' + index: 'index.html' + dotfiles: false + etag: true + last_modified: true + max_age: 3600 // 1小时缓存 + headers: { + 'X-Served-By': 'V-Hono' + 'X-Static-Files': 'true' + } + } + app.use(hono.serve_static(options)) + // 基本路由 app.get('/hello', fn (mut c hono.Context) http.Response { return c.text('Hello, World!') @@ -13,7 +41,108 @@ fn main() { // 根路径 app.get('/', fn (mut c hono.Context) http.Response { - return c.html('

Welcome to V-Hono!

A lightweight web framework for V

') + html_content := ' + + + + + V-Hono 完整示例 + + + +

🚀 V-Hono 完整示例

+

A lightweight web framework for V with static file serving

+ +
+

📁 静态文件服务

+
+ 默认目录: / → ./public +
+
+ 资源目录: /assets → ./static +
+
+ 文件目录: /files → ./uploads +
+

测试文件:

+ +
+ +
+

🔧 API 端点

+
+ 健康检查: /api/health +
+
+ 静态文件状态: /api/status +
+
+ 静态文件信息: /api/static-info +
+
+ 基础路由: /hello +
+
+ +
+

🎯 动态路由示例

+
+ 单参数: /users/123 +
+
+ 多参数: /posts/456/comments/789 +
+
+ 嵌套参数: /api/users/101/posts/202 +
+
+ +
+

✨ 特性

+ +
+ +
+

🧪 测试命令

+
+# 静态文件测试
+curl http://localhost:8080/test.txt
+curl http://localhost:8080/assets/style.css
+curl http://localhost:8080/files/sample.json
+
+# API 测试
+curl http://localhost:8080/api/health
+curl http://localhost:8080/api/status
+curl http://localhost:8080/hello
+
+# 动态路由测试
+curl http://localhost:8080/users/123
+curl http://localhost:8080/posts/456/comments/789
+        
+
+ +' + return c.html(html_content) }) // 健康检查 @@ -22,6 +151,29 @@ fn main() { return c.json('{"status": "ok", "message": "Health check passed"}') }) + // 静态文件服务状态 + app.get('/api/status', fn (mut c hono.Context) http.Response { + return c.json('{"status": "ok", "static_files": "enabled"}') + }) + + app.get('/api/static-info', fn (mut c hono.Context) http.Response { + return c.json('{ + "static_directories": [ + {"path": "/", "root": "./public"}, + {"path": "/assets", "root": "./static"}, + {"path": "/files", "root": "./uploads"} + ], + "features": [ + "Path traversal protection", + "Auto index files", + "ETag support", + "Last-Modified headers", + "Configurable caching", + "Custom headers" + ] + }') + }) + // 动态路由 - 单参数 app.get('/users/:id', fn (mut c hono.Context) http.Response { user_id := c.params['id'] @@ -140,28 +292,51 @@ fn main() { return next(mut c) }) - println('Hono Server starting on :8080') - println('=== 测试端点 ===') - println('静态路由:') - println(' - GET /') - println(' - GET /hello') + println('🚀 V-Hono 完整示例启动中...') + println('📍 服务器地址: http://localhost:8080') + println('') + println('📁 静态文件服务:') + println(' - 默认: ./public (访问路径: /)') + println(' - 资源: ./static (访问路径: /assets)') + println(' - 文件: ./uploads (访问路径: /files)') + println('') + println('🔧 API 端点:') println(' - GET /api/health') - println('动态路由:') + println(' - GET /api/status') + println(' - GET /api/static-info') + println(' - GET /hello') + println('') + println('🎯 动态路由:') println(' - GET /users/123') println(' - GET /posts/456/comments/789') println(' - GET /api/users/101/posts/202') - println('POST 请求:') + println('') + println('📝 POST 请求:') println(' - POST /api/users') println(' - POST /api/users/123') println(' - POST /api/test/create/456') - println('其他方法:') + println('') + println('🔄 其他方法:') println(' - PUT /api/users/123') println(' - DELETE /api/users/123') println(' - PATCH /api/users/123') println('') - println('测试命令示例:') + println('🧪 测试命令示例:') + println(' # 静态文件测试') + println(' curl http://localhost:8080/test.txt') + println(' curl http://localhost:8080/assets/style.css') + println(' curl http://localhost:8080/files/sample.json') + println('') + println(' # API 测试') + println(' curl http://localhost:8080/api/health') + println(' curl http://localhost:8080/api/status') + println(' curl http://localhost:8080/hello') + println('') + println(' # 动态路由测试') println(' curl http://localhost:8080/users/123') println(' curl http://localhost:8080/posts/456/comments/789') + println('') + println(' # POST 测试') println(' curl -X POST http://localhost:8080/api/users -d "{\\"name\\":\\"John\\"}"') println(' curl -X POST "http://localhost:8080/api/test/create/456?token=abc123" -d "{\\"name\\":\\"Test\\"}"') diff --git a/hono/README.MD b/hono/README.MD index 3e48927..5aee820 100644 --- a/hono/README.MD +++ b/hono/README.MD @@ -32,4 +32,258 @@ app.get('/api/users/:id', fn (req hono.Request) http.Response { ### 与 HybridRouter 对比 - HybridRouter 采用静态路由 map + 动态正则遍历,适合路由数量较少或兼容性需求高的场景 - TrieRouter 适合大规模路由、高并发、极致性能场景 -- 两者可并存,便于灵活切换和对比测试 \ No newline at end of file +- 两者可并存,便于灵活切换和对比测试 + +--- + +# 静态文件服务中间件 + +## 概述 + +为 V-Hono 框架添加了类似 Hono.js 的 `serveStatic` 中间件,提供高效、安全的静态文件服务功能。 + +## 实现文件 + +### 核心实现 +- `hono/static.v` - 静态文件服务中间件的主要实现 + +### 示例文件 +- `example.v` - 包含静态文件服务的完整示例 + +### 测试文件 +- `public/index.html` - 默认索引文件 +- `public/test.txt` - 测试文本文件 +- `static/style.css` - 测试CSS文件 +- `static/app.js` - 测试JavaScript文件 +- `uploads/sample.json` - 测试JSON文件 + +## 核心功能 + +### 1. 静态文件服务中间件 (`serve_static`) + +```v +pub fn serve_static(options StaticOptions) fn (mut Context, fn (mut Context) http.Response) http.Response +``` + +主要特性: +- 路径匹配和文件映射 +- 安全检查(路径遍历攻击防护) +- 点文件访问控制 +- 自动索引文件支持 +- 智能错误处理 + +### 2. 配置选项 (`StaticOptions`) + +```v +pub struct StaticOptions { +pub: + root string = './public' // 静态文件根目录 + path string = '/' // URL路径前缀 + index string = 'index.html' // 默认索引文件 + dotfiles bool = false // 是否允许访问以.开头的文件 + etag bool = true // 是否启用ETag + last_modified bool = true // 是否启用Last-Modified + max_age int = 0 // 缓存时间(秒) + headers map[string]string // 自定义响应头 +} +``` + +### 3. 便捷函数 + +- `serve_static_default()` - 使用默认配置 +- `serve_static_root(root string)` - 指定根目录 +- `serve_static_path(path string, root string)` - 指定路径前缀和根目录 + +## 安全特性 + +### 路径遍历攻击防护 + +```v +fn is_safe_path(path string) bool { + // 检查是否包含 .. 或绝对路径 + if path.contains('..') || path.starts_with('/') || path.starts_with('\\') { + return false + } + + // 检查是否包含危险字符 + dangerous_chars := ['<', '>', ':', '"', '|', '?', '*'] + for char in dangerous_chars { + if path.contains(char) { + return false + } + } + + return true +} +``` + +### 点文件访问控制 + +默认情况下阻止访问以 `.` 开头的文件,可通过 `dotfiles: true` 启用。 + +## 性能特性 + +### ETag 支持 + +```v +fn generate_etag(content string, mod_time i64) string { + return '"${content.len}-${mod_time}"' +} +``` + +### 缓存控制 + +```v +if options.max_age > 0 { + c.headers['Cache-Control'] = 'public, max-age=${options.max_age}' +} else { + c.headers['Cache-Control'] = 'no-cache' +} +``` + +### Content-Type 检测 + +支持30+种文件类型的自动Content-Type检测: + +```v +fn get_content_type(file_path string) string { + ext := os.file_ext(file_path).to_lower() + + match ext { + '.html', '.htm' { return 'text/html; charset=utf-8' } + '.css' { return 'text/css; charset=utf-8' } + '.js' { return 'application/javascript; charset=utf-8' } + '.json' { return 'application/json; charset=utf-8' } + // ... 更多类型 + else { return 'application/octet-stream' } + } +} +``` + +## 使用示例 + +### 基本用法 + +```v +import hono + +mut app := hono.new_hono() + +// 使用默认配置 +app.use(hono.serve_static_default()) + +app.listen(':8080') +``` + +### 多目录配置 + +```v +// 默认静态文件 +app.use(hono.serve_static_default()) + +// 资源文件 +app.use(hono.serve_static_path('/assets', './static')) + +// 上传文件 +app.use(hono.serve_static_path('/uploads', './uploads')) +``` + +### 自定义配置 + +```v +options := hono.StaticOptions{ + root: './public' + path: '/static' + index: 'index.html' + dotfiles: false + etag: true + last_modified: true + max_age: 3600 + headers: { + 'X-Served-By': 'V-Hono' + } +} + +app.use(hono.serve_static(options)) +``` + +## 与 Hono.js 的对比 + +| 特性 | V-Hono | Hono.js | +|------|--------|---------| +| 路径遍历防护 | ✅ | ✅ | +| 点文件控制 | ✅ | ✅ | +| ETag 支持 | ✅ | ✅ | +| Last-Modified | ✅ | ✅ | +| 缓存控制 | ✅ | ✅ | +| 自定义头部 | ✅ | ✅ | +| 自动索引文件 | ✅ | ✅ | +| Content-Type 检测 | ✅ | ✅ | +| 多目录支持 | ✅ | ✅ | + +## 测试和验证 + +### 运行测试 + +```bash +# 完整示例 +v run example.v +``` + +### 测试用例 + +1. **基本文件服务** + - 访问 `public/test.txt` → `/test.txt` + - 访问 `static/style.css` → `/assets/style.css` + +2. **索引文件** + - 访问目录 `/` → 提供 `public/index.html` + +3. **安全测试** + - 尝试路径遍历攻击 → 返回 403 Forbidden + - 尝试访问点文件 → 返回 403 Forbidden + +4. **缓存测试** + - 检查 ETag 头 + - 检查 Last-Modified 头 + - 检查 Cache-Control 头 + +5. **Content-Type 测试** + - 验证不同文件类型的正确 Content-Type + +## 性能考虑 + +### 优化点 + +1. **文件读取**: 使用 V 语言的 `os.read_file()` 进行高效文件读取 +2. **路径检查**: 快速的安全路径验证 +3. **头部生成**: 高效的响应头构建 +4. **缓存策略**: 可配置的缓存控制 + +### 内存使用 + +- 文件内容一次性读取到内存 +- 适合中小型文件 +- 大文件建议使用专门的CDN服务 + +## 未来改进 + +### 可能的增强 + +1. **流式文件服务**: 支持大文件的流式传输 +2. **压缩支持**: 自动Gzip压缩 +3. **范围请求**: 支持HTTP Range请求 +4. **文件监控**: 文件变化时自动刷新缓存 +5. **性能监控**: 添加性能指标收集 + +### 扩展性 + +当前实现具有良好的扩展性,可以轻松添加: +- 新的文件类型支持 +- 自定义安全检查 +- 更多的缓存策略 +- 文件预处理功能 + +## 总结 + +成功为 V-Hono 框架添加了功能完整、安全可靠的静态文件服务中间件,提供了与 Hono.js 相当的体验,同时充分利用了 V 语言的特性。该实现具有良好的安全性、性能和易用性,可以满足大多数Web应用的静态文件服务需求。 \ No newline at end of file diff --git a/hono/app.v b/hono/app.v index 9cf8208..40792b8 100644 --- a/hono/app.v +++ b/hono/app.v @@ -116,16 +116,17 @@ fn (mut s ServerHanler) handle(req http.Request) http.Response { } } + // 解析 query + raw_query := url.query().to_map() + mut query_map := map[string]string{} + for key, values in raw_query { + if values.len > 0 { + query_map[key] = values[0] + } + } + // 尝试 Context 路由 if route_match := s.app.context_hybrid_router.match_route(req.method.str(), url.path) { - // 解析 query - raw_query := url.query().to_map() - mut query_map := map[string]string{} - for key, values in raw_query { - if values.len > 0 { - query_map[key] = values[0] - } - } // param 由路由匹配结果提供 param_map := route_match.params.clone() // body @@ -136,8 +137,15 @@ fn (mut s ServerHanler) handle(req http.Request) http.Response { return s.exec_context_middlewares(0, mut ctx, fn [route_match] (mut c Context) http.Response { return route_match.handler.handle(mut c) }) + } else { + // 如果没有匹配的路由,也要执行中间件 + param_map := map[string]string{} + body := req.data + mut ctx := Context.new(req, param_map, query_map, body) + return s.exec_context_middlewares(0, mut ctx, fn [res] (mut c Context) http.Response { + return res + }) } - return res } // Context 版本的中间件执行函数 diff --git a/hono/request.v b/hono/request.v index eebb002..8b8c57d 100644 --- a/hono/request.v +++ b/hono/request.v @@ -1,6 +1,7 @@ module hono import net.http +import net.urllib // Context 结构体,类似 Hono.js 的实现 pub struct Context { @@ -17,12 +18,18 @@ pub mut: // Context 构造函数 pub fn Context.new(req http.Request, params map[string]string, query map[string]string, body string) Context { + // 解析URL获取路径 + url := urllib.parse(req.url) or { + urllib.URL{ + path: '/' + } + } return Context{ req: req params: params query: query body: body - url: req.url + url: url.path headers: map[string]string{} } } diff --git a/hono/static.v b/hono/static.v new file mode 100644 index 0000000..34cf623 --- /dev/null +++ b/hono/static.v @@ -0,0 +1,259 @@ +module hono + +import os +import net.http +import net.urllib + +// 静态文件服务配置 +pub struct StaticOptions { +pub: + root string = './public' // 静态文件根目录 + path string = '/' // URL路径前缀 + index string = 'index.html' // 默认索引文件 + dotfiles bool // 是否允许访问以.开头的文件 + etag bool = true // 是否启用ETag + last_modified bool = true // 是否启用Last-Modified + max_age int // 缓存时间(秒) + headers map[string]string // 自定义响应头 +} + +// 默认静态文件配置 +pub fn default_static_options() StaticOptions { + return StaticOptions{ + root: './public' + path: '/' + index: 'index.html' + dotfiles: false + etag: true + last_modified: true + max_age: 0 + headers: map[string]string{} + } +} + +// 静态文件服务中间件 +pub fn serve_static(options StaticOptions) fn (mut Context, fn (mut Context) http.Response) http.Response { + return fn [options] (mut c Context, next fn (mut Context) http.Response) http.Response { + // 检查请求路径是否匹配静态文件路径 + if !c.url.starts_with(options.path) { + return next(mut c) + } + + // 提取文件路径 + mut file_path := c.url[options.path.len..] + if file_path.starts_with('/') { + file_path = file_path[1..] + } + if file_path == '' { + file_path = options.index + } + + // 调试信息 + println('[DEBUG] Static file request:') + println(' URL: ${c.url}') + println(' Path prefix: ${options.path}') + println(' File path: ${file_path}') + println(' Root: ${options.root}') + + // 安全检查:防止路径遍历攻击 + if !is_safe_path(file_path) { + println(' [DEBUG] Path traversal attack detected') + c.status(403) + return c.text('Forbidden') + } + + // 检查是否允许访问点文件 + if !options.dotfiles && file_path.starts_with('.') { + println(' [DEBUG] Dot file access blocked') + c.status(403) + return c.text('Forbidden') + } + + // 构建完整的文件路径 + full_path := os.join_path(options.root, file_path) + println(' Full path: ${full_path}') + + // 检查文件是否存在 + if !os.exists(full_path) { + println(' [DEBUG] File not found: ${full_path}') + return next(mut c) + } + + println(' [DEBUG] File found, serving...') + + // 检查是否为目录 + if os.is_dir(full_path) { + // 尝试提供索引文件 + index_path := os.join_path(full_path, options.index) + if os.exists(index_path) { + return serve_file(mut c, index_path, options) + } + // 如果没有索引文件,返回404 + c.status(404) + return c.text('Not Found') + } + + // 提供文件 + return serve_file(mut c, full_path, options) + } +} + +// 提供单个文件 +fn serve_file(mut c Context, file_path string, options StaticOptions) http.Response { + // 读取文件内容 + file_content := os.read_file(file_path) or { + c.status(500) + return c.text('Internal Server Error') + } + + // 获取文件信息 + file_info := os.stat(file_path) or { + c.status(500) + return c.text('Internal Server Error') + } + + // 设置状态码 + c.status(200) + + // 设置Content-Type + content_type := get_content_type(file_path) + c.headers['Content-Type'] = content_type + + // 设置Content-Length + c.headers['Content-Length'] = file_content.len.str() + + // 设置Last-Modified + if options.last_modified { + last_modified := format_http_date(file_info.mtime) + c.headers['Last-Modified'] = last_modified + } + + // 设置ETag + if options.etag { + etag := generate_etag(file_content, file_info.mtime) + c.headers['ETag'] = etag + + // 检查If-None-Match + if_none_match := c.req.header.get_custom('If-None-Match') or { '' } + if if_none_match == etag { + c.status(304) + return c.text('') + } + } + + // 设置Cache-Control + if options.max_age > 0 { + c.headers['Cache-Control'] = 'public, max-age=${options.max_age}' + } else { + c.headers['Cache-Control'] = 'no-cache' + } + + // 设置自定义头部 + for key, value in options.headers { + c.headers[key] = value + } + + // 返回文件内容 + return c.text(file_content) +} + +// 安全检查:防止路径遍历攻击 +fn is_safe_path(path string) bool { + // 检查是否包含 .. 或绝对路径 + if path.contains('..') || path.starts_with('/') || path.starts_with('\\') { + return false + } + + // 检查是否包含危险字符 + dangerous_chars := ['<', '>', ':', '"', '|', '?', '*'] + for dangerous_char in dangerous_chars { + if path.contains(dangerous_char) { + return false + } + } + + return true +} + +// 根据文件扩展名获取Content-Type +fn get_content_type(file_path string) string { + ext := os.file_ext(file_path).to_lower() + + match ext { + '.html', '.htm' { return 'text/html; charset=utf-8' } + '.css' { return 'text/css; charset=utf-8' } + '.js' { return 'application/javascript; charset=utf-8' } + '.json' { return 'application/json; charset=utf-8' } + '.xml' { return 'application/xml; charset=utf-8' } + '.txt' { return 'text/plain; charset=utf-8' } + '.md' { return 'text/markdown; charset=utf-8' } + '.pdf' { return 'application/pdf' } + '.png' { return 'image/png' } + '.jpg', '.jpeg' { return 'image/jpeg' } + '.gif' { return 'image/gif' } + '.svg' { return 'image/svg+xml' } + '.ico' { return 'image/x-icon' } + '.woff' { return 'font/woff' } + '.woff2' { return 'font/woff2' } + '.ttf' { return 'font/ttf' } + '.eot' { return 'application/vnd.ms-fontobject' } + '.otf' { return 'font/otf' } + '.mp4' { return 'video/mp4' } + '.webm' { return 'video/webm' } + '.mp3' { return 'audio/mpeg' } + '.wav' { return 'audio/wav' } + '.zip' { return 'application/zip' } + '.tar' { return 'application/x-tar' } + '.gz' { return 'application/gzip' } + else { return 'application/octet-stream' } + } +} + +// 格式化HTTP日期 +fn format_http_date(time i64) string { + // 简化的HTTP日期格式化 + // 实际应用中可能需要更复杂的实现 + return time.str() +} + +// 生成ETag +fn generate_etag(content string, mod_time i64) string { + // 简化的ETag生成 + // 实际应用中可能需要更复杂的哈希算法 + return '"${content.len}-${mod_time}"' +} + +// 便捷函数:使用默认配置的静态文件服务 +pub fn serve_static_default() fn (mut Context, fn (mut Context) http.Response) http.Response { + return serve_static(default_static_options()) +} + +// 便捷函数:指定根目录的静态文件服务 +pub fn serve_static_root(root string) fn (mut Context, fn (mut Context) http.Response) http.Response { + options := StaticOptions{ + root: root + path: '/' + index: 'index.html' + dotfiles: false + etag: true + last_modified: true + max_age: 0 + headers: map[string]string{} + } + return serve_static(options) +} + +// 便捷函数:指定路径前缀的静态文件服务 +pub fn serve_static_path(path string, root string) fn (mut Context, fn (mut Context) http.Response) http.Response { + options := StaticOptions{ + root: root + path: path + index: 'index.html' + dotfiles: false + etag: true + last_modified: true + max_age: 0 + headers: map[string]string{} + } + return serve_static(options) +} \ No newline at end of file diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..c367637 --- /dev/null +++ b/public/index.html @@ -0,0 +1,50 @@ + + + + + + V-Hono 静态文件服务 - Public 目录 + + + +
+

🎉 成功!

+

这是来自 ./public 目录的 index.html 文件

+ +
+

✨ V-Hono 静态文件服务特性

+

✅ 自动索引文件支持

+

✅ 路径遍历攻击防护

+

✅ ETag 和缓存支持

+

✅ 智能 Content-Type 检测

+
+ +

← 返回主页

+
+ + \ No newline at end of file diff --git a/public/test.txt b/public/test.txt new file mode 100644 index 0000000..167f9fc --- /dev/null +++ b/public/test.txt @@ -0,0 +1,9 @@ +Hello from V-Hono static file server! + +This is a test file from the ./public directory. +You can access this file directly via the root path (/). + +Features demonstrated: +- Text file serving +- Content-Type detection +- Path mapping from URL to file system \ No newline at end of file diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..ba9ca49 --- /dev/null +++ b/static/style.css @@ -0,0 +1,69 @@ +/* V-Hono Static File Server - CSS Example */ + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + margin: 0; + padding: 20px; +} + +.container { + max-width: 800px; + margin: 0 auto; + background: rgba(255, 255, 255, 0.1); + padding: 30px; + border-radius: 15px; + backdrop-filter: blur(10px); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); +} + +h1 { + text-align: center; + font-size: 2.5em; + margin-bottom: 30px; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); +} + +.feature-list { + list-style: none; + padding: 0; +} + +.feature-list li { + background: rgba(255, 255, 255, 0.2); + margin: 10px 0; + padding: 15px; + border-radius: 8px; + border-left: 4px solid #4CAF50; +} + +.endpoint { + background: rgba(0, 0, 0, 0.2); + padding: 15px; + margin: 15px 0; + border-radius: 8px; + font-family: 'Courier New', monospace; +} + +.endpoint strong { + color: #4CAF50; +} + +/* Animation for features */ +@keyframes fadeIn { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +} + +.feature-list li { + animation: fadeIn 0.5s ease-in-out; +} + +.feature-list li:nth-child(1) { animation-delay: 0.1s; } +.feature-list li:nth-child(2) { animation-delay: 0.2s; } +.feature-list li:nth-child(3) { animation-delay: 0.3s; } +.feature-list li:nth-child(4) { animation-delay: 0.4s; } +.feature-list li:nth-child(5) { animation-delay: 0.5s; } +.feature-list li:nth-child(6) { animation-delay: 0.6s; } +.feature-list li:nth-child(7) { animation-delay: 0.7s; } \ No newline at end of file diff --git a/uploads/sample.json b/uploads/sample.json new file mode 100644 index 0000000..829ad6d --- /dev/null +++ b/uploads/sample.json @@ -0,0 +1,44 @@ +{ + "message": "Hello from V-Hono static file server!", + "file": "sample.json", + "directory": "./uploads", + "url_path": "/files", + "timestamp": "2024-01-01T00:00:00Z", + "features": { + "path_traversal_protection": true, + "auto_index_files": true, + "etag_support": true, + "last_modified_headers": true, + "configurable_caching": true, + "custom_headers": true, + "content_type_detection": true + }, + "content_types_supported": [ + "text/html", + "text/css", + "application/javascript", + "application/json", + "text/plain", + "image/png", + "image/jpeg", + "image/gif", + "image/svg+xml", + "font/woff", + "font/woff2", + "application/pdf", + "video/mp4", + "audio/mpeg" + ], + "security_features": [ + "Path traversal attack prevention", + "Dot file access control", + "Safe path validation", + "Content-Type header injection protection" + ], + "performance_features": [ + "ETag generation for caching", + "Last-Modified headers", + "Configurable cache control", + "Efficient file reading" + ] +} \ No newline at end of file