From 1e0f8e0aaeaf1bd2ee39c02e922238b641bcf88b Mon Sep 17 00:00:00 2001
From: tjp <tjp@ctrl-c.club>
Date: Mon, 13 Nov 2023 07:25:39 -0700
Subject: refactor contribs to work with a Protocol interface

---
 internal/filetypes.go      | 57 ++++++++++++++++++++++++++++++++++++++++++++++
 internal/types/protocol.go | 19 ++++++++++++++++
 2 files changed, 76 insertions(+)
 create mode 100644 internal/filetypes.go
 create mode 100644 internal/types/protocol.go

(limited to 'internal')

diff --git a/internal/filetypes.go b/internal/filetypes.go
new file mode 100644
index 0000000..6824ffc
--- /dev/null
+++ b/internal/filetypes.go
@@ -0,0 +1,57 @@
+package internal
+
+import (
+	"mime"
+	"os"
+	"strings"
+	"unicode/utf8"
+)
+
+func MediaType(fpath string) string {
+	if strings.HasSuffix(fpath, ".gmi") {
+		// This may not be present in the listings searched by mime.TypeByExtension,
+		// so provide a dedicated fast path for it here.
+		return "text/gemini"
+	}
+
+	slashIdx := strings.LastIndex(fpath, "/")
+	dotIdx := strings.LastIndex(fpath[slashIdx+1:], ".")
+	if dotIdx == -1 {
+		return "application/octet-stream"
+	}
+	ext := fpath[slashIdx+1+dotIdx:]
+
+	mtype := mime.TypeByExtension(ext)
+	if mtype == "" {
+		if ContentsAreText(fpath) {
+			return "text/plain"
+		}
+		return "application/octet-stream"
+	}
+	return mtype
+}
+
+func ContentsAreText(fpath string) bool {
+	f, err := os.Open(fpath)
+	if err != nil {
+		return false
+	}
+	defer func() { _ = f.Close() }()
+
+	var buf [1024]byte
+	n, err := f.Read(buf[:])
+	if err != nil {
+		return false
+	}
+
+	for i, c := range string(buf[:n]) {
+		if i+utf8.UTFMax > n {
+			// incomplete last char
+			break
+		}
+		if c == 0xFFFD || c < ' ' && c != '\n' && c != '\t' && c != '\f' {
+			return false
+		}
+	}
+	return true
+}
diff --git a/internal/types/protocol.go b/internal/types/protocol.go
new file mode 100644
index 0000000..7166d8f
--- /dev/null
+++ b/internal/types/protocol.go
@@ -0,0 +1,19 @@
+package types
+
+import (
+	"io"
+	"net/url"
+)
+
+type ServerProtocol interface {
+	TemporaryRedirect(*url.URL) *Response
+	PermanentRedirect(*url.URL) *Response
+
+	TemporaryServerError(error) *Response
+	PermanentServerError(error) *Response
+	CGIFailure(error) *Response
+
+	Success(filename string, body io.Reader) *Response
+
+	ParseResponse(io.Reader) (*Response, error)
+}
-- 
cgit v1.2.3