commit d4538d8adbb61d8779d25b9a4d360a3ce6dfd7eb Author: Gabe Farrell Date: Mon Feb 24 18:17:26 2025 -0500 First diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f09ca2d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +## syntax=docker/dockerfile:1 +FROM golang:1.23 +WORKDIR /app +COPY go.mod go.sum ./ +RUN go mod download +COPY *.go ./ +RUN mkdir -p /images +RUN CGO_ENABLED=0 GOOS=linux go build -o /random-image-server +CMD ["/random-image-server"] \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..570dfd6 --- /dev/null +++ b/go.mod @@ -0,0 +1,7 @@ +module github.com/gabehf/random-image-server + +go 1.23.0 + +require github.com/fsnotify/fsnotify v1.8.0 + +require golang.org/x/sys v0.13.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b6b937f --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/main.go b/main.go new file mode 100644 index 0000000..344c34e --- /dev/null +++ b/main.go @@ -0,0 +1,137 @@ +package main + +import ( + "fmt" + "log" + "math/rand" + "net/http" + "os" + "path" + "path/filepath" + "slices" + "strings" + + "github.com/fsnotify/fsnotify" +) + +var images []string +var validExtensions = getValidExtensions() + +func main() { + dirPath := os.Getenv("IMAGE_DIR") + if dirPath == "" { + dirPath = "/images" + } + files, err := os.ReadDir(dirPath) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Image directory: %s\n", dirPath) + for _, file := range files { + if fileIsValid(path.Join(dirPath, file.Name())) { + images = appendIfNotExists(images, path.Join(dirPath, file.Name())) + } else { + log.Printf("File %s unreadable or not an image, ignoring.\n", file.Name()) + } + } + + watcher, err := fsnotify.NewWatcher() + if err != nil { + log.Fatal(err) + } + defer watcher.Close() + + go func() { + for { + select { + case event, ok := <-watcher.Events: + if !ok { + return + } + file := filenameFromEvent(event) + if event.Has(fsnotify.Create) { + // happens on rename and new file add + if fileIsValid(file) { + images = appendIfNotExists(images, file) + } + } else if event.Has(fsnotify.Remove) || event.Has(fsnotify.Rename) { + // happens on rm or old file rename + images = remove(images, file) + } else if event.Has(fsnotify.Chmod) { + // happens on chmod and rm if file descriptors are open + // file is deleted or unreadable, remove + if !fileIsValid(file) { + images = remove(images, file) + } else { + // file was unreadable but now is, add + images = appendIfNotExists(images, file) + } + } + case err, ok := <-watcher.Errors: + if !ok { + return + } + log.Println("error:", err) + } + } + }() + + err = watcher.Add(dirPath) + if err != nil { + log.Fatal(err) + } + + http.HandleFunc("/", serveImage) + log.Println("Listening on :8080") + log.Fatal(http.ListenAndServe(":8080", nil)) +} + +func serveImage(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, images[rand.Intn(len(images))]) +} + +// not fast at all but whatever +func remove(s []string, item string) []string { + for i, fn := range s { + if fn == item { + log.Printf("REMOVE %s\n", item) + return append(s[:i], s[i+1:]...) + } + } + return s +} + +func appendIfNotExists(slice []string, element string) []string { + for _, ele := range slice { + if ele == element { + return slice + } + } + log.Printf("ADD %s\n", element) + return append(slice, element) +} + +func filenameFromEvent(event fsnotify.Event) string { + return strings.Split(event.String(), "\"")[1] +} + +func fileIsValid(file string) bool { + return slices.Contains(validExtensions, filepath.Ext(file)) && isReadable(file) +} + +func getValidExtensions() []string { + env := os.Getenv("ALLOWED_EXTENSIONS") + if env == "" { + return []string{".png", ".jpg", ".jpeg", ".webp"} + } else { + return strings.Split(env, ",") + } +} + +// maybe expensive but works +// tried file info mode perms, syscall.Access already, didnt work +func isReadable(file string) bool { + _, err := os.Open(file) + return err == nil +}