Compare commits
15 Commits
07c372e6d2
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12e91032a1 | ||
|
|
fc62cd39be | ||
|
|
6d680ea2e8 | ||
|
|
bd0201b6d3 | ||
|
|
579920bbfc | ||
|
|
f1ee4722c2 | ||
|
|
fa6df14ce7 | ||
|
|
7a8e469baa | ||
|
|
0b687bf9d9 | ||
|
|
99ad6fbff1 | ||
|
|
60a149b669 | ||
|
|
16fa899b91 | ||
|
|
7829000c55 | ||
|
|
11fbbd4b42 | ||
|
|
2274372119 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -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
301
LICENSE
@@ -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.
|
|
||||||
|
|||||||
23
Makefile
23
Makefile
@@ -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 " help - Show this help message"
|
@echo " sync-version - Sync version from debian/changelog to source"
|
||||||
|
@echo " pkg-deb - Build Debian package"
|
||||||
|
@echo " help - Show this help message"
|
||||||
|
|||||||
31
README.md
31
README.md
@@ -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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|||||||
@@ -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: "—",
|
||||||
|
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
8
debian/changelog
vendored
Normal 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
1
debian/compat
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
10
|
||||||
21
debian/control
vendored
Normal file
21
debian/control
vendored
Normal 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
2
debian/install
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
s3-genindex usr/bin
|
||||||
|
docs/s3-genindex.1 usr/share/man/man1
|
||||||
20
debian/rules
vendored
Executable file
20
debian/rules
vendored
Executable 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/
|
||||||
249
docs/DETAILS.md
249
docs/DETAILS.md
@@ -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
126
docs/s3-genindex.1
Normal 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
BIN
docs/screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
@@ -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 = "—"
|
entry.SizePretty = "—"
|
||||||
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">—</td>
|
<td class="hideable">—</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>
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user