Add clipboard monitoring functionality to the agent. Implement clipboard event logging, including text and image capture, with support for retention policies. Update API to allow file deletion and enhance web interface for file management. Add tests for clipboard operations.
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user