108 lines
3 KiB
Go
108 lines
3 KiB
Go
package winprint
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/vlog"
|
|
)
|
|
|
|
// Watcher polls a drop folder written by the spooler through a Local file
|
|
// port (Microsoft Print To PDF → C:\ProgramData\Verae\spool\fallback\verae-job.pdf).
|
|
// When a file is stable and can be opened exclusively it is moved into the
|
|
// per-user work dir under a unique name and handed to Process.
|
|
type Watcher struct {
|
|
Dir string
|
|
WorkDir string
|
|
Interval time.Duration
|
|
Process Processor
|
|
}
|
|
|
|
// Run blocks forever.
|
|
func (w *Watcher) Run() {
|
|
if w.Interval <= 0 {
|
|
w.Interval = time.Second
|
|
}
|
|
_ = os.MkdirAll(w.WorkDir, 0o700)
|
|
vlog.Printf("watch: polling %s every %s (work=%s)", w.Dir, w.Interval, w.WorkDir)
|
|
sizes := map[string]int64{}
|
|
warned := false
|
|
for {
|
|
ents, err := os.ReadDir(w.Dir)
|
|
if err != nil {
|
|
if !warned {
|
|
vlog.Errorf("watch: read %s: %v (fallback printer disabled until it exists)", w.Dir, err)
|
|
warned = true
|
|
}
|
|
time.Sleep(5 * time.Second)
|
|
continue
|
|
}
|
|
warned = false
|
|
seen := map[string]bool{}
|
|
for _, e := range ents {
|
|
if e.IsDir() || strings.HasPrefix(e.Name(), ".") || strings.HasSuffix(strings.ToLower(e.Name()), ".tmp") {
|
|
continue
|
|
}
|
|
p := filepath.Join(w.Dir, e.Name())
|
|
seen[p] = true
|
|
st, err := os.Stat(p)
|
|
if err != nil || st.Size() == 0 {
|
|
continue
|
|
}
|
|
prev, ok := sizes[p]
|
|
sizes[p] = st.Size()
|
|
if !ok || prev != st.Size() || time.Since(st.ModTime()) < 2*time.Second {
|
|
continue // still growing
|
|
}
|
|
w.claim(p, st.Size())
|
|
delete(sizes, p)
|
|
}
|
|
for p := range sizes {
|
|
if !seen[p] {
|
|
delete(sizes, p)
|
|
}
|
|
}
|
|
time.Sleep(w.Interval)
|
|
}
|
|
}
|
|
|
|
func (w *Watcher) claim(p string, size int64) {
|
|
id := vlog.NewJobID("watch")
|
|
lj := vlog.Job(id)
|
|
dst := filepath.Join(w.WorkDir, id+".pdf")
|
|
// Rename fails while the spooler still holds the file open (Windows sharing rules).
|
|
if err := os.Rename(p, dst); err != nil {
|
|
// cross-volume or locked: try copy+remove
|
|
b, rerr := os.ReadFile(p)
|
|
if rerr != nil {
|
|
lj.Debugf("watch: %s not ready: %v / %v", p, err, rerr)
|
|
return
|
|
}
|
|
if werr := os.WriteFile(dst, b, 0o600); werr != nil {
|
|
lj.Errorf("watch: copy to %s: %v", dst, werr)
|
|
return
|
|
}
|
|
if derr := os.Remove(p); derr != nil {
|
|
lj.Errorf("watch: copied but could not remove %s: %v (will re-detect; check ACLs)", p, derr)
|
|
}
|
|
}
|
|
lj.Printf("watch: claimed %s (%d bytes, magic=%q) → %s", p, size, sniff(dst), dst)
|
|
j := Job{ID: id, Source: "watch", Title: "Printed " + time.Now().Format("2006-01-02 1504"), Format: "application/pdf", Path: dst, Bytes: size, Received: time.Now()}
|
|
start := time.Now()
|
|
err := func() (err error) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
err = fmt.Errorf("panic: %v", r)
|
|
}
|
|
}()
|
|
return w.Process(j)
|
|
}()
|
|
if err != nil {
|
|
lj.Errorf("watch: job FAILED after %s: %v", time.Since(start).Round(time.Millisecond), err)
|
|
return
|
|
}
|
|
lj.Printf("watch: job completed in %s", time.Since(start).Round(time.Millisecond))
|
|
}
|