2025-06-08 02:09:00 +01:00
package main
import (
"fmt"
"io/fs"
"mime"
"net/http"
"os"
"path"
"slices"
"strconv"
"strings"
"time"
)
func main ( ) {
if len ( os . Args ) < 2 { printHelp ( ) }
host := "127.0.0.1"
port := 8080
root := "/"
filesDir := "."
i := 1
for {
if i >= len ( os . Args ) { break }
switch os . Args [ i ] {
case "-h" :
fallthrough
case "--help" :
printHelp ( )
case "--host" :
if i + 1 >= len ( os . Args ) {
fmt . Fprintf ( os . Stderr , "fatal: --host argument cannot be empty\n" )
os . Exit ( 1 )
}
i ++
host = os . Args [ i ]
case "--port" :
if i + 1 >= len ( os . Args ) {
fmt . Fprintf ( os . Stderr , "fatal: --port argument cannot be empty\n" )
os . Exit ( 1 )
}
i ++
var err error
port , err = strconv . Atoi ( os . Args [ i ] )
if err != nil {
fmt . Fprintf ( os . Stderr , "fatal: failed to parse port %s: %v\n" , os . Args [ i ] , err )
os . Exit ( 1 )
}
case "--root" :
if i + 1 >= len ( os . Args ) {
fmt . Fprintf ( os . Stderr , "fatal: --root argument cannot be empty\n" )
os . Exit ( 1 )
}
i ++
root = os . Args [ i ]
if ! strings . HasPrefix ( root , "/" ) { root = "/" + root }
if ! strings . HasSuffix ( root , "/" ) { root += "/" }
default :
filesDir = os . Args [ i ]
}
i ++
}
ignoredFiles := [ ] string {
"." ,
".DS_Store" ,
}
stylesheet := `
html {
background : # 101010 ;
color : # f0f0f0 ;
font - family : ' Monaspace Argon ' , monospace ;
}
body {
width : min ( calc ( 100 % - 1 em ) , 1000 px ) ;
margin : 0 auto ;
}
table {
width : 100 % ;
border - collapse : collapse ;
}
tr : hover {
background - color : # 80808040 ;
}
th {
text - align : left ;
}
td {
padding : .2 em 0 ;
}
a {
color : # b7fd49 ;
}
a : hover {
color : white ;
}
footer {
padding : 1 em 0 ;
}
`
fmt . Printf ( "Now hosting \"%s\" at http://%s:%d" , filesDir , host , port )
if root != "/" { fmt . Printf ( "%s" , root ) }
fmt . Println ( "." )
http . ListenAndServe ( fmt . Sprintf ( "%s:%d" , host , port ) , HTTPLog ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
if ! strings . HasPrefix ( r . URL . Path , root ) {
http . NotFound ( w , r )
return
}
isRoot := r . URL . Path == root
responseText := fmt . Sprintf ( ` < ! DOCTYPE html >
< html lang = "en-IE" >
< head >
< title > Listing % s < / title >
< style > % s < / style >
< meta charset = "UTF-8" >
< / head >
< body >
< main >
` , r . URL . Path , stylesheet )
responseText += fmt . Sprintf ( "<h1>Files in %s</h1>\n<hr>" , r . URL . Path )
responseText += "<table>\n"
responseText += "<tr>\n<th>Name</th>\n<th>Size</th>\n<th>Modified</th>\n</tr>\n"
if ! isRoot {
responseText += "<tr><td><a href=\"./..\">../</a></td><td>—</td><td>—</td></tr>\n"
}
filepath := path . Join ( filesDir , strings . TrimPrefix ( r . URL . Path , root ) )
info , err := os . Stat ( filepath )
if err != nil {
http . NotFound ( w , r )
return
}
if ! info . IsDir ( ) && strings . HasSuffix ( r . URL . Path , "/" ) {
http . Redirect ( w , r , strings . TrimSuffix ( r . URL . Path , "/" ) , http . StatusFound )
} else if info . IsDir ( ) && ! strings . HasSuffix ( r . URL . Path , "/" ) {
http . Redirect ( w , r , r . URL . Path + "/" , http . StatusFound )
}
if ! info . IsDir ( ) {
file , err := os . Open ( filepath )
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" , filepath , err )
}
} ( )
mimeType := "application/octet-stream"
extPos := strings . LastIndex ( info . Name ( ) , "." )
if extPos != - 1 {
mimeType = mime . TypeByExtension ( info . Name ( ) [ : extPos ] )
}
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" , filepath , err )
}
return
}
fsDir := os . DirFS ( filepath )
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
}
responseText += fmt . Sprintf (
"<tr><td><a href=\"%s\">%s</a></td>" , uri , name )
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"
}
responseText += fmt . Sprintf ( "<td><code title=\"%d bytes\">%d%s</code></td>" , info . Size ( ) , size , sizeDenom )
} else {
responseText += "<td>—</td>"
}
dateStr := info . ModTime ( ) . Format ( "02-Jan-2006 15:04" )
responseText += fmt . Sprintf ( "<td>%s</td>" , dateStr )
responseText += "</tr>\n"
}
if err != nil {
fmt . Fprintf ( os . Stderr , "failed to open directory: %v\n" , err )
}
responseText += "</table>\n"
responseText += "<hr>\n</main>\n"
2025-06-08 19:18:14 +01:00
responseText += "<footer><em>made with <span aria-label=\"love\">♥</span> by ari, 2025 <a href=\"https://git.arimelody.me/ari/indir\" target=\"_blank\">[source]</a></em></footer>\n"
2025-06-08 02:09:00 +01:00
responseText += "</body>\n</html>\n"
w . Header ( ) . Set ( "Content-Type" , "text/html" )
w . WriteHeader ( 200 )
w . Write ( [ ] byte ( responseText ) )
} ) ) )
}
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 ( ) {
2025-06-08 02:27:00 +01:00
fmt . Printf (
` % s [ -- host address ] [ -- port port ] [ -- root http_root ] directory
-- help shows this help message
-- host address hosts on the specified address
-- port port hosts on the specified port
-- root http_root hosts on the specified subdirectory , i . e . ` +" ` / files / ` \ n " ,
os . Args [ 0 ] ,
)
2025-06-08 02:09:00 +01:00
os . Exit ( 0 )
}