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 DryRun 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) 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, } if opts.DryRun { // Dry run mode: show what would be written fmt.Printf("Would write index file: %s\n", indexPath) fmt.Printf("Directory: %s\n", dirName) fmt.Printf("Entries found: %d\n", len(entries)) for _, entry := range entries { entryType := "file" if entry.IsDir { entryType = "directory" } fmt.Printf(" %s: %s\n", entryType, entry.Name) } } else { // Normal mode: actually write the file file, err := os.Create(indexPath) if err != nil { return fmt.Errorf("cannot create file %s: %w", indexPath, err) } defer file.Close() 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 = `