You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

469 lines
15 KiB

package biz
import (
"context"
2 years ago
"fmt"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"strings"
2 years ago
"time"
pb "moredoc/api/v1"
"moredoc/middleware/auth"
"moredoc/model"
"moredoc/util"
2 years ago
"moredoc/util/filetil"
2 years ago
"github.com/gin-gonic/gin"
"github.com/gofrs/uuid"
"github.com/golang-jwt/jwt"
"go.uber.org/zap"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
)
2 years ago
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
logger *zap.Logger
}
func NewAttachmentAPIService(dbModel *model.DBModel, logger *zap.Logger) (service *AttachmentAPIService) {
return &AttachmentAPIService{dbModel: dbModel, logger: logger.Named("AttachmentAPIService")}
}
// checkPermission 检查用户权限
func (s *AttachmentAPIService) checkPermission(ctx context.Context) (userClaims *auth.UserClaims, err error) {
2 years ago
return checkGRPCPermission(s.dbModel, ctx)
}
2 years ago
// checkPermission 检查用户权限
// 文件等的上传,也要验证用户是否有权限,无论是否是管理员
func (s *AttachmentAPIService) checkGinPermission(ctx *gin.Context) (userClaims *auth.UserClaims, statusCode int, err error) {
return checkGinPermission(s.dbModel, ctx)
}
func (s *AttachmentAPIService) checkLogin(ctx *gin.Context) (userClaims *auth.UserClaims, statusCode int, err error) {
return checkGinLogin(s.dbModel, ctx)
}
// UpdateAttachment 更新附件。只允许更新附件名称、是否合法以及描述字段
func (s *AttachmentAPIService) UpdateAttachment(ctx context.Context, req *pb.Attachment) (*emptypb.Empty, error) {
_, err := s.checkPermission(ctx)
if err != nil {
return nil, err
}
updateFields := []string{"name", "enable", "description"}
err = s.dbModel.UpdateAttachment(&model.Attachment{
Id: req.Id,
Name: req.Name,
Description: req.Description,
Enable: req.Enable,
}, updateFields...)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
return &emptypb.Empty{}, nil
}
func (s *AttachmentAPIService) DeleteAttachment(ctx context.Context, req *pb.DeleteAttachmentRequest) (*emptypb.Empty, error) {
_, err := s.checkPermission(ctx)
if err != nil {
return nil, err
}
err = s.dbModel.DeleteAttachment(req.Id)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
return &emptypb.Empty{}, nil
}
// GetAttachment 查询单个附件信息
func (s *AttachmentAPIService) GetAttachment(ctx context.Context, req *pb.GetAttachmentRequest) (*pb.Attachment, error) {
_, err := s.checkPermission(ctx)
if err != nil {
return nil, err
}
attachment, err := s.dbModel.GetAttachment(req.Id)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
pbAttachment := &pb.Attachment{}
util.CopyStruct(&attachment, pbAttachment)
return pbAttachment, nil
}
func (s *AttachmentAPIService) ListAttachment(ctx context.Context, req *pb.ListAttachmentRequest) (*pb.ListAttachmentReply, error) {
_, err := s.checkPermission(ctx)
if err != nil {
return nil, err
}
opt := &model.OptionGetAttachmentList{
Page: int(req.Page),
Size: int(req.Size_),
WithCount: true,
QueryIn: make(map[string][]interface{}),
}
if len(req.UserId) > 0 {
opt.QueryIn["user_id"] = util.Slice2Interface(req.UserId)
}
if len(req.Enable) > 0 {
opt.QueryIn["enable"] = util.Slice2Interface(req.Enable)
}
if len(req.Type) > 0 {
opt.QueryIn["type"] = util.Slice2Interface(req.Type)
}
req.Wd = strings.TrimSpace(req.Wd)
if req.Wd != "" {
wd := "%" + req.Wd + "%"
opt.QueryLike = map[string][]interface{}{"name": {wd}, "description": {wd}}
}
attachments, total, err := s.dbModel.GetAttachmentList(opt)
if err != nil {
return nil, status.Errorf(codes.Internal, err.Error())
}
var pbAttachments []*pb.Attachment
util.CopyStruct(&attachments, &pbAttachments)
var (
userIds []interface{}
userIdIndexMap = make(map[int64][]int)
)
for idx, attchment := range pbAttachments {
attchment.TypeName = s.dbModel.GetAttachmentTypeName(int(attchment.Type))
userIds = append(userIds, attchment.UserId)
userIdIndexMap[attchment.UserId] = append(userIdIndexMap[attchment.UserId], idx)
pbAttachments[idx] = attchment
}
if size := len(userIds); size > 0 {
users, _, _ := s.dbModel.GetUserList(&model.OptionGetUserList{Ids: userIds, Page: 1, Size: size, SelectFields: []string{"id", "username"}})
s.logger.Debug("GetUserList", zap.Any("users", users))
for _, user := range users {
if indexes, ok := userIdIndexMap[user.Id]; ok {
for _, idx := range indexes {
pbAttachments[idx].Username = user.Username
}
}
}
}
return &pb.ListAttachmentReply{Total: total, Attachment: pbAttachments}, nil
}
// UploadDocument 上传文档
2 years ago
func (s *AttachmentAPIService) UploadDocument(ctx *gin.Context) {
// 检查用户是否已登录
userClaims, statusCodes, err := s.checkLogin(ctx)
if err != nil {
s.logger.Debug("checkGinPermission", zap.Error(err), zap.Any("userClaims", userClaims), zap.Any("statusCodes", statusCodes))
ctx.JSON(statusCodes, ginResponse{Code: statusCodes, Message: err.Error(), Error: err.Error()})
return
}
// 检查用户是否有权限上传文档
if !s.dbModel.CanIUploadDocument(userClaims.UserId) {
ctx.JSON(http.StatusForbidden, ginResponse{Code: http.StatusForbidden, Message: "没有权限上传文档", Error: "没有权限上传文档"})
return
}
name := "file"
fileheader, err := ctx.FormFile(name)
if err != nil {
ctx.JSON(http.StatusBadRequest, ginResponse{Code: http.StatusBadRequest, Message: err.Error(), Error: err.Error()})
return
}
unsuportedExt := "不支持的文档类型"
ext := strings.ToLower(filepath.Ext(fileheader.Filename))
if !filetil.IsDocument(ext) {
ctx.JSON(http.StatusBadRequest, ginResponse{Code: http.StatusBadRequest, Message: unsuportedExt, Error: unsuportedExt})
return
}
allowedExt := s.dbModel.GetConfigOfSecurity(model.ConfigSecurityDocumentAllowedExt).DocumentAllowedExt
if len(allowedExt) > 0 && !util.InSlice(allowedExt, ext) {
ctx.JSON(http.StatusBadRequest, ginResponse{Code: http.StatusBadRequest, Message: unsuportedExt, Error: unsuportedExt})
return
}
attachment, err := s.saveFile(ctx, fileheader, true)
if err != nil {
ctx.JSON(http.StatusInternalServerError, ginResponse{Code: http.StatusInternalServerError, Message: err.Error(), Error: err.Error()})
return
}
attachment.UserId = userClaims.UserId
attachment.Type = model.AttachmentTypeDocument
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: "ok", Data: attachment})
ctx.JSON(http.StatusOK, ginResponse{Code: http.StatusOK, Message: "ok", Data: map[string]interface{}{"id": attachment.Id}})
}
2 years ago
// UploadAvatar 上传头像
func (s *AttachmentAPIService) UploadAvatar(ctx *gin.Context) {
s.uploadImage(ctx, model.AttachmentTypeAvatar)
}
// UploadConfig 上传配置项中的相关图片
func (s *AttachmentAPIService) UploadConfig(ctx *gin.Context) {
s.uploadImage(ctx, model.AttachmentTypeConfig)
}
// ViewDocumentPages 浏览文档页面
func (s *AttachmentAPIService) ViewDocumentPages(ctx *gin.Context) {
hash := ctx.Param("hash")
if len(hash) != 32 {
ctx.JSON(http.StatusNotFound, nil)
return
}
page := strings.TrimLeft(ctx.Param("page"), "./")
if strings.HasSuffix(page, ".gzip.svg") {
ctx.Header("Content-Encoding", "gzip")
}
ctx.Header("Content-Type", "image/svg+xml")
ctx.File(fmt.Sprintf("documents/%s/%s/%s", strings.Join(strings.Split(hash, "")[:5], "/"), hash, page))
}
func (s *AttachmentAPIService) ViewDocumentCover(ctx *gin.Context) {
hash := ctx.Param("hash")
file := fmt.Sprintf("documents/%s/%s/cover.png", strings.Join(strings.Split(hash, "")[:5], "/"), hash)
if len(hash) != 32 {
ctx.JSON(http.StatusNotFound, map[string]interface{}{"code": http.StatusNotFound, "message": "文件不存在"})
return
}
ctx.File(file)
}
// DownloadDocument 下载文档
func (s *AttachmentAPIService) DownloadDocument(ctx *gin.Context) {
claims := &jwt.StandardClaims{}
token := ctx.Param("jwt")
cfg := s.dbModel.GetConfigOfDownload(model.ConfigDownloadSecretKey)
// 验证JWT是否合法
jwtToken, err := jwt.ParseWithClaims(token, claims, func(t *jwt.Token) (interface{}, error) {
return []byte(cfg.SecretKey), nil
})
if err != nil || !jwtToken.Valid {
ctx.String(http.StatusBadRequest, "下载链接已失效")
return
}
filename := ctx.Query("filename")
file := fmt.Sprintf("documents/%s/%s%s", strings.Join(strings.Split(claims.Id, "")[:5], "/"), claims.Id, filepath.Ext(filename))
ctx.FileAttachment(file, filename)
}
// UploadArticle 上传文章相关图片和视频。这里不验证文件格式。
//
// 注意当前适配了wangeditor的接口规范如果需要适配其他编辑器需要修改此接口或者增加其他接口
func (s *AttachmentAPIService) UploadArticle(ctx *gin.Context) {
typ := ctx.Query("type")
if typ != "image" && typ != "video" {
ctx.JSON(http.StatusOK, map[string]interface{}{"errno": 1, "msg": "类型参数错误"})
return
}
userCliams, _, err := s.checkGinPermission(ctx)
if err != nil {
ctx.JSON(http.StatusOK, map[string]interface{}{"errno": 1, "msg": err.Error()})
return
}
name := "file"
fileHeader, err := ctx.FormFile(name)
if err != nil {
s.logger.Error("MultipartForm", zap.Error(err))
ctx.JSON(http.StatusOK, map[string]interface{}{"errno": 1, "msg": err.Error()})
return
}
attachment, err := s.saveFile(ctx, fileHeader)
if err != nil {
s.logger.Error("saveFile", zap.Error(err))
ctx.JSON(http.StatusOK, map[string]interface{}{"errno": 1, "msg": err.Error()})
return
}
attachment.UserId = userCliams.UserId
attachment.Type = model.AttachmentTypeArticle
err = s.dbModel.CreateAttachment(attachment)
if err != nil {
s.logger.Error("CreateAttachments", zap.Error(err))
ctx.JSON(http.StatusOK, map[string]interface{}{"errno": 1, "msg": err.Error()})
return
}
if typ == "image" {
ctx.JSON(http.StatusOK, map[string]interface{}{"errno": 0, "data": map[string]interface{}{
"url": attachment.Path,
"alt": attachment.Name,
// "href": "",
}})
} else {
ctx.JSON(http.StatusOK, map[string]interface{}{"errno": 0, "data": map[string]interface{}{
"url": attachment.Path,
// "poster": "",
}})
}
}
2 years ago
// UploadBanner 上传横幅创建横幅的时候要根据附件id更新附件的type_id字段
func (s *AttachmentAPIService) UploadBanner(ctx *gin.Context) {
s.uploadImage(ctx, model.AttachmentTypeBanner)
}
2 years ago
// 上传文档分类封面
2 years ago
func (s *AttachmentAPIService) UploadCategory(ctx *gin.Context) {
2 years ago
s.uploadImage(ctx, model.AttachmentTypeCategoryCover)
}
func (s *AttachmentAPIService) uploadImage(ctx *gin.Context, attachmentType int) {
name := "file"
userClaims, statusCodes, err := s.checkGinPermission(ctx)
2 years ago
if err != nil {
if !(userClaims != nil && attachmentType == model.AttachmentTypeAvatar) {
ctx.JSON(statusCodes, ginResponse{Code: statusCodes, Message: err.Error(), Error: err.Error()})
return
}
2 years ago
}
// 验证文件是否是图片
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、.gif和.ico格式图片"
2 years ago
ctx.JSON(http.StatusBadRequest, ginResponse{Code: http.StatusBadRequest, Message: message, Error: message})
return
}
2 years ago
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 = attachmentType
attachment.UserId = userClaims.UserId
2 years ago
if attachmentType == model.AttachmentTypeAvatar {
attachment.TypeId = userClaims.UserId
2 years ago
// 更新用户头像信息
err = s.dbModel.UpdateUser(&model.User{Id: userClaims.UserId, Avatar: attachment.Path}, "avatar")
2 years ago
if err != nil {
ctx.JSON(http.StatusInternalServerError, ginResponse{Code: http.StatusInternalServerError, Message: err.Error(), Error: err.Error()})
}
// 标记删除旧头像附件记录
s.dbModel.GetDB().Where("type = ? AND type_id = ?", model.AttachmentTypeAvatar, userClaims.UserId).Delete(&model.Attachment{})
2 years ago
}
// 保存附件信息
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})
}
2 years ago
// saveFile 保存文件。文件以md5值命名以及存储
// 同时,返回附件信息
func (s *AttachmentAPIService) saveFile(ctx *gin.Context, fileHeader *multipart.FileHeader, isDocument ...bool) (attachment *model.Attachment, err error) {
cacheDir := fmt.Sprintf("cache/uploads/%s", time.Now().Format("2006/01/02"))
2 years ago
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() {
os.Remove(cachePath)
2 years ago
}()
// 保存到临时文件
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
}
savePathFormat := "uploads/%s/%s%s"
if len(isDocument) > 0 && isDocument[0] {
savePathFormat = "documents/%s/%s%s"
}
savePath := fmt.Sprintf(savePathFormat, strings.Join(strings.Split(md5hash, "")[0:5], "/"), md5hash, ext)
2 years ago
os.MkdirAll(filepath.Dir(savePath), os.ModePerm)
err = util.CopyFile(cachePath, savePath)
2 years ago
if err != nil {
s.logger.Error("Rename", zap.Error(err), zap.String("cachePath", cachePath), zap.String("savePath", savePath))
return
}
2 years ago
attachment = &model.Attachment{
Size: fileHeader.Size,
Name: fileHeader.Filename,
Ip: ctx.ClientIP(),
Ext: ext,
Enable: true, // 默认都是合法的
Hash: md5hash,
Path: "/" + savePath,
2 years ago
}
// 对于图片,直接获取图片的宽高
if filetil.IsImage(ext) {
attachment.Width, attachment.Height, _ = filetil.GetImageSize(cachePath)
}
return
}
func (s *AttachmentAPIService) Favicon(ctx *gin.Context) {
favicon := strings.TrimLeft(s.dbModel.GetConfigOfSystem("favicon").Favicon, "./")
faviconIco := "favicon.ico"
if favicon != "" {
_, err := os.Stat(favicon)
if err != nil {
favicon = faviconIco
}
} else {
favicon = faviconIco
}
ctx.File(favicon)
}