改进
This commit is contained in:
9
DESCRIPT.ION
Normal file
9
DESCRIPT.ION
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
ui 用户界面前端
|
||||||
|
afi 生产版本释出二进制
|
||||||
|
afi-dev 开发版本释出二进制
|
||||||
|
afi.go 主模块文件
|
||||||
|
afi_embed.go 嵌入资源与模板配置
|
||||||
|
afi_embed_dev.go 热重载资源配置
|
||||||
|
go.mod Golang 模块定义
|
||||||
|
LICENSE MIT 协议文件
|
||||||
|
README.md 自述文件
|
||||||
26
README.md
26
README.md
@@ -1,31 +1,35 @@
|
|||||||
# AFI - gossa 的个人维护分支
|
# AFI - gossa 的现代化改进与维护分支
|
||||||
|
|
||||||
|
|
||||||
本项目 fork 自 [Gossa, 由 Pierre Dubouilh 及 Gossa Authors 开发维护](https://github.com/pldubouilh/gossa), 并重新初始化 git.
|
本项目 fork 自 [Gossa, 由 Pierre Dubouilh 及 Gossa Authors 开发维护](https://github.com/pldubouilh/gossa), 并重新初始化 git.
|
||||||
|
|
||||||
一个敏捷且超轻量的文件网页服务器, 无依赖且代码量极小, 易于审查.
|
一个轻量的文件网页服务器, 无依赖且代码量极小, 易于审查.
|
||||||
|
|
||||||
默认提供简洁的UI, 包含以下功能:
|
可与配置好的 rclone 与 fuse 实例搭配, 但不附带配置界面.
|
||||||
|
|
||||||
|
默认提供简洁现代的用户界面, 并包含以下功能:
|
||||||
|
|
||||||
* 文件/目录浏览与处理
|
* 文件/目录浏览与处理
|
||||||
* 拖放上传
|
* 拖放上传
|
||||||
* 轻量级网页 UI, 毫秒级响应
|
* 轻量级网页用户界面, 毫秒级响应
|
||||||
* 文本编辑器
|
|
||||||
* 键盘导航
|
* 键盘导航
|
||||||
* 轻量级且无依赖的代码库
|
* 轻量级, 且易于维护的代码库
|
||||||
* 快速的 Go 语言静态服务器
|
* 快速的 Go 语言静态服务器
|
||||||
* 只读模式
|
* 基于 DESCRIPT.ION 的文件注释支持
|
||||||
|
* 可选的只读模式
|
||||||
|
* 可选的 symlink 追踪
|
||||||
* 支持 PWA
|
* 支持 PWA
|
||||||
* 多平台支持
|
* 多平台支持
|
||||||
|
|
||||||
## 使用方法
|
## 使用方法
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
% ./afi --help
|
% ./afi --help
|
||||||
```
|
```
|
||||||
|
|
||||||
## 编译
|
## 编译
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go vet && go fmt
|
# go vet && go fmt # 可选
|
||||||
go build -o afi # 单文件, 包括 web 资源
|
go build -o afi # 单文件, 自包括 web 资源, 便于部署
|
||||||
go build -tags dev -o afi-dev # 使用运行目录(优先)或程序目录下的 web 资源
|
go build -tags dev -o afi-dev # 使用运行目录(优先)或程序目录下的 web 资源 (ui 目录)
|
||||||
```
|
```
|
||||||
171
afi.go
171
afi.go
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
|
"bufio"
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
@@ -19,6 +20,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
@@ -26,25 +28,30 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// rowTemplate 定义文件列表中的每一行数据结构
|
// rowTemplate 定义文件列表中的每一行数据结构
|
||||||
// 用于在HTML模板中渲染文件或文件夹的显示信息
|
// 用于在HTML模板中渲染项目的显示信息
|
||||||
type rowTemplate struct {
|
type rowTemplate struct {
|
||||||
Name string // 文件/文件夹名称
|
Name string // 文件/文件夹名称
|
||||||
Href template.URL // 链接地址
|
Href template.URL // 链接地址
|
||||||
Size string // 文件大小(仅文件)
|
Size string // 项目大小
|
||||||
Ext string // 文件扩展名(仅文件)
|
Ext string // 项目扩展名 (文件夹是folder)
|
||||||
|
Desc string // 项目描述
|
||||||
|
Mime string // 项目mime
|
||||||
}
|
}
|
||||||
|
|
||||||
// pageTemplate 定义整个页面渲染所需的数据结构
|
// pageTemplate 定义整个页面渲染所需的数据结构
|
||||||
// 包含页面标题、路径前缀、读写模式标志以及文件和文件夹列表
|
// 包含页面标题、路径前缀、读写模式标志以及文件和文件夹列表
|
||||||
type pageTemplate struct {
|
type pageTemplate struct {
|
||||||
Title template.HTML // 页面标题,显示当前路径
|
Title string // 页面标题
|
||||||
|
Path template.HTML // 页面路径
|
||||||
ExtraPath template.HTML // URL前缀路径
|
ExtraPath template.HTML // URL前缀路径
|
||||||
Ro bool // 是否只读模式(不允许上传、重命名、移动等操作)
|
Ro bool // 是否只读模式(不允许上传、重命名、移动等操作)
|
||||||
RowsFiles []rowTemplate // 文件列表
|
RowsFiles []rowTemplate // 文件列表
|
||||||
RowsFolders []rowTemplate // 文件夹列表
|
RowsFolders []rowTemplate // 文件夹列表
|
||||||
|
TimeStamp string //时间戳
|
||||||
}
|
}
|
||||||
|
|
||||||
// 命令行参数定义
|
// 命令行参数定义
|
||||||
@@ -55,6 +62,7 @@ var symlinks = flag.Bool("symlinks", false, "跟随符号链接 \033[4m警告\03
|
|||||||
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%指代末端文件/目录名, 不会泄漏根目录目录名")
|
||||||
|
|
||||||
// rpcCall 定义RPC调用的JSON数据结构
|
// rpcCall 定义RPC调用的JSON数据结构
|
||||||
// 用于处理前端发送的远程调用请求(创建目录、移动、删除、计算校验和)
|
// 用于处理前端发送的远程调用请求(创建目录、移动、删除、计算校验和)
|
||||||
@@ -64,10 +72,10 @@ type rpcCall struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var rootPath = "" // 共享目录的根路径(绝对路径)
|
var rootPath = "" // 共享目录的根路径(绝对路径)
|
||||||
var handler http.Handler // HTTP处理器,用于处理静态文件服务
|
var handler http.Handler // HTTP处理器, 用于处理静态文件服务
|
||||||
|
|
||||||
// check 检查错误,如果错误不为nil则panic
|
// check 检查错误, 如果错误不为nil则panic
|
||||||
// 用于简化错误处理,配合defer exitPath使用
|
// 用于简化错误处理
|
||||||
func check(e error) {
|
func check(e error) {
|
||||||
if e != nil {
|
if e != nil {
|
||||||
panic(e)
|
panic(e)
|
||||||
@@ -75,7 +83,7 @@ func check(e error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// exitPath 延迟执行的错误恢复和日志记录函数
|
// exitPath 延迟执行的错误恢复和日志记录函数
|
||||||
// 用于捕获panic并返回500错误,同时记录日志
|
// 用于捕获panic并返回500错误, 同时记录日志
|
||||||
// w: HTTP响应写入器
|
// w: HTTP响应写入器
|
||||||
// s: 日志信息参数
|
// s: 日志信息参数
|
||||||
func exitPath(w http.ResponseWriter, s ...interface{}) {
|
func exitPath(w http.ResponseWriter, s ...interface{}) {
|
||||||
@@ -100,15 +108,76 @@ func humanize(bytes int64) string {
|
|||||||
for {
|
for {
|
||||||
if b < 1024 {
|
if b < 1024 {
|
||||||
// 根据当前数量级选择合适的单位
|
// 根据当前数量级选择合适的单位
|
||||||
return strconv.FormatFloat(b, 'f', 1, 64) + [9]string{"B", "k", "M", "G", "T", "P", "E", "Z", "Y"}[u]
|
return strconv.FormatFloat(b, 'f', 1, 64) + [9]string{"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"}[u]
|
||||||
}
|
}
|
||||||
b = b / 1024
|
b = b / 1024
|
||||||
u++
|
u++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// lookupDesc 返回指定文件在 DESCRIPT.ION 中的注释内容
|
||||||
|
func lookupDesc(fullPath string) string {
|
||||||
|
// 1. 获取绝对路径并提取目录和文件名
|
||||||
|
absPath, err := filepath.Abs(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
dir := filepath.Dir(absPath)
|
||||||
|
fileName := filepath.Base(absPath)
|
||||||
|
|
||||||
|
if fileName == "DESCRIPT.ION" {
|
||||||
|
return "备注元数据"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 尝试打开该目录下的 DESCRIPT.ION (不区分大小写)
|
||||||
|
// 在 Windows 下直接打开即可,在 Linux 下可能需要遍历目录匹配文件名
|
||||||
|
descFilePath := filepath.Join(dir, "DESCRIPT.ION")
|
||||||
|
file, err := os.Open(descFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// 3. 逐行扫描文件内容
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetName, description string
|
||||||
|
|
||||||
|
// DESCRIPT.ION 格式处理:
|
||||||
|
// 如果文件名包含空格,通常会被双引号包裹,例如: "My Photo.jpg" This is a photo
|
||||||
|
if strings.HasPrefix(line, "\"") {
|
||||||
|
endQuoteIndex := strings.Index(line[1:], "\"")
|
||||||
|
if endQuoteIndex != -1 {
|
||||||
|
targetName = line[1 : endQuoteIndex+1]
|
||||||
|
description = strings.TrimSpace(line[endQuoteIndex+2:])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 普通格式:文件名 描述 (空格分隔)
|
||||||
|
parts := strings.SplitN(line, " ", 2)
|
||||||
|
targetName = parts[0]
|
||||||
|
if len(parts) > 1 {
|
||||||
|
description = strings.TrimSpace(parts[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 匹配文件名(不区分大小写,符合该标准的一贯做法)
|
||||||
|
if strings.EqualFold(targetName, fileName) {
|
||||||
|
// 部分实现会在描述末尾包含控制字符 \x04,需要清理
|
||||||
|
description = strings.Split(description, "\x04")[0]
|
||||||
|
return strings.TrimSpace(description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// replyList 生成目录列表页面并返回
|
// replyList 生成目录列表页面并返回
|
||||||
// 读取指定目录下的文件和文件夹,构建页面模板数据并渲染
|
// 读取指定目录下的文件和文件夹, 构建页面模板数据并渲染
|
||||||
// w: HTTP响应写入器
|
// w: HTTP响应写入器
|
||||||
// r: HTTP请求
|
// r: HTTP请求
|
||||||
// fullPath: 目录的绝对路径
|
// fullPath: 目录的绝对路径
|
||||||
@@ -117,25 +186,47 @@ func replyList(w http.ResponseWriter, r *http.Request, fullPath string, path str
|
|||||||
// 读取目录内容
|
// 读取目录内容
|
||||||
files, err := os.ReadDir(fullPath)
|
files, err := os.ReadDir(fullPath)
|
||||||
check(err)
|
check(err)
|
||||||
// 按文件名(不区分大小写)排序
|
|
||||||
sort.Slice(files, func(i, j int) bool { return strings.ToLower(files[i].Name()) < strings.ToLower(files[j].Name()) })
|
|
||||||
|
|
||||||
|
// 按文件名(不区分大小写)排序
|
||||||
|
sort.Slice(files, func(i, j int) bool {
|
||||||
|
nameI := files[i].Name()
|
||||||
|
nameJ := files[j].Name()
|
||||||
|
|
||||||
|
// 1. 特殊逻辑:处理 DESCRIPT.ION
|
||||||
|
// 如果 i 是目标文件,且 j 不是,则 i 应该在后 (返回 false)
|
||||||
|
if strings.EqualFold(nameI, "DESCRIPT.ION") && !strings.EqualFold(nameJ, "DESCRIPT.ION") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// 如果 j 是目标文件,且 i 不是,则 i 应该在前 (返回 true)
|
||||||
|
if strings.EqualFold(nameJ, "DESCRIPT.ION") && !strings.EqualFold(nameI, "DESCRIPT.ION") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 普通逻辑:不区分大小写的字母排序
|
||||||
|
return strings.ToLower(nameI) < strings.ToLower(nameJ)
|
||||||
|
})
|
||||||
// 确保路径以斜杠结尾
|
// 确保路径以斜杠结尾
|
||||||
if !strings.HasSuffix(path, "/") {
|
if !strings.HasSuffix(path, "/") {
|
||||||
path += "/"
|
path += "/"
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建页面标题,显示当前路径
|
// 构建当前路径
|
||||||
title := "/" + strings.TrimPrefix(path, *extraPath)
|
pathtext := "/" + strings.TrimPrefix(path, *extraPath) // 删除extraPath前缀
|
||||||
p := pageTemplate{}
|
p := pageTemplate{}
|
||||||
// 如果不是根目录,添加返回上级目录的链接
|
now := time.Now().UTC()
|
||||||
|
p.TimeStamp = now.Format(time.RFC3339)
|
||||||
|
// 如果不是根目录, 添加返回上级目录的链接
|
||||||
if path != *extraPath {
|
if path != *extraPath {
|
||||||
p.RowsFolders = append(p.RowsFolders, rowTemplate{"../", "../", "", "folder"})
|
p.RowsFolders = append(p.RowsFolders, rowTemplate{"../", "..", "4.0kB", "folder", "返回上级目录", "inode/directory"})
|
||||||
}
|
}
|
||||||
p.ExtraPath = template.HTML(html.EscapeString(*extraPath))
|
p.ExtraPath = template.HTML(html.EscapeString(*extraPath))
|
||||||
p.Ro = *ro
|
p.Ro = *ro
|
||||||
p.Title = template.HTML(html.EscapeString(title))
|
p.Path = template.HTML(html.EscapeString(pathtext)) // 安全化路径
|
||||||
|
if pathtext == "/" {
|
||||||
|
p.Title = strings.Replace(strings.Replace(*title, "%PATH%", pathtext, -1), "%ITEM%", "/", -1)
|
||||||
|
} else {
|
||||||
|
p.Title = strings.Replace(strings.Replace(*title, "%PATH%", pathtext, -1), "%ITEM%", filepath.Base(fullPath), -1)
|
||||||
|
}
|
||||||
// 遍历目录中的每个条目
|
// 遍历目录中的每个条目
|
||||||
for _, el := range files {
|
for _, el := range files {
|
||||||
info, errInfo := el.Info()
|
info, errInfo := el.Info()
|
||||||
@@ -154,7 +245,7 @@ func replyList(w http.ResponseWriter, r *http.Request, fullPath string, path str
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// URL编码文件名,避免特殊字符问题
|
// URL编码文件名, 避免特殊字符问题
|
||||||
href := url.PathEscape(el.Name())
|
href := url.PathEscape(el.Name())
|
||||||
name := el.Name()
|
name := el.Name()
|
||||||
|
|
||||||
@@ -163,15 +254,25 @@ func replyList(w http.ResponseWriter, r *http.Request, fullPath string, path str
|
|||||||
href = strings.Replace(href, "/", "", 1)
|
href = strings.Replace(href, "/", "", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var desc string = lookupDesc(fullPath + "/" + el.Name())
|
||||||
|
|
||||||
// 根据类型添加到不同的列表
|
// 根据类型添加到不同的列表
|
||||||
if el.IsDir() {
|
if el.IsDir() {
|
||||||
row := rowTemplate{name + "/", template.URL(href), "4.0k", "folder"}
|
row := rowTemplate{name + "/", template.URL(href), "4.0kB", "folder", desc, "inode/directory"}
|
||||||
p.RowsFolders = append(p.RowsFolders, row)
|
p.RowsFolders = append(p.RowsFolders, row)
|
||||||
} else {
|
} else {
|
||||||
// 提取文件扩展名
|
// 提取文件扩展名
|
||||||
sl := strings.Split(name, ".")
|
ext := strings.TrimPrefix(strings.ToLower(filepath.Ext(name)), ".")
|
||||||
ext := strings.ToLower(sl[len(sl)-1])
|
filemime := mime.TypeByExtension("." + ext)
|
||||||
row := rowTemplate{name, template.URL(href), humanize(el.Size()), ext}
|
filemime, _, _ = mime.ParseMediaType(filemime) // 避免mime瞎猜文档编码
|
||||||
|
if filemime == "" {
|
||||||
|
if name == "DESCRIPT.ION" {
|
||||||
|
filemime = "text/plain"
|
||||||
|
} else {
|
||||||
|
filemime = "unknown/unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
row := rowTemplate{name, template.URL(href), humanize(el.Size()), ext, desc, filemime}
|
||||||
p.RowsFiles = append(p.RowsFiles, row)
|
p.RowsFiles = append(p.RowsFiles, row)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,7 +281,7 @@ func replyList(w http.ResponseWriter, r *http.Request, fullPath string, path str
|
|||||||
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||||
w.Header().Set("Content-Type", "text/html")
|
w.Header().Set("Content-Type", "text/html")
|
||||||
w.Header().Add("Content-Encoding", "gzip")
|
w.Header().Add("Content-Encoding", "gzip")
|
||||||
// 使用gzip压缩响应内容,提高传输效率
|
// 使用gzip压缩响应内容, 提高传输效率
|
||||||
gz, err := gzip.NewWriterLevel(w, gzip.BestSpeed)
|
gz, err := gzip.NewWriterLevel(w, gzip.BestSpeed)
|
||||||
check(err)
|
check(err)
|
||||||
defer gz.Close()
|
defer gz.Close()
|
||||||
@@ -191,11 +292,14 @@ func replyList(w http.ResponseWriter, r *http.Request, fullPath string, path str
|
|||||||
}
|
}
|
||||||
|
|
||||||
// doContent 处理所有内容请求的核心函数
|
// doContent 处理所有内容请求的核心函数
|
||||||
// 根据请求路径判断是目录还是文件,目录则列出文件列表,文件则提供下载
|
// 根据请求路径判断是目录还是文件, 目录则列出文件列表, 文件则提供下载
|
||||||
// w: HTTP响应写入器
|
// w: HTTP响应写入器
|
||||||
// r: HTTP请求
|
// r: HTTP请求
|
||||||
func doContent(w http.ResponseWriter, r *http.Request) {
|
func doContent(w http.ResponseWriter, r *http.Request) {
|
||||||
// 如果URL不以配置的前缀开头,重定向到正确的前缀
|
query := r.URL.Query()
|
||||||
|
name := query.Get("header")
|
||||||
|
name := query.Get("footer")
|
||||||
|
// 如果URL不以配置的前缀开头, 重定向到正确的前缀
|
||||||
if !strings.HasPrefix(r.URL.Path, *extraPath) {
|
if !strings.HasPrefix(r.URL.Path, *extraPath) {
|
||||||
http.Redirect(w, r, *extraPath, http.StatusFound)
|
http.Redirect(w, r, *extraPath, http.StatusFound)
|
||||||
return
|
return
|
||||||
@@ -209,7 +313,7 @@ func doContent(w http.ResponseWriter, r *http.Request) {
|
|||||||
stat, errStat := os.Stat(fullPath)
|
stat, errStat := os.Stat(fullPath)
|
||||||
check(errStat)
|
check(errStat)
|
||||||
|
|
||||||
// 如果是目录,显示文件列表;如果是文件,交给静态文件服务器处理
|
// 如果是目录, 显示文件列表;如果是文件, 交给静态文件服务器处理
|
||||||
if stat.IsDir() {
|
if stat.IsDir() {
|
||||||
replyList(w, r, fullPath, path)
|
replyList(w, r, fullPath, path)
|
||||||
} else {
|
} else {
|
||||||
@@ -261,7 +365,7 @@ func zipRPC(w http.ResponseWriter, r *http.Request) {
|
|||||||
zipWriter := zip.NewWriter(w)
|
zipWriter := zip.NewWriter(w)
|
||||||
defer zipWriter.Close()
|
defer zipWriter.Close()
|
||||||
|
|
||||||
// 遍历目录树,将文件添加到ZIP中
|
// 遍历目录树, 将文件添加到ZIP中
|
||||||
err = filepath.Walk(zipFullPath, func(path string, f fs.FileInfo, err error) error {
|
err = filepath.Walk(zipFullPath, func(path string, f fs.FileInfo, err error) error {
|
||||||
check(err)
|
check(err)
|
||||||
// 跳过目录(目录本身不加入ZIP)
|
// 跳过目录(目录本身不加入ZIP)
|
||||||
@@ -317,6 +421,9 @@ func rpc(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// 根据调用的方法执行相应操作
|
// 根据调用的方法执行相应操作
|
||||||
switch rpc.Call {
|
switch rpc.Call {
|
||||||
|
case "touch":
|
||||||
|
// 创建目录(包括所有父目录)
|
||||||
|
_, err = os.OpenFile(enforcePath(rpc.Args[0]), os.O_RDONLY|os.O_CREATE, 0777)
|
||||||
case "mkdirp":
|
case "mkdirp":
|
||||||
// 创建目录(包括所有父目录)
|
// 创建目录(包括所有父目录)
|
||||||
err = os.MkdirAll(enforcePath(rpc.Args[0]), os.ModePerm)
|
err = os.MkdirAll(enforcePath(rpc.Args[0]), os.ModePerm)
|
||||||
@@ -355,7 +462,7 @@ func rpc(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// enforcePath 确保请求的路径是安全的
|
// enforcePath 确保请求的路径是安全的
|
||||||
// 防止路径遍历攻击,确保访问路径在共享目录内
|
// 防止路径遍历攻击, 确保访问路径在共享目录内
|
||||||
// p: 请求的路径
|
// p: 请求的路径
|
||||||
// 返回: 安全的绝对路径
|
// 返回: 安全的绝对路径
|
||||||
func enforcePath(p string) string {
|
func enforcePath(p string) string {
|
||||||
@@ -367,8 +474,8 @@ func enforcePath(p string) string {
|
|||||||
// 安全检查:
|
// 安全检查:
|
||||||
// 1. 获取绝对路径是否出错
|
// 1. 获取绝对路径是否出错
|
||||||
// 2. 路径是否以rootPath开头(防止路径遍历)
|
// 2. 路径是否以rootPath开头(防止路径遍历)
|
||||||
// 3. 如果跳过隐藏文件,路径中是否包含隐藏目录
|
// 3. 如果跳过隐藏文件, 路径中是否包含隐藏目录
|
||||||
// 4. 如果不允许符号链接,符号链接是否指向rootPath外
|
// 4. 如果不允许符号链接, 符号链接是否指向rootPath外
|
||||||
if err != nil || !strings.HasPrefix(fp, rootPath) || *skipHidden && strings.Contains(p, "/.") || !*symlinks && len(sl) > 0 && !strings.HasPrefix(sl, 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"))
|
panic(errors.New("invalid path"))
|
||||||
}
|
}
|
||||||
@@ -401,7 +508,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
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)))
|
||||||
|
|
||||||
// 输出启动信息
|
// 输出启动信息
|
||||||
|
|||||||
4
ui/DESCRIPT.ION
Normal file
4
ui/DESCRIPT.ION
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
favicon.svg 图标文件
|
||||||
|
script.js 前端脚本
|
||||||
|
style.css 样式表
|
||||||
|
ui.html 前端模板
|
||||||
1
ui/favicon.svg
Normal file
1
ui/favicon.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1774691828293" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="19252" data-spm-anchor-id="a313x.collections_detail.0.i3.40373a81qeY5sg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M811.4 418.7C765.6 297.9 648.9 212 512.2 212S258.8 297.8 213 418.6C127.3 441.1 64 519.1 64 612c0 110.5 89.5 200 199.9 200h496.2C870.5 812 960 722.5 960 612c0-92.7-63.1-170.7-148.6-193.3z m36.3 281c-23.4 23.4-54.5 36.3-87.6 36.3H263.9c-33.1 0-64.2-12.9-87.6-36.3-23.4-23.4-36.3-54.6-36.3-87.7 0-28 9.1-54.3 26.2-76.3 16.7-21.3 40.2-36.8 66.1-43.7l37.9-9.9 13.9-36.6c8.6-22.8 20.6-44.1 35.7-63.4 14.9-19.2 32.6-35.9 52.4-49.9 41.1-28.9 89.5-44.2 140-44.2s98.9 15.3 140 44.2c19.9 14 37.5 30.8 52.4 49.9 15.1 19.3 27.1 40.7 35.7 63.4l13.8 36.5 37.8 10c54.3 14.5 92.1 63.8 92.1 120 0 33.1-12.9 64.3-36.3 87.7z" p-id="19253" fill="#1659a3"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1023 B |
469
ui/script.js
Normal file
469
ui/script.js
Normal file
@@ -0,0 +1,469 @@
|
|||||||
|
afi = {
|
||||||
|
consts: {},
|
||||||
|
params: {},
|
||||||
|
elements: {},
|
||||||
|
utils: {},
|
||||||
|
rpc: {},
|
||||||
|
uploader: {},
|
||||||
|
interface: {},
|
||||||
|
directcall: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
afi.consts = {
|
||||||
|
leftPageWarn: "确认放弃正在进行的传输?",
|
||||||
|
ensureDrop: () => !confirm('确认移动此对象?')
|
||||||
|
}
|
||||||
|
|
||||||
|
afi.params = {
|
||||||
|
readonly: readonly,
|
||||||
|
extraPath: extraPath
|
||||||
|
}
|
||||||
|
|
||||||
|
afi.elements = {
|
||||||
|
itemlinks: Array.from(document.querySelectorAll('a.item-links')),
|
||||||
|
table: document.getElementById("index-table"),
|
||||||
|
badge: document.getElementById("badge"),
|
||||||
|
uploader: document.getElementById("clickupload"),
|
||||||
|
path: document.getElementById("path"),
|
||||||
|
nav: document.getElementById("nav"),
|
||||||
|
title: document.head.querySelector("title"),
|
||||||
|
dropGrid: document.getElementById("drop-grid"),
|
||||||
|
barDisplay: document.getElementById("bar-display"),
|
||||||
|
barProc: document.getElementById("bar-proc"),
|
||||||
|
helpPanel: document.getElementById('help-panel'),
|
||||||
|
helpToggle: document.getElementById('help-toggle'),
|
||||||
|
}
|
||||||
|
|
||||||
|
afi.pulsar = {
|
||||||
|
pulseLeft: 0,
|
||||||
|
pulseNotificator(text) {
|
||||||
|
afi.elements.badge.innerText = text
|
||||||
|
this.pulseEffect(afi.elements.badge)
|
||||||
|
},
|
||||||
|
pulseSuccess() {
|
||||||
|
this.pulseNotificator("操作成功")
|
||||||
|
},
|
||||||
|
pulseFailure(error) {
|
||||||
|
this.pulseNotificator("操作失败 " + error)
|
||||||
|
},
|
||||||
|
pulseEffect: function (element) {
|
||||||
|
element.style.display = "block"
|
||||||
|
afi.pulsar.pulseLeft += 1
|
||||||
|
element.classList.remove('pulse');
|
||||||
|
void element.offsetWidth;
|
||||||
|
element.classList.add('pulse');
|
||||||
|
setTimeout(function () {
|
||||||
|
afi.pulsar.pulseLeft -= 1;
|
||||||
|
if (afi.pulsar.pulseLeft > 0) {
|
||||||
|
console.log("[AFI] Notification stack", afi.pulsar.pulseLeft, "left, skip clear")
|
||||||
|
} else {
|
||||||
|
console.log("[AFI] Notification stack", afi.pulsar.pulseLeft, "left, do clear")
|
||||||
|
element.style.display = 'none';
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
afi.utils = {
|
||||||
|
isDupe: function (itemname) {
|
||||||
|
if (afi.elements.itemlinks.find(a => a.innerText.replace('/', '') === itemname)) {
|
||||||
|
alert('名称 ' + itemname + ' 已被已存在的项目占用');
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
isFolder: function (element) {
|
||||||
|
return element && element.href && element.innerText.endsWith('/');
|
||||||
|
},
|
||||||
|
|
||||||
|
encodeURIHash: function (e) {
|
||||||
|
return encodeURI(e).replaceAll('#', '%23');
|
||||||
|
},
|
||||||
|
|
||||||
|
refresh: function () {
|
||||||
|
afi.interface.browseTo(location.href, true);
|
||||||
|
},
|
||||||
|
|
||||||
|
prependPath: function (a) {
|
||||||
|
return a.startsWith('/') ? a : decodeURI(location.pathname) + a;
|
||||||
|
},
|
||||||
|
cancelDefault(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
},
|
||||||
|
setHighlight: function (t) {
|
||||||
|
t.classList.add('highlight')
|
||||||
|
},
|
||||||
|
getHighlight: function () {
|
||||||
|
return document.querySelector('.highlight') || {}
|
||||||
|
},
|
||||||
|
resetHighlight: function () {
|
||||||
|
try {
|
||||||
|
this.getHighlight().classList.remove('highlight')
|
||||||
|
} catch (e) {
|
||||||
|
/* */
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isHelpMode: () => afi.elements.helpPanel.style.display === 'block',
|
||||||
|
helpOn: function () {
|
||||||
|
afi.elements.helpPanel.style.display = 'block'
|
||||||
|
afi.elements.table.style.display = 'none'
|
||||||
|
afi.elements.helpToggle.innerText = "关闭帮助"
|
||||||
|
afi.elements.helpToggle.onclick = afi.utils.helpOff
|
||||||
|
},
|
||||||
|
helpOff: function () {
|
||||||
|
if (!afi.utils.isHelpMode()) return
|
||||||
|
afi.elements.helpPanel.style.display = 'none'
|
||||||
|
afi.elements.table.style.display = 'table'
|
||||||
|
afi.elements.helpToggle.innerText = "帮助菜单"
|
||||||
|
afi.elements.helpToggle.onclick = afi.utils.helpOn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
afi.rpc = {
|
||||||
|
rpc: function (call, args, cb) {
|
||||||
|
console.log('[AFI] RPC', call, args)
|
||||||
|
const xhr = new XMLHttpRequest()
|
||||||
|
xhr.open('POST', location.origin + afi.params.extraPath + '/rpc')
|
||||||
|
xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8')
|
||||||
|
xhr.send(JSON.stringify({ call, args }))
|
||||||
|
xhr.onload = cb
|
||||||
|
xhr.onerror = () => afi.pulsar.pulseFailure()
|
||||||
|
},
|
||||||
|
|
||||||
|
mkdirCall: function (path, cb) {
|
||||||
|
this.rpc('mkdirp', [afi.utils.prependPath(path)], cb);
|
||||||
|
},
|
||||||
|
|
||||||
|
rmCall: function (path1, cb) {
|
||||||
|
this.rpc('rm', [afi.utils.prependPath(path1)], cb);
|
||||||
|
},
|
||||||
|
|
||||||
|
mvCall: function (path1, path2, cb) {
|
||||||
|
this.rpc('mv', [path1, path2], cb);
|
||||||
|
},
|
||||||
|
|
||||||
|
sumCall: function (path, type, cb) {
|
||||||
|
this.rpc('sum', [afi.utils.prependPath(path), type], cb);
|
||||||
|
},
|
||||||
|
|
||||||
|
touchCall: function (path, cb) {
|
||||||
|
this.rpc('touch', [afi.utils.prependPath(path)], cb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
afi.uploader = {
|
||||||
|
totalUploads: 0,
|
||||||
|
totalUploaded: 0,
|
||||||
|
totalUploadsSize: 0,
|
||||||
|
totalUploadedSize: [],
|
||||||
|
|
||||||
|
shouldRefresh: function () {
|
||||||
|
this.totalUploaded += 1
|
||||||
|
console.log('[AFI] Have uploaded ' + this.totalUploaded + ' files')
|
||||||
|
if (this.totalUploads === this.totalUploaded) {
|
||||||
|
window.onbeforeunload = null
|
||||||
|
console.log('[AFI] Finished after uploaded ' + this.totalUploaded + ' files')
|
||||||
|
this.totalUploaded = 0
|
||||||
|
this.totalUploads = 0
|
||||||
|
this.totalUploadsSize = 0
|
||||||
|
this.totalUploadedSize = []
|
||||||
|
setTimeout(afi.utils.refresh, 200)
|
||||||
|
afi.elements.barDisplay.style.display = 'none'
|
||||||
|
//afi.elements.barProc.innerText = "传输: 就绪"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updatePercent: function (event) {
|
||||||
|
this.totalUploadedSize[event.target.id] = event.loaded
|
||||||
|
const ttlDone = this.totalUploadedSize.reduce((s, x) => s + x)
|
||||||
|
const percent = Math.min(Math.floor(100 * ttlDone / this.totalUploadsSize), 100) + '%' // 此处消除对小文件元数据上传带来的误差, 理论上是不准的
|
||||||
|
afi.elements.barProc.innerText = "传输: " + percent
|
||||||
|
},
|
||||||
|
|
||||||
|
upload: function (id, what, path, cbDone, cbErr, cbUpdate) {
|
||||||
|
const xhr = new XMLHttpRequest()
|
||||||
|
xhr.open('POST', location.origin + afi.params.extraPath + '/post')
|
||||||
|
xhr.setRequestHeader('afi-path', path)
|
||||||
|
xhr.upload.addEventListener('load', cbDone)
|
||||||
|
xhr.upload.addEventListener('progress', cbUpdate)
|
||||||
|
xhr.upload.addEventListener('error', cbErr)
|
||||||
|
xhr.upload.id = id
|
||||||
|
xhr.send(what)
|
||||||
|
},
|
||||||
|
|
||||||
|
uploadFile: function (file, path) {
|
||||||
|
if (afi.params.readonly) return
|
||||||
|
path = decodeURI(location.pathname).slice(0, -1) + path
|
||||||
|
window.onbeforeunload = afi.consts.leftPageWarn
|
||||||
|
afi.elements.barProc.style.display = afi.elements.barDisplay.style.display = 'block'
|
||||||
|
this.totalUploads += 1
|
||||||
|
this.totalUploadsSize += file.size
|
||||||
|
//this.totalUploadedSize[this.totalUploads] = file.size
|
||||||
|
if (typeof upBarName !== 'undefined') {
|
||||||
|
upBarName.innerText = this.totalUploads > 1 ? this.totalUploads + ' 个文件' : file.name
|
||||||
|
}
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append(file.name, file)
|
||||||
|
this.upload(this.totalUploads, formData, encodeURIComponent(path), this.shouldRefresh.bind(this), null, this.updatePercent.bind(this))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
afi.interface = {
|
||||||
|
pushEntry: function (entry) {
|
||||||
|
if (!entry.webkitGetAsEntry && !entry.getAsEntry) {
|
||||||
|
return alert('不兼容此浏览器')
|
||||||
|
} else {
|
||||||
|
entry = entry.webkitGetAsEntry() || entry.getAsEntry()
|
||||||
|
}
|
||||||
|
this.parseDomItem.bind(this)(entry, true)
|
||||||
|
},
|
||||||
|
parseDomFolder: function (f) {
|
||||||
|
f.createReader().readEntries(e => e.forEach(i => afi.interface.parseDomItem.bind(this)(i)))
|
||||||
|
},
|
||||||
|
parseDomItem: function (domFile, shouldCheckDupes) {
|
||||||
|
if (shouldCheckDupes && afi.utils.isDupe(domFile.name)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (domFile.isFile) {
|
||||||
|
domFile.file(f => afi.uploader.uploadFile(f, domFile.fullPath))
|
||||||
|
} else {
|
||||||
|
const f = domFile.fullPath.startsWith('/') ? domFile.fullPath.slice(1) : domFile.fullPath
|
||||||
|
afi.rpc.mkdirCall(f, () => this.parseDomFolder.bind(this)(domFile))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
browseTo: async function (href, pulseEffectDone, skipHistory) {
|
||||||
|
try {
|
||||||
|
const r = await fetch(href, { credentials: 'include' })
|
||||||
|
const t = await r.text()
|
||||||
|
const parsed = new DOMParser().parseFromString(t, 'text/html')
|
||||||
|
|
||||||
|
afi.elements.table.innerHTML = parsed.getElementById('index-table').innerHTML
|
||||||
|
const nav = parsed.getElementById('nav').innerText
|
||||||
|
const path = parsed.getElementById('path').innerText
|
||||||
|
const title = parsed.head.querySelector('title').innerText
|
||||||
|
|
||||||
|
if (afi.elements.path.innerText !== path) {
|
||||||
|
if (!skipHistory) {
|
||||||
|
const escaped = afi.utils.encodeURIHash(afi.params.extraPath + path)
|
||||||
|
history.pushState({}, '', escaped)
|
||||||
|
}
|
||||||
|
afi.elements.title.innerText = title
|
||||||
|
afi.elements.nav.innerText = nav
|
||||||
|
afi.elements.path.innerText = path
|
||||||
|
this.setBreadcrumbs()
|
||||||
|
}
|
||||||
|
init()
|
||||||
|
if (pulseEffectDone) afi.pulsar.pulseSuccess()
|
||||||
|
} catch (error) {
|
||||||
|
afi.pulsar.pulseFailure(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setBreadcrumbs: function () {
|
||||||
|
const parent = afi.elements.nav.parentNode;
|
||||||
|
afi.elements.nav.outerHTML = '<span id="nav" onclick="return afi.directcall.bread_click(event)"><span class="nav">' + afi.elements.path.innerText.split('/').join('/</span><span class="nav">') + '</span></span>'
|
||||||
|
afi.elements.nav = parent.querySelector('#nav');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
afi.directcall = {
|
||||||
|
exec_mkdir: function () {
|
||||||
|
const folder = prompt('新建目录名称', '')
|
||||||
|
if (folder && !afi.utils.isDupe(folder)) {
|
||||||
|
afi.rpc.mkdirCall(folder, afi.utils.refresh)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
exec_touch: function () {
|
||||||
|
const file = prompt('新建文件名称', '')
|
||||||
|
if (file && !afi.utils.isDupe(file)) {
|
||||||
|
afi.rpc.touchCall(file, afi.utils.refresh)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
exec_rm: function () {
|
||||||
|
const file = prompt('删除对象名称', '')
|
||||||
|
afi.rpc.rmCall(file, afi.utils.refresh)
|
||||||
|
},
|
||||||
|
|
||||||
|
exec_mv: function () {
|
||||||
|
const orig = prompt('源地址', '')
|
||||||
|
const dest = prompt('目标地址', '')
|
||||||
|
if (orig && !afi.utils.isDupe(dest)) {
|
||||||
|
afi.rpc.mvCall(afi.utils.prependPath(orig), afi.utils.prependPath(dest), afi.utils.refresh)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bread_click: function (e) {
|
||||||
|
const p = Array.from(document.getElementById('nav').childNodes).map(k => k.innerText)
|
||||||
|
const i = p.findIndex(s => s === e.target.innerText)
|
||||||
|
var dst = p.slice(0, i + 1).join('').slice(1)
|
||||||
|
if (!dst.startsWith('/')) {
|
||||||
|
dst = "/" + dst
|
||||||
|
}
|
||||||
|
const target = location.origin + afi.params.extraPath + afi.utils.encodeURIHash(dst)
|
||||||
|
afi.interface.browseTo(target, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
afi.checksum = {
|
||||||
|
getSum(type) {
|
||||||
|
upBarPc.style.display = 'block'
|
||||||
|
upBarPc.innerText = '远程求解校验和...'
|
||||||
|
upBarPc.style.width = '100%'
|
||||||
|
sumsOff()
|
||||||
|
sumCall(getASelected().innerText, type, loaded => {
|
||||||
|
navigator.clipboard.writeText(loaded.target.responseText) // 复制到剪贴板
|
||||||
|
upBarPc.style.display = 'none'
|
||||||
|
flicker(okBadge)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
isSumsMode: () => sums.style.display === 'block',
|
||||||
|
sumsToggle: () => isSumsMode() ? sumsOff() : sumsOn(),
|
||||||
|
|
||||||
|
sumsOn() {
|
||||||
|
if (isFolder(getASelected())) {
|
||||||
|
alert('无法对目录求解校验和')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sums.style.display = 'block'
|
||||||
|
table.style.display = 'none'
|
||||||
|
},
|
||||||
|
sumsOff() {
|
||||||
|
if (!isSumsMode()) return
|
||||||
|
sums.style.display = 'none'
|
||||||
|
table.style.display = 'table'
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
afi.elements = {
|
||||||
|
itemlinks: Array.from(document.querySelectorAll('a.item-links')),
|
||||||
|
table: document.getElementById("index-table"),
|
||||||
|
badge: document.getElementById("badge"),
|
||||||
|
path: document.getElementById("path"),
|
||||||
|
uploader: document.getElementById("clickupload"),
|
||||||
|
nav: document.getElementById("nav"),
|
||||||
|
title: document.head.querySelector("title"),
|
||||||
|
dropGrid: document.getElementById("drop-grid"),
|
||||||
|
barDisplay: document.getElementById("bar-display"),
|
||||||
|
barProc: document.getElementById("bar-proc"),
|
||||||
|
helpPanel: document.getElementById('help-panel'),
|
||||||
|
helpToggle: document.getElementById('help-toggle'),
|
||||||
|
}
|
||||||
|
afi.interface.setBreadcrumbs()
|
||||||
|
afi.elements.uploader.addEventListener('change', () => {
|
||||||
|
const files = Array.from(afi.elements.uploader.files);
|
||||||
|
|
||||||
|
files.forEach(file => {
|
||||||
|
if (!afi.utils.isDupe(file.name)) {
|
||||||
|
afi.uploader.uploadFile(file, '/' + file.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, false);
|
||||||
|
afi.elements.dropGrid.ondragend = afi.elements.dropGrid.ondragexit = afi.elements.dropGrid.ondragleave = e => {
|
||||||
|
afi.utils.cancelDefault(e)
|
||||||
|
afi.elements.dropGrid.style.display = 'none'
|
||||||
|
}
|
||||||
|
let draggingSrc
|
||||||
|
document.ondragenter = e => {
|
||||||
|
if (afi.params.readonly) { return }
|
||||||
|
afi.utils.cancelDefault(e)
|
||||||
|
afi.utils.resetHighlight()
|
||||||
|
if (!draggingSrc) {
|
||||||
|
afi.elements.dropGrid.style.display = 'flex'
|
||||||
|
e.dataTransfer.dropEffect = 'copy'
|
||||||
|
} else if (draggingSrc) {
|
||||||
|
const t = getClosestRow(e.target)
|
||||||
|
afi.utils.isFolder(t.firstChild) && setHighlight(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
afi.elements.itemlinks.forEach(link => {
|
||||||
|
const row = link.parentElement.parentElement.parentElement.querySelector('.item-icon');
|
||||||
|
if (!row) return;
|
||||||
|
|
||||||
|
let clickTimer = null;
|
||||||
|
let isDoubleClick = false;
|
||||||
|
|
||||||
|
row.addEventListener('click', (e) => {
|
||||||
|
if (afi.params.readonly || link.innerText === '../') return;
|
||||||
|
if (clickTimer) {
|
||||||
|
clearTimeout(clickTimer);
|
||||||
|
clickTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
clickTimer = setTimeout(() => {
|
||||||
|
if (!isDoubleClick) {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (confirm('确认删除此项目?')) {
|
||||||
|
afi.rpc.rmCall(afi.utils.prependPath(link.href), afi.utils.refresh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isDoubleClick = false;
|
||||||
|
clickTimer = null;
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
row.addEventListener('dblclick', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
isDoubleClick = true;
|
||||||
|
|
||||||
|
if (clickTimer) {
|
||||||
|
clearTimeout(clickTimer);
|
||||||
|
clickTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (afi.params.readonly) return;
|
||||||
|
if (link.innerText === '../') return; // 不能操作上级
|
||||||
|
|
||||||
|
const newName = prompt('新名称或目标地址', link.innerText);
|
||||||
|
if (newName && !afi.utils.isDupe(newName)) {
|
||||||
|
afi.rpc.mvCall(
|
||||||
|
afi.utils.prependPath(link.innerText),
|
||||||
|
afi.utils.prependPath(newName),
|
||||||
|
afi.utils.refresh
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
isDoubleClick = false;
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
document.ondragstart = e => { draggingSrc = e.target.innerHTML } // 记录拖拽源
|
||||||
|
document.ondragend = e => resetHighlight() // 清理高亮
|
||||||
|
document.ondragover = e => { afi.utils.cancelDefault(e); return false } // 允许拖拽
|
||||||
|
|
||||||
|
document.ondrop = e => {
|
||||||
|
if (afi.params.readonly) return
|
||||||
|
|
||||||
|
afi.utils.cancelDefault(e)
|
||||||
|
afi.elements.dropGrid.style.display = 'none'
|
||||||
|
const t = afi.utils.getHighlight().firstChild
|
||||||
|
|
||||||
|
if (draggingSrc && t) {
|
||||||
|
const dest = t.innerHTML + draggingSrc
|
||||||
|
afi.consts.ensureDrop() || afi.rpc.mvCall(afi.utils.prependPath(draggingSrc), afi.utils.prependPath(dest), afi.utils.refresh)
|
||||||
|
} else if (e.dataTransfer.items.length) {
|
||||||
|
Array.from(e.dataTransfer.items).forEach(afi.interface.pushEntry.bind(afi.interface))
|
||||||
|
}
|
||||||
|
|
||||||
|
afi.utils.resetHighlight()
|
||||||
|
draggingSrc = null
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!afi.params.readonly) {
|
||||||
|
afi.elements.barProc.innerText = "传输: 就绪"
|
||||||
|
}
|
||||||
|
console.log('[AFI] Initialized')
|
||||||
|
}
|
||||||
|
|
||||||
|
window.onload = init
|
||||||
348
ui/style.css
Normal file
348
ui/style.css
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
@import url('https://fonts.loli.net/css2?family=Cascadia+Code:ital,wght@0,200..700;1,200..700&display=swap');
|
||||||
|
@import url("https://fonts.loli.net/css2?family=Noto+Sans+SC:wght@400;700&display=swap");
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--background: #ffffff;
|
||||||
|
--background-darker: #eeeeee;
|
||||||
|
--background-verydarker: #dddddd;
|
||||||
|
--foreground: #454545;
|
||||||
|
--accent: #0f59a4;
|
||||||
|
--accent-lighten: #2793ff;
|
||||||
|
/*本来有color-mix的, 可惜支持太新*/
|
||||||
|
--radius: 0;
|
||||||
|
--font-size: 1rem;
|
||||||
|
--line-height: 1.54em;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
/* 预留滚动条空间,但不会让背景色断开,保持内容稳定 */
|
||||||
|
scrollbar-gutter: stable;
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
background-color: var(--background);
|
||||||
|
color: var(--foreground);
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-family: "Cascadia Code", "Noto Sans SC", system-ui, sans-serif;
|
||||||
|
background: var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background: var(--background);
|
||||||
|
padding: 4px 32px 4px 20px;
|
||||||
|
border-bottom: 1px solid var(--background-verydarker);
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-img {
|
||||||
|
display: inline-block;
|
||||||
|
height: calc(8px + 1em);
|
||||||
|
width: auto;
|
||||||
|
padding: 0 1em 0 0;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
#operations {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation {
|
||||||
|
padding: 4px 0px;
|
||||||
|
border-color: transparent;
|
||||||
|
color: var(--accent);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ellipsis {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#index-table {
|
||||||
|
margin: 20px 20px 0 20px;
|
||||||
|
width: auto;
|
||||||
|
border-collapse: collapse;
|
||||||
|
background: var(--background);
|
||||||
|
overflow: auto;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#index-table th,
|
||||||
|
#index-table td {
|
||||||
|
/*hack td高度计算的怪异标准(明确指定), 不然icon的div没高度显示不了icon*/
|
||||||
|
height: 1px;
|
||||||
|
text-align: left;
|
||||||
|
/*Noto Fonts SC 和 Cascadia Code(以及各种utf-8 fallback字体) 的em不同, 所以用父元素统一基准, 不用子元素(可能没中文), 不然icon大小不齐*/
|
||||||
|
/*然后导致rem不能有可变的多语言内容, 不然页面之间照样变*/
|
||||||
|
/*css设计时就不能设置以指定字体为参照的em吗? 本来这个问题能用ie盒模型+"指定字体e"解决的 这下非要导致每行content间距可能不一样*/
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
/*em单位简直毫不兼容且毫无预测性*/
|
||||||
|
/*连ex这种东西都设计出来了 就不能设计一个e(任意字符)吗? */
|
||||||
|
/*最终在icon用px+硬编码数字 懒得折腾了*/
|
||||||
|
/*真空球形鸡设计*/
|
||||||
|
}
|
||||||
|
|
||||||
|
#index-table thead th {
|
||||||
|
font-weight: 800;
|
||||||
|
text-transform: uppercase;
|
||||||
|
/*英文标题大写, 虽然我用中文*/
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#index-table tbody th,
|
||||||
|
#index-table tbody td {
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid var(--background-darker);
|
||||||
|
}
|
||||||
|
|
||||||
|
#index-table tr:hover {
|
||||||
|
background: var(--background-darker);
|
||||||
|
}
|
||||||
|
|
||||||
|
#index-table th:nth-child(1) {
|
||||||
|
min-width: 2.5em;
|
||||||
|
padding: 0.25em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#index-table td:nth-child(1) {
|
||||||
|
width: 1.5em;
|
||||||
|
padding: 0.25em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#index-table td:nth-child(2) {
|
||||||
|
width: 0.5em;
|
||||||
|
padding: 0.25em 0em;
|
||||||
|
/*就一个'>'号*/
|
||||||
|
}
|
||||||
|
|
||||||
|
#index-table th:nth-child(2),
|
||||||
|
#index-table td:nth-child(3) {
|
||||||
|
max-width: 0;
|
||||||
|
/*允许压缩*/
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#index-table th:nth-child(3),
|
||||||
|
#index-table td:nth-child(4) {
|
||||||
|
width: auto
|
||||||
|
/*能占就占满*/
|
||||||
|
}
|
||||||
|
|
||||||
|
#index-table th:nth-child(4) {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#index-table td:nth-child(5) {
|
||||||
|
text-align: right;
|
||||||
|
width: 3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#index-table th:nth-child(5) {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
#index-table td:nth-child(6) {
|
||||||
|
width: 12em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#index-table th:last-child,
|
||||||
|
#index-table td:last-child {
|
||||||
|
text-align: right;
|
||||||
|
padding-right: 2em;
|
||||||
|
max-width: 0;
|
||||||
|
/*允许压缩*/
|
||||||
|
min-width: 4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px 12px 4px 12px;
|
||||||
|
background: var(--background);
|
||||||
|
border-top: 1px solid var(--background-verydarker);
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--foreground);
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
20% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
80% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pulse {
|
||||||
|
animation: fade 5s 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-selected::before {
|
||||||
|
content: ">";
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight {
|
||||||
|
background-color: var(--accent-lighten);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#drop-grid {
|
||||||
|
align-items: center;
|
||||||
|
display: none;
|
||||||
|
justify-content: center;
|
||||||
|
position: fixed;
|
||||||
|
top: 0px;
|
||||||
|
bottom: 0px;
|
||||||
|
right: 0px;
|
||||||
|
left: 0px;
|
||||||
|
z-index: 999;
|
||||||
|
border: 5px dashed var(--accent);
|
||||||
|
margin: 0px;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 4em;
|
||||||
|
color: var(--accent);
|
||||||
|
opacity: 1;
|
||||||
|
background-color: rgba(255, 255, 255, 0.6);
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#drop-grid::after {
|
||||||
|
content: "释放光标以传输文件";
|
||||||
|
color: var(--foreground);
|
||||||
|
background-color: rgba(255, 255, 255, 0.6);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#notification {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
padding: 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#notification * {
|
||||||
|
padding: 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#statbar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
padding: 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#statbar * {
|
||||||
|
padding: 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-icon {
|
||||||
|
height: 16px;
|
||||||
|
width: auto;
|
||||||
|
/*垃圾css*/
|
||||||
|
cursor: pointer;
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-icon-folder {
|
||||||
|
background-image: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/PjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+PHN2ZyB0PSIxNzc0NjkwMDA5MjU1IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjEwNTMwIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiPjxwYXRoIGQ9Ik04ODAgMjk4LjRINTIxTDQwMy43IDE4Ni4yYy0xLjUtMS40LTMuNS0yLjItNS41LTIuMkgxNDRjLTE3LjcgMC0zMiAxNC4zLTMyIDMydjU5MmMwIDE3LjcgMTQuMyAzMiAzMiAzMmg3MzZjMTcuNyAwIDMyLTE0LjMgMzItMzJWMzMwLjRjMC0xNy43LTE0LjMtMzItMzItMzJ6TTg0MCA3NjhIMTg0VjI1NmgxODguNWwxMTkuNiAxMTQuNEg4NDBWNzY4eiIgcC1pZD0iMTA1MzEiPjwvcGF0aD48L3N2Zz4=") !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-icon-file {
|
||||||
|
background-image: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/PjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+PHN2ZyB0PSIxNzc0Njg5OTcyMTU2IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjUzNDAiIGRhdGEtc3BtLWFuY2hvci1pZD0iYTMxM3guc2VhcmNoX2luZGV4LjAuaTEuM2YwMTNhODFMUkJncXMiIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCI+PHBhdGggZD0iTTg1NC42IDI4OC42TDYzOS40IDczLjRjLTYtNi0xNC4xLTkuNC0yMi42LTkuNEgxOTJjLTE3LjcgMC0zMiAxNC4zLTMyIDMydjgzMmMwIDE3LjcgMTQuMyAzMiAzMiAzMmg2NDBjMTcuNyAwIDMyLTE0LjMgMzItMzJWMzExLjNjMC04LjUtMy40LTE2LjctOS40LTIyLjd6TTc5MC4yIDMyNkg2MDJWMTM3LjhMNzkwLjIgMzI2eiBtMS44IDU2MkgyMzJWMTM2aDMwMnYyMTZjMCAyMy4yIDE4LjggNDIgNDIgNDJoMjE2djQ5NHoiIHAtaWQ9IjUzNDEiIGRhdGEtc3BtLWFuY2hvci1pZD0iYTMxM3guc2VhcmNoX2luZGV4LjAuaTIuM2YwMTNhODFMUkJncXMiPjwvcGF0aD48L3N2Zz4=");
|
||||||
|
}
|
||||||
|
|
||||||
|
#help-panel {
|
||||||
|
display: flex;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#help-panel h3 {
|
||||||
|
padding: 0.2em 0 0 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#help-panel p {
|
||||||
|
padding: 0.3em 0 0.5em 0;
|
||||||
|
line-height: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#help-panel table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
align-items: center;
|
||||||
|
width: 40em;
|
||||||
|
max-height: 10em;
|
||||||
|
min-width: 30em;
|
||||||
|
max-width: 50%;
|
||||||
|
margin: auto;
|
||||||
|
border: 4px solid var(--foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
#help-panel table td {
|
||||||
|
border: 2px solid var(--background-darker);
|
||||||
|
}
|
||||||
|
|
||||||
|
#help-panel table * {
|
||||||
|
padding: 0.3em 1em;
|
||||||
|
}
|
||||||
204
ui/ui.html
Normal file
204
ui/ui.html
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>{{.Title}}</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.loli.net">
|
||||||
|
<link href="data:image/svg+xml;base64,favicon_will_be_here" rel="icon" type="image/svg+xml" />
|
||||||
|
<style type="text/css">
|
||||||
|
css_will_be_here
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
readonly = {{.Ro }}
|
||||||
|
extraPath = {{.ExtraPath }}.slice(0, -1)
|
||||||
|
js_will_be_here
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<input type="file" id="clickupload" multiple style="display:none" />
|
||||||
|
<header>
|
||||||
|
<a href="/"><img class="logo-img" src="data:image/svg+xml;base64,favicon_will_be_here" /></a>
|
||||||
|
<span class="nav" id="nav">{{.Path}}</span>
|
||||||
|
<span id="path" style="display: none;" onclick="return afi.directcall.bread_click(event)">{{.Path}}</span>
|
||||||
|
<div id="operations">
|
||||||
|
{{if not .Ro}}
|
||||||
|
<a onclick="document.getElementById('clickupload').click()" class="operation">上传文件</a>
|
||||||
|
<a onclick="afi.directcall.exec_touch()" class="operation">新建文件</a>
|
||||||
|
<a onclick="afi.directcall.exec_mkdir()" class="operation">新建目录</a>
|
||||||
|
<a onclick="afi.directcall.exec_mv()" class="operation">移动对象</a>
|
||||||
|
<a onclick="afi.directcall.exec_rm()" class="operation">递归删除</a>
|
||||||
|
<a onclick="" class="operation">登出</a>
|
||||||
|
{{end}}
|
||||||
|
{{if .Ro}}
|
||||||
|
<a onclick="" class="operation">登录</a>
|
||||||
|
{{end}}
|
||||||
|
<a onclick="afi.utils.helpOn()" class="operation" id="help-toggle">帮助菜单</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<table id="index-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th colspan="2">图标</th>
|
||||||
|
<th>名称</th>
|
||||||
|
<th><!--我是占位的--></th>
|
||||||
|
<th>大小</th>
|
||||||
|
<th>类型</th>
|
||||||
|
<th>备注</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range .RowsFolders}}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="item-icon type-icon-folder"></div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="item-selector"></div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="item-name"><a class="item-links" href="{{.Href}}/"><span
|
||||||
|
class="ellipsis">{{.Name}}</span></a></div>
|
||||||
|
</td>
|
||||||
|
<td><!--我是占位的--></td>
|
||||||
|
<td>
|
||||||
|
<div class="item-size">{{.Size}}</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="item-type">{{.Mime}}</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="item-desc"><span class="ellipsis">{{.Desc}}</span></div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
{{range .RowsFiles}}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="item-icon type-icon-file type-icon-{{.Ext}}"></div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="item-selector"></div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="item-name"><a class="item-links" href="{{.Href}}" download><span
|
||||||
|
class="ellipsis">{{.Name}}</span></a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td><!--我是占位的--></td>
|
||||||
|
<td>
|
||||||
|
<div class="item-size">{{.Size}}</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="item-type">{{.Mime}}</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="item-desc"><span class="ellipsis">{{.Desc}}</span></div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div style="display: none;" onclick="afi.utils.helpOff()" id="help-panel">
|
||||||
|
<br>
|
||||||
|
<h3>帮助信息与按键绑定</h3>
|
||||||
|
<p>^[X] 表示组合键 Ctrl + [X] 或 Meta + [X]</p>
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>使用键盘浏览文件系统</td>
|
||||||
|
<td>方向键与回车</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>上传项目</td>
|
||||||
|
<td>拖放外部文件或文件夹到窗口内部;<br>单击按钮或 ^U / Shift + U 以呼出文件上传菜单</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>移动项目</td>
|
||||||
|
<td>直接拖放界面内的项目;<br>对选中项 ^X, 并在目标目录中 ^V</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>下载为归档文件(.zip)</td>
|
||||||
|
<td>Ctrl + 单击任一项目;<br>对选中项 ^↵</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>复制链接</td>
|
||||||
|
<td>右键任一项目复制;<br>对选中项 ^C</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>重命名项目</td>
|
||||||
|
<td>单击任一文件图标;<br>对选中项 ^E</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>删除项目</td>
|
||||||
|
<td>双击任一文件图标;<br>对选中项 ^⌫</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>新建文件夹</td>
|
||||||
|
<td>^M</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>复制多种校验和</td>
|
||||||
|
<td>对选中项目 ^Z</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>进行模糊搜索匹配</td>
|
||||||
|
<td>单独输入其他任意内容</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<br>
|
||||||
|
<h3>关于 AFI</h3>
|
||||||
|
<p>AFI (敏捷文件索引器) 是 Go 编写的轻量级文件服务器.<br>
|
||||||
|
AFI 是基于 Gossa 的现代化改进与维护分支, 以 MIT 协议在<a href="">此处</a>开放源代码.<br>
|
||||||
|
您可在仓库网页中的 Issues 内反馈缺陷, 提出需求或想法 ^v^</p>
|
||||||
|
</div>
|
||||||
|
<div onclick="afi.sumsOff()" style="display: none;" id="sums">
|
||||||
|
<table id="sumsTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td>按键</td>
|
||||||
|
<td>哈希算法</td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>1</td>
|
||||||
|
<td>复制 sha1 校验和</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>2</td>
|
||||||
|
<td>复制 sha256 校验和</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>3</td>
|
||||||
|
<td>复制 sha512 校验和</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>5</td>
|
||||||
|
<td>复制 md5 校验和</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="placeholder"><!--我是液压机--></div>
|
||||||
|
<div id="drop-grid"></div>
|
||||||
|
<footer>
|
||||||
|
<div id="about">AFI - 轻巧敏捷的文件服务器</div>
|
||||||
|
<div style="display: inline-block;">
|
||||||
|
<div id="notification" style="display: inline-block;">
|
||||||
|
<span style="display: none;" id="badge"></span>
|
||||||
|
</div>
|
||||||
|
<div id="statbar" style="display: inline-block;">
|
||||||
|
<span id="bar-proc"></span>
|
||||||
|
<span id="bar-display" style="display: none;"></span>
|
||||||
|
<span id="bar-checksum" style="display: none;"></span>
|
||||||
|
</div>
|
||||||
|
<div id="timestamp" style="display: inline-block;">页面生成于 {{.TimeStamp}}</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user