Files
Gokapi/internal/logging/Logging.go
T
2026-01-26 00:08:01 +01:00

260 lines
9.0 KiB
Go

package logging
import (
"bufio"
"fmt"
"net"
"net/http"
"os"
"strings"
"sync"
"time"
"github.com/forceu/gokapi/internal/environment"
"github.com/forceu/gokapi/internal/environment/deprecation"
"github.com/forceu/gokapi/internal/helper"
"github.com/forceu/gokapi/internal/models"
)
var logPath = "config/log.txt"
var mutex sync.Mutex
const categoryInfo = "info"
const categoryDownload = "download"
const categoryUpload = "upload"
const categoryEdit = "edit"
const categoryAuth = "auth"
const categoryWarning = "warning"
var outputToStdout = false
// Init sets the path where to write the log file to
func Init(filePath string) {
logPath = filePath + "/log.txt"
env := environment.New()
outputToStdout = env.LogToStdout
}
// GetAll returns all log entries as a single string and if the log file exists
func GetAll() (string, bool) {
exists, err := helper.FileExists(logPath)
helper.Check(err)
if exists {
content, err := os.ReadFile(logPath)
helper.Check(err)
return string(content), true
}
return fmt.Sprintf("[%s] No log file found!", categoryWarning), false
}
// createLogEntry adds a line to the logfile including the current date. Also outputs to Stdout if set.
func createLogEntry(category, text string, blocking bool) {
output := createLogFormat(category, text)
if outputToStdout {
fmt.Println(output)
}
if blocking {
writeToFile(output)
} else {
go writeToFile(output)
}
}
func createLogFormat(category, text string) string {
return createLogFormatCustomTimestamp(category, text, time.Now())
}
func createLogFormatCustomTimestamp(category, text string, timestamp time.Time) string {
return fmt.Sprintf("%s [%s] %s", getDate(timestamp), category, text)
}
// LogStartup adds a log entry to indicate that Gokapi has started. Non-blocking
func LogStartup() {
createLogEntry(categoryInfo, "Gokapi started", false)
}
// LogShutdown adds a log entry to indicate that Gokapi is shutting down. Blocking call
func LogShutdown() {
createLogEntry(categoryInfo, "Gokapi shutting down", true)
}
// LogSetup adds a log entry to indicate that the setup was run. Non-blocking
func LogSetup() {
createLogEntry(categoryAuth, "Re-running Gokapi setup", false)
}
// LogDeploymentPassword adds a log entry to indicate that a deployment password was set. Non-blocking
func LogDeploymentPassword() {
createLogEntry(categoryAuth, "Setting new admin password", false)
}
// LogUserDeletion adds a log entry to indicate that a user was deleted. Non-blocking
func LogUserDeletion(modifiedUser, userEditor models.User) {
createLogEntry(categoryAuth, fmt.Sprintf("%s (#%d) was deleted by %s (user #%d)",
modifiedUser.Name, modifiedUser.Id, userEditor.Name, userEditor.Id), false)
}
// LogUserEdit adds a log entry to indicate that a user was modified. Non-blocking
func LogUserEdit(modifiedUser, userEditor models.User) {
createLogEntry(categoryAuth, fmt.Sprintf("%s (#%d) was modified by %s (user #%d)",
modifiedUser.Name, modifiedUser.Id, userEditor.Name, userEditor.Id), false)
}
// LogUserCreation adds a log entry to indicate that a user was created. Non-blocking
func LogUserCreation(modifiedUser, userEditor models.User) {
createLogEntry(categoryAuth, fmt.Sprintf("%s (#%d) was created by %s (user #%d)",
modifiedUser.Name, modifiedUser.Id, userEditor.Name, userEditor.Id), false)
}
// LogDownload adds a log entry when a download was requested. Non-Blocking
func LogDownload(file models.File, r *http.Request, saveIp bool) {
if saveIp {
createLogEntry(categoryDownload, fmt.Sprintf("%s, IP %s, ID %s, Useragent %s", file.Name, getIpAddress(r), file.Id, r.UserAgent()), false)
} else {
createLogEntry(categoryDownload, fmt.Sprintf("%s, ID %s, Useragent %s", file.Name, file.Id, r.UserAgent()), false)
}
}
// LogUpload adds a log entry when an upload was created. Non-Blocking
func LogUpload(file models.File, user models.User, fr models.FileRequest) {
if fr.Id != "" {
createLogEntry(categoryUpload, fmt.Sprintf("%s, ID %s, uploaded to file request %s (%s), owned by %s (user #%d) ", file.Name, file.Id, fr.Id, fr.Name, user.Name, user.Id), false)
} else {
createLogEntry(categoryUpload, fmt.Sprintf("%s, ID %s, uploaded by %s (user #%d)", file.Name, file.Id, user.Name, user.Id), false)
}
}
// LogEdit adds a log entry when an upload was edited. Non-Blocking
func LogEdit(file models.File, user models.User) {
createLogEntry(categoryEdit, fmt.Sprintf("%s, ID %s, edited by %s (user #%d)", file.Name, file.Id, user.Name, user.Id), false)
}
// LogCreateFileRequest adds a log entry when a file request was added. Non-Blocking
func LogCreateFileRequest(fr models.FileRequest, user models.User) {
createLogEntry(categoryEdit, fmt.Sprintf("File request %s (%s) created by %s (user #%d)", fr.Id, fr.Name, user.Name, user.Id), false)
}
// LogEditFileRequest adds a log entry when a file request was edited. Non-Blocking
func LogEditFileRequest(fr models.FileRequest, user models.User) {
createLogEntry(categoryEdit, fmt.Sprintf("File request %s (%s) created by %s (user #%d)", fr.Id, fr.Name, user.Name, user.Id), false)
}
// LogDeleteFileRequest adds a log entry when a file request was deleted. Non-Blocking
func LogDeleteFileRequest(fr models.FileRequest, user models.User) {
createLogEntry(categoryEdit, fmt.Sprintf("File request %s (%s) and associated files deleted by %s (user #%d)", fr.Id, fr.Name, user.Name, user.Id), false)
}
// LogReplace adds a log entry when an upload was replaced. Non-Blocking
func LogReplace(originalFile, newContent models.File, user models.User) {
createLogEntry(categoryEdit, fmt.Sprintf("%s, ID %s had content replaced with %s (ID %s) by %s (user #%d)",
originalFile.Name, originalFile.Id, newContent.Name, newContent.Id, user.Name, user.Id), false)
}
// LogDelete adds a log entry when an upload was deleted. Non-Blocking
func LogDelete(file models.File, user models.User) {
createLogEntry(categoryEdit, fmt.Sprintf("%s, ID %s, deleted by %s (user #%d)", file.Name, file.Id, user.Name, user.Id), false)
}
// LogRestore adds a log entry when the pending deletion of a file was cancelled and the file restored. Non-Blocking
func LogRestore(file models.File, user models.User) {
createLogEntry(categoryEdit, fmt.Sprintf("%s, ID %s, restored by %s (user #%d)", file.Name, file.Id, user.Name, user.Id), false)
}
// LogDeprecation adds a log entry to indicate that a deprecated feature is being used. Blocking
func LogDeprecation(dep deprecation.Deprecation) {
createLogEntry(categoryWarning, "Deprecated feature: "+dep.Name, true)
createLogEntry(categoryWarning, dep.Description, true)
createLogEntry(categoryWarning, "See "+dep.DocUrl+" for more information.", true)
}
// DeleteLogs removes all logs before the cutoff timestamp and inserts a new log that the user
// deleted the previous logs
func DeleteLogs(userName string, userId int, cutoff int64, r *http.Request) {
if cutoff == 0 {
deleteAllLogs(userName, userId, r)
return
}
mutex.Lock()
logFile, err := os.ReadFile(logPath)
helper.Check(err)
var newFile strings.Builder
scanner := bufio.NewScanner(strings.NewReader(string(logFile)))
newFile.WriteString(getLogDeletionMessage(userName, userId, r, time.Unix(cutoff, 0)))
for scanner.Scan() {
line := scanner.Text()
timeEntry, err := parseTimeLogEntry(line)
if err != nil {
fmt.Println(err)
continue
}
if timeEntry.Unix() > cutoff {
newFile.WriteString(line + "\n")
}
}
err = os.WriteFile(logPath, []byte(newFile.String()), 0600)
helper.Check(err)
defer mutex.Unlock()
}
func parseTimeLogEntry(input string) (time.Time, error) {
const layout = "Mon, 02 Jan 2006 15:04:05 MST"
lineContent := strings.Split(input, " [")
return time.Parse(layout, lineContent[0])
}
func getLogDeletionMessage(userName string, userId int, r *http.Request, timestamp time.Time) string {
return createLogFormatCustomTimestamp(categoryWarning, fmt.Sprintf("Previous logs deleted by %s (user #%d) on %s. IP: %s\n",
userName, userId, getDate(time.Now()), getIpAddress(r)), timestamp)
}
func deleteAllLogs(userName string, userId int, r *http.Request) {
mutex.Lock()
defer mutex.Unlock()
message := getLogDeletionMessage(userName, userId, r, time.Now())
err := os.WriteFile(logPath, []byte(message), 0600)
helper.Check(err)
}
func writeToFile(text string) {
mutex.Lock()
file, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
helper.Check(err)
defer file.Close()
defer mutex.Unlock()
_, err = file.WriteString(text + "\n")
helper.Check(err)
}
func getDate(timestamp time.Time) string {
return timestamp.UTC().Format(time.RFC1123)
}
func getIpAddress(r *http.Request) string {
// Get IP from X-FORWARDED-FOR header
ips := r.Header.Get("X-FORWARDED-FOR")
splitIps := strings.Split(ips, ",")
for _, ip := range splitIps {
netIP := net.ParseIP(ip)
if netIP != nil {
return ip
}
}
// Get IP from the X-REAL-IP header
ip := r.Header.Get("X-REAL-IP")
netIP := net.ParseIP(ip)
if netIP != nil {
return ip
}
// Get IP from RemoteAddr
ip, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
return "Unknown IP"
}
netIP = net.ParseIP(ip)
if netIP != nil {
return ip
}
return "Unknown IP"
}