package main import ( "bufio" "fmt" "io/ioutil" "net/url" "os" "path/filepath" "sort" "strings" ) func generateDirectoryListing(URL *url.URL, path string, config Config) (string, error) { var listing string files, err := ioutil.ReadDir(path) if err != nil { return listing, err } listing = "# Directory listing\n\n" // Override with .mollyhead file header_path := filepath.Join(path, ".mollyhead") _, err = os.Stat(header_path) if err == nil { header, err := ioutil.ReadFile(header_path) if err != nil { return listing, err } listing = string(header) } // Do "up" link first if URL.Path != "/" { if strings.HasSuffix(URL.Path, "/") { URL.Path = URL.Path[:len(URL.Path)-1] } up := filepath.Dir(URL.Path) listing += fmt.Sprintf("=> %s %s\n", up, "..") } // Sort files sort.SliceStable(files, func(i, j int) bool { if config.DirectoryReverse { i, j = j, i } if config.DirectorySort == "Name" { if config.DirectoriesFirst { if files[i].IsDir() { if files[j].IsDir() { return files[i].Name() < files[j].Name() } return true } if files[j].IsDir() { if files[i].IsDir() { return files[i].Name() < files[j].Name() } return false } } return files[i].Name() < files[j].Name() } else if config.DirectorySort == "Size" { if config.DirectoriesFirst { if files[i].IsDir() { if files[j].IsDir() { return files[i].Size() < files[j].Size() } return true } if files[j].IsDir() { if files[i].IsDir() { return files[i].Size() < files[j].Size() } return false } } return files[i].Size() < files[j].Size() } else if config.DirectorySort == "Time" { if config.DirectoriesFirst { if files[i].IsDir() { if files[j].IsDir() { return files[i].ModTime().Before(files[j].ModTime()) } return true } if files[j].IsDir() { if files[i].IsDir() { return files[i].ModTime().Before(files[j].ModTime()) } return false } } return files[i].ModTime().Before(files[j].ModTime()) } return false // Should not happen }) // Format lines for _, file := range files { // Skip dotfiles if strings.HasPrefix(file.Name(), ".") { continue } // Only list world readable files if uint64(file.Mode().Perm())&0444 != 0444 { continue } // Make sure links to directories have a trailing slash, // to avoid needless redirects relativeUrl := url.PathEscape(file.Name()) if file.IsDir() { relativeUrl += "/" } listing += fmt.Sprintf("=> %s %s\n", relativeUrl, generatePrettyFileLabel(file, path, config)) } return listing, nil } func generatePrettyFileLabel(info os.FileInfo, path string, config Config) string { var size string if info.IsDir() { size = " " } else if info.Size() < 1024 { size = fmt.Sprintf("%4d B", info.Size()) } else if info.Size() < (1024 << 10) { size = fmt.Sprintf("%4d KiB", info.Size()>>10) } else if info.Size() < 1024<<20 { size = fmt.Sprintf("%4d MiB", info.Size()>>20) } else if info.Size() < 1024<<30 { size = fmt.Sprintf("%4d GiB", info.Size()>>30) } else if info.Size() < 1024<<40 { size = fmt.Sprintf("%4d TiB", info.Size()>>40) } else { size = "GIGANTIC" } name := info.Name() if config.DirectoryTitles && filepath.Ext(name) == "."+config.GeminiExt { name = readHeading(path, info) } if len(name) > 40 { name = name[:36] + "..." } if info.IsDir() { name += "/" } return fmt.Sprintf("%-40s %s %v", name, size, info.ModTime().Format("Jan _2 2006")) } func readHeading(path string, info os.FileInfo) string { filePath := filepath.Join(path, info.Name()) file, err := os.Open(filePath) if err != nil { return info.Name() } defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { line := scanner.Text() if strings.HasPrefix(line, "# ") { return strings.TrimSpace(line[1:]) } } return info.Name() }