feat(logs): make log files downloadable (#706)

* feat(logs): show and download log files

* feat(logs): make logs settings dropdown

* feat(logs): minor cosmetic changes

* fix(logs): send empty response when lohpath not configured

* fix(logs): remove unused imports

* feat(logs): check if logs dir exists

* feat(logs): list log files in settings
This commit is contained in:
ze0s 2023-02-12 17:34:09 +01:00 committed by GitHub
parent 21724f29f6
commit b21c01a7df
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 394 additions and 79 deletions

141
internal/http/logs.go Normal file
View file

@ -0,0 +1,141 @@
package http
import (
"io/fs"
"net/http"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/autobrr/autobrr/internal/config"
"github.com/dustin/go-humanize"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
)
type logsHandler struct {
cfg *config.AppConfig
}
func newLogsHandler(cfg *config.AppConfig) *logsHandler {
return &logsHandler{cfg: cfg}
}
func (h logsHandler) Routes(r chi.Router) {
r.Get("/files", h.files)
r.Get("/files/{logFile}", h.downloadFile)
}
func (h logsHandler) files(w http.ResponseWriter, r *http.Request) {
response := LogfilesResponse{
Files: []logFile{},
Count: 0,
}
if h.cfg.Config.LogPath == "" {
render.JSON(w, r, response)
return
}
logsDir := path.Dir(h.cfg.Config.LogPath)
// check if dir exists before walkDir
if _, err := os.Stat(logsDir); os.IsNotExist(err) {
render.JSON(w, r, response)
return
}
var walk = func(path string, d fs.DirEntry, err error) error {
if d.IsDir() {
return nil
}
if filepath.Ext(path) == ".log" {
i, err := d.Info()
if err != nil {
return err
}
response.Files = append(response.Files, logFile{
Name: d.Name(),
SizeBytes: i.Size(),
Size: humanize.Bytes(uint64(i.Size())),
UpdatedAt: i.ModTime(),
})
}
return nil
}
if err := filepath.WalkDir(logsDir, walk); err != nil {
render.Status(r, http.StatusInternalServerError)
render.JSON(w, r, errorResponse{
Message: err.Error(),
Status: http.StatusInternalServerError,
})
return
}
response.Count = len(response.Files)
render.JSON(w, r, response)
}
func (h logsHandler) downloadFile(w http.ResponseWriter, r *http.Request) {
if h.cfg.Config.LogPath == "" {
render.Status(r, http.StatusNotFound)
return
}
logsDir := path.Dir(h.cfg.Config.LogPath)
// check if dir exists before walkDir
if _, err := os.Stat(logsDir); os.IsNotExist(err) {
render.Status(r, http.StatusNotFound)
render.JSON(w, r, errorResponse{
Message: "log directory not found or inaccessible",
Status: http.StatusNotFound,
})
return
}
logFile := chi.URLParam(r, "logFile")
if logFile == "" {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, errorResponse{
Message: "empty log file",
Status: http.StatusBadRequest,
})
return
} else if !strings.Contains(logFile, ".log") {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, errorResponse{
Message: "invalid file",
Status: http.StatusBadRequest,
})
return
}
w.Header().Set("Content-Disposition", "attachment; filename="+strconv.Quote(logFile))
w.Header().Set("Content-Type", "application/octet-stream")
filePath := filepath.Join(logsDir, logFile)
http.ServeFile(w, r, filePath)
}
type logFile struct {
Name string `json:"filename"`
SizeBytes int64 `json:"size_bytes"`
Size string `json:"size"`
UpdatedAt time.Time `json:"updated_at"`
}
type LogfilesResponse struct {
Files []logFile `json:"files"`
Count int `json:"count"`
}