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() }