package bootstrap import ( "context" "errors" "fmt" "net" "net/http" "os" "time" "go.uber.org/zap" ) // ArtifactServer serves node bootstrap scripts and node binaries from the // configured artifact directory. It is intentionally static: iop-edge prepares // the directory, then serve exposes it without mutating the artifacts. type ArtifactServer struct { listen string dir string logger *zap.Logger server *http.Server } func NewArtifactServer(listen, dir string, logger *zap.Logger) *ArtifactServer { return &ArtifactServer{listen: listen, dir: dir, logger: logger} } func (s *ArtifactServer) Start(ctx context.Context) error { if s == nil || s.listen == "" || s.dir == "" { return nil } if _, err := os.Stat(s.dir); err != nil { if errors.Is(err, os.ErrNotExist) { s.logger.Warn("bootstrap artifact directory does not exist", zap.String("dir", s.dir)) } else { return fmt.Errorf("stat bootstrap artifact dir: %w", err) } } mux := http.NewServeMux() mux.Handle("/", http.FileServer(http.Dir(s.dir))) s.server = &http.Server{ Addr: s.listen, Handler: mux, ReadHeaderTimeout: 5 * time.Second, } ln, err := net.Listen("tcp", s.listen) if err != nil { return fmt.Errorf("bootstrap artifact server listen %s: %w", s.listen, err) } go func() { <-ctx.Done() shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _ = s.server.Shutdown(shutdownCtx) }() go func() { s.logger.Info("bootstrap artifact server listening", zap.String("addr", s.listen), zap.String("dir", s.dir)) if err := s.server.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) { s.logger.Warn("bootstrap artifact server exited", zap.Error(err)) } }() return nil } func (s *ArtifactServer) Stop(ctx context.Context) error { if s == nil || s.server == nil { return nil } return s.server.Shutdown(ctx) }