Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add attachment upload api #20

Merged
merged 5 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${file}"
"program": "cmd/main.go"
},
{
"name": "Development",
Expand Down
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ dev:
@echo "Starting development service..."
@go run cmd/main.go

.PHONY: clean
clean:
@echo "Cleaning up..."
@rm -rf ./release ./dist ./db ./logs ./upload ./cmd/db ./cmd/upload ./cmd/logs
@echo "Cleaned up."

.PHONY: deps
deps:
@echo "Installing dependencies..."
Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,17 @@ rate_limit:
enable_cors: false # Enable CORS
enable_user_registration: true # Enable user registration
upload:
virtual_path: /upload # Virtual path
dir: upload # Upload directory
max_size: 10485760 # 10MB
max_size: 10485760 # 10MB, unit: byte
keep_original_name: true # Keep original file name
create_date_dir: true # Create date directory
allow_types: # Allowed file types
- image/jpeg
- image/jpg
- image/png
- image/gif
- image/bmp
jwt: # JWT settings
access_secret: qhkxjrRmYcVYKSEobqsvhxhtPVeTWquu # Access token secret
refresh_secret: qhkxjrRmYcVYKSEobqsvhxhtPV3TWquu # Refresh token secret
Expand Down
12 changes: 12 additions & 0 deletions cmd/srv/controller/api_v1.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,14 @@ func (v *apiV1) serve() {
r.Use(gogin.Authorize(gogin.AuthorizeOption{
User: true,
IsPage: false,
AllowAPI: true,
Msg: "Please log in first",
Btn: "Log in",
Redirect: fmt.Sprintf("%s/login", singleton.Conf.Site.BaseURL),
}))

r.POST("/attchment/upload", v.upload) // upload file

r.POST("/post", v.postPost) // create post
r.GET("/post/:id", v.getPost) // get post
r.DELETE("/post/:id", v.deletePost) // delete post
Expand All @@ -42,6 +45,15 @@ func (v *apiV1) serve() {

var authModel = model.Auth{}

func (v *apiV1) upload(c *gin.Context) {
result, err := singleton.AttchmentUpload.Upload(c)
if err != nil {
mygin.ResponseJSON(c, 400, gin.H{}, err.Error())
return
}
mygin.ResponseJSON(c, 200, result, "upload success")
}

func (v *apiV1) logout(c *gin.Context) {
isPage := parse.ParseBool(c.Query("page"), false)
gogin.UserLogout(c)
Expand Down
2 changes: 1 addition & 1 deletion cmd/srv/controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func serveStatic(r *gin.Engine) {
r.StaticFS("/static", http.FS(staticFs))

// Serve uploaded files
r.Static("/upload", singleton.Conf.Upload.Dir)
r.Static(singleton.Conf.Upload.VirtualPath, singleton.Conf.Upload.Dir)
}

// Load templates
Expand Down
1 change: 1 addition & 0 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ rate_limit:
enable_cors: false # Enable CORS
enable_user_registration: true # Enable user registration
upload:
virtual_path: /upload # Virtual path
dir: upload # Upload directory
max_size: 10485760 # 10MB
jwt: # JWT settings
Expand Down
8 changes: 6 additions & 2 deletions internal/gconfig/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,12 @@ type Config struct {
EnableCORS bool `mapstructure:"enable_cors"`
EnableUserRegistration bool `mapstructure:"enable_user_registration"`
Upload struct {
Dir string `mapstructure:"dir"`
MaxSize int `mapstructure:"max_size"`
Dir string `mapstructure:"dir"`
VirtualPath string `mapstructure:"virtual_path"`
MaxSize int64 `mapstructure:"max_size"`
KeepOriginalName bool `mapstructure:"keep_original_name"`
CreateDateDir bool `mapstructure:"create_date_dir"`
AllowTypes []string `mapstructure:"allow_types"`
} `mapstructure:"upload"`
Log struct {
Level string `mapstructure:"level"`
Expand Down
8 changes: 8 additions & 0 deletions internal/gogin/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ func GetCurrentUser(c *gin.Context) (user model.User, err error) {
return user, nil
}

func GetCurrentUserId(c *gin.Context) uint64 {
user, err := GetCurrentUser(c)
if err != nil {
return 0
}
return user.ID
}

func ShowErrorPage(c *gin.Context, i mygin.ErrInfo, isPage bool) {
if isPage {
c.HTML(i.Code, "error", CommonEnvironment(c, gin.H{
Expand Down
106 changes: 106 additions & 0 deletions pkg/mygin/attchment_upload.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package mygin

import (
"fmt"
"go-gin/pkg/utils"
"go-gin/pkg/utils/file"
"go-gin/pkg/utils/image"
"os"
"path"
"path/filepath"
"strings"
"time"

"github.com/gin-gonic/gin"
)

type AttchmentUpload struct {
BaseURL string // BaseURL is the base url for the uploaded file
MaxSize int64 // MaxSize is the max file size, default is 2MB
AllowTypes []string // AllowTypes is the allowed file types
FormName string // FormName is the form name for the file, default is "file"
StoreDir string // StoreDir is the directory to store the uploaded file
CreateDateDir bool // CreateDateDir is the flag to create date directory
KeepOriginalName bool // KeepOriginalName is the flag to keep the original file name
}

type AttchmentUploadResult struct {
Url string `json:"url"`
Name string `json:"name"`
OriginalName string `json:"original_name"`
Size int64 `json:"size"`
MiMe string `json:"mime"`
With int `json:"width"`
Hei int `json:"height"`
Ext string `json:"ext"`
MD5 string `json:"md5"`
SavePath string `json:"save_path"`
}

func (a *AttchmentUpload) Upload(c *gin.Context) (*AttchmentUploadResult, error) {
result := &AttchmentUploadResult{}
form_file, err := c.FormFile(a.FormName)
if err != nil {
return result, err
}
if form_file.Size > a.MaxSize {
return result, fmt.Errorf("file size too large")
}
form_file_ext := strings.ToLower(filepath.Ext(form_file.Filename)) // eg: .jpg
form_file_fileilename := form_file.Filename
form_file_fileize := form_file.Size
form_file_mime := form_file.Header.Get("Content-Type")

if len(a.AllowTypes) > 0 && !utils.InArrayString(form_file_mime, a.AllowTypes) {
return result, fmt.Errorf("file type not allowed")
}

now := time.Now()
year := now.Format("2006")
month := now.Format("01")
day := now.Format("02")

saveName := utils.GenHexStr(32) + form_file_ext
if a.KeepOriginalName {
saveName = form_file_fileilename
}

savePath := a.StoreDir
url := fmt.Sprintf("%s/%s", a.BaseURL, saveName)
if a.CreateDateDir {
savePath = path.Join(a.StoreDir, year, month, day)
url = fmt.Sprintf("%s/%s/%s/%s/%s", a.BaseURL, year, month, day, saveName)
}
if err := file.MkdirAllIfNotExists(savePath, os.ModePerm); err != nil {
return result, err
}

if err := c.SaveUploadedFile(form_file, path.Join(savePath, saveName)); err != nil {
return result, err
}

md5, _ := file.FileMD5(path.Join(savePath, saveName))
w, h, _ := image.GetImageSize(path.Join(savePath, saveName))

result.Url = url
result.Name = saveName
result.OriginalName = form_file_fileilename
result.Size = form_file_fileize
result.MiMe = form_file_mime
result.Ext = form_file_ext
result.MD5 = md5
result.With = w
result.Hei = h
result.SavePath = savePath
return result, nil
}

func NewAttchmentUpload() *AttchmentUpload {
return &AttchmentUpload{
BaseURL: "/upload",
MaxSize: 1024 * 1024 * 2,
AllowTypes: []string{"image/jpeg", "image/png", "image/gif", "image/jpg"},
FormName: "file",
StoreDir: "./upload",
}
}
6 changes: 2 additions & 4 deletions pkg/utils/file/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"encoding/hex"
"io"
"os"
"path"
"path/filepath"
)

Expand Down Expand Up @@ -44,10 +43,9 @@ func FileMD5(filePath string) (string, error) {
}

func MkdirAllIfNotExists(pathname string, perm os.FileMode) error {
dir := path.Dir(pathname)
if _, err := os.Stat(dir); err != nil {
if _, err := os.Stat(pathname); err != nil {
if os.IsNotExist(err) {
if err := os.MkdirAll(dir, perm); err != nil {
if err := os.MkdirAll(pathname, perm); err != nil {
return err
}
}
Expand Down
29 changes: 29 additions & 0 deletions pkg/utils/image/image.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package image

import (
"bytes"
"image"
_ "image/gif"
_ "image/jpeg"
_ "image/png"

"os"
)

func GetImageSize(imgpath string) (int, int, error) {
file, err := os.Open(imgpath)
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
}

func IsImage(data []byte) bool {
_, _, err := image.Decode(bytes.NewReader(data))
return err == nil
}
40 changes: 40 additions & 0 deletions pkg/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/hex"
"fmt"
"reflect"
"time"

"github.com/twinj/uuid"
)
Expand Down Expand Up @@ -73,3 +74,42 @@ func PrintStructFieldsAndValues(s interface{}, indent string) {
}
}
}

func InArrayString(needle string, haystack []string) bool {
for _, v := range haystack {
if v == needle {
return true
}
}
return false
}

func InArrayInt(needle int, haystack []int) bool {
for _, v := range haystack {
if v == needle {
return true
}
}
return false
}

func InArrayInt64(needle int64, haystack []int64) bool {
for _, v := range haystack {
if v == needle {
return true
}
}
return false
}

func TimeFormatString(time time.Time, format string) string {
return time.Format(format)
}

func TimeNowFormatString(format string) string {
return time.Now().Format(format)
}

func TimeNowString() string {
return TimeNowFormatString("2006-01-02 15:04:05")
}
8 changes: 5 additions & 3 deletions service/singleton/singleton.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package singleton
import (
"fmt"
"os"
"path"
"time"

_ "github.com/ncruces/go-sqlite3/embed"
Expand Down Expand Up @@ -33,6 +34,7 @@ var (
func LoadSingleton() {
LoadCronTasks()
LoadNotifications()
LoadUpload()
}

func InitTimezoneAndCache() {
Expand Down Expand Up @@ -66,12 +68,12 @@ func InitLog(conf *gconfig.Config) {
}

// InitDBFromPath initialize the database from the given path
func InitDBFromPath(path string) {
func InitDBFromPath(dbpath string) {
var err error
if err = file.MkdirAllIfNotExists(path, os.ModePerm); err != nil {
if err = file.MkdirAllIfNotExists(path.Dir(dbpath), os.ModePerm); err != nil {
panic(err)
}
DB, err = gorm.Open(gormlite.Open(path), &gorm.Config{
DB, err = gorm.Open(gormlite.Open(dbpath), &gorm.Config{
CreateBatchSize: 200,
})
if err != nil {
Expand Down
21 changes: 21 additions & 0 deletions service/singleton/upload.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package singleton

import (
"go-gin/pkg/mygin"
)

var (
AttchmentUpload *mygin.AttchmentUpload
)

func LoadUpload() {
AttchmentUpload = &mygin.AttchmentUpload{
BaseURL: Conf.Site.BaseURL + Conf.Upload.VirtualPath,
MaxSize: Conf.Upload.MaxSize,
AllowTypes: Conf.Upload.AllowTypes,
FormName: "file",
StoreDir: Conf.Upload.Dir,
CreateDateDir: Conf.Upload.CreateDateDir,
KeepOriginalName: Conf.Upload.KeepOriginalName,
}
}