diff options
Diffstat (limited to 'app')
| -rw-r--r-- | app/cache.go | 132 | ||||
| -rw-r--r-- | app/cli.go | 63 | ||||
| -rw-r--r-- | app/config.go | 16 | ||||
| -rw-r--r-- | app/parsers.go | 8 | ||||
| -rw-r--r-- | app/router.go | 33 | ||||
| -rw-r--r-- | app/stat-freebsd.go | 13 | ||||
| -rw-r--r-- | app/stat.go | 13 | ||||
| -rw-r--r-- | app/util.go | 136 | ||||
| -rw-r--r-- | app/wrapper.go | 40 |
9 files changed, 283 insertions, 171 deletions
diff --git a/app/cache.go b/app/cache.go new file mode 100644 index 0000000..dedba83 --- /dev/null +++ b/app/cache.go @@ -0,0 +1,132 @@ +// TODO: реализовать кеширование JSON и почистить код +package app + +import ( + "crypto/sha1" + "encoding/hex" + "io" + "os" + "strings" + "sync" + "syscall" + "time" +) + +type file struct { + Score int + Content []byte +} + +var tempFS = make(map[[20]byte]*file) +var mx = &sync.RWMutex{} + +func (s skunkyart) DownloadAndSendMedia(subdomain, path string) { + var url strings.Builder + url.WriteString("https://images-wixmp-") + url.WriteString(subdomain) + url.WriteString(".wixmp.com/") + url.WriteString(path) + url.WriteString("?token=") + url.WriteString(s.Args.Get("token")) + + var response []byte + + switch { + case CFG.Cache.Enabled: + fileName := sha1.Sum([]byte(subdomain + path)) + filePath := CFG.Cache.Path + "/" + hex.EncodeToString(fileName[:]) + + mx.Lock() + if tempFS[fileName] == nil { + tempFS[fileName] = &file{} + } + f := *tempFS[fileName] + mx.Unlock() + + if f.Content != nil { + f.Score += 2 + } else { + file, err := os.Open(filePath) + if err != nil { + if dwnld := Download(url.String()); dwnld.Status == 200 && dwnld.Headers["Content-Type"][0][:5] == "image" { + f.Content = dwnld.Body + try(os.WriteFile(filePath, f.Content, 0700)) + } else { + s.ReturnHTTPError(dwnld.Status) + return + } + } else { + file, e := io.ReadAll(file) + try(e) + f.Content = file + } + + go func() { + defer restore() + for { + time.Sleep(1 * time.Minute) + + mx.Lock() + if tempFS[fileName].Score <= 0 { + delete(tempFS, fileName) + mx.Unlock() + return + } + tempFS[fileName].Score-- + mx.Unlock() + } + }() + } + + mx.Lock() + tempFS[fileName] = &f + mx.Unlock() + response = f.Content + case CFG.Proxy: + dwnld := Download(url.String()) + if dwnld.Status != 200 { + s.ReturnHTTPError(dwnld.Status) + return + } + response = dwnld.Body + default: + s.Writer.WriteHeader(403) + response = []byte("Sorry, butt proxy on this instance are disabled.") + } + + s.Writer.Write(response) +} + +func InitCacheSystem() { + c := &CFG.Cache + os.Mkdir(c.Path, 0700) + for { + dir, e := os.Open(c.Path) + try(e) + stat, e := dir.Stat() + try(e) + + dirnames, e := dir.Readdirnames(-1) + try(e) + for _, a := range dirnames { + a = c.Path + "/" + a + if c.Lifetime != "" { + now := time.Now().UnixMilli() + + f, _ := os.Stat(a) + stat := f.Sys().(*syscall.Stat_t) + time := statTime(stat) + + if time+lifetimeParsed <= now { + try(os.RemoveAll(a)) + } + } + if c.MaxSize != 0 && stat.Size() > c.MaxSize { + try(os.RemoveAll(a)) + } + } + + dir.Close() + time.Sleep(time.Second * time.Duration(c.UpdateInterval)) + } +} @@ -78,16 +78,16 @@ func addInstance() { try(err) defer instancesJson.Close() - instances, err := os.OpenFile("INSTANCES.md", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + instancesFile, err := os.OpenFile("INSTANCES.md", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) try(err) - defer instances.Close() + defer instancesFile.Close() for { - if Templates["instances.json"] == "" { + if string(instances) == "" { print("\rDownloading instance list...") } else { println("\r\033[2KDownloaded!") - try(json.Unmarshal([]byte(Templates["instances.json"]), &settingsVar)) + try(json.Unmarshal(instances, &settingsVar)) settingsVar.Instances = append(settingsVar.Instances, settings{ Title: prompt("Title", true), @@ -113,51 +113,46 @@ func addInstance() { settingsVar := &settingsVar.Instances[len(settingsVar.Instances)-1] var mdstr bytes.Buffer - mdstr.WriteString("\n|") - if settingsVar.Urls.Clearnet != "" { - mdstr.WriteString("[") - mdstr.WriteString(settingsVar.Title) - mdstr.WriteString("](") - mdstr.WriteString(settingsVar.Urls.Clearnet) - mdstr.WriteString(")") - } else { - mdstr.WriteString(settingsVar.Title) + mdbuilder := func(yes bool, link string, title string) { + switch { + case yes && (title != "" && link != ""): + mdstr.WriteString("[") + mdstr.WriteString(title) + mdstr.WriteString("](") + mdstr.WriteString(link) + mdstr.WriteString(")") + case yes && link != "": + mdstr.WriteString("[Yes](") + mdstr.WriteString(link) + mdstr.WriteString(")") + case yes: + mdstr.WriteString("Yes") + default: + mdstr.WriteString("No") + } + mdstr.WriteString("|") } - mdstr.WriteString("|") + + mdstr.WriteString("\n|") + mdbuilder(settingsVar.Urls.Clearnet != "", settingsVar.Urls.Clearnet, settingsVar.Title) urls := []string{settingsVar.Urls.Ygg, settingsVar.Urls.I2P, settingsVar.Urls.Tor} for i, l := 0, len(urls); i < l; i++ { url := urls[i] - if url != "" { - mdstr.WriteString("[Yes](") - mdstr.WriteString(url) - mdstr.WriteString(")|") - } else { - mdstr.WriteString("No|") - } + mdbuilder(url != "", url, "") } settings := []bool{settingsVar.Settings.Nsfw, settingsVar.Settings.Proxy} for i, l := 0, len(settings); i < l; i++ { - if settings[i] { - mdstr.WriteString("Yes|") - } else { - mdstr.WriteString("No|") - } + mdbuilder(settings[i], "", "") } - if settingsVar.ModifiedSrc != "" { - mdstr.WriteString("[Yes](") - mdstr.WriteString(settingsVar.ModifiedSrc) - mdstr.WriteString(")|") - } else { - mdstr.WriteString("No|") - } + mdbuilder(settingsVar.ModifiedSrc != "", settingsVar.ModifiedSrc, "") mdstr.WriteString(settingsVar.Country) mdstr.WriteString("|") - instances.Write(mdstr.Bytes()) + instancesFile.Write(mdstr.Bytes()) break } time.Sleep(500 * time.Millisecond) diff --git a/app/config.go b/app/config.go index a6ac756..7d159e7 100644 --- a/app/config.go +++ b/app/config.go @@ -4,6 +4,7 @@ import ( "encoding/json" "os" "regexp" + "skunkyart/static" "strconv" "time" @@ -24,9 +25,9 @@ type config struct { URI string `json:"uri"` Cache cache_config Proxy, Nsfw bool - UserAgent string `json:"user-agent"` - DownloadProxy string `json:"download-proxy"` - Dirs []string `json:"dirs-to-memory"` + UserAgent string `json:"user-agent"` + DownloadProxy string `json:"download-proxy"` + StaticPath string `json:"static-path"` } var CFG = config{ @@ -38,10 +39,10 @@ var CFG = config{ Path: "cache", UpdateInterval: 1, }, - Dirs: []string{"html", "css", "misc"}, - UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36", - Proxy: true, - Nsfw: true, + StaticPath: "static", + UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36", + Proxy: true, + Nsfw: true, } var lifetimeParsed int64 @@ -56,6 +57,7 @@ func ExecuteConfig() { exit("Incompatible settings detected: cannot use caching media content without proxy", 1) } + static.StaticPath = CFG.StaticPath if CFG.Cache.Enabled { if CFG.Cache.Lifetime != "" { var duration int64 diff --git a/app/parsers.go b/app/parsers.go index b39e172..f93c18d 100644 --- a/app/parsers.go +++ b/app/parsers.go @@ -41,7 +41,9 @@ func (s skunkyart) ParseComments(c devianter.Comments) string { cmmts.WriteString("</b></a> ") if x.Parent > 0 { - cmmts.WriteString(` In reply to <a href="#`) + cmmts.WriteString(` In reply to <a href="`) + cmmts.WriteString(Path) + cmmts.WriteString("#") cmmts.WriteString(strconv.Itoa(x.Parent)) cmmts.WriteString(`">`) if replied[x.Parent] == "" { @@ -80,7 +82,7 @@ func (s skunkyart) DeviationList(devs []devianter.Deviation, allowAtom bool, con for i, l := 0, len(devs); i < l; i++ { data := &devs[i] - if preview, fullview := ParseMedia(data.Media, 320), ParseMedia(data.Media); !(data.NSFW && !CFG.Nsfw) { + if preview, fullview := ParseMedia(data.Media, data.Title, 320), ParseMedia(data.Media, data.Title); !(data.NSFW && !CFG.Nsfw) { if allowAtom && s.Atom { id := strconv.Itoa(data.ID) listContent.WriteString(`<entry><author><name>`) @@ -284,7 +286,7 @@ func ParseDescription(dscr devianter.Text) string { parsedDescription.WriteString(`<a href="`) parsedDescription.WriteString(ConvertDeviantArtUrlToSkunkyArt(d.Url)) parsedDescription.WriteString(`"><img width="50%" src="`) - parsedDescription.WriteString(ParseMedia(d.Media)) + parsedDescription.WriteString(ParseMedia(d.Media, d.Title)) parsedDescription.WriteString(`" title="`) parsedDescription.WriteString(d.Author.Username) parsedDescription.WriteString(" - ") diff --git a/app/router.go b/app/router.go index f87aa50..35550f8 100644 --- a/app/router.go +++ b/app/router.go @@ -1,13 +1,15 @@ package app import ( + "io" "net/http" u "net/url" + "skunkyart/static" "strconv" "strings" ) -var Host string +var Host, Path string func Router() { parsepath := func(path string) map[int]string { @@ -41,6 +43,15 @@ func Router() { return } + open := func(name string) []byte { + file, err := static.Templates.Open(name) + try(err) + fileReaded, err := io.ReadAll(file) + try(err) + + return fileReaded + } + // функция, что управляет всем handle := func(w http.ResponseWriter, r *http.Request) { if h := r.Header["X-Forwarded-Proto"]; len(h) != 0 && h[0] == "https" { @@ -49,7 +60,8 @@ func Router() { Host = "http://" + r.Host } - path := parsepath(r.URL.Path) + Path = r.URL.Path + path := parsepath(Path) // структура с функциями var skunky skunkyart skunky.Writer = w @@ -70,12 +82,14 @@ func Router() { skunky.Atom = true } + skunky.Endpoint = path[1] + // пути - switch path[1] { + switch skunky.Endpoint { default: skunky.ReturnHTTPError(404) case "": - skunky.ExecuteTemplate("index.htm", &CFG.URI) + w.Write(open("html/index.htm")) case "post": skunky.Deviation(path[2], path[3]) case "search": @@ -88,6 +102,13 @@ func Router() { case "media": switch path[2] { case "file": + if a := arg("filename"); a != "" { + var filename strings.Builder + filename.WriteString(`filename="`) + filename.WriteString(a) + filename.WriteString(`"`) + w.Header().Add("Content-Disposition", filename.String()) + } skunky.DownloadAndSendMedia(path[3], next(path, 4)) case "emojitar": skunky.Emojitar(path[3]) @@ -96,9 +117,9 @@ func Router() { skunky.About() case "stylesheet": w.Header().Add("content-type", "text/css") - wr(w, Templates["skunky.css"]) + w.Write(open("css/skunky.css")) case "favicon.ico": - wr(w, Templates["logo.png"]) + w.Write(open("images/logo.png")) } } diff --git a/app/stat-freebsd.go b/app/stat-freebsd.go new file mode 100644 index 0000000..445eeae --- /dev/null +++ b/app/stat-freebsd.go @@ -0,0 +1,13 @@ +//go:build freebsd +// +build freebsd + +package app + +import ( + "syscall" + "time" +) + +func statTime(stat *syscall.Stat_t) int64 { + return time.Unix(stat.Ctimespec.Unix()).UnixMilli() +} diff --git a/app/stat.go b/app/stat.go new file mode 100644 index 0000000..a5bebcc --- /dev/null +++ b/app/stat.go @@ -0,0 +1,13 @@ +//go:build !freebsd +// +build !freebsd + +package app + +import ( + "syscall" + "time" +) + +func statTime(stat *syscall.Stat_t) int64 { + return time.Unix(stat.Ctim.Unix()).UnixMilli() +} diff --git a/app/util.go b/app/util.go index 8c26760..a5b978d 100644 --- a/app/util.go +++ b/app/util.go @@ -1,14 +1,13 @@ package app import ( - "encoding/base64" "io" "net/http" - u "net/url" + "net/url" "os" + "skunkyart/static" "strconv" "strings" - "syscall" "text/template" "time" @@ -32,26 +31,34 @@ func tryWithExitStatus(err error, code int) { } } +func restore() { + if r := recover(); r != nil { + recover() + } +} + +var instances []byte + func RefreshInstances() { for { func() { - defer func() { - if r := recover(); r != nil { - recover() - } - }() - Templates["instances.json"] = string(Download("https://git.macaw.me/skunky/SkunkyArt/raw/branch/master/instances.json").Body) + defer restore() + instances = Download("https://git.macaw.me/skunky/SkunkyArt/raw/branch/master/instances.json").Body }() time.Sleep(1 * time.Hour) } } // some crap for frontend -func (s skunkyart) ExecuteTemplate(file string, data any) { +func (s skunkyart) ExecuteTemplate(file, dir string, data any) { var buf strings.Builder tmp := template.New(file) - tmp, e := tmp.Parse(Templates[file]) - try(e) + tmp, err := tmp.ParseFS(static.Templates, dir+"/*") + if err != nil { + s.Writer.WriteHeader(500) + wr(s.Writer, err.Error()) + return + } try(tmp.Execute(&buf, &data)) wr(s.Writer, buf.String()) } @@ -91,15 +98,15 @@ type Downloaded struct { Body []byte } -func Download(url string) (d Downloaded) { +func Download(urlString string) (d Downloaded) { cli := &http.Client{} if CFG.DownloadProxy != "" { - u, e := u.Parse(CFG.DownloadProxy) + u, e := url.Parse(CFG.DownloadProxy) try(e) cli.Transport = &http.Transport{Proxy: http.ProxyURL(u)} } - req, e := http.NewRequest("GET", url, nil) + req, e := http.NewRequest("GET", urlString, nil) try(e) req.Header.Set("User-Agent", CFG.UserAgent) @@ -115,97 +122,16 @@ func Download(url string) (d Downloaded) { return } -// caching -func (s skunkyart) DownloadAndSendMedia(subdomain, path string) { - var url strings.Builder - url.WriteString("https://images-wixmp-") - url.WriteString(subdomain) - url.WriteString(".wixmp.com/") - url.WriteString(path) - url.WriteString("?token=") - url.WriteString(s.Args.Get("token")) - - if CFG.Cache.Enabled { - fname := CFG.Cache.Path + "/" + base64.StdEncoding.EncodeToString([]byte(subdomain+path)) - file, e := os.Open(fname) - - if e != nil { - dwnld := Download(url.String()) - if dwnld.Status == 200 && dwnld.Headers["Content-Type"][0][:5] == "image" { - try(os.WriteFile(fname, dwnld.Body, 0700)) - s.Writer.Write(dwnld.Body) - } - } else { - file, e := io.ReadAll(file) - try(e) - s.Writer.Write(file) - } - } else if CFG.Proxy { - dwnld := Download(url.String()) - s.Writer.Write(dwnld.Body) - } else { - s.Writer.WriteHeader(403) - s.Writer.Write([]byte("Sorry, butt proxy on this instance are disabled.")) - } -} - -func InitCacheSystem() { - c := &CFG.Cache - os.Mkdir(CFG.Cache.Path, 0700) - for { - dir, e := os.Open(c.Path) - try(e) - stat, e := dir.Stat() - try(e) - - dirnames, e := dir.Readdirnames(-1) - try(e) - for _, a := range dirnames { - a = c.Path + "/" + a - if c.Lifetime != "" { - now := time.Now().UnixMilli() - - f, _ := os.Stat(a) - stat := f.Sys().(*syscall.Stat_t) - time := time.Unix(stat.Ctim.Unix()).UnixMilli() - - if time+lifetimeParsed <= now { - try(os.RemoveAll(a)) - } - } - if c.MaxSize != 0 && stat.Size() > c.MaxSize { - try(os.RemoveAll(a)) - } - } - - dir.Close() - time.Sleep(time.Second * time.Duration(CFG.Cache.UpdateInterval)) - } -} - -func CopyTemplatesToMemory() { - for _, dirname := range CFG.Dirs { - dir, e := os.ReadDir(dirname) - tryWithExitStatus(e, 1) - - for _, x := range dir { - file, e := os.ReadFile(dirname + "/" + x.Name()) - tryWithExitStatus(e, 1) - Templates[x.Name()] = string(file) - } - } -} - /* PARSING HELPERS */ -func ParseMedia(media devianter.Media, thumb ...int) string { - url := devianter.UrlFromMedia(media, thumb...) - if len(url) != 0 && CFG.Proxy { - url = url[21:] - dot := strings.Index(url, ".") +func ParseMedia(media devianter.Media, filename string, thumb ...int) string { + mediaUrl := devianter.UrlFromMedia(media, thumb...) + if len(mediaUrl) != 0 && CFG.Proxy { + mediaUrl = mediaUrl[21:] + dot := strings.Index(mediaUrl, ".") - return UrlBuilder("media", "file", url[:dot], url[dot+11:]) + return UrlBuilder("media", "file", mediaUrl[:dot], mediaUrl[dot+11:], "&filename=", url.QueryEscape(filename)) } - return url + return mediaUrl } func ConvertDeviantArtUrlToSkunkyArt(url string) (output string) { @@ -255,7 +181,9 @@ func (s skunkyart) NavBase(c DeviationList) string { list.WriteString("<br>") prevrev := func(msg string, page int, onpage bool) { if !onpage { - list.WriteString(`<a href="?p=`) + list.WriteString(`<a href="`) + list.WriteString(Path) + list.WriteString(`?p=`) list.WriteString(strconv.Itoa(page)) if s.Type != 0 { list.WriteString("&type=") diff --git a/app/wrapper.go b/app/wrapper.go index 250e88b..5e8b6c1 100644 --- a/app/wrapper.go +++ b/app/wrapper.go @@ -15,17 +15,17 @@ import ( ) var wr = io.WriteString -var Templates = make(map[string]string) type skunkyart struct { Writer http.ResponseWriter - Args url.Values - BasePath string - Type rune - Query, QueryRaw string - Page int - Atom bool + Args url.Values + Page int + Type rune + Atom bool + + BasePath, Endpoint string + Query, QueryRaw string Templates struct { About struct { @@ -136,7 +136,7 @@ func (s skunkyart) GRUser() { case "cover_deviation": group.About.BGMeta = x.ModuleData.CoverDeviation.Deviation group.About.BGMeta.Url = ConvertDeviantArtUrlToSkunkyArt(group.About.BGMeta.Url) - group.About.BG = ParseMedia(group.About.BGMeta.Media) + group.About.BG = ParseMedia(group.About.BGMeta.Media, group.About.BGMeta.Title) case "group_admins": var htm strings.Builder for _, z := range x.ModuleData.GroupAdmins.Results { @@ -171,7 +171,7 @@ func (s skunkyart) GRUser() { folders.WriteString(`<a href="`) folders.WriteString(ConvertDeviantArtUrlToSkunkyArt(x.Thumb.Url)) folders.WriteString(`"><img loading="lazy" src="`) - folders.WriteString(ParseMedia(x.Thumb.Media)) + folders.WriteString(ParseMedia(x.Thumb.Media, x.Thumb.Title)) folders.WriteString(`" title="`) folders.WriteString(x.Thumb.Title) folders.WriteString(`"></a>`) @@ -180,7 +180,7 @@ func (s skunkyart) GRUser() { } folders.WriteString("<br>") - folders.WriteString(`<a href="?folder=`) + folders.WriteString(`<a href="group_user?folder=`) folders.WriteString(strconv.Itoa(x.FolderId)) folders.WriteString("&q=") folders.WriteString(s.Query) @@ -209,7 +209,7 @@ func (s skunkyart) GRUser() { } if !s.Atom { - s.ExecuteTemplate("gruser.htm", &s) + s.ExecuteTemplate("gruser.htm", "html", &s) } } @@ -233,7 +233,7 @@ func (s skunkyart) Deviation(author, postname string) { } // время публикации post.StringTime = post.Post.Deviation.PublishedTime.UTC().String() - post.Post.IMG = ParseMedia(post.Post.Deviation.Media) + post.Post.IMG = ParseMedia(post.Post.Deviation.Media, post.Post.Deviation.Title) for _, x := range post.Post.Deviation.Extended.RelatedContent { if len(x.Deviations) != 0 { post.Related += s.DeviationList(x.Deviations, false) @@ -258,7 +258,7 @@ func (s skunkyart) Deviation(author, postname string) { post.Comments = s.ParseComments(devianter.GetComments(id, post.Post.Comments.Cursor, s.Page, 1)) - s.ExecuteTemplate("deviantion.htm", &s) + s.ExecuteTemplate("deviantion.htm", "html", &s) } func (s skunkyart) DD() { @@ -281,11 +281,16 @@ func (s skunkyart) DD() { More: dd.HasMore, }) if !s.Atom { - s.ExecuteTemplate("daily.htm", &s) + s.ExecuteTemplate("daily.htm", "html", &s) } } func (s skunkyart) Search() { + if s.Query == "" { + s.ReturnHTTPError(400) + return + } + var err error ss := &s.Templates.Search switch s.Type { @@ -338,6 +343,7 @@ func (s skunkyart) Search() { } default: s.ReturnHTTPError(400) + return } try(err) @@ -348,7 +354,7 @@ func (s skunkyart) Search() { }) } - s.ExecuteTemplate("search.htm", &s) + s.ExecuteTemplate("search.htm", "html", &s) } func (s skunkyart) Emojitar(name string) { @@ -367,6 +373,6 @@ func (s skunkyart) Emojitar(name string) { func (s skunkyart) About() { s.Templates.About.Nsfw = CFG.Nsfw s.Templates.About.Proxy = CFG.Proxy - try(json.Unmarshal([]byte(Templates["instances.json"]), &s.Templates.About)) - s.ExecuteTemplate("about.htm", &s) + try(json.Unmarshal(instances, &s.Templates.About)) + s.ExecuteTemplate("about.htm", "html", &s) } |
