初次提交并重新初始化
This commit is contained in:
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
afi
|
||||||
|
afi-linux64
|
||||||
|
afi-linux-arm
|
||||||
|
afi-linux-arm64
|
||||||
|
afi-mac
|
||||||
|
afi-windows.exe
|
||||||
|
builds
|
||||||
|
afi.test
|
||||||
|
|
||||||
|
afi-dev
|
||||||
|
afi-dev-linux64
|
||||||
|
afi-dev-linux-arm
|
||||||
|
afi-dev-linux-arm64
|
||||||
|
afi-dev-mac
|
||||||
|
afi-dev-windows.exe
|
||||||
|
afi-dev.test
|
||||||
|
|
||||||
|
*.out
|
||||||
|
.vscode
|
||||||
31
LICENSE
31
LICENSE
@@ -1,18 +1,23 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2026 pluv
|
Copyright (c) 2018 Pierre Dubouilh
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
Modifications Copyright (c) 2026 Wang Zhiyu
|
||||||
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
|
||||||
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
|
||||||
following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all copies or substantial
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
portions of the Software.
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
The above copyright notice and this permission notice shall be included in all
|
||||||
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
copies or substantial portions of the Software.
|
||||||
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
||||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|||||||
34
README.md
34
README.md
@@ -1,3 +1,31 @@
|
|||||||
# AFI
|
# AFI - gossa 的个人维护分支
|
||||||
|
|
||||||
敏捷文件索引器 支持上下行文件传输的轻量级网络文件索引软件
|
|
||||||
|
本项目 fork 自 [Gossa, 由 Pierre Dubouilh 及 Gossa Authors 开发维护](https://github.com/pldubouilh/gossa), 并重新初始化 git.
|
||||||
|
|
||||||
|
一个敏捷且超轻量的文件网页服务器, 无依赖且代码量极小, 易于审查.
|
||||||
|
|
||||||
|
默认提供简洁的UI, 包含以下功能:
|
||||||
|
|
||||||
|
* 文件/目录浏览与处理
|
||||||
|
* 拖放上传
|
||||||
|
* 轻量级网页 UI, 毫秒级响应
|
||||||
|
* 文本编辑器
|
||||||
|
* 键盘导航
|
||||||
|
* 轻量级且无依赖的代码库
|
||||||
|
* 快速的 Go 语言静态服务器
|
||||||
|
* 只读模式
|
||||||
|
* 支持 PWA
|
||||||
|
* 多平台支持
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
```sh
|
||||||
|
% ./afi --help
|
||||||
|
```
|
||||||
|
|
||||||
|
## 编译
|
||||||
|
```bash
|
||||||
|
go vet && go fmt
|
||||||
|
go build -o afi # 单文件, 包括 web 资源
|
||||||
|
go build -tags dev -o afi-dev # 使用运行目录(优先)或程序目录下的 web 资源
|
||||||
|
```
|
||||||
417
afi.go
Normal file
417
afi.go
Normal file
@@ -0,0 +1,417 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"compress/gzip"
|
||||||
|
"crypto/md5"
|
||||||
|
"crypto/sha1"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/sha512"
|
||||||
|
_ "embed"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"hash"
|
||||||
|
"html"
|
||||||
|
"html/template"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// rowTemplate 定义文件列表中的每一行数据结构
|
||||||
|
// 用于在HTML模板中渲染文件或文件夹的显示信息
|
||||||
|
type rowTemplate struct {
|
||||||
|
Name string // 文件/文件夹名称
|
||||||
|
Href template.URL // 链接地址
|
||||||
|
Size string // 文件大小(仅文件)
|
||||||
|
Ext string // 文件扩展名(仅文件)
|
||||||
|
}
|
||||||
|
|
||||||
|
// pageTemplate 定义整个页面渲染所需的数据结构
|
||||||
|
// 包含页面标题、路径前缀、读写模式标志以及文件和文件夹列表
|
||||||
|
type pageTemplate struct {
|
||||||
|
Title template.HTML // 页面标题,显示当前路径
|
||||||
|
ExtraPath template.HTML // URL前缀路径
|
||||||
|
Ro bool // 是否只读模式(不允许上传、重命名、移动等操作)
|
||||||
|
RowsFiles []rowTemplate // 文件列表
|
||||||
|
RowsFolders []rowTemplate // 文件夹列表
|
||||||
|
}
|
||||||
|
|
||||||
|
// 命令行参数定义
|
||||||
|
var host = flag.String("h", "127.0.0.1", "监听的主机地址") // 监听的主机地址
|
||||||
|
var port = flag.String("p", "8001", "监听的端口") // 监听的端口
|
||||||
|
var extraPath = flag.String("prefix", "/", "afi 的 URL 访问路径前缀, 例如 /afi/ (斜杠很重要)") // URL前缀路径
|
||||||
|
var symlinks = flag.Bool("symlinks", false, "跟随符号链接 \033[4m警告\033[0m: 符号链接可能会跳出已定义的\"根目录\" (默认值: false)") // 是否跟随符号链接
|
||||||
|
var verb = flag.Bool("verb", false, "详细输出") // 是否输出详细日志
|
||||||
|
var skipHidden = flag.Bool("k", true, "\n跳过隐藏文件") // 是否跳过隐藏文件(以.开头)
|
||||||
|
var ro = flag.Bool("ro", false, "只读模式(无法修改文件系统)") // 是否只读模式
|
||||||
|
|
||||||
|
// rpcCall 定义RPC调用的JSON数据结构
|
||||||
|
// 用于处理前端发送的远程调用请求(创建目录、移动、删除、计算校验和)
|
||||||
|
type rpcCall struct {
|
||||||
|
Call string `json:"call"` // 调用的方法名
|
||||||
|
Args []string `json:"args"` // 方法参数
|
||||||
|
}
|
||||||
|
|
||||||
|
var rootPath = "" // 共享目录的根路径(绝对路径)
|
||||||
|
var handler http.Handler // HTTP处理器,用于处理静态文件服务
|
||||||
|
|
||||||
|
// check 检查错误,如果错误不为nil则panic
|
||||||
|
// 用于简化错误处理,配合defer exitPath使用
|
||||||
|
func check(e error) {
|
||||||
|
if e != nil {
|
||||||
|
panic(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// exitPath 延迟执行的错误恢复和日志记录函数
|
||||||
|
// 用于捕获panic并返回500错误,同时记录日志
|
||||||
|
// w: HTTP响应写入器
|
||||||
|
// s: 日志信息参数
|
||||||
|
func exitPath(w http.ResponseWriter, s ...interface{}) {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
// 发生panic时记录错误并返回500状态码
|
||||||
|
log.Println("error", s, r)
|
||||||
|
w.WriteHeader(500)
|
||||||
|
w.Write([]byte("error"))
|
||||||
|
} else if *verb {
|
||||||
|
// 详细模式下记录正常日志
|
||||||
|
log.Println(s...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// humanize 将字节数转换为人类可读的格式
|
||||||
|
// 例如: 1024 -> "1.0k", 1048576 -> "1.0M"
|
||||||
|
// bytes: 文件大小(字节)
|
||||||
|
// 返回: 格式化后的字符串(如"1.5M")
|
||||||
|
func humanize(bytes int64) string {
|
||||||
|
b := float64(bytes)
|
||||||
|
u := 0
|
||||||
|
for {
|
||||||
|
if b < 1024 {
|
||||||
|
// 根据当前数量级选择合适的单位
|
||||||
|
return strconv.FormatFloat(b, 'f', 1, 64) + [9]string{"B", "k", "M", "G", "T", "P", "E", "Z", "Y"}[u]
|
||||||
|
}
|
||||||
|
b = b / 1024
|
||||||
|
u++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// replyList 生成目录列表页面并返回
|
||||||
|
// 读取指定目录下的文件和文件夹,构建页面模板数据并渲染
|
||||||
|
// w: HTTP响应写入器
|
||||||
|
// r: HTTP请求
|
||||||
|
// fullPath: 目录的绝对路径
|
||||||
|
// path: 请求的URL路径
|
||||||
|
func replyList(w http.ResponseWriter, r *http.Request, fullPath string, path string) {
|
||||||
|
// 读取目录内容
|
||||||
|
files, err := os.ReadDir(fullPath)
|
||||||
|
check(err)
|
||||||
|
// 按文件名(不区分大小写)排序
|
||||||
|
sort.Slice(files, func(i, j int) bool { return strings.ToLower(files[i].Name()) < strings.ToLower(files[j].Name()) })
|
||||||
|
|
||||||
|
// 确保路径以斜杠结尾
|
||||||
|
if !strings.HasSuffix(path, "/") {
|
||||||
|
path += "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建页面标题,显示当前路径
|
||||||
|
title := "/" + strings.TrimPrefix(path, *extraPath)
|
||||||
|
p := pageTemplate{}
|
||||||
|
// 如果不是根目录,添加返回上级目录的链接
|
||||||
|
if path != *extraPath {
|
||||||
|
p.RowsFolders = append(p.RowsFolders, rowTemplate{"../", "../", "", "folder"})
|
||||||
|
}
|
||||||
|
p.ExtraPath = template.HTML(html.EscapeString(*extraPath))
|
||||||
|
p.Ro = *ro
|
||||||
|
p.Title = template.HTML(html.EscapeString(title))
|
||||||
|
|
||||||
|
// 遍历目录中的每个条目
|
||||||
|
for _, el := range files {
|
||||||
|
info, errInfo := el.Info()
|
||||||
|
el, err := os.Stat(fullPath + "/" + el.Name())
|
||||||
|
if err != nil || errInfo != nil {
|
||||||
|
log.Println("error - cant stat a file", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳过隐藏文件(如果配置了跳过)
|
||||||
|
if *skipHidden && strings.HasPrefix(el.Name(), ".") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// 跳过符号链接(如果不允许跟随)
|
||||||
|
if !*symlinks && info.Mode()&os.ModeSymlink != 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// URL编码文件名,避免特殊字符问题
|
||||||
|
href := url.PathEscape(el.Name())
|
||||||
|
name := el.Name()
|
||||||
|
|
||||||
|
// 处理文件夹链接的斜杠问题
|
||||||
|
if el.IsDir() && strings.HasPrefix(href, "/") {
|
||||||
|
href = strings.Replace(href, "/", "", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据类型添加到不同的列表
|
||||||
|
if el.IsDir() {
|
||||||
|
row := rowTemplate{name + "/", template.URL(href), "4.0k", "folder"}
|
||||||
|
p.RowsFolders = append(p.RowsFolders, row)
|
||||||
|
} else {
|
||||||
|
// 提取文件扩展名
|
||||||
|
sl := strings.Split(name, ".")
|
||||||
|
ext := strings.ToLower(sl[len(sl)-1])
|
||||||
|
row := rowTemplate{name, template.URL(href), humanize(el.Size()), ext}
|
||||||
|
p.RowsFiles = append(p.RowsFiles, row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查客户端是否支持gzip压缩
|
||||||
|
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||||
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
w.Header().Add("Content-Encoding", "gzip")
|
||||||
|
// 使用gzip压缩响应内容,提高传输效率
|
||||||
|
gz, err := gzip.NewWriterLevel(w, gzip.BestSpeed)
|
||||||
|
check(err)
|
||||||
|
defer gz.Close()
|
||||||
|
tmpl.Execute(gz, p)
|
||||||
|
} else {
|
||||||
|
tmpl.Execute(w, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// doContent 处理所有内容请求的核心函数
|
||||||
|
// 根据请求路径判断是目录还是文件,目录则列出文件列表,文件则提供下载
|
||||||
|
// w: HTTP响应写入器
|
||||||
|
// r: HTTP请求
|
||||||
|
func doContent(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// 如果URL不以配置的前缀开头,重定向到正确的前缀
|
||||||
|
if !strings.HasPrefix(r.URL.Path, *extraPath) {
|
||||||
|
http.Redirect(w, r, *extraPath, http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解码URL中的HTML实体
|
||||||
|
path := html.UnescapeString(r.URL.Path)
|
||||||
|
defer exitPath(w, "get content", path)
|
||||||
|
// 获取安全的绝对路径
|
||||||
|
fullPath := enforcePath(path)
|
||||||
|
stat, errStat := os.Stat(fullPath)
|
||||||
|
check(errStat)
|
||||||
|
|
||||||
|
// 如果是目录,显示文件列表;如果是文件,交给静态文件服务器处理
|
||||||
|
if stat.IsDir() {
|
||||||
|
replyList(w, r, fullPath, path)
|
||||||
|
} else {
|
||||||
|
handler.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// upload 处理文件上传请求
|
||||||
|
// 从multipart请求中读取文件内容并保存到指定路径
|
||||||
|
// w: HTTP响应写入器
|
||||||
|
// r: HTTP请求
|
||||||
|
func upload(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// 从header中获取目标路径
|
||||||
|
path := r.Header.Get("afi-path")
|
||||||
|
defer exitPath(w, "upload", path)
|
||||||
|
|
||||||
|
path, err := url.PathUnescape(path)
|
||||||
|
check(err)
|
||||||
|
// 创建multipart reader解析上传的文件
|
||||||
|
reader, err := r.MultipartReader()
|
||||||
|
check(err)
|
||||||
|
part, err := reader.NextPart()
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
check(err)
|
||||||
|
}
|
||||||
|
// 创建目标文件
|
||||||
|
dst, err := os.Create(enforcePath(path))
|
||||||
|
check(err)
|
||||||
|
// 将上传内容复制到目标文件
|
||||||
|
io.Copy(dst, part)
|
||||||
|
w.Write([]byte("ok"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// zipRPC 处理ZIP打包下载请求
|
||||||
|
// 将指定目录或文件打包成ZIP并下载
|
||||||
|
// w: HTTP响应写入器
|
||||||
|
// r: HTTP请求
|
||||||
|
func zipRPC(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// 获取要打包的路径和ZIP文件名
|
||||||
|
zipPath := r.URL.Query().Get("zipPath")
|
||||||
|
zipName := r.URL.Query().Get("zipName")
|
||||||
|
defer exitPath(w, "zip", zipPath)
|
||||||
|
zipFullPath := enforcePath(zipPath)
|
||||||
|
_, err := os.Lstat(zipFullPath)
|
||||||
|
check(err)
|
||||||
|
|
||||||
|
// 设置下载响应头
|
||||||
|
w.Header().Add("Content-Disposition", "attachment; filename=\""+zipName+".zip\"")
|
||||||
|
zipWriter := zip.NewWriter(w)
|
||||||
|
defer zipWriter.Close()
|
||||||
|
|
||||||
|
// 遍历目录树,将文件添加到ZIP中
|
||||||
|
err = filepath.Walk(zipFullPath, func(path string, f fs.FileInfo, err error) error {
|
||||||
|
check(err)
|
||||||
|
// 跳过目录(目录本身不加入ZIP)
|
||||||
|
if f.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算相对路径作为ZIP内的路径
|
||||||
|
rel, err := filepath.Rel(zipFullPath, path)
|
||||||
|
check(err)
|
||||||
|
// 跳过隐藏文件
|
||||||
|
if *skipHidden && (strings.HasPrefix(rel, ".") || strings.HasPrefix(f.Name(), ".")) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// 不允许打包符号链接
|
||||||
|
if f.Mode()&os.ModeSymlink != 0 {
|
||||||
|
panic(errors.New("symlink not allowed in zip downloads"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建ZIP文件头
|
||||||
|
header, err := zip.FileInfoHeader(f)
|
||||||
|
check(err)
|
||||||
|
header.Name = filepath.ToSlash(rel) // 统一使用斜杠分隔
|
||||||
|
header.Method = zip.Store // 使用存储模式(不压缩)
|
||||||
|
headerWriter, err := zipWriter.CreateHeader(header)
|
||||||
|
check(err)
|
||||||
|
// 打开文件并复制到ZIP中
|
||||||
|
file, err := os.Open(path)
|
||||||
|
check(err)
|
||||||
|
defer file.Close()
|
||||||
|
_, err = io.Copy(headerWriter, file)
|
||||||
|
check(err)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
check(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// rpc 处理RPC请求
|
||||||
|
// 支持创建目录、移动/重命名、删除文件、计算校验和等操作
|
||||||
|
// w: HTTP响应写入器
|
||||||
|
// r: HTTP请求
|
||||||
|
func rpc(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var err error
|
||||||
|
var rpc rpcCall
|
||||||
|
defer exitPath(w, "rpc", &rpc)
|
||||||
|
|
||||||
|
// 读取并解析JSON请求体
|
||||||
|
bodyBytes, err := io.ReadAll(r.Body)
|
||||||
|
check(err)
|
||||||
|
json.Unmarshal(bodyBytes, &rpc)
|
||||||
|
ret := []byte("ok")
|
||||||
|
|
||||||
|
// 根据调用的方法执行相应操作
|
||||||
|
switch rpc.Call {
|
||||||
|
case "mkdirp":
|
||||||
|
// 创建目录(包括所有父目录)
|
||||||
|
err = os.MkdirAll(enforcePath(rpc.Args[0]), os.ModePerm)
|
||||||
|
case "mv":
|
||||||
|
// 移动或重命名文件/目录
|
||||||
|
err = os.Rename(enforcePath(rpc.Args[0]), enforcePath(rpc.Args[1]))
|
||||||
|
case "rm":
|
||||||
|
// 删除文件或目录(递归删除)
|
||||||
|
err = os.RemoveAll(enforcePath(rpc.Args[0]))
|
||||||
|
case "sum":
|
||||||
|
// 计算文件的校验和(md5, sha1, sha256, sha512)
|
||||||
|
file, err := os.Open(enforcePath(rpc.Args[0]))
|
||||||
|
check(err)
|
||||||
|
var hash hash.Hash
|
||||||
|
// 根据参数选择哈希算法
|
||||||
|
switch rpc.Args[1] {
|
||||||
|
case "md5":
|
||||||
|
hash = md5.New()
|
||||||
|
case "sha1":
|
||||||
|
hash = sha1.New()
|
||||||
|
case "sha256":
|
||||||
|
hash = sha256.New()
|
||||||
|
case "sha512":
|
||||||
|
hash = sha512.New()
|
||||||
|
}
|
||||||
|
_, err = io.Copy(hash, file)
|
||||||
|
check(err)
|
||||||
|
checksum := hash.Sum(nil)
|
||||||
|
// 将哈希值转换为十六进制字符串
|
||||||
|
ret = make([]byte, hex.EncodedLen(len(checksum)))
|
||||||
|
hex.Encode(ret, checksum)
|
||||||
|
}
|
||||||
|
|
||||||
|
check(err)
|
||||||
|
w.Write(ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
// enforcePath 确保请求的路径是安全的
|
||||||
|
// 防止路径遍历攻击,确保访问路径在共享目录内
|
||||||
|
// p: 请求的路径
|
||||||
|
// 返回: 安全的绝对路径
|
||||||
|
func enforcePath(p string) string {
|
||||||
|
// 构建完整路径并移除URL前缀
|
||||||
|
joined := filepath.Join(rootPath, strings.TrimPrefix(p, *extraPath))
|
||||||
|
fp, err := filepath.Abs(joined)
|
||||||
|
sl, _ := filepath.EvalSymlinks(fp) // 评估符号链接的实际路径
|
||||||
|
|
||||||
|
// 安全检查:
|
||||||
|
// 1. 获取绝对路径是否出错
|
||||||
|
// 2. 路径是否以rootPath开头(防止路径遍历)
|
||||||
|
// 3. 如果跳过隐藏文件,路径中是否包含隐藏目录
|
||||||
|
// 4. 如果不允许符号链接,符号链接是否指向rootPath外
|
||||||
|
if err != nil || !strings.HasPrefix(fp, rootPath) || *skipHidden && strings.Contains(p, "/.") || !*symlinks && len(sl) > 0 && !strings.HasPrefix(sl, rootPath) {
|
||||||
|
panic(errors.New("invalid path"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return fp
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// 解析命令行参数
|
||||||
|
if flag.Parse(); len(flag.Args()) == 1 {
|
||||||
|
rootPath = flag.Args()[0]
|
||||||
|
} else {
|
||||||
|
fmt.Printf("\nusage: ./afi [OPTIONS] ~/directory-to-share\n\n")
|
||||||
|
flag.PrintDefaults()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取共享目录的绝对路径
|
||||||
|
var err error
|
||||||
|
rootPath, err = filepath.Abs(rootPath)
|
||||||
|
check(err)
|
||||||
|
|
||||||
|
// 创建HTTP服务器
|
||||||
|
server := &http.Server{Addr: *host + ":" + *port, Handler: handler}
|
||||||
|
|
||||||
|
// 注册路由处理器(非只读模式下启用修改操作)
|
||||||
|
if !*ro {
|
||||||
|
http.HandleFunc(*extraPath+"rpc", rpc) // RPC接口(创建目录、移动、删除、校验和)
|
||||||
|
http.HandleFunc(*extraPath+"post", upload) // 文件上传接口
|
||||||
|
}
|
||||||
|
http.HandleFunc(*extraPath+"zip", zipRPC) // ZIP打包下载接口
|
||||||
|
http.HandleFunc("/", doContent) // 主内容处理接口
|
||||||
|
// 创建静态文件服务器,用于直接提供文件下载
|
||||||
|
handler = http.StripPrefix(*extraPath, http.FileServer(http.Dir(rootPath)))
|
||||||
|
|
||||||
|
// 输出启动信息
|
||||||
|
fmt.Printf("Agile File Indexer 是 Gossa 的分支\n")
|
||||||
|
fmt.Printf("AFI 已启动, 根目录为 %s\n", rootPath)
|
||||||
|
fmt.Printf("详细输出: %t, 符号链接跟随: %t, 只读模式: %t, 跳过隐藏文件: %t\n", *verb, *symlinks, *ro, *skipHidden)
|
||||||
|
fmt.Printf("正在监听: http://%s:%s%s\n", *host, *port, *extraPath)
|
||||||
|
|
||||||
|
// 启动HTTP服务
|
||||||
|
if err = server.ListenAndServe(); err != http.ErrServerClosed {
|
||||||
|
check(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
36
afi_embed.go
Normal file
36
afi_embed.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
//go:build !dev
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"encoding/base64"
|
||||||
|
"html/template"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed ui/script.js
|
||||||
|
var scriptJs string
|
||||||
|
|
||||||
|
//go:embed ui/style.css
|
||||||
|
var styleCss string
|
||||||
|
|
||||||
|
//go:embed ui/favicon.svg
|
||||||
|
var faviconSvg []byte
|
||||||
|
|
||||||
|
//go:embed ui/ui.html
|
||||||
|
var uiTmpl string
|
||||||
|
|
||||||
|
var tmpl *template.Template
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
t := strings.Replace(uiTmpl, "css_will_be_here", styleCss, 1)
|
||||||
|
t = strings.Replace(t, "js_will_be_here", scriptJs, 1)
|
||||||
|
t = strings.Replace(t, "favicon_will_be_here", base64.StdEncoding.EncodeToString(faviconSvg), 2)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
tmpl, err = template.New("").Parse(t)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
59
afi_embed_dev.go
Normal file
59
afi_embed_dev.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
//go:build dev
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"html/template"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var tmpl *template.Template
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// 开发模式下从当前目录读取 UI 文件
|
||||||
|
uiDir := "ui"
|
||||||
|
|
||||||
|
// 如果当前目录没有 ui,尝试从可执行文件目录查找
|
||||||
|
if _, err := os.Stat(uiDir); os.IsNotExist(err) {
|
||||||
|
execPath, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
uiDir = filepath.Join(filepath.Dir(execPath), "ui")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取 UI 文件
|
||||||
|
styleCSS, err := os.ReadFile(filepath.Join(uiDir, "style.css"))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
scriptJS, err := os.ReadFile(filepath.Join(uiDir, "script.js"))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
faviconSVG, err := os.ReadFile(filepath.Join(uiDir, "favicon.svg"))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
uiTmpl, err := os.ReadFile(filepath.Join(uiDir, "ui.html"))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组装模板
|
||||||
|
t := strings.Replace(string(uiTmpl), "css_will_be_here", string(styleCSS), 1)
|
||||||
|
t = strings.Replace(t, "js_will_be_here", string(scriptJS), 1)
|
||||||
|
t = strings.Replace(t, "favicon_will_be_here", base64.StdEncoding.EncodeToString(faviconSVG), 2)
|
||||||
|
|
||||||
|
var parseErr error
|
||||||
|
tmpl, parseErr = template.New("").Parse(t)
|
||||||
|
if parseErr != nil {
|
||||||
|
panic(parseErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user