Skip to content

Commit c238c24

Browse files
committed
monitor and fetch folder in real time and uplod it too
Signed-off-by: Vivek Kumar Sahu <[email protected]>
1 parent 2230f7e commit c238c24

File tree

10 files changed

+231
-11
lines changed

10 files changed

+231
-11
lines changed

cmd/transfer.go

+5
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ func init() {
7373
// processing mode: sequential or parallel
7474
transferCmd.Flags().String("processing-mode", "parallel", "processing strategy (parallel, sequential)")
7575

76+
// enable daemon mode
77+
transferCmd.Flags().BoolP("daemon", "d", false, "enable daemon mode")
78+
7679
transferCmd.Flags().BoolP("debug", "D", false, "enable debug logging")
7780

7881
// Manually register adapter flags for each adapter
@@ -134,6 +137,7 @@ func parseConfig(cmd *cobra.Command) (types.Config, error) {
134137
outputType, _ := cmd.Flags().GetString("output-adapter")
135138
dr, _ := cmd.Flags().GetBool("dry-run")
136139
processingMode, _ := cmd.Flags().GetString("processing-mode")
140+
daemon, _ := cmd.Flags().GetBool("daemon")
137141

138142
validInputAdapter := map[string]bool{"github": true, "folder": true}
139143
validOutputAdapter := map[string]bool{"interlynk": true, "folder": true, "dtrack": true}
@@ -177,6 +181,7 @@ func parseConfig(cmd *cobra.Command) (types.Config, error) {
177181
DestinationAdapter: outputType,
178182
DryRun: dr,
179183
ProcessingStrategy: processingMode,
184+
Daemon: daemon,
180185
}
181186

182187
return config, nil

pkg/adapter/factory.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,11 @@ func NewAdapter(ctx *tcontext.TransferMetadata, config types.Config) (map[types.
6363
switch types.AdapterType(config.SourceAdapter) {
6464

6565
case types.GithubAdapterType:
66-
adapters[types.InputAdapterRole] = &github.GitHubAdapter{Role: types.InputAdapterRole, ProcessingMode: processingMode}
66+
adapters[types.InputAdapterRole] = &github.GitHubAdapter{Role: types.InputAdapterRole, ProcessingMode: processingMode, Daemon: config.Daemon}
6767
inputAdp = "github"
6868

6969
case types.FolderAdapterType:
70-
adapters[types.InputAdapterRole] = &ifolder.FolderAdapter{Role: types.InputAdapterRole, ProcessingMode: processingMode}
70+
adapters[types.InputAdapterRole] = &ifolder.FolderAdapter{Role: types.InputAdapterRole, ProcessingMode: processingMode, Config: &ifolder.FolderConfig{Daemon: config.Daemon}}
7171
inputAdp = "folder"
7272

7373
// case types.InterlynkAdapterType:

pkg/engine/transfer.go

+16-3
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"github.com/interlynk-io/sbommv/pkg/converter"
2828
"github.com/interlynk-io/sbommv/pkg/iterator"
2929
"github.com/interlynk-io/sbommv/pkg/logger"
30+
"github.com/interlynk-io/sbommv/pkg/monitor"
3031
"github.com/interlynk-io/sbommv/pkg/tcontext"
3132
"github.com/interlynk-io/sbommv/pkg/types"
3233
"github.com/spf13/cobra"
@@ -72,11 +73,23 @@ func TransferRun(ctx context.Context, cmd *cobra.Command, config types.Config) e
7273

7374
logger.LogDebug(transferCtx.Context, "Output adapter instance config", "value", outputAdapterInstance)
7475

76+
var sbomIterator iterator.SBOMIterator
7577
// Fetch SBOMs lazily using the iterator
76-
sbomIterator, err := inputAdapterInstance.FetchSBOMs(transferCtx)
77-
if err != nil {
78-
return fmt.Errorf("failed to fetch SBOMs: %w", err)
78+
if config.Daemon {
79+
if ma, ok := inputAdapterInstance.(monitor.MonitorAdapter); ok {
80+
sbomIterator, err = ma.Monitor(transferCtx)
81+
} else {
82+
return fmt.Errorf("input adapter %s does not support daemon mode", config.SourceAdapter)
83+
}
84+
} else {
85+
sbomIterator, err = inputAdapterInstance.FetchSBOMs(transferCtx)
86+
if err != nil {
87+
return fmt.Errorf("failed to fetch SBOMs: %w", err)
88+
}
7989
}
90+
// if err != nil {
91+
// return fmt.Errorf("failed to fetch SBOMs: %w", err)
92+
// }
8093

8194
if config.DryRun {
8295
logger.LogDebug(transferCtx.Context, "Dry-run mode enabled: Displaying retrieved SBOMs", "values", config.DryRun)

pkg/monitor/monitor.go

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright 2025 Interlynk.io
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package monitor
16+
17+
import (
18+
"github.com/interlynk-io/sbommv/pkg/iterator"
19+
"github.com/interlynk-io/sbommv/pkg/tcontext"
20+
)
21+
22+
// MonitorAdapter only for input adapter
23+
type MonitorAdapter interface {
24+
// it watches sboms and triggers as soon as the new sbom comes
25+
Monitor(ctx *tcontext.TransferMetadata) (iterator.SBOMIterator, error)
26+
}

pkg/source/folder/adapter.go

+19-6
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import (
2828

2929
// FolderAdapter handles fetching SBOMs from folders
3030
type FolderAdapter struct {
31-
config *FolderConfig
31+
Config *FolderConfig
3232
Role types.AdapterRole // "input" or "output" adapter type
3333
Fetcher SBOMFetcher
3434
ProcessingMode types.ProcessingMode
@@ -83,9 +83,12 @@ func (f *FolderAdapter) ParseAndValidateParams(cmd *cobra.Command) error {
8383
return fmt.Errorf("invalid input adapter flag usage:\n %s\n\nUse 'sbommv transfer --help' for correct usage.", strings.Join(invalidFlags, "\n "))
8484
}
8585
var fetcher SBOMFetcher
86+
daemon := f.Config.Daemon
8687

87-
// SequentialFetcher
88-
if f.ProcessingMode == types.FetchSequential {
88+
if daemon {
89+
// daemon fether initialized
90+
fetcher = NewWatcherFetcher()
91+
} else if f.ProcessingMode == types.FetchSequential {
8992
fetcher = &SequentialFetcher{}
9093
} else if f.ProcessingMode == types.FetchParallel {
9194
fetcher = &ParallelFetcher{}
@@ -94,18 +97,28 @@ func (f *FolderAdapter) ParseAndValidateParams(cmd *cobra.Command) error {
9497
cfg := FolderConfig{
9598
FolderPath: folderPath,
9699
Recursive: folderRecurse,
100+
Daemon: daemon,
97101
}
98102

99-
f.config = &cfg
103+
f.Config = &cfg
100104
f.Fetcher = fetcher
101105

102106
return nil
103107
}
104108

105109
// FetchSBOMs initializes the Folder SBOM iterator using the unified method
106110
func (f *FolderAdapter) FetchSBOMs(ctx *tcontext.TransferMetadata) (iterator.SBOMIterator, error) {
107-
logger.LogDebug(ctx.Context, "Initializing SBOM fetching", "mode", f.config.ProcessingMode)
108-
return f.Fetcher.Fetch(ctx, f.config)
111+
logger.LogDebug(ctx.Context, "Initializing SBOM fetching", "mode", f.Config.ProcessingMode)
112+
return f.Fetcher.Fetch(ctx, f.Config)
113+
}
114+
115+
func (f *FolderAdapter) Monitor(ctx *tcontext.TransferMetadata) (iterator.SBOMIterator, error) {
116+
if !f.Config.Daemon {
117+
return nil, fmt.Errorf("daemon mode not enabled for folder adapter")
118+
}
119+
120+
logger.LogDebug(ctx.Context, "Initializing SBOM monitoring")
121+
return f.Fetcher.Fetch(ctx, f.Config)
109122
}
110123

111124
// OutputSBOMs should return an error since Folder does not support SBOM uploads

pkg/source/folder/config.go

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ type FolderConfig struct {
2121
FolderPath string
2222
Recursive bool
2323
ProcessingMode types.ProcessingMode
24+
Daemon bool
2425
}
2526

2627
func NewFolderConfig() *FolderConfig {

pkg/source/folder/iterator.go

+18
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package folder
1616

1717
import (
1818
"context"
19+
"fmt"
1920
"io"
2021

2122
"github.com/interlynk-io/sbommv/pkg/iterator"
@@ -45,3 +46,20 @@ func (it *FolderIterator) Next(ctx context.Context) (*iterator.SBOM, error) {
4546
it.index++
4647
return sbom, nil
4748
}
49+
50+
// watchiterator collects sbom on the real time via channel
51+
type WatcherIterator struct {
52+
sbomChan chan *iterator.SBOM
53+
}
54+
55+
func (it *WatcherIterator) Next(ctx context.Context) (*iterator.SBOM, error) {
56+
select {
57+
case sbom, ok := <-it.sbomChan:
58+
if !ok {
59+
return nil, fmt.Errorf("watcher channel closed")
60+
}
61+
return sbom, nil
62+
case <-ctx.Done():
63+
return nil, ctx.Err()
64+
}
65+
}

pkg/source/folder/watcher.go

+140
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// Copyright 2025 Interlynk.io
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
// -------------------------------------------------------------------------
15+
16+
package folder
17+
18+
import (
19+
"fmt"
20+
"log"
21+
"os"
22+
"path/filepath"
23+
24+
"github.com/fsnotify/fsnotify"
25+
"github.com/interlynk-io/sbommv/pkg/iterator"
26+
"github.com/interlynk-io/sbommv/pkg/logger"
27+
"github.com/interlynk-io/sbommv/pkg/sbom"
28+
"github.com/interlynk-io/sbommv/pkg/source"
29+
"github.com/interlynk-io/sbommv/pkg/tcontext"
30+
)
31+
32+
type WatcherFetcher struct{}
33+
34+
func NewWatcherFetcher() *WatcherFetcher {
35+
return &WatcherFetcher{}
36+
}
37+
38+
func (f *WatcherFetcher) Fetch(ctx *tcontext.TransferMetadata, config *FolderConfig) (iterator.SBOMIterator, error) {
39+
logger.LogDebug(ctx.Context, "Starting folder watcher", "path", config.FolderPath, "recurssive", config.ProcessingMode)
40+
41+
// Create new watcher.
42+
watcher, err := fsnotify.NewWatcher()
43+
if err != nil {
44+
return nil, fmt.Errorf("failed to create watcher: %w", err)
45+
}
46+
47+
sbomChan := make(chan *iterator.SBOM, 10)
48+
49+
// add to watch more sub-directories if recurssive is true
50+
err = filepath.Walk(config.FolderPath, func(path string, info os.FileInfo, err error) error {
51+
if err != nil {
52+
logger.LogError(ctx.Context, err, "Error accessing path", "path", path)
53+
return nil
54+
}
55+
if info.IsDir() {
56+
if !config.Recursive && path != config.FolderPath {
57+
return filepath.SkipDir
58+
}
59+
60+
// add it to the watcher
61+
if err := watcher.Add(path); err != nil {
62+
logger.LogError(ctx.Context, err, "Failed to watch directory", "path", path)
63+
} else {
64+
logger.LogDebug(ctx.Context, "Watching directory", "path", path)
65+
}
66+
}
67+
return nil
68+
})
69+
if err != nil {
70+
watcher.Close()
71+
return nil, fmt.Errorf("failed to walk directory: %w", err)
72+
}
73+
74+
// Start listening for events.
75+
go func() {
76+
defer watcher.Close()
77+
for {
78+
select {
79+
case event, ok := <-watcher.Events:
80+
if !ok {
81+
close(sbomChan)
82+
return
83+
}
84+
log.Println("event:", event)
85+
// if event.Has(fsnotify.Write) {
86+
// log.Println("modified file:", event.Name)
87+
// }
88+
89+
if event.Op&(fsnotify.Create|fsnotify.Write) != 0 && source.IsSBOMFile(event.Name) {
90+
content, err := os.ReadFile(event.Name)
91+
if err != nil {
92+
logger.LogError(ctx.Context, err, "Failed to read SBOM", "path", event.Name)
93+
continue
94+
}
95+
96+
primaryComp, err := sbom.ExtractPrimaryComponentName(content)
97+
if err != nil {
98+
logger.LogDebug(ctx.Context, "Failed to parse SBOM for primary component", "path", event.Name, "error", err)
99+
}
100+
101+
fileName := getFilePath(config.FolderPath, event.Name)
102+
logger.LogDebug(ctx.Context, "Detected SBOM", "file", fileName)
103+
sbomChan <- &iterator.SBOM{
104+
Data: content,
105+
Path: fileName,
106+
Namespace: primaryComp,
107+
}
108+
}
109+
case err, ok := <-watcher.Errors:
110+
if !ok {
111+
close(sbomChan)
112+
return
113+
}
114+
logger.LogError(ctx.Context, err, "Watcher error")
115+
116+
case <-ctx.Done():
117+
close(sbomChan)
118+
return
119+
}
120+
}
121+
}()
122+
123+
return &WatcherIterator{sbomChan: sbomChan}, nil
124+
}
125+
126+
// type WatcherIterator struct {
127+
// sbomChan chan *iterator.SBOM
128+
// }
129+
130+
// func (it *WatcherIterator) Next(ctx context.Context) (*iterator.SBOM, error) {
131+
// select {
132+
// case sbom, ok := <-it.sbomChan:
133+
// if !ok {
134+
// return nil, fmt.Errorf("watcher channel closed")
135+
// }
136+
// return sbom, nil
137+
// case <-ctx.Done():
138+
// return nil, ctx.Err()
139+
// }
140+
// }

pkg/source/github/adapter.go

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ type GitHubAdapter struct {
4646
GithubToken string
4747
Role types.AdapterRole
4848
ProcessingMode types.ProcessingMode
49+
Daemon bool
4950

5051
// Comma-separated list (e.g., "repo1,repo2")
5152
IncludeRepos []string

pkg/types/mvtypes.go

+3
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,7 @@ type Config struct {
2727

2828
// dry run mode
2929
DryRun bool
30+
31+
// daemon mode
32+
Daemon bool
3033
}

0 commit comments

Comments
 (0)