diff --git a/.env.example b/.env.example index 6b60415..f12db21 100644 --- a/.env.example +++ b/.env.example @@ -3,6 +3,11 @@ PORT=3000 FIBER_PREFORK=false IMGUR_CLIENT_ID=546c25a59c58ad7 +# Create IMAGE_CACHE_DIR before enabling image caching +IMAGE_CACHE=false +IMAGE_CACHE_DIR=/var/cache/rimgo +IMAGE_CACHE_CLEANUP_INTERVAL=24h + # Instance privacy # For more information, see https://codeberg.org/librarian/librarian/wiki/Instance-privacy # Required to be on the instance list. diff --git a/api/album.go b/api/album.go index 18903e2..eca7ab3 100644 --- a/api/album.go +++ b/api/album.go @@ -42,7 +42,7 @@ func FetchAlbum(albumID string) (Album, error) { return cacheData.(Album), nil } - data, err := utils.GetJSON("https://api.imgur.com/post/v1/albums/" + albumID + "?client_id=" + utils.Config["imgurId"].(string) + "&include=media%2Caccount") + data, err := utils.GetJSON("https://api.imgur.com/post/v1/albums/" + albumID + "?client_id=" + utils.Config.ImgurId + "&include=media%2Caccount") if err != nil { return Album{}, err } @@ -62,7 +62,7 @@ func FetchPosts(albumID string) (Album, error) { return cacheData.(Album), nil } - data, err := utils.GetJSON("https://api.imgur.com/post/v1/posts/" + albumID + "?client_id=" + utils.Config["imgurId"].(string) + "&include=media%2Caccount%2Ctags") + data, err := utils.GetJSON("https://api.imgur.com/post/v1/posts/" + albumID + "?client_id=" + utils.Config.ImgurId + "&include=media%2Caccount%2Ctags") if err != nil { return Album{}, err } @@ -82,7 +82,7 @@ func FetchMedia(mediaID string) (Album, error) { return cacheData.(Album), nil } - data, err := utils.GetJSON("https://api.imgur.com/post/v1/media/" + mediaID + "?client_id=" + utils.Config["imgurId"].(string) + "&include=media%2Caccount") + data, err := utils.GetJSON("https://api.imgur.com/post/v1/media/" + mediaID + "?client_id=" + utils.Config.ImgurId + "&include=media%2Caccount") if err != nil { return Album{}, err } diff --git a/api/comments.go b/api/comments.go index f6263c4..cf5c785 100644 --- a/api/comments.go +++ b/api/comments.go @@ -33,7 +33,7 @@ func FetchComments(galleryID string) ([]Comment, error) { return cacheData.([]Comment), nil } - data, err := utils.GetJSON("https://api.imgur.com/comment/v1/comments?client_id=" + utils.Config["imgurId"].(string) + "&filter[post]=eq:" + galleryID + "&include=account,adconfig&per_page=30&sort=best") + data, err := utils.GetJSON("https://api.imgur.com/comment/v1/comments?client_id=" + utils.Config.ImgurId + "&filter[post]=eq:" + galleryID + "&include=account,adconfig&per_page=30&sort=best") if err != nil { return []Comment{}, nil } diff --git a/api/tag.go b/api/tag.go index 0e80148..b9b8f6f 100644 --- a/api/tag.go +++ b/api/tag.go @@ -36,7 +36,7 @@ func FetchTag(tag string, sort string, page string) (Tag, error) { } q := req.URL.Query() - q.Add("client_id", utils.Config["imgurId"].(string)) + q.Add("client_id", utils.Config.ImgurId) q.Add("include", "cover") q.Add("page", page) diff --git a/api/user.go b/api/user.go index 895ea78..979e314 100644 --- a/api/user.go +++ b/api/user.go @@ -43,7 +43,7 @@ func FetchUser(username string) (User, error) { return cacheData.(User), nil } - res, err := http.Get("https://api.imgur.com/account/v1/accounts/" + username + "?client_id=" + utils.Config["imgurId"].(string)) + res, err := http.Get("https://api.imgur.com/account/v1/accounts/" + username + "?client_id=" + utils.Config.ImgurId) if err != nil { return User{}, err } @@ -77,7 +77,7 @@ func FetchSubmissions(username string, sort string, page string) ([]Submission, return cacheData.([]Submission), nil } - data, err := utils.GetJSON("https://api.imgur.com/3/account/" + username + "/submissions/" + page + "/" + sort + "?album_previews=1&client_id=" + utils.Config["imgurId"].(string)) + data, err := utils.GetJSON("https://api.imgur.com/3/account/" + username + "/submissions/" + page + "/" + sort + "?album_previews=1&client_id=" + utils.Config.ImgurId) if err != nil { return []Submission{}, err } diff --git a/main.go b/main.go index 8860b2f..f6bf3c6 100644 --- a/main.go +++ b/main.go @@ -3,7 +3,11 @@ package main import ( "flag" "fmt" + "log" "net/http" + "os" + "path/filepath" + "time" "codeberg.org/video-prize-ranch/rimgo/pages" "codeberg.org/video-prize-ranch/rimgo/static" @@ -24,10 +28,22 @@ func main() { } utils.LoadConfig() + if utils.Config.ImageCache { + go func() { + for range time.Tick(utils.Config.CleanupInterval) { + log.Println("Cache cleaned") + files, _ := filepath.Glob(filepath.Join(utils.Config.CacheDir, "*")) + for _, file := range files { + os.RemoveAll(file) + } + } + }() + } + engine := handlebars.NewFileSystem(http.FS(views.GetFiles()), ".hbs") app := fiber.New(fiber.Config{ Views: engine, - Prefork: utils.Config["fiberPrefork"].(bool), + Prefork: utils.Config.FiberPrefork, UnescapePath: true, StreamRequestBody: true, ErrorHandler: func(ctx *fiber.Ctx, err error) error { @@ -90,5 +106,5 @@ func main() { app.Get("/gallery/:postID", pages.HandlePost) app.Get("/gallery/:postID/embed", pages.HandleEmbed) - app.Listen(utils.Config["addr"].(string) + ":" + utils.Config["port"].(string)) + app.Listen(utils.Config.Addr + ":" + utils.Config.Port) } diff --git a/pages/media.go b/pages/media.go index c43b971..4a46cab 100644 --- a/pages/media.go +++ b/pages/media.go @@ -1,6 +1,9 @@ package pages import ( + "crypto/sha256" + "encoding/hex" + "io" "net/http" "os" "strings" @@ -37,6 +40,19 @@ func handleMedia(c *fiber.Ctx, url string) error { url = strings.ReplaceAll(url, ".jpeg", ".webp") } + optionsHash := "" + if utils.Config.ImageCache { + hasher := sha256.New() + hasher.Write([]byte(url)) + optionsHash = hex.EncodeToString(hasher.Sum(nil)) + + image, err := os.ReadFile(utils.Config.CacheDir + "/" + optionsHash) + if err == nil { + _, err := c.Write(image) + return err + } + } + req, err := http.NewRequest("GET", url, nil) if err != nil { return err @@ -63,5 +79,20 @@ func handleMedia(c *fiber.Ctx, url string) error { c.Set("Content-Range", res.Header.Get("Content-Range")) } - return c.SendStream(res.Body) + if strings.HasPrefix(res.Header.Get("Content-Type"), "image/") && utils.Config.ImageCache && res.StatusCode == 200 { + data, err := io.ReadAll(res.Body) + if err != nil { + return err + } + + err = os.WriteFile(utils.Config.CacheDir + "/" + optionsHash, data, 0644) + if err != nil { + return err + } + + _, err = c.Write(data) + return err + } else { + return c.SendStream(res.Body) + } } \ No newline at end of file diff --git a/utils/config.go b/utils/config.go index e2d7d59..98c14d1 100644 --- a/utils/config.go +++ b/utils/config.go @@ -1,8 +1,23 @@ package utils -import "os" +import ( + "log" + "os" + "time" +) -var Config map[string]interface{} +type config struct { + Port string + Addr string + ImgurId string + FiberPrefork bool + ImageCache bool + CleanupInterval time.Duration + CacheDir string + Privacy map[string]interface{} +} + +var Config config func LoadConfig() { port := "3000" @@ -13,11 +28,6 @@ func LoadConfig() { port = os.Getenv("RIMGU_PORT") } - fiberPrefork := false - if os.Getenv("FIBER_PREFORK") == "true" { - fiberPrefork = true - } - addr := "0.0.0.0" if os.Getenv("ADDRESS") != "" { addr = os.Getenv("ADDRESS") @@ -34,14 +44,29 @@ func LoadConfig() { imgurId = os.Getenv("RIMGU_IMGUR_CLIENT_ID") } - Config = map[string]interface{}{ - "port": port, - "addr": addr, - "imgurId": imgurId, - "fiberPrefork": fiberPrefork, - "privacy": map[string]interface{}{ - "set": os.Getenv("PRIVACY_NOT_COLLECTED") != "", - "policy": os.Getenv("PRIVACY_POLICY"), + imageCache := os.Getenv("IMAGE_CACHE") == "true" + + cleanupInterval, err := time.ParseDuration(os.Getenv("IMAGE_CACHE_CLEANUP_INTERVAL")) + if err != nil && imageCache { + log.Fatal("invalid configuration: invalid duration for IMAGE_CACHE_CLEANUP_INTERVAL") + } + + cacheDir := os.Getenv("IMAGE_CACHE_DIR") + if cacheDir == "" && imageCache { + log.Fatal("invalid configuration: no IMAGE_CACHE_DIR") + } + + Config = config{ + Port: port, + Addr: addr, + ImgurId: imgurId, + FiberPrefork: os.Getenv("FIBER_PREFORK") == "true", + ImageCache: imageCache, + CleanupInterval: cleanupInterval, + CacheDir: cacheDir, + Privacy: map[string]interface{}{ + "set": os.Getenv("PRIVACY_NOT_COLLECTED") != "", + "policy": os.Getenv("PRIVACY_POLICY"), "message": os.Getenv("PRIVACY_MESSAGE"), "country": os.Getenv("PRIVACY_COUNTRY"), "provider": os.Getenv("PRIVACY_PROVIDER"), diff --git a/views/privacy.hbs b/views/privacy.hbs index 3538ea0..cb45faf 100644 --- a/views/privacy.hbs +++ b/views/privacy.hbs @@ -14,18 +14,18 @@

Instance Privacy

- {{#if config.privacy.policy}} + {{#if config.Privacy.policy}}

The instance operator has indicated their instance's privacy practices below. For more information, see the - instance operator's privacy policy.

+ instance operator's privacy policy.

{{else}}

The instance operator has indicated their instance's privacy practices below.

{{/if}} - {{#if config.privacy.message}} -

{{{config.privacy.message}}}

+ {{#if config.Privacy.message}} +

{{{config.Privacy.message}}}

{{/if}} - {{#if config.privacy.not_collected}} + {{#if config.Privacy.not_collected}}
@@ -37,7 +37,7 @@
{{/if}} - {{#unless config.privacy.set}} + {{#unless config.Privacy.set}}
@@ -49,7 +49,7 @@
{{else}} - {{#unless config.privacy.not_collected}} + {{#unless config.Privacy.not_collected}}
@@ -58,25 +58,25 @@

Data Collected

The following data may be collected:

    - {{#if config.privacy.ip}} + {{#if config.Privacy.ip}}
  • password Internet address (IP Address)
  • {{/if}} - {{#if config.privacy.url}} + {{#if config.Privacy.url}}
  • link Page viewed (Request URL)
  • {{/if}} - {{#if config.privacy.device}} + {{#if config.Privacy.device}}
  • phonelink Device Type (User agent)
  • {{/if}} - {{#if config.privacy.diagnostics}} + {{#if config.Privacy.diagnostics}}
  • settings Diagnostics @@ -119,9 +119,9 @@

    Additional information

    • Version: {{version}}
    • -
    • Country: {{config.privacy.country}}
    • -
    • Provider: {{config.privacy.provider}}
    • - {{#if config.privacy.cloudflare}} +
    • Country: {{config.Privacy.country}}
    • +
    • Provider: {{config.Privacy.provider}}
    • + {{#if config.Privacy.cloudflare}}
    • Using Cloudflare?: Yes
    • {{else}}
    • Using Cloudflare?: No