Compare commits

...

4 commits

Author SHA1 Message Date
56d8733a40
add some common code file extensions as text/plain 2026-03-18 09:48:45 +00:00
d40dec3566
tidy source files
separating logging and web serving into modules
2026-03-18 09:39:02 +00:00
3c38fa2f7f
update readme 2026-03-18 09:19:52 +00:00
3b64b2b978
credit contributors in footer 2026-03-18 09:18:43 +00:00
5 changed files with 296 additions and 265 deletions

View file

@ -2,7 +2,7 @@
a nifty static file indexer a nifty static file indexer
indir provides a nice, simple frontend for browsing your web directories! indir provides a nice, simple web frontend for browsing your file directories!
heavily inspired by traditional file indexers provided by the likes of nginx heavily inspired by traditional file indexers provided by the likes of nginx
and apache, with some modern amenities on top :) and apache, with some modern amenities on top :)
@ -18,23 +18,9 @@ indir [--host address] [--port port] [--root http_root] directory
## features ## features
### file serving - **index files:** if available, indir will prefer serving an `index.html` file
indir has the possibility of automatically serving an `index.html` instead of displaying the directory view! instead of a directory listing.
- **custom CSS:** indir will append the contents of a local `index.css` file to
### customization the default stylesheet, if applicable.
- **readme files:** adding a `readme.md` file (case insensitive) will append
#### readme its contents beneath the directory tree in rendered markdown.
drop a `README.md` into any directory and indir will render
it below the file listing with full markdown support!
#### custom css
using a 'index.css' file, you can customize the directory view!
---
## to-do:
- [x] use templates instead of hard-coded HTML (i was lazy)
- [x] directory header from readme file
- [x] directory stylesheet overrides
- [x] index.html serving support
- [x] fix mime-types for browser file view

57
log/main.go Normal file
View file

@ -0,0 +1,57 @@
package log
import (
"fmt"
"net/http"
"strconv"
"time"
)
type LoggingResponseWriter struct {
http.ResponseWriter
Status int
}
var COL_Reset = "\033[0m"
var COL_Red = "\033[31m"
var COL_Green = "\033[32m"
var COL_Yellow = "\033[33m"
var COL_Blue = "\033[34m"
var COL_Purple = "\033[35m"
var COL_Cyan = "\033[36m"
var COL_Gray = "\033[37m"
var COL_White = "\033[97m"
func HTTPLog(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
lrw := LoggingResponseWriter{w, http.StatusOK}
next.ServeHTTP(&lrw, r)
after := time.Now()
difference := (after.Nanosecond() - start.Nanosecond()) / 1_000_000
elapsed := "<1"
if difference >= 1 {
elapsed = strconv.Itoa(difference)
}
statusColour := COL_Reset
if lrw.Status - 600 <= 0 { statusColour = COL_Red }
if lrw.Status - 500 <= 0 { statusColour = COL_Yellow }
if lrw.Status - 400 <= 0 { statusColour = COL_White }
if lrw.Status - 300 <= 0 { statusColour = COL_Green }
fmt.Printf("[%s] %s %s - %s%d%s (%sms) (%s)\n",
after.Format(time.UnixDate),
r.Method,
r.URL.Path,
statusColour,
lrw.Status,
COL_Reset,
elapsed,
r.Header["User-Agent"][0])
})
}

254
main.go
View file

@ -2,43 +2,13 @@ package main
import ( import (
"fmt" "fmt"
"html/template"
"io/fs"
"mime"
"net/http" "net/http"
"os" "os"
"path"
"path/filepath"
"slices"
"strconv" "strconv"
"strings" "strings"
"time"
_ "embed"
"github.com/gomarkdown/markdown" "forge.arimelody.space/ari/indir/log"
"github.com/gomarkdown/markdown/html" "forge.arimelody.space/ari/indir/web"
"github.com/gomarkdown/markdown/parser"
)
//go:embed templates/dir.html
var dirTemplateSrc string
type (
Directory struct {
Name string
Root bool
Files []*File
Readme template.HTML
CSS template.CSS
}
File struct {
Name string
URI string
IsDir bool
Size string
ModifiedDate string
}
) )
func main() { func main() {
@ -108,230 +78,30 @@ func main() {
".DS_Store", ".DS_Store",
} }
dirTemplate, err := template.New("dir").Parse(dirTemplateSrc)
if err != nil {
fmt.Fprintf(os.Stderr, "fatal: failed to parse directory template: %v\n", err)
os.Exit(1)
}
fmt.Printf("Now hosting \"%s\" at http://%s:%d", filesDir, host, port) fmt.Printf("Now hosting \"%s\" at http://%s:%d", filesDir, host, port)
if root != "/" { fmt.Printf("%s", root) } if root != "/" { fmt.Printf("%s", root) }
fmt.Println(".") fmt.Println(".")
http.ListenAndServe(fmt.Sprintf("%s:%d", host, port), HTTPLog(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { app := web.AppState{
if !strings.HasPrefix(r.URL.Path, root) { Root: root,
http.NotFound(w, r) FilesDir: filesDir,
return IgnoredFiles: ignoredFiles,
} }
isRoot := r.URL.Path == root
fpath := path.Join(filesDir, strings.TrimPrefix(r.URL.Path, root)) http.ListenAndServe(
info, err := os.Stat(fpath) fmt.Sprintf("%s:%d", host, port),
if err != nil { log.HTTPLog(web.Handler(&app)),
http.NotFound(w, r) )
return
}
// downloading file
if !info.IsDir() {
if strings.HasSuffix(r.URL.Path, "/") {
http.Redirect(w, r, strings.TrimSuffix(r.URL.Path, "/"), http.StatusFound)
return
}
file, err := os.Open(fpath)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
defer func() {
err := file.Close()
if err != nil {
fmt.Fprintf(os.Stderr, "failed to close file %s: %v\n", fpath, err)
}
}()
mimeType := "application/octet-stream"
extPos := strings.LastIndex(info.Name(), ".")
if extPos != -1 {
if m := mime.TypeByExtension(info.Name()[extPos:]); m != "" {
mimeType = m
}
}
w.Header().Set("Content-Type", mimeType)
w.WriteHeader(http.StatusOK)
_, err = file.WriteTo(w)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to send file %s: %v\n", fpath, err)
}
return
}
if !strings.HasSuffix(r.URL.Path, "/") {
http.Redirect(w, r, r.URL.Path + "/", http.StatusFound)
return
}
// serve index.html if present (case-sensitive)
indexPath := filepath.Join(fpath, "index.html")
if indexInfo, err := os.Stat(indexPath); err == nil && !indexInfo.IsDir() {
http.ServeFile(w, r, indexPath)
return
}
// load index.css if present
var customCSS template.CSS
cssPath := filepath.Join(fpath, "index.css")
if cssInfo, err := os.Stat(cssPath); err == nil && !cssInfo.IsDir() {
if src, err := os.ReadFile(cssPath); err == nil {
customCSS = template.CSS(src)
}
}
// embeded readme
var readmeHTML template.HTML
entries, err := os.ReadDir(fpath)
if err == nil {
for _, entry := range entries {
if strings.EqualFold(entry.Name(), "readme.md") {
src, err := os.ReadFile(filepath.Join(fpath, entry.Name()))
if err == nil {
mdFlags := html.CommonFlags | html.HrefTargetBlank
mdRenderer := html.NewRenderer(html.RendererOptions{Flags: mdFlags})
mdParser := parser.NewWithExtensions(parser.CommonExtensions | parser.AutoHeadingIDs)
md := mdParser.Parse(src)
readmeHTML = template.HTML(markdown.Render(md, mdRenderer))
}
break
}
}
}
data := Directory{
Root: isRoot,
Name: r.URL.Path,
Files: []*File{},
Readme: readmeHTML,
CSS: customCSS,
}
fsDir := os.DirFS(fpath)
directories, err := fs.ReadDir(fsDir, ".")
for _, dir := range directories {
name := dir.Name()
if slices.Contains(ignoredFiles, name) { continue }
info, err := dir.Info()
if err != nil { continue }
var uri string
if isRoot {
uri = root + name
} else {
uri = r.URL.Path + name
}
sizeStr := "&mdash;"
if !info.IsDir() {
size := info.Size()
sizeDenom := "B"
if size > 1000 {
size /= 1000
sizeDenom = "KB"
}
if size > 1000 {
size /= 1000
sizeDenom = "MB"
}
if size > 1000 {
size /= 1000
sizeDenom = "GB"
}
sizeStr = fmt.Sprintf("%d%s", size, sizeDenom)
}
dateStr := info.ModTime().Format("02-Jan-2006 15:04")
data.Files = append(data.Files, &File{
Name: name,
URI: uri,
IsDir: info.IsDir(),
Size: sizeStr,
ModifiedDate: dateStr,
})
}
if err != nil {
fmt.Fprintf(os.Stderr, "failed to open directory: %v\n", err)
}
w.Header().Set("Content-Type", "text/html")
w.Header().Set("Server", "indir")
w.WriteHeader(http.StatusOK)
dirTemplate.Execute(w, data)
})))
}
type LoggingResponseWriter struct {
http.ResponseWriter
Status int
}
var COL_Reset = "\033[0m"
var COL_Red = "\033[31m"
var COL_Green = "\033[32m"
var COL_Yellow = "\033[33m"
var COL_Blue = "\033[34m"
var COL_Purple = "\033[35m"
var COL_Cyan = "\033[36m"
var COL_Gray = "\033[37m"
var COL_White = "\033[97m"
func HTTPLog(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
lrw := LoggingResponseWriter{w, http.StatusOK}
next.ServeHTTP(&lrw, r)
after := time.Now()
difference := (after.Nanosecond() - start.Nanosecond()) / 1_000_000
elapsed := "<1"
if difference >= 1 {
elapsed = strconv.Itoa(difference)
}
statusColour := COL_Reset
if lrw.Status - 600 <= 0 { statusColour = COL_Red }
if lrw.Status - 500 <= 0 { statusColour = COL_Yellow }
if lrw.Status - 400 <= 0 { statusColour = COL_White }
if lrw.Status - 300 <= 0 { statusColour = COL_Green }
fmt.Printf("[%s] %s %s - %s%d%s (%sms) (%s)\n",
after.Format(time.UnixDate),
r.Method,
r.URL.Path,
statusColour,
lrw.Status,
COL_Reset,
elapsed,
r.Header["User-Agent"][0])
})
} }
func printHelp() { func printHelp() {
fmt.Printf( fmt.Printf(
`%s [--host address] [--port port] [--root http_root] directory `indir [--host address] [--port port] [--root http_root] directory
--help shows this help message --help shows this help message
--host address hosts on the specified address --host address hosts on the specified address
--port port hosts on the specified port --port port hosts on the specified port
--root http_root hosts on the specified subdirectory, i.e. `+"`/files/`\n", --root http_root hosts on the specified subdirectory, i.e. `+"`/files/`\n",
os.Args[0],
) )
os.Exit(0) os.Exit(0)
} }

View file

@ -179,9 +179,9 @@
</main> </main>
<footer> <footer>
<em <em
>made with <span aria-label="love"></span> by ari, 2025 >made with <span aria-label="love"></span> by ari melody and contributors, 2026
<a <a
href="https://forge.arimelody.space/ari/indir" href="https://codeberg.org/arimelody/indir"
target="_blank" target="_blank"
>[source]</a >[source]</a
></em ></em

218
web/main.go Normal file
View file

@ -0,0 +1,218 @@
package web
import (
_ "embed"
"fmt"
"html/template"
"io/fs"
"mime"
"net/http"
"os"
"path"
"path/filepath"
"slices"
"strings"
"github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/html"
"github.com/gomarkdown/markdown/parser"
)
type (
AppState struct {
Root string
FilesDir string
IgnoredFiles []string
}
Directory struct {
Name string
Root bool
Files []*File
Readme template.HTML
CSS template.CSS
}
File struct {
Name string
URI string
IsDir bool
Size string
ModifiedDate string
}
)
var mimeTypes = map[string]string {
".go": "text/plain",
".rs": "text/plain",
".c": "text/plain",
".h": "text/plain",
".cpp": "text/plain",
".hpp": "text/plain",
".java": "text/plain",
}
//go:embed dir.html
var dirTemplateSrc string
var dirTemplate = template.Must(template.New("dir").Parse(dirTemplateSrc))
func Handler(app *AppState) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !strings.HasPrefix(r.URL.Path, app.Root) {
http.NotFound(w, r)
return
}
isRoot := r.URL.Path == app.Root
fpath := path.Join(app.FilesDir, strings.TrimPrefix(r.URL.Path, app.Root))
info, err := os.Stat(fpath)
if err != nil {
http.NotFound(w, r)
return
}
// downloading file
if !info.IsDir() {
if strings.HasSuffix(r.URL.Path, "/") {
http.Redirect(w, r, strings.TrimSuffix(r.URL.Path, "/"), http.StatusFound)
return
}
file, err := os.Open(fpath)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
defer func() {
err := file.Close()
if err != nil {
fmt.Fprintf(os.Stderr, "failed to close file %s: %v\n", fpath, err)
}
}()
mimeType := "application/octet-stream"
extPos := strings.LastIndex(info.Name(), ".")
ext := string(info.Name()[strings.LastIndex(info.Name(), "."):])
fmt.Println(ext)
if extPos != -1 {
if m := mime.TypeByExtension(ext); m != "" {
mimeType = m
} else if m, ok := mimeTypes[ext]; ok {
mimeType = m
}
}
w.Header().Set("Content-Type", mimeType)
w.WriteHeader(http.StatusOK)
_, err = file.WriteTo(w)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to send file %s: %v\n", fpath, err)
}
return
}
if !strings.HasSuffix(r.URL.Path, "/") {
http.Redirect(w, r, r.URL.Path + "/", http.StatusFound)
return
}
// serve index.html if present (case-sensitive)
indexPath := filepath.Join(fpath, "index.html")
if indexInfo, err := os.Stat(indexPath); err == nil && !indexInfo.IsDir() {
http.ServeFile(w, r, indexPath)
return
}
// load index.css if present
var customCSS template.CSS
cssPath := filepath.Join(fpath, "index.css")
if cssInfo, err := os.Stat(cssPath); err == nil && !cssInfo.IsDir() {
if src, err := os.ReadFile(cssPath); err == nil {
customCSS = template.CSS(src)
}
}
// embeded readme
var readmeHTML template.HTML
entries, err := os.ReadDir(fpath)
if err == nil {
for _, entry := range entries {
if strings.EqualFold(entry.Name(), "readme.md") {
src, err := os.ReadFile(filepath.Join(fpath, entry.Name()))
if err == nil {
mdFlags := html.CommonFlags | html.HrefTargetBlank
mdRenderer := html.NewRenderer(html.RendererOptions{Flags: mdFlags})
mdParser := parser.NewWithExtensions(parser.CommonExtensions | parser.AutoHeadingIDs)
md := mdParser.Parse(src)
readmeHTML = template.HTML(markdown.Render(md, mdRenderer))
}
break
}
}
}
data := Directory{
Root: isRoot,
Name: r.URL.Path,
Files: []*File{},
Readme: readmeHTML,
CSS: customCSS,
}
fsDir := os.DirFS(fpath)
directories, err := fs.ReadDir(fsDir, ".")
for _, dir := range directories {
name := dir.Name()
if slices.Contains(app.IgnoredFiles, name) { continue }
info, err := dir.Info()
if err != nil { continue }
var uri string
if isRoot {
uri = app.Root + name
} else {
uri = r.URL.Path + name
}
sizeStr := "&mdash;"
if !info.IsDir() {
size := info.Size()
sizeDenom := "B"
if size > 1000 {
size /= 1000
sizeDenom = "KB"
}
if size > 1000 {
size /= 1000
sizeDenom = "MB"
}
if size > 1000 {
size /= 1000
sizeDenom = "GB"
}
sizeStr = fmt.Sprintf("%d%s", size, sizeDenom)
}
dateStr := info.ModTime().Format("02-Jan-2006 15:04")
data.Files = append(data.Files, &File{
Name: name,
URI: uri,
IsDir: info.IsDir(),
Size: sizeStr,
ModifiedDate: dateStr,
})
}
if err != nil {
fmt.Fprintf(os.Stderr, "failed to open directory: %v\n", err)
}
w.Header().Set("Content-Type", "text/html")
w.Header().Set("Server", "indir")
w.WriteHeader(http.StatusOK)
dirTemplate.Execute(w, data)
}
}