移动设备改进
This commit is contained in:
56
README.md
56
README.md
@@ -7,60 +7,72 @@ AFI 是 Go 编写的轻量级文件服务器, 是基于 Gossa 的现代化增强
|
|||||||
特色:
|
特色:
|
||||||
|
|
||||||
- 后端代码量低, 易于审查; 我们(虽然目前只有一个维护者 但是还是称"我们" :))在 Gossa 的基础上进行了大量重构, 大幅增强了前后端的可维护性并提升了效率
|
- 后端代码量低, 易于审查; 我们(虽然目前只有一个维护者 但是还是称"我们" :))在 Gossa 的基础上进行了大量重构, 大幅增强了前后端的可维护性并提升了效率
|
||||||
- 清晰现代且轻量的网页界面, 比原版 Gossa 的信息量更高且更悦目, 相较于 FileBrowser, AFI 易于嵌入至 `<iframe>` 且不依赖任何 web 框架, 操作无动画不拖泥带水, 无外部资源引用 (显然我们的前端不会引用 is-odd 和 is-even, 也不会因 left-pad 而崩溃 :D)
|
- 清晰现代且轻量的网页界面, 比原版 Gossa 的信息量更高且更悦目, 不依赖任何 web 框架, 操作无动画不拖泥带水, 除字体外无任何外部资源引用 (显然我们的前端不会引用 is-odd 和 is-even, 也不会因 left-pad 而崩溃 :D)
|
||||||
|
- 相较于 FileBrowser, AFI 易于嵌入至 `<iframe>` 且速度极快, 您可以将它用作图床或美观地嵌入到自己的博客附件页
|
||||||
- 单个可执行文件, 无需数据库, 速度极快, 方便快速部署
|
- 单个可执行文件, 无需数据库, 速度极快, 方便快速部署
|
||||||
- 与 FUSE 文件系统兼容 (如 rclone 实例配置的网络挂载)
|
- 与 FUSE 文件系统兼容 (如 rclone 实例配置的网络挂载)
|
||||||
- 支持拖拽上传文件和目录, 并以归档(.zip)形式方便地下载任意目录
|
- 支持拖拽上传文件和目录, 并以归档(.zip)形式方便地下载任意目录
|
||||||
- 支持 MD5, SHA-1, SHA-256, SHA-512 文件校验和的远程解算
|
- 支持 MD5, SHA-1, SHA-256, SHA-512 文件校验和的远程解算
|
||||||
- 支持通过 `DESCRIPT.ION` 文件存储的备注元信息
|
- 支持通过 `DESCRIPT.ION` 文件存储的备注元信息
|
||||||
- 完备的键盘操作支持
|
- 完备的键盘操作支持
|
||||||
- 基本的用户管理系统 (基于 HTTP Basic Authication), 我们移除了 Gossa 单一的只读/可写模式 (即 -ro 参数), 并保留了未登录的只读模式
|
- 基本的用户管理系统 (基于 HTTP Basic Authication), 我们移除了 Gossa 单一的只读/可写模式 (即 -ro 参数), 并保留了未登录状态下的默认只读模式
|
||||||
- 支持跳过隐藏项目 (以 `.` 开头的文件/目录)
|
- 支持跳过隐藏项目 (以 `.` 开头的文件/目录)
|
||||||
- 开发/生产模式分离, 便于免编译重载前端资源
|
- 开发/生产模式分离, 便于免编译重载前端资源
|
||||||
- 同时提供 RPC 调用接口便于编程与 webdav 接口便于挂载, 网页用户界面的 JavaScript 使用前者
|
- 同时提供简易的 RPC 调用接口便于编程与 webdav 接口(TODO)便于挂载, 网页用户界面的 JavaScript 使用前者
|
||||||
- 低占用高性能, 网页用户界面每秒可承受万级以上响应并持续保持毫秒级低延迟, 接口性能更高
|
- 低占用高性能, 网页用户界面每秒可承受万级以上响应并持续保持毫秒级低延迟, 接口性能更高
|
||||||
- 默认启用 gzip 压缩, 并为 iframe 嵌入优化
|
- 默认启用 gzip 压缩, 并为 iframe 嵌入优化
|
||||||
- 性能高效, 提供真正的毫秒级响应, 用户使用体验不让位于技术审美与性能
|
- 性能高效, 提供毫秒级响应, 用户使用体验不让位于技术审美与性能
|
||||||
|
- 改进的安全设计, 相较原项目增加了对 XSS 等攻击的防护
|
||||||
|
|
||||||
## 安全与不安全设计
|
## 安全与不安全设计
|
||||||
|
|
||||||
考虑到此程序可能被使用的场景与便于方案选型, 笔者必须指出此程序存在的问题
|
考虑到此程序可能被使用的场景与便于方案选型, 笔者必须指出此程序存在的问题, 以免您产生虚假的安全感
|
||||||
如您发现了其他可能的漏洞, 请在 issues 指出, 若属实, 笔者会将其修复或加入下方列表
|
如果您在为安全性抉择是否使用此项目, 我们高度建议您不要使用它
|
||||||
|
如您发现了其他可能的漏洞, 请在 issues 指出, 若属实, 笔者会将其修复或加入下方列表, 并且将您的名字加入致谢名单
|
||||||
|
注意: 请不要把"文档写出来了"等同于"只有这些问题"(我们当然追求安全审计文档的完善度)
|
||||||
|
|
||||||
安全建议:
|
安全建议:
|
||||||
|
|
||||||
- 强烈建议配置一个 nginx 反向代理
|
- 强烈建议配置一个 nginx 反向代理并向外部阻断应用本身的端口
|
||||||
- 必要时设置超时, 限制请求大小与频率
|
- 必要时设置超时, 限制请求大小与频率
|
||||||
- 完全避免使用 HTTP
|
- 如果可以的话, 最好套一层 WAF
|
||||||
|
- 配置好 SSL 证书, 完全避免使用 HTTP
|
||||||
- 必要时设置磁盘配额
|
- 必要时设置磁盘配额
|
||||||
|
- 一定不要把分享目录设置为用户目录或根目录
|
||||||
|
- 使用环境变量 `AFI_AUTH` 而不是参数设置用户验证键值对
|
||||||
|
- 使用普通用户, chroot 或容器运行
|
||||||
|
|
||||||
- 安全设计:
|
- 安全设计:
|
||||||
- 可防止路径穿越攻击
|
- 可防止路径穿越攻击
|
||||||
- 有基本的权限管理系统 (基于 HTTP Basic Authication)
|
- 有基本的权限管理系统 (基于 HTTP Basic Authentication)
|
||||||
- 不安全设计:
|
- 不安全设计:
|
||||||
- 不能完全防范的攻击
|
- 不能防范且无法容忍的攻击
|
||||||
- XSS (不可防范)
|
- 如上文所述, 这里没写不等于没有, 欢迎通过 issues 提出
|
||||||
- Timing Attack (不可防范, 但可通过设置较复杂的用户名规避)
|
- 未防范但几乎不可能成功的攻击
|
||||||
|
- Timing Attack (未防范, 密码在后端明文比较, 但可通过设置较不寻常的用户名规避) -> 我们未来大概率不会改
|
||||||
- 能防范的攻击
|
- 能防范的攻击
|
||||||
- symlink 逃逸攻击 (可选且默认开启, 但开启后您就不能使用 linux 的符号链接便利了)
|
- XSS 攻击
|
||||||
|
- Symlink 逃逸攻击 (可选且默认开启, 但开启后您就不能使用 linux 的符号链接便利了)
|
||||||
- 路径穿越攻击
|
- 路径穿越攻击
|
||||||
- CSRF (HTTP Basic Authication 天然防范)
|
- CSRF 攻击 (HTTP Basic Authentication 天然防范)
|
||||||
- 其他不安全设计:
|
- 其他不安全设计:
|
||||||
- 密码以明文形式传输 (因此千万不要在非 HTTPS 环境公网部署或预留 HTTP 访问, 除非您完全不配置用户(相当于原版 Gossa 的只读模式))
|
- 密码以明文形式传输 (因此千万不要在非 HTTPS 环境公网部署或预留 HTTP 访问, 除非您完全不配置用户(相当于原版 Gossa 的只读模式)) -> 我们未来会改, 但会在增加 Argon2 的基础上保留明文与 MD5 模式
|
||||||
- 后端密码参数化 (可登录服务器后用 ps 或类似工具查看)
|
- 后端密码参数化 (可登录服务器后轻松用 ps 或类似工具查看), 已经可以环境变量安全设置, 但不会移除 `-auth` 选项以防您需要测试或使用 PaaS 部署
|
||||||
- 日志可能泄漏敏感信息 (可以通过修改源代码编译解决, 但我们提供的二进制释出没有移除敏感信息输出)
|
|
||||||
- DoS 攻击 - 我们认为这件事应该由 nginx 反向代理预防
|
- DoS 攻击 - 我们认为这件事应该由 nginx 反向代理预防
|
||||||
- 请求大小无限制导致的超大文件占用和可能导致的 OOM 崩溃 - 我们认为这件事应该由 nginx 反向代理或磁盘配额限制预防, 并且就事实而言, 这很大程度上是有权限的用户行为不当引起的问题
|
- 请求大小和配额无限制导致的超大文件占用和可能导致的 OOM 崩溃 - 我们认为这件事应该由 nginx 反向代理或磁盘配额限制预防, 并且就事实而言, 内部威胁不在模型内, 这很大程度上是有权限的用户行为不当引起的问题
|
||||||
- 外部字体 Referer 泄露 (仅会得知您访问的链接, 但如果您需要, 可以移除前端中引用的在线字体链接) 就事实而言, 外部字体提供商没有收集这种数据的动机, 但如果您在内网部署, 则您可能不希望任何外部服务知道内网的文件结构
|
|
||||||
- 无自带的响应超时: 可能招致 Slowloris 服务瘫痪 - 但这可由 nginx 反向代理预防
|
- 无自带的响应超时: 可能招致 Slowloris 服务瘫痪 - 但这可由 nginx 反向代理预防
|
||||||
- 权限提升风险: AFI 以什么用户运行, 就有什么权限 -- 如果以 root 运行且将分享目录设置为根目录, 则任何登录用户都能删除系统
|
- 权限提升风险: AFI 以什么用户运行, 就有什么权限 -- 如果以 root 运行且将分享目录设置为根目录, 则任何登录用户都能删除系统, 或者通过上传 `.ssh/authorized_keys` 或 `.bashrc` 远程控制您的服务器操作系统 -> 我们改不了
|
||||||
|
- 未默认杜绝的隐私问题:
|
||||||
|
- 外部字体 Referer 泄露 (仅会得知您访问的链接, 但如果您需要, 可以移除前端中引用的在线字体链接) 就事实而言, 我们认为外部字体提供商不太可能有收集这种数据的动机, 但如果您在高敏感内网部署, 则您可能不希望任何外部服务能够得知内网的文件结构
|
||||||
|
- 日志可能泄漏敏感信息 (可以通过修改源代码编译解决, 但我们提供的二进制释出没有移除敏感信息输出)
|
||||||
- 未对特定安全需求进行的设计:
|
- 未对特定安全需求进行的设计:
|
||||||
|
- 不自带 https 服务器 - 如果您在公网直接跑这个程序并且使用它, 您等同于裸奔
|
||||||
- 不支持细粒度权限控制 (我们支持配置多个用户, 但登录后您就只能是对分享目录下的文件完全可写(取决于运行 afi 的用户)的状态了)
|
- 不支持细粒度权限控制 (我们支持配置多个用户, 但登录后您就只能是对分享目录下的文件完全可写(取决于运行 afi 的用户)的状态了)
|
||||||
- 没有自带的防暴力破解机制
|
- 没有自带的防暴力破解机制
|
||||||
- 没有操作审计日志
|
- 没有操作审计日志
|
||||||
- 没有密码哈希存储 (后端明文比较)
|
- 没有密码哈希存储 (后端明文比较)
|
||||||
- 可能被误解为是不恰当的设计
|
- 可能被误解为是不恰当的设计
|
||||||
- 代码中不恰当的注释, 但这些注释(html, js 和 css 中的)事实上会被 go 的模板系统清除(即使有 "dev" tag), 因此您不必担心这些注释会随页面传递给浏览器, 在"查看源代码页"呈现给用户
|
- 代码中不恰当的注释, 但这些注释(html, js 和 css 中的)事实上会被 go 的 embed 清除(即使有 "dev" tag), 因此您不必担心这些注释会随页面传递给浏览器, 在"查看源代码页"呈现给用户
|
||||||
- 注意: 这些不安全设计并非都是运维或服务器管理员能解决的, 因此请量需求而行
|
- 注意: 这些不安全设计并非都是运维或服务器管理员能解决的, 因此请量需求而行
|
||||||
|
|
||||||
## 使用方法
|
## 使用方法
|
||||||
@@ -109,7 +121,7 @@ QPS Performace from test:
|
|||||||
Server QPS Data QPS Bar (higher is better) Gzip Page
|
Server QPS Data QPS Bar (higher is better) Gzip Page
|
||||||
Nginx 60,242 QPS ████████████████████████████████ Disabled Default Index
|
Nginx 60,242 QPS ████████████████████████████████ Disabled Default Index
|
||||||
Apache 34,207 QPS █████████████████░░░░░░░░░░░░░░░ Disabled Default Index
|
Apache 34,207 QPS █████████████████░░░░░░░░░░░░░░░ Disabled Default Index
|
||||||
AFI 15,262 QPS ████████░░░░░░░░░░░░░░░░░░░░░░░░ BestSpeed Full-functional web file manager (without CSS & JS embeded in bench) (net/http)
|
AFI 15,262 QPS ████████░░░░░░░░░░░░░░░░░░░░░░░░ BestSpeed Full-functional web file manager (without CSS & JS embedded in bench) (net/http)
|
||||||
PyPy 4,169 QPS ██░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ Disabled Default Index (http.server)
|
PyPy 4,169 QPS ██░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ Disabled Default Index (http.server)
|
||||||
CPython 2,128 QPS █░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ Disabled Default Index (http.server)
|
CPython 2,128 QPS █░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ Disabled Default Index (http.server)
|
||||||
|
|
||||||
|
|||||||
88
afi.go
88
afi.go
@@ -31,6 +31,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const ver = "26.04.03"
|
||||||
|
|
||||||
// rowTemplate 定义文件列表中的每一行数据结构
|
// rowTemplate 定义文件列表中的每一行数据结构
|
||||||
type rowTemplate struct {
|
type rowTemplate struct {
|
||||||
Name string // 文件/文件夹名称
|
Name string // 文件/文件夹名称
|
||||||
@@ -53,15 +55,15 @@ type pageTemplate struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 命令行参数定义
|
// 命令行参数定义
|
||||||
var host = flag.String("h", "127.0.0.1", "监听的主机地址") // 监听的主机地址
|
var host = flag.String("h", "127.0.0.1", "监听的主机地址") // 监听的主机地址
|
||||||
var port = flag.String("p", "8001", "监听的端口") // 监听的端口
|
var port = flag.String("p", "8001", "监听的端口") // 监听的端口
|
||||||
var extraPath = flag.String("prefix", "/", "afi 的 URL 访问路径前缀, 例如 /afi/ (斜杠很重要)") // URL前缀路径
|
var extraPath = flag.String("prefix", "/", "afi 的 URL 访问路径前缀, 例如 /afi/ (斜杠很重要)") // URL前缀路径
|
||||||
var symlinks = flag.Bool("symlinks", false, "跟随符号链接 \033[4m警告\033[0m: 符号链接可能会跳出已定义的\"根目录\" (默认值: false)") // 是否跟随符号链接
|
var symlinks = flag.Bool("symlinks", false, "跟随符号链接 警告: 启用后符号链接可能会跳出已定义的\"根目录\" (default false)") // 是否跟随符号链接
|
||||||
var verb = flag.Bool("verb", false, "详细输出") // 是否输出详细日志
|
var verb = flag.Bool("verb", false, "详细输出") // 是否输出详细日志
|
||||||
var skipHidden = flag.Bool("k", true, "\n跳过隐藏文件") // 是否跳过隐藏文件(以.开头)
|
var skipHidden = flag.Bool("k", true, "\n跳过隐藏文件") // 是否跳过隐藏文件(以.开头)
|
||||||
// var ro = flag.Bool("ro", false, "只读模式(无法修改文件系统)") // 是否只读模式
|
// var ro = flag.Bool("ro", false, "只读模式(无法修改文件系统)") // 是否只读模式
|
||||||
var title = flag.String("title", "%PATH%", "页面标题, 用%PATH%指代完整路径, %ITEM%指代末端文件/目录名, 不会泄漏根目录目录名")
|
var title = flag.String("title", "%PATH%", "页面标题, 用 %PATH% 指代完整路径, %ITEM% 指代末端文件/目录名, 不会泄露根目录目录名")
|
||||||
var authstr = flag.String("auth", `{"admin": "password"}`, "可写用户的认证数据") // 如果非要安全 应该用stdin注入密码防盗窃, 这个flag只能用来调试 不然服务器一个ps就泄露了
|
var authstr = flag.String("auth", `{"admin": "password"}`, "可写用户的认证数据 (也可以用环境变量 AFI_AUTH 设置, AFI_AUTH 是最优先且最安全的)") // 这个flag只能用来调试 不然服务器一个ps就泄露了
|
||||||
|
|
||||||
// rpcCall 定义RPC调用的JSON数据结构
|
// rpcCall 定义RPC调用的JSON数据结构
|
||||||
// 用于处理前端发送的远程调用请求(创建目录、移动、删除、计算校验和)
|
// 用于处理前端发送的远程调用请求(创建目录、移动、删除、计算校验和)
|
||||||
@@ -256,7 +258,7 @@ func replyList(w http.ResponseWriter, r *http.Request, fullPath string, path str
|
|||||||
|
|
||||||
// 根据类型添加到不同的列表
|
// 根据类型添加到不同的列表
|
||||||
if el.IsDir() {
|
if el.IsDir() {
|
||||||
row := rowTemplate{name + "/", template.URL(href), "4.0kB", "folder", desc, "inode/directory"}
|
row := rowTemplate{html.EscapeString(name + "/"), template.URL(href), "4.0kB", "folder", html.EscapeString(desc), "inode/directory"}
|
||||||
p.RowsFolders = append(p.RowsFolders, row)
|
p.RowsFolders = append(p.RowsFolders, row)
|
||||||
} else {
|
} else {
|
||||||
// 提取文件扩展名
|
// 提取文件扩展名
|
||||||
@@ -270,7 +272,7 @@ func replyList(w http.ResponseWriter, r *http.Request, fullPath string, path str
|
|||||||
filemime = "unknown/unknown"
|
filemime = "unknown/unknown"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
row := rowTemplate{name, template.URL(href), humanize(el.Size()), ext, desc, filemime}
|
row := rowTemplate{html.EscapeString(name), template.URL(href), humanize(el.Size()), html.EscapeString(ext), html.EscapeString(desc), filemime}
|
||||||
p.RowsFiles = append(p.RowsFiles, row)
|
p.RowsFiles = append(p.RowsFiles, row)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -337,34 +339,34 @@ func checkAuthRequest(w http.ResponseWriter, r *http.Request) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 拒绝现代JWT, 古法auth, 我们有先进的https
|
// 拒绝现代JWT, 古法auth, 我们有先进的https
|
||||||
// 这样不能防 XSS
|
// 这样不利好防御 XSS 虽说已经把模板搞安全了
|
||||||
// 但是至于XSS, 用户都在浏览器随便运行tampermonkey了我还能说什么呢, 说不定是用户想写脚本调试呢, 我把xss预防了让他们费劲去找token反而不好
|
// 但是至于XSS, 用户都在浏览器随便运行tampermonkey了我还能说什么呢, 说不定是用户想写脚本调试呢, 我把xss预防了让他们费劲去找token反而不好
|
||||||
// 这才叫利好curl, 而且还防 CSRF
|
// 这才叫利好curl, 而且还防 CSRF
|
||||||
func authLogin(w http.ResponseWriter, r *http.Request) {
|
func authLogin(w http.ResponseWriter, r *http.Request) {
|
||||||
username, password, ok := r.BasicAuth()
|
username, password, ok := r.BasicAuth()
|
||||||
if !ok {
|
if !ok {
|
||||||
w.Header().Set("WWW-Authenticate", `Basic realm="AFI File Manager"`)
|
w.Header().Set("WWW-Authenticate", `Basic realm="AFI File Manager"`)
|
||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if checkAuth(username, password) {
|
if checkAuth(username, password) {
|
||||||
http.Redirect(w, r, *extraPath, http.StatusFound)
|
http.Redirect(w, r, *extraPath, http.StatusFound)
|
||||||
} else {
|
} else {
|
||||||
w.Header().Set("WWW-Authenticate", `Basic realm="AFI File Manager"`)
|
w.Header().Set("WWW-Authenticate", `Basic realm="AFI File Manager"`)
|
||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func authState(w http.ResponseWriter, r *http.Request) {
|
func authState(w http.ResponseWriter, r *http.Request) {
|
||||||
username, password, ok := r.BasicAuth()
|
username, password, ok := r.BasicAuth()
|
||||||
if !ok {
|
if !ok {
|
||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if checkAuth(username, password) {
|
if checkAuth(username, password) {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
} else {
|
} else {
|
||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// upload 处理文件上传请求
|
// upload 处理文件上传请求
|
||||||
@@ -540,7 +542,11 @@ func enforcePath(p string) string {
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// 解析命令行参数
|
// 解析命令行参数
|
||||||
if flag.Parse(); len(flag.Args()) == 1 {
|
flag.Parse()
|
||||||
|
if envAuth := os.Getenv("AFI_AUTH"); envAuth != "" {
|
||||||
|
*authstr = envAuth
|
||||||
|
}
|
||||||
|
if len(flag.Args()) == 1 {
|
||||||
rootPath = flag.Args()[0]
|
rootPath = flag.Args()[0]
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("Usage: ./afi [OPTION]... [ROOTDIR]\n\n")
|
fmt.Printf("Usage: ./afi [OPTION]... [ROOTDIR]\n\n")
|
||||||
@@ -558,18 +564,18 @@ func main() {
|
|||||||
server := &http.Server{Addr: *host + ":" + *port, Handler: handler}
|
server := &http.Server{Addr: *host + ":" + *port, Handler: handler}
|
||||||
|
|
||||||
// 注册路由处理器
|
// 注册路由处理器
|
||||||
http.HandleFunc(*extraPath+"rpc", rpc) // RPC接口 (增删移与校验和)
|
http.HandleFunc(*extraPath+"rpc", rpc) // RPC接口 (增删移与校验和)
|
||||||
http.HandleFunc(*extraPath+"post", upload) // 文件上传接口
|
http.HandleFunc(*extraPath+"post", upload) // 文件上传接口
|
||||||
http.HandleFunc(*extraPath+"dav", upload) // TODO: webdav 接口
|
http.HandleFunc(*extraPath+"dav", upload) // TODO: webdav 接口
|
||||||
http.HandleFunc(*extraPath+"auth", authState) // 仅检查登录接口(不会弹窗)
|
http.HandleFunc(*extraPath+"auth", authState) // 仅检查登录接口(不会弹窗)
|
||||||
http.HandleFunc(*extraPath+"login", authLogin) // 登录接口
|
http.HandleFunc(*extraPath+"login", authLogin) // 登录接口
|
||||||
http.HandleFunc(*extraPath+"zip", zipRPC) // ZIP打包下载接口
|
http.HandleFunc(*extraPath+"zip", zipRPC) // ZIP打包下载接口
|
||||||
http.HandleFunc("/", doContent) // 主内容处理接口
|
http.HandleFunc("/", doContent) // 主内容处理接口
|
||||||
// 创建静态文件服务器, 用于直接提供文件下载
|
// 创建静态文件服务器, 用于直接提供文件下载
|
||||||
handler = http.StripPrefix(*extraPath, http.FileServer(http.Dir(rootPath)))
|
handler = http.StripPrefix(*extraPath, http.FileServer(http.Dir(rootPath)))
|
||||||
|
|
||||||
// 输出启动信息
|
// 输出启动信息
|
||||||
fmt.Printf("Agile File Indexer\n")
|
fmt.Printf("Agile File Indexer 版本 %s\n", ver)
|
||||||
fmt.Printf("AFI 是 Gossa 的增强分支, 但是并不完全向下兼容原版 Gossa 的参数\n")
|
fmt.Printf("AFI 是 Gossa 的增强分支, 但是并不完全向下兼容原版 Gossa 的参数\n")
|
||||||
fmt.Printf("AFI 已启动, 根目录为 %s\n", rootPath)
|
fmt.Printf("AFI 已启动, 根目录为 %s\n", rootPath)
|
||||||
fmt.Printf("详细输出: %t, 符号链接跟随: %t, 跳过隐藏文件: %t\n", *verb, *symlinks, *skipHidden)
|
fmt.Printf("详细输出: %t, 符号链接跟随: %t, 跳过隐藏文件: %t\n", *verb, *symlinks, *skipHidden)
|
||||||
|
|||||||
39
ui/style.css
39
ui/style.css
@@ -19,6 +19,45 @@ html {
|
|||||||
scrollbar-gutter: stable;
|
scrollbar-gutter: stable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
#index-table th:nth-child(5) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#index-table td:nth-child(6) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ellipsis {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
line-clamp: 2;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktoponly {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#index-table th:last-child,
|
||||||
|
#index-table td:last-child {
|
||||||
|
text-align: right;
|
||||||
|
padding: 0.25em 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#index-table th:nth-child(2),
|
||||||
|
#index-table td:nth-child(3) {
|
||||||
|
width: 52%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#index-table th:nth-child(3),
|
||||||
|
#index-table td:nth-child(4) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
*,
|
*,
|
||||||
*::before,
|
*::before,
|
||||||
*::after {
|
*::after {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
<html>
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>{{.Title}}</title>
|
<title>{{.Title}}</title>
|
||||||
<link rel="preconnect" href="https://fonts.loli.net">
|
<link rel="preconnect" href="https://fonts.loli.net">
|
||||||
@@ -99,9 +100,9 @@
|
|||||||
</table>
|
</table>
|
||||||
<div style="display: none;" onclick="afi.utils.helpOff()" id="help-panel">
|
<div style="display: none;" onclick="afi.utils.helpOff()" id="help-panel">
|
||||||
<br>
|
<br>
|
||||||
<h3>帮助信息与按键绑定</h3>
|
<h3 class="desktoponly">帮助信息与按键绑定</h3>
|
||||||
<p>^[X] 表示组合键 Ctrl + [X] 或 Meta + [X]</p>
|
<p class="desktoponly">^[X] 表示组合键 Ctrl + [X] 或 Meta + [X]</p>
|
||||||
<table>
|
<table class="desktoponly">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>使用键盘浏览文件系统</td>
|
<td>使用键盘浏览文件系统</td>
|
||||||
@@ -145,7 +146,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<br>
|
<br class="desktoponly">
|
||||||
<h3>关于 AFI</h3>
|
<h3>关于 AFI</h3>
|
||||||
<p>AFI (敏捷文件索引器) 是 Go 编写的轻量级文件服务器.<br>
|
<p>AFI (敏捷文件索引器) 是 Go 编写的轻量级文件服务器.<br>
|
||||||
AFI 是基于 Gossa 的现代化改进与维护分支, 以 MIT 协议在<a href="">此处</a>开放源代码.<br>
|
AFI 是基于 Gossa 的现代化改进与维护分支, 以 MIT 协议在<a href="">此处</a>开放源代码.<br>
|
||||||
|
|||||||
Reference in New Issue
Block a user