diff --git a/docs/checks/commands/check_files.md b/docs/checks/commands/check_files.md index 09d3300d..38f0d852 100644 --- a/docs/checks/commands/check_files.md +++ b/docs/checks/commands/check_files.md @@ -62,6 +62,7 @@ Naemon Config | ---------------------------- | -------------------------------------------------------------------------------------- | | calculate-subdirectory-sizes | For subdirectories that are found under the search paths, calculate the subdirectory sizes based on found files. This calculation may be expensive. Default: false | | file | Alias for path | +| follow-symlinks | Follow symlinks of files and subdirectories while traversing. The file paths will be registered originating from search path. Default: true | | max-depth | Maximum recursion depth. Default: no limit. '0' and '1' disable recursion and only include files/directories directly under path., '2' starts to include files/directories of subdirectories with given depth. | | path | Path in which to search for files | | paths | A comma separated list of paths | @@ -98,3 +99,4 @@ these can be used in filters and thresholds (along with the default attributes): | sha256_checksum | SHA256 checksum of the file | | sha384_checksum | SHA384 checksum of the file | | sha512_checksum | SHA512 checksum of the file | +| is_symlink | The file or its parent is a symlink | diff --git a/pkg/snclient/check_files.go b/pkg/snclient/check_files.go index 9cf05948..2e6da36a 100644 --- a/pkg/snclient/check_files.go +++ b/pkg/snclient/check_files.go @@ -29,12 +29,17 @@ type FileInfoUnified struct { Ctime time.Time // Create time } +const ( + CheckFilesDefaultFollowSymlinks = true +) + type CheckFiles struct { paths []string pathList CommaStringList pattern string // constructor NewCheckFiles sets this as '*' maxDepth int64 // constructor NewCheckFiles sets this as -1 calculateSubdirectorySizes bool // constructor NewCheckFiles sets this as false + followSymlinks bool // constructor NewCheckFiles sets this as true } func NewCheckFiles() CheckHandler { @@ -43,6 +48,7 @@ func NewCheckFiles() CheckHandler { pattern: "*", maxDepth: int64(-1), calculateSubdirectorySizes: false, + followSymlinks: CheckFilesDefaultFollowSymlinks, } } @@ -62,6 +68,8 @@ func (l *CheckFiles) Build() *CheckData { "max-depth": {value: &l.maxDepth, description: "Maximum recursion depth. Default: no limit. '0' and '1' disable recursion and only include files/directories directly under path." + ", '2' starts to include files/directories of subdirectories with given depth. "}, "timezone": {description: "Sets the timezone for time metrics (default is local time)"}, + "follow-symlinks": {value: &l.followSymlinks, description: fmt.Sprintf("Follow symlinks of files and subdirectories while traversing. "+ + "The file paths will be registered originating from search path. Default: %t", CheckFilesDefaultFollowSymlinks)}, "calculate-subdirectory-sizes": {value: &l.calculateSubdirectorySizes, description: "For subdirectories that are found under the search paths, " + "calculate the subdirectory sizes based on found files. This calculation may be expensive. Default: false"}, }, @@ -93,6 +101,7 @@ func (l *CheckFiles) Build() *CheckData { {name: "sha256_checksum", description: "SHA256 checksum of the file"}, {name: "sha384_checksum", description: "SHA384 checksum of the file"}, {name: "sha512_checksum", description: "SHA512 checksum of the file"}, + {name: "is_symlink", description: "The file or its parent is a symlink", unit: UBool}, }, exampleDefault: ` Alert if there are logs older than 1 hour in /tmp: @@ -119,10 +128,22 @@ func (l *CheckFiles) Check(_ context.Context, _ *Agent, check *CheckData, _ []Ar checkPath = l.normalizePath(checkPath) log.Tracef("normalized checkPath: %s", checkPath) - err := filepath.WalkDir(checkPath, func(path string, dirEntry fs.DirEntry, err error) error { - return l.addFile(check, path, checkPath, dirEntry, err) - }) - if err != nil { + walker := &fileWalker{cf: l, check: check, checkPath: checkPath, seenInodes: make(map[uint64]bool)} + + realRoot := checkPath + rootIsSymlink := false + if l.followSymlinks { + if lstat, lerr := os.Lstat(checkPath); lerr == nil && isLink(lstat) { + rootIsSymlink = true + } + if resolved, evalErr := resolveLink(checkPath); evalErr == nil { + realRoot = resolved + } else if resolved, evalErr := filepath.EvalSymlinks(checkPath); evalErr == nil { + realRoot = resolved + } + } + + if err := walker.walk(realRoot, checkPath, rootIsSymlink); err != nil { return nil, fmt.Errorf("error walking directory %s: %s", checkPath, err.Error()) } } @@ -153,7 +174,130 @@ func (l *CheckFiles) Check(_ context.Context, _ *Agent, check *CheckData, _ []Ar return check.Finalize() } -func (l *CheckFiles) addFile(check *CheckData, path, checkPath string, dirEntry fs.DirEntry, err error) error { +// fileWalker walks one check path while following symlinks. +// it registers new paths under the checkPath, even if the links take it outside of it +// the depth is calculated with the checkpath being the root +type fileWalker struct { + cf *CheckFiles + check *CheckData + checkPath string // original check path this walk started from + visited []string // real directories already walked , normally or via a symlink + seenInodes map[uint64]bool // track inodes to skip hard links +} + +// walk walks realRoot on disk but registers paths under displayRoot +// realRoot is the canonical path on the filesystem +// isSymlink marks whether realRoot was reached via a symlink. +// +//nolint:wrapcheck // filepath walker functions need to return an error, wrapping and appending a header to each call would return a large error message +func (w *fileWalker) walk(realRoot, displayRoot string, usedSymlink bool) error { + return filepath.WalkDir(realRoot, func(path string, dirEntry fs.DirEntry, err error) error { + // map the current root under display root + displayPath := path + if rel, relErr := filepath.Rel(realRoot, path); relErr == nil { + displayPath = filepath.Join(displayRoot, rel) + } + + if w.cf.followSymlinks && dirEntry != nil && dirEntry.IsDir() { + log.Tracef("adding into walked paths of the filewalker: %s", path) + w.visited = append(w.visited, path) + } + + // at the real root, isSymlink is not determined through recursion. + // it is passed as usedSymlink before calling walk() for the first time + isSymlink := false + entryIsLink := false + + if path == realRoot { + isSymlink = usedSymlink + // when entered via a symlink, the link itself was already + // recorded by followSymlink, skip re-adding the root + if usedSymlink { + return nil + } + } else if dirEntry != nil { + if fi, fie := dirEntry.Info(); fie == nil && isLink(fi) { + isSymlink = true + entryIsLink = true + } + } + + if err != nil { + return w.cf.addFile(w.check, displayPath, w.checkPath, dirEntry, isSymlink, err, w.seenInodes) + } + + if entryIsLink { + if w.cf.followSymlinks { + linkDepth := w.cf.getDepth(displayPath, w.checkPath) + if w.cf.maxDepth == -1 || linkDepth < w.cf.maxDepth { + return w.followSymlink(path, displayPath) + } + } + + // always add links, but only follow them if followSymlinks is on + return w.cf.addFile(w.check, displayPath, w.checkPath, dirEntry, isSymlink, nil, w.seenInodes) + } + + return w.cf.addFile(w.check, displayPath, w.checkPath, dirEntry, isSymlink, err, w.seenInodes) + }) +} + +// resolveLink resolves a symlink or junction to its canonical target path. +// This resolves junctions on Windows, filepath.EvalSymlinks can fail to do so +func resolveLink(linkPath string) (string, error) { + target, err := os.Readlink(linkPath) + if err != nil { + return "", fmt.Errorf("error when reading link details for path: %s , error: %w", linkPath, err) + } + if !filepath.IsAbs(target) { + target = filepath.Join(filepath.Dir(linkPath), target) + } + + return filepath.Clean(target), nil +} + +// followSymlink resolves the symlink at linkPath +// if its a file, add it as a file entry +// if its a directory, record it and let WalkDir descend normally +// keep a list of visited directories to prevent infinite recursion +func (w *fileWalker) followSymlink(linkPath, displayPath string) error { + // os.Stat follows the link and therefore also resolves relative targets correctly + info, err := os.Stat(linkPath) + if err != nil { + // broken symlink (dangling target, loop, ...): record it as an errored file + return w.cf.addFile(w.check, displayPath, w.checkPath, nil, true, err, w.seenInodes) + } + + if !info.IsDir() { + return w.cf.addFile(w.check, displayPath, w.checkPath, fs.FileInfoToDirEntry(info), true, nil, w.seenInodes) + } + + resolvedSymlink, err := resolveLink(linkPath) + if err != nil { + // fallback to filepath.EvalSymlinks + resolvedSymlink, err = filepath.EvalSymlinks(linkPath) + } + if err != nil { + return w.cf.addFile(w.check, displayPath, w.checkPath, nil, true, err, w.seenInodes) + } + + if slices.Contains(w.visited, resolvedSymlink) { + log.Tracef("not descending into symlink %s, target %s already visited", linkPath, resolvedSymlink) + } else { + w.visited = append(w.visited, resolvedSymlink) + } + + // record the symlink/junction itself, let WalkDir descend into it naturally + addErr := w.cf.addFile(w.check, displayPath, w.checkPath, fs.FileInfoToDirEntry(info), true, nil, w.seenInodes) + if addErr != nil && !errors.Is(addErr, filepath.SkipDir) { + return addErr + } + + return nil +} + +//nolint:gocyclo // need to perform a lot of checks before adding a file +func (l *CheckFiles) addFile(check *CheckData, path, checkPath string, dirEntry fs.DirEntry, isSymlink bool, err error, seenInodes map[uint64]bool) error { // if the search path is a directory e.g '/usr/bin' , the program assumes you are looking for files/subdirectories under it // therefore it does not add the search path directory to the entry list // if it is a file like /usr/bin/bash however, it will add that @@ -171,6 +315,7 @@ func (l *CheckFiles) addFile(check *CheckData, path, checkPath string, dirEntry "fullname": path, "type": "file", "check_path": checkPath, + "is_symlink": fmt.Sprintf("%t", isSymlink), } matchAndAdd := func() { @@ -225,17 +370,36 @@ func (l *CheckFiles) addFile(check *CheckData, path, checkPath string, dirEntry } // check filter and pattern before doing more expensive things - // pattern matching is only done on files, directories are always added + // pattern matching is only done on files and to their filenames, directories are always added // directories that do not have any matched files under them are later removed if match, _ := filepath.Match(l.pattern, entry["filename"]); entry["type"] == "file" && !match { log.Tracef("filename: %s did not match the pattern: %s , skipping", entry["filename"], l.pattern) return nil } + // check if it passes through the check filter if !check.MatchMapCondition(check.filter, entry, true) { return nil } + var fileInfo fs.FileInfo + var fileInfoErr error + + if dirEntry != nil { + fileInfo, fileInfoErr = dirEntry.Info() + } + + if dirEntry != nil && entry["type"] == "file" && !isSymlink && fileInfo != nil && fileInfoErr == nil { + if inode, ok := getFileInode(fileInfo); ok && inode > 0 { + if seenInodes[inode] { + log.Tracef("skipping hard link (inode %d already seen): %s", inode, path) + + return nil + } + seenInodes[inode] = true + } + } + defer matchAndAdd() // check for errors here, maybe the file would have been filtered out anyway @@ -245,16 +409,27 @@ func (l *CheckFiles) addFile(check *CheckData, path, checkPath string, dirEntry return nil } - fileInfo, err := dirEntry.Info() - if err != nil { + if fileInfoErr != nil { if dirEntry != nil && dirEntry.IsDir() { return fs.SkipDir } - l.setError(entry, err) + l.setError(entry, fileInfoErr) return nil } + if err := l.populateEntryDetails(check, entry, fileInfo, path); err != nil { + return err + } + + if err := checkSlowFileOperations(check, entry, path); err != nil { + return err + } + + return nil +} + +func (l *CheckFiles) populateEntryDetails(check *CheckData, entry map[string]string, fileInfo fs.FileInfo, path string) error { fileInfoSys, err := getCheckFileTimes(fileInfo) if err != nil { return fmt.Errorf("type assertion for fileInfo.Sys() failed") @@ -276,10 +451,6 @@ func (l *CheckFiles) addFile(check *CheckData, path, checkPath string, dirEntry entry["version"] = version } - if err := checkSlowFileOperations(check, entry, path); err != nil { - return err - } - return nil } diff --git a/pkg/snclient/check_files_helper_linux.go b/pkg/snclient/check_files_helper_linux.go index 017fab2a..319414d0 100644 --- a/pkg/snclient/check_files_helper_linux.go +++ b/pkg/snclient/check_files_helper_linux.go @@ -23,3 +23,16 @@ func getCheckFileTimes(fileInfo fs.FileInfo) (*FileInfoUnified, error) { func getFileVersion(path string) (string, error) { return "0.0.0.0", fmt.Errorf("file version not supported: %s", path) } + +func isLink(fi fs.FileInfo) bool { + return fi.Mode()&fs.ModeSymlink != 0 +} + +func getFileInode(fi fs.FileInfo) (uint64, bool) { + stat, ok := fi.Sys().(*syscall.Stat_t) + if !ok { + return 0, false + } + + return stat.Ino, true +} diff --git a/pkg/snclient/check_files_helper_osx_bsd.go b/pkg/snclient/check_files_helper_osx_bsd.go index fbcfa524..753af6fd 100644 --- a/pkg/snclient/check_files_helper_osx_bsd.go +++ b/pkg/snclient/check_files_helper_osx_bsd.go @@ -25,3 +25,16 @@ func getCheckFileTimes(fileInfo fs.FileInfo) (*FileInfoUnified, error) { func getFileVersion(path string) (string, error) { return "0.0.0.0", fmt.Errorf("file version not supported: %s", path) } + +func isLink(fi fs.FileInfo) bool { + return fi.Mode()&fs.ModeSymlink != 0 +} + +func getFileInode(fi fs.FileInfo) (uint64, bool) { + stat, ok := fi.Sys().(*syscall.Stat_t) + if !ok { + return 0, false + } + + return stat.Ino, true +} diff --git a/pkg/snclient/check_files_helper_windows.go b/pkg/snclient/check_files_helper_windows.go index cfec8998..8dc9394a 100644 --- a/pkg/snclient/check_files_helper_windows.go +++ b/pkg/snclient/check_files_helper_windows.go @@ -30,3 +30,15 @@ func getFileVersion(path string) (string, error) { return f.FixedInfo().FileVersion.String(), nil } + +func isLink(fi fs.FileInfo) bool { + if data, ok := fi.Sys().(*syscall.Win32FileAttributeData); ok { + return data.FileAttributes&syscall.FILE_ATTRIBUTE_REPARSE_POINT != 0 + } + + return fi.Mode()&fs.ModeSymlink != 0 +} + +func getFileInode(_ fs.FileInfo) (uint64, bool) { + return 0, false +} diff --git a/pkg/snclient/check_files_test.go b/pkg/snclient/check_files_test.go index ac5905b0..77081790 100644 --- a/pkg/snclient/check_files_test.go +++ b/pkg/snclient/check_files_test.go @@ -523,3 +523,136 @@ func TestCheckFilesSizePerfdata(t *testing.T) { StopTestAgent(t, snc) } + +func TestCheckFilesFilesystemLinks(t *testing.T) { + testDir, _ := os.Getwd() + scriptsDir := filepath.Join(testDir, "t", "scripts") + scriptName := "check_files_filesystem_links" + scriptFilename := scriptName + + switch runtime.GOOS { + case "windows": + scriptFilename += ".ps1" + case "linux": + scriptFilename += ".sh" + case "darwin": + scriptFilename += ".sh" + default: + t.Skipf("Test is not intended to be run on %s", runtime.GOOS) + } + + config := checkFilesTestConfigWithScript(t, scriptsDir, scriptName, scriptFilename) + snc := StartTestAgent(t, config) + + geneartionDirectory := t.TempDir() + + res := snc.RunCheck(scriptName, []string{geneartionDirectory}) + assert.Equalf(t, CheckExitOK, res.State, "script return state check is correct") + outputString := string(res.BuildPluginOutput()) + + // check_files_filesystem_links script generates a structure using the filesystem links + + switch runtime.GOOS { + case "windows": + + // the powershell script generates a filetree that looks like this + // \link_test + // │ file1_hardlink1.txt + // │ file1_symlink1.txt + // ├───dir1 + // │ file1.txt + // ├───dir1_junction1 + // │ file1.txt + // ├───dir1_symlink1 + // └───file1.txt + + assert.Containsf(t, outputString, `symbolic link created for dir1_symlink1 <<===>> dir1`, "output matches") + assert.Containsf(t, outputString, `symbolic link created for file1_symlink1.txt <<===>> dir1\file1.txt`, "output matches") + assert.Containsf(t, outputString, `Hardlink created for file1_hardlink1.txt <<===>> dir1\file1.txt`, "output matches") + assert.Containsf(t, outputString, `Junction created for dir1_junction1 <<===>> dir1`, "output matches") + + res = snc.RunCheck("check_files", []string{"path=" + geneartionDirectory, "filter=type eq file", "show-all"}) + assert.Equalf(t, CheckExitOK, res.State, "state OK") + outputString = string(res.BuildPluginOutput()) + assert.Containsf(t, outputString, "file1.txt", "output has file") + assert.Containsf(t, outputString, "file1_symlink1.txt", "output does have symlink file") + assert.Containsf(t, outputString, "file1_hardlink1.txt", "output does have hardlink file") + assert.NotContainsf(t, outputString, "dir1", "output does not have directory") + assert.NotContainsf(t, outputString, "dir1_junction1", "output does not have junction directory") + + res = snc.RunCheck("check_files", []string{"path=" + geneartionDirectory, "filter=type eq file and is_symlink eq true", "show-all"}) + assert.Equalf(t, CheckExitOK, res.State, "state OK") + outputString = string(res.BuildPluginOutput()) + assert.Containsf(t, outputString, "file1_symlink1.txt", "output does have symlink file") + assert.NotContainsf(t, outputString, "file1_hardlink1.txt", "output does not have hardlink file") + assert.NotContainsf(t, outputString, "dir1", "output does not have directory") + assert.NotContainsf(t, outputString, "dir1_junction1", "output does not have junction directory") + + res = snc.RunCheck("check_files", []string{"path=" + geneartionDirectory, "filter=type eq dir", "show-all"}) + assert.Equalf(t, CheckExitOK, res.State, "state OK") + outputString = string(res.BuildPluginOutput()) + assert.Containsf(t, outputString, "dir1", "output has directory") + assert.Containsf(t, outputString, "dir1_junction1", "output has directory") + assert.Containsf(t, outputString, "dir1_symlink1", "output has directory") + + res = snc.RunCheck("check_files", []string{"path=" + geneartionDirectory, "filter=type eq dir and is_symlink eq true", "show-all"}) + assert.Equalf(t, CheckExitOK, res.State, "state OK") + outputString = string(res.BuildPluginOutput()) + assert.Containsf(t, outputString, "dir1_junction1", "output has directory") + assert.Containsf(t, outputString, "dir1_symlink1", "output has directory") + assert.NotContainsf(t, strings.FieldsFunc(outputString, func(r rune) bool { return r == ' ' || r == ',' }), "dir1", "output does not have directory") + + case "linux", "darwin": + + // bash script generates a filetree that looks like this + // /link_test + // ├── dir1 + // │ └── file1.txt + // ├── dir1_relative1 -> dir1 + // ├── dir1_symbolic1 -> dir1 + // ├── file1_physical1.txt + // ├── file1_relative1.txt -> dir1/file1.txt + // └── file1_symbolic1.txt -> dir1/file1.txt + + assert.Containsf(t, outputString, "ok - Generated 2 files for testing", "output matches") + assert.Containsf(t, outputString, "ok - Generated 1 directories for testing", "output matches") + + res = snc.RunCheck("check_files", []string{"path=" + geneartionDirectory, "filter=type eq file", "show-all"}) + assert.Equalf(t, CheckExitOK, res.State, "state OK") + outputString = string(res.BuildPluginOutput()) + assert.Containsf(t, outputString, "file1.txt", "output has file") + assert.Containsf(t, outputString, "file1_symbolic1.txt", "output does have symlink file") + assert.Containsf(t, outputString, "file1_relative1.txt", "output does have relative symlink file") + assert.NotContainsf(t, outputString, "file1_physical1.txt", "output does not have hardlink file") + assert.NotContainsf(t, outputString, "dir1", "output does not have directory") + + res = snc.RunCheck("check_files", []string{"path=" + geneartionDirectory, "filter=type eq file"}) + assert.Equalf(t, CheckExitOK, res.State, "state OK") + assert.Containsf(t, string(res.BuildPluginOutput()), "All 3 files are ok", "should count 3 files (excluding hard link)") + + res = snc.RunCheck("check_files", []string{"path=" + geneartionDirectory, "filter=type eq file and is_symlink eq true", "show-all"}) + assert.Equalf(t, CheckExitOK, res.State, "state OK") + outputString = string(res.BuildPluginOutput()) + assert.Containsf(t, outputString, "file1_symbolic1.txt", "output does have symlink file") + assert.Containsf(t, outputString, "file1_relative1.txt", "output does have relative symlink file") + assert.NotContainsf(t, outputString, "file1_physical1.txt", "output does not have hardlink file") + assert.NotContainsf(t, outputString, "file1.txt", "output does not have regular file") + assert.NotContainsf(t, outputString, "dir1", "output does not have directory") + + res = snc.RunCheck("check_files", []string{"path=" + geneartionDirectory, "filter=type eq dir", "show-all"}) + assert.Equalf(t, CheckExitOK, res.State, "state OK") + outputString = string(res.BuildPluginOutput()) + assert.Containsf(t, outputString, "dir1", "output has directory") + assert.Containsf(t, outputString, "dir1_symbolic1", "output has symlink directory") + assert.Containsf(t, outputString, "dir1_relative1", "output has relative symlink directory") + + res = snc.RunCheck("check_files", []string{"path=" + geneartionDirectory, "filter=type eq dir and is_symlink eq true", "show-all"}) + assert.Equalf(t, CheckExitOK, res.State, "state OK") + outputString = string(res.BuildPluginOutput()) + assert.Containsf(t, outputString, "dir1_symbolic1", "output has symlink directory") + assert.Containsf(t, outputString, "dir1_relative1", "output has relative symlink directory") + assert.NotContainsf(t, strings.FieldsFunc(outputString, func(r rune) bool { return r == ' ' || r == ',' }), "dir1", "output does not have regular directory") + } + + StopTestAgent(t, snc) +} diff --git a/pkg/snclient/t/scripts/check_files_filesystem_links.ps1 b/pkg/snclient/t/scripts/check_files_filesystem_links.ps1 new file mode 100644 index 00000000..33c3494c --- /dev/null +++ b/pkg/snclient/t/scripts/check_files_filesystem_links.ps1 @@ -0,0 +1,56 @@ +#!/usr/bin/env pwsh + +param( + [Parameter(Mandatory=$true)] + [string]$TestingDir +) + +$ErrorActionPreference = 'Stop' + +New-Item -ItemType Directory -Path $TestingDir -Force + +Push-Location -Path $TestingDir +try { + # Since the new location is pushed on to the stack, it is automatically applied. + + New-Item -ItemType Directory -Path "dir1" -Force > $null + + New-Item -ItemType File -Path "dir1/file1.txt" -Force > $null + $string = "This is a test line.`r`n" + $content = $string * 100 + Set-Content -Path "dir1/file1.txt" -Value $content -NoNewline + + # Symbolic link to folder + cmd /c mklink /d "dir1_symlink1" "dir1" + + # Symbolic link to file + cmd /c mklink "file1_symlink1.txt" "dir1\file1.txt" + + # Hard link to file + cmd /c mklink /h "file1_hardlink1.txt" "dir1\file1.txt" + + # Junction to folder + cmd /c mklink /j "dir1_junction1" "dir1" + + $fileCount = (Get-ChildItem -Recurse -File | Measure-Object).Count + Write-Output "ok - Generated $fileCount files for testing" + + $dirCount = (Get-ChildItem -Recurse -Directory | Measure-Object).Count + Write-Output "ok - Generated $dirCount directories for testing" + + Write-Output "printing the tree of the files" + # if tree is available, use it. Otherwise we have to use a for loop + if (Get-Command tree -ErrorAction SilentlyContinue) { + tree /f . + } else { + Get-ChildItem -Recurse | Sort-Object FullName | ForEach-Object { + $relativePath = $_.FullName.Substring($PWD.Path.Length).TrimStart([io.path]::DirectorySeparatorChar) + Write-Output $relativePath + } + } +} +finally { + Pop-Location +} + +exit 0 diff --git a/pkg/snclient/t/scripts/check_files_filesystem_links.sh b/pkg/snclient/t/scripts/check_files_filesystem_links.sh new file mode 100755 index 00000000..3fb5da16 --- /dev/null +++ b/pkg/snclient/t/scripts/check_files_filesystem_links.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +# This script is intended to generate systemlink files to be used in a test +# These files need to be generated dynamically, so they cant be simply saved in the repository. + +set -o errexit +set -o nounset +set -o pipefail + +TESTING_DIR="$1" + +mkdir -p "${TESTING_DIR}" + +( + cd "${TESTING_DIR}" + + mkdir -p "dir1" + + touch "dir1/file1.txt" + echo > "dir1/file1.txt" + + for ((i=1; i<=100; i++)); do + printf 'This is a test line \n' >> "dir1/file1.txt" + done + + # Symbolic link to folder + ln -s "dir1" "dir1_symbolic1" + + # Symbolic link to file + ln -s "dir1/file1.txt" "file1_symbolic1.txt" + + # Relative link to folder + ln -s "dir1" "dir1_relative1" + + # Relative link to file + ln -s "dir1/file1.txt" "file1_relative1.txt" + + # Physical links to directories are not allowed + # ln "dir1" "dir1_physical1" + + # Physical link to file + ln "dir1/file1.txt" "file1_physical1.txt" + + FILE_COUNT=$(find "${TESTING_DIR}" -type f | wc -l) + echo "ok - Generated ${FILE_COUNT} files for testing" + + DIRECTORY_COUNT=$(find "${TESTING_DIR}" -type d | wc -l) + echo "ok - Generated $(( DIRECTORY_COUNT - 1 )) directories for testing" + + if command -v tree >/dev/null 2>&1; then + echo "printing the tree of the files" + tree "${TESTING_DIR}" + else + echo "warning: tree command not found, skipping tree output" + fi +) + +exit 0