Compare commits

...

4 commits

Author SHA1 Message Date
d377c9419d
docs: update README
Some checks are pending
Go / build (push) Waiting to run
2025-02-10 05:32:28 +03:00
6393fd1e4d
meta: integrate editorconfig 2025-02-10 05:29:09 +03:00
7d76753050
better caching 2025-02-10 05:28:54 +03:00
05843107d6
ci: update workflows 2025-02-10 05:25:53 +03:00
4 changed files with 82 additions and 35 deletions

3
.editorconfig Normal file
View file

@ -0,0 +1,3 @@
[{Makefile,go.mod,go.sum,*.go,.gitmodules}]
indent_style = tab
indent_size = 4

View file

@ -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"

View file

@ -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)

67
main.go
View file

@ -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)