Fix s3 file+dir handling

This commit is contained in:
Pim van Pelt
2025-12-03 12:14:13 +01:00
parent 2274372119
commit 11fbbd4b42
2 changed files with 162 additions and 69 deletions

View File

@@ -28,6 +28,13 @@ type S3Config struct {
UseSSL bool UseSSL bool
} }
// S3Object represents an S3 object
type S3Object struct {
Key string
Size int64
LastModified time.Time
}
// parseS3URL parses S3 URL and extracts endpoint and bucket // parseS3URL parses S3 URL and extracts endpoint and bucket
// Example: http://minio0.chbtl0.net.ipng.ch:9000/ctlog-ro // Example: http://minio0.chbtl0.net.ipng.ch:9000/ctlog-ro
// Returns: endpoint=minio0.chbtl0.net.ipng.ch:9000, bucket=ctlog-ro, useSSL=false // Returns: endpoint=minio0.chbtl0.net.ipng.ch:9000, bucket=ctlog-ro, useSSL=false
@@ -62,15 +69,6 @@ func parseS3URL(s3URL string) (*S3Config, error) {
// processS3Bucket processes an S3 bucket and generates index files // processS3Bucket processes an S3 bucket and generates index files
func processS3Bucket(s3Config *S3Config, opts *indexgen.Options) error { func processS3Bucket(s3Config *S3Config, opts *indexgen.Options) error {
if opts.DryRun {
// In dry run mode, just show what would be done without connecting
fmt.Printf("Would connect to S3 endpoint: %s\n", s3Config.Endpoint)
fmt.Printf("Would list objects in bucket: %s\n", s3Config.Bucket)
fmt.Printf("Would write S3 index file: %s\n", opts.OutputFile)
fmt.Printf("Note: Dry run mode - no actual S3 connection made\n")
return nil
}
// Get credentials from environment variables // Get credentials from environment variables
accessKey := os.Getenv("AWS_ACCESS_KEY_ID") accessKey := os.Getenv("AWS_ACCESS_KEY_ID")
secretKey := os.Getenv("AWS_SECRET_ACCESS_KEY") secretKey := os.Getenv("AWS_SECRET_ACCESS_KEY")
@@ -108,8 +106,8 @@ func processS3Bucket(s3Config *S3Config, opts *indexgen.Options) error {
return fmt.Errorf("failed to list S3 objects: %w", err) return fmt.Errorf("failed to list S3 objects: %w", err)
} }
// Convert S3 objects to FileEntry format // Collect all S3 objects
var entries []indexgen.FileEntry var allObjects []S3Object
for _, obj := range result.Contents { for _, obj := range result.Contents {
if obj.Key == nil { if obj.Key == nil {
continue continue
@@ -135,67 +133,136 @@ func processS3Bucket(s3Config *S3Config, opts *indexgen.Options) error {
} }
} }
entry := indexgen.FileEntry{ allObjects = append(allObjects, S3Object{
Name: keyName, Key: keyName,
Path: keyName,
IsDir: false,
Size: *obj.Size, Size: *obj.Size,
ModTime: *obj.LastModified, LastModified: *obj.LastModified,
IsSymlink: false, })
IconType: indexgen.GetIconType(keyName),
SizePretty: indexgen.PrettySize(*obj.Size),
ModTimeISO: obj.LastModified.Format(time.RFC3339),
ModTimeHuman: obj.LastModified.Format(time.RFC822),
}
// Set CSS class based on file type
if entry.IsDir {
entry.CSSClass = "dir"
} else if entry.IsSymlink {
entry.CSSClass = "symlink"
} else {
entry.CSSClass = "file"
}
entries = append(entries, entry)
if opts.Verbose { if opts.Verbose {
log.Printf("Found object: %s (%s)", entry.Name, entry.SizePretty) log.Printf("Found object: %s (%s)", keyName, indexgen.PrettySize(*obj.Size))
} }
} }
// Process hierarchical directory structure
return processS3Hierarchy(allObjects, opts, client, s3Config)
}
// processS3Hierarchy processes S3 objects hierarchically like filesystem directories
func processS3Hierarchy(objects []S3Object, opts *indexgen.Options, client *s3.Client, s3Config *S3Config) error {
// Group objects by directory path
dirMap := make(map[string][]indexgen.FileEntry)
// Track all directory paths we need to create indexes for
allDirs := make(map[string]bool)
for _, obj := range objects {
// Split the key into directory parts
parts := strings.Split(obj.Key, "/")
if len(parts) == 1 {
// Root level file
entry := createFileEntry(obj, obj.Key)
dirMap[""] = append(dirMap[""], entry)
} else {
// File in a subdirectory
fileName := parts[len(parts)-1]
dirPath := strings.Join(parts[:len(parts)-1], "/")
// Create file entry
entry := createFileEntry(obj, fileName)
dirMap[dirPath] = append(dirMap[dirPath], entry)
// Track all parent directories
currentPath := ""
for i, part := range parts[:len(parts)-1] {
if i == 0 {
currentPath = part
} else {
currentPath = currentPath + "/" + part
}
allDirs[currentPath] = true
}
}
}
// Add directory entries to parent directories
for dirPath := range allDirs {
parentPath := ""
if strings.Contains(dirPath, "/") {
parts := strings.Split(dirPath, "/")
parentPath = strings.Join(parts[:len(parts)-1], "/")
}
dirName := filepath.Base(dirPath)
// Build the correct path for S3
dirEntryPath := dirPath + "/"
if opts.DirAppend {
dirEntryPath += opts.OutputFile
}
dirEntry := indexgen.FileEntry{
Name: dirName,
Path: dirEntryPath,
IsDir: true,
Size: -1,
IsSymlink: false,
IconType: "folder",
CSSClass: "folder_filled",
SizePretty: "—",
ModTimeISO: time.Now().Format(time.RFC3339),
ModTimeHuman: time.Now().Format(time.RFC822),
}
dirMap[parentPath] = append(dirMap[parentPath], dirEntry)
}
// Set TopDir to bucket name for template generation // Set TopDir to bucket name for template generation
opts.TopDir = s3Config.Bucket opts.TopDir = s3Config.Bucket
// Generate HTML from entries - need to implement this function // Generate index.html for each directory
return generateS3HTML(entries, opts) for dirPath, entries := range dirMap {
indexKey := dirPath
if indexKey != "" {
indexKey += "/"
}
indexKey += opts.OutputFile
err := generateS3HTML(entries, opts, client, s3Config, indexKey)
if err != nil {
return fmt.Errorf("failed to generate index for %s: %w", dirPath, err)
}
}
return nil
} }
// generateS3HTML generates HTML index for S3 objects using the existing template system // createFileEntry creates a FileEntry from an S3Object
func generateS3HTML(entries []indexgen.FileEntry, opts *indexgen.Options) error { func createFileEntry(obj S3Object, displayName string) indexgen.FileEntry {
return indexgen.FileEntry{
Name: displayName,
Path: displayName,
IsDir: false,
Size: obj.Size,
ModTime: obj.LastModified,
IsSymlink: false,
IconType: indexgen.GetIconType(displayName),
CSSClass: "file",
SizePretty: indexgen.PrettySize(obj.Size),
ModTimeISO: obj.LastModified.Format(time.RFC3339),
ModTimeHuman: obj.LastModified.Format(time.RFC822),
}
}
// generateS3HTML generates HTML index for S3 objects and uploads to S3
func generateS3HTML(entries []indexgen.FileEntry, opts *indexgen.Options, client *s3.Client, s3Config *S3Config, indexKey string) error {
// Sort entries by name (similar to filesystem behavior) // Sort entries by name (similar to filesystem behavior)
sort.Slice(entries, func(i, j int) bool { sort.Slice(entries, func(i, j int) bool {
return entries[i].Name < entries[j].Name return entries[i].Name < entries[j].Name
}) })
// Determine output file // Use the provided index key
outputFile := opts.OutputFile
if outputFile == "" {
outputFile = indexgen.DefaultOutputFile
}
if opts.DryRun {
// Dry run mode: show what would be written
fmt.Printf("Would write S3 index file: %s\n", outputFile)
fmt.Printf("S3 bucket: %s\n", opts.TopDir)
fmt.Printf("Objects found: %d\n", len(entries))
for _, entry := range entries {
fmt.Printf(" object: %s (%s)\n", entry.Name, entry.SizePretty)
}
return nil
}
// Normal mode: actually write the file
// Get the HTML template // Get the HTML template
tmpl := indexgen.GetHTMLTemplate() tmpl := indexgen.GetHTMLTemplate()
if tmpl == nil { if tmpl == nil {
@@ -215,21 +282,47 @@ func generateS3HTML(entries []indexgen.FileEntry, opts *indexgen.Options) error
OutputFile: opts.OutputFile, OutputFile: opts.OutputFile,
} }
// Create output file // Generate HTML content in memory
file, err := os.Create(outputFile) var htmlBuffer strings.Builder
if err != nil { err := tmpl.Execute(&htmlBuffer, data)
return fmt.Errorf("failed to create output file %s: %w", outputFile, err)
}
defer file.Close()
// Execute template
err = tmpl.Execute(file, data)
if err != nil { if err != nil {
return fmt.Errorf("failed to execute template: %w", err) return fmt.Errorf("failed to execute template: %w", err)
} }
htmlContent := htmlBuffer.String()
if opts.DryRun {
// Dry run mode: show what would be written but don't upload
fmt.Printf("Would upload S3 index file: s3://%s/%s\n", s3Config.Bucket, indexKey)
fmt.Printf("Directory level: %s\n", strings.TrimSuffix(indexKey, "/"+opts.OutputFile))
fmt.Printf("Objects found: %d\n", len(entries))
fmt.Printf("Generated HTML size: %d bytes\n", len(htmlContent))
for _, entry := range entries {
entryType := "file"
if entry.IsDir {
entryType = "directory"
}
fmt.Printf(" %s: %s (%s)\n", entryType, entry.Name, entry.SizePretty)
}
return nil
}
// Upload HTML to S3
ctx := context.Background()
putInput := &s3.PutObjectInput{
Bucket: aws.String(s3Config.Bucket),
Key: aws.String(indexKey),
Body: strings.NewReader(htmlContent),
ContentType: aws.String("text/html"),
}
_, err = client.PutObject(ctx, putInput)
if err != nil {
return fmt.Errorf("failed to upload %s to S3: %w", indexKey, err)
}
if opts.Verbose { if opts.Verbose {
log.Printf("Generated index file: %s (%d entries)", outputFile, len(entries)) log.Printf("Uploaded index file: %s to S3 bucket %s (%d entries)", indexKey, s3Config.Bucket, len(entries))
} }
return nil return nil

View File

@@ -973,7 +973,7 @@ const htmlTemplateString = `<!DOCTYPE html>
<tr class="file"> <tr class="file">
<td></td> <td></td>
<td> <td>
<a href="{{urlEscape .Path}}"> <a href="{{.Path}}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><use xlink:href="#{{.IconType}}" class="{{.CSSClass}}"></use></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><use xlink:href="#{{.IconType}}" class="{{.CSSClass}}"></use></svg>
<span class="name">{{.Name}}</span> <span class="name">{{.Name}}</span>
</a> </a>