Files

276 lines
6.6 KiB
Go

package main
import (
"archive/tar"
"bufio"
"compress/gzip"
"errors"
"fmt"
"io"
"io/fs"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"time"
)
var safeFilenamePattern = regexp.MustCompile(`[^A-Za-z0-9_.-]`)
func (app *App) log_line(line string) {
handle, err := os.OpenFile(app.paths.LogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
if err != nil {
return
}
defer handle.Close()
_, _ = fmt.Fprintln(handle, line)
}
func load_logging_pref(path string) bool {
file, err := os.Open(path)
if err != nil {
return false
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
key, value, found := strings.Cut(line, "=")
if !found || !strings.EqualFold(strings.TrimSpace(key), "saveSessionLogs") {
continue
}
switch strings.ToLower(strings.TrimSpace(value)) {
case "1", "true", "yes", "on":
return true
default:
return false
}
}
return false
}
func save_logging_pref(path string, enabled bool) error {
return write_private_file(path, func(writer io.Writer) error {
value := 0
if enabled {
value = 1
}
_, err := fmt.Fprintf(writer, "saveSessionLogs=%d\n", value)
return err
})
}
func sanitize_filename(value string) string {
return safeFilenamePattern.ReplaceAllString(value, "_")
}
func prepare_session_log(logDir, serverName string) (string, error) {
if err := os.MkdirAll(logDir, 0700); err != nil {
return "", err
}
path := filepath.Join(logDir, fmt.Sprintf("%s-%s.log", sanitize_filename(serverName), time.Now().Format("2006-01-02")))
handle, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
if err != nil {
return "", err
}
if err := handle.Chmod(0600); err != nil {
handle.Close()
return "", err
}
if err := handle.Close(); err != nil {
return "", err
}
return path, nil
}
func log_files(logDir string) ([]string, error) {
entries, err := os.ReadDir(logDir)
if err != nil {
return nil, err
}
paths := []string{}
for _, entry := range entries {
if entry.Type().IsRegular() && strings.HasSuffix(entry.Name(), ".log") {
paths = append(paths, filepath.Join(logDir, entry.Name()))
}
}
return paths, nil
}
func create_log_archive(logDir, cacheDir string) (string, error) {
paths, err := log_files(logDir)
if err != nil {
return "", err
}
if len(paths) == 0 {
return "", fs.ErrNotExist
}
if err := os.MkdirAll(cacheDir, 0700); err != nil {
return "", err
}
archiveFile, err := os.CreateTemp(cacheDir, "barnard-logs-*.tar.gz")
if err != nil {
return "", err
}
archivePath := archiveFile.Name()
failed := true
defer func() {
if failed {
_ = os.Remove(archivePath)
}
}()
if err := archiveFile.Chmod(0600); err != nil {
archiveFile.Close()
return "", err
}
gzipWriter := gzip.NewWriter(archiveFile)
tarWriter := tar.NewWriter(gzipWriter)
for _, path := range paths {
info, err := os.Stat(path)
if err != nil {
tarWriter.Close()
gzipWriter.Close()
archiveFile.Close()
return "", err
}
header, err := tar.FileInfoHeader(info, "")
if err != nil {
tarWriter.Close()
gzipWriter.Close()
archiveFile.Close()
return "", err
}
header.Name = filepath.Base(path)
header.Mode = 0600
if err := tarWriter.WriteHeader(header); err != nil {
tarWriter.Close()
gzipWriter.Close()
archiveFile.Close()
return "", err
}
file, err := os.Open(path)
if err != nil {
tarWriter.Close()
gzipWriter.Close()
archiveFile.Close()
return "", err
}
_, copyErr := io.Copy(tarWriter, file)
closeErr := file.Close()
if copyErr != nil {
tarWriter.Close()
gzipWriter.Close()
archiveFile.Close()
return "", copyErr
}
if closeErr != nil {
tarWriter.Close()
gzipWriter.Close()
archiveFile.Close()
return "", closeErr
}
}
if err := tarWriter.Close(); err != nil {
gzipWriter.Close()
archiveFile.Close()
return "", err
}
if err := gzipWriter.Close(); err != nil {
archiveFile.Close()
return "", err
}
if err := archiveFile.Sync(); err != nil {
archiveFile.Close()
return "", err
}
if err := archiveFile.Close(); err != nil {
return "", err
}
failed = false
return archivePath, nil
}
func (app *App) send_logs() error {
wormhole, err := exec.LookPath("wormhole")
if err != nil {
return app.ui.message("Required command not found: wormhole")
}
archivePath, err := create_log_archive(app.paths.LogDir, app.paths.CacheDir)
if errors.Is(err, fs.ErrNotExist) || errors.Is(err, os.ErrNotExist) {
return app.ui.message("No logs to send. Logs are saved to: " + app.paths.LogDir)
}
if err != nil {
return app.ui.message("Could not create log archive: " + err.Error())
}
defer os.Remove(archivePath)
if err := app.ui.message("Wormhole will display the transfer code in the normal terminal. Press Ctrl+C there to cancel the transfer."); err != nil {
return err
}
command := exec.Command(wormhole, "send", archivePath)
command.Stdin = os.Stdin
command.Stdout = os.Stdout
command.Stderr = os.Stderr
app.ui.close()
commandErr := run_external(app.ui, command)
var signalErr *terminalSignalError
if errors.As(commandErr, &signalErr) {
return signalErr
}
if err := app.ui.open(); err != nil {
return err
}
if receivedSignal := app.ui.take_termination(); receivedSignal != nil {
return &terminalSignalError{signal: receivedSignal}
}
if commandErr == nil {
app.log_line("Sent log archive with wormhole")
return app.ui.message("Logs sent successfully.")
}
return app.ui.message("Log transfer did not complete successfully: " + commandErr.Error())
}
func (app *App) toggle_session_logging() error {
question := "Session logging is currently disabled. Enable saving logs to the logs directory?"
if app.saveSessionLogs {
question = "Session logging is currently enabled. Disable it?"
}
confirmed, err := app.ui.confirm(question)
if err != nil || !confirmed {
return err
}
newValue := !app.saveSessionLogs
if err := save_logging_pref(app.paths.LogPrefsFile, newValue); err != nil {
return app.ui.message("Could not save logging preference: " + err.Error())
}
app.saveSessionLogs = newValue
return nil
}
func (app *App) manage_logs() error {
for {
toggleLabel := "Enable logs"
if app.saveSessionLogs {
toggleLabel = "Disable logs"
}
options := []string{toggleLabel, "Send logs with wormhole", "Go Back"}
selection, cancelled, err := app.ui.menu(options)
if err != nil || cancelled || selection == len(options)-1 {
return err
}
switch selection {
case 0:
err = app.toggle_session_logging()
case 1:
err = app.send_logs()
}
if err != nil {
return err
}
}
}