Initial checkin
This commit is contained in:
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
s3-genindex
|
||||||
|
|
||||||
|
# Debian packaging artifacts
|
||||||
|
debian/.debhelper/
|
||||||
|
debian/.gocache/
|
||||||
|
debian/go/
|
||||||
|
debian/s3-genindex/
|
||||||
|
debian/files
|
||||||
|
debian/*.substvars
|
||||||
|
debian/debhelper-build-stamp
|
||||||
|
debian/*.debhelper
|
||||||
165
LICENSE
Normal file
165
LICENSE
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
GNU LESSER GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
|
||||||
|
This version of the GNU Lesser General Public License incorporates
|
||||||
|
the terms and conditions of version 3 of the GNU General Public
|
||||||
|
License, supplemented by the additional permissions listed below.
|
||||||
|
|
||||||
|
0. Additional Definitions.
|
||||||
|
|
||||||
|
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||||
|
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
||||||
|
General Public License.
|
||||||
|
|
||||||
|
"The Library" refers to a covered work governed by this License,
|
||||||
|
other than an Application or a Combined Work as defined below.
|
||||||
|
|
||||||
|
An "Application" is any work that makes use of an interface provided
|
||||||
|
by the Library, but which is not otherwise based on the Library.
|
||||||
|
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
|
||||||
|
Application with the Library. The particular version of the Library
|
||||||
|
with which the Combined Work was made is also called the "Linked
|
||||||
|
Version".
|
||||||
|
|
||||||
|
The "Minimal Corresponding Source" for a Combined Work means the
|
||||||
|
Corresponding Source for the Combined Work, excluding any source code
|
||||||
|
for portions of the Combined Work that, considered in isolation, are
|
||||||
|
based on the Application, and not on the Linked Version.
|
||||||
|
|
||||||
|
The "Corresponding Application Code" for a Combined Work means the
|
||||||
|
object code and/or source code for the Application, including any data
|
||||||
|
and utility programs needed for reproducing the Combined Work from the
|
||||||
|
Application, but excluding the System Libraries of the Combined Work.
|
||||||
|
|
||||||
|
1. Exception to Section 3 of the GNU GPL.
|
||||||
|
|
||||||
|
You may convey a covered work under sections 3 and 4 of this License
|
||||||
|
without being bound by section 3 of the GNU GPL.
|
||||||
|
|
||||||
|
2. Conveying Modified Versions.
|
||||||
|
|
||||||
|
If you modify a copy of the Library, and, in your modifications, a
|
||||||
|
facility refers to a function or data to be supplied by an Application
|
||||||
|
that uses the facility (other than as an argument passed when the
|
||||||
|
facility is invoked), then you may convey a copy of the modified
|
||||||
|
version:
|
||||||
|
|
||||||
|
a) under this License, provided that you make a good faith effort to
|
||||||
|
ensure that, in the event an Application does not supply the
|
||||||
|
function or data, the facility still operates, and performs
|
||||||
|
whatever part of its purpose remains meaningful, or
|
||||||
|
|
||||||
|
b) under the GNU GPL, with none of the additional permissions of
|
||||||
|
this License applicable to that copy.
|
||||||
|
|
||||||
|
3. Object Code Incorporating Material from Library Header Files.
|
||||||
|
|
||||||
|
The object code form of an Application may incorporate material from
|
||||||
|
a header file that is part of the Library. You may convey such object
|
||||||
|
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
|
||||||
|
Library is used in it and that the Library and its use are
|
||||||
|
covered by this License.
|
||||||
|
|
||||||
|
b) Accompany the object code with a copy of the GNU GPL and this license
|
||||||
|
document.
|
||||||
|
|
||||||
|
4. Combined Works.
|
||||||
|
|
||||||
|
You may convey a Combined Work under terms of your choice that,
|
||||||
|
taken together, effectively do not restrict modification of the
|
||||||
|
portions of the Library contained in the Combined Work and reverse
|
||||||
|
engineering for debugging such modifications, if you also do each of
|
||||||
|
the following:
|
||||||
|
|
||||||
|
a) Give prominent notice with each copy of the Combined Work that
|
||||||
|
the Library is used in it and that the Library and its use are
|
||||||
|
covered by this License.
|
||||||
|
|
||||||
|
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||||
|
document.
|
||||||
|
|
||||||
|
c) For a Combined Work that displays copyright notices during
|
||||||
|
execution, include the copyright notice for the Library among
|
||||||
|
these notices, as well as a reference directing the user to the
|
||||||
|
copies of the GNU GPL and this license document.
|
||||||
|
|
||||||
|
d) Do one of the following:
|
||||||
|
|
||||||
|
0) Convey the Minimal Corresponding Source under the terms of this
|
||||||
|
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
|
||||||
|
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
|
||||||
|
be required to provide such information under section 6 of the
|
||||||
|
GNU GPL, and only to the extent that such information is
|
||||||
|
necessary to install and execute a modified version of the
|
||||||
|
Combined Work produced by recombining or relinking the
|
||||||
|
Application with a modified version of the Linked Version. (If
|
||||||
|
you use option 4d0, the Installation Information must accompany
|
||||||
|
the Minimal Corresponding Source and Corresponding Application
|
||||||
|
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.
|
||||||
|
|
||||||
|
You may place library facilities that are a work based on the
|
||||||
|
Library side by side in a single library together with other library
|
||||||
|
facilities that are not Applications and are not covered by this
|
||||||
|
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
|
||||||
|
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
|
||||||
|
is a work based on the Library, and explaining where to find the
|
||||||
|
accompanying uncombined form of the same work.
|
||||||
|
|
||||||
|
6. Revised Versions of the GNU Lesser General Public 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.
|
||||||
89
Makefile
Normal file
89
Makefile
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
.PHONY: build clean wipe test help
|
||||||
|
|
||||||
|
# Default target
|
||||||
|
all: build
|
||||||
|
|
||||||
|
# Build the binary
|
||||||
|
build:
|
||||||
|
@echo "Building s3-genindex..."
|
||||||
|
go build -o s3-genindex ./cmd/s3-genindex
|
||||||
|
@echo "Build complete: s3-genindex"
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
test:
|
||||||
|
@echo "Running tests..."
|
||||||
|
go test -v ./...
|
||||||
|
@echo "Tests complete"
|
||||||
|
|
||||||
|
# Run tests with coverage
|
||||||
|
test-coverage:
|
||||||
|
@echo "Running tests with coverage..."
|
||||||
|
go test -v -coverprofile=coverage.out ./...
|
||||||
|
go tool cover -html=coverage.out -o coverage.html
|
||||||
|
@echo "Coverage report generated: coverage.html"
|
||||||
|
|
||||||
|
# Clean build artifacts
|
||||||
|
clean:
|
||||||
|
@echo "Cleaning build artifacts..."
|
||||||
|
rm -f s3-genindex
|
||||||
|
rm -f coverage.out coverage.html
|
||||||
|
@echo "Clean complete"
|
||||||
|
|
||||||
|
# Wipe everything including test caches
|
||||||
|
wipe: clean
|
||||||
|
@echo "Wiping test cache and module cache..."
|
||||||
|
go clean -testcache
|
||||||
|
go clean -modcache
|
||||||
|
@echo "Wipe complete"
|
||||||
|
|
||||||
|
# Install binary to $GOPATH/bin or $GOBIN
|
||||||
|
install:
|
||||||
|
@echo "Installing s3-genindex..."
|
||||||
|
go install ./cmd/s3-genindex
|
||||||
|
@echo "Install complete"
|
||||||
|
|
||||||
|
# Format code
|
||||||
|
fmt:
|
||||||
|
@echo "Formatting code..."
|
||||||
|
go fmt ./...
|
||||||
|
@echo "Format complete"
|
||||||
|
|
||||||
|
# Vet code for issues
|
||||||
|
vet:
|
||||||
|
@echo "Vetting code..."
|
||||||
|
go vet ./...
|
||||||
|
@echo "Vet complete"
|
||||||
|
|
||||||
|
# Lint code (requires golangci-lint)
|
||||||
|
lint:
|
||||||
|
@echo "Linting code..."
|
||||||
|
@if command -v golangci-lint >/dev/null 2>&1; then \
|
||||||
|
golangci-lint run; \
|
||||||
|
else \
|
||||||
|
echo "golangci-lint not found, skipping lint"; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run all checks
|
||||||
|
check: fmt vet lint test
|
||||||
|
@echo "All checks passed"
|
||||||
|
|
||||||
|
# Run benchmarks
|
||||||
|
bench:
|
||||||
|
@echo "Running benchmarks..."
|
||||||
|
go test -bench=. ./...
|
||||||
|
|
||||||
|
# Show help
|
||||||
|
help:
|
||||||
|
@echo "Available targets:"
|
||||||
|
@echo " build - Build the s3-genindex binary"
|
||||||
|
@echo " test - Run all tests"
|
||||||
|
@echo " test-coverage - Run tests with coverage report"
|
||||||
|
@echo " clean - Remove build artifacts"
|
||||||
|
@echo " wipe - Clean everything including caches"
|
||||||
|
@echo " install - Install binary to GOPATH/bin"
|
||||||
|
@echo " fmt - Format code"
|
||||||
|
@echo " vet - Vet code for issues"
|
||||||
|
@echo " lint - Lint code (requires golangci-lint)"
|
||||||
|
@echo " check - Run fmt, vet, lint, and test"
|
||||||
|
@echo " bench - Run benchmarks"
|
||||||
|
@echo " help - Show this help message"
|
||||||
31
README.md
Normal file
31
README.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# s3-genindex
|
||||||
|
|
||||||
|
Generate HTML directory indexes with file type icons and responsive design.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go install git.ipng.ch/ipng/s3-genindex/cmd/s3-genindex@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate index.html in current directory
|
||||||
|
s3-genindex
|
||||||
|
|
||||||
|
# Generate recursively with custom output
|
||||||
|
s3-genindex -r -o listing.html /path/to/dir
|
||||||
|
|
||||||
|
# Exclude files by regex
|
||||||
|
s3-genindex -x "(build|node_modules|\.tmp)"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make build
|
||||||
|
make test
|
||||||
|
```
|
||||||
|
|
||||||
|
See [docs/DETAILS.md](docs/DETAILS.md) for complete documentation.
|
||||||
288
docs/DETAILS.md
Normal file
288
docs/DETAILS.md
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
# s3-genindex - Detailed Documentation
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **File Type Detection**: Recognizes 100+ file extensions with appropriate icons
|
||||||
|
- **Responsive Design**: Works on desktop and mobile devices
|
||||||
|
- **Dark Mode**: Automatic dark mode support based on system preferences
|
||||||
|
- **Recursive Processing**: Generate indexes for entire directory trees
|
||||||
|
- **File Filtering**: Include/exclude files by pattern or regex
|
||||||
|
- **Symlink Support**: Special handling for symbolic links
|
||||||
|
- **Custom Output**: Configurable output filename
|
||||||
|
- **Breadcrumb Navigation**: Parent directory navigation
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### From Source
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://git.ipng.ch/ipng/s3-genindex.git
|
||||||
|
cd s3-genindex
|
||||||
|
make build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Go Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go install git.ipng.ch/ipng/s3-genindex/cmd/s3-genindex@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Command Line Options
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage: s3-genindex [OPTIONS] [directory]
|
||||||
|
|
||||||
|
-d, --dir-append append output file to directory href
|
||||||
|
-f, --filter string only include files matching glob (default "*")
|
||||||
|
-i, --include-hidden include dot hidden files
|
||||||
|
-o, --output-file string custom output file (default "index.html")
|
||||||
|
-r, --recursive recursively process nested dirs
|
||||||
|
-v, --verbose verbosely list every processed file
|
||||||
|
-x, --exclude-regex string exclude files matching regular expression
|
||||||
|
--top-dir string top folder from which to start generating indexes
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate index.html in current directory
|
||||||
|
s3-genindex
|
||||||
|
|
||||||
|
# Generate index for specific directory
|
||||||
|
s3-genindex /path/to/directory
|
||||||
|
|
||||||
|
# Generate with custom output filename
|
||||||
|
s3-genindex -o listing.html
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recursive Processing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Process directory tree recursively
|
||||||
|
s3-genindex -r
|
||||||
|
|
||||||
|
# Process recursively with verbose output
|
||||||
|
s3-genindex -rv /var/www
|
||||||
|
```
|
||||||
|
|
||||||
|
### File Filtering
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Include only Python files
|
||||||
|
s3-genindex --filter "*.py"
|
||||||
|
|
||||||
|
# Exclude build artifacts and dependencies
|
||||||
|
s3-genindex -x "(build|dist|node_modules|__pycache__|\\.tmp)"
|
||||||
|
|
||||||
|
# Include hidden files
|
||||||
|
s3-genindex -i
|
||||||
|
```
|
||||||
|
|
||||||
|
### Advanced Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Recursive with custom output and exclusions
|
||||||
|
s3-genindex -r -o index.html -x "(\.git|\.svn|node_modules)" /var/www
|
||||||
|
|
||||||
|
# Verbose processing with directory appending
|
||||||
|
s3-genindex -rv --dir-append /home/user/public
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Type Support
|
||||||
|
|
||||||
|
The tool recognizes and provides appropriate icons for:
|
||||||
|
|
||||||
|
### Programming Languages
|
||||||
|
- Go (`.go`)
|
||||||
|
- Python (`.py`, `.pyc`, `.pyo`)
|
||||||
|
- JavaScript/TypeScript (`.js`, `.ts`, `.json`)
|
||||||
|
- HTML/CSS (`.html`, `.htm`, `.css`, `.scss`)
|
||||||
|
- Shell scripts (`.sh`, `.bash`, `.bat`, `.ps1`)
|
||||||
|
- SQL (`.sql`)
|
||||||
|
|
||||||
|
### Documents
|
||||||
|
- PDF (`.pdf`)
|
||||||
|
- Text/Markdown (`.txt`, `.md`, `.markdown`)
|
||||||
|
- Office documents (`.doc`, `.docx`, `.xls`, `.xlsx`, `.ppt`, `.pptx`)
|
||||||
|
- CSV (`.csv`)
|
||||||
|
|
||||||
|
### Media Files
|
||||||
|
- Images (`.jpg`, `.png`, `.gif`, `.svg`, `.webp`)
|
||||||
|
- Videos (`.mp4`, `.mov`, `.avi`, `.webm`)
|
||||||
|
- Audio (`.mp3`, `.wav`, `.flac`, `.ogg`)
|
||||||
|
|
||||||
|
### Archives
|
||||||
|
- Zip files (`.zip`, `.tar`, `.gz`, `.7z`, `.rar`)
|
||||||
|
- Package files (`.deb`, `.rpm`, `.dmg`, `.pkg`)
|
||||||
|
|
||||||
|
### System Files
|
||||||
|
- Certificates (`.crt`, `.pem`, `.key`)
|
||||||
|
- Configuration files
|
||||||
|
- License files (LICENSE, README)
|
||||||
|
|
||||||
|
## HTML Output Features
|
||||||
|
|
||||||
|
### Responsive Design
|
||||||
|
- Mobile-friendly layout
|
||||||
|
- Collapsible columns on small screens
|
||||||
|
- Touch-friendly navigation
|
||||||
|
|
||||||
|
### Dark Mode
|
||||||
|
- Automatic detection of system preference
|
||||||
|
- Clean dark color scheme
|
||||||
|
- Proper contrast ratios
|
||||||
|
|
||||||
|
### File Information
|
||||||
|
- File sizes in human-readable format
|
||||||
|
- Last modified timestamps
|
||||||
|
- File type icons
|
||||||
|
- Sorting (directories first, then alphabetical)
|
||||||
|
|
||||||
|
### Navigation
|
||||||
|
- Parent directory links
|
||||||
|
- Breadcrumb-style navigation
|
||||||
|
- Clickable file/directory entries
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
s3-genindex/
|
||||||
|
├── cmd/s3-genindex/ # Main application
|
||||||
|
│ ├── main.go # CLI entry point
|
||||||
|
│ └── main_test.go # CLI tests
|
||||||
|
├── internal/indexgen/ # Core library
|
||||||
|
│ ├── indexgen.go # Main functionality
|
||||||
|
│ ├── indexgen_test.go # Unit tests
|
||||||
|
│ └── integration_test.go # Integration tests
|
||||||
|
├── docs/ # Documentation
|
||||||
|
├── Makefile # Build automation
|
||||||
|
├── README.md # Quick start guide
|
||||||
|
└── go.mod # Go module definition
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make build # Build binary
|
||||||
|
make test # Run tests
|
||||||
|
make test-coverage # Run tests with coverage
|
||||||
|
make check # Run all checks (fmt, vet, lint, test)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
The project includes comprehensive tests:
|
||||||
|
|
||||||
|
- **Unit tests**: Test individual functions and utilities
|
||||||
|
- **Integration tests**: Test complete directory processing workflows
|
||||||
|
- **Template tests**: Verify HTML template generation
|
||||||
|
- **CLI tests**: Test command-line argument parsing
|
||||||
|
|
||||||
|
Run tests with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make test # Basic test run
|
||||||
|
make test-coverage # With coverage report
|
||||||
|
go test -v ./... # Verbose output
|
||||||
|
go test -short ./... # Skip integration tests
|
||||||
|
```
|
||||||
|
|
||||||
|
### Code Quality
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make fmt # Format code
|
||||||
|
make vet # Vet for issues
|
||||||
|
make lint # Run golangci-lint (if installed)
|
||||||
|
make check # Run all quality checks
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
No configuration files are needed. All options are provided via command-line arguments.
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
The tool respects standard Go environment variables:
|
||||||
|
- `GOOS` and `GOARCH` for cross-compilation
|
||||||
|
- `GOPATH` and `GOBIN` for installation paths
|
||||||
|
|
||||||
|
## Comparison with Original
|
||||||
|
|
||||||
|
This Go version provides the same functionality as the original Python script with these improvements:
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- Faster execution, especially for large directory trees
|
||||||
|
- Lower memory usage
|
||||||
|
- Single binary deployment
|
||||||
|
|
||||||
|
### Maintenance
|
||||||
|
- Comprehensive test suite
|
||||||
|
- Type safety
|
||||||
|
- Better error handling
|
||||||
|
- Structured codebase
|
||||||
|
|
||||||
|
### Compatibility
|
||||||
|
- Identical HTML output
|
||||||
|
- Same command-line interface
|
||||||
|
- Cross-platform binary
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**Permission Errors**
|
||||||
|
```bash
|
||||||
|
# Ensure read permissions on target directory
|
||||||
|
chmod +r /path/to/directory
|
||||||
|
```
|
||||||
|
|
||||||
|
**Large Directories**
|
||||||
|
```bash
|
||||||
|
# Use verbose mode to monitor progress
|
||||||
|
s3-genindex -v /large/directory
|
||||||
|
|
||||||
|
# Exclude unnecessary files
|
||||||
|
s3-genindex -x "(\.git|node_modules|__pycache__)"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Memory Usage**
|
||||||
|
```bash
|
||||||
|
# Process directories individually for very large trees
|
||||||
|
for dir in */; do s3-genindex "$dir"; done
|
||||||
|
```
|
||||||
|
|
||||||
|
### Debug Mode
|
||||||
|
|
||||||
|
Enable verbose output to see detailed processing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
s3-genindex -v /path/to/debug
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Licensed under the Apache License 2.0. See original Python script for full license text.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch
|
||||||
|
3. Add tests for new functionality
|
||||||
|
4. Run `make check` to ensure code quality
|
||||||
|
5. Submit a pull request
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
### v1.0.0
|
||||||
|
- Initial Go rewrite
|
||||||
|
- Complete feature parity with Python version
|
||||||
|
- Comprehensive test suite
|
||||||
|
- Modern Go project structure
|
||||||
975
internal/indexgen/indexgen.go
Normal file
975
internal/indexgen/indexgen.go
Normal file
@@ -0,0 +1,975 @@
|
|||||||
|
package indexgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultOutputFile = "index.html"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ExtensionTypes = map[string]string{
|
||||||
|
"id_rsa": "cert",
|
||||||
|
"LICENSE": "license",
|
||||||
|
"README": "license",
|
||||||
|
".jpg": "image",
|
||||||
|
".jpeg": "image",
|
||||||
|
".png": "image",
|
||||||
|
".gif": "image",
|
||||||
|
".webp": "image",
|
||||||
|
".tiff": "image",
|
||||||
|
".bmp": "image",
|
||||||
|
".heif": "image",
|
||||||
|
".heic": "image",
|
||||||
|
".svg": "image",
|
||||||
|
".mp4": "video",
|
||||||
|
".mov": "video",
|
||||||
|
".mpeg": "video",
|
||||||
|
".avi": "video",
|
||||||
|
".ogv": "video",
|
||||||
|
".webm": "video",
|
||||||
|
".mkv": "video",
|
||||||
|
".vob": "video",
|
||||||
|
".gifv": "video",
|
||||||
|
".3gp": "video",
|
||||||
|
".mp3": "audio",
|
||||||
|
".m4a": "audio",
|
||||||
|
".aac": "audio",
|
||||||
|
".ogg": "audio",
|
||||||
|
".flac": "audio",
|
||||||
|
".wav": "audio",
|
||||||
|
".wma": "audio",
|
||||||
|
".midi": "audio",
|
||||||
|
".cda": "audio",
|
||||||
|
".aiff": "audio",
|
||||||
|
".aif": "audio",
|
||||||
|
".caf": "audio",
|
||||||
|
".pdf": "pdf",
|
||||||
|
".csv": "csv",
|
||||||
|
".txt": "doc",
|
||||||
|
".doc": "doc",
|
||||||
|
".docx": "doc",
|
||||||
|
".odt": "doc",
|
||||||
|
".fodt": "doc",
|
||||||
|
".rtf": "doc",
|
||||||
|
".abw": "doc",
|
||||||
|
".pages": "doc",
|
||||||
|
".xls": "sheet",
|
||||||
|
".xlsx": "sheet",
|
||||||
|
".ods": "sheet",
|
||||||
|
".fods": "sheet",
|
||||||
|
".numbers": "sheet",
|
||||||
|
".ppt": "ppt",
|
||||||
|
".pptx": "ppt",
|
||||||
|
".odp": "ppt",
|
||||||
|
".fodp": "ppt",
|
||||||
|
".zip": "archive",
|
||||||
|
".gz": "archive",
|
||||||
|
".xz": "archive",
|
||||||
|
".tar": "archive",
|
||||||
|
".7z": "archive",
|
||||||
|
".rar": "archive",
|
||||||
|
".zst": "archive",
|
||||||
|
".bz2": "archive",
|
||||||
|
".bzip": "archive",
|
||||||
|
".arj": "archive",
|
||||||
|
".z": "archive",
|
||||||
|
".deb": "deb",
|
||||||
|
".dpkg": "deb",
|
||||||
|
".rpm": "dist",
|
||||||
|
".exe": "dist",
|
||||||
|
".flatpak": "dist",
|
||||||
|
".appimage": "dist",
|
||||||
|
".jar": "dist",
|
||||||
|
".msi": "dist",
|
||||||
|
".apk": "dist",
|
||||||
|
".ps1": "ps1",
|
||||||
|
".py": "py",
|
||||||
|
".pyc": "py",
|
||||||
|
".pyo": "py",
|
||||||
|
".egg": "py",
|
||||||
|
".sh": "sh",
|
||||||
|
".bash": "sh",
|
||||||
|
".com": "sh",
|
||||||
|
".bat": "sh",
|
||||||
|
".dll": "sh",
|
||||||
|
".so": "sh",
|
||||||
|
".dmg": "dmg",
|
||||||
|
".iso": "iso",
|
||||||
|
".img": "iso",
|
||||||
|
".md": "md",
|
||||||
|
".mdown": "md",
|
||||||
|
".markdown": "md",
|
||||||
|
".ttf": "font",
|
||||||
|
".ttc": "font",
|
||||||
|
".otf": "font",
|
||||||
|
".woff": "font",
|
||||||
|
".woff2": "font",
|
||||||
|
".eof": "font",
|
||||||
|
".apf": "font",
|
||||||
|
".go": "go",
|
||||||
|
".html": "html",
|
||||||
|
".htm": "html",
|
||||||
|
".php": "html",
|
||||||
|
".php3": "html",
|
||||||
|
".asp": "html",
|
||||||
|
".aspx": "html",
|
||||||
|
".css": "css",
|
||||||
|
".scss": "css",
|
||||||
|
".less": "css",
|
||||||
|
".json": "json",
|
||||||
|
".json5": "json",
|
||||||
|
".jsonc": "json",
|
||||||
|
".ts": "ts",
|
||||||
|
".js": "js",
|
||||||
|
".sql": "sql",
|
||||||
|
".db": "db",
|
||||||
|
".sqlite": "db",
|
||||||
|
".mdb": "db",
|
||||||
|
".odb": "db",
|
||||||
|
".eml": "email",
|
||||||
|
".email": "email",
|
||||||
|
".mailbox": "email",
|
||||||
|
".mbox": "email",
|
||||||
|
".msg": "email",
|
||||||
|
".crt": "cert",
|
||||||
|
".pem": "cert",
|
||||||
|
".x509": "cert",
|
||||||
|
".cer": "cert",
|
||||||
|
".der": "cert",
|
||||||
|
".ca-bundle": "cert",
|
||||||
|
".key": "keystore",
|
||||||
|
".keystore": "keystore",
|
||||||
|
".jks": "keystore",
|
||||||
|
".p12": "keystore",
|
||||||
|
".pfx": "keystore",
|
||||||
|
".pub": "keystore",
|
||||||
|
"symlink": "symlink",
|
||||||
|
"generic": "generic",
|
||||||
|
}
|
||||||
|
|
||||||
|
type Options struct {
|
||||||
|
TopDir string
|
||||||
|
Filter string
|
||||||
|
OutputFile string
|
||||||
|
DirAppend bool
|
||||||
|
Recursive bool
|
||||||
|
IncludeHidden bool
|
||||||
|
ExcludeRegex *regexp.Regexp
|
||||||
|
Verbose bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileEntry struct {
|
||||||
|
Name string
|
||||||
|
Path string
|
||||||
|
IsDir bool
|
||||||
|
IsSymlink bool
|
||||||
|
Size int64
|
||||||
|
ModTime time.Time
|
||||||
|
IconType string
|
||||||
|
CSSClass string
|
||||||
|
SizePretty string
|
||||||
|
ModTimeISO string
|
||||||
|
ModTimeHuman string
|
||||||
|
}
|
||||||
|
|
||||||
|
func ProcessDir(topDir string, opts *Options) error {
|
||||||
|
absPath, err := filepath.Abs(topDir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get absolute path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.Verbose {
|
||||||
|
fmt.Printf("Traversing dir %s\n", absPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
indexPath := filepath.Join(absPath, opts.OutputFile)
|
||||||
|
|
||||||
|
file, err := os.Create(indexPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot create file %s: %w", indexPath, err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
dirName := filepath.Base(absPath)
|
||||||
|
|
||||||
|
entries, err := ReadDirEntries(absPath, opts)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(entries, func(i, j int) bool {
|
||||||
|
if entries[i].IsDir != entries[j].IsDir {
|
||||||
|
return entries[i].IsDir
|
||||||
|
}
|
||||||
|
return entries[i].Name < entries[j].Name
|
||||||
|
})
|
||||||
|
|
||||||
|
templateData := struct {
|
||||||
|
DirName string
|
||||||
|
Entries []FileEntry
|
||||||
|
DirAppend bool
|
||||||
|
OutputFile string
|
||||||
|
}{
|
||||||
|
DirName: dirName,
|
||||||
|
Entries: entries,
|
||||||
|
DirAppend: opts.DirAppend,
|
||||||
|
OutputFile: opts.OutputFile,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = GetHTMLTemplate().Execute(file, templateData)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to execute template: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.Recursive {
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir && !entry.IsSymlink {
|
||||||
|
fullPath := filepath.Join(absPath, entry.Name)
|
||||||
|
err := ProcessDir(fullPath, opts)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error processing directory %s: %v\n", fullPath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadDirEntries(dirPath string, opts *Options) ([]FileEntry, error) {
|
||||||
|
files, err := os.ReadDir(dirPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var entries []FileEntry
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
fileName := file.Name()
|
||||||
|
|
||||||
|
if strings.EqualFold(fileName, opts.OutputFile) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !opts.IncludeHidden && strings.HasPrefix(fileName, ".") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.ExcludeRegex != nil && opts.ExcludeRegex.MatchString(fileName) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fullPath := filepath.Join(dirPath, fileName)
|
||||||
|
|
||||||
|
info, err := file.Info()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("*** WARNING *** entry %s is not accessible! SKIPPING! Error: %v\n", fullPath, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.Verbose {
|
||||||
|
fmt.Println(fullPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := FileEntry{
|
||||||
|
Name: fileName,
|
||||||
|
Path: fileName,
|
||||||
|
IsDir: file.IsDir(),
|
||||||
|
ModTime: info.ModTime(),
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.IsSymlink = info.Mode()&os.ModeSymlink != 0
|
||||||
|
|
||||||
|
if file.IsDir() && !entry.IsSymlink {
|
||||||
|
entry.Size = -1
|
||||||
|
entry.SizePretty = "—"
|
||||||
|
entry.IconType = "folder"
|
||||||
|
entry.CSSClass = "folder_filled"
|
||||||
|
entry.Path = fileName + "/"
|
||||||
|
} else if file.IsDir() && entry.IsSymlink {
|
||||||
|
entry.Size = -1
|
||||||
|
entry.SizePretty = "—"
|
||||||
|
entry.IconType = "folder-symlink"
|
||||||
|
fmt.Printf("dir-symlink %s\n", fullPath)
|
||||||
|
} else if !file.IsDir() && entry.IsSymlink {
|
||||||
|
entry.Size = info.Size()
|
||||||
|
entry.SizePretty = PrettySize(entry.Size)
|
||||||
|
entry.IconType = "symlink"
|
||||||
|
fmt.Printf("file-symlink %s\n", fullPath)
|
||||||
|
} else {
|
||||||
|
entry.Size = info.Size()
|
||||||
|
entry.SizePretty = PrettySize(entry.Size)
|
||||||
|
entry.IconType = GetIconType(fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if file.IsDir() && opts.DirAppend {
|
||||||
|
entry.Path += opts.OutputFile
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.ModTimeISO = entry.ModTime.Format(time.RFC3339)
|
||||||
|
entry.ModTimeHuman = entry.ModTime.Format(time.RFC822)
|
||||||
|
|
||||||
|
entries = append(entries, entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetIconType(fileName string) string {
|
||||||
|
ext := strings.ToLower(filepath.Ext(fileName))
|
||||||
|
if iconType, exists := ExtensionTypes[ext]; exists {
|
||||||
|
return iconType
|
||||||
|
}
|
||||||
|
|
||||||
|
if iconType, exists := ExtensionTypes[fileName]; exists {
|
||||||
|
return iconType
|
||||||
|
}
|
||||||
|
|
||||||
|
return "generic"
|
||||||
|
}
|
||||||
|
|
||||||
|
func PrettySize(bytes int64) string {
|
||||||
|
units := []struct {
|
||||||
|
factor int64
|
||||||
|
suffix string
|
||||||
|
}{
|
||||||
|
{1024 * 1024 * 1024 * 1024 * 1024, " PB"},
|
||||||
|
{1024 * 1024 * 1024 * 1024, " TB"},
|
||||||
|
{1024 * 1024 * 1024, " GB"},
|
||||||
|
{1024 * 1024, " MB"},
|
||||||
|
{1024, " KB"},
|
||||||
|
{1, " byte"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, unit := range units {
|
||||||
|
if bytes >= unit.factor {
|
||||||
|
amount := bytes / unit.factor
|
||||||
|
if unit.suffix == " byte" {
|
||||||
|
if amount == 1 {
|
||||||
|
return strconv.FormatInt(amount, 10) + " byte"
|
||||||
|
}
|
||||||
|
return strconv.FormatInt(amount, 10) + " bytes"
|
||||||
|
}
|
||||||
|
return strconv.FormatInt(amount, 10) + unit.suffix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "0 bytes"
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetHTMLTemplate() *template.Template {
|
||||||
|
return template.Must(template.New("index").Funcs(template.FuncMap{
|
||||||
|
"urlEscape": func(s string) string {
|
||||||
|
return url.QueryEscape(s)
|
||||||
|
},
|
||||||
|
"safeHTML": func(s string) template.HTML {
|
||||||
|
return template.HTML(s)
|
||||||
|
},
|
||||||
|
}).Parse(htmlTemplateString))
|
||||||
|
}
|
||||||
|
|
||||||
|
const htmlTemplateString = `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<style>
|
||||||
|
* { padding: 0; margin: 0; }
|
||||||
|
|
||||||
|
path_FIXME {
|
||||||
|
color: #ffb900 !important;
|
||||||
|
}
|
||||||
|
path {
|
||||||
|
color: #8a8a8a;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
text-rendering: optimizespeed;
|
||||||
|
background-color: #ffffff;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
body,
|
||||||
|
a,
|
||||||
|
svg,
|
||||||
|
.layout.current,
|
||||||
|
.layout.current svg,
|
||||||
|
.go-up {
|
||||||
|
color: #333;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #006ed3;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover,
|
||||||
|
h1 a:hover {
|
||||||
|
color: #319cff;
|
||||||
|
}
|
||||||
|
|
||||||
|
header,
|
||||||
|
#summary {
|
||||||
|
padding-left: 5%;
|
||||||
|
padding-right: 5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
th:first-child,
|
||||||
|
td:first-child {
|
||||||
|
width: 5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
th:last-child,
|
||||||
|
td:last-child {
|
||||||
|
width: 5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
padding-top: 25px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: normal;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow-x: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 a {
|
||||||
|
color: #000;
|
||||||
|
margin: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 a:first-child {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
display: block;
|
||||||
|
margin: 3em auto 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 0 2px 5px 1px rgb(0 0 0 / 5%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: Verdana, sans-serif;
|
||||||
|
border-bottom: 1px solid #9C9C9C;
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item {
|
||||||
|
margin-right: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#filter {
|
||||||
|
padding: 4px;
|
||||||
|
border: 1px solid #CCC;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr {
|
||||||
|
border-bottom: 1px dashed #dadada;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr:hover {
|
||||||
|
background-color: #ffffec;
|
||||||
|
}
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
text-align: left;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
padding-top: 15px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
font-size: 16px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
th a {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
th svg {
|
||||||
|
vertical-align: middle;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
td {
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
td:nth-child(2) {
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
td:nth-child(3) {
|
||||||
|
padding: 0 20px 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th:nth-child(4),
|
||||||
|
td:nth-child(4) {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
td:nth-child(2) svg {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
td .name {
|
||||||
|
margin-left: 2.34em;
|
||||||
|
word-break: break-all;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
td .goup {
|
||||||
|
margin-left: 1.75em;
|
||||||
|
padding: 0;
|
||||||
|
word-break: break-all;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
tr.clickable a {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-filled {
|
||||||
|
color: #ffb900 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
|
||||||
|
* {
|
||||||
|
font-size: 1.06rem;
|
||||||
|
}
|
||||||
|
.hideable {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
td:nth-child(2) {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
th:nth-child(3),
|
||||||
|
td:nth-child(3) {
|
||||||
|
padding-right: 5%;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 a {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#filter {
|
||||||
|
max-width: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body {
|
||||||
|
color: #eee;
|
||||||
|
background: #121212;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
color: #eee;
|
||||||
|
background: #151515;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr:hover {
|
||||||
|
background-color: #000020;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round" style="color: red;">
|
||||||
|
<defs>
|
||||||
|
<g id="go-up">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M18 18h-6a3 3 0 0 1 -3 -3v-10l-4 4m8 0l-4 -4"></path>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="folder">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M9 3a1 1 0 0 1 .608 .206l.1 .087l2.706 2.707h6.586a3 3 0 0 1 2.995 2.824l.005 .176v8a3 3 0 0 1 -2.824 2.995l-.176 .005h-14a3 3 0 0 1 -2.995 -2.824l-.005 -.176v-11a3 3 0 0 1 2.824 -2.995l.176 -.005h4z" stroke-width="0" fill="#ffb900"></path>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="folder-symlink">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M9 3a1 1 0 0 1 .608 .206l.1 .087l2.706 2.707h6.586a3 3 0 0 1 2.995 2.824l.005 .176v8a3 3 0 0 1 -2.824 2.995l-.176 .005h-14a3 3 0 0 1 -2.995 -2.824l-.005 -.176v-11a3 3 0 0 1 2.824 -2.995l.176 -.005h4z" stroke-width="0" fill="#ffb900"></path>
|
||||||
|
<path
|
||||||
|
fill="#000000"
|
||||||
|
d="M 2.4,49.68 C 2.4,31.056 16.464,15.84 34.392,14.088 V 5.424 c 0,-2.688 3.216,-4.056 5.112,-2.136 l 19.224,19.552 c 0.408,0.416 0.408,1.056 0,1.472 l -19.224,19.552 c -1.896,1.92 -5.112,0.544 -5.112,-2.136 V 33.464 C 19.576,35.8 7.44,44.376 4.448,59.04 3.072,56.064 2.4,52.944 2.4,49.68 Z"
|
||||||
|
id="path1"
|
||||||
|
transform="scale(0.16) translate(20,50)"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="symlink">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M4 21v-4a3 3 0 0 1 3 -3h5"></path>
|
||||||
|
<path d="M9 17l3 -3l-3 -3"></path>
|
||||||
|
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
|
||||||
|
<path d="M5 11v-6a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2h-9.5"></path>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="generic">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
|
||||||
|
<path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z"></path>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="license">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M15 21h-9a3 3 0 0 1 -3 -3v-1h10v2a2 2 0 0 0 4 0v-14a2 2 0 1 1 2 2h-2m2 -4h-11a3 3 0 0 0 -3 3v11"></path>
|
||||||
|
<path d="M9 7l4 0"></path>
|
||||||
|
<path d="M9 11l4 0"></path>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="image">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M15 8h.01"></path>
|
||||||
|
<path d="M3 6a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v12a3 3 0 0 1 -3 3h-12a3 3 0 0 1 -3 -3v-12z"></path>
|
||||||
|
<path d="M3 16l5 -5c.928 -.893 2.072 -.893 3 0l5 5"></path>
|
||||||
|
<path d="M14 14l1 -1c.928 -.893 2.072 -.893 3 0l3 3"></path>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="video">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path>
|
||||||
|
<path d="M8 4l0 16"></path>
|
||||||
|
<path d="M16 4l0 16"></path>
|
||||||
|
<path d="M4 8l4 0"></path>
|
||||||
|
<path d="M4 16l4 0"></path>
|
||||||
|
<path d="M4 12l16 0"></path>
|
||||||
|
<path d="M16 8l4 0"></path>
|
||||||
|
<path d="M16 16l4 0"></path>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="audio">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M6 17m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0"></path>
|
||||||
|
<path d="M16 17m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0"></path>
|
||||||
|
<path d="M9 17l0 -13l10 0l0 13"></path>
|
||||||
|
<path d="M9 8l10 0"></path>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="pdf">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
|
||||||
|
<path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"></path>
|
||||||
|
<path d="M5 18h1.5a1.5 1.5 0 0 0 0 -3h-1.5v6"></path>
|
||||||
|
<path d="M17 18h2"></path>
|
||||||
|
<path d="M20 15h-3v6"></path>
|
||||||
|
<path d="M11 15v6h1a2 2 0 0 0 2 -2v-2a2 2 0 0 0 -2 -2h-1z"></path>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="csv">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
|
||||||
|
<path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"></path>
|
||||||
|
<path d="M7 16.5a1.5 1.5 0 0 0 -3 0v3a1.5 1.5 0 0 0 3 0"></path>
|
||||||
|
<path d="M10 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75"></path>
|
||||||
|
<path d="M16 15l2 6l2 -6"></path>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="doc">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
|
||||||
|
<path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z"></path>
|
||||||
|
<path d="M9 9l1 0"></path>
|
||||||
|
<path d="M9 13l6 0"></path>
|
||||||
|
<path d="M9 17l6 0"></path>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="sheet">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
|
||||||
|
<path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z"></path>
|
||||||
|
<path d="M8 11h8v7h-8z"></path>
|
||||||
|
<path d="M8 15h8"></path>
|
||||||
|
<path d="M11 11v7"></path>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="ppt">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M9 12v-4"></path>
|
||||||
|
<path d="M15 12v-2"></path>
|
||||||
|
<path d="M12 12v-1"></path>
|
||||||
|
<path d="M3 4h18"></path>
|
||||||
|
<path d="M4 4v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-10"></path>
|
||||||
|
<path d="M12 16v4"></path>
|
||||||
|
<path d="M9 20h6"></path>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="archive">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M6 20.735a2 2 0 0 1 -1 -1.735v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2h-1"></path>
|
||||||
|
<path d="M11 17a2 2 0 0 1 2 2v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a2 2 0 0 1 2 -2z"></path>
|
||||||
|
<path d="M11 5l-1 0"></path>
|
||||||
|
<path d="M13 7l-1 0"></path>
|
||||||
|
<path d="M11 9l-1 0"></path>
|
||||||
|
<path d="M13 11l-1 0"></path>
|
||||||
|
<path d="M11 13l-1 0"></path>
|
||||||
|
<path d="M13 15l-1 0"></path>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="deb">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M12 17c-2.397 -.943 -4 -3.153 -4 -5.635c0 -2.19 1.039 -3.14 1.604 -3.595c2.646 -2.133 6.396 -.27 6.396 3.23c0 2.5 -2.905 2.121 -3.5 1.5c-.595 -.621 -1 -1.5 -.5 -2.5"></path>
|
||||||
|
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"></path>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="dist">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M12 3l8 4.5l0 9l-8 4.5l-8 -4.5l0 -9l8 -4.5"></path>
|
||||||
|
<path d="M12 12l8 -4.5"></path>
|
||||||
|
<path d="M12 12l0 9"></path>
|
||||||
|
<path d="M12 12l-8 -4.5"></path>
|
||||||
|
<path d="M16 5.25l-8 4.5"></path>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="ps1">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M4.887 20h11.868c.893 0 1.664 -.665 1.847 -1.592l2.358 -12c.212 -1.081 -.442 -2.14 -1.462 -2.366a1.784 1.784 0 0 0 -.385 -.042h-11.868c-.893 0 -1.664 .665 -1.847 1.592l-2.358 12c-.212 1.081 .442 2.14 1.462 2.366c.127 .028 .256 .042 .385 .042z"></path>
|
||||||
|
<path d="M9 8l4 4l-6 4"></path>
|
||||||
|
<path d="M12 16h3"></path>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="py">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M12 9h-7a2 2 0 0 0 -2 2v4a2 2 0 0 0 2 2h3"></path>
|
||||||
|
<path d="M12 15h7a2 2 0 0 0 2 -2v-4a2 2 0 0 0 -2 -2h-3"></path>
|
||||||
|
<path d="M8 9v-4a2 2 0 0 1 2 -2h4a2 2 0 0 1 2 2v5a2 2 0 0 1 -2 2h-4a2 2 0 0 0 -2 2v5a2 2 0 0 0 2 2h4a2 2 0 0 0 2 -2v-4"></path>
|
||||||
|
<path d="M11 6l0 .01"></path>
|
||||||
|
<path d="M13 18l0 .01"></path>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="sh">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M17 20h-11a3 3 0 0 1 0 -6h11a3 3 0 0 0 0 6h1a3 3 0 0 0 3 -3v-11a2 2 0 0 0 -2 -2h-10a2 2 0 0 0 -2 2v8"></path>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="dmg">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M3 4m0 1a1 1 0 0 1 1 -1h16a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-16a1 1 0 0 1 -1 -1z"></path>
|
||||||
|
<path d="M7 8v1"></path>
|
||||||
|
<path d="M17 8v1"></path>
|
||||||
|
<path d="M12.5 4c-.654 1.486 -1.26 3.443 -1.5 9h2.5c-.19 2.867 .094 5.024 .5 7"></path>
|
||||||
|
<path d="M7 15.5c3.667 2 6.333 2 10 0"></path>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="iso">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"></path>
|
||||||
|
<path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path>
|
||||||
|
<path d="M7 12a5 5 0 0 1 5 -5"></path>
|
||||||
|
<path d="M12 17a5 5 0 0 0 5 -5"></path>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="md">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M3 5m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z"></path>
|
||||||
|
<path d="M7 15v-6l2 2l2 -2v6"></path>
|
||||||
|
<path d="M14 13l2 2l2 -2m-2 2v-6"></path>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="font">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
|
||||||
|
<path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z"></path>
|
||||||
|
<path d="M11 18h2"></path>
|
||||||
|
<path d="M12 18v-7"></path>
|
||||||
|
<path d="M9 12v-1h6v1"></path>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="go">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M15.695 14.305c1.061 1.06 2.953 .888 4.226 -.384c1.272 -1.273 1.444 -3.165 .384 -4.226c-1.061 -1.06 -2.953 -.888 -4.226 .384c-1.272 1.273 -1.444 3.165 -.384 4.226z"></path>
|
||||||
|
<path d="M12.68 9.233c-1.084 -.497 -2.545 -.191 -3.591 .846c-1.284 1.273 -1.457 3.165 -.388 4.226c1.07 1.06 2.978 .888 4.261 -.384a3.669 3.669 0 0 0 1.038 -1.921h-2.427"></path>
|
||||||
|
<path d="M5.5 15h-1.5"></path>
|
||||||
|
<path d="M6 9h-2"></path>
|
||||||
|
<path d="M5 12h-3"></path>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="html">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
|
||||||
|
<path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"></path>
|
||||||
|
<path d="M2 21v-6"></path>
|
||||||
|
<path d="M5 15v6"></path>
|
||||||
|
<path d="M2 18h3"></path>
|
||||||
|
<path d="M20 15v6h2"></path>
|
||||||
|
<path d="M13 21v-6l2 3l2 -3v6"></path>
|
||||||
|
<path d="M7.5 15h3"></path>
|
||||||
|
<path d="M9 15v6"></path>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="js">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
|
||||||
|
<path d="M3 15h3v4.5a1.5 1.5 0 0 1 -3 0"></path>
|
||||||
|
<path d="M9 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75"></path>
|
||||||
|
<path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2h-1"></path>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="css">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
|
||||||
|
<path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"></path>
|
||||||
|
<path d="M8 16.5a1.5 1.5 0 0 0 -3 0v3a1.5 1.5 0 0 0 3 0"></path>
|
||||||
|
<path d="M11 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75"></path>
|
||||||
|
<path d="M17 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75"></path>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="json">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M20 16v-8l3 8v-8"></path>
|
||||||
|
<path d="M15 8a2 2 0 0 1 2 2v4a2 2 0 1 1 -4 0v-4a2 2 0 0 1 2 -2z"></path>
|
||||||
|
<path d="M1 8h3v6.5a1.5 1.5 0 0 1 -3 0v-.5"></path>
|
||||||
|
<path d="M7 15a1 1 0 0 0 1 1h1a1 1 0 0 0 1 -1v-2a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1h1a1 1 0 0 1 1 1"></path>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="ts">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
|
||||||
|
<path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2h-1"></path>
|
||||||
|
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
|
||||||
|
<path d="M9 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75"></path>
|
||||||
|
<path d="M3.5 15h3"></path>
|
||||||
|
<path d="M5 15v6"></path>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="sql">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
|
||||||
|
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
|
||||||
|
<path d="M5 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75"></path>
|
||||||
|
<path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"></path>
|
||||||
|
<path d="M18 15v6h2"></path>
|
||||||
|
<path d="M13 15a2 2 0 0 1 2 2v2a2 2 0 1 1 -4 0v-2a2 2 0 0 1 2 -2z"></path>
|
||||||
|
<path d="M14 20l1.5 1.5"></path>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="db">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M12 6m-8 0a8 3 0 1 0 16 0a8 3 0 1 0 -16 0"></path>
|
||||||
|
<path d="M4 6v6a8 3 0 0 0 16 0v-6"></path>
|
||||||
|
<path d="M4 12v6a8 3 0 0 0 16 0v-6"></path>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="email">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M3 7a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-10z"></path>
|
||||||
|
<path d="M3 7l9 6l9 -6"></path>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="cert">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M15 15m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0"></path>
|
||||||
|
<path d="M13 17.5v4.5l2 -1.5l2 1.5v-4.5"></path>
|
||||||
|
<path d="M10 19h-5a2 2 0 0 1 -2 -2v-10c0 -1.1 .9 -2 2 -2h14a2 2 0 0 1 2 2v10a2 2 0 0 1 -1 1.73"></path>
|
||||||
|
<path d="M6 9l12 0"></path>
|
||||||
|
<path d="M6 12l3 0"></path>
|
||||||
|
<path d="M6 15l2 0"></path>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="keystore">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M16.555 3.843l3.602 3.602a2.877 2.877 0 0 1 0 4.069l-2.643 2.643a2.877 2.877 0 0 1 -4.069 0l-.301 -.301l-6.558 6.558a2 2 0 0 1 -1.239 .578l-.175 .008h-1.172a1 1 0 0 1 -.993 -.883l-.007 -.117v-1.172a2 2 0 0 1 .467 -1.284l.119 -.13l.414 -.414h2v-2h2v-2l2.144 -2.144l-.301 -.301a2.877 2.877 0 0 1 0 -4.069l2.643 -2.643a2.877 2.877 0 0 1 4.069 0z"></path>
|
||||||
|
<path d="M15 9h.01"></path>
|
||||||
|
</g>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
<header>
|
||||||
|
<h1>{{.DirName}}</h1>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<div class="listing">
|
||||||
|
<table aria-describedby="summary">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Size</th>
|
||||||
|
<th class="hideable">
|
||||||
|
Modified
|
||||||
|
</th>
|
||||||
|
<th class="hideable"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr class="clickable">
|
||||||
|
<td></td>
|
||||||
|
<td><a href="../{{if .DirAppend}}{{.OutputFile}}{{end}}">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-corner-left-up" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M18 18h-6a3 3 0 0 1 -3 -3v-10l-4 4m8 0l-4 -4"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="goup">..</span></a></td>
|
||||||
|
<td>—</td>
|
||||||
|
<td class="hideable">—</td>
|
||||||
|
<td class="hideable"></td>
|
||||||
|
</tr>
|
||||||
|
{{range .Entries}}
|
||||||
|
<tr class="file">
|
||||||
|
<td></td>
|
||||||
|
<td>
|
||||||
|
<a href="{{urlEscape .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>
|
||||||
|
<span class="name">{{.Name}}</span>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td data-order="{{.Size}}">{{safeHTML .SizePretty}}</td>
|
||||||
|
<td class="hideable"><time datetime="{{.ModTimeISO}}">{{.ModTimeHuman}}</time></td>
|
||||||
|
<td class="hideable"></td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
385
internal/indexgen/indexgen_test.go
Normal file
385
internal/indexgen/indexgen_test.go
Normal file
@@ -0,0 +1,385 @@
|
|||||||
|
package indexgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPrettySize(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input int64
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{0, "0 bytes"},
|
||||||
|
{1, "1 byte"},
|
||||||
|
{2, "2 bytes"},
|
||||||
|
{1023, "1023 bytes"},
|
||||||
|
{1024, "1 KB"},
|
||||||
|
{1536, "1 KB"},
|
||||||
|
{2048, "2 KB"},
|
||||||
|
{1024 * 1024, "1 MB"},
|
||||||
|
{1024 * 1024 * 1024, "1 GB"},
|
||||||
|
{1024 * 1024 * 1024 * 1024, "1 TB"},
|
||||||
|
{1024 * 1024 * 1024 * 1024 * 1024, "1 PB"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
result := PrettySize(tt.input)
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("PrettySize(%d) = %s, want %s", tt.input, result, tt.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetIconType(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
filename string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"test.go", "go"},
|
||||||
|
{"test.py", "py"},
|
||||||
|
{"test.js", "js"},
|
||||||
|
{"test.html", "html"},
|
||||||
|
{"test.css", "css"},
|
||||||
|
{"test.json", "json"},
|
||||||
|
{"test.md", "md"},
|
||||||
|
{"test.pdf", "pdf"},
|
||||||
|
{"test.jpg", "image"},
|
||||||
|
{"test.png", "image"},
|
||||||
|
{"test.mp4", "video"},
|
||||||
|
{"test.mp3", "audio"},
|
||||||
|
{"test.zip", "archive"},
|
||||||
|
{"test.txt", "doc"},
|
||||||
|
{"README", "license"},
|
||||||
|
{"LICENSE", "license"},
|
||||||
|
{"unknown.ext", "generic"},
|
||||||
|
{"noext", "generic"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
result := GetIconType(tt.filename)
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("GetIconType(%s) = %s, want %s", tt.filename, result, tt.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetIconTypeCaseInsensitive(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
filename string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"test.GO", "go"},
|
||||||
|
{"test.Py", "py"},
|
||||||
|
{"test.JPG", "image"},
|
||||||
|
{"test.PNG", "image"},
|
||||||
|
{"test.MP4", "video"},
|
||||||
|
{"test.MP3", "audio"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
result := GetIconType(tt.filename)
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("GetIconType(%s) = %s, want %s", tt.filename, result, tt.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTMLTemplate(t *testing.T) {
|
||||||
|
tmpl := GetHTMLTemplate()
|
||||||
|
if tmpl == nil {
|
||||||
|
t.Fatal("GetHTMLTemplate() returned nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test template execution with sample data
|
||||||
|
data := struct {
|
||||||
|
DirName string
|
||||||
|
Entries []FileEntry
|
||||||
|
DirAppend bool
|
||||||
|
OutputFile string
|
||||||
|
}{
|
||||||
|
DirName: "test-dir",
|
||||||
|
Entries: []FileEntry{},
|
||||||
|
DirAppend: false,
|
||||||
|
OutputFile: "index.html",
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err := tmpl.Execute(&buf, data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Template execution failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output := buf.String()
|
||||||
|
if !bytes.Contains([]byte(output), []byte("test-dir")) {
|
||||||
|
t.Error("Template output should contain directory name")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Contains([]byte(output), []byte("<!DOCTYPE html>")) {
|
||||||
|
t.Error("Template output should contain HTML doctype")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTMLTemplateWithEntries(t *testing.T) {
|
||||||
|
tmpl := GetHTMLTemplate()
|
||||||
|
|
||||||
|
entries := []FileEntry{
|
||||||
|
{
|
||||||
|
Name: "test.go",
|
||||||
|
Path: "test.go",
|
||||||
|
IsDir: false,
|
||||||
|
IsSymlink: false,
|
||||||
|
Size: 1024,
|
||||||
|
ModTime: time.Now(),
|
||||||
|
IconType: "go",
|
||||||
|
CSSClass: "",
|
||||||
|
SizePretty: "1 KB",
|
||||||
|
ModTimeISO: "2023-01-01T12:00:00Z",
|
||||||
|
ModTimeHuman: "01 Jan 23 12:00 UTC",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "subfolder",
|
||||||
|
Path: "subfolder/",
|
||||||
|
IsDir: true,
|
||||||
|
IsSymlink: false,
|
||||||
|
Size: -1,
|
||||||
|
ModTime: time.Now(),
|
||||||
|
IconType: "folder",
|
||||||
|
CSSClass: "folder_filled",
|
||||||
|
SizePretty: "—",
|
||||||
|
ModTimeISO: "2023-01-01T12:00:00Z",
|
||||||
|
ModTimeHuman: "01 Jan 23 12:00 UTC",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
data := struct {
|
||||||
|
DirName string
|
||||||
|
Entries []FileEntry
|
||||||
|
DirAppend bool
|
||||||
|
OutputFile string
|
||||||
|
}{
|
||||||
|
DirName: "test-dir",
|
||||||
|
Entries: entries,
|
||||||
|
DirAppend: false,
|
||||||
|
OutputFile: "index.html",
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err := tmpl.Execute(&buf, data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Template execution with entries failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output := buf.String()
|
||||||
|
if !bytes.Contains([]byte(output), []byte("test.go")) {
|
||||||
|
t.Error("Template output should contain file name")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Contains([]byte(output), []byte("subfolder")) {
|
||||||
|
t.Error("Template output should contain folder name")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Contains([]byte(output), []byte("#go")) {
|
||||||
|
t.Error("Template output should contain Go icon reference")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Contains([]byte(output), []byte("#folder")) {
|
||||||
|
t.Error("Template output should contain folder icon reference")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadDirEntries(t *testing.T) {
|
||||||
|
// Create a temporary directory with test files
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
|
||||||
|
// Create test files
|
||||||
|
testFiles := []string{"test.go", "test.py", "README.md", ".hidden"}
|
||||||
|
for _, file := range testFiles {
|
||||||
|
f, err := os.Create(filepath.Join(tempDir, file))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test file %s: %v", file, err)
|
||||||
|
}
|
||||||
|
_, err = f.WriteString("test content")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to write to file: %v", err)
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a subdirectory
|
||||||
|
subDir := filepath.Join(tempDir, "subdir")
|
||||||
|
err := os.Mkdir(subDir, 0755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create subdirectory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := &Options{
|
||||||
|
OutputFile: "index.html",
|
||||||
|
IncludeHidden: false,
|
||||||
|
Verbose: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := ReadDirEntries(tempDir, opts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReadDirEntries failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should have 4 entries (3 visible files + 1 directory), .hidden should be excluded
|
||||||
|
expectedCount := 4
|
||||||
|
if len(entries) != expectedCount {
|
||||||
|
t.Errorf("Expected %d entries, got %d", expectedCount, len(entries))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that we have the expected files (ReadDirEntries doesn't sort, ProcessDir does)
|
||||||
|
foundDir := false
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir && entry.Name == "subdir" {
|
||||||
|
foundDir = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundDir {
|
||||||
|
t.Error("Expected to find subdirectory 'subdir'")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that hidden file is excluded
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.Name == ".hidden" {
|
||||||
|
t.Error("Hidden file should be excluded when IncludeHidden is false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadDirEntriesWithHidden(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
|
||||||
|
// Create test files including hidden
|
||||||
|
testFiles := []string{"test.go", ".hidden"}
|
||||||
|
for _, file := range testFiles {
|
||||||
|
f, err := os.Create(filepath.Join(tempDir, file))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test file %s: %v", file, err)
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := &Options{
|
||||||
|
OutputFile: "index.html",
|
||||||
|
IncludeHidden: true,
|
||||||
|
Verbose: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := ReadDirEntries(tempDir, opts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReadDirEntries failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should include hidden file
|
||||||
|
found := false
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.Name == ".hidden" {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Error("Hidden file should be included when IncludeHidden is true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadDirEntriesWithRegexExclusion(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
|
||||||
|
// Create test files
|
||||||
|
testFiles := []string{"test.go", "test.py", "build.log", "node_modules.txt"}
|
||||||
|
for _, file := range testFiles {
|
||||||
|
f, err := os.Create(filepath.Join(tempDir, file))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test file %s: %v", file, err)
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
regex := regexp.MustCompile("(build|node_modules)")
|
||||||
|
opts := &Options{
|
||||||
|
OutputFile: "index.html",
|
||||||
|
IncludeHidden: false,
|
||||||
|
ExcludeRegex: regex,
|
||||||
|
Verbose: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := ReadDirEntries(tempDir, opts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReadDirEntries failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that excluded files are not present
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.Name == "build.log" || entry.Name == "node_modules.txt" {
|
||||||
|
t.Errorf("File %s should be excluded by regex", entry.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should have 2 entries (test.go, test.py)
|
||||||
|
if len(entries) != 2 {
|
||||||
|
t.Errorf("Expected 2 entries after regex exclusion, got %d", len(entries))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileEntryProperties(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
|
||||||
|
// Create a test file with known content
|
||||||
|
testFile := filepath.Join(tempDir, "test.go")
|
||||||
|
content := "package main\nfunc main() {}\n"
|
||||||
|
err := os.WriteFile(testFile, []byte(content), 0644)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := &Options{
|
||||||
|
OutputFile: "index.html",
|
||||||
|
IncludeHidden: false,
|
||||||
|
Verbose: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := ReadDirEntries(tempDir, opts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReadDirEntries failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(entries) != 1 {
|
||||||
|
t.Fatalf("Expected 1 entry, got %d", len(entries))
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := entries[0]
|
||||||
|
|
||||||
|
// Verify properties
|
||||||
|
if entry.Name != "test.go" {
|
||||||
|
t.Errorf("Expected name 'test.go', got '%s'", entry.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry.IsDir {
|
||||||
|
t.Error("Expected file to not be a directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry.IsSymlink {
|
||||||
|
t.Error("Expected file to not be a symlink")
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry.IconType != "go" {
|
||||||
|
t.Errorf("Expected icon type 'go', got '%s'", entry.IconType)
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry.Size != int64(len(content)) {
|
||||||
|
t.Errorf("Expected size %d, got %d", len(content), entry.Size)
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry.SizePretty != PrettySize(int64(len(content))) {
|
||||||
|
t.Errorf("Size pretty mismatch")
|
||||||
|
}
|
||||||
|
}
|
||||||
354
internal/indexgen/integration_test.go
Normal file
354
internal/indexgen/integration_test.go
Normal file
@@ -0,0 +1,354 @@
|
|||||||
|
package indexgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestProcessDirBasic(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
|
||||||
|
// Create test files
|
||||||
|
testFiles := []string{"test.go", "test.py", "README.md"}
|
||||||
|
for _, file := range testFiles {
|
||||||
|
f, err := os.Create(filepath.Join(tempDir, file))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test file %s: %v", file, err)
|
||||||
|
}
|
||||||
|
_, err = f.WriteString("test content")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to write to file: %v", err)
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := &Options{
|
||||||
|
TopDir: tempDir,
|
||||||
|
OutputFile: "index.html",
|
||||||
|
IncludeHidden: false,
|
||||||
|
Verbose: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := ProcessDir(tempDir, opts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ProcessDir failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that index.html was created
|
||||||
|
indexPath := filepath.Join(tempDir, "index.html")
|
||||||
|
if _, err := os.Stat(indexPath); os.IsNotExist(err) {
|
||||||
|
t.Fatal("index.html was not created")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read and verify content
|
||||||
|
content, err := os.ReadFile(indexPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read index.html: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlContent := string(content)
|
||||||
|
|
||||||
|
// Check for HTML structure
|
||||||
|
if !strings.Contains(htmlContent, "<!DOCTYPE html>") {
|
||||||
|
t.Error("index.html should contain HTML doctype")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for file entries
|
||||||
|
for _, file := range testFiles {
|
||||||
|
if !strings.Contains(htmlContent, file) {
|
||||||
|
t.Errorf("index.html should contain file %s", file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for proper file icons
|
||||||
|
if !strings.Contains(htmlContent, "#go") {
|
||||||
|
t.Error("index.html should contain Go icon reference for .go files")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(htmlContent, "#py") {
|
||||||
|
t.Error("index.html should contain Python icon reference for .py files")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(htmlContent, "#md") {
|
||||||
|
t.Error("index.html should contain Markdown icon reference for .md files")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessDirRecursive(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
|
||||||
|
// Create test files in root
|
||||||
|
f, err := os.Create(filepath.Join(tempDir, "root.txt"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create root file: %v", err)
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
|
||||||
|
// Create subdirectory with files
|
||||||
|
subDir := filepath.Join(tempDir, "subdir")
|
||||||
|
err = os.Mkdir(subDir, 0755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create subdirectory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
subFile, err := os.Create(filepath.Join(subDir, "sub.txt"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create sub file: %v", err)
|
||||||
|
}
|
||||||
|
subFile.Close()
|
||||||
|
|
||||||
|
// Create nested subdirectory
|
||||||
|
nestedDir := filepath.Join(subDir, "nested")
|
||||||
|
err = os.Mkdir(nestedDir, 0755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create nested directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nestedFile, err := os.Create(filepath.Join(nestedDir, "nested.txt"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create nested file: %v", err)
|
||||||
|
}
|
||||||
|
nestedFile.Close()
|
||||||
|
|
||||||
|
opts := &Options{
|
||||||
|
TopDir: tempDir,
|
||||||
|
OutputFile: "index.html",
|
||||||
|
Recursive: true,
|
||||||
|
IncludeHidden: false,
|
||||||
|
Verbose: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ProcessDir(tempDir, opts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ProcessDir failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that index.html files were created in each directory
|
||||||
|
indexPaths := []string{
|
||||||
|
filepath.Join(tempDir, "index.html"),
|
||||||
|
filepath.Join(subDir, "index.html"),
|
||||||
|
filepath.Join(nestedDir, "index.html"),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, indexPath := range indexPaths {
|
||||||
|
if _, err := os.Stat(indexPath); os.IsNotExist(err) {
|
||||||
|
t.Errorf("index.html was not created at %s", indexPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check root index.html contains subdirectory
|
||||||
|
rootContent, err := os.ReadFile(filepath.Join(tempDir, "index.html"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read root index.html: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(string(rootContent), "subdir") {
|
||||||
|
t.Error("Root index.html should contain subdirectory")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check sub index.html contains nested directory
|
||||||
|
subContent, err := os.ReadFile(filepath.Join(subDir, "index.html"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read sub index.html: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(string(subContent), "nested") {
|
||||||
|
t.Error("Sub index.html should contain nested directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(string(subContent), "sub.txt") {
|
||||||
|
t.Error("Sub index.html should contain sub.txt file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessDirWithExcludeRegex(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
|
||||||
|
// Create test files
|
||||||
|
testFiles := []string{"include.go", "exclude.tmp", "node_modules.txt"}
|
||||||
|
for _, file := range testFiles {
|
||||||
|
f, err := os.Create(filepath.Join(tempDir, file))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test file %s: %v", file, err)
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
regex := regexp.MustCompile("(tmp|node_modules)")
|
||||||
|
opts := &Options{
|
||||||
|
TopDir: tempDir,
|
||||||
|
OutputFile: "index.html",
|
||||||
|
ExcludeRegex: regex,
|
||||||
|
IncludeHidden: false,
|
||||||
|
Verbose: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := ProcessDir(tempDir, opts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ProcessDir failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read and verify content
|
||||||
|
content, err := os.ReadFile(filepath.Join(tempDir, "index.html"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read index.html: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlContent := string(content)
|
||||||
|
|
||||||
|
// Check that included file is present
|
||||||
|
if !strings.Contains(htmlContent, "include.go") {
|
||||||
|
t.Error("index.html should contain include.go")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that excluded files are not present
|
||||||
|
if strings.Contains(htmlContent, "exclude.tmp") {
|
||||||
|
t.Error("index.html should not contain exclude.tmp (excluded by regex)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(htmlContent, "node_modules.txt") {
|
||||||
|
t.Error("index.html should not contain node_modules.txt (excluded by regex)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessDirWithDirAppend(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
|
||||||
|
// Create a subdirectory
|
||||||
|
subDir := filepath.Join(tempDir, "subdir")
|
||||||
|
err := os.Mkdir(subDir, 0755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create subdirectory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := &Options{
|
||||||
|
TopDir: tempDir,
|
||||||
|
OutputFile: "index.html",
|
||||||
|
DirAppend: true,
|
||||||
|
IncludeHidden: false,
|
||||||
|
Verbose: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ProcessDir(tempDir, opts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ProcessDir failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read and verify content
|
||||||
|
content, err := os.ReadFile(filepath.Join(tempDir, "index.html"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read index.html: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlContent := string(content)
|
||||||
|
|
||||||
|
// Check that directory links include index.html (URL escaped)
|
||||||
|
if !strings.Contains(htmlContent, "subdir%2Findex.html") {
|
||||||
|
t.Errorf("Directory links should include index.html when DirAppend is true. Expected subdir%%2Findex.html in content")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessDirVerbose(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
|
||||||
|
// Create test file
|
||||||
|
f, err := os.Create(filepath.Join(tempDir, "test.txt"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test file: %v", err)
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
|
||||||
|
opts := &Options{
|
||||||
|
TopDir: tempDir,
|
||||||
|
OutputFile: "index.html",
|
||||||
|
Verbose: true,
|
||||||
|
IncludeHidden: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ProcessDir(tempDir, opts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ProcessDir failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that index.html was created
|
||||||
|
indexPath := filepath.Join(tempDir, "index.html")
|
||||||
|
if _, err := os.Stat(indexPath); os.IsNotExist(err) {
|
||||||
|
t.Fatal("index.html was not created")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessDirErrorHandling(t *testing.T) {
|
||||||
|
// Test with non-existent directory
|
||||||
|
opts := &Options{
|
||||||
|
TopDir: "/non/existent/directory",
|
||||||
|
OutputFile: "index.html",
|
||||||
|
IncludeHidden: false,
|
||||||
|
Verbose: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := ProcessDir("/non/existent/directory", opts)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("ProcessDir should fail with non-existent directory")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessDirWithSymlinks(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
|
||||||
|
// Create a regular file
|
||||||
|
regularFile := filepath.Join(tempDir, "regular.txt")
|
||||||
|
f, err := os.Create(regularFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create regular file: %v", err)
|
||||||
|
}
|
||||||
|
_, err = f.WriteString("content")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to write to file: %v", err)
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
|
||||||
|
// Create a symlink to the file (skip on Windows)
|
||||||
|
symlinkFile := filepath.Join(tempDir, "symlink.txt")
|
||||||
|
err = os.Symlink(regularFile, symlinkFile)
|
||||||
|
if err != nil {
|
||||||
|
// Skip symlink tests on systems that don't support them
|
||||||
|
t.Skipf("Skipping symlink test: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := &Options{
|
||||||
|
TopDir: tempDir,
|
||||||
|
OutputFile: "index.html",
|
||||||
|
IncludeHidden: false,
|
||||||
|
Verbose: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ProcessDir(tempDir, opts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ProcessDir failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read and verify content
|
||||||
|
content, err := os.ReadFile(filepath.Join(tempDir, "index.html"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read index.html: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlContent := string(content)
|
||||||
|
|
||||||
|
// Check that both regular file and symlink are present
|
||||||
|
if !strings.Contains(htmlContent, "regular.txt") {
|
||||||
|
t.Error("index.html should contain regular file")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(htmlContent, "symlink.txt") {
|
||||||
|
t.Error("index.html should contain symlink file")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that symlink has appropriate icon
|
||||||
|
if !strings.Contains(htmlContent, "#symlink") {
|
||||||
|
t.Error("index.html should contain symlink icon for symlinked file")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user