Skip to content

Commit 20534ba

Browse files
author
broccoli
committed
feature: saving files as blob in database
+ files (e.g. branding pictues, attachments) can be saved as a blob in a database. The feature can be turned on and off (feature toggle) with the env var FILE_STORAGE_MODE + a new controller with the endpoints /file/branding etc. had to be created since the files are statically linked when fs is used. + the files plus metadata are stored in the table 'file'
1 parent eddedfd commit 20534ba

File tree

9 files changed

+201
-30
lines changed

9 files changed

+201
-30
lines changed

cmd/wire_gen.go

Lines changed: 7 additions & 24 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/controller/controller.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,5 @@ var ProviderSetController = wire.NewSet(
5353
NewEmbedController,
5454
NewBadgeController,
5555
NewRenderController,
56+
NewFileController,
5657
)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package controller
2+
3+
import (
4+
"strconv"
5+
6+
"github.com/apache/answer/internal/base/handler"
7+
"github.com/apache/answer/internal/repo/file"
8+
"github.com/gin-gonic/gin"
9+
)
10+
11+
type FileController struct {
12+
FileRepo file.FileRepo
13+
}
14+
15+
func NewFileController(repo file.FileRepo) *FileController {
16+
return &FileController{FileRepo: repo}
17+
}
18+
19+
func (bc *FileController) GetFile(ctx *gin.Context) {
20+
id := ctx.Param("id")
21+
download := ctx.DefaultQuery("download", "")
22+
23+
blob, err := bc.FileRepo.GetByID(ctx.Request.Context(), id)
24+
if err != nil || blob == nil {
25+
handler.HandleResponse(ctx, err, "file not found")
26+
return
27+
}
28+
29+
ctx.Header("Content-Type", blob.MimeType)
30+
ctx.Header("Content-Length", strconv.FormatInt(blob.Size, 10))
31+
if download != "" {
32+
ctx.Header("Content-Disposition", "attachment; filename=\""+download+"\"")
33+
}
34+
35+
ctx.Data(200, blob.MimeType, blob.Content)
36+
}

internal/entity/file_entity.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package entity
2+
3+
import (
4+
"time"
5+
)
6+
7+
type File struct {
8+
ID string `xorm:"pk varchar(36)"`
9+
FileName string `xorm:"varchar(255) not null"`
10+
MimeType string `xorm:"varchar(100)"`
11+
Size int64 `xorm:"bigint"`
12+
Content []byte `xorm:"blob"`
13+
CreatedAt time.Time `xorm:"created"`
14+
}
15+
16+
func (File) TableName() string {
17+
return "file"
18+
}

internal/migrations/init_data.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ var (
7676
&entity.BadgeAward{},
7777
&entity.FileRecord{},
7878
&entity.PluginKVStorage{},
79+
&entity.File{},
7980
}
8081

8182
roles = []*entity.Role{

internal/repo/file/file_repo.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package file
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
7+
"github.com/apache/answer/internal/base/data"
8+
"github.com/apache/answer/internal/base/reason"
9+
"github.com/apache/answer/internal/entity"
10+
"github.com/segmentfault/pacman/errors"
11+
)
12+
13+
type FileRepo interface {
14+
Save(ctx context.Context, file *entity.File) error
15+
GetByID(ctx context.Context, id string) (*entity.File, error)
16+
}
17+
18+
type fileRepo struct {
19+
data *data.Data
20+
}
21+
22+
func NewFileRepo(data *data.Data) FileRepo {
23+
return &fileRepo{data: data}
24+
}
25+
26+
func (r *fileRepo) Save(ctx context.Context, file *entity.File) error {
27+
_, err := r.data.DB.Context(ctx).Insert(file)
28+
if err != nil {
29+
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
30+
}
31+
return nil
32+
}
33+
34+
func (r *fileRepo) GetByID(ctx context.Context, id string) (*entity.File, error) {
35+
var blob entity.File
36+
ok, err := r.data.DB.Context(ctx).ID(id).Get(&blob)
37+
if err != nil {
38+
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
39+
}
40+
if !ok {
41+
return nil, sql.ErrNoRows
42+
}
43+
return &blob, nil
44+
}

internal/router/answer_api_router.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ type AnswerAPIRouter struct {
5757
metaController *controller.MetaController
5858
badgeController *controller.BadgeController
5959
adminBadgeController *controller_admin.BadgeController
60+
fileController *controller.FileController
6061
}
6162

6263
func NewAnswerAPIRouter(
@@ -90,6 +91,7 @@ func NewAnswerAPIRouter(
9091
metaController *controller.MetaController,
9192
badgeController *controller.BadgeController,
9293
adminBadgeController *controller_admin.BadgeController,
94+
fileController *controller.FileController,
9395
) *AnswerAPIRouter {
9496
return &AnswerAPIRouter{
9597
langController: langController,
@@ -122,6 +124,7 @@ func NewAnswerAPIRouter(
122124
metaController: metaController,
123125
badgeController: badgeController,
124126
adminBadgeController: adminBadgeController,
127+
fileController: fileController,
125128
}
126129
}
127130

@@ -148,6 +151,9 @@ func (a *AnswerAPIRouter) RegisterMustUnAuthAnswerAPIRouter(authUserMiddleware *
148151

149152
// plugins
150153
r.GET("/plugin/status", a.pluginController.GetAllPluginStatus)
154+
155+
// file branding
156+
r.GET("/file/branding/:id", a.fileController.GetFile)
151157
}
152158

153159
func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
@@ -171,6 +177,10 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
171177
r.GET("/personal/question/page", a.questionController.PersonalQuestionPage)
172178
r.GET("/question/link", a.questionController.GetQuestionLink)
173179

180+
//file
181+
r.GET("/file/post/:id", a.fileController.GetFile)
182+
r.GET("/file/avatar/:id", a.fileController.GetFile)
183+
174184
// comment
175185
r.GET("/comment/page", a.commentController.GetCommentWithPage)
176186
r.GET("/personal/comment/page", a.commentController.GetCommentPersonalWithPage)
@@ -310,6 +320,7 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
310320

311321
// meta
312322
r.PUT("/meta/reaction", a.metaController.AddOrUpdateReaction)
323+
313324
}
314325

315326
func (a *AnswerAPIRouter) RegisterAnswerAdminAPIRouter(r *gin.RouterGroup) {

internal/service/provider.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
package service
2121

2222
import (
23+
"github.com/apache/answer/internal/repo/file"
2324
"github.com/apache/answer/internal/service/action"
2425
"github.com/apache/answer/internal/service/activity"
2526
"github.com/apache/answer/internal/service/activity_common"
@@ -40,7 +41,7 @@ import (
4041
"github.com/apache/answer/internal/service/follow"
4142
"github.com/apache/answer/internal/service/importer"
4243
"github.com/apache/answer/internal/service/meta"
43-
"github.com/apache/answer/internal/service/meta_common"
44+
metacommon "github.com/apache/answer/internal/service/meta_common"
4445
"github.com/apache/answer/internal/service/notice_queue"
4546
"github.com/apache/answer/internal/service/notification"
4647
notficationcommon "github.com/apache/answer/internal/service/notification_common"
@@ -128,4 +129,5 @@ var ProviderSetService = wire.NewSet(
128129
badge.NewBadgeGroupService,
129130
importer.NewImporterService,
130131
file_record.NewFileRecordService,
132+
file.NewFileRepo,
131133
)

0 commit comments

Comments
 (0)