Files
耗子 5391911a97 fix: 修复日志查看器多处问题
- 首屏加载后不贴底:改由 useResizeObserver 把"跟随时保持贴底"维持成不变式,
  覆盖弹窗入场、横向滚动条、字体度量、tab 由隐藏转可见等后续布局变化
- 向上翻页内容重复:文件日志改用首屏 size 作为反向分页锚点,
  不再受跟踪期间写入的新日志影响
- 首屏到建连之间的日志丢失:follow 支持从锚点字节位置续读(tail -c +N),
  重连仍只跟新增以免整段重放
- 断线重连残留的半行与新流拼接会拼出错行
- 点"跳到顶部"被翻页的位置补偿拽回原处
- 实时裁剪未退还翻页游标,导致下次上翻跳过被裁掉的那一段
- 搜索跳转改按相对位置计算,不再连带滚动外层页面
- 首屏日志不足一页时凭空出现"没有更多日志"

性能:
- ws 帧合并到 rAF 统一落盘,繁忙日志下渲染次数从帧数级降到 60/秒级
- 反向读取按字节切分,只把返回的那页转 string;首块按预估行长一次读足
- 日志行 markRaw 并移除小写副本字段

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 20:01:12 +08:00

110 lines
4.2 KiB
Go

package request
import (
"net/http"
"github.com/spf13/cast"
)
type FileList struct {
Path string `json:"path" form:"path" validate:"required && unix_path"`
Sort string `json:"sort" form:"sort"`
Keyword string `form:"keyword" json:"keyword"`
Sub bool `form:"sub" json:"sub"`
}
func (r *FileList) Prepare(req *http.Request) error {
r.Sub = cast.ToBool(req.FormValue("sub"))
return nil
}
type FilePath struct {
Path string `json:"path" form:"path" validate:"required && unix_path"`
}
type FileTail struct {
Path string `json:"path" form:"path"`
Service string `json:"service" form:"service"`
Container string `json:"container" form:"container"`
Offset int `json:"offset" form:"offset"`
Limit int `json:"limit" form:"limit"`
Cursor string `json:"cursor" form:"cursor"`
// Size 为首屏返回的文件大小,翻页时回传作为反向分页锚点,避免日志持续写入导致错位
Size int64 `json:"size" form:"size"`
}
type FileFollow struct {
Path string `json:"path" form:"path"`
Service string `json:"service" form:"service"`
Container string `json:"container" form:"container"`
// Offset 为首屏锚点字节位置,从该处开始跟踪以衔接首屏与实时流,避免中间写入的日志丢失
Offset int64 `json:"offset" form:"offset"`
}
type FileCreate struct {
Dir bool `json:"dir" form:"dir"`
Path string `json:"path" form:"path" validate:"required && unix_path"`
}
type FileSave struct {
Path string `form:"path" json:"path" validate:"required && unix_path"`
Content string `form:"content" json:"content"`
}
type FileControl struct {
Source string `form:"source" json:"source" validate:"required && unix_path"`
Target string `form:"target" json:"target" validate:"required && unix_path"`
Force bool `form:"force" json:"force"`
}
type FileRemoteDownload struct {
Path string `form:"path" json:"path" validate:"required && unix_path"`
URL string `form:"url" json:"url" validate:"required && url"`
}
type FilePermission struct {
Path string `form:"path" json:"path" validate:"required && unix_path"`
Mode string `form:"mode" json:"mode" validate:"required"`
Owner string `form:"owner" json:"owner" validate:"required"`
Group string `form:"group" json:"group" validate:"required"`
}
type FileCompress struct {
Dir string `form:"dir" json:"dir" validate:"required && unix_path"`
Paths []string `form:"paths" json:"paths" validate:"required && unique && dive && required"`
File string `form:"file" json:"file" validate:"required && unix_path"`
}
type FileUnCompress struct {
File string `form:"file" json:"file" validate:"required && unix_path"`
Path string `form:"path" json:"path" validate:"required && unix_path"`
}
type FileShareCreate struct {
Path string `form:"path" json:"path" validate:"required && unix_path"`
MaxDownloads uint `form:"max_downloads" json:"max_downloads"` // 最大下载次数(0 不限)
ExpireHours uint `form:"expire_hours" json:"expire_hours" validate:"required && min:1 && max:8760"` // 有效期(小时)
}
type FileShareToken struct {
Token string `json:"token" form:"token" uri:"token" validate:"required"`
}
// ChunkUploadStart 分块上传开始请求
type ChunkUploadStart struct {
Path string `json:"path" validate:"required && unix_path"` // 目标目录
FileName string `json:"file_name" validate:"required"` // 文件名
FileHash string `json:"file_hash" validate:"required && len:64"` // 文件SHA256
ChunkCount int `json:"chunk_count" validate:"required && min:1"` // 分块总数
Force bool `json:"force"` // 是否覆盖已存在文件
}
// ChunkUploadFinish 分块上传完成请求
type ChunkUploadFinish struct {
Path string `json:"path" validate:"required && unix_path"` // 目标目录
FileName string `json:"file_name" validate:"required"` // 文件名
FileHash string `json:"file_hash" validate:"required && len:64"` // 文件SHA256
ChunkCount int `json:"chunk_count" validate:"required && min:1"` // 分块总数
Force bool `json:"force"` // 是否覆盖已存在文件
}