Fix s3 file+dir handling
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user