Skip to content

Commit

Permalink
feat: add attachment upload api
Browse files Browse the repository at this point in the history
* Add virtual path for file uploads

* Update launch.json and api_v1.go

* Remove unused import and fix directory creation bug

* Add clean target to Makefile

* feat: add attachment upload api
  • Loading branch information
funnyzak authored Feb 21, 2024
1 parent f3b2cae commit 3136c4e
Show file tree
Hide file tree
Showing 14 changed files with 248 additions and 12 deletions.
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,
}
}

0 comments on commit 3136c4e

Please sign in to comment.