上传图片

dev
truthhun 2 years ago
parent 3a0914a4a6
commit 01ff237e03

@ -2,19 +2,35 @@ package biz
import (
"context"
"fmt"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"strings"
"time"
pb "moredoc/api/v1"
"moredoc/middleware/auth"
"moredoc/model"
"moredoc/util"
"moredoc/util/filetil"
"github.com/gin-gonic/gin"
"github.com/gofrs/uuid"
"go.uber.org/zap"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
)
type ginResponse struct {
Error string `json:"error,omitempty"`
Message string `json:"message,omitempty"`
Code int `json:"code,omitempty"`
Data interface{} `json:"data,omitempty"`
}
type AttachmentAPIService struct {
pb.UnimplementedAttachmentAPIServer
dbModel *model.DBModel
@ -27,17 +43,13 @@ func NewAttachmentAPIService(dbModel *model.DBModel, logger *zap.Logger) (servic
// checkPermission 检查用户权限
func (s *AttachmentAPIService) checkPermission(ctx context.Context) (userClaims *auth.UserClaims, err error) {
var ok bool
userClaims, ok = ctx.Value(auth.CtxKeyUserClaims).(*auth.UserClaims)
if !ok {
return nil, status.Errorf(codes.Unauthenticated, ErrorMessageInvalidToken)
}
return checkGRPCPermission(s.dbModel, ctx)
}
fullMethod, _ := ctx.Value(auth.CtxKeyFullMethod).(string)
if yes := s.dbModel.CheckPermissionByUserId(userClaims.UserId, fullMethod); !yes {
return nil, status.Errorf(codes.PermissionDenied, ErrorMessagePermissionDenied)
}
return
// checkPermission 检查用户权限
// 文件等的上传,也要验证用户是否有权限,无论是否是管理员
func (s *AttachmentAPIService) checkGinPermission(ctx *gin.Context) (userClaims *auth.UserClaims, statusCode int, err error) {
return checkGinPermission(s.dbModel, ctx)
}
// UpdateAttachment 更新附件。只允许更新附件名称、是否合法以及描述字段
@ -159,22 +171,132 @@ func (s *AttachmentAPIService) ListAttachment(ctx context.Context, req *pb.ListA
return &pb.ListAttachmentReply{Total: total, Attachment: pbAttachments}, nil
}
// 上传头像
func (s *AttachmentAPIService) UploadAvatar() {
// 上传文档
func (s *AttachmentAPIService) UploadDocument(ctx *gin.Context) {
}
// 上传横幅
func (s *AttachmentAPIService) UploadBanner() {
// UploadAvatar 上传头像
func (s *AttachmentAPIService) UploadAvatar(ctx *gin.Context) {
s.uploadImage(ctx, model.AttachmentTypeAvatar)
}
// UploadBanner 上传横幅创建横幅的时候要根据附件id更新附件的type_id字段
func (s *AttachmentAPIService) UploadBanner(ctx *gin.Context) {
s.uploadImage(ctx, model.AttachmentTypeBanner)
}
// 上传文档
func (s *AttachmentAPIService) UploadDocument() {
// 上传文档分类封面
func (s *AttachmentAPIService) UploadCategoryCover(ctx *gin.Context) {
s.uploadImage(ctx, model.AttachmentTypeCategoryCover)
}
func (s *AttachmentAPIService) uploadImage(ctx *gin.Context, attachmentType int) {
name := "file"
switch attachmentType {
case model.AttachmentTypeBanner:
name = "banner"
case model.AttachmentTypeAvatar:
name = "avatar"
case model.AttachmentTypeCategoryCover:
name = "cover"
}
userCliams, statusCodes, err := s.checkGinPermission(ctx)
if err != nil {
ctx.JSON(statusCodes, ginResponse{Code: statusCodes, Message: err.Error(), Error: err.Error()})
return
}
// 验证文件是否是图片
fileHeader, err := ctx.FormFile(name)
if err != nil {
ctx.JSON(http.StatusBadRequest, ginResponse{Code: http.StatusBadRequest, Message: err.Error(), Error: err.Error()})
return
}
ext := strings.ToLower(filepath.Ext(fileHeader.Filename))
if !filetil.IsImage(ext) {
message := "请上传图片格式文件,支持.jpg、.jpeg和.png格式图片"
ctx.JSON(http.StatusBadRequest, ginResponse{Code: http.StatusBadRequest, Message: message, Error: message})
return
}
attachment, err := s.saveFile(ctx, fileHeader)
if err != nil {
ctx.JSON(http.StatusBadRequest, ginResponse{Code: http.StatusBadRequest, Message: err.Error(), Error: err.Error()})
return
}
attachment.Type = model.AttachmentTypeAvatar
attachment.UserId = userCliams.UserId
if attachmentType == model.AttachmentTypeAvatar {
attachment.TypeId = userCliams.UserId
// 更新用户头像信息
err = s.dbModel.UpdateUser(&model.User{Id: userCliams.UserId, Avatar: attachment.Path}, "avatar")
if err != nil {
ctx.JSON(http.StatusInternalServerError, ginResponse{Code: http.StatusInternalServerError, Message: err.Error(), Error: err.Error()})
}
}
// 保存附件信息
err = s.dbModel.CreateAttachment(attachment)
if err != nil {
ctx.JSON(http.StatusInternalServerError, ginResponse{Code: http.StatusInternalServerError, Message: err.Error(), Error: err.Error()})
return
}
ctx.JSON(http.StatusOK, ginResponse{Code: http.StatusOK, Message: "上传成功", Data: attachment})
}
// 上传文档分类封面
func (s *AttachmentAPIService) UploadCategoryCover() {
// saveFile 保存文件。文件以md5值命名以及存储
// 同时,返回附件信息
func (s *AttachmentAPIService) saveFile(ctx *gin.Context, fileHeader *multipart.FileHeader) (attachment *model.Attachment, err error) {
cacheDir := fmt.Sprintf("cache/%s", time.Now().Format("2006/01/02"))
os.MkdirAll(cacheDir, os.ModePerm)
ext := strings.ToLower(filepath.Ext(fileHeader.Filename))
cachePath := fmt.Sprintf("%s/%s%s", cacheDir, uuid.Must(uuid.NewV4()).String(), ext)
defer func() {
if err != nil {
os.Remove(cachePath)
}
}()
// 保存到临时文件
err = ctx.SaveUploadedFile(fileHeader, cachePath)
if err != nil {
s.logger.Error("SaveUploadedFile", zap.Error(err), zap.String("filename", fileHeader.Filename), zap.String("cachePath", cachePath))
return
}
// 获取文件md5值
md5hash, errHash := filetil.GetFileMD5(cachePath)
if errHash != nil {
err = errHash
return
}
savePath := fmt.Sprintf("uploads/%s/%s%s", strings.Join(strings.Split(md5hash, "")[0:5], "/"), md5hash, ext)
os.MkdirAll(filepath.Dir(savePath), os.ModePerm)
err = os.Rename(cachePath, savePath)
if err != nil {
s.logger.Error("Rename", zap.Error(err), zap.String("cachePath", cachePath), zap.String("savePath", savePath))
return
}
attachment = &model.Attachment{
Size: fileHeader.Size,
Name: fileHeader.Filename,
Ip: ctx.ClientIP(),
Ext: ext,
IsApproved: 1, // 默认都是合法的
Hash: md5hash,
Path: "/" + savePath,
}
// 对于图片,直接获取图片的宽高
if filetil.IsImage(ext) {
attachment.Width, attachment.Height, _ = filetil.GetImageSize(cachePath)
}
return
}

@ -26,17 +26,7 @@ func NewFriendlinkAPIService(dbModel *model.DBModel, logger *zap.Logger) (servic
// checkPermission 检查用户权限
func (s *FriendlinkAPIService) checkPermission(ctx context.Context) (userClaims *auth.UserClaims, err error) {
var ok bool
userClaims, ok = ctx.Value(auth.CtxKeyUserClaims).(*auth.UserClaims)
if !ok {
return nil, status.Errorf(codes.Unauthenticated, ErrorMessageInvalidToken)
}
fullMethod, _ := ctx.Value(auth.CtxKeyFullMethod).(string)
if yes := s.dbModel.CheckPermissionByUserId(userClaims.UserId, fullMethod); !yes {
return nil, status.Errorf(codes.PermissionDenied, ErrorMessagePermissionDenied)
}
return
return checkGRPCPermission(s.dbModel, ctx)
}
// CreateFriendlink 创建友情链接,需要鉴权

@ -25,6 +25,10 @@ func NewGroupAPIService(dbModel *model.DBModel, logger *zap.Logger) (service *Gr
return &GroupAPIService{dbModel: dbModel, logger: logger.Named("GroupAPIService")}
}
func (s *GroupAPIService) checkPermission(ctx context.Context) (userClaims *auth.UserClaims, err error) {
return checkGRPCPermission(s.dbModel, ctx)
}
// CreateGroup 创建用户组
// 0. 检查用户权限
// 1. 检查用户组是否存在
@ -32,14 +36,9 @@ func NewGroupAPIService(dbModel *model.DBModel, logger *zap.Logger) (service *Gr
func (s *GroupAPIService) CreateGroup(ctx context.Context, req *pb.Group) (*pb.Group, error) {
s.logger.Debug("CreateGroup", zap.Any("req", req))
userClaims, ok := ctx.Value(auth.CtxKeyUserClaims).(*auth.UserClaims)
if !ok {
return nil, status.Errorf(codes.Unauthenticated, ErrorMessageInvalidToken)
}
fullMethod, _ := ctx.Value(auth.CtxKeyFullMethod).(string)
if yes := s.dbModel.CheckPermissionByUserId(userClaims.UserId, fullMethod); !yes {
return nil, status.Errorf(codes.PermissionDenied, ErrorMessagePermissionDenied)
_, err := s.checkPermission(ctx)
if err != nil {
return nil, err
}
existGroup, err := s.dbModel.GetGroupByTitle(req.Title)

@ -0,0 +1,42 @@
package biz
import (
"context"
"errors"
"moredoc/middleware/auth"
"moredoc/model"
"net/http"
"github.com/gin-gonic/gin"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func checkGinPermission(dbModel *model.DBModel, ctx *gin.Context) (userClaims *auth.UserClaims, statusCode int, err error) {
var ok bool
userClaims, ok = ctx.Value(auth.CtxKeyUserClaims.String()).(*auth.UserClaims)
if !ok || dbModel.IsInvalidToken(userClaims.UUID) {
statusCode = http.StatusUnauthorized
return nil, statusCode, errors.New(ErrorMessageInvalidToken)
}
if yes := dbModel.CheckPermissionByUserId(userClaims.UserId, ctx.Request.URL.Path, ctx.Request.Method); !yes {
statusCode = http.StatusForbidden
return nil, statusCode, errors.New(ErrorMessagePermissionDenied)
}
return
}
func checkGRPCPermission(dbModel *model.DBModel, ctx context.Context) (userClaims *auth.UserClaims, err error) {
var ok bool
userClaims, ok = ctx.Value(auth.CtxKeyUserClaims).(*auth.UserClaims)
if !ok || dbModel.IsInvalidToken(userClaims.UUID) {
return nil, status.Errorf(codes.Unauthenticated, ErrorMessageInvalidToken)
}
fullMethod, _ := ctx.Value(auth.CtxKeyFullMethod).(string)
if yes := dbModel.CheckPermissionByUserId(userClaims.UserId, fullMethod); !yes {
return nil, status.Errorf(codes.PermissionDenied, ErrorMessagePermissionDenied)
}
return
}

@ -24,13 +24,14 @@ require (
require (
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
golang.org/x/image v0.0.0-20190802002840-cff245a6509b // indirect
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 // indirect
google.golang.org/genproto v0.0.0-20220228195345-15d65a4533f7 // indirect
gopkg.in/go-playground/validator.v9 v9.29.1 // indirect
)
require (
github.com/alexandrevicenzi/unchained v1.3.0
github.com/disintegration/imaging v1.6.2
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-contrib/static v0.0.1

@ -35,8 +35,8 @@ type Attachment struct {
Path string `form:"path" json:"path,omitempty" gorm:"column:path;type:varchar(255);size:255;comment:文件存储路径;"`
Name string `form:"name" json:"name,omitempty" gorm:"column:name;type:varchar(255);size:255;comment:文件原名称;"`
Size int64 `form:"size" json:"size,omitempty" gorm:"column:size;type:bigint(20) unsigned;default:0;comment:文件大小;"`
Width int64 `form:"width" json:"width,omitempty" gorm:"column:width;type:bigint(20) unsigned;default:0;comment:宽度;"`
Height int64 `form:"height" json:"height,omitempty" gorm:"column:height;type:bigint(20) unsigned;default:0;comment:高度;"`
Width int `form:"width" json:"width,omitempty" gorm:"column:width;type:int(11) unsigned;default:0;comment:宽度;"`
Height int `form:"height" json:"height,omitempty" gorm:"column:height;type:int(11) unsigned;default:0;comment:高度;"`
Ext string `form:"ext" json:"ext,omitempty" gorm:"column:ext;type:varchar(32);size:32;comment:文件类型,如 .pdf 。统一处理成小写;"`
Ip string `form:"ip" json:"ip,omitempty" gorm:"column:ip;type:varchar(16);size:16;comment:上传文档的用户IP地址;"`
Description string `form:"description" json:"description,omitempty" gorm:"column:description;type:varchar(255);size:255;comment:描述、备注;"`
@ -49,7 +49,6 @@ func (Attachment) TableName() string {
}
// CreateAttachment 创建Attachment
// TODO: 创建成功之后,注意相关表统计字段数值的增减
func (m *DBModel) CreateAttachment(attachment *Attachment) (err error) {
err = m.db.Create(attachment).Error
if err != nil {

@ -74,7 +74,7 @@ func Run(cfg *conf.Config, logger *zap.Logger) {
return
}
serve.RegisterGinRouter(app)
serve.RegisterGinRouter(app, dbModel, logger, auth)
// 根目录访问静态文件,要放在 grpc 服务的前面
// 可以在 dist 目录下创建一个 index.html 文件并添加内容,然后访问 http://ip:port

@ -1,15 +1,31 @@
package serve
import (
"moredoc/biz"
"moredoc/middleware/auth"
"moredoc/model"
"net/http"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// RegisterGinRouter 注册gin路由
func RegisterGinRouter(app *gin.Engine) (err error) {
func RegisterGinRouter(app *gin.Engine, dbModel *model.DBModel, logger *zap.Logger, auth *auth.Auth) (err error) {
attachmentAPIService := biz.NewAttachmentAPIService(dbModel, logger)
app.GET("/helloworld", func(ctx *gin.Context) {
ctx.JSON(http.StatusOK, "hello world")
})
checkPermissionGroup := app.Group("/api/v1/upload")
checkPermissionGroup.Use(auth.AuthGin())
{
checkPermissionGroup.POST("avatar", attachmentAPIService.UploadAvatar)
checkPermissionGroup.POST("banner", attachmentAPIService.UploadBanner)
checkPermissionGroup.POST("document", attachmentAPIService.UploadDocument)
checkPermissionGroup.POST("category/cover", attachmentAPIService.UploadCategoryCover)
}
return
}

@ -0,0 +1,70 @@
package filetil
import (
"crypto/md5"
"encoding/hex"
"errors"
"image"
"io"
"os"
"path/filepath"
"strings"
"github.com/disintegration/imaging"
)
var imagesExt = map[string]struct{}{
".jpg": {},
".jpeg": {},
".png": {},
".gif": {},
// ".bmp": {},
// ".webp": {},
}
// IsImage 判断文件是否是图片
func IsImage(ext string) bool {
_, ok := imagesExt[ext]
return ok
}
// GetFileMD5 获取文件MD5值
func GetFileMD5(filePath string) (string, error) {
file, err := os.Open(filePath)
if err != nil {
return "", err
}
hash := md5.New()
_, _ = io.Copy(hash, file)
return hex.EncodeToString(hash.Sum(nil)), nil
}
// CropImage 居中裁剪图片
func CropImage(file string, width, height int) (err error) {
var img image.Image
img, err = imaging.Open(file)
if err != nil {
return
}
ext := strings.ToLower(filepath.Ext(file))
switch ext {
case ".jpeg", ".jpg", ".png", ".gif":
img = imaging.Fill(img, width, height, imaging.Center, imaging.CatmullRom)
default:
err = errors.New("unsupported image format")
return
}
return imaging.Save(img, file)
}
// GetImageSize 获取图片宽高信息
func GetImageSize(file string) (width, height int, err error) {
var img image.Image
img, err = imaging.Open(file)
if err != nil {
return
}
width = img.Bounds().Max.X
height = img.Bounds().Max.Y
return
}
Loading…
Cancel
Save