diff --git a/internal/webindexer/local.go b/internal/webindexer/local.go
index c769a38..fac5e7c 100644
--- a/internal/webindexer/local.go
+++ b/internal/webindexer/local.go
@@ -16,8 +16,8 @@ type LocalBackend struct {
var _ FileSource = &LocalBackend{}
-func (l *LocalBackend) Read(path string) ([]Item, bool, error) {
- var items []Item
+func (l *LocalBackend) Read(path string) ([]*Item, bool, error) {
+ var items []*Item
log.Debugf("Listing files in %s", path)
files, err := os.ReadDir(path)
if err != nil {
@@ -38,7 +38,7 @@ func (l *LocalBackend) Read(path string) ([]Item, bool, error) {
log.Infof("Skipping indexing of %s (found skipindex file %s), will include in parent directory", path, file.Name())
// Return empty items but mark as not having noindex file
// This will prevent indexing this directory but still include it in the parent
- return []Item{}, false, nil
+ return []*Item{}, false, nil
}
}
}
@@ -76,15 +76,13 @@ func (l *LocalBackend) Read(path string) ([]Item, bool, error) {
}
}
- size := humanizeBytes(stat.Size())
- modified := stat.ModTime().Format(l.cfg.DateFormat)
-
itemName := file.Name()
- item := Item{
+ item := &Item{
Name: itemName,
- Size: size,
- LastModified: modified,
+ Size: stat.Size(),
+ LastModified: stat.ModTime(),
IsDir: stat.IsDir(),
+ HasMetadata: true,
}
items = append(items, item)
diff --git a/internal/webindexer/s3.go b/internal/webindexer/s3.go
index cc2a280..7fe9cbf 100644
--- a/internal/webindexer/s3.go
+++ b/internal/webindexer/s3.go
@@ -23,7 +23,7 @@ type S3API interface {
var _ FileSource = &S3Backend{}
-func (s *S3Backend) Read(prefix string) ([]Item, bool, error) {
+func (s *S3Backend) Read(prefix string) ([]*Item, bool, error) {
// Ensure the prefix has a trailing slash for s3 keys
if !strings.HasSuffix(prefix, "/") {
prefix = prefix + "/"
@@ -62,11 +62,11 @@ func (s *S3Backend) Read(prefix string) ([]Item, bool, error) {
log.Infof("Skipping indexing of %s/%s (found skipindex file %s), will include in parent directory", s.bucket, prefix, fileName)
// Return empty items but mark as not having noindex file
// This will prevent indexing this directory but still include it in the parent
- return []Item{}, false, nil
+ return []*Item{}, false, nil
}
}
- var items []Item
+ var items []*Item
// Process all other files
for _, content := range resp.Contents {
if shouldSkip(*content.Key, s.cfg.IndexFile, s.cfg.Skips) {
@@ -76,11 +76,12 @@ func (s *S3Backend) Read(prefix string) ([]Item, bool, error) {
// Get the relative name by removing the prefix
itemName := strings.TrimPrefix(*content.Key, prefix)
- item := Item{
+ item := &Item{
Name: itemName,
- Size: humanizeBytes(*content.Size),
- LastModified: content.LastModified.Format(s.cfg.DateFormat),
+ Size: *content.Size,
+ LastModified: *content.LastModified,
IsDir: false,
+ HasMetadata: true,
}
items = append(items, item)
@@ -117,7 +118,7 @@ func (s *S3Backend) Read(prefix string) ([]Item, bool, error) {
}
dirName := strings.TrimPrefix(*commonPrefix.Prefix, prefix)
- item := Item{
+ item := &Item{
Name: dirName,
IsDir: true,
}
diff --git a/internal/webindexer/sort.go b/internal/webindexer/sort.go
index 7763592..61d1608 100644
--- a/internal/webindexer/sort.go
+++ b/internal/webindexer/sort.go
@@ -6,7 +6,7 @@ import (
"unicode"
)
-func (i *Indexer) sort(items *[]Item) {
+func (i *Indexer) sort(items *[]TemplateItem) {
switch i.Cfg.SortByValue() {
case SortByDate:
orderByLastModified(items)
@@ -27,13 +27,13 @@ func (i *Indexer) sort(items *[]Item) {
}
}
-func orderByName(items *[]Item) {
+func orderByName(items *[]TemplateItem) {
sort.SliceStable(*items, func(i, j int) bool {
return (*items)[i].Name < (*items)[j].Name
})
}
-func orderByLastModified(items *[]Item) {
+func orderByLastModified(items *[]TemplateItem) {
sort.SliceStable(*items, func(i, j int) bool {
return (*items)[i].LastModified > (*items)[j].LastModified
})
@@ -41,7 +41,7 @@ func orderByLastModified(items *[]Item) {
// orderByNaturalName sorts items by their names with numbers ordered
// naturally. e.g. 1,2,10 instead of 1,10,2 or 0.8.2 before 0.8.10
-func orderByNaturalName(items *[]Item) {
+func orderByNaturalName(items *[]TemplateItem) {
sort.SliceStable(*items, func(i, j int) bool {
return cmpNatural((*items)[i].Name, (*items)[j].Name)
})
@@ -95,7 +95,7 @@ func cmpNatural(a, b string) bool {
return len(aSegments) < len(bSegments)
}
-func orderDirsFirst(items *[]Item) {
+func orderDirsFirst(items *[]TemplateItem) {
sort.SliceStable(*items, func(i, j int) bool {
if (*items)[i].IsDir && !(*items)[j].IsDir {
return true
diff --git a/internal/webindexer/sort_test.go b/internal/webindexer/sort_test.go
index cd875c3..5778048 100644
--- a/internal/webindexer/sort_test.go
+++ b/internal/webindexer/sort_test.go
@@ -1,9 +1,11 @@
package webindexer
-import "testing"
+import (
+ "testing"
+)
func TestOrderByName(t *testing.T) {
- items := []Item{
+ items := []TemplateItem{
{Name: "banana"},
{Name: "apple"},
{Name: "cherry"},
@@ -19,7 +21,7 @@ func TestOrderByName(t *testing.T) {
}
func TestOrderByLastModified(t *testing.T) {
- items := []Item{
+ items := []TemplateItem{
{Name: "banana", LastModified: "2020-01-03"},
{Name: "apple", LastModified: "2020-01-01"},
{Name: "cherry", LastModified: "2020-01-02"},
@@ -35,7 +37,7 @@ func TestOrderByLastModified(t *testing.T) {
}
func TestOrderByNaturalName(t *testing.T) {
- items := []Item{
+ items := []TemplateItem{
{Name: "item10"},
{Name: "item2"},
{Name: "item1"},
@@ -51,7 +53,7 @@ func TestOrderByNaturalName(t *testing.T) {
}
func TestOrderDirsFirst(t *testing.T) {
- items := []Item{
+ items := []TemplateItem{
{Name: "file.txt", IsDir: false},
{Name: "folder", IsDir: true},
{Name: "another_folder", IsDir: true},
diff --git a/internal/webindexer/templates/themes/default.html.tmpl b/internal/webindexer/templates/themes/default.html.tmpl
index cf44f75..b6e8378 100644
--- a/internal/webindexer/templates/themes/default.html.tmpl
+++ b/internal/webindexer/templates/themes/default.html.tmpl
@@ -65,11 +65,9 @@
{{ if .ParentURL }}
- |
+ |
🔼Go Up
|
- - |
- - |
{{end}}
{{range .Items}}
@@ -85,14 +83,14 @@
{{.Name}}
- {{if not .IsDir}}
+ {{if .Size }}
{{.Size}}
{{else}}
-
{{end}}
|
- {{if not .IsDir}}
+ {{if .LastModified}}
{{.LastModified}}
{{else}}
-
diff --git a/internal/webindexer/templates/themes/dracula.html.tmpl b/internal/webindexer/templates/themes/dracula.html.tmpl
index 719d752..4c9b064 100644
--- a/internal/webindexer/templates/themes/dracula.html.tmpl
+++ b/internal/webindexer/templates/themes/dracula.html.tmpl
@@ -124,11 +124,9 @@
{{ if .ParentURL }}
|
- |
+ |
🔼Go Up
|
- - |
- - |
{{end}}
{{range .Items}}
@@ -144,14 +142,14 @@
{{.Name}}
- {{if not .IsDir}}
+ {{if .Size}}
{{.Size}}
{{else}}
-
{{end}}
|
- {{if not .IsDir}}
+ {{if .LastModified}}
{{.LastModified}}
{{else}}
-
diff --git a/internal/webindexer/templates/themes/nord.html.tmpl b/internal/webindexer/templates/themes/nord.html.tmpl
index ed31d61..28cf619 100644
--- a/internal/webindexer/templates/themes/nord.html.tmpl
+++ b/internal/webindexer/templates/themes/nord.html.tmpl
@@ -135,11 +135,9 @@
{{ if .ParentURL }}
|
- |
+ |
🔼Go Up
|
- - |
- - |
{{end}}
{{range .Items}}
@@ -155,14 +153,14 @@
{{.Name}}
- {{if not .IsDir}}
+ {{if .Size}}
{{.Size}}
{{else}}
-
{{end}}
|
- {{if not .IsDir}}
+ {{if .LastModified}}
{{.LastModified}}
{{else}}
-
diff --git a/internal/webindexer/templates/themes/solarized.html.tmpl b/internal/webindexer/templates/themes/solarized.html.tmpl
index 836cc27..4c264b3 100644
--- a/internal/webindexer/templates/themes/solarized.html.tmpl
+++ b/internal/webindexer/templates/themes/solarized.html.tmpl
@@ -128,11 +128,9 @@
{{ if .ParentURL }}
|
- |
+ |
🔼Go Up
|
- - |
- - |
{{end}}
{{range .Items}}
@@ -148,14 +146,14 @@
{{.Name}}
- {{if not .IsDir}}
+ {{if .Size}}
{{.Size}}
{{else}}
-
{{end}}
|
- {{if not .IsDir}}
+ {{if .LastModified}}
{{.LastModified}}
{{else}}
-
diff --git a/internal/webindexer/webindexer.go b/internal/webindexer/webindexer.go
index e2af563..c93aec5 100644
--- a/internal/webindexer/webindexer.go
+++ b/internal/webindexer/webindexer.go
@@ -11,6 +11,7 @@ import (
"path"
"path/filepath"
"strings"
+ "time"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
@@ -41,7 +42,7 @@ type Indexer struct {
// FileSource is an interface for listing the contents of a directory or S3
// bucket.
type FileSource interface {
- Read(path string) ([]Item, bool, error)
+ Read(path string) ([]*Item, bool, error)
Write(data Data, content string) error
EnsureDirExists(relativePath string) error
}
@@ -49,11 +50,12 @@ type FileSource interface {
// Item represents an S3 key, or a local file/directory.
type Item struct {
Name string
- Size string
- LastModified string
+ Size int64
+ LastModified time.Time
URL string
IsDir bool
Items []Item
+ HasMetadata bool
}
// Data holds the template data.
@@ -63,12 +65,20 @@ type Data struct {
RootPath string
RelativePath string
URL string
- Items []Item
+ Items []TemplateItem
Parent string
HasParent bool
ParentURL string
}
+type TemplateItem struct {
+ Name string
+ Size string
+ LastModified string
+ URL string
+ IsDir bool
+}
+
type BackendSetup interface {
Setup(indexer *Indexer) error
}
@@ -199,7 +209,7 @@ func setupBackend(uri string, indexer *Indexer) (FileSource, error) {
}
// Generate the index file for the given path.
-func (i Indexer) Generate(path string) error {
+func (i Indexer) Generate(parent *Item, path string) error {
var err error
items, hasNoIndex, err := i.Source.Read(path)
@@ -213,17 +223,48 @@ func (i Indexer) Generate(path string) error {
return nil
}
+ // Ensure the target directory exists before attempting to write or recurse
+ relativePath := strings.TrimPrefix(path, i.Cfg.BasePath)
+
+ // Ensure relative path is prefixed with a slash. This will also set an
+ // empty base path to "/" (such as when listing the root of an S3 bucket).
+ // S3 keys don't have a leading slash, but we normalize for consistency
+ if !strings.HasPrefix(relativePath, "/") {
+ relativePath = "/" + relativePath
+ }
+
+ if err := i.Target.EnsureDirExists(relativePath); err != nil {
+ return fmt.Errorf("failed to ensure target directory exists for %s: %w", relativePath, err)
+ }
+
+ // Process items to handle recursion.
+ // This loop won't execute if items is empty.
+ for _, item := range items { // Iterate over original items
+ err := i.parseItem(path, item) // Pass item by value, check error
+ if err != nil {
+ // Stop processing if any subdirectory fails? Or just log?
+ // Return the error to propagate it up.
+ log.Errorf("Error generating index for %s: %v", item.Name, err)
+ return err
+ }
+
+ if parent != nil {
+ parent.Size += item.Size
+
+ if !parent.HasMetadata || item.LastModified.After(parent.LastModified) {
+ parent.LastModified = item.LastModified
+ }
+
+ parent.HasMetadata = true
+ }
+ }
+
// Prepare template data regardless of whether items were found
- data, err := i.data(items, path)
+ data, err := i.data(items, path, relativePath)
if err != nil {
return err
}
- // Ensure the target directory exists before attempting to write or recurse
- if err := i.Target.EnsureDirExists(data.RelativePath); err != nil {
- return fmt.Errorf("failed to ensure target directory exists for %s: %w", data.RelativePath, err)
- }
-
// Only generate and write the index file if there are items to list.
// This handles the skipindex case (Read returns empty items) and empty directories.
if len(items) > 0 {
@@ -264,17 +305,6 @@ func (i Indexer) Generate(path string) error {
log.Debugf("Skipping index file generation for %s (no items or skipindex found)", path)
}
- // Process items to handle recursion.
- // This loop won't execute if items is empty.
- for _, item := range items { // Iterate over original items
- err := i.parseItem(path, item) // Pass item by value, check error
- if err != nil {
- // Stop processing if any subdirectory fails? Or just log?
- // Return the error to propagate it up.
- return err
- }
- }
-
return nil
}
@@ -292,19 +322,10 @@ func getThemeTemplate(theme string) string {
}
}
-func (i Indexer) data(items []Item, path string) (Data, error) {
- relativePath := strings.TrimPrefix(path, i.Cfg.BasePath)
-
- // Ensure relative path is prefixed with a slash. This will also set an
- // empty base path to "/" (such as when listing the root of an S3 bucket).
- // S3 keys don't have a leading slash, but we normalize for consistency
- if !strings.HasPrefix(relativePath, "/") {
- relativePath = "/" + relativePath
- }
-
+func (i Indexer) data(items []*Item, path, relativePath string) (Data, error) {
data := Data{
RootPath: i.Cfg.BasePath,
- Items: make([]Item, 0, len(items)),
+ Items: make([]TemplateItem, 0, len(items)),
Path: path,
RelativePath: relativePath,
URL: i.Cfg.BaseURL,
@@ -328,7 +349,7 @@ func (i Indexer) data(items []Item, path string) (Data, error) {
}
// Process items within the data function to set their URLs
- processedItems := make([]Item, 0, len(items))
+ processedItems := make([]TemplateItem, 0, len(items))
for _, item := range items {
processedItem, err := i.processItemForData(path, item) // Rename to avoid confusion with recursive call
if err != nil {
@@ -343,7 +364,7 @@ func (i Indexer) data(items []Item, path string) (Data, error) {
}
// processItemForData generates the URL for an item. Does NOT handle recursion.
-func (i Indexer) processItemForData(path string, item Item) (Item, error) {
+func (i Indexer) processItemForData(path string, item *Item) (TemplateItem, error) {
// Calculate the relative path by removing the base path
relativePath := strings.TrimPrefix(path, i.Cfg.BasePath)
// Ensure relative path is prefixed with a slash
@@ -351,18 +372,27 @@ func (i Indexer) processItemForData(path string, item Item) (Item, error) {
relativePath = "/" + relativePath
}
- item.URL = resolveItemURL(i.Cfg.BaseURL, relativePath, item.Name, item.IsDir, i.Cfg.LinkToIndexes, i.Cfg.IndexFile)
- // Return the item with the URL set
- return item, nil
+ processed := TemplateItem{
+ Name: item.Name,
+ URL: resolveItemURL(i.Cfg.BaseURL, relativePath, item.Name, item.IsDir, i.Cfg.LinkToIndexes, i.Cfg.IndexFile),
+ IsDir: item.IsDir,
+ }
+
+ if item.HasMetadata {
+ processed.Size = humanizeBytes(item.Size)
+ processed.LastModified = item.LastModified.Format(i.Cfg.DateFormat)
+ }
+
+ return processed, nil
}
// parseItem handles the recursive call for directories.
-func (i Indexer) parseItem(path string, item Item) error {
+func (i Indexer) parseItem(path string, item *Item) error {
// If the item is a directory and recursive mode is enabled, generate its index
if item.IsDir && i.Cfg.Recursive {
// Construct the full path for the subdirectory
subDirPath := filepath.Join(path, item.Name)
- if err := i.Generate(subDirPath); err != nil {
+ if err := i.Generate(item, subDirPath); err != nil {
// Log the error but also return it to stop processing this branch
log.Errorf("Error generating index for subdirectory %s: %v", subDirPath, err)
return fmt.Errorf("error generating index for subdirectory %s: %w", subDirPath, err)
diff --git a/internal/webindexer/webindexer_test.go b/internal/webindexer/webindexer_test.go
index 5df0be0..9ce53af 100644
--- a/internal/webindexer/webindexer_test.go
+++ b/internal/webindexer/webindexer_test.go
@@ -19,9 +19,9 @@ type MockSource struct {
mock.Mock
}
-func (m *MockSource) Read(path string) ([]Item, bool, error) {
+func (m *MockSource) Read(path string) ([]*Item, bool, error) {
args := m.Called(path)
- return args.Get(0).([]Item), args.Bool(1), args.Error(2)
+ return args.Get(0).([]*Item), args.Bool(1), args.Error(2)
}
func (m *MockSource) Write(data Data, content string) error {
@@ -45,13 +45,13 @@ func TestIndexer_Generate(t *testing.T) {
},
}
- mockSource.On("Read", mock.Anything).Return([]Item{}, false, nil)
+ mockSource.On("Read", mock.Anything).Return([]*Item{}, false, nil)
// Expect EnsureDirExists to be called on the target
mockTarget.On("EnsureDirExists", mock.AnythingOfType("string")).Return(nil)
// Write should NOT be called when Read returns empty items
// mockTarget.On("Write", mock.Anything, mock.Anything).Return(nil)
- err := indexer.Generate("path/to/generate")
+ err := indexer.Generate(nil, "path/to/generate")
assert.NoError(t, err)
mockSource.AssertExpectations(t)
@@ -88,13 +88,13 @@ func TestCustomTemplate(t *testing.T) {
require.NoError(t, err)
- mockSource.On("Read", mock.Anything).Return([]Item{}, false, nil)
+ mockSource.On("Read", mock.Anything).Return([]*Item{}, false, nil)
// Expect EnsureDirExists to be called on the target before writing
mockTarget.On("EnsureDirExists", mock.AnythingOfType("string")).Return(nil)
// Write should NOT be called when Read returns empty items
// mockTarget.On("Write", mock.Anything, mock.Anything).Return(nil)
- err = indexer.Generate("path/to/generate")
+ err = indexer.Generate(nil, "path/to/generate")
assert.NoError(t, err)
// Check the file content
@@ -298,8 +298,8 @@ func TestIndexer_ProcessItemForData(t *testing.T) {
// No need to call setupBackends as we manually set BasePath
// Simulate items
- itemDir := Item{Name: "dir", IsDir: true}
- itemFile1 := Item{Name: "file1.txt", IsDir: false}
+ itemDir := &Item{Name: "dir", IsDir: true}
+ itemFile1 := &Item{Name: "file1.txt", IsDir: false}
// Test directory item
modifiedItemDir, err := indexer.processItemForData(sourceDir, itemDir)
@@ -437,7 +437,7 @@ func TestGenerate_Recursive(t *testing.T) {
}), mock.AnythingOfType("string")).Return(nil).Once()
// 5. Call Generate from the root source path
- err = indexer.Generate(absSourceDir)
+ err = indexer.Generate(nil, absSourceDir)
require.NoError(t, err)
// 6. Assert mock expectations were met
diff --git a/main.go b/main.go
index 345dec4..4994418 100644
--- a/main.go
+++ b/main.go
@@ -88,7 +88,7 @@ func run(args []string) error {
}
log.Infof("Generating index for %s", cfg.Source)
- err = indexer.Generate(indexer.Cfg.BasePath)
+ err = indexer.Generate(nil, indexer.Cfg.BasePath)
if err != nil {
return fmt.Errorf("unable to generate index: %w", err)
}
|