sources.skip_rules_file names a JSON file of filters, each with a
comment, an active flag, a logic mode ("or" by default, or "and") and
rules by buyer, title words or CPV codes. The fetcher applies them
before saving, so only new opportunities are marked; the marks of
stored ones are never touched. CPV rules match when the opportunity's
codes are all in the list, or any of them with exclusive false, and
by prefix with exact false.
The file is read again when its time or size changes: an invalid edit
is logged and the last good rules kept, and a deleted file means no
rules until it's back. fetcher service and serve refuse to start if it
is define but missing or invalid.
job-search skip-rules check validates a rules file, and skip-rules
apply marks the stored opportunities that match Skipped, or only lists
them with --dry-run.
93 lines
2.3 KiB
Go
93 lines
2.3 KiB
Go
package fetch
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"git.avsdev.uk/Go/job-search/internal/cli"
|
|
"git.avsdev.uk/Go/job-search/internal/fetcher"
|
|
)
|
|
|
|
func commandService(opts *cli.Options) *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "service",
|
|
Short: "Run every enabled source's fetcher on its interval until stopped",
|
|
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
|
slog.Debug("command persistent pre-run", "command", "service")
|
|
},
|
|
PersistentPostRun: func(cmd *cobra.Command, args []string) {
|
|
slog.Debug("command persistent post-run", "command", "service")
|
|
},
|
|
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
|
slog.Debug("command run", "command", "service")
|
|
|
|
foreground, _ := cmd.Flags().GetBool("foreground")
|
|
|
|
conf, err := opts.RunSetup(cmd, true, foreground)
|
|
if err != nil {
|
|
return fmt.Errorf("load configuration: %w", err)
|
|
}
|
|
|
|
since, sinceErr := cmd.Flags().GetTime("since")
|
|
if sinceErr != nil {
|
|
return sinceErr
|
|
}
|
|
|
|
// ctx is cancelled on the first SIGINT (Ctrl+C) or SIGTERM.
|
|
ctx, stop := signal.NotifyContext(
|
|
context.Background(), os.Interrupt, syscall.SIGTERM,
|
|
)
|
|
defer stop()
|
|
|
|
// After the first signal, restore default signal handling so a second
|
|
// Ctrl+C force-kills the process if graceful shutdown hangs.
|
|
go func() {
|
|
<-ctx.Done()
|
|
stop()
|
|
}()
|
|
|
|
fetcherOptions, err := cli.SkipRulesOptions(conf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
opportunityStore, openErr := cli.OpenConfiguredStore(ctx, conf)
|
|
switch {
|
|
case errors.Is(openErr, cli.ErrNoDatabase):
|
|
slog.Warn("not storing fetched opportunities", "reason", openErr)
|
|
case openErr != nil:
|
|
return openErr
|
|
default:
|
|
defer func() {
|
|
err = errors.Join(err, opportunityStore.Close())
|
|
}()
|
|
fetcherOptions = append(
|
|
fetcherOptions, fetcher.WithStore(opportunityStore),
|
|
)
|
|
}
|
|
|
|
return fetcher.New(&conf.Sources, fetcherOptions...).Run(ctx, since)
|
|
},
|
|
}
|
|
cmd.Flags().Time(
|
|
"since",
|
|
time.Time{},
|
|
[]string{
|
|
time.DateOnly, time.DateTime,
|
|
},
|
|
"contract updates published since YYYY-MM-DD[ hh:mm:ss] "+
|
|
"(default: each source's newest stored release, or the last day)",
|
|
)
|
|
cmd.Flags().Bool("foreground", false, "run in the foreground")
|
|
|
|
return cmd
|
|
}
|