Compare commits
4 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4fe077d229 | |||
| c58b578023 | |||
| 53e270015c | |||
| 858818c17e |
6 changed files with 150 additions and 84 deletions
|
|
@ -19,14 +19,14 @@ import (
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
db *sqlc.Queries
|
db *sqlc.Queries
|
||||||
// publisher *rmq.Publisher
|
ImageServerURL string
|
||||||
RPCclient *rmq.RPCClient
|
RPCclient *rmq.RPCClient
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServer(db *sqlc.Queries, rpcclient *rmq.RPCClient) *Server {
|
func NewServer(db *sqlc.Queries, ImageServerURL string, rpcclient *rmq.RPCClient) *Server {
|
||||||
return &Server{
|
return &Server{
|
||||||
db: db,
|
db: db,
|
||||||
// publisher: publisher,
|
ImageServerURL: ImageServerURL,
|
||||||
RPCclient: rpcclient,
|
RPCclient: rpcclient,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,32 +3,35 @@ package handlers
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/jpeg"
|
"image/jpeg"
|
||||||
"image/png"
|
"image/png"
|
||||||
"io"
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
oapi "nyanimedb/api"
|
oapi "nyanimedb/api"
|
||||||
"os"
|
sqlc "nyanimedb/sql"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/disintegration/imaging"
|
"github.com/disintegration/imaging"
|
||||||
|
"github.com/jackc/pgx/v5/pgconn"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.org/x/image/webp"
|
"golang.org/x/image/webp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PostMediaUpload implements oapi.StrictServerInterface.
|
// PostMediaUpload implements oapi.StrictServerInterface.
|
||||||
func (s *Server) PostMediaUpload(ctx context.Context, request oapi.PostMediaUploadRequestObject) (oapi.PostMediaUploadResponseObject, error) {
|
func (s *Server) PostMediaUpload(ctx context.Context, request oapi.PostMediaUploadRequestObject) (oapi.PostMediaUploadResponseObject, error) {
|
||||||
// Получаем multipart body
|
// 1. Получаем multipart body
|
||||||
mp := request.MultipartBody
|
mp := request.MultipartBody
|
||||||
if mp == nil {
|
if mp == nil {
|
||||||
log.Errorf("PostMedia without body")
|
log.Errorf("PostMedia without body")
|
||||||
return oapi.PostMediaUpload400JSONResponse("Multipart body is required"), nil
|
return oapi.PostMediaUpload400JSONResponse("Multipart body is required"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Парсим первую часть (предполагаем, что файл в поле "file")
|
|
||||||
part, err := mp.NextPart()
|
part, err := mp.NextPart()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("PostMedia without file")
|
log.Errorf("PostMedia without file")
|
||||||
|
|
@ -36,8 +39,6 @@ func (s *Server) PostMediaUpload(ctx context.Context, request oapi.PostMediaUplo
|
||||||
}
|
}
|
||||||
defer part.Close()
|
defer part.Close()
|
||||||
|
|
||||||
// Читаем ВЕСЬ файл в память (для небольших изображений — нормально)
|
|
||||||
// Если файлы могут быть большими — используйте лимитированный буфер (см. ниже)
|
|
||||||
data, err := io.ReadAll(part)
|
data, err := io.ReadAll(part)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("PostMedia cannot read file")
|
log.Errorf("PostMedia cannot read file")
|
||||||
|
|
@ -45,97 +46,142 @@ func (s *Server) PostMediaUpload(ctx context.Context, request oapi.PostMediaUplo
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(data) == 0 {
|
if len(data) == 0 {
|
||||||
log.Errorf("PostMedia empty file")
|
|
||||||
return oapi.PostMediaUpload400JSONResponse("Empty file"), nil
|
return oapi.PostMediaUpload400JSONResponse("Empty file"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверка MIME по первым 512 байтам
|
// 2. Проверка и декодирование (оставляем как было)
|
||||||
mimeType := http.DetectContentType(data)
|
mimeType := http.DetectContentType(data)
|
||||||
if mimeType != "image/jpeg" && mimeType != "image/png" && mimeType != "image/webp" {
|
|
||||||
log.Errorf("PostMedia bad type")
|
|
||||||
return oapi.PostMediaUpload400JSONResponse("Bad data type"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Декодируем изображение из буфера
|
|
||||||
var img image.Image
|
var img image.Image
|
||||||
switch mimeType {
|
switch mimeType {
|
||||||
case "image/jpeg":
|
case "image/jpeg":
|
||||||
{
|
|
||||||
img, err = jpeg.Decode(bytes.NewReader(data))
|
img, err = jpeg.Decode(bytes.NewReader(data))
|
||||||
if err != nil {
|
|
||||||
log.Errorf("PostMedia cannot decode file: %v", err)
|
|
||||||
return oapi.PostMediaUpload500Response{}, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "image/png":
|
case "image/png":
|
||||||
{
|
|
||||||
img, err = png.Decode(bytes.NewReader(data))
|
img, err = png.Decode(bytes.NewReader(data))
|
||||||
if err != nil {
|
|
||||||
log.Errorf("PostMedia cannot decode file: %v", err)
|
|
||||||
return oapi.PostMediaUpload500Response{}, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "image/webp":
|
case "image/webp":
|
||||||
{
|
|
||||||
img, err = webp.Decode(bytes.NewReader(data))
|
img, err = webp.Decode(bytes.NewReader(data))
|
||||||
if err != nil {
|
default:
|
||||||
log.Errorf("PostMedia cannot decode file: %v", err)
|
log.Errorf("PostMedia unsupported type: %s", mimeType)
|
||||||
return oapi.PostMediaUpload500Response{}, nil
|
return oapi.PostMediaUpload400JSONResponse("Unsupported image type"), nil
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("PostMedia decode error: %v", err)
|
||||||
|
return oapi.PostMediaUpload500Response{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Перекодируем в PNG перед отправкой
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
err = imaging.Encode(&buf, img, imaging.PNG)
|
if err := imaging.Encode(&buf, img, imaging.PNG); err != nil {
|
||||||
if err != nil {
|
log.Errorf("PostMedia encode error: %v", err)
|
||||||
log.Errorf("PostMedia failed to re-encode JPEG: %v", err)
|
|
||||||
return oapi.PostMediaUpload500Response{}, nil
|
return oapi.PostMediaUpload500Response{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: to delete
|
// 4. Подготавливаем запрос к внешнему хранилищу (Image Server)
|
||||||
filename := part.FileName()
|
body := &bytes.Buffer{}
|
||||||
if filename == "" {
|
writer := multipart.NewWriter(body)
|
||||||
filename = "upload_" + generateRandomHex(8) + ".jpg"
|
|
||||||
|
// Создаем часть с файлом
|
||||||
|
partWriter, err := writer.CreateFormFile("file", "image.png")
|
||||||
|
if err != nil {
|
||||||
|
return oapi.PostMediaUpload500Response{}, nil
|
||||||
|
}
|
||||||
|
if _, err := io.Copy(partWriter, &buf); err != nil {
|
||||||
|
return oapi.PostMediaUpload500Response{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем subdir
|
||||||
|
_ = writer.WriteField("subdir", "posters")
|
||||||
|
writer.Close()
|
||||||
|
|
||||||
|
// Формируем полный URL (s.AppConfig.ImageSocket теперь base_url, например "http://storage.local")
|
||||||
|
// Убедимся, что путь не дублирует слэши
|
||||||
|
uploadURL := strings.TrimSuffix(s.ImageServerURL, "/") + "/upload"
|
||||||
|
|
||||||
|
// 5. Отправляем обычный HTTP POST
|
||||||
|
reqUpstream, err := http.NewRequestWithContext(ctx, "POST", uploadURL, body)
|
||||||
|
if err != nil {
|
||||||
|
return oapi.PostMediaUpload500Response{}, nil
|
||||||
|
}
|
||||||
|
reqUpstream.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(reqUpstream)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("PostMedia upstream request failed: %v", err)
|
||||||
|
return oapi.PostMediaUpload500Response{}, nil
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
log.Errorf("PostMedia storage returned status: %d", resp.StatusCode)
|
||||||
|
return oapi.PostMediaUpload500Response{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Получаем путь из ответа хранилища
|
||||||
|
var storageResp struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&storageResp); err != nil {
|
||||||
|
return oapi.PostMediaUpload500Response{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// В storageResp.Path теперь лежит что-то вроде "posters/a1/b2/hash.png"
|
||||||
|
log.Infof("Successfully uploaded: %s", storageResp.Path)
|
||||||
|
|
||||||
|
params := sqlc.CreateImageParams{
|
||||||
|
StorageType: sqlc.StorageTypeTLocal,
|
||||||
|
ImagePath: storageResp.Path,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: param for local/s3 case
|
||||||
|
_image, err := s.db.CreateImage(ctx, params)
|
||||||
|
if err != nil {
|
||||||
|
var pgErr *pgconn.PgError
|
||||||
|
if errors.As(err, &pgErr) {
|
||||||
|
if pgErr.Code == pgErrDuplicateKey { //duplicate key value
|
||||||
|
log.Errorf("%v", err)
|
||||||
|
|
||||||
|
_image, err = s.db.GetImageByPath(ctx, storageResp.Path)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("%v", err)
|
||||||
|
return oapi.PostMediaUpload500Response{}, nil
|
||||||
|
}
|
||||||
|
image, err := mapImage(_image)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("%v", err)
|
||||||
|
return oapi.PostMediaUpload500Response{}, nil
|
||||||
|
}
|
||||||
|
return oapi.PostMediaUpload200JSONResponse(*image), nil
|
||||||
} else {
|
} else {
|
||||||
filename = sanitizeFilename(filename)
|
log.Errorf("%v", err)
|
||||||
if !strings.HasSuffix(strings.ToLower(filename), ".png") {
|
return oapi.PostMediaUpload500Response{}, nil
|
||||||
filename += ".png"
|
}
|
||||||
|
} else {
|
||||||
|
log.Errorf("%v", err)
|
||||||
|
return oapi.PostMediaUpload500Response{}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: пойти на хуй ( вызвать файловую помойку)
|
image, err := mapImage(_image)
|
||||||
os.Mkdir("uploads", 0644)
|
|
||||||
err = os.WriteFile(filepath.Join("./uploads", filename), buf.Bytes(), 0644)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("PostMedia failed to write: %v", err)
|
log.Errorf("%v", err)
|
||||||
return oapi.PostMediaUpload500Response{}, nil
|
return oapi.PostMediaUpload500Response{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return oapi.PostMediaUpload200JSONResponse{}, nil
|
return oapi.PostMediaUpload200JSONResponse(*image), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Вспомогательные функции — как раньше
|
func mapImage(_image sqlc.Image) (*oapi.Image, error) {
|
||||||
func generateRandomHex(n int) string {
|
|
||||||
b := make([]byte, n)
|
sType, err := sql2StorageType(&_image.StorageType)
|
||||||
for i := range b {
|
if err != nil {
|
||||||
b[i] = byte('a' + (i % 16))
|
return nil, fmt.Errorf("mapImage: %v", err)
|
||||||
}
|
|
||||||
return fmt.Sprintf("%x", b)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func sanitizeFilename(name string) string {
|
image := oapi.Image{
|
||||||
var clean strings.Builder
|
Id: &_image.ID,
|
||||||
for _, r := range name {
|
ImagePath: &_image.ImagePath,
|
||||||
if (r >= 'a' && r <= 'z') ||
|
StorageType: sType,
|
||||||
(r >= 'A' && r <= 'Z') ||
|
|
||||||
(r >= '0' && r <= '9') ||
|
|
||||||
r == '.' || r == '_' || r == '-' {
|
|
||||||
clean.WriteRune(r)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
s := clean.String()
|
return &image, nil
|
||||||
if s == "" {
|
|
||||||
return "file"
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
oapi "nyanimedb/api"
|
oapi "nyanimedb/api"
|
||||||
|
|
||||||
handlers "nyanimedb/modules/backend/handlers"
|
handlers "nyanimedb/modules/backend/handlers"
|
||||||
middleware "nyanimedb/modules/backend/middlewares"
|
middleware "nyanimedb/modules/backend/middlewares"
|
||||||
"nyanimedb/modules/backend/rmq"
|
"nyanimedb/modules/backend/rmq"
|
||||||
|
|
@ -61,7 +62,7 @@ func main() {
|
||||||
|
|
||||||
rpcClient := rmq.NewRPCClient(rmqConn, 30*time.Second)
|
rpcClient := rmq.NewRPCClient(rmqConn, 30*time.Second)
|
||||||
|
|
||||||
server := handlers.NewServer(queries, rpcClient)
|
server := handlers.NewServer(queries, AppConfig.ImageServerURL, rpcClient)
|
||||||
|
|
||||||
r.Use(cors.New(cors.Config{
|
r.Use(cors.New(cors.Config{
|
||||||
AllowOrigins: []string{AppConfig.ServiceAddress},
|
AllowOrigins: []string{AppConfig.ServiceAddress},
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,11 @@ SELECT id, storage_type, image_path
|
||||||
FROM images
|
FROM images
|
||||||
WHERE id = sqlc.arg('illust_id')::bigint;
|
WHERE id = sqlc.arg('illust_id')::bigint;
|
||||||
|
|
||||||
|
-- name: GetImageByPath :one
|
||||||
|
SELECT id, storage_type, image_path
|
||||||
|
FROM images
|
||||||
|
WHERE image_path = sqlc.arg('illust_path')::text;
|
||||||
|
|
||||||
-- name: CreateImage :one
|
-- name: CreateImage :one
|
||||||
INSERT INTO images (storage_type, image_path)
|
INSERT INTO images (storage_type, image_path)
|
||||||
VALUES ($1, $2)
|
VALUES ($1, $2)
|
||||||
|
|
|
||||||
|
|
@ -8,4 +8,5 @@ type Config struct {
|
||||||
LogLevel string `toml:"LogLevel" env:"LOG_LEVEL"`
|
LogLevel string `toml:"LogLevel" env:"LOG_LEVEL"`
|
||||||
RmqURL string `toml:"RabbitMQUrl" env:"RABBITMQ_URL"`
|
RmqURL string `toml:"RabbitMQUrl" env:"RABBITMQ_URL"`
|
||||||
AuthEnabled string `toml:"AuthEnabled" env:"AUTH_ENABLED"`
|
AuthEnabled string `toml:"AuthEnabled" env:"AUTH_ENABLED"`
|
||||||
|
ImageServerURL string `toml:"ImageServerURL" env:"IMAGES_BASE_URL"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,19 @@ func (q *Queries) GetImageByID(ctx context.Context, illustID int64) (Image, erro
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getImageByPath = `-- name: GetImageByPath :one
|
||||||
|
SELECT id, storage_type, image_path
|
||||||
|
FROM images
|
||||||
|
WHERE image_path = $1::text
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetImageByPath(ctx context.Context, illustPath string) (Image, error) {
|
||||||
|
row := q.db.QueryRow(ctx, getImageByPath, illustPath)
|
||||||
|
var i Image
|
||||||
|
err := row.Scan(&i.ID, &i.StorageType, &i.ImagePath)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const getReviewByID = `-- name: GetReviewByID :one
|
const getReviewByID = `-- name: GetReviewByID :one
|
||||||
|
|
||||||
SELECT id, data, rating, user_id, title_id, created_at
|
SELECT id, data, rating, user_id, title_id, created_at
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue