diff --git a/cmd/add.go b/cmd/add.go index f8da624..ed3d175 100644 --- a/cmd/add.go +++ b/cmd/add.go @@ -3,6 +3,7 @@ package cmd import ( "fmt" "os" + "time" "github.com/experimental-software/logbook2/core" "github.com/experimental-software/logbook2/logging" @@ -16,7 +17,7 @@ var addCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { title := args[0] - result, err := core.AddLogEntry(configuration.LogDirectory, title) + result, err := core.AddLogEntry(configuration.LogDirectory, title, time.Now()) if err != nil { logging.Error("Failed to create log entry", err) os.Exit(1) diff --git a/cmd/search.go b/cmd/search.go index 77c909a..ad77271 100644 --- a/cmd/search.go +++ b/cmd/search.go @@ -5,23 +5,44 @@ import ( "fmt" "os" "strings" + "time" "github.com/aquasecurity/table" "github.com/experimental-software/logbook2/core" + "github.com/experimental-software/logbook2/utils" "github.com/spf13/cobra" ) +var FromParameter string +var ToParameter string + var searchCmd = &cobra.Command{ Use: "search [flags] search_term", Aliases: []string{"s"}, Short: "Search for logbook entries", - + PreRunE: func(cmd *cobra.Command, args []string) error { + _, err := time.Parse(utils.RFC3339date, FromParameter) + if err != nil { + return fmt.Errorf("unexpected format for --from flag") + } + _, err = time.Parse(utils.RFC3339date, ToParameter) + if err != nil { + return fmt.Errorf("unexpected format for --to flag") + } + return nil + }, Run: func(cmd *cobra.Command, args []string) { searchTerm := "" if len(args) > 0 { searchTerm = args[0] } - logEntries := core.Search(configuration.LogDirectory, searchTerm) + + from, _ := time.Parse(utils.RFC3339date, FromParameter) + to, _ := time.Parse(utils.RFC3339date, ToParameter) + + logEntries := core.Search( + configuration.LogDirectory, searchTerm, from, to, + ) outputFormat, err := cmd.Flags().GetString("output-format") if err != nil { @@ -61,6 +82,9 @@ var searchCmd = &cobra.Command{ func init() { flags := searchCmd.Flags() + flags.StringVarP(&FromParameter, "from", "f", "1970-01-01", "RFC 3339 formatted date for the earliest considered logbook entry.") + flags.StringVarP(&ToParameter, "to", "t", "2100-01-01", "RFC 3339 formatted date for the latest considered logbook entry.") + flags.VarP(StringChoice([]string{ "table", "list", "json", }), "output-format", "o", "The format in which the log entries are printed to the terminal.\n[table (default), list, json]") diff --git a/core/adding.go b/core/adding.go index b5e544f..572aec7 100644 --- a/core/adding.go +++ b/core/adding.go @@ -9,22 +9,24 @@ import ( "time" ) -func AddLogEntry(baseDirectory, title string) (LogbookEntry, error) { - currentTime := time.Now() +var epoc, _ = time.Parse("2006-01-02", "1970-01-01") +var nextCentury, _ = time.Parse("2006-01-02", "2100-01-01") + +func AddLogEntry(baseDirectory, title string, dateTime time.Time) (LogbookEntry, error) { slug := slugify(title) - dateTime := fmt.Sprintf("%d-0%d-0%dT0%d:0%d", - currentTime.Year(), - currentTime.Month(), - currentTime.Day(), - currentTime.Hour(), - currentTime.Minute(), + formattedDateTime := fmt.Sprintf("%d-0%d-0%dT0%d:0%d", + dateTime.Year(), + dateTime.Month(), + dateTime.Day(), + dateTime.Hour(), + dateTime.Minute(), ) logDirectoryPath := filepath.Join(baseDirectory, - fmt.Sprintf("%d", currentTime.Year()), - fmt.Sprintf("%02d", currentTime.Month()), - fmt.Sprintf("%02d", currentTime.Day()), - fmt.Sprintf("%02d.%02d_%s", currentTime.Hour(), currentTime.Minute(), slug), + fmt.Sprintf("%d", dateTime.Year()), + fmt.Sprintf("%02d", dateTime.Month()), + fmt.Sprintf("%02d", dateTime.Day()), + fmt.Sprintf("%02d.%02d_%s", dateTime.Hour(), dateTime.Minute(), slug), ) err := os.MkdirAll(logDirectoryPath, 0777) if err != nil { @@ -37,7 +39,7 @@ func AddLogEntry(baseDirectory, title string) (LogbookEntry, error) { return LogbookEntry{}, err } - return LogbookEntry{DateTime: dateTime, Title: title, Directory: logDirectoryPath}, nil + return LogbookEntry{DateTime: formattedDateTime, Title: title, Directory: logDirectoryPath}, nil } func slugify(s string) string { diff --git a/core/adding_test.go b/core/adding_test.go index 9c72422..28cb32b 100644 --- a/core/adding_test.go +++ b/core/adding_test.go @@ -3,6 +3,7 @@ package core import ( "os" "testing" + "time" ) func TestAddLogEntry(t *testing.T) { @@ -15,7 +16,7 @@ func TestAddLogEntry(t *testing.T) { defer os.RemoveAll(tempDir) // Act - entry, err := AddLogEntry(tempDir, "This is a new log entry") + entry, err := AddLogEntry(tempDir, "This is a new log entry", time.Now()) // Assert if err != nil { @@ -24,7 +25,7 @@ func TestAddLogEntry(t *testing.T) { if entry.Title != "This is a new log entry" { t.Errorf("AddLogEntry returned wrong title") } - allLogEntries := Search(tempDir, "") + allLogEntries := Search(tempDir, "", epoc, nextCentury) if len(allLogEntries) != 1 { t.Errorf("AddLogEntry returned wrong number of entries") } diff --git a/core/management_test.go b/core/management_test.go index 37e8686..32d762f 100644 --- a/core/management_test.go +++ b/core/management_test.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "testing" + "time" "github.com/experimental-software/logbook2/config" ) @@ -18,11 +19,11 @@ func Test_Delete_happy_path(t *testing.T) { _ = os.RemoveAll(archiveBaseDir) }(logBaseDir) - logEntry, err := AddLogEntry(logBaseDir, "Log entry for archive test") + logEntry, err := AddLogEntry(logBaseDir, "Log entry for archive test", time.Now()) if err != nil { t.Fatal(err) } - searchResultForArchiveBaseDir := Search(logBaseDir, "") + searchResultForArchiveBaseDir := Search(logBaseDir, "", epoc, nextCentury) if len(searchResultForArchiveBaseDir) != 1 { t.Fatal("Expected 1 search result") } @@ -34,7 +35,7 @@ func Test_Delete_happy_path(t *testing.T) { } // Assert - searchResultForLogBaseDir := Search(logBaseDir, "") + searchResultForLogBaseDir := Search(logBaseDir, "", epoc, nextCentury) if len(searchResultForLogBaseDir) != 0 { t.Fatal("Expected empty search result") } @@ -49,7 +50,7 @@ func Test_Archive_happy_path(t *testing.T) { _ = os.RemoveAll(archiveBaseDir) }(logBaseDir) - logEntry, err := AddLogEntry(logBaseDir, "Log entry for archive test") + logEntry, err := AddLogEntry(logBaseDir, "Log entry for archive test", time.Now()) if err != nil { t.Fatal(err) } @@ -66,11 +67,11 @@ func Test_Archive_happy_path(t *testing.T) { } // Assert - searchResultForLogBaseDir := Search(logBaseDir, "") + searchResultForLogBaseDir := Search(logBaseDir, "", epoc, nextCentury) if len(searchResultForLogBaseDir) != 0 { t.Fatal("Expected empty search result") } - searchResultForArchiveBaseDir := Search(archiveBaseDir, "") + searchResultForArchiveBaseDir := Search(archiveBaseDir, "", epoc, nextCentury) if len(searchResultForArchiveBaseDir) != 1 { t.Fatal("Expected 1 search result") } @@ -85,7 +86,7 @@ func Test_Archive_path_in_subdirectory(t *testing.T) { _ = os.RemoveAll(archiveBaseDir) }(logBaseDir) - logEntry, err := AddLogEntry(logBaseDir, "Log entry for archive test") + logEntry, err := AddLogEntry(logBaseDir, "Log entry for archive test", time.Now()) if err != nil { t.Fatal(err) } @@ -103,11 +104,11 @@ func Test_Archive_path_in_subdirectory(t *testing.T) { } // Assert - searchResultForLogBaseDir := Search(logBaseDir, "") + searchResultForLogBaseDir := Search(logBaseDir, "", epoc, nextCentury) if len(searchResultForLogBaseDir) != 0 { t.Fatal("Expected empty search result") } - searchResultForArchiveBaseDir := Search(archiveBaseDir, "") + searchResultForArchiveBaseDir := Search(archiveBaseDir, "", epoc, nextCentury) if len(searchResultForArchiveBaseDir) != 1 { t.Fatal("Expected 1 search result") } diff --git a/core/searching.go b/core/searching.go index 1dd77f8..bddd2ea 100644 --- a/core/searching.go +++ b/core/searching.go @@ -6,13 +6,14 @@ import ( "path/filepath" "regexp" "strings" + "time" "github.com/experimental-software/logbook2/logging" ) var searchPathPattern = regexp.MustCompile(`.*[/\\](\d{4})[/\\](\d{2})[/\\](\d{2})[/\\](\d{2})\.(\d{2})_.*`) -func Search(baseDirectory, searchTerm string) []LogbookEntry { +func Search(baseDirectory, searchTerm string, from time.Time, to time.Time) []LogbookEntry { var result = make([]LogbookEntry, 0) err := filepath.Walk(baseDirectory, func(path string, info os.FileInfo, err error) error { @@ -30,6 +31,9 @@ func Search(baseDirectory, searchTerm string) []LogbookEntry { pathDatetimeMatch[4], pathDatetimeMatch[5], ) + if !isInRequestedTimeRange(logDatetime, from, to) { + return nil + } logFileBytes, err := os.ReadFile(path) if err != nil { @@ -56,6 +60,10 @@ func Search(baseDirectory, searchTerm string) []LogbookEntry { return result } +func isInRequestedTimeRange(datetime string, from time.Time, to time.Time) bool { + return datetime >= from.Format(time.RFC3339) && datetime <= to.Format(time.RFC3339) +} + func isLogEntryFile(path string) bool { pathParts := strings.Split(path, string(os.PathSeparator)) lastPathPart := pathParts[len(pathParts)-1] diff --git a/core/searching_test.go b/core/searching_test.go index c06d49a..d093ecc 100644 --- a/core/searching_test.go +++ b/core/searching_test.go @@ -3,27 +3,74 @@ package core import ( "fmt" "testing" + "time" + + "github.com/experimental-software/logbook2/utils" ) -func TestSearchWithoutSearchTerm(t *testing.T) { - result := Search("./t/2026/01", "") +func Test_Search_without_search_term(t *testing.T) { + result := Search("./t/2026/01", "", epoc, nextCentury) if len(result) != 2 { t.Errorf("Expected two search results but found %v", len(result)) } } -func TestSearchWithSearchTerm(t *testing.T) { - result := Search("./t/2026/01", "ANOTHER") +func Test_Search_with_search_term(t *testing.T) { + result := Search("./t/2026/01", "ANOTHER", epoc, nextCentury) if len(result) != 1 { t.Errorf("Expected only one search result but found %v", len(result)) } fmt.Println(result) } -func TestIgnoreUnexpectedData(t *testing.T) { - result := Search("./t/2023", "") +func Test_Search_ignore_unexpected_data(t *testing.T) { + result := Search("./t/2023", "", epoc, nextCentury) if len(result) != 0 { t.Errorf("Expected no search result but found %v", len(result)) } fmt.Println(result) } + +func Test_isInRequestedTimeRange(t *testing.T) { + + testCases := []struct { + name string + dateTime string + from string + to string + expected bool + }{ + { + name: "happy path", + dateTime: "2025-12-12T20:25", + from: "1970-01-01", + to: "2100-01-01", + expected: true, + }, + { + name: "ignore too early", + dateTime: "2025-12-12T20:25", + from: "2025-12-13", + to: "2100-01-01", + expected: false, + }, + { + name: "ignore too late", + dateTime: "2025-12-12T20:25", + from: "1970-01-01", + to: "2025-12-11", + expected: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + from, _ := time.Parse(utils.RFC3339date, tc.from) + to, _ := time.Parse(utils.RFC3339date, tc.to) + result := isInRequestedTimeRange(tc.dateTime, from, to) + if result != tc.expected { + t.Errorf("expected: %v, got: %v", tc.expected, result) + } + }) + } +} diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 0000000..3bc71c8 --- /dev/null +++ b/utils/utils.go @@ -0,0 +1,3 @@ +package utils + +const RFC3339date = "2006-01-02"