added s3 compatible, and updated stuff

This commit is contained in:
goro 2026-04-18 11:51:44 +03:00
parent 2810370c02
commit 771eb60ea4
26 changed files with 364 additions and 51 deletions

View File

@ -215,13 +215,29 @@ func (server *Server) getLocation(ctx *gin.Context) {
return
}
type reviewWithImages struct {
db.GetListLocationReviewsRow
Images []db.GetImagesByReviewRow `json:"images"`
}
attachImages := func(reviews []db.GetListLocationReviewsRow) []reviewWithImages {
result := make([]reviewWithImages, 0, len(reviews))
for _, r := range reviews {
images, _ := server.Store.GetImagesByReview(ctx, r.ID)
if images == nil {
images = []db.GetImagesByReviewRow{}
}
result = append(result, reviewWithImages{r, images})
}
return result
}
users_reviews, err := server.Store.GetListLocationReviews(ctx, db.GetListLocationReviewsParams{
LocationID: req.ID,
Limit: 5,
Offset: 0,
IsCritics: false,
})
if err != nil {
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong while try to receive user reviews for this location"))
return
@ -233,7 +249,6 @@ func (server *Server) getLocation(ctx *gin.Context) {
Offset: 0,
IsCritics: true,
})
if err != nil {
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong while try to receive user reviews for this location"))
return
@ -242,8 +257,8 @@ func (server *Server) getLocation(ctx *gin.Context) {
res := gin.H{
"tags": tags,
"detail": location,
"users_review": users_reviews,
"critics_review": critics_reviews,
"users_review": attachImages(users_reviews),
"critics_review": attachImages(critics_reviews),
}
ctx.JSON(http.StatusOK, res)

View File

@ -1,13 +1,19 @@
package api
import (
"fmt"
"log"
"net/http"
"path/filepath"
"strconv"
"time"
db "git.nochill.in/nochill/hiling_go/db/repository"
"git.nochill.in/nochill/hiling_go/util"
"git.nochill.in/nochill/hiling_go/util/token"
"github.com/gin-gonic/gin"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
)
type createReviewReq struct {
@ -17,6 +23,7 @@ type createReviewReq struct {
IsFromCritic *bool `json:"is_from_critic"`
IsHided *bool `json:"is_hided"`
LocationId int32 `json:"location_id" binding:"required"`
Title string `json:"title"`
}
func (server *Server) createReview(ctx *gin.Context) {
@ -51,6 +58,7 @@ func (server *Server) createReview(ctx *gin.Context) {
IsFromCritic: *req.IsFromCritic,
IsHided: *req.IsHided,
LocationID: req.LocationId,
Title: pgtype.Text{String: req.Title, Valid: req.Title != ""},
}
review, err := server.Store.CreateReview(ctx, arg)
@ -63,10 +71,77 @@ func (server *Server) createReview(ctx *gin.Context) {
ctx.JSON(http.StatusOK, review)
}
func (server *Server) uploadReviewImages(ctx *gin.Context) {
reviewIdStr := ctx.PostForm("review_id")
reviewId, err := strconv.Atoi(reviewIdStr)
if err != nil || reviewId <= 0 {
ctx.JSON(http.StatusBadRequest, gin.H{"message": "Invalid review_id"})
return
}
authPayload := ctx.MustGet(authorizationPayloadKey).(*token.Payload)
form, err := ctx.MultipartForm()
if err != nil {
ctx.JSON(http.StatusBadRequest, ErrorResponse(err, "Failed to parse multipart form"))
return
}
files := form.File["images"]
if len(files) == 0 {
ctx.JSON(http.StatusBadRequest, gin.H{"message": "No images provided"})
return
}
var params []db.CreateImageParams
for _, fileHeader := range files {
fileExt := filepath.Ext(fileHeader.Filename)
key := fmt.Sprintf("reviews/%d/%s%d%s", reviewId, util.RandomString(5), time.Now().UnixNano(), fileExt)
f, err := fileHeader.Open()
if err != nil {
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Failed to open image"))
return
}
defer f.Close()
contentType := fileHeader.Header.Get("Content-Type")
if contentType == "" {
contentType = "application/octet-stream"
}
publicURL, err := server.R2.UploadFile(ctx.Request.Context(), key, f, contentType)
if err != nil {
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Failed to upload image to storage"))
return
}
params = append(params, db.CreateImageParams{
ImageUrl: publicURL,
UploadedBy: int32(authPayload.UserID),
ImageType: "reviews",
ImageOf: int32(reviewId),
})
}
if err := server.Store.CreateImage(ctx, params); err != nil {
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Failed to save image records"))
return
}
ctx.JSON(http.StatusOK, gin.H{"uploaded": len(params)})
}
type getUserReviewByLocationReq struct {
LocationID int32 `uri:"location_id" binding:"required"`
}
type userReviewWithImagesResponse struct {
db.GetUserReviewByLocationRow
Images []db.GetImagesByReviewRow `json:"images"`
}
func (server *Server) getUserReviewByLocation(ctx *gin.Context) {
var req getUserReviewByLocationReq
@ -95,5 +170,13 @@ func (server *Server) getUserReviewByLocation(ctx *gin.Context) {
return
}
ctx.JSON(http.StatusOK, review)
images, err := server.Store.GetImagesByReview(ctx, review.ID)
if err != nil {
images = []db.GetImagesByReviewRow{}
}
ctx.JSON(http.StatusOK, userReviewWithImagesResponse{
GetUserReviewByLocationRow: review,
Images: images,
})
}

View File

@ -5,6 +5,7 @@ import (
db "git.nochill.in/nochill/hiling_go/db/repository"
"git.nochill.in/nochill/hiling_go/util"
"git.nochill.in/nochill/hiling_go/util/cloudfare"
"git.nochill.in/nochill/hiling_go/util/token"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
@ -17,6 +18,7 @@ type Server struct {
TokenMaker token.Maker
Router *gin.Engine
MeilisearchClient *meilisearch.Client
R2 *cloudfare.R2Client
}
func NewServer(config util.Config, store db.Store) (*Server, error) {
@ -30,11 +32,23 @@ func NewServer(config util.Config, store db.Store) (*Server, error) {
APIKey: config.MeilisearchKey,
})
r2Client, err := cloudfare.NewR2Client(
config.R2AccountID,
config.R2AccessKeyID,
config.R2SecretAccessKey,
config.R2BucketName,
config.R2PublicApiID,
)
if err != nil {
return nil, fmt.Errorf("cannot create R2 client: %w", err)
}
server := &Server{
Config: config,
Store: store,
TokenMaker: tokenMaker,
MeilisearchClient: meiliClient,
R2: r2Client,
}
server.getRoutes()
@ -45,7 +59,7 @@ func (server *Server) getRoutes() {
router := gin.Default()
router.Use(cors.New(cors.Config{
AllowOrigins: []string{"http://localhost:5173", "http://192.168.18.164:5173", "http://192.168.1.13:5173"},
AllowOrigins: []string{"http://localhost:5173", "http://192.168.18.164:5173", "http://192.168.18.150:5173", "http://192.168.1.9:5173"},
AllowCredentials: true,
AllowHeaders: []string{"Content-Type", "Content-Length", "Accept-Encoding", "Authorization", "accept", "origin", "Cache-Control", "X-Requested-With", "X-XSRF-TOKEN"},
AllowMethods: []string{"POST", "PUT", "GET", "DELETE", "PATCH"},
@ -77,6 +91,7 @@ func (server *Server) getRoutes() {
// REQUIRE AUTH TOKEN
authRoutes := router.Use(authMiddleware(server.TokenMaker))
authRoutes.POST("/review/location", server.createReview)
authRoutes.POST("/review/location/images", server.uploadReviewImages)
authRoutes.PATCH("/user", server.updateUser)
authRoutes.GET("/user/review/location/:location_id", server.getUserReviewByLocation)
authRoutes.GET("/user/profile", server.getUserStats)

View File

@ -118,6 +118,7 @@ CREATE TABLE location_images (
CREATE TABLE reviews (
"id" serial primary key not null,
"title" varchar,
"submitted_by" integer references "users"("id") not null,
"comments" text not null,
"score" smallint not null,

View File

@ -84,6 +84,21 @@ func (mr *MockStoreMockRecorder) CreateImage(arg0, arg1 any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateImage", reflect.TypeOf((*MockStore)(nil).CreateImage), arg0, arg1)
}
// GetImagesByReview mocks base method.
func (m *MockStore) GetImagesByReview(arg0 context.Context, arg1 int32) ([]db.GetImagesByReviewRow, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetImagesByReview", arg0, arg1)
ret0, _ := ret[0].([]db.GetImagesByReviewRow)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetImagesByReview indicates an expected call of GetImagesByReview.
func (mr *MockStoreMockRecorder) GetImagesByReview(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetImagesByReview", reflect.TypeOf((*MockStore)(nil).GetImagesByReview), arg0, arg1)
}
// CreateLocation mocks base method.
func (m *MockStore) CreateLocation(arg0 context.Context, arg1 db.CreateLocationParams) (int32, error) {
m.ctrl.T.Helper()

View File

@ -7,6 +7,8 @@ WHERE reviews.location_id = $1 AND reviews.submitted_by = $2;
-- name: GetUserReviewByLocation :one
SELECT
re.id,
re.title,
re.submitted_by,
re.location_id,
re.score,
re.comments,

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// sqlc v1.30.0
package db

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// sqlc v1.30.0
// source: follow.sql
package db

View File

@ -61,6 +61,39 @@ func (q *Queries) GetImagesByLocation(ctx context.Context, arg GetImagesByLocati
return items, nil
}
type GetImagesByReviewRow struct {
ID int32 `json:"id"`
Src string `json:"src"`
}
const getImagesByReviewQ = `
SELECT id, image_url as src
FROM images
WHERE image_type = 'reviews' AND image_of = $1
`
func (q *Queries) GetImagesByReview(ctx context.Context, reviewID int32) ([]GetImagesByReviewRow, error) {
rows, err := q.db.Query(ctx, getImagesByReviewQ, reviewID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetImagesByReviewRow{}
for rows.Next() {
var i GetImagesByReviewRow
if err := rows.Scan(&i.ID, &i.Src); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
type CreateImageParams struct {
ImageUrl string `json:"url"`
UploadedBy int32 `json:"uploaded_by"`
@ -87,11 +120,7 @@ func (q *Queries) CreateImage(ctx context.Context, arg []CreateImageParams) erro
// Replacing ? with $n for postgres
queryStr = util.ReplaceSQL(queryStr, "?")
// prepare the statement
_, err := q.db.Exec(ctx, queryStr)
// format all vals at once
// _, err := stmt.Ex(ctx, values...)
_, err := q.db.Exec(ctx, queryStr, values...)
return err
}

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// sqlc v1.30.0
// source: images.sql
package db

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// sqlc v1.30.0
// source: locations.sql
package db

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// sqlc v1.30.0
package db
@ -58,11 +58,12 @@ func (ns NullCommentType) Value() (driver.Value, error) {
type LocationType string
const (
LocationTypeBeach LocationType = "beach"
LocationTypeAmusementpark LocationType = "amusement park"
LocationTypeCulinary LocationType = "culinary"
LocationTypeHikingCamping LocationType = "hiking / camping"
LocationTypeOther LocationType = "other"
LocationTypeTraditionalmarket LocationType = "traditional market"
LocationTypeMall LocationType = "mall"
LocationTypeCulinary LocationType = "culinary"
LocationTypeRecreation LocationType = "recreation"
LocationTypeAccommodation LocationType = "accommodation"
LocationTypeOther LocationType = "other"
)
func (e *LocationType) Scan(src interface{}) error {
@ -168,6 +169,12 @@ type Comment struct {
UpdatedAt pgtype.Timestamp `json:"updated_at"`
}
type Emote struct {
ID int32 `json:"id"`
Name string `json:"name"`
EmoteID string `json:"emote_id"`
}
type Image struct {
ID int32 `json:"id"`
ImageUrl string `json:"image_url"`
@ -204,6 +211,15 @@ type LocationImage struct {
UpdatedAt pgtype.Timestamp `json:"updated_at"`
}
type LocationPageVisit struct {
ID int32 `json:"id"`
LocationID int32 `json:"location_id"`
WeekKey string `json:"week_key"`
VisitCount int64 `json:"visit_count"`
IsDeleted pgtype.Bool `json:"is_deleted"`
UpdatedAt pgtype.Timestamp `json:"updated_at"`
}
type NewsEvent struct {
ID int32 `json:"id"`
Title string `json:"title"`
@ -243,6 +259,7 @@ type Region struct {
type Review struct {
ID int32 `json:"id"`
Title pgtype.Text `json:"title"`
SubmittedBy int32 `json:"submitted_by"`
Comments string `json:"comments"`
Score int16 `json:"score"`

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// sqlc v1.30.0
// source: news_events.sql
package db

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// sqlc v1.30.0
// source: provinces.sql
package db

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// sqlc v1.30.0
package db

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// sqlc v1.30.0
// source: regencies.sql
package db

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// sqlc v1.30.0
// source: regions.sql
package db

View File

@ -14,18 +14,20 @@ INSERT INTO reviews (
score,
is_from_critic,
is_hided,
location_id
) VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id, submitted_by, comments, score, is_from_critic, is_hided, location_id, created_at, updated_at
location_id,
title
) VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id, submitted_by, comments, score, is_from_critic, is_hided, location_id, title, created_at, updated_at
`
type CreateReviewParams struct {
SubmittedBy int32 `json:"submitted_by"`
Comments string `json:"comments"`
Score int16 `json:"score"`
IsFromCritic bool `json:"is_from_critic"`
IsHided bool `json:"is_hided"`
LocationID int32 `json:"location_id"`
SubmittedBy int32 `json:"submitted_by"`
Comments string `json:"comments"`
Score int16 `json:"score"`
IsFromCritic bool `json:"is_from_critic"`
IsHided bool `json:"is_hided"`
LocationID int32 `json:"location_id"`
Title pgtype.Text `json:"title"`
}
func (q *Queries) CreateReview(ctx context.Context, arg CreateReviewParams) (Review, error) {
@ -36,6 +38,7 @@ func (q *Queries) CreateReview(ctx context.Context, arg CreateReviewParams) (Rev
arg.IsFromCritic,
arg.IsHided,
arg.LocationID,
arg.Title,
)
var i Review
err := row.Scan(
@ -46,6 +49,7 @@ func (q *Queries) CreateReview(ctx context.Context, arg CreateReviewParams) (Rev
&i.IsFromCritic,
&i.IsHided,
&i.LocationID,
&i.Title,
&i.CreatedAt,
&i.UpdatedAt,
)
@ -61,6 +65,7 @@ type GetListLocationReviewsParams struct {
type GetListLocationReviewsRow struct {
ID int32 `json:"id"`
Title pgtype.Text `json:"title"`
Score int8 `json:"score"`
Comment string `json:"comments"`
UserID int32 `json:"user_id"`
@ -73,6 +78,7 @@ type GetListLocationReviewsRow struct {
const getListLocationReviews = `
SELECT
re.id as id,
re.title as title,
re.score as score,
re.comments as comments,
u.id as user_id,
@ -101,6 +107,7 @@ func (q *Queries) GetListLocationReviews(ctx context.Context, arg GetListLocatio
var i GetListLocationReviewsRow
if err := rows.Scan(
&i.ID,
&i.Title,
&i.Score,
&i.Comment,
&i.UserID,

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// sqlc v1.30.0
// source: reviews.sql
package db
@ -32,6 +32,8 @@ func (q *Queries) CheckIfReviewExists(ctx context.Context, arg CheckIfReviewExis
const getUserReviewByLocation = `-- name: GetUserReviewByLocation :one
SELECT
re.id,
re.title,
re.submitted_by,
re.location_id,
re.score,
re.comments,
@ -47,12 +49,14 @@ type GetUserReviewByLocationParams struct {
}
type GetUserReviewByLocationRow struct {
ID int32 `json:"id"`
LocationID int32 `json:"location_id"`
Score int16 `json:"score"`
Comments string `json:"comments"`
CreatedAt pgtype.Timestamp `json:"created_at"`
UpdatedAt pgtype.Timestamp `json:"updated_at"`
ID int32 `json:"id"`
Title pgtype.Text `json:"title"`
SubmittedBy int32 `json:"submitted_by"`
LocationID int32 `json:"location_id"`
Score int16 `json:"score"`
Comments string `json:"comments"`
CreatedAt pgtype.Timestamp `json:"created_at"`
UpdatedAt pgtype.Timestamp `json:"updated_at"`
}
func (q *Queries) GetUserReviewByLocation(ctx context.Context, arg GetUserReviewByLocationParams) (GetUserReviewByLocationRow, error) {
@ -60,6 +64,8 @@ func (q *Queries) GetUserReviewByLocation(ctx context.Context, arg GetUserReview
var i GetUserReviewByLocationRow
err := row.Scan(
&i.ID,
&i.Title,
&i.SubmittedBy,
&i.LocationID,
&i.Score,
&i.Comments,

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// sqlc v1.30.0
// source: sessions.sql
package db

View File

@ -19,6 +19,7 @@ type Store interface {
GetListLocationReviews(ctx context.Context, arg GetListLocationReviewsParams) ([]GetListLocationReviewsRow, error)
CreateLocation(ctx context.Context, arg CreateLocationParams) (int32, error)
CreateImage(ctx context.Context, arg []CreateImageParams) error
GetImagesByReview(ctx context.Context, reviewID int32) ([]GetImagesByReviewRow, error)
CreateLocationTx(ctx context.Context, arg CreateLocationTxParams) error
GetNewsEventsList(ctx context.Context, arg GetNewsEventsListParams) ([]NewsEventRow, error)
GetListRecentLocationsWithRatings(ctx context.Context, arg GetListRecentLocationsParams) ([]GetListRecentLocationsWithRatingsRow, error)

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// sqlc v1.30.0
// source: users.sql
package db

25
go.mod
View File

@ -1,13 +1,15 @@
module git.nochill.in/nochill/hiling_go
go 1.20
go 1.24
require (
github.com/aead/chacha20poly1305 v0.0.0-20201124145622-1a5aba2a8b29
github.com/gin-contrib/cors v1.4.0
github.com/gin-gonic/gin v1.9.1
github.com/go-playground/validator/v10 v10.17.0
github.com/henvic/pgq v0.0.2
github.com/jackc/pgx/v5 v5.5.3
github.com/meilisearch/meilisearch-go v0.26.2
github.com/o1egl/paseto v1.0.0
github.com/spf13/viper v1.16.0
github.com/stretchr/testify v1.8.4
@ -20,6 +22,25 @@ require (
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 // indirect
github.com/andybalholm/brotli v1.0.4 // indirect
github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.14 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.14 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.99.0 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.15 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 // indirect
github.com/aws/smithy-go v1.24.2 // indirect
github.com/bytedance/sonic v1.10.2 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect
@ -32,7 +53,6 @@ require (
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/henvic/pgq v0.0.2 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
@ -44,7 +64,6 @@ require (
github.com/magiconair/properties v1.8.7 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/meilisearch/meilisearch-go v0.26.2 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect

43
go.sum
View File

@ -47,6 +47,44 @@ github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyY
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us=
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY=
github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
github.com/aws/aws-sdk-go-v2/config v1.32.14 h1:opVIRo/ZbbI8OIqSOKmpFaY7IwfFUOCCXBsUpJOwDdI=
github.com/aws/aws-sdk-go-v2/config v1.32.14/go.mod h1:U4/V0uKxh0Tl5sxmCBZ3AecYny4UNlVmObYjKuuaiOo=
github.com/aws/aws-sdk-go-v2/credentials v1.19.14 h1:n+UcGWAIZHkXzYt87uMFBv/l8THYELoX6gVcUvgl6fI=
github.com/aws/aws-sdk-go-v2/credentials v1.19.14/go.mod h1:cJKuyWB59Mqi0jM3nFYQRmnHVQIcgoxjEMAbLkpr62w=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 h1:NUS3K4BTDArQqNu2ih7yeDLaS3bmHD0YndtA6UP884g=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21/go.mod h1:YWNWJQNjKigKY1RHVJCuupeWDrrHjRqHm0N9rdrWzYI=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 h1:rWyie/PxDRIdhNf4DzRk0lvjVOqFJuNnO8WwaIRVxzQ=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22/go.mod h1:zd/JsJ4P7oGfUhXn1VyLqaRZwPmZwg44Jf2dS84Dm3Y=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 h1:JRaIgADQS/U6uXDqlPiefP32yXTda7Kqfx+LgspooZM=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13/go.mod h1:CEuVn5WqOMilYl+tbccq8+N2ieCy0gVn3OtRb0vBNNM=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 h1:ZlvrNcHSFFWURB8avufQq9gFsheUgjVD9536obIknfM=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21/go.mod h1:cv3TNhVrssKR0O/xxLJVRfd2oazSnZnkUeTf6ctUwfQ=
github.com/aws/aws-sdk-go-v2/service/s3 v1.99.0 h1:hlSuz394kV0vhv9drL5lhuEFbEOEP1VyQpy15qWh1Pk=
github.com/aws/aws-sdk-go-v2/service/s3 v1.99.0/go.mod h1:uoA43SdFwacedBfSgfFSjjCvYe8aYBS7EnU5GZ/YKMM=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 h1:QKZH0S178gCmFEgst8hN0mCX1KxLgHBKKY/CLqwP8lg=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.9/go.mod h1:7yuQJoT+OoH8aqIxw9vwF+8KpvLZ8AWmvmUWHsGQZvI=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.15 h1:lFd1+ZSEYJZYvv9d6kXzhkZu07si3f+GQ1AaYwa2LUM=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.15/go.mod h1:WSvS1NLr7JaPunCXqpJnWk1Bjo7IxzZXrZi1QQCkuqM=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19 h1:dzztQ1YmfPrxdrOiuZRMF6fuOwWlWpD2StNLTceKpys=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19/go.mod h1:YO8TrYtFdl5w/4vmjL8zaBSsiNp3w0L1FfKVKenZT7w=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 h1:p8ogvvLugcR/zLBXTXrTkj0RYBUdErbMnAFFp12Lm/U=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.10/go.mod h1:60dv0eZJfeVXfbT1tFJinbHrDfSJ2GZl4Q//OSSNAVw=
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE=
@ -77,6 +115,7 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
@ -93,6 +132,7 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
@ -146,6 +186,7 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@ -200,6 +241,7 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@ -242,6 +284,7 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=

View File

@ -1,2 +1,57 @@
package cloudfare
import (
"context"
"fmt"
"mime/multipart"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
)
type R2Client struct {
s3Client *s3.Client
bucketName string
publicBaseURL string
}
func NewR2Client(accountID, accessKeyID, secretAccessKey, bucketName, publicApiID string) (*R2Client, error) {
r2Endpoint := fmt.Sprintf("https://%s.r2.cloudflarestorage.com", accountID)
cfg, err := config.LoadDefaultConfig(context.Background(),
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(accessKeyID, secretAccessKey, "")),
config.WithRegion("auto"),
)
if err != nil {
return nil, fmt.Errorf("failed to load R2 config: %w", err)
}
client := s3.NewFromConfig(cfg, func(o *s3.Options) {
o.BaseEndpoint = aws.String(r2Endpoint)
})
return &R2Client{
s3Client: client,
bucketName: bucketName,
publicBaseURL: fmt.Sprintf("https://pub-%s.r2.dev", publicApiID),
}, nil
}
// UploadFile uploads a multipart file to R2 and returns the public URL.
// key is the object path inside the bucket, e.g. "reviews/18/image.png"
func (r *R2Client) UploadFile(ctx context.Context, key string, file multipart.File, contentType string) (string, error) {
_, err := r.s3Client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(r.bucketName),
Key: aws.String(key),
Body: file,
ContentType: aws.String(contentType),
})
if err != nil {
return "", fmt.Errorf("failed to upload to R2: %w", err)
}
publicURL := fmt.Sprintf("%s/%s", r.publicBaseURL, key)
return publicURL, nil
}

View File

@ -17,6 +17,11 @@ type Config struct {
RefreshTokenDuration time.Duration `mapstructure:"REFRESH_TOKEN_DURATION"`
MeilisearchHost string `mapstructure:"MEILISEARCH_HOST"`
MeilisearchKey string `mapstructure:"MEILISEARCH_KEY"`
R2AccountID string `mapstructure:"R2_ACCOUNT_ID"`
R2AccessKeyID string `mapstructure:"R2_ACCESS_KEY_ID"`
R2SecretAccessKey string `mapstructure:"R2_SECRET_ACCESS_KEY"`
R2BucketName string `mapstructure:"R2_BUCKET_NAME"`
R2PublicApiID string `mapstructure:"R2_PUBLIC_API_ID"`
}
func LoadConfig(path string) (config Config, err error) {