154 lines
3.0 KiB
Go
154 lines
3.0 KiB
Go
package clipmon
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"crypto/rand"
|
||
|
|
"encoding/hex"
|
||
|
|
"fmt"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"strings"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
const maxLoggedText = 1 << 16 // ponytail: 64KB cap per clipboard text entry
|
||
|
|
|
||
|
|
type Writer struct {
|
||
|
|
dir string
|
||
|
|
imagesDir string
|
||
|
|
file *os.File
|
||
|
|
bucket string
|
||
|
|
}
|
||
|
|
|
||
|
|
func NewWriter(dir string) (*Writer, error) {
|
||
|
|
imagesDir := filepath.Join(dir, "images")
|
||
|
|
if err := os.MkdirAll(imagesDir, 0o700); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
return &Writer{dir: dir, imagesDir: imagesDir}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (w *Writer) Write(event Event) error {
|
||
|
|
bucket := HourBucket(event.Time)
|
||
|
|
if bucket != w.bucket {
|
||
|
|
if err := w.rotate(bucket); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
}
|
||
|
|
_, err := w.file.WriteString(formatLine(event))
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
func (w *Writer) SavePNG(data []byte) (string, error) {
|
||
|
|
name, err := randomName(".png")
|
||
|
|
if err != nil {
|
||
|
|
return "", err
|
||
|
|
}
|
||
|
|
path := filepath.Join(w.imagesDir, name)
|
||
|
|
if err := os.WriteFile(path, data, 0o600); err != nil {
|
||
|
|
return "", fmt.Errorf("write image: %w", err)
|
||
|
|
}
|
||
|
|
return filepath.ToSlash(filepath.Join("images", name)), nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (w *Writer) rotate(bucket string) error {
|
||
|
|
if w.file != nil {
|
||
|
|
if err := w.file.Close(); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
w.file = nil
|
||
|
|
}
|
||
|
|
path := filepath.Join(w.dir, LogFilename(bucket))
|
||
|
|
file, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
w.file = file
|
||
|
|
w.bucket = bucket
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (w *Writer) Close() error {
|
||
|
|
if w.file == nil {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
err := w.file.Close()
|
||
|
|
w.file = nil
|
||
|
|
w.bucket = ""
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
func TrimText(text string) string {
|
||
|
|
if len(text) <= maxLoggedText {
|
||
|
|
return text
|
||
|
|
}
|
||
|
|
return text[:maxLoggedText] + "\n...(truncated)"
|
||
|
|
}
|
||
|
|
|
||
|
|
func randomName(ext string) (string, error) {
|
||
|
|
var buf [16]byte
|
||
|
|
if _, err := rand.Read(buf[:]); err != nil {
|
||
|
|
return "", err
|
||
|
|
}
|
||
|
|
if !strings.HasPrefix(ext, ".") {
|
||
|
|
ext = "." + ext
|
||
|
|
}
|
||
|
|
return hex.EncodeToString(buf[:]) + ext, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func PruneOld(dir string, retentionDays int) error {
|
||
|
|
if retentionDays <= 0 {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
cutoff := time.Now().AddDate(0, 0, -retentionDays)
|
||
|
|
if err := pruneLogs(dir, cutoff); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
return pruneImages(filepath.Join(dir, "images"), cutoff)
|
||
|
|
}
|
||
|
|
|
||
|
|
func pruneLogs(dir string, cutoff time.Time) error {
|
||
|
|
entries, err := os.ReadDir(dir)
|
||
|
|
if err != nil {
|
||
|
|
if os.IsNotExist(err) {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
for _, entry := range entries {
|
||
|
|
if entry.IsDir() || !ValidLogFilename(entry.Name()) {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
info, err := entry.Info()
|
||
|
|
if err != nil {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if info.ModTime().Before(cutoff) {
|
||
|
|
_ = os.Remove(filepath.Join(dir, entry.Name()))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func pruneImages(dir string, cutoff time.Time) error {
|
||
|
|
entries, err := os.ReadDir(dir)
|
||
|
|
if err != nil {
|
||
|
|
if os.IsNotExist(err) {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
for _, entry := range entries {
|
||
|
|
if entry.IsDir() {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
info, err := entry.Info()
|
||
|
|
if err != nil {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if info.ModTime().Before(cutoff) {
|
||
|
|
_ = os.Remove(filepath.Join(dir, entry.Name()))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|