Ztorrent/internal/sessionlog/sessionlog.go

185 lines
4.6 KiB
Go

package sessionlog
import (
"fmt"
"io"
"log"
"os"
"path/filepath"
"sort"
"strings"
"time"
)
// Entry представляет один лог-файл сессии.
type Entry struct {
Name string // имя файла без расширения
Path string // полный путь
ModTime time.Time // время изменения
}
// Dir возвращает директорию логов (~/.ztorrent/logs).
func Dir() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
dir := filepath.Join(home, ".ztorrent", "logs")
if err := os.MkdirAll(dir, 0755); err != nil {
return "", err
}
return dir, nil
}
// NewWriter создаёт новый md-файл для текущей сессии и возвращает:
// - путь к файлу
// - io.WriteCloser для записи
// - ошибку
func NewWriter(torrentName string) (string, io.WriteCloser, error) {
dir, err := Dir()
if err != nil {
return "", nil, err
}
ts := time.Now().Format("2006-01-02_15-04-05")
safe := sanitize(torrentName)
if safe == "" {
safe = "session"
}
fname := fmt.Sprintf("%s_%s.md", ts, safe)
path := filepath.Join(dir, fname)
f, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
if err != nil {
return "", nil, err
}
// Записываем заголовок
header := fmt.Sprintf("# Сессия: %s\n\n**Начало:** %s \n**Торрент:** %s\n\n---\n\n```\n",
fname, time.Now().Format("02.01.2006 15:04:05"), torrentName)
_, _ = f.WriteString(header)
return path, f, nil
}
// sanitize убирает из имени файла недопустимые символы.
func sanitize(s string) string {
replacer := strings.NewReplacer(
"/", "_", "\\", "_", ":", "_", "*", "_",
"?", "_", "\"", "_", "<", "_", ">", "_", "|", "_",
" ", "_",
)
s = replacer.Replace(s)
if len(s) > 40 {
s = s[:40]
}
return s
}
// List возвращает список лог-файлов, отсортированных от новых к старым.
func List() ([]Entry, error) {
dir, err := Dir()
if err != nil {
return nil, err
}
entries, err := os.ReadDir(dir)
if err != nil {
return nil, err
}
var result []Entry
for _, e := range entries {
if e.IsDir() || !strings.HasSuffix(e.Name(), ".md") {
continue
}
info, err := e.Info()
if err != nil {
continue
}
result = append(result, Entry{
Name: strings.TrimSuffix(e.Name(), ".md"),
Path: filepath.Join(dir, e.Name()),
ModTime: info.ModTime(),
})
}
// Сортировка от новых к старым
sort.Slice(result, func(i, j int) bool {
return result[i].ModTime.After(result[j].ModTime)
})
return result, nil
}
// Read читает содержимое лог-файла.
func Read(path string) (string, error) {
data, err := os.ReadFile(path)
if err != nil {
return "", err
}
return string(data), nil
}
// Delete удаляет лог-файл.
func Delete(path string) error {
return os.Remove(path)
}
// LogWriter оборачивает os.File и при записи:
// - пишет в файл
// - пишет в стандартный log (если enable)
type LogWriter struct {
file *os.File
path string
closed bool
}
// NewLogWriter создаёт LogWriter, настраивает стандартный log на запись в файл.
func NewLogWriter(torrentName string) (*LogWriter, error) {
dir, err := Dir()
if err != nil {
return nil, err
}
ts := time.Now().Format("2006-01-02_15-04-05")
safe := sanitize(torrentName)
if safe == "" {
safe = "session"
}
fname := fmt.Sprintf("%s_%s.md", ts, safe)
path := filepath.Join(dir, fname)
f, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
if err != nil {
return nil, err
}
header := fmt.Sprintf("# Сессия: %s\n\n**Начало:** %s \n**Торрент:** %s\n\n---\n\n```\n",
fname, time.Now().Format("02.01.2006 15:04:05"), torrentName)
_, _ = f.WriteString(header)
lw := &LogWriter{file: f, path: path}
log.SetOutput(lw)
return lw, nil
}
// Write реализует io.Writer.
func (lw *LogWriter) Write(p []byte) (n int, err error) {
if lw.file == nil {
return len(p), nil
}
return lw.file.Write(p)
}
// Path возвращает путь к файлу лога.
func (lw *LogWriter) Path() string {
return lw.path
}
// Close закрывает файл и дописывает завершающий блок.
func (lw *LogWriter) Close() error {
if lw.closed || lw.file == nil {
return nil
}
lw.closed = true
_, _ = lw.file.WriteString("```\n\n---\n\n*Сессия завершена: " + time.Now().Format("02.01.2006 15:04:05") + "*\n")
return lw.file.Close()
}