feat: implement basic structure with dockerfile
This commit is contained in:
parent
eb1daac82f
commit
ba52b25fbd
30
build/package/Dockerfile
Normal file
30
build/package/Dockerfile
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
FROM golang:alpine as builder
|
||||||
|
|
||||||
|
WORKDIR /go
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN apk add -U --no-cache ca-certificates && update-ca-certificates && go install github.com/swaggo/swag/cmd/swag@latest
|
||||||
|
|
||||||
|
# Cache dependencies
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY . ./
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags "-w -s" -o /app ./cmd/playground/
|
||||||
|
|
||||||
|
FROM scratch
|
||||||
|
|
||||||
|
ARG VERSION
|
||||||
|
ENV VERSION=$VERSION
|
||||||
|
|
||||||
|
WORKDIR /
|
||||||
|
|
||||||
|
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||||
|
COPY --from=builder /app ./
|
||||||
|
COPY web ./web
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
CMD ["/app"]
|
@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"git.anthrove.art/Anthrove/gorse-playground/internal/logic"
|
"git.anthrove.art/Anthrove/gorse-playground/internal/logic"
|
||||||
"git.anthrove.art/Anthrove/gorse-playground/pkg/models"
|
"git.anthrove.art/Anthrove/gorse-playground/pkg/models"
|
||||||
"git.anthrove.art/Anthrove/gorse-playground/pkg/utils"
|
"git.anthrove.art/Anthrove/gorse-playground/pkg/utils"
|
||||||
@ -9,9 +10,18 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
/*err := logic.SubmitItems(context.Background())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}*/
|
||||||
|
|
||||||
|
go Routine(context.Background())
|
||||||
|
|
||||||
router := gin.Default()
|
router := gin.Default()
|
||||||
store := cookie.NewStore([]byte("secret"))
|
store := cookie.NewStore([]byte("secret"))
|
||||||
router.Use(sessions.Sessions("mysession", store))
|
router.Use(sessions.Sessions("mysession", store))
|
||||||
@ -126,7 +136,48 @@ func main() {
|
|||||||
|
|
||||||
c.HTML(http.StatusOK, "post.gohtml", gin.H{"recs": recs, "next_page": pageInt + 1, "last_page": pageInt - 1})
|
c.HTML(http.StatusOK, "post.gohtml", gin.H{"recs": recs, "next_page": pageInt + 1, "last_page": pageInt - 1})
|
||||||
})
|
})
|
||||||
|
router.POST("/like/:id", func(c *gin.Context) {
|
||||||
|
session := sessions.Default(c)
|
||||||
|
userid := session.Get("userid")
|
||||||
|
id := c.Param("id")
|
||||||
|
|
||||||
|
err := logic.UpsertFavorites(c, []models.GorseFavorite{{
|
||||||
|
Comment: "",
|
||||||
|
FeedbackType: "like",
|
||||||
|
ItemId: id,
|
||||||
|
Timestamp: time.Now().String(),
|
||||||
|
UserId: userid.(string),
|
||||||
|
}})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"upsert favorite error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"item": id})
|
||||||
|
})
|
||||||
|
|
||||||
router.Run(":8080")
|
router.Run(":8080")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Routine(c context.Context) {
|
||||||
|
now := time.Now()
|
||||||
|
next := time.Date(now.Year(), now.Month(), now.Day(), 4, 0, 0, 0, now.Location())
|
||||||
|
if now.After(next) {
|
||||||
|
next = next.Add(24 * time.Hour)
|
||||||
|
}
|
||||||
|
duration := next.Sub(now)
|
||||||
|
time.Sleep(duration)
|
||||||
|
ticker := time.NewTicker(24 * time.Hour)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-c.Done():
|
||||||
|
ticker.Stop()
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
logic.SubmitItems(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -5,11 +5,18 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"git.anthrove.art/Anthrove/gorse-playground/internal/config"
|
"git.anthrove.art/Anthrove/gorse-playground/internal/config"
|
||||||
|
"git.anthrove.art/Anthrove/gorse-playground/pkg/e621"
|
||||||
|
"git.anthrove.art/Anthrove/gorse-playground/pkg/models"
|
||||||
|
"git.anthrove.art/Anthrove/gorse-playground/pkg/utils"
|
||||||
"github.com/anthrove/openapi-e621-go"
|
"github.com/anthrove/openapi-e621-go"
|
||||||
"github.com/caarlos0/env/v11"
|
"github.com/caarlos0/env/v11"
|
||||||
"golang.org/x/time/rate"
|
"golang.org/x/time/rate"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
_ "github.com/joho/godotenv/autoload"
|
_ "github.com/joho/godotenv/autoload"
|
||||||
)
|
)
|
||||||
@ -80,6 +87,83 @@ func GetFavoritePage(ctx context.Context, userId int, pageIdentifier int) ([]ope
|
|||||||
return favorites.Posts, nil
|
return favorites.Posts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SubmitItems(ctx context.Context) error {
|
||||||
|
currentDate := time.Now().Format("2006-01-02")
|
||||||
|
err := utils.DownloadE6Data(ctx, "posts-"+currentDate+".csv.gz", "post-file.csv")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fileReader, err := os.Open("post-file.csv")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
inputE621PostChannel := make(chan e621.Post)
|
||||||
|
outputAnthrovePostChannel := make(chan models.GorseItem)
|
||||||
|
postChan := utils.GetStreamingData[e621.Post](ctx, fileReader)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer close(inputE621PostChannel)
|
||||||
|
for post := range postChan {
|
||||||
|
inputE621PostChannel <- post
|
||||||
|
}
|
||||||
|
log.Println("Loading ended")
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer close(outputAnthrovePostChannel)
|
||||||
|
err := postToItem(inputE621PostChannel, outputAnthrovePostChannel)
|
||||||
|
if err != nil { //TODO: DEADLOCK
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
log.Println("Convert ended")
|
||||||
|
}()
|
||||||
|
|
||||||
|
log.Println("Start with comparison check")
|
||||||
|
|
||||||
|
items := make([]models.GorseItem, 0)
|
||||||
|
length := 0
|
||||||
|
|
||||||
|
for item := range outputAnthrovePostChannel {
|
||||||
|
timeDate, err := time.Parse(time.DateTime, item.Timestamp)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !timeDate.After(time.Date(2024, 1, 1, 1, 1, 1, 0, time.UTC)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
items = append(items, item)
|
||||||
|
|
||||||
|
if length%20_000 == 0 && length > 0 {
|
||||||
|
log.Println("Worked ", length, " items")
|
||||||
|
err := UpsertItems(ctx, items)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
items = make([]models.GorseItem, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
length++
|
||||||
|
}
|
||||||
|
|
||||||
|
err = UpsertItems(ctx, items)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func newRateMiddleware(transport *http.Transport) http.RoundTripper {
|
func newRateMiddleware(transport *http.Transport) http.RoundTripper {
|
||||||
return &rateMiddleware{
|
return &rateMiddleware{
|
||||||
transport: transport,
|
transport: transport,
|
||||||
@ -99,3 +183,19 @@ func (r rateMiddleware) RoundTrip(request *http.Request) (*http.Response, error)
|
|||||||
|
|
||||||
return r.transport.RoundTrip(request)
|
return r.transport.RoundTrip(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func postToItem(input chan e621.Post, output chan models.GorseItem) error {
|
||||||
|
for e6Post := range input {
|
||||||
|
|
||||||
|
tagParts := strings.Split(e6Post.TagString, " ")
|
||||||
|
|
||||||
|
output <- models.GorseItem{
|
||||||
|
Comment: e6Post.Description,
|
||||||
|
IsHidden: e6Post.IsDeleted,
|
||||||
|
ItemId: strconv.Itoa(e6Post.ID),
|
||||||
|
Labels: tagParts,
|
||||||
|
Timestamp: e6Post.CreatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -44,12 +44,14 @@ func GetUserFavorites(ctx context.Context, userid string, page int) ([]string, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
q := req.URL.Query()
|
q := req.URL.Query()
|
||||||
q.Set("name", "50")
|
q.Set("n", "20")
|
||||||
q.Set("offset", strconv.Itoa((page-1)*50))
|
q.Set("offset", strconv.Itoa((page-1)*20))
|
||||||
|
req.URL.RawQuery = q.Encode()
|
||||||
|
|
||||||
req = req.WithContext(ctx)
|
req = req.WithContext(ctx)
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
req.Header.Set("X-API-Key", gorseConfig.ApiKey)
|
req.Header.Set("X-API-Key", gorseConfig.ApiKey)
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
33
pkg/e621/model.go
Normal file
33
pkg/e621/model.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package e621
|
||||||
|
|
||||||
|
type Post struct {
|
||||||
|
ID int `csv:"id"`
|
||||||
|
UploaderID int `csv:"uploader_id"`
|
||||||
|
CreatedAt string `csv:"created_at"`
|
||||||
|
MD5 string `csv:"md5"`
|
||||||
|
Source string `csv:"source"`
|
||||||
|
Rating string `csv:"rating"`
|
||||||
|
ImageWidth int `csv:"image_width"`
|
||||||
|
ImageHeight int `csv:"image_height"`
|
||||||
|
TagString string `csv:"tag_string"`
|
||||||
|
LockedTags string `csv:"locked_tags"`
|
||||||
|
FavCount int `csv:"fav_count"`
|
||||||
|
FileExt string `csv:"file_ext"`
|
||||||
|
ParentID int `csv:"parent_id"`
|
||||||
|
ChangeSeq int `csv:"change_seq"`
|
||||||
|
ApproverID int `csv:"approver_id"`
|
||||||
|
FileSize int `csv:"file_size"`
|
||||||
|
CommentCount int `csv:"comment_count"`
|
||||||
|
Description string `csv:"description"`
|
||||||
|
Duration int `csv:"duration"`
|
||||||
|
UpdatedAt string `csv:"updated_at"`
|
||||||
|
IsDeleted bool `csv:"is_deleted"`
|
||||||
|
IsPending bool `csv:"is_pending"`
|
||||||
|
IsFlagged bool `csv:"is_flagged"`
|
||||||
|
Score int `csv:"score"`
|
||||||
|
UpScore int `csv:"up_score"`
|
||||||
|
DownScore int `csv:"down_score"`
|
||||||
|
IsRatingLocked bool `csv:"is_rating_locked"`
|
||||||
|
IsStatusLocked bool `csv:"is_status_locked"`
|
||||||
|
IsNoteLocked bool `csv:"is_note_locked"`
|
||||||
|
}
|
57
pkg/utils/e621.go
Normal file
57
pkg/utils/e621.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"compress/gzip"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var httpClient http.Client
|
||||||
|
|
||||||
|
func DownloadE6Data(ctx context.Context, filename string, targetPath string) error {
|
||||||
|
req, err := buildE6Request(fmt.Sprintf("/db_export/%s", filename))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
resp, err := httpClient.Do(req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
uncompressedStream, err := gzip.NewReader(resp.Body)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer uncompressedStream.Close()
|
||||||
|
|
||||||
|
out, err := os.Create(targetPath)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer out.Close()
|
||||||
|
_, err = io.Copy(out, uncompressedStream)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildE6Request(url string) (*http.Request, error) {
|
||||||
|
request, err := http.NewRequest("GET", fmt.Sprintf("%s%s", "https://e621.net", url), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
request.Header.Add("User-Agent", "Anthrove downloader (by alphyron)")
|
||||||
|
|
||||||
|
return request, nil
|
||||||
|
}
|
117
pkg/utils/streaming.go
Normal file
117
pkg/utils/streaming.go
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/csv"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetStreamingFileData[T any](ctx context.Context, filePath string) chan T {
|
||||||
|
csvIn, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetStreamingData[T](ctx, csvIn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetStreamingData[T any](ctx context.Context, rc io.Reader) chan T {
|
||||||
|
ch := make(chan T)
|
||||||
|
go func() {
|
||||||
|
inputChan := make(chan []string)
|
||||||
|
r := csv.NewReader(rc)
|
||||||
|
var header []string
|
||||||
|
var err error
|
||||||
|
if header, err = r.Read(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer close(inputChan)
|
||||||
|
go func() {
|
||||||
|
defer close(ch)
|
||||||
|
returnChannel := parseRecord[T](header, inputChan)
|
||||||
|
for data := range returnChannel {
|
||||||
|
ch <- data
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
rec, err := r.Read()
|
||||||
|
if err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rec) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
inputChan <- rec
|
||||||
|
}
|
||||||
|
log.Println("Input finished")
|
||||||
|
}()
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRecord[T any](header []string, input chan []string) chan T {
|
||||||
|
channel := make(chan T)
|
||||||
|
go func() {
|
||||||
|
defer close(channel)
|
||||||
|
var e T
|
||||||
|
et := reflect.TypeOf(e)
|
||||||
|
var headers = make(map[string]int, et.NumField())
|
||||||
|
for i := 0; i < et.NumField(); i++ {
|
||||||
|
headers[et.Field(i).Name] = func(element string, array []string) int {
|
||||||
|
for k, v := range array {
|
||||||
|
if v == element {
|
||||||
|
return k
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}(et.Field(i).Tag.Get("csv"), header)
|
||||||
|
}
|
||||||
|
for record := range input {
|
||||||
|
if len(record) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for h, i := range headers {
|
||||||
|
if i == -1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
elem := reflect.ValueOf(&e).Elem()
|
||||||
|
field := elem.FieldByName(h)
|
||||||
|
if field.CanSet() {
|
||||||
|
switch field.Type().Name() {
|
||||||
|
case "bool":
|
||||||
|
a, _ := strconv.ParseBool(record[i])
|
||||||
|
field.Set(reflect.ValueOf(a))
|
||||||
|
case "int":
|
||||||
|
a, _ := strconv.Atoi(record[i])
|
||||||
|
field.Set(reflect.ValueOf(a))
|
||||||
|
case "float64":
|
||||||
|
a, _ := strconv.ParseFloat(record[i], 64)
|
||||||
|
field.Set(reflect.ValueOf(a))
|
||||||
|
case "Time":
|
||||||
|
a, _ := time.Parse("2006-01-02T00:00:00Z", record[i])
|
||||||
|
field.Set(reflect.ValueOf(a))
|
||||||
|
case "string":
|
||||||
|
field.Set(reflect.ValueOf(record[i]))
|
||||||
|
default:
|
||||||
|
log.Printf("Unknown Fieldtype: %s\n", field.Type().Name())
|
||||||
|
field.Set(reflect.ValueOf(record[i]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
channel <- e
|
||||||
|
}
|
||||||
|
log.Println("parsing ended")
|
||||||
|
}()
|
||||||
|
return channel
|
||||||
|
}
|
@ -6,7 +6,7 @@
|
|||||||
<h1>Available Recommondations</h1>
|
<h1>Available Recommondations</h1>
|
||||||
<div>
|
<div>
|
||||||
{{range .recs}}
|
{{range .recs}}
|
||||||
<a href="https://e621.net/posts/{{.}}" target="_blank">{{.}}</a><br>
|
<a href="https://e621.net/posts/{{.}}" target="_blank">{{.}}</a><button onclick="rec({{.}})">Like</button><br>
|
||||||
{{end}}
|
{{end}}
|
||||||
<form method="get">
|
<form method="get">
|
||||||
<input value="{{ .last_page }}" style="display: none" name="page">
|
<input value="{{ .last_page }}" style="display: none" name="page">
|
||||||
@ -17,5 +17,13 @@
|
|||||||
<button type="submit">Next Page</button>
|
<button type="submit">Next Page</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
<script>
|
||||||
|
function rec(id){
|
||||||
|
const xmlHttp = new XMLHttpRequest();
|
||||||
|
xmlHttp.open( "POST",window.location.origin + "/like/" + id, false ); // false for synchronous request
|
||||||
|
xmlHttp.send( null );
|
||||||
|
return xmlHttp.responseText;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
Loading…
x
Reference in New Issue
Block a user