From 2a86488732814debbbae902a349bab621f7a369c Mon Sep 17 00:00:00 2001 From: Vivek Kumar Sahu Date: Mon, 17 Feb 2025 13:54:16 +0530 Subject: [PATCH 1/4] added folder system as a input adapter Signed-off-by: Vivek Kumar Sahu --- cmd/transfer.go | 9 +- docs/how_to_plugin_new_adapter.md | 106 +++++++++++++++++ pkg/adapter/factory.go | 4 + pkg/source/folder/adapter.go | 186 ++++++++++++++++++++++++++++++ pkg/source/folder/fetchMode.go | 181 +++++++++++++++++++++++++++++ pkg/source/folder/iterator.go | 47 ++++++++ pkg/source/utils.go | 21 ++-- pkg/types/adapter_types.go | 1 + 8 files changed, 547 insertions(+), 8 deletions(-) create mode 100644 docs/how_to_plugin_new_adapter.md create mode 100644 pkg/source/folder/adapter.go create mode 100644 pkg/source/folder/fetchMode.go create mode 100644 pkg/source/folder/iterator.go diff --git a/cmd/transfer.go b/cmd/transfer.go index 9ebc3a2..c296c19 100644 --- a/cmd/transfer.go +++ b/cmd/transfer.go @@ -19,6 +19,8 @@ import ( "fmt" "github.com/interlynk-io/sbommv/pkg/engine" + ifolder "github.com/interlynk-io/sbommv/pkg/source/folder" + "github.com/interlynk-io/sbommv/pkg/source/github" "github.com/interlynk-io/sbommv/pkg/target/interlynk" "github.com/interlynk-io/sbommv/pkg/types" @@ -69,6 +71,11 @@ func registerAdapterFlags(cmd *cobra.Command) { githubAdapter := &github.GitHubAdapter{} githubAdapter.AddCommandParams(cmd) + // Register Folder Adapter Flags + // Register Input Folder Adapter Flags + folderInputAdapter := &ifolder.FolderAdapter{} + folderInputAdapter.AddCommandParams(cmd) + // Register Interlynk Adapter Flags interlynkAdapter := &interlynk.InterlynkAdapter{} interlynkAdapter.AddCommandParams(cmd) @@ -109,7 +116,7 @@ func parseConfig(cmd *cobra.Command) (types.Config, error) { outputType, _ := cmd.Flags().GetString("output-adapter") dr, _ := cmd.Flags().GetBool("dry-run") - validInputAdapter := map[string]bool{"github": true} + validInputAdapter := map[string]bool{"github": true, "folder": true} validOutputAdapter := map[string]bool{"interlynk": true} // Custom validation for required flags diff --git a/docs/how_to_plugin_new_adapter.md b/docs/how_to_plugin_new_adapter.md new file mode 100644 index 0000000..1668e8a --- /dev/null +++ b/docs/how_to_plugin_new_adapter.md @@ -0,0 +1,106 @@ +# πŸ“– Writing a New Adapter for sbommv + +## Understanding Adapters in sbommv + +`sbommv` follows a pluggable architecture where adapters act as interfaces between SBOM sources and destinations. + +- **Input Adapters** β†’ Fetch SBOMs (e.g., from GitHub, Folder, etc.). +- **Output Adapters** β†’ Send SBOMs (e.g., to Interlynk, Folder, etc.). +- **Each Adapter Implements the** `Adapter` **Interface** β†’ This ensures a common API across different sources & destinations. + +## Implementing a New Adapter + +To add a new adapter, follow these steps: + +### Step 1: Define a Struct for Your Adapter + +Each adapter has its own struct. This struct will hold relevant configuration details. + +For **FolderAdapter**, we define: + +```go +// FolderAdapter struct represents an adapter for local folder storage +type FolderAdapter struct { + Role types.AdapterRole + FolderPath string + Recursive bool +} +``` + +πŸ’‘ **Note**: + +- **Role**: Defines whether the adapter is for input or output. +- **FolderPath**: Directory to scan or store SBOMs. +- **Recursive**: If true, scans subdirectories when acting as an input adapter. + +### Step 2: Implement `AddCommandParams` + +- This method adds CLI flags related to the adapter. + +```go +// AddCommandParams adds folder adapter-specific CLI flags +func (f *FolderAdapter) AddCommandParams(cmd *cobra.Command) { +// implementation code +} +``` + +- This ensures the correct flags are registered depending on the adapter role. + +### Step 3: Implement `ParseAndValidateParams` + +- This method validates and extracts CLI parameters. + +```go +// ParseAndValidateParams extracts and validates folder adapter parameters +func (f *FolderAdapter) ParseAndValidateParams(cmd *cobra.Command) error { +// implementation code +} +``` + +- This ensures we have valid folder paths before proceeding. + +### Step 4: Implement `FetchSBOMs` for Input Adapter + +- This method scans a folder, detects SBOMs, and returns an iterator. + +```go +// FetchSBOMs retrieves SBOMs from the specified folder +func (f *FolderAdapter) FetchSBOMs(ctx *tcontext.TransferMetadata) (iterator.SBOMIterator, error) { +// implementation code +} +``` + +- This method: + + - Scans the directory recursively, if resursive flag is `true`. + - Detects SBOMs using utils.IsValidSBOM(). + - Returns an iterator for processing SBOMs. + +### Step 5: Implement UploadSBOMs for Output Adapter + +- This method saves SBOMs to the specified folder. + +```go +// UploadSBOMs writes SBOMs to the specified folder +func (f *FolderAdapter) UploadSBOMs(ctx *tcontext.TransferMetadata, it iterator.SBOMIterator) error { + // implementation code +} +``` + +- This method: + - Creates a folder if it doesn’t exist. + - Writes SBOMs using either their original filename or a generated UUID. + +### Step 6: Implement DryRun + +```go +// DryRun simulates fetching or uploading SBOMs +func (f *FolderAdapter) DryRun(ctx *tcontext.TransferMetadata, it iterator.SBOMIterator) error { +// implementation code +} +``` + +- This method: + + - In input mode, lists detected SBOMs. + - In output mode, shows where SBOMs will be saved. diff --git a/pkg/adapter/factory.go b/pkg/adapter/factory.go index 4fed7e9..cbabd74 100644 --- a/pkg/adapter/factory.go +++ b/pkg/adapter/factory.go @@ -20,6 +20,7 @@ import ( "github.com/interlynk-io/sbommv/pkg/iterator" "github.com/interlynk-io/sbommv/pkg/logger" + "github.com/interlynk-io/sbommv/pkg/source/folder" "github.com/interlynk-io/sbommv/pkg/source/github" "github.com/interlynk-io/sbommv/pkg/target/interlynk" "github.com/interlynk-io/sbommv/pkg/tcontext" @@ -58,6 +59,9 @@ func NewAdapter(ctx *tcontext.TransferMetadata, config types.Config) (map[types. case types.GithubAdapterType: adapters[types.InputAdapterRole] = &github.GitHubAdapter{Role: types.InputAdapterRole} + case types.FolderAdapterType: + adapters[types.InputAdapterRole] = &folder.FolderAdapter{Role: types.InputAdapterRole} + case types.InterlynkAdapterType: adapters[types.InputAdapterRole] = &interlynk.InterlynkAdapter{Role: types.InputAdapterRole} diff --git a/pkg/source/folder/adapter.go b/pkg/source/folder/adapter.go new file mode 100644 index 0000000..c0350f1 --- /dev/null +++ b/pkg/source/folder/adapter.go @@ -0,0 +1,186 @@ +// Copyright 2025 Interlynk.io +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package folder + +import ( + "fmt" + "io" + "strings" + + "github.com/interlynk-io/sbommv/pkg/iterator" + "github.com/interlynk-io/sbommv/pkg/logger" + "github.com/interlynk-io/sbommv/pkg/sbom" + "github.com/interlynk-io/sbommv/pkg/tcontext" + "github.com/interlynk-io/sbommv/pkg/types" + "github.com/spf13/cobra" +) + +// FolderAdapter handles fetching SBOMs from folders +type FolderAdapter struct { + FolderPath string // Folder path where SBOMs exist or will be stored + Recursive bool // Scan subdirectories (for input mode) + ProcessingMode types.ProcessingMode + + Role types.AdapterRole // "input" or "output" adapter type +} + +// AddCommandParams adds Folder-specific CLI flags +func (f *FolderAdapter) AddCommandParams(cmd *cobra.Command) { + cmd.Flags().String("in-folder-path", "", "Folder path") + cmd.Flags().Bool("in-folder-recursive", false, "Folder recurssive (default: false)") + cmd.Flags().String("in-folder-processing-mode", "sequential", "Folder processing mode (sequential/parallel)") +} + +// ParseAndValidateParams validates the Folder adapter params +func (f *FolderAdapter) ParseAndValidateParams(cmd *cobra.Command) error { + var ( + pathFlag, recursiveFlag, processingModeFlag string + missingFlags []string + invalidFlags []string + ) + + switch f.Role { + case types.InputAdapterRole: + pathFlag = "in-folder-path" + recursiveFlag = "in-folder-recursive" + processingModeFlag = "in-folder-processing-mode" + + case types.OutputAdapterRole: + return fmt.Errorf("The Folder adapter doesn't support output adapter functionalities.") + + default: + return fmt.Errorf("The adapter is neither an input type nor an output type") + + } + + // Extract Folder Path + folderPath, _ := cmd.Flags().GetString(pathFlag) + if folderPath == "" { + missingFlags = append(missingFlags, "--"+pathFlag) + } + + // Extract Folder Path + folderRecurse, _ := cmd.Flags().GetBool(recursiveFlag) + + validModes := map[string]bool{"sequential": true, "parallel": true} + + // Extract the processing mode: sequential/parallel + mode, _ := cmd.Flags().GetString(processingModeFlag) + if !validModes[mode] { + invalidFlags = append(invalidFlags, fmt.Sprintf("%s=%s (must be one of: sequential, parallel mode)", processingModeFlag, mode)) + } + + // Validate required flags + if len(missingFlags) > 0 { + return fmt.Errorf("missing input adapter required flags: %v\n\nUse 'sbommv transfer --help' for usage details.", missingFlags) + } + + // Validate incorrect flag usage + if len(invalidFlags) > 0 { + return fmt.Errorf("invalid input adapter flag usage:\n %s\n\nUse 'sbommv transfer --help' for correct usage.", strings.Join(invalidFlags, "\n ")) + } + + f.FolderPath = folderPath + f.Recursive = folderRecurse + f.ProcessingMode = types.ProcessingMode(mode) + + return nil +} + +// FetchSBOMs initializes the Folder SBOM iterator using the unified method +func (f *FolderAdapter) FetchSBOMs(ctx *tcontext.TransferMetadata) (iterator.SBOMIterator, error) { + logger.LogDebug(ctx.Context, "Initializing SBOM fetching process") + logger.LogDebug(ctx.Context, "Scanning folder for SBOMs", "path", f.FolderPath, "recursive", f.Recursive) + + var sbomIterator iterator.SBOMIterator + var err error + + switch f.ProcessingMode { + case types.FetchParallel: + sbomIterator, err = f.fetchSBOMsConcurrently(ctx) + case types.FetchSequential: + sbomIterator, err = f.fetchSBOMsSequentially(ctx) + default: + return nil, fmt.Errorf("Unsupported Processing Mode !!") + } + + if err != nil { + return nil, fmt.Errorf("failed to fetch SBOMs: %w", err) + } + + return sbomIterator, nil +} + +// OutputSBOMs should return an error since Folder does not support SBOM uploads +func (f *FolderAdapter) UploadSBOMs(ctx *tcontext.TransferMetadata, iterator iterator.SBOMIterator) error { + return fmt.Errorf("Folder adapter does not support SBOM uploading") +} + +// DryRun for Folder Adapter: Displays all fetched SBOMs from folder adapter +func (f *FolderAdapter) DryRun(ctx *tcontext.TransferMetadata, iterator iterator.SBOMIterator) error { + logger.LogDebug(ctx.Context, "Dry-run mode: Displaying SBOMs fetched from folder input adapter") + + var outputDir string + var verbose bool + + processor := sbom.NewSBOMProcessor(outputDir, verbose) + sbomCount := 0 + fmt.Println() + fmt.Printf("πŸ“¦ Details of all Fetched SBOMs by Folder Input Adapter\n") + + for { + + sbom, err := iterator.Next(ctx.Context) + if err == io.EOF { + break // No more sboms + } + + if err != nil { + logger.LogError(ctx.Context, err, "Error retrieving SBOM from iterator") + } + + // update processor with current SBOM data + processor.Update(sbom.Data, "", sbom.Path) + + doc, err := processor.ProcessSBOMs() + if err != nil { + logger.LogError(ctx.Context, err, "Failed to process SBOM") + continue + } + + // if outputDir is provided, save the SBOM file + if outputDir != "" { + if err := processor.WriteSBOM(doc, ""); err != nil { + logger.LogError(ctx.Context, err, "Failed to write SBOM to output directory") + } + } + + // Print SBOM content if verbose mode is enabled + if verbose { + fmt.Println("\n-------------------- πŸ“œ SBOM Content --------------------") + fmt.Printf("πŸ“‚ Filename: %s\n", doc.Filename) + fmt.Printf("πŸ“¦ Format: %s | SpecVersion: %s\n\n", doc.Format, doc.SpecVersion) + fmt.Println(string(doc.Content)) + fmt.Println("------------------------------------------------------") + fmt.Println() + } + + sbomCount++ + fmt.Printf(" - πŸ“ Folder: %s | Format: %s | SpecVersion: %s | Filename: %s \n", sbom.Namespace, doc.Format, doc.SpecVersion, doc.Filename) + + } + + return nil +} diff --git a/pkg/source/folder/fetchMode.go b/pkg/source/folder/fetchMode.go new file mode 100644 index 0000000..1843658 --- /dev/null +++ b/pkg/source/folder/fetchMode.go @@ -0,0 +1,181 @@ +// Copyright 2025 Interlynk.io +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package folder + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "sync" + + "github.com/interlynk-io/sbommv/pkg/iterator" + "github.com/interlynk-io/sbommv/pkg/logger" + "github.com/interlynk-io/sbommv/pkg/source" + "github.com/interlynk-io/sbommv/pkg/tcontext" +) + +// fetchSBOMsSequentially scans the folder for SBOMs one-by-one +// 1. Walks through the folder file-by-file +// 2. Detects valid SBOMs using source.IsSBOMFile(). +// 3. Reads the content & adds it to the iterator along with path. +func (f *FolderAdapter) fetchSBOMsSequentially(ctx *tcontext.TransferMetadata) (iterator.SBOMIterator, error) { + logger.LogDebug(ctx.Context, "Scanning folder sequentially for SBOMs", "path", f.FolderPath, "recursive", f.Recursive) + + var sbomList []*iterator.SBOM + + err := filepath.Walk(f.FolderPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + logger.LogError(ctx.Context, err, "Error accessing file", "path", path) + return nil + } + + // Skip directories (except the root folder) + if info.IsDir() && !f.Recursive && path != f.FolderPath { + return filepath.SkipDir + } + fmt.Println("path", path) + + // Check if the file is a valid SBOM + if source.IsSBOMFile(path) { + content, err := os.ReadFile(path) + if err != nil { + logger.LogError(ctx.Context, err, "Failed to read SBOM", "path", path) + return nil + } + + // Extract project name from the top-level directory + projectName := getTopLevelDir(f.FolderPath, path) + + sbomList = append(sbomList, &iterator.SBOM{ + Data: content, + // Format: utils.DetectSBOMFormat(content), + Path: path, + Namespace: projectName, + }) + + logger.LogDebug(ctx.Context, "SBOM Detected", "file", path) + } + + return nil + }) + if err != nil { + return nil, fmt.Errorf("error scanning folder: %w", err) + } + + logger.LogDebug(ctx.Context, "Total SBOMs fetched (Sequential Mode)", "count", len(sbomList)) + + if len(sbomList) == 0 { + return nil, fmt.Errorf("no SBOMs found in the specified folder") + } + + return NewFolderIterator(sbomList), nil +} + +// fetchSBOMsConcurrently scans the folder for SBOMs using parallel processing +// 1. Walks through the folder file-by-file. +// 2. Launch a goroutine for each file. +// 3. Detects valid SBOMs using source.IsSBOMFile(). +// 4. Uses channels to store SBOMs & errors. +// 5. Reads the content & adds it to the iterator along with path. +func (f *FolderAdapter) fetchSBOMsConcurrently(ctx *tcontext.TransferMetadata) (iterator.SBOMIterator, error) { + logger.LogDebug(ctx.Context, "Using PARALLEL processing mode") + + var wg sync.WaitGroup + sbomsChan := make(chan *iterator.SBOM, 100) + errChan := make(chan error, 10) + + // Walk the folder and process files in parallel + err := filepath.Walk(f.FolderPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + errChan <- fmt.Errorf("error accessing file %s: %w", path, err) + return nil + } + + fmt.Println("path", path) + // Skip directories (except root folder) + if info.IsDir() && !f.Recursive && path != f.FolderPath { + return filepath.SkipDir + } + + // Launch a goroutine for each file + wg.Add(1) + + go func(path string) { + defer wg.Done() + + if source.IsSBOMFile(path) { + content, err := os.ReadFile(path) + if err != nil { + logger.LogError(ctx.Context, err, "Failed to read SBOM", "path", path) + errChan <- err + return + } + + // Extract project name from the top-level directory + projectName := getTopLevelDir(f.FolderPath, path) + + sbomsChan <- &iterator.SBOM{ + Data: content, + Path: path, + Namespace: projectName, + } + + } + }(path) + return nil + }) + + // Close channels after all goroutines complete + go func() { + wg.Wait() + close(sbomsChan) + close(errChan) + }() + + // Collect SBOMs from channel + var sboms []*iterator.SBOM + for sbom := range sbomsChan { + sboms = append(sboms, sbom) + } + + // Check for errors + for err := range errChan { + logger.LogError(ctx.Context, err, "Error processing files in parallel mode") + } + + if err != nil { + return nil, fmt.Errorf("error scanning folder: %w", err) + } + + logger.LogDebug(ctx.Context, "Total SBOMs fetched (Parallel Mode)", "count", len(sboms)) + return iterator.NewMemoryIterator(sboms), nil +} + +// getTopLevelDir extracts the top-level directory from a given path +func getTopLevelDir(basePath, fullPath string) string { + relPath, err := filepath.Rel(basePath, fullPath) + if err != nil { + return "unknown" // Fallback in case of an error + } + + // Split the relative path and return the first directory + parts := strings.Split(relPath, string(filepath.Separator)) + if len(parts) > 1 { + return parts[0] // Return the top-level folder (e.g., "cdx" or "spdx") + } + + return "unknown" +} diff --git a/pkg/source/folder/iterator.go b/pkg/source/folder/iterator.go new file mode 100644 index 0000000..997ee49 --- /dev/null +++ b/pkg/source/folder/iterator.go @@ -0,0 +1,47 @@ +// Copyright 2025 Interlynk.io +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package folder + +import ( + "context" + "io" + + "github.com/interlynk-io/sbommv/pkg/iterator" +) + +// FolderIterator iterates over SBOMs found in a folder +type FolderIterator struct { + sboms []*iterator.SBOM + index int +} + +// NewFolderIterator initializes and returns a new FolderIterator +func NewFolderIterator(sboms []*iterator.SBOM) *FolderIterator { + return &FolderIterator{ + sboms: sboms, + index: 0, + } +} + +// Next retrieves the next SBOM in the iteration +func (it *FolderIterator) Next(ctx context.Context) (*iterator.SBOM, error) { + if it.index >= len(it.sboms) { + return nil, io.EOF + } + + sbom := it.sboms[it.index] + it.index++ + return sbom, nil +} diff --git a/pkg/source/utils.go b/pkg/source/utils.go index 814aa47..2bb2a65 100644 --- a/pkg/source/utils.go +++ b/pkg/source/utils.go @@ -17,6 +17,7 @@ package source import ( "encoding/base64" "fmt" + "regexp" "strings" ) @@ -24,14 +25,12 @@ import ( func IsSBOMFile(name string) bool { name = strings.ToLower(name) - // Common SBOM file patterns + // Extended SBOM patterns for better detection patterns := []string{ - ".spdx.", - ".sbom", - "bom.", - "cyclonedx", - "spdx", - ".cdx.", + ".spdx.", "spdx-", "spdx_", "spdx.", + ".sbom", "sbom-", "sbom_", "sbom.", + "bom.", "bom-", "bom_", + "cyclonedx", "cdx-", "cdx_", "cdx.", } // Common SBOM file extensions @@ -44,6 +43,14 @@ func IsSBOMFile(name string) bool { ".txt", // for SPDX tag-value } + // Regular expression for detecting known SBOM file naming conventions + sbomRegex := regexp.MustCompile(`(sbom|bom|spdx|cdx)[-_\.].+\.(json|xml|yaml|yml|txt)$`) + + // Check if name matches the regex pattern + if sbomRegex.MatchString(name) { + return true + } + // Check if name contains any SBOM pattern hasPattern := false for _, pattern := range patterns { diff --git a/pkg/types/adapter_types.go b/pkg/types/adapter_types.go index a70ea8a..50d65ab 100644 --- a/pkg/types/adapter_types.go +++ b/pkg/types/adapter_types.go @@ -27,6 +27,7 @@ type AdapterType string const ( GithubAdapterType AdapterType = "github" InterlynkAdapterType AdapterType = "interlynk" + FolderAdapterType AdapterType = "folder" ) type ProcessingMode string From e2f5c9ac6182ff97aa505e793ea23f8558604580 Mon Sep 17 00:00:00 2001 From: Vivek Kumar Sahu Date: Mon, 17 Feb 2025 18:31:46 +0530 Subject: [PATCH 2/4] added folder system as a output adapter Signed-off-by: Vivek Kumar Sahu --- cmd/transfer.go | 8 +- go.mod | 1 + go.sum | 2 + pkg/adapter/factory.go | 10 +- pkg/source/folder/fetchMode.go | 21 ++-- pkg/target/folder/adapter.go | 206 +++++++++++++++++++++++++++++++++ 6 files changed, 233 insertions(+), 15 deletions(-) create mode 100644 pkg/target/folder/adapter.go diff --git a/cmd/transfer.go b/cmd/transfer.go index c296c19..f153fe2 100644 --- a/cmd/transfer.go +++ b/cmd/transfer.go @@ -20,6 +20,7 @@ import ( "github.com/interlynk-io/sbommv/pkg/engine" ifolder "github.com/interlynk-io/sbommv/pkg/source/folder" + ofolder "github.com/interlynk-io/sbommv/pkg/target/folder" "github.com/interlynk-io/sbommv/pkg/source/github" "github.com/interlynk-io/sbommv/pkg/target/interlynk" @@ -71,7 +72,6 @@ func registerAdapterFlags(cmd *cobra.Command) { githubAdapter := &github.GitHubAdapter{} githubAdapter.AddCommandParams(cmd) - // Register Folder Adapter Flags // Register Input Folder Adapter Flags folderInputAdapter := &ifolder.FolderAdapter{} folderInputAdapter.AddCommandParams(cmd) @@ -80,6 +80,10 @@ func registerAdapterFlags(cmd *cobra.Command) { interlynkAdapter := &interlynk.InterlynkAdapter{} interlynkAdapter.AddCommandParams(cmd) + // Register Output Folder Adapter Flags + folderOutputAdapter := &ofolder.FolderAdapter{} + folderOutputAdapter.AddCommandParams(cmd) + // similarly for all other Adapters } @@ -117,7 +121,7 @@ func parseConfig(cmd *cobra.Command) (types.Config, error) { dr, _ := cmd.Flags().GetBool("dry-run") validInputAdapter := map[string]bool{"github": true, "folder": true} - validOutputAdapter := map[string]bool{"interlynk": true} + validOutputAdapter := map[string]bool{"interlynk": true, "folder": true} // Custom validation for required flags missingFlags := []string{} diff --git a/go.mod b/go.mod index 1e5857f..a4beed3 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require github.com/google/go-cmp v0.6.0 // indirect require ( github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/google/uuid v1.6.0 github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/magiconair/properties v1.8.7 // indirect diff --git a/go.sum b/go.sum index eb8d772..4200f43 100644 --- a/go.sum +++ b/go.sum @@ -9,6 +9,8 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= diff --git a/pkg/adapter/factory.go b/pkg/adapter/factory.go index cbabd74..c7b5c7b 100644 --- a/pkg/adapter/factory.go +++ b/pkg/adapter/factory.go @@ -20,7 +20,9 @@ import ( "github.com/interlynk-io/sbommv/pkg/iterator" "github.com/interlynk-io/sbommv/pkg/logger" - "github.com/interlynk-io/sbommv/pkg/source/folder" + ofolder "github.com/interlynk-io/sbommv/pkg/target/folder" + + ifolder "github.com/interlynk-io/sbommv/pkg/source/folder" "github.com/interlynk-io/sbommv/pkg/source/github" "github.com/interlynk-io/sbommv/pkg/target/interlynk" "github.com/interlynk-io/sbommv/pkg/tcontext" @@ -60,7 +62,7 @@ func NewAdapter(ctx *tcontext.TransferMetadata, config types.Config) (map[types. adapters[types.InputAdapterRole] = &github.GitHubAdapter{Role: types.InputAdapterRole} case types.FolderAdapterType: - adapters[types.InputAdapterRole] = &folder.FolderAdapter{Role: types.InputAdapterRole} + adapters[types.InputAdapterRole] = &ifolder.FolderAdapter{Role: types.InputAdapterRole} case types.InterlynkAdapterType: adapters[types.InputAdapterRole] = &interlynk.InterlynkAdapter{Role: types.InputAdapterRole} @@ -76,8 +78,8 @@ func NewAdapter(ctx *tcontext.TransferMetadata, config types.Config) (map[types. switch types.AdapterType(config.DestinationType) { - case types.GithubAdapterType: - adapters[types.OutputAdapterRole] = &github.GitHubAdapter{Role: types.OutputAdapterRole} + case types.FolderAdapterType: + adapters[types.OutputAdapterRole] = &ofolder.FolderAdapter{Role: types.OutputAdapterRole} case types.InterlynkAdapterType: adapters[types.OutputAdapterRole] = &interlynk.InterlynkAdapter{Role: types.OutputAdapterRole} diff --git a/pkg/source/folder/fetchMode.go b/pkg/source/folder/fetchMode.go index 1843658..c0114f4 100644 --- a/pkg/source/folder/fetchMode.go +++ b/pkg/source/folder/fetchMode.go @@ -46,7 +46,6 @@ func (f *FolderAdapter) fetchSBOMsSequentially(ctx *tcontext.TransferMetadata) ( if info.IsDir() && !f.Recursive && path != f.FolderPath { return filepath.SkipDir } - fmt.Println("path", path) // Check if the file is a valid SBOM if source.IsSBOMFile(path) { @@ -57,7 +56,7 @@ func (f *FolderAdapter) fetchSBOMsSequentially(ctx *tcontext.TransferMetadata) ( } // Extract project name from the top-level directory - projectName := getTopLevelDir(f.FolderPath, path) + projectName, path := getTopLevelDirAndFile(f.FolderPath, path) sbomList = append(sbomList, &iterator.SBOM{ Data: content, @@ -125,7 +124,7 @@ func (f *FolderAdapter) fetchSBOMsConcurrently(ctx *tcontext.TransferMetadata) ( } // Extract project name from the top-level directory - projectName := getTopLevelDir(f.FolderPath, path) + projectName, path := getTopLevelDirAndFile(f.FolderPath, path) sbomsChan <- &iterator.SBOM{ Data: content, @@ -164,18 +163,22 @@ func (f *FolderAdapter) fetchSBOMsConcurrently(ctx *tcontext.TransferMetadata) ( return iterator.NewMemoryIterator(sboms), nil } -// getTopLevelDir extracts the top-level directory from a given path -func getTopLevelDir(basePath, fullPath string) string { +// getTopLevelDirAndFile extracts the first subdirectory after basePath and the filename. +func getTopLevelDirAndFile(basePath, fullPath string) (string, string) { + // Get the relative path from basePath to fullPath relPath, err := filepath.Rel(basePath, fullPath) if err != nil { - return "unknown" // Fallback in case of an error + return "unknown", "unknown" // Fallback in case of error } - // Split the relative path and return the first directory + // Split the relative path into directory components parts := strings.Split(relPath, string(filepath.Separator)) + + // If there are at least two parts, return the first directory and the filename if len(parts) > 1 { - return parts[0] // Return the top-level folder (e.g., "cdx" or "spdx") + return parts[0], parts[len(parts)-1] // First directory and last part (filename) } - return "unknown" + // If there's no subdirectory, return "unknown" for directory and actual filename + return "unknown", relPath } diff --git a/pkg/target/folder/adapter.go b/pkg/target/folder/adapter.go new file mode 100644 index 0000000..f7ae1c6 --- /dev/null +++ b/pkg/target/folder/adapter.go @@ -0,0 +1,206 @@ +// Copyright 2025 Interlynk.io +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package folder + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/google/uuid" + "github.com/interlynk-io/sbommv/pkg/iterator" + "github.com/interlynk-io/sbommv/pkg/logger" + "github.com/interlynk-io/sbommv/pkg/tcontext" + "github.com/interlynk-io/sbommv/pkg/types" + "github.com/spf13/cobra" +) + +// FolderAdapter handles storing SBOMs in a local folder +type FolderAdapter struct { + Role types.AdapterRole + FolderPath string + settings types.UploadSettings +} + +// AddCommandParams defines folder adapter CLI flags +func (f *FolderAdapter) AddCommandParams(cmd *cobra.Command) { + cmd.Flags().String("out-folder-path", "", "The folder where SBOMs should be stored") + cmd.Flags().String("out-folder-processing-mode", "sequential", "Folder processing mode (sequential/parallel)") +} + +// ParseAndValidateParams validates the folder path +func (f *FolderAdapter) ParseAndValidateParams(cmd *cobra.Command) error { + var pathFlag string + var processingModeFlag string + var missingFlags []string + var invalidFlags []string + + switch f.Role { + case types.InputAdapterRole: + return fmt.Errorf("The Folder adapter doesn't support output adapter functionalities.") + + case types.OutputAdapterRole: + pathFlag = "out-folder-path" + processingModeFlag = "out-folder-processing-mode" + + default: + return fmt.Errorf("The adapter is neither an input type nor an output type") + + } + + // Extract Folder Path + folderPath, _ := cmd.Flags().GetString(pathFlag) + if folderPath == "" { + missingFlags = append(missingFlags, "--"+pathFlag) + } + + validModes := map[string]bool{"sequential": true, "parallel": true} + mode, _ := cmd.Flags().GetString(processingModeFlag) + if !validModes[mode] { + invalidFlags = append(invalidFlags, fmt.Sprintf("%s=%s (must be one of: sequential, parallel mode)", processingModeFlag, mode)) + } + + // Validate required flags + if len(missingFlags) > 0 { + return fmt.Errorf("missing input adapter required flags: %v\n\nUse 'sbommv transfer --help' for usage details.", missingFlags) + } + + // Validate incorrect flag usage + if len(invalidFlags) > 0 { + return fmt.Errorf("invalid input adapter flag usage:\n %s\n\nUse 'sbommv transfer --help' for correct usage.", strings.Join(invalidFlags, "\n ")) + } + + f.FolderPath = folderPath + f.settings.ProcessingMode = types.UploadMode(mode) + + logger.LogDebug(cmd.Context(), "Folder Output Adapter Initialized", "path", f.FolderPath) + return nil +} + +// FetchSBOMs retrieves SBOMs lazily +func (i *FolderAdapter) FetchSBOMs(ctx *tcontext.TransferMetadata) (iterator.SBOMIterator, error) { + return nil, fmt.Errorf("Folder adapter does not support SBOM Fetching") +} + +// UploadSBOMs writes SBOMs to the output folder +func (f *FolderAdapter) UploadSBOMs(ctx *tcontext.TransferMetadata, iterator iterator.SBOMIterator) error { + logger.LogDebug(ctx.Context, "Starting SBOM upload", "mode", f.settings.ProcessingMode) + + if f.settings.ProcessingMode != "sequential" { + return fmt.Errorf("unsupported processing mode: %s", f.settings.ProcessingMode) // Future-proofed for parallel & batch + } + + switch f.settings.ProcessingMode { + + case types.UploadParallel: + // TODO: cuncurrent upload: As soon as we get the SBOM, upload it + // f.uploadParallel() + return fmt.Errorf("processing mode %q not yet implemented", f.settings.ProcessingMode) + + case types.UploadBatching: + // TODO: hybrid of sequential + parallel + // f.uploadBatch() + return fmt.Errorf("processing mode %q not yet implemented", f.settings.ProcessingMode) + + case types.UploadSequential: + // Sequential Processing: Fetch SBOM β†’ Upload β†’ Repeat + f.uploadSequential(ctx, iterator) + + default: + // + return fmt.Errorf("invalid processing mode: %q", f.settings.ProcessingMode) + } + + logger.LogDebug(ctx.Context, "All SBOMs have been successfully saved in directory", "value", f.FolderPath) + return nil +} + +// DryRun for Output Adapter: Simulates writing SBOMs to a folder +func (f *FolderAdapter) DryRun(ctx *tcontext.TransferMetadata, sbomIter iterator.SBOMIterator) error { + logger.LogDebug(ctx.Context, "Dry-run mode: Displaying SBOMs that would be stored in folder") + + fmt.Println("\nπŸ“¦ **Folder Output Adapter Dry-Run**") + + sbomCount := 0 + + for { + sbom, err := sbomIter.Next(ctx.Context) + if err != nil { + if err.Error() == "EOF" { + break + } + logger.LogError(ctx.Context, err, "Error retrieving SBOM from iterator") + continue + } + + namespace := filepath.Base(sbom.Namespace) + if namespace == "" { + namespace = fmt.Sprintf("sbom_%s.json", uuid.New().String()) // Generate unique filename + } + + outputPath := filepath.Join(f.FolderPath, namespace) + outputFile := filepath.Join(outputPath, sbom.Path) + + fmt.Printf("- πŸ“‚ Would write: %s\n", outputFile) + sbomCount++ + } + + fmt.Printf("\nπŸ“Š Total SBOMs to be stored: %d\n", sbomCount) + logger.LogDebug(ctx.Context, "Dry-run mode completed for folder output adapter", "total_sboms", sbomCount) + return nil +} + +func (f *FolderAdapter) uploadSequential(ctx *tcontext.TransferMetadata, sbomIter iterator.SBOMIterator) error { + logger.LogDebug(ctx.Context, "Writing SBOMs in sequential mode", "folder", f.FolderPath) + + // Process SBOMs + for { + sbom, err := sbomIter.Next(ctx.Context) + if err != nil { + if err.Error() == "EOF" { + break + } + logger.LogError(ctx.Context, err, "Error retrieving SBOM from iterator") + continue + } + + namespace := filepath.Base(sbom.Namespace) + if namespace == "" { + namespace = fmt.Sprintf("sbom_%s.json", uuid.New().String()) // Generate unique filename + } + + // Construct output path (preserve filename if available) + outputDir := filepath.Join(f.FolderPath, namespace) + if err := os.MkdirAll(outputDir, 0o755); err != nil { + logger.LogError(ctx.Context, err, "Failed to create folder", "path", outputDir) + continue + } + + outputFile := filepath.Join(outputDir, sbom.Path) + if sbom.Path == "" { + outputFile = filepath.Join(outputDir, fmt.Sprintf("%s.sbom.json", uuid.New().String())) + } + + // Write SBOM file + if err := os.WriteFile(outputFile, sbom.Data, 0o644); err != nil { + logger.LogError(ctx.Context, err, "Failed to write SBOM file", "path", outputFile) + continue + } + + logger.LogDebug(ctx.Context, "Successfully written SBOM", "path", outputFile) + } + return nil +} From c939e1f6309bd8ceabbf31a143c396d438618916 Mon Sep 17 00:00:00 2001 From: Vivek Kumar Sahu Date: Thu, 20 Feb 2025 20:39:18 +0530 Subject: [PATCH 3/4] implement SOLID principles for folder input adapter Signed-off-by: Vivek Kumar Sahu --- pkg/source/folder/adapter.go | 159 +++++++++--------- pkg/source/folder/config.go | 30 ++++ .../folder/{fetchMode.go => fetcher.go} | 88 ++++------ pkg/source/folder/reporter.go | 77 +++++++++ 4 files changed, 214 insertions(+), 140 deletions(-) create mode 100644 pkg/source/folder/config.go rename pkg/source/folder/{fetchMode.go => fetcher.go} (60%) create mode 100644 pkg/source/folder/reporter.go diff --git a/pkg/source/folder/adapter.go b/pkg/source/folder/adapter.go index c0350f1..a7952c8 100644 --- a/pkg/source/folder/adapter.go +++ b/pkg/source/folder/adapter.go @@ -16,12 +16,10 @@ package folder import ( "fmt" - "io" "strings" "github.com/interlynk-io/sbommv/pkg/iterator" "github.com/interlynk-io/sbommv/pkg/logger" - "github.com/interlynk-io/sbommv/pkg/sbom" "github.com/interlynk-io/sbommv/pkg/tcontext" "github.com/interlynk-io/sbommv/pkg/types" "github.com/spf13/cobra" @@ -29,11 +27,18 @@ import ( // FolderAdapter handles fetching SBOMs from folders type FolderAdapter struct { - FolderPath string // Folder path where SBOMs exist or will be stored - Recursive bool // Scan subdirectories (for input mode) - ProcessingMode types.ProcessingMode + config *FolderConfig + Role types.AdapterRole // "input" or "output" adapter type - Role types.AdapterRole // "input" or "output" adapter type + fetcher SBOMFetcher +} + +func NewFolderAdapter(config *FolderConfig) *FolderAdapter { + fetcher, ok := fetcherFactory[config.ProcessingMode] + if !ok { + fetcher = fetcherFactory[types.FetchSequential] + } + return &FolderAdapter{config: config, fetcher: fetcher} } // AddCommandParams adds Folder-specific CLI flags @@ -92,35 +97,17 @@ func (f *FolderAdapter) ParseAndValidateParams(cmd *cobra.Command) error { return fmt.Errorf("invalid input adapter flag usage:\n %s\n\nUse 'sbommv transfer --help' for correct usage.", strings.Join(invalidFlags, "\n ")) } - f.FolderPath = folderPath - f.Recursive = folderRecurse - f.ProcessingMode = types.ProcessingMode(mode) + f.config.FolderPath = folderPath + f.config.Recursive = folderRecurse + f.config.ProcessingMode = types.ProcessingMode(mode) return nil } // FetchSBOMs initializes the Folder SBOM iterator using the unified method func (f *FolderAdapter) FetchSBOMs(ctx *tcontext.TransferMetadata) (iterator.SBOMIterator, error) { - logger.LogDebug(ctx.Context, "Initializing SBOM fetching process") - logger.LogDebug(ctx.Context, "Scanning folder for SBOMs", "path", f.FolderPath, "recursive", f.Recursive) - - var sbomIterator iterator.SBOMIterator - var err error - - switch f.ProcessingMode { - case types.FetchParallel: - sbomIterator, err = f.fetchSBOMsConcurrently(ctx) - case types.FetchSequential: - sbomIterator, err = f.fetchSBOMsSequentially(ctx) - default: - return nil, fmt.Errorf("Unsupported Processing Mode !!") - } - - if err != nil { - return nil, fmt.Errorf("failed to fetch SBOMs: %w", err) - } - - return sbomIterator, nil + logger.LogDebug(ctx.Context, "Initializing SBOM fetching", "mode", f.config.ProcessingMode) + return f.fetcher.Fetch(ctx, f.config) } // OutputSBOMs should return an error since Folder does not support SBOM uploads @@ -129,58 +116,64 @@ func (f *FolderAdapter) UploadSBOMs(ctx *tcontext.TransferMetadata, iterator ite } // DryRun for Folder Adapter: Displays all fetched SBOMs from folder adapter -func (f *FolderAdapter) DryRun(ctx *tcontext.TransferMetadata, iterator iterator.SBOMIterator) error { - logger.LogDebug(ctx.Context, "Dry-run mode: Displaying SBOMs fetched from folder input adapter") - - var outputDir string - var verbose bool - - processor := sbom.NewSBOMProcessor(outputDir, verbose) - sbomCount := 0 - fmt.Println() - fmt.Printf("πŸ“¦ Details of all Fetched SBOMs by Folder Input Adapter\n") - - for { - - sbom, err := iterator.Next(ctx.Context) - if err == io.EOF { - break // No more sboms - } - - if err != nil { - logger.LogError(ctx.Context, err, "Error retrieving SBOM from iterator") - } - - // update processor with current SBOM data - processor.Update(sbom.Data, "", sbom.Path) - - doc, err := processor.ProcessSBOMs() - if err != nil { - logger.LogError(ctx.Context, err, "Failed to process SBOM") - continue - } - - // if outputDir is provided, save the SBOM file - if outputDir != "" { - if err := processor.WriteSBOM(doc, ""); err != nil { - logger.LogError(ctx.Context, err, "Failed to write SBOM to output directory") - } - } - - // Print SBOM content if verbose mode is enabled - if verbose { - fmt.Println("\n-------------------- πŸ“œ SBOM Content --------------------") - fmt.Printf("πŸ“‚ Filename: %s\n", doc.Filename) - fmt.Printf("πŸ“¦ Format: %s | SpecVersion: %s\n\n", doc.Format, doc.SpecVersion) - fmt.Println(string(doc.Content)) - fmt.Println("------------------------------------------------------") - fmt.Println() - } - - sbomCount++ - fmt.Printf(" - πŸ“ Folder: %s | Format: %s | SpecVersion: %s | Filename: %s \n", sbom.Namespace, doc.Format, doc.SpecVersion, doc.Filename) - - } - - return nil +func (f *FolderAdapter) DryRun(ctx *tcontext.TransferMetadata, iter iterator.SBOMIterator) error { + reporter := NewFolderReporter(false, "") + return reporter.DryRun(ctx.Context, iter) } + +// DryRun for Folder Adapter: Displays all fetched SBOMs from folder adapter +// func (f *FolderAdapter) DryRun(ctx *tcontext.TransferMetadata, iterator iterator.SBOMIterator) error { +// logger.LogDebug(ctx.Context, "Dry-run mode: Displaying SBOMs fetched from folder input adapter") + +// var outputDir string +// var verbose bool + +// processor := sbom.NewSBOMProcessor(outputDir, verbose) +// sbomCount := 0 +// fmt.Println() +// fmt.Printf("πŸ“¦ Details of all Fetched SBOMs by Folder Input Adapter\n") + +// for { + +// sbom, err := iterator.Next(ctx.Context) +// if err == io.EOF { +// break // No more sboms +// } + +// if err != nil { +// logger.LogError(ctx.Context, err, "Error retrieving SBOM from iterator") +// } + +// // update processor with current SBOM data +// processor.Update(sbom.Data, "", sbom.Path) + +// doc, err := processor.ProcessSBOMs() +// if err != nil { +// logger.LogError(ctx.Context, err, "Failed to process SBOM") +// continue +// } + +// // if outputDir is provided, save the SBOM file +// if outputDir != "" { +// if err := processor.WriteSBOM(doc, ""); err != nil { +// logger.LogError(ctx.Context, err, "Failed to write SBOM to output directory") +// } +// } + +// // Print SBOM content if verbose mode is enabled +// if verbose { +// fmt.Println("\n-------------------- πŸ“œ SBOM Content --------------------") +// fmt.Printf("πŸ“‚ Filename: %s\n", doc.Filename) +// fmt.Printf("πŸ“¦ Format: %s | SpecVersion: %s\n\n", doc.Format, doc.SpecVersion) +// fmt.Println(string(doc.Content)) +// fmt.Println("------------------------------------------------------") +// fmt.Println() +// } + +// sbomCount++ +// fmt.Printf(" - πŸ“ Folder: %s | Format: %s | SpecVersion: %s | Filename: %s \n", sbom.Namespace, doc.Format, doc.SpecVersion, doc.Filename) + +// } + +// return nil +// } diff --git a/pkg/source/folder/config.go b/pkg/source/folder/config.go new file mode 100644 index 0000000..a375a49 --- /dev/null +++ b/pkg/source/folder/config.go @@ -0,0 +1,30 @@ +// Copyright 2025 Interlynk.io +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------- + +package folder + +import "github.com/interlynk-io/sbommv/pkg/types" + +type FolderConfig struct { + FolderPath string + Recursive bool + ProcessingMode types.ProcessingMode +} + +func NewFolderConfig() *FolderConfig { + return &FolderConfig{ + ProcessingMode: types.FetchSequential, // Default + } +} diff --git a/pkg/source/folder/fetchMode.go b/pkg/source/folder/fetcher.go similarity index 60% rename from pkg/source/folder/fetchMode.go rename to pkg/source/folder/fetcher.go index c0114f4..78cc598 100644 --- a/pkg/source/folder/fetchMode.go +++ b/pkg/source/folder/fetcher.go @@ -11,6 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. +// ------------------------------------------------------------------------- package folder @@ -25,141 +26,114 @@ import ( "github.com/interlynk-io/sbommv/pkg/logger" "github.com/interlynk-io/sbommv/pkg/source" "github.com/interlynk-io/sbommv/pkg/tcontext" + "github.com/interlynk-io/sbommv/pkg/types" ) -// fetchSBOMsSequentially scans the folder for SBOMs one-by-one +type SBOMFetcher interface { + Fetch(ctx *tcontext.TransferMetadata, config *FolderConfig) (iterator.SBOMIterator, error) +} + +var fetcherFactory = map[types.ProcessingMode]SBOMFetcher{ + types.FetchSequential: &SequentialFetcher{}, + types.FetchParallel: &ParallelFetcher{}, +} + +type SequentialFetcher struct{} + +// SequentialFetcher Fetch() scans the folder for SBOMs one-by-one // 1. Walks through the folder file-by-file // 2. Detects valid SBOMs using source.IsSBOMFile(). // 3. Reads the content & adds it to the iterator along with path. -func (f *FolderAdapter) fetchSBOMsSequentially(ctx *tcontext.TransferMetadata) (iterator.SBOMIterator, error) { - logger.LogDebug(ctx.Context, "Scanning folder sequentially for SBOMs", "path", f.FolderPath, "recursive", f.Recursive) - +func (f *SequentialFetcher) Fetch(ctx *tcontext.TransferMetadata, config *FolderConfig) (iterator.SBOMIterator, error) { var sbomList []*iterator.SBOM - - err := filepath.Walk(f.FolderPath, func(path string, info os.FileInfo, err error) error { + err := filepath.Walk(config.FolderPath, func(path string, info os.FileInfo, err error) error { if err != nil { logger.LogError(ctx.Context, err, "Error accessing file", "path", path) return nil } - - // Skip directories (except the root folder) - if info.IsDir() && !f.Recursive && path != f.FolderPath { + if info.IsDir() && !config.Recursive && path != config.FolderPath { return filepath.SkipDir } - - // Check if the file is a valid SBOM if source.IsSBOMFile(path) { content, err := os.ReadFile(path) if err != nil { logger.LogError(ctx.Context, err, "Failed to read SBOM", "path", path) return nil } - - // Extract project name from the top-level directory - projectName, path := getTopLevelDirAndFile(f.FolderPath, path) - + projectName, path := getTopLevelDirAndFile(config.FolderPath, path) sbomList = append(sbomList, &iterator.SBOM{ - Data: content, - // Format: utils.DetectSBOMFormat(content), + Data: content, Path: path, Namespace: projectName, }) - - logger.LogDebug(ctx.Context, "SBOM Detected", "file", path) } - return nil }) if err != nil { - return nil, fmt.Errorf("error scanning folder: %w", err) + return nil, err } - - logger.LogDebug(ctx.Context, "Total SBOMs fetched (Sequential Mode)", "count", len(sbomList)) - if len(sbomList) == 0 { - return nil, fmt.Errorf("no SBOMs found in the specified folder") + return nil, fmt.Errorf("no SBOMs found in folder") } - return NewFolderIterator(sbomList), nil } -// fetchSBOMsConcurrently scans the folder for SBOMs using parallel processing +type ParallelFetcher struct{} + +// ParallelFetcher Fetch() scans the folder for SBOMs using parallel processing // 1. Walks through the folder file-by-file. // 2. Launch a goroutine for each file. // 3. Detects valid SBOMs using source.IsSBOMFile(). // 4. Uses channels to store SBOMs & errors. // 5. Reads the content & adds it to the iterator along with path. -func (f *FolderAdapter) fetchSBOMsConcurrently(ctx *tcontext.TransferMetadata) (iterator.SBOMIterator, error) { - logger.LogDebug(ctx.Context, "Using PARALLEL processing mode") - +func (f *ParallelFetcher) Fetch(ctx *tcontext.TransferMetadata, config *FolderConfig) (iterator.SBOMIterator, error) { var wg sync.WaitGroup sbomsChan := make(chan *iterator.SBOM, 100) errChan := make(chan error, 10) - // Walk the folder and process files in parallel - err := filepath.Walk(f.FolderPath, func(path string, info os.FileInfo, err error) error { + err := filepath.Walk(config.FolderPath, func(path string, info os.FileInfo, err error) error { if err != nil { - errChan <- fmt.Errorf("error accessing file %s: %w", path, err) + errChan <- err return nil } - - fmt.Println("path", path) - // Skip directories (except root folder) - if info.IsDir() && !f.Recursive && path != f.FolderPath { + if info.IsDir() && !config.Recursive && path != config.FolderPath { return filepath.SkipDir } - - // Launch a goroutine for each file wg.Add(1) - go func(path string) { defer wg.Done() - if source.IsSBOMFile(path) { content, err := os.ReadFile(path) if err != nil { - logger.LogError(ctx.Context, err, "Failed to read SBOM", "path", path) errChan <- err return } - - // Extract project name from the top-level directory - projectName, path := getTopLevelDirAndFile(f.FolderPath, path) - + projectName, path := getTopLevelDirAndFile(config.FolderPath, path) sbomsChan <- &iterator.SBOM{ Data: content, Path: path, Namespace: projectName, } - } }(path) return nil }) - - // Close channels after all goroutines complete go func() { wg.Wait() close(sbomsChan) close(errChan) }() - // Collect SBOMs from channel var sboms []*iterator.SBOM for sbom := range sbomsChan { sboms = append(sboms, sbom) } - - // Check for errors for err := range errChan { - logger.LogError(ctx.Context, err, "Error processing files in parallel mode") + logger.LogError(ctx.Context, err, "Error in parallel fetch") } - if err != nil { - return nil, fmt.Errorf("error scanning folder: %w", err) + return nil, err } - - logger.LogDebug(ctx.Context, "Total SBOMs fetched (Parallel Mode)", "count", len(sboms)) return iterator.NewMemoryIterator(sboms), nil } diff --git a/pkg/source/folder/reporter.go b/pkg/source/folder/reporter.go new file mode 100644 index 0000000..7ebdfc3 --- /dev/null +++ b/pkg/source/folder/reporter.go @@ -0,0 +1,77 @@ +// Copyright 2025 Interlynk.io +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------- + +package folder + +import ( + "context" + "fmt" + "io" + + "github.com/interlynk-io/sbommv/pkg/iterator" + "github.com/interlynk-io/sbommv/pkg/logger" + "github.com/interlynk-io/sbommv/pkg/sbom" +) + +type FolderReporter struct { + verbose bool + outputDir string +} + +func NewFolderReporter(verbose bool, outputDir string) *FolderReporter { + return &FolderReporter{verbose: verbose, outputDir: outputDir} +} + +func (r *FolderReporter) DryRun(ctx context.Context, iter iterator.SBOMIterator) error { + logger.LogDebug(ctx, "Dry-run mode: Displaying SBOMs fetched from folder") + processor := sbom.NewSBOMProcessor(r.outputDir, r.verbose) + sbomCount := 0 + fmt.Println("\nπŸ“¦ Details of all Fetched SBOMs by Folder Input Adapter") + + for { + sbom, err := iter.Next(ctx) + if err == io.EOF { + break + } + if err != nil { + logger.LogError(ctx, err, "Error retrieving SBOM from iterator") + return err + } + processor.Update(sbom.Data, "", sbom.Path) + doc, err := processor.ProcessSBOMs() + if err != nil { + logger.LogError(ctx, err, "Failed to process SBOM") + return err + } + if r.outputDir != "" { + if err := processor.WriteSBOM(doc, ""); err != nil { + logger.LogError(ctx, err, "Failed to write SBOM") + return err + } + } + if r.verbose { + fmt.Printf("\n-------------------- πŸ“œ SBOM Content --------------------\n") + fmt.Printf("πŸ“‚ Filename: %s\n", doc.Filename) + fmt.Printf("πŸ“¦ Format: %s | SpecVersion: %s\n\n", doc.Format, doc.SpecVersion) + fmt.Println(string(doc.Content)) + fmt.Println("------------------------------------------------------") + } + sbomCount++ + fmt.Printf(" - πŸ“ Folder: %s | Format: %s | SpecVersion: %s | Filename: %s\n", + sbom.Namespace, doc.Format, doc.SpecVersion, doc.Filename) + } + fmt.Printf("πŸ“Š Total SBOMs: %d\n", sbomCount) + return nil +} From e3617cad8c21bea07517c404214956099ccb8b3b Mon Sep 17 00:00:00 2001 From: Vivek Kumar Sahu Date: Thu, 20 Feb 2025 21:41:22 +0530 Subject: [PATCH 4/4] implement SOLID principles for folder output adapter Signed-off-by: Vivek Kumar Sahu --- pkg/adapter/factory.go | 4 +- pkg/source/folder/adapter.go | 84 +++------------------- pkg/target/folder/adapter.go | 128 ++++------------------------------ pkg/target/folder/config.go | 28 ++++++++ pkg/target/folder/reporter.go | 68 ++++++++++++++++++ pkg/target/folder/uploader.go | 73 +++++++++++++++++++ 6 files changed, 197 insertions(+), 188 deletions(-) create mode 100644 pkg/target/folder/config.go create mode 100644 pkg/target/folder/reporter.go create mode 100644 pkg/target/folder/uploader.go diff --git a/pkg/adapter/factory.go b/pkg/adapter/factory.go index c7b5c7b..85fd147 100644 --- a/pkg/adapter/factory.go +++ b/pkg/adapter/factory.go @@ -62,7 +62,7 @@ func NewAdapter(ctx *tcontext.TransferMetadata, config types.Config) (map[types. adapters[types.InputAdapterRole] = &github.GitHubAdapter{Role: types.InputAdapterRole} case types.FolderAdapterType: - adapters[types.InputAdapterRole] = &ifolder.FolderAdapter{Role: types.InputAdapterRole} + adapters[types.InputAdapterRole] = &ifolder.FolderAdapter{Role: types.InputAdapterRole, Fetcher: &ifolder.SequentialFetcher{}} case types.InterlynkAdapterType: adapters[types.InputAdapterRole] = &interlynk.InterlynkAdapter{Role: types.InputAdapterRole} @@ -79,7 +79,7 @@ func NewAdapter(ctx *tcontext.TransferMetadata, config types.Config) (map[types. switch types.AdapterType(config.DestinationType) { case types.FolderAdapterType: - adapters[types.OutputAdapterRole] = &ofolder.FolderAdapter{Role: types.OutputAdapterRole} + adapters[types.OutputAdapterRole] = &ofolder.FolderAdapter{Role: types.OutputAdapterRole, Uploader: &ofolder.SequentialUploader{}} case types.InterlynkAdapterType: adapters[types.OutputAdapterRole] = &interlynk.InterlynkAdapter{Role: types.OutputAdapterRole} diff --git a/pkg/source/folder/adapter.go b/pkg/source/folder/adapter.go index a7952c8..769bc84 100644 --- a/pkg/source/folder/adapter.go +++ b/pkg/source/folder/adapter.go @@ -27,18 +27,9 @@ import ( // FolderAdapter handles fetching SBOMs from folders type FolderAdapter struct { - config *FolderConfig - Role types.AdapterRole // "input" or "output" adapter type - - fetcher SBOMFetcher -} - -func NewFolderAdapter(config *FolderConfig) *FolderAdapter { - fetcher, ok := fetcherFactory[config.ProcessingMode] - if !ok { - fetcher = fetcherFactory[types.FetchSequential] - } - return &FolderAdapter{config: config, fetcher: fetcher} + config *FolderConfig + Role types.AdapterRole // "input" or "output" adapter type + Fetcher SBOMFetcher } // AddCommandParams adds Folder-specific CLI flags @@ -97,9 +88,13 @@ func (f *FolderAdapter) ParseAndValidateParams(cmd *cobra.Command) error { return fmt.Errorf("invalid input adapter flag usage:\n %s\n\nUse 'sbommv transfer --help' for correct usage.", strings.Join(invalidFlags, "\n ")) } - f.config.FolderPath = folderPath - f.config.Recursive = folderRecurse - f.config.ProcessingMode = types.ProcessingMode(mode) + cfg := FolderConfig{ + FolderPath: folderPath, + Recursive: folderRecurse, + ProcessingMode: types.ProcessingMode(mode), + } + + f.config = &cfg return nil } @@ -107,7 +102,7 @@ func (f *FolderAdapter) ParseAndValidateParams(cmd *cobra.Command) error { // FetchSBOMs initializes the Folder SBOM iterator using the unified method func (f *FolderAdapter) FetchSBOMs(ctx *tcontext.TransferMetadata) (iterator.SBOMIterator, error) { logger.LogDebug(ctx.Context, "Initializing SBOM fetching", "mode", f.config.ProcessingMode) - return f.fetcher.Fetch(ctx, f.config) + return f.Fetcher.Fetch(ctx, f.config) } // OutputSBOMs should return an error since Folder does not support SBOM uploads @@ -120,60 +115,3 @@ func (f *FolderAdapter) DryRun(ctx *tcontext.TransferMetadata, iter iterator.SBO reporter := NewFolderReporter(false, "") return reporter.DryRun(ctx.Context, iter) } - -// DryRun for Folder Adapter: Displays all fetched SBOMs from folder adapter -// func (f *FolderAdapter) DryRun(ctx *tcontext.TransferMetadata, iterator iterator.SBOMIterator) error { -// logger.LogDebug(ctx.Context, "Dry-run mode: Displaying SBOMs fetched from folder input adapter") - -// var outputDir string -// var verbose bool - -// processor := sbom.NewSBOMProcessor(outputDir, verbose) -// sbomCount := 0 -// fmt.Println() -// fmt.Printf("πŸ“¦ Details of all Fetched SBOMs by Folder Input Adapter\n") - -// for { - -// sbom, err := iterator.Next(ctx.Context) -// if err == io.EOF { -// break // No more sboms -// } - -// if err != nil { -// logger.LogError(ctx.Context, err, "Error retrieving SBOM from iterator") -// } - -// // update processor with current SBOM data -// processor.Update(sbom.Data, "", sbom.Path) - -// doc, err := processor.ProcessSBOMs() -// if err != nil { -// logger.LogError(ctx.Context, err, "Failed to process SBOM") -// continue -// } - -// // if outputDir is provided, save the SBOM file -// if outputDir != "" { -// if err := processor.WriteSBOM(doc, ""); err != nil { -// logger.LogError(ctx.Context, err, "Failed to write SBOM to output directory") -// } -// } - -// // Print SBOM content if verbose mode is enabled -// if verbose { -// fmt.Println("\n-------------------- πŸ“œ SBOM Content --------------------") -// fmt.Printf("πŸ“‚ Filename: %s\n", doc.Filename) -// fmt.Printf("πŸ“¦ Format: %s | SpecVersion: %s\n\n", doc.Format, doc.SpecVersion) -// fmt.Println(string(doc.Content)) -// fmt.Println("------------------------------------------------------") -// fmt.Println() -// } - -// sbomCount++ -// fmt.Printf(" - πŸ“ Folder: %s | Format: %s | SpecVersion: %s | Filename: %s \n", sbom.Namespace, doc.Format, doc.SpecVersion, doc.Filename) - -// } - -// return nil -// } diff --git a/pkg/target/folder/adapter.go b/pkg/target/folder/adapter.go index f7ae1c6..f5d6b1d 100644 --- a/pkg/target/folder/adapter.go +++ b/pkg/target/folder/adapter.go @@ -16,11 +16,8 @@ package folder import ( "fmt" - "os" - "path/filepath" "strings" - "github.com/google/uuid" "github.com/interlynk-io/sbommv/pkg/iterator" "github.com/interlynk-io/sbommv/pkg/logger" "github.com/interlynk-io/sbommv/pkg/tcontext" @@ -30,9 +27,9 @@ import ( // FolderAdapter handles storing SBOMs in a local folder type FolderAdapter struct { - Role types.AdapterRole - FolderPath string - settings types.UploadSettings + Role types.AdapterRole + config *FolderConfig + Uploader SBOMUploader } // AddCommandParams defines folder adapter CLI flags @@ -83,10 +80,13 @@ func (f *FolderAdapter) ParseAndValidateParams(cmd *cobra.Command) error { return fmt.Errorf("invalid input adapter flag usage:\n %s\n\nUse 'sbommv transfer --help' for correct usage.", strings.Join(invalidFlags, "\n ")) } - f.FolderPath = folderPath - f.settings.ProcessingMode = types.UploadMode(mode) + cfg := FolderConfig{ + FolderPath: folderPath, + Settings: types.UploadSettings{ProcessingMode: types.UploadMode(mode)}, + } + f.config = &cfg - logger.LogDebug(cmd.Context(), "Folder Output Adapter Initialized", "path", f.FolderPath) + logger.LogDebug(cmd.Context(), "Folder Output Adapter Initialized", "path", f.config.FolderPath) return nil } @@ -96,111 +96,13 @@ func (i *FolderAdapter) FetchSBOMs(ctx *tcontext.TransferMetadata) (iterator.SBO } // UploadSBOMs writes SBOMs to the output folder -func (f *FolderAdapter) UploadSBOMs(ctx *tcontext.TransferMetadata, iterator iterator.SBOMIterator) error { - logger.LogDebug(ctx.Context, "Starting SBOM upload", "mode", f.settings.ProcessingMode) - - if f.settings.ProcessingMode != "sequential" { - return fmt.Errorf("unsupported processing mode: %s", f.settings.ProcessingMode) // Future-proofed for parallel & batch - } - - switch f.settings.ProcessingMode { - - case types.UploadParallel: - // TODO: cuncurrent upload: As soon as we get the SBOM, upload it - // f.uploadParallel() - return fmt.Errorf("processing mode %q not yet implemented", f.settings.ProcessingMode) - - case types.UploadBatching: - // TODO: hybrid of sequential + parallel - // f.uploadBatch() - return fmt.Errorf("processing mode %q not yet implemented", f.settings.ProcessingMode) - - case types.UploadSequential: - // Sequential Processing: Fetch SBOM β†’ Upload β†’ Repeat - f.uploadSequential(ctx, iterator) - - default: - // - return fmt.Errorf("invalid processing mode: %q", f.settings.ProcessingMode) - } - - logger.LogDebug(ctx.Context, "All SBOMs have been successfully saved in directory", "value", f.FolderPath) - return nil +func (f *FolderAdapter) UploadSBOMs(ctx *tcontext.TransferMetadata, iter iterator.SBOMIterator) error { + logger.LogDebug(ctx.Context, "Starting SBOM upload", "mode", f.config.Settings.ProcessingMode) + return f.Uploader.Upload(ctx, f.config, iter) } // DryRun for Output Adapter: Simulates writing SBOMs to a folder -func (f *FolderAdapter) DryRun(ctx *tcontext.TransferMetadata, sbomIter iterator.SBOMIterator) error { - logger.LogDebug(ctx.Context, "Dry-run mode: Displaying SBOMs that would be stored in folder") - - fmt.Println("\nπŸ“¦ **Folder Output Adapter Dry-Run**") - - sbomCount := 0 - - for { - sbom, err := sbomIter.Next(ctx.Context) - if err != nil { - if err.Error() == "EOF" { - break - } - logger.LogError(ctx.Context, err, "Error retrieving SBOM from iterator") - continue - } - - namespace := filepath.Base(sbom.Namespace) - if namespace == "" { - namespace = fmt.Sprintf("sbom_%s.json", uuid.New().String()) // Generate unique filename - } - - outputPath := filepath.Join(f.FolderPath, namespace) - outputFile := filepath.Join(outputPath, sbom.Path) - - fmt.Printf("- πŸ“‚ Would write: %s\n", outputFile) - sbomCount++ - } - - fmt.Printf("\nπŸ“Š Total SBOMs to be stored: %d\n", sbomCount) - logger.LogDebug(ctx.Context, "Dry-run mode completed for folder output adapter", "total_sboms", sbomCount) - return nil -} - -func (f *FolderAdapter) uploadSequential(ctx *tcontext.TransferMetadata, sbomIter iterator.SBOMIterator) error { - logger.LogDebug(ctx.Context, "Writing SBOMs in sequential mode", "folder", f.FolderPath) - - // Process SBOMs - for { - sbom, err := sbomIter.Next(ctx.Context) - if err != nil { - if err.Error() == "EOF" { - break - } - logger.LogError(ctx.Context, err, "Error retrieving SBOM from iterator") - continue - } - - namespace := filepath.Base(sbom.Namespace) - if namespace == "" { - namespace = fmt.Sprintf("sbom_%s.json", uuid.New().String()) // Generate unique filename - } - - // Construct output path (preserve filename if available) - outputDir := filepath.Join(f.FolderPath, namespace) - if err := os.MkdirAll(outputDir, 0o755); err != nil { - logger.LogError(ctx.Context, err, "Failed to create folder", "path", outputDir) - continue - } - - outputFile := filepath.Join(outputDir, sbom.Path) - if sbom.Path == "" { - outputFile = filepath.Join(outputDir, fmt.Sprintf("%s.sbom.json", uuid.New().String())) - } - - // Write SBOM file - if err := os.WriteFile(outputFile, sbom.Data, 0o644); err != nil { - logger.LogError(ctx.Context, err, "Failed to write SBOM file", "path", outputFile) - continue - } - - logger.LogDebug(ctx.Context, "Successfully written SBOM", "path", outputFile) - } - return nil +func (f *FolderAdapter) DryRun(ctx *tcontext.TransferMetadata, iter iterator.SBOMIterator) error { + reporter := NewFolderOutputReporter(f.config.FolderPath) + return reporter.DryRun(ctx.Context, iter) } diff --git a/pkg/target/folder/config.go b/pkg/target/folder/config.go new file mode 100644 index 0000000..df1612c --- /dev/null +++ b/pkg/target/folder/config.go @@ -0,0 +1,28 @@ +// Copyright 2025 Interlynk.io +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package folder + +import "github.com/interlynk-io/sbommv/pkg/types" + +type FolderConfig struct { + FolderPath string + Settings types.UploadSettings +} + +func NewFolderConfig() *FolderConfig { + return &FolderConfig{ + Settings: types.UploadSettings{ProcessingMode: types.UploadSequential}, + } +} diff --git a/pkg/target/folder/reporter.go b/pkg/target/folder/reporter.go new file mode 100644 index 0000000..c16a236 --- /dev/null +++ b/pkg/target/folder/reporter.go @@ -0,0 +1,68 @@ +// Copyright 2025 Interlynk.io +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package folder + +import ( + "context" + "fmt" + "io" + "path/filepath" + + "github.com/google/uuid" + "github.com/interlynk-io/sbommv/pkg/iterator" + "github.com/interlynk-io/sbommv/pkg/logger" +) + +type FolderOutputReporter struct { + folderPath string +} + +func NewFolderOutputReporter(folderPath string) *FolderOutputReporter { + return &FolderOutputReporter{folderPath: folderPath} +} + +func (r *FolderOutputReporter) DryRun(ctx context.Context, iter iterator.SBOMIterator) error { + logger.LogDebug(ctx, "Dry-run mode: Displaying SBOMs for folder output") + fmt.Println("\nπŸ“¦ **Folder Output Adapter Dry-Run**") + sbomCount := 0 + + for { + sbom, err := iter.Next(ctx) + if err == io.EOF { + break + } + if err != nil { + logger.LogError(ctx, err, "Error retrieving SBOM from iterator") + return err + } + + namespace := filepath.Base(sbom.Namespace) + if namespace == "" { + namespace = fmt.Sprintf("sbom_%s.json", uuid.New().String()) + } + outputPath := filepath.Join(r.folderPath, namespace) + outputFile := filepath.Join(outputPath, sbom.Path) + if sbom.Path == "" { + outputFile = filepath.Join(outputPath, fmt.Sprintf("%s.sbom.json", uuid.New().String())) + } + + fmt.Printf("- πŸ“‚ Would write: %s\n", outputFile) + sbomCount++ + } + + fmt.Printf("\nπŸ“Š Total SBOMs to be stored: %d\n", sbomCount) + logger.LogDebug(ctx, "Dry-run completed", "total_sboms", sbomCount) + return nil +} diff --git a/pkg/target/folder/uploader.go b/pkg/target/folder/uploader.go new file mode 100644 index 0000000..699fd48 --- /dev/null +++ b/pkg/target/folder/uploader.go @@ -0,0 +1,73 @@ +// Copyright 2025 Interlynk.io +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package folder + +import ( + "fmt" + "io" + "os" + "path/filepath" + + "github.com/google/uuid" + "github.com/interlynk-io/sbommv/pkg/iterator" + "github.com/interlynk-io/sbommv/pkg/logger" + "github.com/interlynk-io/sbommv/pkg/tcontext" + "github.com/interlynk-io/sbommv/pkg/types" +) + +type SBOMUploader interface { + Upload(ctx *tcontext.TransferMetadata, config *FolderConfig, iter iterator.SBOMIterator) error +} + +var uploaderFactory = map[types.UploadMode]SBOMUploader{ + types.UploadSequential: &SequentialUploader{}, + // Add parallel uploader later +} + +type SequentialUploader struct{} + +func (u *SequentialUploader) Upload(ctx *tcontext.TransferMetadata, config *FolderConfig, iter iterator.SBOMIterator) error { + logger.LogDebug(ctx.Context, "Writing SBOMs sequentially", "folder", config.FolderPath) + for { + sbom, err := iter.Next(ctx.Context) + if err == io.EOF { + break + } + if err != nil { + logger.LogError(ctx.Context, err, "Error retrieving SBOM from iterator") + return err + } + + namespace := filepath.Base(sbom.Namespace) + if namespace == "" { + namespace = fmt.Sprintf("sbom_%s.json", uuid.New().String()) + } + outputDir := filepath.Join(config.FolderPath, namespace) + if err := os.MkdirAll(outputDir, 0o755); err != nil { + logger.LogError(ctx.Context, err, "Failed to create folder", "path", outputDir) + return err + } + outputFile := filepath.Join(outputDir, sbom.Path) + if sbom.Path == "" { + outputFile = filepath.Join(outputDir, fmt.Sprintf("%s.sbom.json", uuid.New().String())) + } + if err := os.WriteFile(outputFile, sbom.Data, 0o644); err != nil { + logger.LogError(ctx.Context, err, "Failed to write SBOM file", "path", outputFile) + return err + } + logger.LogDebug(ctx.Context, "Successfully written SBOM", "path", outputFile) + } + return nil +}