Compare commits

...

15 Commits

Author SHA1 Message Date
Pim van Pelt
12e91032a1 Only emit *-symlink with -v set 2025-12-03 17:55:27 +01:00
Pim van Pelt
fc62cd39be Fix make clean; don't build debug package 2025-12-03 17:42:55 +01:00
Pim van Pelt
6d680ea2e8 License under AP2 2025-12-03 17:29:09 +01:00
Pim van Pelt
bd0201b6d3 Add Debian package building, add manpage, release v1.0.0 2025-12-03 17:22:33 +01:00
Pim van Pelt
579920bbfc Don't show non-https use, it's not a good practice 2025-12-03 14:13:07 +01:00
Pim van Pelt
f1ee4722c2 Add screenshot 2025-12-03 14:10:47 +01:00
Pim van Pelt
fa6df14ce7 bugfix: use dirName not the (full) dirPath 2025-12-03 13:19:59 +01:00
Pim van Pelt
7a8e469baa Add backref to https://github.com/glowinthedark/index-html-generator 2025-12-03 12:54:09 +01:00
Pim van Pelt
0b687bf9d9 Make logo 48px 2025-12-03 12:39:37 +01:00
Pim van Pelt
99ad6fbff1 Add logo feature 2025-12-03 12:32:36 +01:00
Pim van Pelt
60a149b669 Refresh README 2025-12-03 12:27:14 +01:00
Pim van Pelt
16fa899b91 Add a -i flag to force showing 'index.html' in the output listing; by default do not show them. 2025-12-03 12:23:40 +01:00
Pim van Pelt
7829000c55 Do not render parent of root directory 2025-12-03 12:16:54 +01:00
Pim van Pelt
11fbbd4b42 Fix s3 file+dir handling 2025-12-03 12:14:13 +01:00
Pim van Pelt
2274372119 Remove index.html files 2025-12-03 00:23:54 +01:00
16 changed files with 915 additions and 306 deletions

1
.gitignore vendored
View File

@@ -2,6 +2,7 @@
# Generated index files # Generated index files
**/index.html **/index.html
coverage.*
# Debian packaging artifacts # Debian packaging artifacts
debian/.debhelper/ debian/.debhelper/

301
LICENSE
View File

@@ -1,165 +1,202 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> Apache License
Everyone is permitted to copy and distribute verbatim copies Version 2.0, January 2004
of this license document, but changing it is not allowed. http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
This version of the GNU Lesser General Public License incorporates 1. Definitions.
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions. "License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
As used herein, "this License" refers to version 3 of the GNU Lesser "Licensor" shall mean the copyright owner or entity authorized by
General Public License, and the "GNU GPL" refers to version 3 of the GNU the copyright owner that is granting the License.
General Public License.
"The Library" refers to a covered work governed by this License, "Legal Entity" shall mean the union of the acting entity and all
other than an Application or a Combined Work as defined below. other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
An "Application" is any work that makes use of an interface provided "You" (or "Your") shall mean an individual or Legal Entity
by the Library, but which is not otherwise based on the Library. exercising permissions granted by this License.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an "Source" form shall mean the preferred form for making modifications,
Application with the Library. The particular version of the Library including but not limited to software source code, documentation
with which the Combined Work was made is also called the "Linked source, and configuration files.
Version".
The "Minimal Corresponding Source" for a Combined Work means the "Object" form shall mean any form resulting from mechanical
Corresponding Source for the Combined Work, excluding any source code transformation or translation of a Source form, including but
for portions of the Combined Work that, considered in isolation, are not limited to compiled object code, generated documentation,
based on the Application, and not on the Linked Version. and conversions to other media types.
The "Corresponding Application Code" for a Combined Work means the "Work" shall mean the work of authorship, whether in Source or
object code and/or source code for the Application, including any data Object form, made available under the License, as indicated by a
and utility programs needed for reproducing the Combined Work from the copyright notice that is included in or attached to the work
Application, but excluding the System Libraries of the Combined Work. (an example is provided in the Appendix below).
1. Exception to Section 3 of the GNU GPL. "Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
You may convey a covered work under sections 3 and 4 of this License "Contribution" shall mean any work of authorship, including
without being bound by section 3 of the GNU GPL. the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
2. Conveying Modified Versions. "Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
If you modify a copy of the Library, and, in your modifications, a 2. Grant of Copyright License. Subject to the terms and conditions of
facility refers to a function or data to be supplied by an Application this License, each Contributor hereby grants to You a perpetual,
that uses the facility (other than as an argument passed when the worldwide, non-exclusive, no-charge, royalty-free, irrevocable
facility is invoked), then you may convey a copy of the modified copyright license to reproduce, prepare Derivative Works of,
version: publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
a) under this License, provided that you make a good faith effort to 3. Grant of Patent License. Subject to the terms and conditions of
ensure that, in the event an Application does not supply the this License, each Contributor hereby grants to You a perpetual,
function or data, the facility still operates, and performs worldwide, non-exclusive, no-charge, royalty-free, irrevocable
whatever part of its purpose remains meaningful, or (except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
b) under the GNU GPL, with none of the additional permissions of 4. Redistribution. You may reproduce and distribute copies of the
this License applicable to that copy. Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
3. Object Code Incorporating Material from Library Header Files. (a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
The object code form of an Application may incorporate material from (b) You must cause any modified files to carry prominent notices
a header file that is part of the Library. You may convey such object stating that You changed the files; and
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the (c) You must retain, in the Source form of any Derivative Works
Library is used in it and that the Library and its use are that You distribute, all copyright, patent, trademark, and
covered by this License. attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
b) Accompany the object code with a copy of the GNU GPL and this license (d) If the Work includes a "NOTICE" text file as part of its
document. distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
4. Combined Works. You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
You may convey a Combined Work under terms of your choice that, 5. Submission of Contributions. Unless You explicitly state otherwise,
taken together, effectively do not restrict modification of the any Contribution intentionally submitted for inclusion in the Work
portions of the Library contained in the Combined Work and reverse by You to the Licensor shall be under the terms and conditions of
engineering for debugging such modifications, if you also do each of this License, without any additional terms or conditions.
the following: Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
a) Give prominent notice with each copy of the Combined Work that 6. Trademarks. This License does not grant permission to use the trade
the Library is used in it and that the Library and its use are names, trademarks, service marks, or product names of the Licensor,
covered by this License. except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
b) Accompany the Combined Work with a copy of the GNU GPL and this license 7. Disclaimer of Warranty. Unless required by applicable law or
document. agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
c) For a Combined Work that displays copyright notices during 8. Limitation of Liability. In no event and under no legal theory,
execution, include the copyright notice for the Library among whether in tort (including negligence), contract, or otherwise,
these notices, as well as a reference directing the user to the unless required by applicable law (such as deliberate and grossly
copies of the GNU GPL and this license document. negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
d) Do one of the following: 9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
0) Convey the Minimal Corresponding Source under the terms of this END OF TERMS AND CONDITIONS
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the APPENDIX: How to apply the Apache License to your work.
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise To apply the Apache License to your work, attach the following
be required to provide such information under section 6 of the boilerplate notice, with the fields enclosed by brackets "[]"
GNU GPL, and only to the extent that such information is replaced with your own identifying information. (Don't include
necessary to install and execute a modified version of the the brackets!) The text should be enclosed in the appropriate
Combined Work produced by recombining or relinking the comment syntax for the file format. We also recommend that a
Application with a modified version of the Linked Version. (If file or class name and description of purpose be included on the
you use option 4d0, the Installation Information must accompany same "printed page" as the copyright notice for easier
the Minimal Corresponding Source and Corresponding Application identification within third-party archives.
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries. Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
You may place library facilities that are a work based on the Licensed under the Apache License, Version 2.0 (the "License");
Library side by side in a single library together with other library you may not use this file except in compliance with the License.
facilities that are not Applications and are not covered by this You may obtain a copy of the License at
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based http://www.apache.org/licenses/LICENSE-2.0
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it Unless required by applicable law or agreed to in writing, software
is a work based on the Library, and explaining where to find the distributed under the License is distributed on an "AS IS" BASIS,
accompanying uncombined form of the same work. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
6. Revised Versions of the GNU Lesser General Public License. limitations under the License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

View File

@@ -1,4 +1,4 @@
.PHONY: build clean wipe test help .PHONY: build clean wipe test help sync-version pkg-deb
# Default target # Default target
all: build all: build
@@ -27,6 +27,10 @@ clean:
@echo "Cleaning build artifacts..." @echo "Cleaning build artifacts..."
rm -f s3-genindex rm -f s3-genindex
rm -f coverage.out coverage.html rm -f coverage.out coverage.html
rm -rf debian/s3-genindex debian/files debian/*.substvars debian/debhelper-build-stamp
[ -d debian/go ] && chmod -R +w debian/go || true
rm -rf debian/.gocache debian/go
find . -name index.html -delete
@echo "Clean complete" @echo "Clean complete"
# Wipe everything including test caches # Wipe everything including test caches
@@ -72,6 +76,19 @@ bench:
@echo "Running benchmarks..." @echo "Running benchmarks..."
go test -bench=. ./... go test -bench=. ./...
# Sync version from debian/changelog to source code
sync-version:
@echo "Syncing version..."
@version=$$(head -1 debian/changelog | sed -n 's/.*(\([^)]*\)).*/\1/p'); \
sed -i "s/const Version = .*/const Version = \"$$version\"/" cmd/s3-genindex/main.go; \
echo "Updated version to $$version"
# Build Debian package
pkg-deb: sync-version
@echo "Building Debian package..."
DEB_BUILD_OPTIONS=noautodbgsym fakeroot dpkg-buildpackage -us -uc -b
@echo "Debian package build complete"
# Show help # Show help
help: help:
@echo "Available targets:" @echo "Available targets:"
@@ -86,4 +103,6 @@ help:
@echo " lint - Lint code (requires golangci-lint)" @echo " lint - Lint code (requires golangci-lint)"
@echo " check - Run fmt, vet, lint, and test" @echo " check - Run fmt, vet, lint, and test"
@echo " bench - Run benchmarks" @echo " bench - Run benchmarks"
@echo " sync-version - Sync version from debian/changelog to source"
@echo " pkg-deb - Build Debian package"
@echo " help - Show this help message" @echo " help - Show this help message"

View File

@@ -1,6 +1,19 @@
# s3-genindex # s3-genindex
Generate HTML directory indexes with file type icons and responsive design. Generate HTML directory indexes with file type icons and responsive design for local directories and S3-compatible storage.
This is particularly useful for S3 buckets that are publicly readable.
![Screenshot](docs/screenshot.png)
## Features
- **Local directory indexing** with recursive traversal
- **S3-compatible storage support** (MinIO, AWS S3, etc.)
- **Hierarchical directory structure** for S3 buckets
- **Responsive HTML design** with file type icons
- **Dry run mode** for testing
- **Flexible filtering** with glob patterns and regex exclusion
- **Hidden file control** and index.html visibility options
## Install ## Install
@@ -8,17 +21,17 @@ Generate HTML directory indexes with file type icons and responsive design.
go install git.ipng.ch/ipng/s3-genindex/cmd/s3-genindex@latest go install git.ipng.ch/ipng/s3-genindex/cmd/s3-genindex@latest
``` ```
## Usage ## Quick Start
```bash ```bash
# Generate index.html in current directory # Dry run to see what would be generated
s3-genindex s3-genindex -d /path/to/dir -n
# Generate recursively with custom output # Local directory
s3-genindex -r -o listing.html /path/to/dir s3-genindex -d /path/to/dir
# Exclude files by regex # S3 bucket (requires AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY)
s3-genindex -x "(build|node_modules|\.tmp)" s3-genindex -s3 https://minio.example.com/bucket
``` ```
## Build ## Build
@@ -28,4 +41,4 @@ make build
make test make test
``` ```
See [docs/DETAILS.md](docs/DETAILS.md) for complete documentation. See [docs/DETAILS.md](docs/DETAILS.md) for complete documentation and examples.

View File

@@ -20,6 +20,9 @@ import (
"git.ipng.ch/ipng/s3-genindex/internal/indexgen" "git.ipng.ch/ipng/s3-genindex/internal/indexgen"
) )
// Version is the application version (sync'd from debian/changelog)
const Version = "1.0.0-1"
// S3Config holds S3 connection configuration // S3Config holds S3 connection configuration
type S3Config struct { type S3Config struct {
Endpoint string Endpoint string
@@ -28,6 +31,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 +72,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 +109,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
@@ -127,6 +128,11 @@ func processS3Bucket(s3Config *S3Config, opts *indexgen.Options) error {
continue continue
} }
// Skip index.html files unless ShowIndexFiles is enabled
if !opts.ShowIndexFiles && strings.HasSuffix(keyName, opts.OutputFile) {
continue
}
// Simple glob matching for filter // Simple glob matching for filter
if opts.Filter != "*" && opts.Filter != "" { if opts.Filter != "*" && opts.Filter != "" {
matched, err := filepath.Match(opts.Filter, keyName) matched, err := filepath.Match(opts.Filter, keyName)
@@ -135,101 +141,203 @@ 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 relative path for S3 (relative to current directory)
dirEntryPath := dirName + "/"
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: "&mdash;",
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 {
return fmt.Errorf("failed to get HTML template") return fmt.Errorf("failed to get HTML template")
} }
// Determine if we're at root level (no parent directory)
isRoot := (indexKey == opts.OutputFile) // root level index.html
// Prepare template data (similar to ProcessDir in indexgen) // Prepare template data (similar to ProcessDir in indexgen)
data := struct { data := struct {
DirName string DirName string
Entries []indexgen.FileEntry Entries []indexgen.FileEntry
DirAppend bool DirAppend bool
OutputFile string OutputFile string
IsRoot bool
WatermarkURL string
}{ }{
DirName: opts.TopDir, // Use bucket name as directory name DirName: opts.TopDir, // Use bucket name as directory name
Entries: entries, Entries: entries,
DirAppend: opts.DirAppend, DirAppend: opts.DirAppend,
OutputFile: opts.OutputFile, OutputFile: opts.OutputFile,
IsRoot: isRoot,
WatermarkURL: opts.WatermarkURL,
} }
// 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
@@ -241,6 +349,9 @@ func main() {
var directory string var directory string
var s3URL string var s3URL string
var dryRun bool var dryRun bool
var showIndexFiles bool
var watermarkURL string
var showVersion bool
// Set defaults // Set defaults
opts.DirAppend = true opts.DirAppend = true
@@ -254,6 +365,9 @@ func main() {
flag.BoolVar(&dryRun, "n", false, "dry run: show what would be written without actually writing") flag.BoolVar(&dryRun, "n", false, "dry run: show what would be written without actually writing")
flag.StringVar(&excludeRegexStr, "x", "", "exclude files matching regular expression") flag.StringVar(&excludeRegexStr, "x", "", "exclude files matching regular expression")
flag.BoolVar(&opts.Verbose, "v", false, "verbosely list every processed file") flag.BoolVar(&opts.Verbose, "v", false, "verbosely list every processed file")
flag.BoolVar(&showIndexFiles, "i", false, "show index.html files in directory listings")
flag.StringVar(&watermarkURL, "wm", "", "watermark logo URL to display in top left corner")
flag.BoolVar(&showVersion, "version", false, "show version information")
flag.Usage = func() { flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Generate directory index files (recursive is ON, hidden files included by default).\n") fmt.Fprintf(os.Stderr, "Generate directory index files (recursive is ON, hidden files included by default).\n")
@@ -272,6 +386,12 @@ func main() {
flag.Parse() flag.Parse()
// Handle version flag
if showVersion {
fmt.Printf("s3-genindex version %s\n", Version)
os.Exit(0)
}
// Check mutual exclusion and that exactly one option is provided // Check mutual exclusion and that exactly one option is provided
if directory == "" && s3URL == "" { if directory == "" && s3URL == "" {
fmt.Fprintf(os.Stderr, "Error: Either -d <directory> or -s3 <url> must be specified.\n\n") fmt.Fprintf(os.Stderr, "Error: Either -d <directory> or -s3 <url> must be specified.\n\n")
@@ -293,8 +413,10 @@ func main() {
} }
} }
// Set dry run flag // Set dry run, show index files, and watermark URL
opts.DryRun = dryRun opts.DryRun = dryRun
opts.ShowIndexFiles = showIndexFiles
opts.WatermarkURL = watermarkURL
if s3URL != "" { if s3URL != "" {
// Parse S3 URL // Parse S3 URL

8
debian/changelog vendored Normal file
View File

@@ -0,0 +1,8 @@
s3-genindex (1.0.0-1) unstable; urgency=medium
* Initial release
* HTML directory index generator for local and S3 storage
* Support for file type icons and responsive design
* Watermark support and hierarchical S3 navigation
-- Pim van Pelt <pim@ipng.ch> Tue, 03 Dec 2025 14:30:00 +0100

1
debian/compat vendored Normal file
View File

@@ -0,0 +1 @@
10

21
debian/control vendored Normal file
View File

@@ -0,0 +1,21 @@
Source: s3-genindex
Section: utils
Priority: optional
Maintainer: Pim van Pelt <pim@ipng.ch>
Build-Depends: debhelper (>= 10), golang-go
Standards-Version: 4.1.2
Homepage: https://git.ipng.ch/ipng/s3-genindex
Package: s3-genindex
Architecture: any
Depends: ${shlibs:Depends}, ${misc:Depends}
Description: HTML directory index generator for local and S3 storage
Generate HTML directory indexes with file type icons and responsive design
for local directories and S3-compatible storage. This is particularly useful
for S3 buckets that are publicly readable.
.
Features include local directory indexing with recursive traversal,
S3-compatible storage support (MinIO, AWS S3, etc.), hierarchical directory
structure for S3 buckets, responsive HTML design with file type icons,
dry run mode for testing, flexible filtering with glob patterns and regex
exclusion, and hidden file control.

2
debian/install vendored Normal file
View File

@@ -0,0 +1,2 @@
s3-genindex usr/bin
docs/s3-genindex.1 usr/share/man/man1

20
debian/rules vendored Executable file
View File

@@ -0,0 +1,20 @@
#!/usr/bin/make -f
export GO111MODULE=on
export GOPROXY=direct
export GOSUMDB=off
export GOCACHE=$(CURDIR)/debian/.gocache
export GOPATH=$(CURDIR)/debian/go
%:
dh $@
override_dh_auto_build:
go build -o s3-genindex ./cmd/s3-genindex
override_dh_auto_test:
go test ./...
override_dh_auto_install:
mkdir -p debian/s3-genindex/usr/bin
cp s3-genindex debian/s3-genindex/usr/bin/

View File

@@ -2,18 +2,20 @@
## Overview ## Overview
s3-genindex is a Go rewrite of the original Python genindex.py script. It generates HTML directory listings with file type icons, responsive design, and dark mode support. s3-genindex is a program that generates HTML directory listings with file type icons, responsive design, and dark mode support for both local directories and S3-compatible storage systems.
## Features ## Features
- **Local Directory Indexing**: Recursive traversal of filesystem directories
- **S3-Compatible Storage**: Support for MinIO, AWS S3, and other S3-compatible systems
- **Hierarchical Structure**: Creates proper directory navigation for S3 buckets
- **File Type Detection**: Recognizes 100+ file extensions with appropriate icons - **File Type Detection**: Recognizes 100+ file extensions with appropriate icons
- **Responsive Design**: Works on desktop and mobile devices - **Responsive Design**: Works on desktop and mobile devices
- **Dark Mode**: Automatic dark mode support based on system preferences - **Dark Mode**: Automatic dark mode support based on system preferences
- **Recursive Processing**: Generate indexes for entire directory trees - **Dry Run Mode**: Preview what would be generated without writing files
- **File Filtering**: Include/exclude files by pattern or regex - **File Filtering**: Include/exclude files by glob patterns or regex
- **Symlink Support**: Special handling for symbolic links - **Symlink Support**: Special handling for symbolic links
- **Custom Output**: Configurable output filename - **Index File Control**: Show/hide index.html files in directory listings
- **Breadcrumb Navigation**: Parent directory navigation
## Installation ## Installation
@@ -34,66 +36,118 @@ go install git.ipng.ch/ipng/s3-genindex/cmd/s3-genindex@latest
## Command Line Options ## Command Line Options
``` ```
Usage: s3-genindex [OPTIONS] [directory] Usage: s3-genindex [OPTIONS]
-d append output file to directory href -d string
local directory to process
-s3 string
S3 URL to process
-f string -f string
only include files matching glob (default "*") only include files matching glob (default "*")
-i include dot hidden files -i show index.html files in directory listings
-o string -n dry run: show what would be written without actually writing
custom output file (default "index.html")
-r recursively process nested dirs
-v verbosely list every processed file -v verbosely list every processed file
-x string -x string
exclude files matching regular expression exclude files matching regular expression
``` ```
**Note**: Either `-d <directory>` or `-s3 <url>` must be specified (mutually exclusive).
## Usage Examples ## Usage Examples
### Basic Usage ### Local Directory Processing (`-d`)
```bash ```bash
# Generate index.html in current directory # Generate index.html for a local directory
s3-genindex s3-genindex -d /var/www/html
# Generate index for specific directory # Process with verbose output to see all files
s3-genindex /path/to/directory s3-genindex -d /home/user/documents -v
# Generate with custom output filename # Dry run to preview what would be generated
s3-genindex -o listing.html s3-genindex -d /path/to/dir -n
# Show index.html files in directory listings
s3-genindex -d /var/www -i
``` ```
### Recursive Processing ### S3 Storage Processing (`-s3`)
```bash ```bash
# Process directory tree recursively # Basic S3 bucket processing (MinIO)
s3-genindex -r export AWS_ACCESS_KEY_ID="your-access-key"
export AWS_SECRET_ACCESS_KEY="your-secret-key"
s3-genindex -s3 http://minio.example.com:9000/my-bucket
# Process recursively with verbose output # AWS S3 bucket processing
s3-genindex -rv /var/www export AWS_ACCESS_KEY_ID="your-aws-key"
export AWS_SECRET_ACCESS_KEY="your-aws-secret"
s3-genindex -s3 https://s3.amazonaws.com/my-bucket
# S3 with verbose output and dry run
s3-genindex -s3 http://localhost:9000/test-bucket -v -n
# S3 processing with file filtering
s3-genindex -s3 http://minio.local:9000/logs -f "*.log"
``` ```
### File Filtering ### File Filtering Examples
```bash ```bash
# Include only Python files # Include only specific file types
s3-genindex -f "*.py" s3-genindex -d /var/log -f "*.log"
s3-genindex -s3 http://minio:9000/images -f "*.{jpg,png,gif}"
# Exclude build artifacts and dependencies # Exclude build artifacts and temporary files
s3-genindex -x "(build|dist|node_modules|__pycache__|\\.tmp)" s3-genindex -d /home/dev/project -x "(build|dist|node_modules|__pycache__|\\.tmp)"
# Include hidden files # Exclude version control and system files
s3-genindex -i s3-genindex -d /var/www -x "(\.git|\.svn|\.DS_Store|Thumbs\.db)"
# Complex filtering with multiple patterns
s3-genindex -d /data -f "*.{json,xml,csv}" -x "(backup|temp|cache)"
``` ```
### Advanced Usage ### Advanced Usage Scenarios
```bash ```bash
# Recursive with custom output and exclusions # Documentation site generation for local directory
s3-genindex -r -o index.html -x "(\.git|\.svn|node_modules)" /var/www s3-genindex -d /var/www/docs -i -v
# Verbose processing with directory appending # Log file indexing on S3 with size filtering
s3-genindex -r -v -d /home/user/public s3-genindex -s3 http://minio:9000/application-logs -f "*.log" -v
# Website asset indexing (excluding index files)
s3-genindex -d /var/www/assets -x "(index\.html|\.htaccess)"
# Backup verification with dry run
s3-genindex -s3 https://backup.s3.amazonaws.com/daily-backups -n -v
# Development file browsing with hidden files
s3-genindex -d /home/dev/src -i -x "(\.git|node_modules|vendor)"
# Media gallery generation
s3-genindex -d /var/media -f "*.{jpg,jpeg,png,gif,mp4,mov}" -i
```
### Integration Examples
```bash
# Automated documentation updates (cron job)
#!/bin/bash
export AWS_ACCESS_KEY_ID="docs-access-key"
export AWS_SECRET_ACCESS_KEY="docs-secret-key"
s3-genindex -s3 https://docs.s3.amazonaws.com/api-docs -v
# Local web server directory indexing
s3-genindex -d /var/www/html -i
nginx -s reload
# CI/CD artifact indexing
s3-genindex -s3 http://artifacts.internal:9000/build-artifacts -f "*.{tar.gz,zip}" -v
# Photo gallery with metadata
s3-genindex -d /var/photos -f "*.{jpg,jpeg,png,heic}" -i -v
``` ```
## File Type Support ## File Type Support
@@ -206,11 +260,40 @@ make lint # Run golangci-lint (if installed)
make check # Run all quality checks make check # Run all quality checks
``` ```
## S3 Configuration
### Environment Variables for S3
When using S3 storage (`-s3` flag), the following environment variables are **required**:
- `AWS_ACCESS_KEY_ID`: Your S3 access key ID
- `AWS_SECRET_ACCESS_KEY`: Your S3 secret access key
### S3 URL Format
S3 URLs should follow this format:
```
http://host:port/bucket # For MinIO or custom S3-compatible storage
https://host/bucket # For HTTPS endpoints
```
Examples:
- `http://minio.example.com:9000/my-bucket`
- `https://s3.amazonaws.com/my-bucket`
- `http://localhost:9000/test-bucket`
### S3 Features
- **Hierarchical Processing**: Creates index.html files for each directory level in S3
- **Path-Style URLs**: Uses path-style S3 URLs for MinIO compatibility
- **Bucket Navigation**: Generates proper directory navigation within S3 buckets
- **No Parent Directory at Root**: Root bucket index doesn't show parent (..) link
## Configuration ## Configuration
No configuration files are needed. All options are provided via command-line arguments. No configuration files are needed. All options are provided via command-line arguments.
### Environment Variables ### Standard Environment Variables
The tool respects standard Go environment variables: The tool respects standard Go environment variables:
- `GOOS` and `GOARCH` for cross-compilation - `GOOS` and `GOARCH` for cross-compilation
@@ -240,25 +323,55 @@ This Go version provides the same functionality as the original Python script wi
### Common Issues ### Common Issues
**Permission Errors** **Local Directory Permission Errors**
```bash ```bash
# Ensure read permissions on target directory # Ensure read permissions on target directory
chmod +r /path/to/directory chmod +r /path/to/directory
``` ```
**Large Directories** **S3 Connection Issues**
```bash ```bash
# Use verbose mode to monitor progress # Verify credentials are set
s3-genindex -v /large/directory echo $AWS_ACCESS_KEY_ID
echo $AWS_SECRET_ACCESS_KEY
# Exclude unnecessary files # Test connection with dry run
s3-genindex -x "(\.git|node_modules|__pycache__)" s3-genindex -s3 http://minio.example.com:9000/bucket -n -v
# Check S3 endpoint connectivity
curl http://minio.example.com:9000/
``` ```
**Memory Usage** **S3 Permission Errors**
```bash ```bash
# Process directories individually for very large trees # Verify bucket access permissions
for dir in */; do s3-genindex "$dir"; done aws s3 ls s3://your-bucket/ --endpoint-url http://minio.example.com:9000
# Check if bucket exists and is accessible
s3-genindex -s3 http://minio.example.com:9000/bucket -v
```
**Large Directory/Bucket Processing**
```bash
# Use verbose mode to monitor progress
s3-genindex -d /large/directory -v
s3-genindex -s3 http://minio:9000/large-bucket -v
# Exclude unnecessary files to reduce processing time
s3-genindex -d /data -x "(\.git|node_modules|__pycache__|\.tmp)"
s3-genindex -s3 http://minio:9000/bucket -x "(backup|temp|cache)"
# Use dry run to estimate processing time
s3-genindex -s3 http://minio:9000/bucket -n
```
**Network Timeout Issues (S3)**
```bash
# For slow connections, use verbose mode to see progress
s3-genindex -s3 http://slow-endpoint:9000/bucket -v
# Test with smaller buckets first
s3-genindex -s3 http://endpoint:9000/small-test-bucket -n
``` ```
### Debug Mode ### Debug Mode
@@ -266,7 +379,27 @@ for dir in */; do s3-genindex "$dir"; done
Enable verbose output to see detailed processing: Enable verbose output to see detailed processing:
```bash ```bash
s3-genindex -v /path/to/debug # Local directory debugging
s3-genindex -d /path/to/debug -v
# S3 debugging with dry run
s3-genindex -s3 http://minio:9000/bucket -n -v
# Full S3 processing with verbose output
s3-genindex -s3 http://minio:9000/bucket -v
```
### S3-Specific Debugging
```bash
# Test S3 connectivity without processing
curl -I http://minio.example.com:9000/bucket/
# List S3 objects directly (if aws-cli is available)
aws s3 ls s3://bucket/ --endpoint-url http://minio.example.com:9000
# Verify S3 URL format
s3-genindex -s3 http://wrong-format -n # Will show URL parsing errors
``` ```
## License ## License
@@ -283,8 +416,26 @@ Licensed under the Apache License 2.0. See original Python script for full licen
## Changelog ## Changelog
### v2.0.0 (Current)
- **S3 Support**: Complete S3-compatible storage support (MinIO, AWS S3)
- **Hierarchical S3 Processing**: Creates proper directory navigation for S3 buckets
- **Dry Run Mode**: Preview functionality with `-n` flag
- **Index File Control**: Show/hide index.html files with `-i` flag
- **Mutual Exclusive Flags**: Clean separation between `-d` and `-s3` modes
- **Enhanced Error Handling**: Better error messages and validation
- **Comprehensive Testing**: Extended test suite covering S3 functionality
- **URL Handling Fix**: Proper S3 navigation without URL encoding issues
### v1.0.0 ### v1.0.0
- Initial Go rewrite - Initial Go rewrite from Python genindex.py
- Complete feature parity with Python version - Complete feature parity with Python version for local directories
- Comprehensive test suite - Comprehensive test suite
- Modern Go project structure - Modern Go project structure
- Recursive directory processing
- File type detection and icons
- Responsive HTML design with dark mode support
## Acknowledgement
This tool was inspired by
[[index-html-generator](https://github.com/glowinthedark/index-html-generator)] on GitHub.

126
docs/s3-genindex.1 Normal file
View File

@@ -0,0 +1,126 @@
.TH S3-GENINDEX 1 "December 2025" "s3-genindex 1.0.0" "User Commands"
.SH NAME
s3-genindex \- Generate HTML directory indexes for local directories and S3 storage
.SH SYNOPSIS
.B s3-genindex
[\fIOPTIONS\fR]
.SH DESCRIPTION
.B s3-genindex
generates HTML directory indexes with file type icons and responsive design for local directories and S3-compatible storage. This is particularly useful for S3 buckets that are publicly readable.
The tool creates hierarchical directory structures with responsive HTML pages that include file type icons, sorting capabilities, and optional watermark branding.
By default, recursive processing is enabled, hidden files are included, and the output file is named 'index.html' with directory href appending enabled.
.SH OPTIONS
.TP
.BR \-d " \fIDIRECTORY\fR"
Process local directory. Mutually exclusive with \-s3.
.TP
.BR \-s3 " \fIURL\fR"
Process S3-compatible storage. URL format: http://host:port/bucket or https://host/bucket.
Mutually exclusive with \-d.
.TP
.BR \-f " \fIGLOB\fR"
Only include files matching glob pattern (default: "*").
Example: "*.py" to include only Python files.
.TP
.BR \-n
Dry run mode. Show what would be written without actually creating files.
.TP
.BR \-x " \fIREGEX\fR"
Exclude files matching regular expression pattern.
.TP
.BR \-v
Verbose mode. List every processed file during operation.
.TP
.BR \-i
Show index.html files in directory listings (normally hidden).
.TP
.BR \-wm " \fIURL\fR"
Watermark logo URL to display in top left corner of generated pages.
.TP
.BR \-\-version
Show version information and exit.
.SH ENVIRONMENT
For S3 operations, the following environment variables must be set:
.TP
.B AWS_ACCESS_KEY_ID
S3 access key ID for authentication.
.TP
.B AWS_SECRET_ACCESS_KEY
S3 secret access key for authentication.
.SH EXAMPLES
.TP
Process a local directory:
.B s3-genindex -d /var/www/html
.TP
Process an S3 bucket with verbose output:
.B s3-genindex -v -s3 http://minio.example.com:9000/my-bucket
.TP
Dry run with file filtering:
.B s3-genindex -n -f "*.log" -d /var/log
.TP
S3 processing with watermark and filtering:
.B s3-genindex -v -f "*.jpg" -wm https://example.com/logo.svg -s3 https://s3.amazonaws.com/photos
.TP
Show version information:
.B s3-genindex --version
.SH FILES
.TP
.B index.html
Default output filename for generated directory indexes.
.SH EXIT STATUS
.TP
.B 0
Success
.TP
.B 1
Error (invalid arguments, missing credentials, file system errors, etc.)
.SH FEATURES
.IP \(bu 2
Local directory indexing with recursive traversal
.IP \(bu 2
S3-compatible storage support (MinIO, AWS S3, etc.)
.IP \(bu 2
Hierarchical directory structure for S3 buckets
.IP \(bu 2
Responsive HTML design with file type icons
.IP \(bu 2
Dry run mode for testing
.IP \(bu 2
Flexible filtering with glob patterns and regex exclusion
.IP \(bu 2
Hidden file control and index.html visibility options
.IP \(bu 2
Watermark support for branding
.SH SEE ALSO
.BR ls (1),
.BR find (1),
.BR tree (1)
.SH AUTHOR
Pim van Pelt <pim@ipng.ch>
.SH COPYRIGHT
Copyright \(co 2025 Pim van Pelt. Licensed under the APACHE-2.0 License.

BIN
docs/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View File

@@ -158,15 +158,17 @@ var ExtensionTypes = map[string]string{
} }
type Options struct { type Options struct {
TopDir string TopDir string
Filter string Filter string
OutputFile string OutputFile string
DirAppend bool DirAppend bool
Recursive bool Recursive bool
IncludeHidden bool IncludeHidden bool
ExcludeRegex *regexp.Regexp ExcludeRegex *regexp.Regexp
Verbose bool Verbose bool
DryRun bool DryRun bool
ShowIndexFiles bool
WatermarkURL string
} }
type FileEntry struct { type FileEntry struct {
@@ -210,15 +212,19 @@ func ProcessDir(topDir string, opts *Options) error {
}) })
templateData := struct { templateData := struct {
DirName string DirName string
Entries []FileEntry Entries []FileEntry
DirAppend bool DirAppend bool
OutputFile string OutputFile string
IsRoot bool
WatermarkURL string
}{ }{
DirName: dirName, DirName: dirName,
Entries: entries, Entries: entries,
DirAppend: opts.DirAppend, DirAppend: opts.DirAppend,
OutputFile: opts.OutputFile, OutputFile: opts.OutputFile,
IsRoot: false, // Local filesystem always shows parent directory
WatermarkURL: opts.WatermarkURL,
} }
if opts.DryRun { if opts.DryRun {
@@ -273,7 +279,7 @@ func ReadDirEntries(dirPath string, opts *Options) ([]FileEntry, error) {
for _, file := range files { for _, file := range files {
fileName := file.Name() fileName := file.Name()
if strings.EqualFold(fileName, opts.OutputFile) { if !opts.ShowIndexFiles && strings.EqualFold(fileName, opts.OutputFile) {
continue continue
} }
@@ -316,12 +322,16 @@ func ReadDirEntries(dirPath string, opts *Options) ([]FileEntry, error) {
entry.Size = -1 entry.Size = -1
entry.SizePretty = "&mdash;" entry.SizePretty = "&mdash;"
entry.IconType = "folder-symlink" entry.IconType = "folder-symlink"
fmt.Printf("dir-symlink %s\n", fullPath) if opts.Verbose {
fmt.Printf("dir-symlink %s\n", fullPath)
}
} else if !file.IsDir() && entry.IsSymlink { } else if !file.IsDir() && entry.IsSymlink {
entry.Size = info.Size() entry.Size = info.Size()
entry.SizePretty = PrettySize(entry.Size) entry.SizePretty = PrettySize(entry.Size)
entry.IconType = "symlink" entry.IconType = "symlink"
fmt.Printf("file-symlink %s\n", fullPath) if opts.Verbose {
fmt.Printf("file-symlink %s\n", fullPath)
}
} else { } else {
entry.Size = info.Size() entry.Size = info.Size()
entry.SizePretty = PrettySize(entry.Size) entry.SizePretty = PrettySize(entry.Size)
@@ -455,6 +465,15 @@ const htmlTemplateString = `<!DOCTYPE html>
padding-top: 25px; padding-top: 25px;
padding-bottom: 15px; padding-bottom: 15px;
background-color: #f2f2f2; background-color: #f2f2f2;
display: flex;
align-items: center;
}
.watermark {
height: 48px;
width: auto;
margin-right: 12px;
flex-shrink: 0;
} }
h1 { h1 {
@@ -464,6 +483,8 @@ const htmlTemplateString = `<!DOCTYPE html>
overflow-x: hidden; overflow-x: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
color: #999; color: #999;
margin: 0;
flex: 1;
} }
h1 a { h1 a {
@@ -940,6 +961,7 @@ const htmlTemplateString = `<!DOCTYPE html>
</defs> </defs>
</svg> </svg>
<header> <header>
{{if .WatermarkURL}}<img src="{{.WatermarkURL}}" class="watermark" alt="Logo">{{end}}
<h1>{{.DirName}}</h1> <h1>{{.DirName}}</h1>
</header> </header>
<main> <main>
@@ -957,6 +979,7 @@ const htmlTemplateString = `<!DOCTYPE html>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{{if not .IsRoot}}
<tr class="clickable"> <tr class="clickable">
<td></td> <td></td>
<td><a href="../{{if .DirAppend}}{{.OutputFile}}{{end}}"> <td><a href="../{{if .DirAppend}}{{.OutputFile}}{{end}}">
@@ -969,11 +992,12 @@ const htmlTemplateString = `<!DOCTYPE html>
<td class="hideable">&mdash;</td> <td class="hideable">&mdash;</td>
<td class="hideable"></td> <td class="hideable"></td>
</tr> </tr>
{{end}}
{{range .Entries}} {{range .Entries}}
<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>

View File

@@ -97,15 +97,19 @@ func TestHTMLTemplate(t *testing.T) {
// Test template execution with sample data // Test template execution with sample data
data := struct { data := struct {
DirName string DirName string
Entries []FileEntry Entries []FileEntry
DirAppend bool DirAppend bool
OutputFile string OutputFile string
IsRoot bool
WatermarkURL string
}{ }{
DirName: "test-dir", DirName: "test-dir",
Entries: []FileEntry{}, Entries: []FileEntry{},
DirAppend: false, DirAppend: false,
OutputFile: "index.html", OutputFile: "index.html",
IsRoot: false,
WatermarkURL: "",
} }
var buf bytes.Buffer var buf bytes.Buffer
@@ -157,15 +161,19 @@ func TestHTMLTemplateWithEntries(t *testing.T) {
} }
data := struct { data := struct {
DirName string DirName string
Entries []FileEntry Entries []FileEntry
DirAppend bool DirAppend bool
OutputFile string OutputFile string
IsRoot bool
WatermarkURL string
}{ }{
DirName: "test-dir", DirName: "test-dir",
Entries: entries, Entries: entries,
DirAppend: false, DirAppend: false,
OutputFile: "index.html", OutputFile: "index.html",
IsRoot: false,
WatermarkURL: "",
} }
var buf bytes.Buffer var buf bytes.Buffer
@@ -192,6 +200,62 @@ func TestHTMLTemplateWithEntries(t *testing.T) {
} }
} }
func TestHTMLTemplateWithWatermark(t *testing.T) {
tmpl := GetHTMLTemplate()
if tmpl == nil {
t.Fatal("GetHTMLTemplate() returned nil")
}
// Test template execution with watermark
data := struct {
DirName string
Entries []FileEntry
DirAppend bool
OutputFile string
IsRoot bool
WatermarkURL string
}{
DirName: "test-dir",
Entries: []FileEntry{},
DirAppend: false,
OutputFile: "index.html",
IsRoot: false,
WatermarkURL: "https://example.com/logo.svg",
}
var buf bytes.Buffer
err := tmpl.Execute(&buf, data)
if err != nil {
t.Fatalf("Template execution with watermark failed: %v", err)
}
output := buf.String()
// Check that watermark image is included
if !bytes.Contains([]byte(output), []byte(`src="https://example.com/logo.svg"`)) {
t.Error("Template output should contain watermark image URL")
}
if !bytes.Contains([]byte(output), []byte(`class="watermark"`)) {
t.Error("Template output should contain watermark CSS class")
}
// Test without watermark
data.WatermarkURL = ""
buf.Reset()
err = tmpl.Execute(&buf, data)
if err != nil {
t.Fatalf("Template execution without watermark failed: %v", err)
}
outputNoWatermark := buf.String()
// Check that watermark image is NOT included when URL is empty
if bytes.Contains([]byte(outputNoWatermark), []byte(`class="watermark"`)) {
t.Error("Template output should not contain watermark when URL is empty")
}
}
func TestReadDirEntries(t *testing.T) { func TestReadDirEntries(t *testing.T) {
// Create a temporary directory with test files // Create a temporary directory with test files
tempDir := t.TempDir() tempDir := t.TempDir()

View File

@@ -245,9 +245,9 @@ func TestProcessDirWithDirAppend(t *testing.T) {
htmlContent := string(content) htmlContent := string(content)
// Check that directory links include index.html (URL escaped) // Check that directory links include index.html
if !strings.Contains(htmlContent, "subdir%2Findex.html") { if !strings.Contains(htmlContent, "subdir/index.html") {
t.Errorf("Directory links should include index.html when DirAppend is true. Expected subdir%%2Findex.html in content") t.Errorf("Directory links should include index.html when DirAppend is true. Expected subdir/index.html in content")
} }
} }