commit 5964169a4702eb2c6bfe2d79ddea34545be51b18 Author: ari melody Date: Sun Jun 8 02:09:00 2025 +0100 first commit! 🎉 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7e016e8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +tmp +indir diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c405b03 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.arimelody.me/ari/indir + +go 1.24.3 diff --git a/main.go b/main.go new file mode 100755 index 0000000..1bb0637 --- /dev/null +++ b/main.go @@ -0,0 +1,301 @@ +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% - 1em), 1000px); + margin: 0 auto; + } + + table { + width: 100%; + border-collapse: collapse; + } + + tr:hover { + background-color: #80808040; + } + + th { + text-align: left; + } + + td { + padding: .2em 0; + } + + a { + color: #b7fd49; + } + a:hover { + color: white; + } + + footer { + padding: 1em 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(` + + + Listing %s + + + + +
+ `, r.URL.Path, stylesheet) + + responseText += fmt.Sprintf("

Files in %s

\n
", r.URL.Path) + + responseText += "\n" + responseText += "\n\n\n\n\n" + if !isRoot { + responseText += "\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 + fmt.Printf("uri: %s\n", uri) + } else { + uri = r.URL.Path + name + } + + responseText += fmt.Sprintf( + "", 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("", info.Size(), size, sizeDenom) + } else { + responseText += "" + } + + dateStr := info.ModTime().Format("02-Jan-2006 15:04") + responseText += fmt.Sprintf("", dateStr) + + responseText += "\n" + } + if err != nil { + fmt.Fprintf(os.Stderr, "failed to open directory: %v\n", err) + } + responseText += "
NameSizeModified
../
%s%d%s%s
\n" + + responseText += "
\n
\n" + responseText += "\n" + responseText += "\n\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() { + fmt.Printf("%s [--host address] [--port port] [--root http_root] directory\n", os.Args[0]) + os.Exit(0) +}