Commit 1537da9b16

1537da9b16dfda1ecb2730b5d1de3b9005397618

parent: 4db018fb7f

Unsigned

lost+skunk <skunky@ebloid.ru> · 2024-08-13 12:59 UTC

темплейты в бинарнике и улучшенная система кеша

Layout: unified · split

README.md +4 −4
@@ -1,15 +1,15 @@
1<img src="https://git.macaw.me/skunky/SkunkyArt/raw/branch/master/misc/logo.png" alt="SkunkyArt" title="SkunkyArt Logo" width="20%" loading="lazy"/> 1<img src="/skunky/SkunkyArt/raw/branch/master/misc/logo.png" alt="SkunkyArt" title="SkunkyArt Logo" width="20%" loading="lazy"/>
2 2
3[![Matrix room](https://img.shields.io/badge/matrix-000000?style=for-the-badge&logo=Matrix&logoColor=white)](https://go.kde.org/matrix/#/#skunkyart:ebloid.ru) 3[![Matrix room](https://img.shields.io/badge/matrix-000000?style=for-the-badge&logo=Matrix&logoColor=white)](https://go.kde.org/matrix/#/#skunkyart:ebloid.ru)
4 4
5Instances: [`INSTANCES.md`](https://git.macaw.me/skunky/SkunkyArt/src/branch/master/INSTANCES.md) 5Instances: [`INSTANCES.md`](/skunky/SkunkyArt/src/branch/master/INSTANCES.md)
6 6
7# EN 🇺🇸 7# EN 🇺🇸
8## Description 8## Description
9SkunkyArt 🦨 — alternative frontend for DevianArt, which works without JS. 9SkunkyArt 🦨 — alternative frontend for DevianArt, which works without JS.
10## Setup 10## Setup
11The sample config is in the `config.example.json` file. For custom config, use `--config` option. 11The sample config is in the `config.example.json` file. For custom config, use `--config` option.
12See the [`SETUP.md`](https://git.macaw.me/skunky/SkunkyArt/src/branch/master/SETUP.md) file for more info about directives. 12See the [`SETUP.md`](/skunky/SkunkyArt/src/branch/master/SETUP.md) file for more info about directives.
13## Adding instance to the list 13## Adding instance to the list
14To do this, you must either make a PR by adding your instance to the `instances.json` and `INSTANCES.md` files (you can use `--add-instance` cli-argument to automatically add the instance to these files), or create an Issue, or report it to the room in Matrix. Keep in mind that your instance must comply with the following rules: 14To do this, you must either make a PR by adding your instance to the `instances.json` and `INSTANCES.md` files (you can use `--add-instance` cli-argument to automatically add the instance to these files), or create an Issue, or report it to the room in Matrix. Keep in mind that your instance must comply with the following rules:
151. the Instance must not use Cloudflare. 151. the Instance must not use Cloudflare.
@@ -23,7 +23,7 @@ To do this, you must either make a PR by adding your instance to the `instances.
23SkunkyArt 🦨 — альтернативный фронтенд к DeviantArt, который полностью работает без JS (JavaScript). 23SkunkyArt 🦨 — альтернативный фронтенд к DeviantArt, который полностью работает без JS (JavaScript).
24## Настройка 24## Настройка
25Пример конфига находится в файле `config.example.json`. Чтобы указать свой конфиг, используйте cli-аргумент `--config`. 25Пример конфига находится в файле `config.example.json`. Чтобы указать свой конфиг, используйте cli-аргумент `--config`.
26См. [`SETUP-RU.md`](https://git.macaw.me/skunky/SkunkyArt/src/branch/master/SETUP-RU.md) для информации о настройки фронтенда. 26См. [`SETUP-RU.md`](/skunky/SkunkyArt/src/branch/master/SETUP-RU.md) для информации о настройки фронтенда.
27## Добавление инстанса в список 27## Добавление инстанса в список
28Чтобы это сделать, вы должны либо сделать PR, добавив в файлы `instances.json` и `INSTANCES.md` свой инстанс (можете воспользоваться cli-аргументом `--add-instance`, который автоматически это сделает), либо создать Issue, или сообщить о нём в комнате в Matrix. Учтите, что ваш инстанс должен соблюсти следущие правила: 28Чтобы это сделать, вы должны либо сделать PR, добавив в файлы `instances.json` и `INSTANCES.md` свой инстанс (можете воспользоваться cli-аргументом `--add-instance`, который автоматически это сделает), либо создать Issue, или сообщить о нём в комнате в Matrix. Учтите, что ваш инстанс должен соблюсти следущие правила:
291. Инстанс не должен использовать Cloudflare итп. 291. Инстанс не должен использовать Cloudflare итп.
SETUP-RU.md +1 −1
@@ -1,4 +1,4 @@
1[English version 🇬🇧](https://git.macaw.me/skunky/SkunkyArt/src/branch/master/SETUP.md) 1[English version 🇬🇧](/skunky/SkunkyArt/src/branch/master/SETUP.md)
2 2
3# Единицы измерения 3# Единицы измерения
4Размер файла в кеше измеряется в мегабайтах.<br> 4Размер файла в кеше измеряется в мегабайтах.<br>
SETUP.md +1 −1
@@ -1,4 +1,4 @@
1[Версия на русском языке 🇷🇺](https://git.macaw.me/skunky/SkunkyArt/src/branch/master/SETUP-RU.md) 1[Версия на русском языке 🇷🇺](/skunky/SkunkyArt/src/branch/master/SETUP-RU.md)
2 2
3# Units 3# Units
4Maximum file size in megabytes, requires numeric value.<br> 4Maximum file size in megabytes, requires numeric value.<br>
TODO.md +10 −8
@@ -1,19 +1,21 @@
1# v1.3.x 1# v1.3.x
2* Почистить говнокод 2* Почистить говнокод
3* ~~Сделать порт под FreeBSD~~ ✔️
3* **Доделать парсинг описания** 4* **Доделать парсинг описания**
4* Избавиться от хардкода под Linux 5* ~~Реализовать стрипы в ежедневных артах~~ ✔️
5* ~~Реализовать стрипы в ежедневных артах~~ 6* ~~Исправить баг с навигацией по страницам~~ ✔️
6* Сделать нормальное отображение ошибок 7* Сделать нормальное отображение ошибок
7* ~~Исправить баг с навигацией по страницам~~ 8* ~~Сделать единицы в конфиге более понятными~~ ✔️
8* ~~Сделать единицы в конфиге более понятными~~ 9* Добавить просмотр понравившихся артов пользователю
9* Добавить возможность включить темплейты в бинарник 10* Добавить возможность включить темплейты в бинарник [P]
10* ~~Реализовать миниатюры и оптимизировать CSS под маленькие экраны~~ 11* ~~Реализовать миниатюры и оптимизировать CSS под маленькие экраны~~ ✔️
11* Написать Makefile и скрипт для автоматического развёртывания инстанса 12* Написать Makefile и скрипт для автоматического развёртывания инстанса
12* **Реализовать отображение контента, отличного от картинок (видео, аудио, etc)** 13* **Реализовать отображение контента, отличного от картинок (видео, аудио, etc)**
13* Исправить баг с эмоджи, когда некоторые кастомные эмоции могут не отображаться 14* Исправить баг с эмоджи, когда некоторые кастомные эмоции могут не отображаться
14* Добавить флаг сборки, который позволит собрать бинарник со встроенными темплейтами 15* ~~Добавить аргумент &filename, который будет выдавать файл с нормально выглядещем именем~~ ✔️
15* Улучшить систему кеширования: добавить рейтинг для удаления и копирование изображений в ОЗУ 16* ~~Улучшить систему кеширования: добавить рейтинг для удаления и копирование изображений в ОЗУ~~
16# v1.4 17# v1.4
17* Реализовать API 18* Реализовать API
18* Реализовать темы 19* Реализовать темы
20* Перейти на арены в кеше
19* Реализовать многоязычный интерфейс\ No newline at end of file 21* Реализовать многоязычный интерфейс\ No newline at end of file
app/cache.go added +132
@@ -0,0 +1,132 @@
1// TODO: реализовать кеширование JSON и почистить код
2package app
3
4import (
5 "crypto/sha1"
6 "encoding/hex"
7 "io"
8 "os"
9 "strings"
10 "sync"
11 "syscall"
12 "time"
13)
14
15type file struct {
16 Score int
17 Content []byte
18}
19
20var tempFS = make(map[[20]byte]*file)
21var mx = &sync.RWMutex{}
22
23func (s skunkyart) DownloadAndSendMedia(subdomain, path string) {
24 var url strings.Builder
25 url.WriteString("https://images-wixmp-")
26 url.WriteString(subdomain)
27 url.WriteString(".wixmp.com/")
28 url.WriteString(path)
29 url.WriteString("?token=")
30 url.WriteString(s.Args.Get("token"))
31
32 var response []byte
33
34 switch {
35 case CFG.Cache.Enabled:
36 fileName := sha1.Sum([]byte(subdomain + path))
37 filePath := CFG.Cache.Path + "/" + hex.EncodeToString(fileName[:])
38
39 mx.Lock()
40 if tempFS[fileName] == nil {
41 tempFS[fileName] = &file{}
42 }
43 f := *tempFS[fileName]
44 mx.Unlock()
45
46 if f.Content != nil {
47 f.Score += 2
48 } else {
49 file, err := os.Open(filePath)
50 if err != nil {
51 if dwnld := Download(url.String()); dwnld.Status == 200 && dwnld.Headers["Content-Type"][0][:5] == "image" {
52 f.Content = dwnld.Body
53 try(os.WriteFile(filePath, f.Content, 0700))
54 } else {
55 s.ReturnHTTPError(dwnld.Status)
56 return
57 }
58 } else {
59 file, e := io.ReadAll(file)
60 try(e)
61 f.Content = file
62 }
63
64 go func() {
65 defer restore()
66 for {
67 time.Sleep(1 * time.Minute)
68
69 mx.Lock()
70 if tempFS[fileName].Score <= 0 {
71 delete(tempFS, fileName)
72 mx.Unlock()
73 return
74 }
75 tempFS[fileName].Score--
76 mx.Unlock()
77 }
78 }()
79 }
80
81 mx.Lock()
82 tempFS[fileName] = &f
83 mx.Unlock()
84 response = f.Content
85 case CFG.Proxy:
86 dwnld := Download(url.String())
87 if dwnld.Status != 200 {
88 s.ReturnHTTPError(dwnld.Status)
89 return
90 }
91 response = dwnld.Body
92 default:
93 s.Writer.WriteHeader(403)
94 response = []byte("Sorry, butt proxy on this instance are disabled.")
95 }
96
97 s.Writer.Write(response)
98}
99
100func InitCacheSystem() {
101 c := &CFG.Cache
102 os.Mkdir(c.Path, 0700)
103 for {
104 dir, e := os.Open(c.Path)
105 try(e)
106 stat, e := dir.Stat()
107 try(e)
108
109 dirnames, e := dir.Readdirnames(-1)
110 try(e)
111 for _, a := range dirnames {
112 a = c.Path + "/" + a
113 if c.Lifetime != "" {
114 now := time.Now().UnixMilli()
115
116 f, _ := os.Stat(a)
117 stat := f.Sys().(*syscall.Stat_t)
118 time := statTime(stat)
119
120 if time+lifetimeParsed <= now {
121 try(os.RemoveAll(a))
122 }
123 }
124 if c.MaxSize != 0 && stat.Size() > c.MaxSize {
125 try(os.RemoveAll(a))
126 }
127 }
128
129 dir.Close()
130 time.Sleep(time.Second * time.Duration(c.UpdateInterval))
131 }
132}
app/cli.go +29 −34
@@ -78,16 +78,16 @@ func addInstance() {
78 try(err) 78 try(err)
79 defer instancesJson.Close() 79 defer instancesJson.Close()
80 80
81 instances, err := os.OpenFile("INSTANCES.md", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 81 instancesFile, err := os.OpenFile("INSTANCES.md", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
82 try(err) 82 try(err)
83 defer instances.Close() 83 defer instancesFile.Close()
84 84
85 for { 85 for {
86 if Templates["instances.json"] == "" { 86 if string(instances) == "" {
87 print("\rDownloading instance list...") 87 print("\rDownloading instance list...")
88 } else { 88 } else {
89 println("\r\033[2KDownloaded!") 89 println("\r\033[2KDownloaded!")
90 try(json.Unmarshal([]byte(Templates["instances.json"]), &settingsVar)) 90 try(json.Unmarshal(instances, &settingsVar))
91 91
92 settingsVar.Instances = append(settingsVar.Instances, settings{ 92 settingsVar.Instances = append(settingsVar.Instances, settings{
93 Title: prompt("Title", true), 93 Title: prompt("Title", true),
@@ -113,51 +113,46 @@ func addInstance() {
113 settingsVar := &settingsVar.Instances[len(settingsVar.Instances)-1] 113 settingsVar := &settingsVar.Instances[len(settingsVar.Instances)-1]
114 var mdstr bytes.Buffer 114 var mdstr bytes.Buffer
115 115
116 mdstr.WriteString("\n|") 116 mdbuilder := func(yes bool, link string, title string) {
117 if settingsVar.Urls.Clearnet != "" { 117 switch {
118 mdstr.WriteString("[") 118 case yes && (title != "" && link != ""):
119 mdstr.WriteString(settingsVar.Title) 119 mdstr.WriteString("[")
120 mdstr.WriteString("](") 120 mdstr.WriteString(title)
121 mdstr.WriteString(settingsVar.Urls.Clearnet) 121 mdstr.WriteString("](")
122 mdstr.WriteString(")") 122 mdstr.WriteString(link)
123 } else { 123 mdstr.WriteString(")")
124 mdstr.WriteString(settingsVar.Title) 124 case yes && link != "":
125 mdstr.WriteString("[Yes](")
126 mdstr.WriteString(link)
127 mdstr.WriteString(")")
128 case yes:
129 mdstr.WriteString("Yes")
130 default:
131 mdstr.WriteString("No")
132 }
133 mdstr.WriteString("|")
125 } 134 }
126 mdstr.WriteString("|") 135
136 mdstr.WriteString("\n|")
137 mdbuilder(settingsVar.Urls.Clearnet != "", settingsVar.Urls.Clearnet, settingsVar.Title)
127 138
128 urls := []string{settingsVar.Urls.Ygg, settingsVar.Urls.I2P, settingsVar.Urls.Tor} 139 urls := []string{settingsVar.Urls.Ygg, settingsVar.Urls.I2P, settingsVar.Urls.Tor}
129 for i, l := 0, len(urls); i < l; i++ { 140 for i, l := 0, len(urls); i < l; i++ {
130 url := urls[i] 141 url := urls[i]
131 if url != "" { 142 mdbuilder(url != "", url, "")
132 mdstr.WriteString("[Yes](")
133 mdstr.WriteString(url)
134 mdstr.WriteString(")|")
135 } else {
136 mdstr.WriteString("No|")
137 }
138 } 143 }
139 144
140 settings := []bool{settingsVar.Settings.Nsfw, settingsVar.Settings.Proxy} 145 settings := []bool{settingsVar.Settings.Nsfw, settingsVar.Settings.Proxy}
141 for i, l := 0, len(settings); i < l; i++ { 146 for i, l := 0, len(settings); i < l; i++ {
142 if settings[i] { 147 mdbuilder(settings[i], "", "")
143 mdstr.WriteString("Yes|")
144 } else {
145 mdstr.WriteString("No|")
146 }
147 } 148 }
148 149
149 if settingsVar.ModifiedSrc != "" { 150 mdbuilder(settingsVar.ModifiedSrc != "", settingsVar.ModifiedSrc, "")
150 mdstr.WriteString("[Yes](")
151 mdstr.WriteString(settingsVar.ModifiedSrc)
152 mdstr.WriteString(")|")
153 } else {
154 mdstr.WriteString("No|")
155 }
156 151
157 mdstr.WriteString(settingsVar.Country) 152 mdstr.WriteString(settingsVar.Country)
158 mdstr.WriteString("|") 153 mdstr.WriteString("|")
159 154
160 instances.Write(mdstr.Bytes()) 155 instancesFile.Write(mdstr.Bytes())
161 break 156 break
162 } 157 }
163 time.Sleep(500 * time.Millisecond) 158 time.Sleep(500 * time.Millisecond)
app/config.go +9 −7
@@ -4,6 +4,7 @@ import (
4 "encoding/json" 4 "encoding/json"
5 "os" 5 "os"
6 "regexp" 6 "regexp"
7 "skunkyart/static"
7 "strconv" 8 "strconv"
8 "time" 9 "time"
9 10
@@ -24,9 +25,9 @@ type config struct {
24 URI string `json:"uri"` 25 URI string `json:"uri"`
25 Cache cache_config 26 Cache cache_config
26 Proxy, Nsfw bool 27 Proxy, Nsfw bool
27 UserAgent string `json:"user-agent"` 28 UserAgent string `json:"user-agent"`
28 DownloadProxy string `json:"download-proxy"` 29 DownloadProxy string `json:"download-proxy"`
29 Dirs []string `json:"dirs-to-memory"` 30 StaticPath string `json:"static-path"`
30} 31}
31 32
32var CFG = config{ 33var CFG = config{
@@ -38,10 +39,10 @@ var CFG = config{
38 Path: "cache", 39 Path: "cache",
39 UpdateInterval: 1, 40 UpdateInterval: 1,
40 }, 41 },
41 Dirs: []string{"html", "css", "misc"}, 42 StaticPath: "static",
42 UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36", 43 UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36",
43 Proxy: true, 44 Proxy: true,
44 Nsfw: true, 45 Nsfw: true,
45} 46}
46 47
47var lifetimeParsed int64 48var lifetimeParsed int64
@@ -56,6 +57,7 @@ func ExecuteConfig() {
56 exit("Incompatible settings detected: cannot use caching media content without proxy", 1) 57 exit("Incompatible settings detected: cannot use caching media content without proxy", 1)
57 } 58 }
58 59
60 static.StaticPath = CFG.StaticPath
59 if CFG.Cache.Enabled { 61 if CFG.Cache.Enabled {
60 if CFG.Cache.Lifetime != "" { 62 if CFG.Cache.Lifetime != "" {
61 var duration int64 63 var duration int64
app/parsers.go +5 −3
@@ -41,7 +41,9 @@ func (s skunkyart) ParseComments(c devianter.Comments) string {
41 cmmts.WriteString("</b></a> ") 41 cmmts.WriteString("</b></a> ")
42 42
43 if x.Parent > 0 { 43 if x.Parent > 0 {
44 cmmts.WriteString(` In reply to <a href="#`) 44 cmmts.WriteString(` In reply to <a href="`)
45 cmmts.WriteString(Path)
46 cmmts.WriteString("#")
45 cmmts.WriteString(strconv.Itoa(x.Parent)) 47 cmmts.WriteString(strconv.Itoa(x.Parent))
46 cmmts.WriteString(`">`) 48 cmmts.WriteString(`">`)
47 if replied[x.Parent] == "" { 49 if replied[x.Parent] == "" {
@@ -80,7 +82,7 @@ func (s skunkyart) DeviationList(devs []devianter.Deviation, allowAtom bool, con
80 82
81 for i, l := 0, len(devs); i < l; i++ { 83 for i, l := 0, len(devs); i < l; i++ {
82 data := &devs[i] 84 data := &devs[i]
83 if preview, fullview := ParseMedia(data.Media, 320), ParseMedia(data.Media); !(data.NSFW && !CFG.Nsfw) { 85 if preview, fullview := ParseMedia(data.Media, data.Title, 320), ParseMedia(data.Media, data.Title); !(data.NSFW && !CFG.Nsfw) {
84 if allowAtom && s.Atom { 86 if allowAtom && s.Atom {
85 id := strconv.Itoa(data.ID) 87 id := strconv.Itoa(data.ID)
86 listContent.WriteString(`<entry><author><name>`) 88 listContent.WriteString(`<entry><author><name>`)
@@ -284,7 +286,7 @@ func ParseDescription(dscr devianter.Text) string {
284 parsedDescription.WriteString(`<a href="`) 286 parsedDescription.WriteString(`<a href="`)
285 parsedDescription.WriteString(ConvertDeviantArtUrlToSkunkyArt(d.Url)) 287 parsedDescription.WriteString(ConvertDeviantArtUrlToSkunkyArt(d.Url))
286 parsedDescription.WriteString(`"><img width="50%" src="`) 288 parsedDescription.WriteString(`"><img width="50%" src="`)
287 parsedDescription.WriteString(ParseMedia(d.Media)) 289 parsedDescription.WriteString(ParseMedia(d.Media, d.Title))
288 parsedDescription.WriteString(`" title="`) 290 parsedDescription.WriteString(`" title="`)
289 parsedDescription.WriteString(d.Author.Username) 291 parsedDescription.WriteString(d.Author.Username)
290 parsedDescription.WriteString(" - ") 292 parsedDescription.WriteString(" - ")
app/router.go +27 −6
@@ -1,13 +1,15 @@
1package app 1package app
2 2
3import ( 3import (
4 "io"
4 "net/http" 5 "net/http"
5 u "net/url" 6 u "net/url"
7 "skunkyart/static"
6 "strconv" 8 "strconv"
7 "strings" 9 "strings"
8) 10)
9 11
10var Host string 12var Host, Path string
11 13
12func Router() { 14func Router() {
13 parsepath := func(path string) map[int]string { 15 parsepath := func(path string) map[int]string {
@@ -41,6 +43,15 @@ func Router() {
41 return 43 return
42 } 44 }
43 45
46 open := func(name string) []byte {
47 file, err := static.Templates.Open(name)
48 try(err)
49 fileReaded, err := io.ReadAll(file)
50 try(err)
51
52 return fileReaded
53 }
54
44 // функция, что управляет всем 55 // функция, что управляет всем
45 handle := func(w http.ResponseWriter, r *http.Request) { 56 handle := func(w http.ResponseWriter, r *http.Request) {
46 if h := r.Header["X-Forwarded-Proto"]; len(h) != 0 && h[0] == "https" { 57 if h := r.Header["X-Forwarded-Proto"]; len(h) != 0 && h[0] == "https" {
@@ -49,7 +60,8 @@ func Router() {
49 Host = "http://" + r.Host 60 Host = "http://" + r.Host
50 } 61 }
51 62
52 path := parsepath(r.URL.Path) 63 Path = r.URL.Path
64 path := parsepath(Path)
53 // структура с функциями 65 // структура с функциями
54 var skunky skunkyart 66 var skunky skunkyart
55 skunky.Writer = w 67 skunky.Writer = w
@@ -70,12 +82,14 @@ func Router() {
70 skunky.Atom = true 82 skunky.Atom = true
71 } 83 }
72 84
85 skunky.Endpoint = path[1]
86
73 // пути 87 // пути
74 switch path[1] { 88 switch skunky.Endpoint {
75 default: 89 default:
76 skunky.ReturnHTTPError(404) 90 skunky.ReturnHTTPError(404)
77 case "": 91 case "":
78 skunky.ExecuteTemplate("index.htm", &CFG.URI) 92 w.Write(open("html/index.htm"))
79 case "post": 93 case "post":
80 skunky.Deviation(path[2], path[3]) 94 skunky.Deviation(path[2], path[3])
81 case "search": 95 case "search":
@@ -88,6 +102,13 @@ func Router() {
88 case "media": 102 case "media":
89 switch path[2] { 103 switch path[2] {
90 case "file": 104 case "file":
105 if a := arg("filename"); a != "" {
106 var filename strings.Builder
107 filename.WriteString(`filename="`)
108 filename.WriteString(a)
109 filename.WriteString(`"`)
110 w.Header().Add("Content-Disposition", filename.String())
111 }
91 skunky.DownloadAndSendMedia(path[3], next(path, 4)) 112 skunky.DownloadAndSendMedia(path[3], next(path, 4))
92 case "emojitar": 113 case "emojitar":
93 skunky.Emojitar(path[3]) 114 skunky.Emojitar(path[3])
@@ -96,9 +117,9 @@ func Router() {
96 skunky.About() 117 skunky.About()
97 case "stylesheet": 118 case "stylesheet":
98 w.Header().Add("content-type", "text/css") 119 w.Header().Add("content-type", "text/css")
99 wr(w, Templates["skunky.css"]) 120 w.Write(open("css/skunky.css"))
100 case "favicon.ico": 121 case "favicon.ico":
101 wr(w, Templates["logo.png"]) 122 w.Write(open("images/logo.png"))
102 } 123 }
103 } 124 }
104 125
app/stat-freebsd.go added +13
@@ -0,0 +1,13 @@
1//go:build freebsd
2// +build freebsd
3
4package app
5
6import (
7 "syscall"
8 "time"
9)
10
11func statTime(stat *syscall.Stat_t) int64 {
12 return time.Unix(stat.Ctimespec.Unix()).UnixMilli()
13}
app/stat.go added +13
@@ -0,0 +1,13 @@
1//go:build !freebsd
2// +build !freebsd
3
4package app
5
6import (
7 "syscall"
8 "time"
9)
10
11func statTime(stat *syscall.Stat_t) int64 {
12 return time.Unix(stat.Ctim.Unix()).UnixMilli()
13}
app/util.go +32 −104
@@ -1,14 +1,13 @@
1package app 1package app
2 2
3import ( 3import (
4 "encoding/base64"
5 "io" 4 "io"
6 "net/http" 5 "net/http"
7 u "net/url" 6 "net/url"
8 "os" 7 "os"
8 "skunkyart/static"
9 "strconv" 9 "strconv"
10 "strings" 10 "strings"
11 "syscall"
12 "text/template" 11 "text/template"
13 "time" 12 "time"
14 13
@@ -32,26 +31,34 @@ func tryWithExitStatus(err error, code int) {
32 } 31 }
33} 32}
34 33
34func restore() {
35 if r := recover(); r != nil {
36 recover()
37 }
38}
39
40var instances []byte
41
35func RefreshInstances() { 42func RefreshInstances() {
36 for { 43 for {
37 func() { 44 func() {
38 defer func() { 45 defer restore()
39 if r := recover(); r != nil { 46 instances = Download("https://git.macaw.me/skunky/SkunkyArt/raw/branch/master/instances.json").Body
40 recover()
41 }
42 }()
43 Templates["instances.json"] = string(Download("https://git.macaw.me/skunky/SkunkyArt/raw/branch/master/instances.json").Body)
44 }() 47 }()
45 time.Sleep(1 * time.Hour) 48 time.Sleep(1 * time.Hour)
46 } 49 }
47} 50}
48 51
49// some crap for frontend 52// some crap for frontend
50func (s skunkyart) ExecuteTemplate(file string, data any) { 53func (s skunkyart) ExecuteTemplate(file, dir string, data any) {
51 var buf strings.Builder 54 var buf strings.Builder
52 tmp := template.New(file) 55 tmp := template.New(file)
53 tmp, e := tmp.Parse(Templates[file]) 56 tmp, err := tmp.ParseFS(static.Templates, dir+"/*")
54 try(e) 57 if err != nil {
58 s.Writer.WriteHeader(500)
59 wr(s.Writer, err.Error())
60 return
61 }
55 try(tmp.Execute(&buf, &data)) 62 try(tmp.Execute(&buf, &data))
56 wr(s.Writer, buf.String()) 63 wr(s.Writer, buf.String())
57} 64}
@@ -91,15 +98,15 @@ type Downloaded struct {
91 Body []byte 98 Body []byte
92} 99}
93 100
94func Download(url string) (d Downloaded) { 101func Download(urlString string) (d Downloaded) {
95 cli := &http.Client{} 102 cli := &http.Client{}
96 if CFG.DownloadProxy != "" { 103 if CFG.DownloadProxy != "" {
97 u, e := u.Parse(CFG.DownloadProxy) 104 u, e := url.Parse(CFG.DownloadProxy)
98 try(e) 105 try(e)
99 cli.Transport = &http.Transport{Proxy: http.ProxyURL(u)} 106 cli.Transport = &http.Transport{Proxy: http.ProxyURL(u)}
100 } 107 }
101 108
102 req, e := http.NewRequest("GET", url, nil) 109 req, e := http.NewRequest("GET", urlString, nil)
103 try(e) 110 try(e)
104 req.Header.Set("User-Agent", CFG.UserAgent) 111 req.Header.Set("User-Agent", CFG.UserAgent)
105 112
@@ -115,97 +122,16 @@ func Download(url string) (d Downloaded) {
115 return 122 return
116} 123}
117 124
118// caching
119func (s skunkyart) DownloadAndSendMedia(subdomain, path string) {
120 var url strings.Builder
121 url.WriteString("https://images-wixmp-")
122 url.WriteString(subdomain)
123 url.WriteString(".wixmp.com/")
124 url.WriteString(path)
125 url.WriteString("?token=")
126 url.WriteString(s.Args.Get("token"))
127
128 if CFG.Cache.Enabled {
129 fname := CFG.Cache.Path + "/" + base64.StdEncoding.EncodeToString([]byte(subdomain+path))
130 file, e := os.Open(fname)
131
132 if e != nil {
133 dwnld := Download(url.String())
134 if dwnld.Status == 200 && dwnld.Headers["Content-Type"][0][:5] == "image" {
135 try(os.WriteFile(fname, dwnld.Body, 0700))
136 s.Writer.Write(dwnld.Body)
137 }
138 } else {
139 file, e := io.ReadAll(file)
140 try(e)
141 s.Writer.Write(file)
142 }
143 } else if CFG.Proxy {
144 dwnld := Download(url.String())
145 s.Writer.Write(dwnld.Body)
146 } else {
147 s.Writer.WriteHeader(403)
148 s.Writer.Write([]byte("Sorry, butt proxy on this instance are disabled."))
149 }
150}
151
152func InitCacheSystem() {
153 c := &CFG.Cache
154 os.Mkdir(CFG.Cache.Path, 0700)
155 for {
156 dir, e := os.Open(c.Path)
157 try(e)
158 stat, e := dir.Stat()
159 try(e)
160
161 dirnames, e := dir.Readdirnames(-1)
162 try(e)
163 for _, a := range dirnames {
164 a = c.Path + "/" + a
165 if c.Lifetime != "" {
166 now := time.Now().UnixMilli()
167
168 f, _ := os.Stat(a)
169 stat := f.Sys().(*syscall.Stat_t)
170 time := time.Unix(stat.Ctim.Unix()).UnixMilli()
171
172 if time+lifetimeParsed <= now {
173 try(os.RemoveAll(a))
174 }
175 }
176 if c.MaxSize != 0 && stat.Size() > c.MaxSize {
177 try(os.RemoveAll(a))
178 }
179 }
180
181 dir.Close()
182 time.Sleep(time.Second * time.Duration(CFG.Cache.UpdateInterval))
183 }
184}
185
186func CopyTemplatesToMemory() {
187 for _, dirname := range CFG.Dirs {
188 dir, e := os.ReadDir(dirname)
189 tryWithExitStatus(e, 1)
190
191 for _, x := range dir {
192 file, e := os.ReadFile(dirname + "/" + x.Name())
193 tryWithExitStatus(e, 1)
194 Templates[x.Name()] = string(file)
195 }
196 }
197}
198
199/* PARSING HELPERS */ 125/* PARSING HELPERS */
200func ParseMedia(media devianter.Media, thumb ...int) string { 126func ParseMedia(media devianter.Media, filename string, thumb ...int) string {
201 url := devianter.UrlFromMedia(media, thumb...) 127 mediaUrl := devianter.UrlFromMedia(media, thumb...)
202 if len(url) != 0 && CFG.Proxy { 128 if len(mediaUrl) != 0 && CFG.Proxy {
203 url = url[21:] 129 mediaUrl = mediaUrl[21:]
204 dot := strings.Index(url, ".") 130 dot := strings.Index(mediaUrl, ".")
205 131
206 return UrlBuilder("media", "file", url[:dot], url[dot+11:]) 132 return UrlBuilder("media", "file", mediaUrl[:dot], mediaUrl[dot+11:], "&filename=", url.QueryEscape(filename))
207 } 133 }
208 return url 134 return mediaUrl
209} 135}
210 136
211func ConvertDeviantArtUrlToSkunkyArt(url string) (output string) { 137func ConvertDeviantArtUrlToSkunkyArt(url string) (output string) {
@@ -255,7 +181,9 @@ func (s skunkyart) NavBase(c DeviationList) string {
255 list.WriteString("<br>") 181 list.WriteString("<br>")
256 prevrev := func(msg string, page int, onpage bool) { 182 prevrev := func(msg string, page int, onpage bool) {
257 if !onpage { 183 if !onpage {
258 list.WriteString(`<a href="?p=`) 184 list.WriteString(`<a href="`)
185 list.WriteString(Path)
186 list.WriteString(`?p=`)
259 list.WriteString(strconv.Itoa(page)) 187 list.WriteString(strconv.Itoa(page))
260 if s.Type != 0 { 188 if s.Type != 0 {
261 list.WriteString("&type=") 189 list.WriteString("&type=")
app/wrapper.go +23 −17
@@ -15,17 +15,17 @@ import (
15) 15)
16 16
17var wr = io.WriteString 17var wr = io.WriteString
18var Templates = make(map[string]string)
19 18
20type skunkyart struct { 19type skunkyart struct {
21 Writer http.ResponseWriter 20 Writer http.ResponseWriter
22 21
23 Args url.Values 22 Args url.Values
24 BasePath string 23 Page int
25 Type rune 24 Type rune
26 Query, QueryRaw string 25 Atom bool
27 Page int 26
28 Atom bool 27 BasePath, Endpoint string
28 Query, QueryRaw string
29 29
30 Templates struct { 30 Templates struct {
31 About struct { 31 About struct {
@@ -136,7 +136,7 @@ func (s skunkyart) GRUser() {
136 case "cover_deviation": 136 case "cover_deviation":
137 group.About.BGMeta = x.ModuleData.CoverDeviation.Deviation 137 group.About.BGMeta = x.ModuleData.CoverDeviation.Deviation
138 group.About.BGMeta.Url = ConvertDeviantArtUrlToSkunkyArt(group.About.BGMeta.Url) 138 group.About.BGMeta.Url = ConvertDeviantArtUrlToSkunkyArt(group.About.BGMeta.Url)
139 group.About.BG = ParseMedia(group.About.BGMeta.Media) 139 group.About.BG = ParseMedia(group.About.BGMeta.Media, group.About.BGMeta.Title)
140 case "group_admins": 140 case "group_admins":
141 var htm strings.Builder 141 var htm strings.Builder
142 for _, z := range x.ModuleData.GroupAdmins.Results { 142 for _, z := range x.ModuleData.GroupAdmins.Results {
@@ -171,7 +171,7 @@ func (s skunkyart) GRUser() {
171 folders.WriteString(`<a href="`) 171 folders.WriteString(`<a href="`)
172 folders.WriteString(ConvertDeviantArtUrlToSkunkyArt(x.Thumb.Url)) 172 folders.WriteString(ConvertDeviantArtUrlToSkunkyArt(x.Thumb.Url))
173 folders.WriteString(`"><img loading="lazy" src="`) 173 folders.WriteString(`"><img loading="lazy" src="`)
174 folders.WriteString(ParseMedia(x.Thumb.Media)) 174 folders.WriteString(ParseMedia(x.Thumb.Media, x.Thumb.Title))
175 folders.WriteString(`" title="`) 175 folders.WriteString(`" title="`)
176 folders.WriteString(x.Thumb.Title) 176 folders.WriteString(x.Thumb.Title)
177 folders.WriteString(`"></a>`) 177 folders.WriteString(`"></a>`)
@@ -180,7 +180,7 @@ func (s skunkyart) GRUser() {
180 } 180 }
181 folders.WriteString("<br>") 181 folders.WriteString("<br>")
182 182
183 folders.WriteString(`<a href="?folder=`) 183 folders.WriteString(`<a href="group_user?folder=`)
184 folders.WriteString(strconv.Itoa(x.FolderId)) 184 folders.WriteString(strconv.Itoa(x.FolderId))
185 folders.WriteString("&q=") 185 folders.WriteString("&q=")
186 folders.WriteString(s.Query) 186 folders.WriteString(s.Query)
@@ -209,7 +209,7 @@ func (s skunkyart) GRUser() {
209 } 209 }
210 210
211 if !s.Atom { 211 if !s.Atom {
212 s.ExecuteTemplate("gruser.htm", &s) 212 s.ExecuteTemplate("gruser.htm", "html", &s)
213 } 213 }
214} 214}
215 215
@@ -233,7 +233,7 @@ func (s skunkyart) Deviation(author, postname string) {
233 } 233 }
234 // время публикации 234 // время публикации
235 post.StringTime = post.Post.Deviation.PublishedTime.UTC().String() 235 post.StringTime = post.Post.Deviation.PublishedTime.UTC().String()
236 post.Post.IMG = ParseMedia(post.Post.Deviation.Media) 236 post.Post.IMG = ParseMedia(post.Post.Deviation.Media, post.Post.Deviation.Title)
237 for _, x := range post.Post.Deviation.Extended.RelatedContent { 237 for _, x := range post.Post.Deviation.Extended.RelatedContent {
238 if len(x.Deviations) != 0 { 238 if len(x.Deviations) != 0 {
239 post.Related += s.DeviationList(x.Deviations, false) 239 post.Related += s.DeviationList(x.Deviations, false)
@@ -258,7 +258,7 @@ func (s skunkyart) Deviation(author, postname string) {
258 258
259 post.Comments = s.ParseComments(devianter.GetComments(id, post.Post.Comments.Cursor, s.Page, 1)) 259 post.Comments = s.ParseComments(devianter.GetComments(id, post.Post.Comments.Cursor, s.Page, 1))
260 260
261 s.ExecuteTemplate("deviantion.htm", &s) 261 s.ExecuteTemplate("deviantion.htm", "html", &s)
262} 262}
263 263
264func (s skunkyart) DD() { 264func (s skunkyart) DD() {
@@ -281,11 +281,16 @@ func (s skunkyart) DD() {
281 More: dd.HasMore, 281 More: dd.HasMore,
282 }) 282 })
283 if !s.Atom { 283 if !s.Atom {
284 s.ExecuteTemplate("daily.htm", &s) 284 s.ExecuteTemplate("daily.htm", "html", &s)
285 } 285 }
286} 286}
287 287
288func (s skunkyart) Search() { 288func (s skunkyart) Search() {
289 if s.Query == "" {
290 s.ReturnHTTPError(400)
291 return
292 }
293
289 var err error 294 var err error
290 ss := &s.Templates.Search 295 ss := &s.Templates.Search
291 switch s.Type { 296 switch s.Type {
@@ -338,6 +343,7 @@ func (s skunkyart) Search() {
338 } 343 }
339 default: 344 default:
340 s.ReturnHTTPError(400) 345 s.ReturnHTTPError(400)
346 return
341 } 347 }
342 try(err) 348 try(err)
343 349
@@ -348,7 +354,7 @@ func (s skunkyart) Search() {
348 }) 354 })
349 } 355 }
350 356
351 s.ExecuteTemplate("search.htm", &s) 357 s.ExecuteTemplate("search.htm", "html", &s)
352} 358}
353 359
354func (s skunkyart) Emojitar(name string) { 360func (s skunkyart) Emojitar(name string) {
@@ -367,6 +373,6 @@ func (s skunkyart) Emojitar(name string) {
367func (s skunkyart) About() { 373func (s skunkyart) About() {
368 s.Templates.About.Nsfw = CFG.Nsfw 374 s.Templates.About.Nsfw = CFG.Nsfw
369 s.Templates.About.Proxy = CFG.Proxy 375 s.Templates.About.Proxy = CFG.Proxy
370 try(json.Unmarshal([]byte(Templates["instances.json"]), &s.Templates.About)) 376 try(json.Unmarshal(instances, &s.Templates.About))
371 s.ExecuteTemplate("about.htm", &s) 377 s.ExecuteTemplate("about.htm", "html", &s)
372} 378}
config.example.json +1 −5
@@ -8,11 +8,7 @@
8 "max-size": 1024, 8 "max-size": 1024,
9 "update-interval": 5 9 "update-interval": 5
10 }, 10 },
11 "dirs-to-memory": [ 11 "static-path": "static",
12 "html",
13 "css",
14 "misc"
15 ],
16 "download-proxy": "http://127.0.0.1:8080", 12 "download-proxy": "http://127.0.0.1:8080",
17 "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36", 13 "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36",
18 "proxy": true, 14 "proxy": true,
html/daily.htm deleted −28
@@ -1,28 +0,0 @@
1<!DOCTYPE html>
2<html>
3 <head>
4 <title>SkunkyArt | Daily Deviations</title>
5 <link rel="stylesheet" href="{{.BasePath}}stylesheet">
6 <link rel="icon" type="image/x-icon" href="{{.BasePath}}favicon.ico">
7 </head>
8 <main>
9 <header>
10 <h1><a href="{{.BasePath}}">HOME</a> | <a href="{{.BasePath}}dd">DD</a> | <a href="?atom=true">RSS</a></h1>
11 <form method="get" action="{{.BasePath}}search">
12 <input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false">
13 <select name="type">
14 <option value="all">All</option>
15 <option value="tag">Tag</option>
16 <option value="r">Groups</option>
17 </select>
18 <button type="submit">Search!</button>
19 </form>
20 </header>
21 {{if ne .Templates.DDStrips ""}}
22 <h2 id="strips"><a href="#strips">#</a> Strips</h2>
23 {{.Templates.DDStrips}}
24 {{end}}
25 <h2 id="content"><a href="#content">#</a> Content</h2>
26 {{.Templates.SomeList}}
27 </main>
28</html>\ No newline at end of file
html/search.htm deleted −30
@@ -1,30 +0,0 @@
1<!DOCTYPE html>
2<html>
3 <head>
4 <title>SkunkyArt | Search "{{.QueryRaw}}"</title>
5 <link rel="stylesheet" href="{{.BasePath}}stylesheet">
6 <link rel="icon" type="image/x-icon" href="{{.BasePath}}favicon.ico">
7 </head>
8 <main>
9 <header>
10 <h1><a href="{{.BasePath}}">HOME</a> | <a href="{{.BasePath}}dd">DD</a></h1>
11 <form method="get" action="search">
12 <input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false">
13 <select name="type">
14 <option value="all">All</option>
15 <option value="tag">Tag</option>
16 <option value="r">Groups</option>
17 </select>
18 <button type="submit">Search!</button>
19 </form>
20 </header>
21 {{if ne .Templates.Search.List ""}}
22 {{if ne .Templates.Search.Content.Total 0}}
23 <h1>Results by request '{{.QueryRaw}}': {{.Templates.Search.Content.Total}}</h1>
24 {{end}}
25 {{.Templates.Search.List}}
26 {{else}}
27 <p>No results :(</p>
28 {{end}}
29 </main>
30</html>\ No newline at end of file
main.go +2 −1
@@ -2,6 +2,7 @@ package main
2 2
3import ( 3import (
4 "skunkyart/app" 4 "skunkyart/app"
5 "skunkyart/static"
5 "time" 6 "time"
6 7
7 "git.macaw.me/skunky/devianter" 8 "git.macaw.me/skunky/devianter"
@@ -12,7 +13,7 @@ func main() {
12 13
13 app.ExecuteCommandLineArguments() 14 app.ExecuteCommandLineArguments()
14 app.ExecuteConfig() 15 app.ExecuteConfig()
15 app.CopyTemplatesToMemory() 16 static.CopyTemplatesToMemory()
16 17
17 go func() { 18 go func() {
18 for { 19 for {
css/skunky.css → static/css/skunky.css renamed +11 −2
@@ -22,12 +22,19 @@ header form {
22header { 22header {
23 display: flex; 23 display: flex;
24} 24}
25form {
26 font-size: 0;
27 border: solid #164e3e 1px;
28 max-width: fit-content;
29}
25form input, button, select { 30form input, button, select {
26 background-color: #134134; 31 background-color: #134134;
27 padding: 5px; 32 padding: 5px;
28 color: whitesmoke; 33 color: whitesmoke;
29 border: 0px; 34 border: 0;
30 border-radius: 1px; 35}
36input:focus {
37 outline: none;
31} 38}
32 39
33/* BLOCKS */ 40/* BLOCKS */
@@ -146,6 +153,8 @@ form input, button, select {
146 153
147 header form { 154 header form {
148 font-size: 60%; 155 font-size: 60%;
156 max-width: unset;
157 border: 0;
149 } 158 }
150 header, center { 159 header, center {
151 text-align: center; 160 text-align: center;
html/about.htm → static/html/about.htm renamed +2 −17
@@ -1,23 +1,8 @@
1<!DOCTYPE html> 1<!DOCTYPE html>
2<html> 2<html>
3 <head> 3 {{template "head" .}}
4 <title>SkunkyArt</title>
5 <link rel="stylesheet" href="{{.BasePath}}stylesheet">
6 <link rel="icon" type="image/x-icon" href="{{.BasePath}}favicon.ico">
7 </head>
8 <main> 4 <main>
9 <header> 5 {{template "header" .}}
10 <h1><a href="{{.BasePath}}">HOME</a> | <a href="{{.BasePath}}dd">DD</a></h1>
11 <form method="get" action="{{.BasePath}}search">
12 <input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false">
13 <select name="type">
14 <option value="all">All</option>
15 <option value="tag">Tag</option>
16 <option value="r">Groups</option>
17 </select>
18 <button type="submit">Search!</button>
19 </form>
20 </header>
21 <p> 6 <p>
22 SkunkyArt is an alternative frontend for deviantart.com, written in Go. 7 SkunkyArt is an alternative frontend for deviantart.com, written in Go.
23 </p> 8 </p>
static/html/daily.htm added +13
@@ -0,0 +1,13 @@
1<!DOCTYPE html>
2<html>
3 {{template "head" . }}
4 <main>
5 {{template "header" . }}
6 {{if ne .Templates.DDStrips ""}}
7 <h2 id="strips"><a href="#strips">#</a> Strips</h2>
8 {{.Templates.DDStrips}}
9 {{end}}
10 <h2 id="content"><a href="#content">#</a> Content</h2>
11 {{.Templates.SomeList}}
12 </main>
13</html>\ No newline at end of file
html/deviantion.htm → static/html/deviantion.htm renamed +2 −18
@@ -1,24 +1,8 @@
1<!DOCTYPE html> 1<!DOCTYPE html>
2<html> 2<html>
3 <head> 3 {{template "head" . }}
4 <title>SkunkyArt | {{.Templates.Deviation.Post.Deviation.Author.Username}} - {{.Templates.Deviation.Post.Deviation.Title}}</title>
5 <link rel="stylesheet" href="{{.BasePath}}stylesheet">
6 <meta name="referrer" content="no-referrer" />
7 <link rel="icon" type="image/x-icon" href="{{.BasePath}}favicon.ico">
8 </head>
9 <main> 4 <main>
10 <header> 5 {{template "header" . }}
11 <h1><a href="{{.BasePath}}">HOME</a> | <a href="{{.BasePath}}dd">DD</a></h1>
12 <form method="get" action="{{.BasePath}}search">
13 <input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false">
14 <select name="type">
15 <option value="all">All</option>
16 <option value="tag">Tag</option>
17 <option value="r">Groups</option>
18 </select>
19 <button type="submit">Search!</button>
20 </form>
21 </header>
22 <figure> 6 <figure>
23 <img src="{{.BasePath}}media/emojitar/{{.Templates.Deviation.Post.Deviation.Author.Username}}?type=a" width="30px"> 7 <img src="{{.BasePath}}media/emojitar/{{.Templates.Deviation.Post.Deviation.Author.Username}}?type=a" width="30px">
24 <span><strong><a href="{{.BasePath}}group_user?type=about&q={{.Templates.Deviation.Post.Deviation.Author.Username}}">{{.Templates.Deviation.Post.Deviation.Author.Username}}</a></strong> — {{if (.Templates.Deviation.Post.Deviation.DD)}} 8 <span><strong><a href="{{.BasePath}}group_user?type=about&q={{.Templates.Deviation.Post.Deviation.Author.Username}}">{{.Templates.Deviation.Post.Deviation.Author.Username}}</a></strong> — {{if (.Templates.Deviation.Post.Deviation.DD)}}
html/gruser.htm → static/html/gruser.htm renamed +3 −13
@@ -1,21 +1,11 @@
1<!DOCTYPE html> 1<!DOCTYPE html>
2<html> 2<html>
3 <head> 3 {{template "head" . }}
4 <title>SkunkyArt |
5 {{if eq .Type 'a'}}
6 {{.Templates.GroupUser.GR.Owner.Username}}
7 {{else}}
8 gallery of {{.Templates.GroupUser.GR.Owner.Username}}
9 {{end}}
10 </title>
11 <link rel="stylesheet" href="{{.BasePath}}stylesheet">
12 <link rel="icon" type="image/x-icon" href="{{.BasePath}}favicon.ico">
13 </head>
14 <main> 4 <main>
15 <header> 5 <header>
16 <h1><a href="{{.BasePath}}">HOME</a> | <a href="{{.BasePath}}dd">DD</a> 6 <h1><a href="{{.BasePath}}">HOME</a> | <a href="{{.BasePath}}dd">DD</a>
17 | <a href="?q={{.Templates.GroupUser.GR.Owner.Username}}&type={{if eq .Type 'a'}}gallery">Gallery{{else}}about">About{{end}}</a> 7 | <a href="group_user?q={{.Templates.GroupUser.GR.Owner.Username}}&type={{if eq .Type 'a'}}gallery">Gallery{{else}}about">About{{end}}</a>
18 | <a href="?q={{.Templates.GroupUser.GR.Owner.Username}}&type=gallery&atom=true">RSS</a></h1> 8 | <a href="group_user?q={{.Templates.GroupUser.GR.Owner.Username}}&type=gallery&atom=true">RSS</a></h1>
19 <form method="get" action="{{.BasePath}}search"> 9 <form method="get" action="{{.BasePath}}search">
20 <input type="gallery" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false"> 10 <input type="gallery" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false">
21 <input type="hidden" name="usr" value="{{.Templates.GroupUser.GR.Owner.Username}}"> 11 <input type="hidden" name="usr" value="{{.Templates.GroupUser.GR.Owner.Username}}">
static/html/head.htm added +24
@@ -0,0 +1,24 @@
1{{define "head"}}
2<head>
3 <title>SkunkyArt |
4 {{if eq .Endpoint "search"}}
5 "{{.QueryRaw}}"
6 {{else if eq .Endpoint "post"}}
7 {{.Templates.Deviation.Post.Deviation.Author.Username}} — {{.Templates.Deviation.Post.Deviation.Title}}
8 {{else if eq .Type 'a'}}
9 {{if .Templates.GroupUser.GR.Owner.Username}}
10 {{.Templates.GroupUser.GR.Owner.Username}}
11 {{else}}
12 gallery of {{.Templates.GroupUser.GR.Owner.Username}}
13 {{end}}
14 {{else}}
15 {{.Endpoint}}
16 {{end}}
17 </title>
18
19 <base href="{{.BasePath}}">
20 <meta name="referrer" content="no-referrer" />
21 <link rel="stylesheet" href="{{.BasePath}}stylesheet">
22 <link rel="icon" type="image/x-icon" href="{{.BasePath}}favicon.ico">
23</head>
24{{end}}\ No newline at end of file
static/html/header.htm added +14
@@ -0,0 +1,14 @@
1{{define "header"}}
2<header>
3 <h1><a href="">HOME</a> | <a href="dd">DD</a> {{if eq .Endpoint "dd"}}| <a href="{{.Endpoint}}?atom=true">RSS</a>{{end}}</h1>
4 <form method="get" action="search">
5 <input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false" value="{{.QueryRaw}}">
6 <select name="type">
7 <option value="all">All</option>
8 <option value="tag">Tag</option>
9 <option value="r">Groups</option>
10 </select>
11 <button type="submit">Search!</button>
12 </form>
13</header>
14{{end}}\ No newline at end of file
html/index.htm → static/html/index.htm renamed +4 −4
@@ -2,12 +2,12 @@
2<html> 2<html>
3 <head> 3 <head>
4 <title>SkunkyArt</title> 4 <title>SkunkyArt</title>
5 <link rel="stylesheet" href="{{.}}stylesheet"/> 5 <link rel="stylesheet" href="stylesheet"/>
6 <link rel="icon" type="image/x-icon" href="{{.}}favicon.ico"> 6 <link rel="icon" type="image/x-icon" href="favicon.ico">
7 </head> 7 </head>
8 <main> 8 <main>
9 <center> 9 <center>
10 <form method="get" action="{{.}}search"> 10 <form method="get" action="search">
11 <input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false"> 11 <input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false">
12 <select name="type"> 12 <select name="type">
13 <option value="all">All</option> 13 <option value="all">All</option>
@@ -16,7 +16,7 @@
16 </select> 16 </select>
17 <button type="submit">Search!</button> 17 <button type="submit">Search!</button>
18 </form> 18 </form>
19 <h1><a href="{{.}}dd">Daily Deviations</a> | <a href="{{.}}about">About</a> | <a href="https://git.macaw.me/skunky/SkunkyArt" target="_blank">Source Code</a></h1> 19 <h1><a href="dd">Daily Deviations</a> | <a href="about">About</a> | <a href="https://git.macaw.me/skunky/SkunkyArt" target="_blank">Source Code</a></h1>
20 </center> 20 </center>
21 </main> 21 </main>
22</html>\ No newline at end of file 22</html>\ No newline at end of file
static/html/search.htm added +16
@@ -0,0 +1,16 @@
1<!DOCTYPE html>
2<html>
3 {{template "head" . }}
4 <main>
5 {{template "header" . }}
6
7 {{if ne .Templates.Search.List ""}}
8 {{if ne .Templates.Search.Content.Total 0}}
9 <h1>Results by request '{{.QueryRaw}}': {{.Templates.Search.Content.Total}}</h1>
10 {{end}}
11 {{.Templates.Search.List}}
12 {{else}}
13 <p>No results :(</p>
14 {{end}}
15 </main>
16</html>\ No newline at end of file
misc/logo.png → static/images/logo.png renamed
static/templates-noembed.go added +148
@@ -0,0 +1,148 @@
1//go:build !embed
2// +build !embed
3
4package static
5
6import (
7 "bytes"
8 "io/fs"
9 "os"
10 "strings"
11 "time"
12)
13
14var Templates FS
15
16type file struct {
17 path string
18 name string
19 content []byte
20}
21
22var templateNames = []string{}
23var templates = make(map[string][]file)
24var StaticPath string
25
26func CopyTemplatesToMemory() {
27 baseDir, err := os.ReadDir(StaticPath)
28 try(err)
29
30 for _, c := range baseDir {
31 if c.IsDir() {
32 templateNames = append(templateNames, c.Name())
33
34 var filePath strings.Builder
35 filePath.WriteString(StaticPath)
36 filePath.WriteString("/")
37 filePath.WriteString(c.Name())
38
39 dir, err := os.ReadDir(filePath.String())
40 try(err)
41
42 filePath.WriteString("/")
43 for _, cd := range dir {
44 f, err := os.ReadFile(filePath.String() + cd.Name())
45 try(err)
46 templates[c.Name()] = append(templates[c.Name()], file{
47 content: f,
48 name: cd.Name(),
49 path: c.Name() + "/" + cd.Name(),
50 })
51 }
52 }
53 }
54}
55
56type FS struct{}
57
58func (FS) Open(name string) (fs.File, error) {
59 for i, l := 0, len(templateNames); i < l; i++ {
60 for _, x := range templates[templateNames[i]] {
61 if x.content != nil && name == x.path {
62 return &File{
63 name: x.path,
64 content: bytes.NewBuffer(x.content),
65 }, nil
66 }
67 }
68 }
69 return nil, &fs.PathError{}
70}
71
72func (FS) Glob(pattern string) ([]string, error) {
73 trimmed := strings.Split(pattern, "/")
74 var matches = []string{}
75 for x, s := range templates {
76 for i, l := 0, len(s); i < l && trimmed[0] == x; i++ {
77 s := s[i]
78 matches = append(matches, s.path)
79 }
80 }
81 if len(matches) != 0 {
82 return matches, nil
83 }
84 return nil, &fs.PathError{}
85}
86
87func try(err error) {
88 if err != nil {
89 println(err.Error())
90 os.Exit(1)
91 }
92}
93
94/* сделано на основе https://github.com/psanford/memfs; требуется для корректной работы templates.ParseFS */
95type fileInfo struct {
96 name string
97}
98
99func (fi fileInfo) Name() string {
100 return fi.name
101}
102
103func (fi fileInfo) Size() int64 {
104 return 4096
105}
106
107func (fileInfo) Mode() fs.FileMode {
108 return 0
109}
110
111func (fileInfo) ModTime() time.Time {
112 return time.Time{}
113}
114
115func (fileInfo) IsDir() bool {
116 return false
117}
118
119func (fileInfo) Sys() interface{} {
120 return nil
121}
122
123type File struct {
124 name string
125 content *bytes.Buffer
126 closed bool
127}
128
129func (f *File) Stat() (fs.FileInfo, error) {
130 return fileInfo{
131 name: f.name,
132 }, nil
133}
134
135func (f *File) Read(b []byte) (int, error) {
136 if f.closed {
137 return 0, fs.ErrClosed
138 }
139 return f.content.Read(b)
140}
141
142func (f *File) Close() error {
143 if f.closed {
144 return fs.ErrClosed
145 }
146 f.closed = true
147 return nil
148}
static/templates.go added +16
@@ -0,0 +1,16 @@
1//go:build embed
2// +build embed
3
4package static
5
6import "embed"
7
8//go:embed *
9var Templates embed.FS
10var Enabled bool = true
11
12var StaticPath string
13
14func CopyTemplatesToMemory() {
15 _ = StaticPath
16}