diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1eecd60 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,3 @@ +[{Makefile,go.mod,go.sum,*.go,.gitmodules}] +indent_style = tab +indent_size = 4 diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 21273ee..7a655a4 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -21,9 +21,7 @@ jobs: run: go build -v ./.. - name: Upload a Build Artifact - uses: actions/upload-artifact@v3.1.3 + uses: actions/upload-artifact@v4 with: - # Artifact name name: "catApi" - # A file, directory or wildcard pattern that describes what to upload path: "catApi" diff --git a/README.md b/README.md index 2a2866b..3e42cd8 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,40 @@ # 🐾 catApi -> **catApi** is a minimal, self-hostable API endpoint for serving pictures of, you guessed it, cats! -> but it could be used to serve anything, really +catApi is a minimal, self-hostable API (and frontend) for serving pictures of... +you guessed, it cats! Who doesn't like cats? -## Usage +## API Documentation -There are two ways to "use" **catApi** - you can either serve it, blessing the world with pictures of your cat -or be served. Below is the API documentation for visiting an existing instance of catApi. +There are several API endpoints that you may query. -### API Documentation - -**catApi** exposes several endpoints. - -#### ID +### `/api/id` `/api/id` will return the image with the associated ID. -For example **`http://localhost:3000/api/id?id=3`** will return the image with the ID of "3". +For example **`http://localhost:3000/api/id?id=3`** will return the image with +the ID of "3". -#### List +### `/api/list` -`/api/list` will return eturn a JSON object containing data about the images within the /images directory +`/api/list` will return return a JSON object containing data about the images +within the /images directory -For example, **`http://localhost:3000/api/random`** will return a JSON object that might be as follows +For example, querying **`http://localhost:3000/api/random`** will return a JSON +object that might be as follows -> `[{"id":"0","url":"/api/id?id=0"},{"id":"1","url":"/api/id?id=1"},{"id":"2","url":"/api/id?id=2"}]` +```json +[ + { "filename": "0.jpg", "id": "0", "url": "/api/id?id=0" }, + { "filename": "1.jpg", "id": "1", "url": "/api/id?id=1" }, + { "filename": "10.jpg", "id": "2", "url": "/api/id?id=2" }, + { "filename": "11.jpg", "id": "3", "url": "/api/id?id=3" } +] +``` -#### Random +### `/api/random` `/api/random` will return a random image from the list of available images -### Self-hosting - -TODO - ## License -> **catApi** is licensed under the [MIT](https://github.com/NotAShelf/catApi/blob/v2/LICENSE) license. +**catApi** is licensed under the [MIT License](./LICENSE) diff --git a/main.go b/main.go index 0496b63..592de8f 100644 --- a/main.go +++ b/main.go @@ -2,8 +2,8 @@ package main import ( "encoding/json" - "flag" "html/template" + "image" "log" "math/rand" "net/http" @@ -20,6 +20,13 @@ var images []string var logger = logrus.New() var port string +// Cache for image list, it should expire every 10 minutes +// but until it does, images should load faster in the frontend. +var cachedImages struct { + images []string + expiry time.Time +} + // Okay I admit, this is bad. Just a workaround for now, until I figure out a clean // way of displaying all images in a grid. var tmpl = template.Must(template.New("index").Parse(` @@ -78,13 +85,12 @@ func main() { viper.SetConfigName("config") // name of config file (without extension) viper.SetConfigType("yaml") // REQUIRED if the config file does not have the extension in the name viper.AddConfigPath(".") // path to look for the config file in + if err := viper.ReadInConfig(); err != nil { log.Fatalf("Error reading configuration file: %v", err) } port = viper.GetString("server.port") - - flag.Parse() images = getImages() // Add request logging middleware @@ -104,6 +110,14 @@ func main() { log.Fatal(http.ListenAndServe(":"+port, nil)) } +func getCachedImages() []string { + if time.Now().After(cachedImages.expiry) { + cachedImages.images = getImages() + cachedImages.expiry = time.Now().Add(10 * time.Minute) + } + return cachedImages.images +} + func getImages() []string { files, err := os.ReadDir("images/") if err != nil { @@ -126,11 +140,12 @@ func homeHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html") tmpl.Execute(w, struct { Images []string - }{Images: images}) + }{Images: getCachedImages()}) } func idHandler(w http.ResponseWriter, r *http.Request) { id := r.URL.Query().Get("id") + if id == "" { http.Error(w, "Missing id", http.StatusBadRequest) return @@ -159,11 +174,13 @@ func isValidImagePath(path string) bool { } func listHandler(w http.ResponseWriter, r *http.Request) { - imageList := []map[string]string{} - for i := range images { - imageInfo := map[string]string{ - "id": strconv.Itoa(i), - "url": "/api/id?id=" + strconv.Itoa(i), + imageList := []map[string]interface{}{} + for i := range getCachedImages() { + imageInfo := map[string]interface{}{ + "id": strconv.Itoa(i), + "url": "/api/id?id=" + strconv.Itoa(i), + "filename": images[i], + "size": getImageSize("images/" + images[i]), } imageList = append(imageList, imageInfo) } @@ -178,6 +195,35 @@ func listHandler(w http.ResponseWriter, r *http.Request) { w.Write(jsonData) } +func getImageSize(path string) map[string]interface{} { + file, err := os.Open(path) + if err != nil { + logger.WithError(err).Error("Error opening file for size") + return nil + } + defer file.Close() + + // Decode image to get dimensions (JPEG/PNG only) + img, _, err := image.Decode(file) + if err != nil { + logger.WithError(err).Error("Error decoding image") + return nil + } + + // Get file info for size + fileInfo, err := file.Stat() + if err != nil { + logger.WithError(err).Error("Error getting file info") + return nil + } + + return map[string]interface{}{ + "width": img.Bounds().Dx(), + "height": img.Bounds().Dy(), + "size": fileInfo.Size(), + } +} + func logRequest(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() @@ -189,8 +235,7 @@ func logRequest(next http.Handler) http.Handler { } func randomHandler(w http.ResponseWriter, r *http.Request) { - source := rand.NewSource(time.Now().UnixNano()) - rand.New(source) + rand.New(rand.NewSource(time.Now().UnixNano())) i := rand.Intn(len(images)) http.Redirect(w, r, "/api/id?id="+strconv.Itoa(i), http.StatusSeeOther)