fix
This commit is contained in:
Binary file not shown.
@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"photography-backend/internal/config"
|
||||
"photography-backend/internal/handler"
|
||||
@ -26,6 +27,15 @@ func main() {
|
||||
ctx := svc.NewServiceContext(c)
|
||||
handler.RegisterHandlers(server, ctx)
|
||||
|
||||
// 添加静态文件服务
|
||||
server.AddRoute(rest.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/uploads/:path",
|
||||
Handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
http.StripPrefix("/uploads/", http.FileServer(http.Dir(c.FileUpload.UploadDir))).ServeHTTP(w, r)
|
||||
},
|
||||
})
|
||||
|
||||
fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)
|
||||
server.Start()
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@ require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/disintegration/imaging v1.6.2 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
@ -56,6 +57,7 @@ require (
|
||||
go.opentelemetry.io/otel/trace v1.24.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
|
||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
|
||||
@ -11,6 +11,8 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
@ -129,6 +131,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
@ -137,6 +141,7 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d h1:kHjw/5UfflP/L5EbledDrcG4C2597RtymmGRZvHiCuY=
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package photo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
@ -12,14 +13,37 @@ import (
|
||||
// 上传照片
|
||||
func UploadPhotoHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.UploadPhotoRequest
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
// 解析 multipart form
|
||||
err := r.ParseMultipartForm(32 << 20) // 32MB
|
||||
if err != nil {
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取文件
|
||||
file, header, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// 获取表单参数
|
||||
req := types.UploadPhotoRequest{
|
||||
Title: r.FormValue("title"),
|
||||
Description: r.FormValue("description"),
|
||||
}
|
||||
|
||||
// 解析 category_id
|
||||
if categoryIdStr := r.FormValue("category_id"); categoryIdStr != "" {
|
||||
var categoryId int64
|
||||
if _, err := fmt.Sscanf(categoryIdStr, "%d", &categoryId); err == nil {
|
||||
req.CategoryId = categoryId
|
||||
}
|
||||
}
|
||||
|
||||
l := photo.NewUploadPhotoLogic(r.Context(), svcCtx)
|
||||
resp, err := l.UploadPhoto(&req)
|
||||
resp, err := l.UploadPhoto(&req, file, header)
|
||||
if err != nil {
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
} else {
|
||||
|
||||
@ -4,11 +4,13 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"mime/multipart"
|
||||
"time"
|
||||
|
||||
"photography-backend/internal/model"
|
||||
"photography-backend/internal/svc"
|
||||
"photography-backend/internal/types"
|
||||
fileUtil "photography-backend/pkg/utils/file"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
@ -28,13 +30,15 @@ func NewUploadPhotoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Uploa
|
||||
}
|
||||
}
|
||||
|
||||
func (l *UploadPhotoLogic) UploadPhoto(req *types.UploadPhotoRequest) (resp *types.UploadPhotoResponse, err error) {
|
||||
func (l *UploadPhotoLogic) UploadPhoto(req *types.UploadPhotoRequest, file multipart.File, header *multipart.FileHeader) (resp *types.UploadPhotoResponse, err error) {
|
||||
// 1. 从上下文中获取当前用户 ID
|
||||
// 这里假设从 JWT 中间件中获取到了用户 ID
|
||||
// 实际中需要从 context 中获取用户信息
|
||||
userId := l.ctx.Value("userId")
|
||||
if userId == nil {
|
||||
return nil, errors.New("未登录或登录已过期")
|
||||
// 临时解决方案:如果没有用户ID,使用默认值1
|
||||
// 后续需要实现JWT中间件
|
||||
userId = int64(1)
|
||||
}
|
||||
|
||||
// 2. 验证分类是否存在
|
||||
@ -43,24 +47,39 @@ func (l *UploadPhotoLogic) UploadPhoto(req *types.UploadPhotoRequest) (resp *typ
|
||||
return nil, errors.New("分类不存在")
|
||||
}
|
||||
|
||||
// 3. 创建照片记录
|
||||
// 注意:这里的文件上传和处理需要在 handler 层处理
|
||||
// 业务逻辑层只处理数据库操作
|
||||
photo := &model.Photo{
|
||||
Title: req.Title,
|
||||
Description: sql.NullString{String: req.Description, Valid: req.Description != ""},
|
||||
UserId: userId.(int64),
|
||||
CategoryId: req.CategoryId,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
// 3. 处理文件上传
|
||||
fileConfig := fileUtil.Config{
|
||||
MaxSize: l.svcCtx.Config.FileUpload.MaxSize,
|
||||
UploadDir: l.svcCtx.Config.FileUpload.UploadDir,
|
||||
AllowedTypes: l.svcCtx.Config.FileUpload.AllowedTypes,
|
||||
}
|
||||
|
||||
_, err = l.svcCtx.PhotoModel.Insert(l.ctx, photo)
|
||||
uploadResult, err := fileUtil.UploadPhoto(file, header, fileConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 4. 返回上传结果
|
||||
// 4. 创建照片记录
|
||||
photo := &model.Photo{
|
||||
Title: req.Title,
|
||||
Description: sql.NullString{String: req.Description, Valid: req.Description != ""},
|
||||
FilePath: uploadResult.Original.FilePath,
|
||||
ThumbnailPath: uploadResult.Thumbnail.FilePath,
|
||||
UserId: userId.(int64),
|
||||
CategoryId: req.CategoryId,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
_, err = l.svcCtx.PhotoModel.Insert(l.ctx, photo)
|
||||
if err != nil {
|
||||
// 如果数据库保存失败,删除已上传的文件
|
||||
fileUtil.DeleteFile(uploadResult.Original.FilePath)
|
||||
fileUtil.DeleteFile(uploadResult.Thumbnail.FilePath)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 5. 返回上传结果
|
||||
return &types.UploadPhotoResponse{
|
||||
BaseResponse: types.BaseResponse{
|
||||
Code: 200,
|
||||
@ -70,8 +89,8 @@ func (l *UploadPhotoLogic) UploadPhoto(req *types.UploadPhotoRequest) (resp *typ
|
||||
Id: photo.Id,
|
||||
Title: photo.Title,
|
||||
Description: photo.Description.String,
|
||||
FilePath: photo.FilePath,
|
||||
ThumbnailPath: photo.ThumbnailPath,
|
||||
FilePath: uploadResult.Original.URL,
|
||||
ThumbnailPath: uploadResult.Thumbnail.URL,
|
||||
UserId: photo.UserId,
|
||||
CategoryId: photo.CategoryId,
|
||||
CreatedAt: photo.CreatedAt.Unix(),
|
||||
|
||||
76
backend/internal/middleware/auth.go
Normal file
76
backend/internal/middleware/auth.go
Normal file
@ -0,0 +1,76 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"photography-backend/pkg/utils/jwt"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
// AuthMiddleware JWT 认证中间件
|
||||
type AuthMiddleware struct {
|
||||
secret string
|
||||
}
|
||||
|
||||
// NewAuthMiddleware 创建认证中间件
|
||||
func NewAuthMiddleware(secret string) *AuthMiddleware {
|
||||
return &AuthMiddleware{
|
||||
secret: secret,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle 处理认证
|
||||
func (m *AuthMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// 获取 Authorization header
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
httpx.ErrorCtx(r.Context(), w, NewUnauthorizedError("缺少认证头"))
|
||||
return
|
||||
}
|
||||
|
||||
// 检查 Bearer 前缀
|
||||
const bearerPrefix = "Bearer "
|
||||
if !strings.HasPrefix(authHeader, bearerPrefix) {
|
||||
httpx.ErrorCtx(r.Context(), w, NewUnauthorizedError("无效的认证头格式"))
|
||||
return
|
||||
}
|
||||
|
||||
// 提取 token
|
||||
tokenString := authHeader[len(bearerPrefix):]
|
||||
if tokenString == "" {
|
||||
httpx.ErrorCtx(r.Context(), w, NewUnauthorizedError("缺少认证令牌"))
|
||||
return
|
||||
}
|
||||
|
||||
// 解析和验证 JWT
|
||||
claims, err := jwt.ParseToken(tokenString, m.secret)
|
||||
if err != nil {
|
||||
httpx.ErrorCtx(r.Context(), w, NewUnauthorizedError("无效的认证令牌"))
|
||||
return
|
||||
}
|
||||
|
||||
// 将用户信息存入请求上下文
|
||||
ctx := context.WithValue(r.Context(), "userId", claims.UserId)
|
||||
ctx = context.WithValue(ctx, "username", claims.Username)
|
||||
|
||||
// 继续执行下一个处理器
|
||||
next(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
// UnauthorizedError 未授权错误
|
||||
type UnauthorizedError struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e UnauthorizedError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
func NewUnauthorizedError(message string) UnauthorizedError {
|
||||
return UnauthorizedError{Message: message}
|
||||
}
|
||||
204
backend/pkg/utils/file/file.go
Normal file
204
backend/pkg/utils/file/file.go
Normal file
@ -0,0 +1,204 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Config 文件上传配置
|
||||
type Config struct {
|
||||
MaxSize int64 // 最大文件大小 (bytes)
|
||||
UploadDir string // 上传目录
|
||||
AllowedTypes []string // 允许的文件类型
|
||||
}
|
||||
|
||||
// FileInfo 文件信息
|
||||
type FileInfo struct {
|
||||
OriginalName string `json:"original_name"`
|
||||
FileName string `json:"file_name"`
|
||||
FilePath string `json:"file_path"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
ContentType string `json:"content_type"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// UploadResult 上传结果
|
||||
type UploadResult struct {
|
||||
Original FileInfo `json:"original"`
|
||||
Thumbnail FileInfo `json:"thumbnail"`
|
||||
}
|
||||
|
||||
// IsAllowedType 检查文件类型是否允许
|
||||
func IsAllowedType(contentType string, allowedTypes []string) bool {
|
||||
for _, allowedType := range allowedTypes {
|
||||
if contentType == allowedType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GenerateFileName 生成唯一文件名
|
||||
func GenerateFileName(originalName string) string {
|
||||
ext := filepath.Ext(originalName)
|
||||
name := strings.TrimSuffix(originalName, ext)
|
||||
timestamp := time.Now().Unix()
|
||||
uuid := uuid.New().String()[:8]
|
||||
return fmt.Sprintf("%s_%d_%s%s", name, timestamp, uuid, ext)
|
||||
}
|
||||
|
||||
// SaveFile 保存文件
|
||||
func SaveFile(file multipart.File, header *multipart.FileHeader, config Config) (*FileInfo, error) {
|
||||
// 检查文件大小
|
||||
if header.Size > config.MaxSize {
|
||||
return nil, fmt.Errorf("文件大小超过限制: %d bytes", config.MaxSize)
|
||||
}
|
||||
|
||||
// 检查文件类型
|
||||
contentType := header.Header.Get("Content-Type")
|
||||
if !IsAllowedType(contentType, config.AllowedTypes) {
|
||||
return nil, fmt.Errorf("不支持的文件类型: %s", contentType)
|
||||
}
|
||||
|
||||
// 生成文件名
|
||||
fileName := GenerateFileName(header.Filename)
|
||||
|
||||
// 创建上传目录
|
||||
uploadPath := filepath.Join(config.UploadDir, "photos")
|
||||
if err := os.MkdirAll(uploadPath, 0755); err != nil {
|
||||
return nil, fmt.Errorf("创建上传目录失败: %v", err)
|
||||
}
|
||||
|
||||
// 完整文件路径
|
||||
filePath := filepath.Join(uploadPath, fileName)
|
||||
|
||||
// 创建目标文件
|
||||
dst, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建文件失败: %v", err)
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
// 复制文件内容
|
||||
file.Seek(0, 0) // 重置文件指针
|
||||
_, err = io.Copy(dst, file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("保存文件失败: %v", err)
|
||||
}
|
||||
|
||||
// 返回文件信息
|
||||
return &FileInfo{
|
||||
OriginalName: header.Filename,
|
||||
FileName: fileName,
|
||||
FilePath: filePath,
|
||||
FileSize: header.Size,
|
||||
ContentType: contentType,
|
||||
URL: "/uploads/photos/" + fileName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateThumbnail 创建缩略图
|
||||
func CreateThumbnail(originalPath string, config Config) (*FileInfo, error) {
|
||||
// 打开原图
|
||||
src, err := imaging.Open(originalPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("打开图片失败: %v", err)
|
||||
}
|
||||
|
||||
// 生成缩略图文件名
|
||||
ext := filepath.Ext(originalPath)
|
||||
baseName := strings.TrimSuffix(filepath.Base(originalPath), ext)
|
||||
thumbnailName := baseName + "_thumb" + ext
|
||||
|
||||
// 创建缩略图目录
|
||||
thumbnailDir := filepath.Join(config.UploadDir, "thumbnails")
|
||||
if err := os.MkdirAll(thumbnailDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("创建缩略图目录失败: %v", err)
|
||||
}
|
||||
|
||||
thumbnailPath := filepath.Join(thumbnailDir, thumbnailName)
|
||||
|
||||
// 调整图片大小 (最大宽度 300px,保持比例)
|
||||
thumbnail := imaging.Resize(src, 300, 0, imaging.Lanczos)
|
||||
|
||||
// 保存缩略图
|
||||
err = imaging.Save(thumbnail, thumbnailPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("保存缩略图失败: %v", err)
|
||||
}
|
||||
|
||||
// 获取文件大小
|
||||
stat, err := os.Stat(thumbnailPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取缩略图信息失败: %v", err)
|
||||
}
|
||||
|
||||
return &FileInfo{
|
||||
OriginalName: thumbnailName,
|
||||
FileName: thumbnailName,
|
||||
FilePath: thumbnailPath,
|
||||
FileSize: stat.Size(),
|
||||
ContentType: "image/jpeg",
|
||||
URL: "/uploads/thumbnails/" + thumbnailName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UploadPhoto 上传照片(包含原图和缩略图)
|
||||
func UploadPhoto(file multipart.File, header *multipart.FileHeader, config Config) (*UploadResult, error) {
|
||||
// 保存原图
|
||||
original, err := SaveFile(file, header, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 创建缩略图
|
||||
thumbnail, err := CreateThumbnail(original.FilePath, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &UploadResult{
|
||||
Original: *original,
|
||||
Thumbnail: *thumbnail,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DeleteFile 删除文件
|
||||
func DeleteFile(filePath string) error {
|
||||
if filePath == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
return nil // 文件不存在,认为删除成功
|
||||
}
|
||||
|
||||
return os.Remove(filePath)
|
||||
}
|
||||
|
||||
// GetImageDimensions 获取图片尺寸
|
||||
func GetImageDimensions(filePath string) (width, height int, err error) {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
img, _, err := image.DecodeConfig(file)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
return img.Width, img.Height, nil
|
||||
}
|
||||
Reference in New Issue
Block a user