notify/file_receive_fs.go
starainrt 09d972c7b7
feat(notify): 重构通信内核并补齐 stream/bulk/record/transfer 能力
- 引入 LogicalConn/TransportConn 分层,ClientConn 保留兼容适配层
  - 新增 Stream、Bulk、RecordStream 三条数据面能力及对应控制路径
  - 完成 transfer/file 传输内核与状态快照、诊断能力
  - 补齐 reconnect、inbound dispatcher、modern psk 等基础模块
  - 增加大规模回归、并发与基准测试覆盖
  - 更新依赖库
2026-04-15 15:24:36 +08:00

148 lines
3.2 KiB
Go

package notify
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
)
func computeFileChecksum(path string) (string, error) {
fd, err := os.Open(path)
if err != nil {
return "", err
}
defer fd.Close()
h := sha256.New()
if _, err := io.Copy(h, fd); err != nil {
return "", err
}
return hex.EncodeToString(h.Sum(nil)), nil
}
func filePacketModTime(packet FilePacket) time.Time {
if packet.ModTime <= 0 {
return time.Time{}
}
return time.Unix(0, packet.ModTime)
}
func applyReceivedFileMeta(path string, mode os.FileMode, modTime time.Time) {
if mode != 0 {
_ = os.Chmod(path, mode.Perm())
}
if !modTime.IsZero() {
_ = os.Chtimes(path, modTime, modTime)
}
}
func sanitizeFileName(name string) string {
trimmed := strings.TrimSpace(name)
if trimmed == "" {
return "unnamed"
}
trimmed = strings.ReplaceAll(trimmed, "/", "_")
trimmed = strings.ReplaceAll(trimmed, "\\", "_")
trimmed = strings.ReplaceAll(trimmed, ":", "_")
return trimmed
}
func shortFileIDSuffix(fileID string) string {
cleaned := sanitizeFileName(fileID)
if len(cleaned) > 12 {
return cleaned[:12]
}
if cleaned == "" {
return "copy"
}
return cleaned
}
func pathExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
func (p *fileReceivePool) receiveDirLocked() string {
if p.dir != "" {
return p.dir
}
return os.TempDir()
}
func (p *fileReceivePool) uniqueFinalPathLocked(baseDir string, name string, fileID string) string {
cleanName := sanitizeFileName(filepath.Base(name))
if cleanName == "" {
cleanName = "unnamed.bin"
}
ext := filepath.Ext(cleanName)
base := strings.TrimSuffix(cleanName, ext)
candidate := filepath.Join(baseDir, cleanName)
if !p.pathReservedLocked(candidate) && !pathExists(candidate) {
return candidate
}
suffix := shortFileIDSuffix(fileID)
candidate = filepath.Join(baseDir, fmt.Sprintf("%s.%s%s", base, suffix, ext))
if !p.pathReservedLocked(candidate) && !pathExists(candidate) {
return candidate
}
for i := 1; ; i++ {
candidate = filepath.Join(baseDir, fmt.Sprintf("%s.%s.%d%s", base, suffix, i, ext))
if !p.pathReservedLocked(candidate) && !pathExists(candidate) {
return candidate
}
}
}
func (p *fileReceivePool) pathReservedLocked(path string) bool {
for _, session := range p.sessions {
if session.finalPath == path || session.tmpPath == path {
return true
}
}
return false
}
func (p *fileReceivePool) trimCompletedLocked() {
if p.completedLimit <= 0 || len(p.completed) <= p.completedLimit {
return
}
for len(p.completed) > p.completedLimit {
oldestKey := ""
oldestTime := time.Time{}
for key, session := range p.completed {
candidateTime := completedFileReceiveTime(session)
if oldestKey == "" || candidateTime.Before(oldestTime) || (candidateTime.Equal(oldestTime) && key < oldestKey) {
oldestKey = key
oldestTime = candidateTime
}
}
if oldestKey == "" {
return
}
delete(p.completed, oldestKey)
}
}
func completedFileReceiveTime(session *fileReceiveSession) time.Time {
if session == nil {
return time.Time{}
}
if !session.updatedAt.IsZero() {
return session.updatedAt
}
return session.startedAt
}
func (s *fileReceiveSession) copy() *fileReceiveSession {
if s == nil {
return nil
}
dup := *s
return &dup
}