Skip to content

Commit 5cffab8

Browse files
committedJan 10, 2025
feat(service): implement file cleanup and deletion functionality
1 parent 47c6662 commit 5cffab8

File tree

24 files changed

+504
-40
lines changed

24 files changed

+504
-40
lines changed
 

‎cmd/wire_gen.go

+8-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎configs/config.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ swaggerui:
3333
address: ':80'
3434
service_config:
3535
upload_path: "/data/uploads"
36+
clean_up_uploads: true
37+
clean_orphan_uploads_period_hours: 48
38+
purge_deleted_files_period_days: 30
3639
ui:
3740
public_url: '/'
3841
api_url: '/'

‎go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ require (
6060
golang.org/x/crypto v0.27.0
6161
golang.org/x/image v0.20.0
6262
golang.org/x/net v0.29.0
63+
golang.org/x/text v0.18.0
6364
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
6465
gopkg.in/yaml.v3 v3.0.1
6566
modernc.org/sqlite v1.33.0
@@ -160,7 +161,6 @@ require (
160161
golang.org/x/arch v0.10.0 // indirect
161162
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
162163
golang.org/x/sys v0.25.0 // indirect
163-
golang.org/x/text v0.18.0 // indirect
164164
golang.org/x/tools v0.25.0 // indirect
165165
google.golang.org/protobuf v1.34.2 // indirect
166166
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect

‎internal/base/constant/upload.go

+1
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@ const (
2525
PostSubPath = "post"
2626
BrandingSubPath = "branding"
2727
FilesPostSubPath = "files/post"
28+
DeletedSubPath = "deleted"
2829
)

‎internal/base/cron/cron.go

+36-7
Original file line numberDiff line numberDiff line change
@@ -24,36 +24,45 @@ import (
2424
"fmt"
2525

2626
"github.com/apache/answer/internal/service/content"
27+
"github.com/apache/answer/internal/service/file_record"
28+
"github.com/apache/answer/internal/service/service_config"
2729
"github.com/apache/answer/internal/service/siteinfo_common"
2830
"github.com/robfig/cron/v3"
2931
"github.com/segmentfault/pacman/log"
3032
)
3133

3234
// ScheduledTaskManager scheduled task manager
3335
type ScheduledTaskManager struct {
34-
siteInfoService siteinfo_common.SiteInfoCommonService
35-
questionService *content.QuestionService
36+
siteInfoService siteinfo_common.SiteInfoCommonService
37+
questionService *content.QuestionService
38+
fileRecordService *file_record.FileRecordService
39+
serviceConfig *service_config.ServiceConfig
3640
}
3741

3842
// NewScheduledTaskManager new scheduled task manager
3943
func NewScheduledTaskManager(
4044
siteInfoService siteinfo_common.SiteInfoCommonService,
4145
questionService *content.QuestionService,
46+
fileRecordService *file_record.FileRecordService,
47+
serviceConfig *service_config.ServiceConfig,
4248
) *ScheduledTaskManager {
4349
manager := &ScheduledTaskManager{
44-
siteInfoService: siteInfoService,
45-
questionService: questionService,
50+
siteInfoService: siteInfoService,
51+
questionService: questionService,
52+
fileRecordService: fileRecordService,
53+
serviceConfig: serviceConfig,
4654
}
4755
return manager
4856
}
4957

5058
func (s *ScheduledTaskManager) Run() {
51-
fmt.Println("start cron")
59+
log.Infof("cron job manager start")
60+
5261
s.questionService.SitemapCron(context.Background())
5362
c := cron.New()
5463
_, err := c.AddFunc("0 */1 * * *", func() {
5564
ctx := context.Background()
56-
fmt.Println("sitemap cron execution")
65+
log.Infof("sitemap cron execution")
5766
s.questionService.SitemapCron(ctx)
5867
})
5968
if err != nil {
@@ -62,12 +71,32 @@ func (s *ScheduledTaskManager) Run() {
6271

6372
_, err = c.AddFunc("0 */1 * * *", func() {
6473
ctx := context.Background()
65-
fmt.Println("refresh hottest cron execution")
74+
log.Infof("refresh hottest cron execution")
6675
s.questionService.RefreshHottestCron(ctx)
6776
})
6877
if err != nil {
6978
log.Error(err)
7079
}
7180

81+
if s.serviceConfig.CleanUpUploads {
82+
log.Infof("clean up uploads cron enabled")
83+
84+
conf := s.serviceConfig
85+
_, err = c.AddFunc(fmt.Sprintf("0 */%d * * *", conf.CleanOrphanUploadsPeriodHours), func() {
86+
log.Infof("clean orphan upload files cron execution")
87+
s.fileRecordService.CleanOrphanUploadFiles(context.Background())
88+
})
89+
if err != nil {
90+
log.Error(err)
91+
}
92+
93+
_, err = c.AddFunc(fmt.Sprintf("0 0 */%d * *", conf.PurgeDeletedFilesPeriodDays), func() {
94+
log.Infof("purge deleted files cron execution")
95+
s.fileRecordService.PurgeDeletedFiles(context.Background())
96+
})
97+
if err != nil {
98+
log.Error(err)
99+
}
100+
}
72101
c.Start()
73102
}

‎internal/controller/upload_controller.go

+5-4
Original file line numberDiff line numberDiff line change
@@ -70,19 +70,20 @@ func (uc *UploadController) UploadFile(ctx *gin.Context) {
7070
)
7171

7272
source := ctx.PostForm("source")
73+
userID := middleware.GetLoginUserIDFromContext(ctx)
7374
switch source {
7475
case fileFromAvatar:
75-
url, err = uc.uploaderService.UploadAvatarFile(ctx)
76+
url, err = uc.uploaderService.UploadAvatarFile(ctx, userID)
7677
case fileFromPost:
77-
url, err = uc.uploaderService.UploadPostFile(ctx)
78+
url, err = uc.uploaderService.UploadPostFile(ctx, userID)
7879
case fileFromBranding:
7980
if !middleware.GetIsAdminFromContext(ctx) {
8081
handler.HandleResponse(ctx, errors.Forbidden(reason.ForbiddenError), nil)
8182
return
8283
}
83-
url, err = uc.uploaderService.UploadBrandingFile(ctx)
84+
url, err = uc.uploaderService.UploadBrandingFile(ctx, userID)
8485
case fileFromPostAttachment:
85-
url, err = uc.uploaderService.UploadPostAttachment(ctx)
86+
url, err = uc.uploaderService.UploadPostAttachment(ctx, userID)
8687
default:
8788
handler.HandleResponse(ctx, errors.BadRequest(reason.UploadFileSourceUnsupported), nil)
8889
return

‎internal/entity/file_record_entity.go

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package entity
21+
22+
import "time"
23+
24+
const (
25+
FileRecordStatusAvailable = 1
26+
FileRecordStatusDeleted = 10
27+
)
28+
29+
// FileRecord file record
30+
type FileRecord struct {
31+
ID int `xorm:"not null pk autoincr INT(10) id"`
32+
CreatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP created TIMESTAMP created_at"`
33+
UpdatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP updated TIMESTAMP updated_at"`
34+
UserID string `xorm:"not null default 0 BIGINT(20) user_id"`
35+
FilePath string `xorm:"not null VARCHAR(256) file_path"`
36+
FileURL string `xorm:"not null VARCHAR(1024) file_url"`
37+
ObjectID string `xorm:"not null default 0 INDEX BIGINT(20) object_id"`
38+
Source string `xorm:"not null VARCHAR(128) source"`
39+
Status int `xorm:"not null default 0 TINYINT(4) status"`
40+
}
41+
42+
// TableName file record table name
43+
func (FileRecord) TableName() string {
44+
return "file_record"
45+
}

‎internal/migrations/migrations.go

+1
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ var migrations = []Migration{
100100
NewMigration("v1.4.0", "add badge/badge_group/badge_award table", addBadges, true),
101101
NewMigration("v1.4.1", "add question link", addQuestionLink, true),
102102
NewMigration("v1.4.2", "add the number of question links", addQuestionLinkedCount, true),
103+
NewMigration("v1.4.5", "add file record", addFileRecord, true),
103104
}
104105

105106
func GetMigrations() []Migration {

‎internal/migrations/v25.go

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package migrations
21+
22+
import (
23+
"context"
24+
25+
"github.com/apache/answer/internal/entity"
26+
"xorm.io/xorm"
27+
)
28+
29+
func addFileRecord(ctx context.Context, x *xorm.Engine) error {
30+
return x.Context(ctx).Sync(new(entity.FileRecord))
31+
}

‎internal/repo/answer/answer_repo.go

+18-1
Original file line numberDiff line numberDiff line change
@@ -529,7 +529,24 @@ func (ar *answerRepo) updateSearch(ctx context.Context, answerID string) (err er
529529
}
530530

531531
func (ar *answerRepo) DeletePermanentlyAnswers(ctx context.Context) error {
532-
_, err := ar.data.DB.Context(ctx).Where("status = ?", entity.AnswerStatusDeleted).Delete(&entity.Answer{})
532+
// get all deleted answers ids
533+
ids := make([]string, 0)
534+
err := ar.data.DB.Context(ctx).Select("id").Table(new(entity.Answer).TableName()).
535+
Where("status = ?", entity.AnswerStatusDeleted).Find(&ids)
536+
if err != nil {
537+
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
538+
}
539+
if len(ids) == 0 {
540+
return nil
541+
}
542+
543+
// delete all revisions permanently
544+
_, err = ar.data.DB.Context(ctx).In("object_id", ids).Delete(&entity.Revision{})
545+
if err != nil {
546+
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
547+
}
548+
549+
_, err = ar.data.DB.Context(ctx).Where("status = ?", entity.AnswerStatusDeleted).Delete(&entity.Answer{})
533550
if err != nil {
534551
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
535552
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package file_record
21+
22+
import (
23+
"context"
24+
25+
"github.com/apache/answer/internal/base/pager"
26+
"github.com/apache/answer/internal/service/file_record"
27+
28+
"github.com/apache/answer/internal/base/data"
29+
"github.com/apache/answer/internal/base/reason"
30+
"github.com/apache/answer/internal/entity"
31+
"github.com/segmentfault/pacman/errors"
32+
)
33+
34+
// fileRecordRepo fileRecord repository
35+
type fileRecordRepo struct {
36+
data *data.Data
37+
}
38+
39+
// NewFileRecordRepo new repository
40+
func NewFileRecordRepo(data *data.Data) file_record.FileRecordRepo {
41+
return &fileRecordRepo{
42+
data: data,
43+
}
44+
}
45+
46+
// AddFileRecord add file record
47+
func (fr *fileRecordRepo) AddFileRecord(ctx context.Context, fileRecord *entity.FileRecord) (err error) {
48+
_, err = fr.data.DB.Context(ctx).Insert(fileRecord)
49+
if err != nil {
50+
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
51+
}
52+
return
53+
}
54+
55+
// GetFileRecordPage get fileRecord page
56+
func (fr *fileRecordRepo) GetFileRecordPage(ctx context.Context, page, pageSize int, cond *entity.FileRecord) (
57+
fileRecordList []*entity.FileRecord, total int64, err error) {
58+
fileRecordList = make([]*entity.FileRecord, 0)
59+
60+
session := fr.data.DB.Context(ctx)
61+
total, err = pager.Help(page, pageSize, &fileRecordList, cond, session)
62+
if err != nil {
63+
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
64+
}
65+
return
66+
}
67+
68+
// DeleteFileRecord delete file record
69+
func (fr *fileRecordRepo) DeleteFileRecord(ctx context.Context, id int) (err error) {
70+
_, err = fr.data.DB.Context(ctx).ID(id).Cols("status").Update(&entity.FileRecord{Status: entity.FileRecordStatusDeleted})
71+
if err != nil {
72+
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
73+
}
74+
return
75+
}
76+
77+
// UpdateFileRecord update file record
78+
func (fr *fileRecordRepo) UpdateFileRecord(ctx context.Context, fileRecord *entity.FileRecord) (err error) {
79+
_, err = fr.data.DB.Context(ctx).ID(fileRecord.ID).Update(fileRecord)
80+
if err != nil {
81+
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
82+
}
83+
return
84+
}

‎internal/repo/provider.go

+2
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333
"github.com/apache/answer/internal/repo/comment"
3434
"github.com/apache/answer/internal/repo/config"
3535
"github.com/apache/answer/internal/repo/export"
36+
"github.com/apache/answer/internal/repo/file_record"
3637
"github.com/apache/answer/internal/repo/limit"
3738
"github.com/apache/answer/internal/repo/meta"
3839
"github.com/apache/answer/internal/repo/notification"
@@ -107,4 +108,5 @@ var ProviderSetRepo = wire.NewSet(
107108
badge.NewEventRuleRepo,
108109
badge_group.NewBadgeGroupRepo,
109110
badge_award.NewBadgeAwardRepo,
111+
file_record.NewFileRecordRepo,
110112
)

‎internal/repo/question/question_repo.go

+17
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,23 @@ func (qr *questionRepo) UpdateQuestionStatusWithOutUpdateTime(ctx context.Contex
168168
}
169169

170170
func (qr *questionRepo) DeletePermanentlyQuestions(ctx context.Context) (err error) {
171+
// get all deleted question ids
172+
ids := make([]string, 0)
173+
err = qr.data.DB.Context(ctx).Select("id").Table(new(entity.Question).TableName()).
174+
Where("status = ?", entity.QuestionStatusDeleted).Find(&ids)
175+
if err != nil {
176+
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
177+
}
178+
if len(ids) == 0 {
179+
return nil
180+
}
181+
182+
// delete all revisions permanently
183+
_, err = qr.data.DB.Context(ctx).In("object_id", ids).Delete(&entity.Revision{})
184+
if err != nil {
185+
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
186+
}
187+
171188
_, err = qr.data.DB.Context(ctx).Where("status = ?", entity.QuestionStatusDeleted).Delete(&entity.Question{})
172189
if err != nil {
173190
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()

‎internal/repo/revision/revision_repo.go

+11-1
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,17 @@ func (rr *revisionRepo) GetLastRevisionByObjectID(ctx context.Context, objectID
155155
revision *entity.Revision, exist bool, err error,
156156
) {
157157
revision = &entity.Revision{}
158-
exist, err = rr.data.DB.Context(ctx).Where("object_id = ?", objectID).OrderBy("created_at DESC").Get(revision)
158+
exist, err = rr.data.DB.Context(ctx).Where("object_id = ?", objectID).Desc("created_at").Get(revision)
159+
if err != nil {
160+
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
161+
}
162+
return
163+
}
164+
165+
// GetLastRevisionByFileURL get object's last revision by file url
166+
func (rr *revisionRepo) GetLastRevisionByFileURL(ctx context.Context, fileURL string) (revision *entity.Revision, exist bool, err error) {
167+
revision = &entity.Revision{}
168+
exist, err = rr.data.DB.Context(ctx).Where("content LIKE ?", "%"+fileURL+"%").Desc("created_at").Get(revision)
159169
if err != nil {
160170
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
161171
}

‎internal/router/static_router.go

+10-2
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,14 @@
2020
package router
2121

2222
import (
23+
"net/http"
24+
"path/filepath"
25+
"strings"
26+
2327
"github.com/apache/answer/internal/base/constant"
2428
"github.com/apache/answer/internal/service/service_config"
29+
"github.com/apache/answer/pkg/dir"
2530
"github.com/gin-gonic/gin"
26-
"path/filepath"
27-
"strings"
2831
)
2932

3033
// StaticRouter static api router
@@ -54,6 +57,11 @@ func (a *StaticRouter) RegisterStaticRouter(r *gin.RouterGroup) {
5457
realFilename := strings.TrimSuffix(filePath, "/"+originalFilename) + filepath.Ext(originalFilename)
5558
// The file local path is /uploads/files/post/hash.pdf
5659
fileLocalPath := filepath.Join(a.serviceConfig.UploadPath, constant.FilesPostSubPath, realFilename)
60+
// If the file is not exist, return 404
61+
if !dir.CheckFileExist(fileLocalPath) {
62+
c.Redirect(http.StatusFound, "/404")
63+
return
64+
}
5765
c.FileAttachment(fileLocalPath, originalFilename)
5866
})
5967
}

‎internal/service/content/question_service.go

+2
Original file line numberDiff line numberDiff line change
@@ -1398,6 +1398,8 @@ func (qs *QuestionService) GetQuestionPage(ctx context.Context, req *schema.Ques
13981398
return nil, 0, err
13991399
}
14001400
tagIDs = append(synTagIds, tagInfo.ID)
1401+
} else {
1402+
return questions, 0, nil
14011403
}
14021404
}
14031405

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package file_record
21+
22+
import (
23+
"context"
24+
"fmt"
25+
"os"
26+
"path/filepath"
27+
"time"
28+
29+
"github.com/apache/answer/internal/base/constant"
30+
"github.com/apache/answer/internal/entity"
31+
"github.com/apache/answer/internal/service/revision"
32+
"github.com/apache/answer/internal/service/service_config"
33+
"github.com/apache/answer/internal/service/siteinfo_common"
34+
"github.com/apache/answer/pkg/checker"
35+
"github.com/apache/answer/pkg/dir"
36+
"github.com/apache/answer/pkg/writer"
37+
"github.com/segmentfault/pacman/log"
38+
)
39+
40+
// FileRecordRepo file record repository
41+
type FileRecordRepo interface {
42+
AddFileRecord(ctx context.Context, fileRecord *entity.FileRecord) (err error)
43+
UpdateFileRecord(ctx context.Context, fileRecord *entity.FileRecord) (err error)
44+
GetFileRecordPage(ctx context.Context, page, pageSize int, cond *entity.FileRecord) (
45+
fileRecordList []*entity.FileRecord, total int64, err error)
46+
DeleteFileRecord(ctx context.Context, id int) (err error)
47+
}
48+
49+
// FileRecordService file record service
50+
type FileRecordService struct {
51+
fileRecordRepo FileRecordRepo
52+
revisionRepo revision.RevisionRepo
53+
serviceConfig *service_config.ServiceConfig
54+
siteInfoService siteinfo_common.SiteInfoCommonService
55+
}
56+
57+
// NewFileRecordService new file record service
58+
func NewFileRecordService(
59+
fileRecordRepo FileRecordRepo,
60+
revisionRepo revision.RevisionRepo,
61+
serviceConfig *service_config.ServiceConfig,
62+
siteInfoService siteinfo_common.SiteInfoCommonService,
63+
) *FileRecordService {
64+
return &FileRecordService{
65+
fileRecordRepo: fileRecordRepo,
66+
revisionRepo: revisionRepo,
67+
serviceConfig: serviceConfig,
68+
siteInfoService: siteInfoService,
69+
}
70+
}
71+
72+
// AddFileRecord add file record
73+
func (fs *FileRecordService) AddFileRecord(ctx context.Context, userID, filePath, fileURL, source string) {
74+
record := &entity.FileRecord{
75+
UserID: userID,
76+
FilePath: filePath,
77+
FileURL: fileURL,
78+
Source: source,
79+
Status: entity.FileRecordStatusAvailable,
80+
ObjectID: "0",
81+
}
82+
if err := fs.fileRecordRepo.AddFileRecord(ctx, record); err != nil {
83+
log.Errorf("add file record error: %v", err)
84+
}
85+
}
86+
87+
// CleanOrphanUploadFiles clean orphan upload files
88+
func (fs *FileRecordService) CleanOrphanUploadFiles(ctx context.Context) {
89+
page, pageSize := 1, 1000
90+
91+
for {
92+
fileRecordList, total, err := fs.fileRecordRepo.GetFileRecordPage(ctx, page, pageSize, &entity.FileRecord{
93+
Status: entity.FileRecordStatusAvailable,
94+
})
95+
if err != nil {
96+
log.Errorf("get file record page error: %v", err)
97+
return
98+
}
99+
if len(fileRecordList) == 0 || total == 0 {
100+
break
101+
}
102+
for _, fileRecord := range fileRecordList {
103+
// If this file record created in 48 hours, no need to check
104+
if fileRecord.CreatedAt.AddDate(0, 0, 2).After(time.Now()) {
105+
continue
106+
}
107+
if checker.IsNotZeroString(fileRecord.ObjectID) {
108+
_, exist, err := fs.revisionRepo.GetLastRevisionByObjectID(ctx, fileRecord.ObjectID)
109+
if err != nil {
110+
log.Errorf("get last revision by object id error: %v", err)
111+
continue
112+
}
113+
if exist {
114+
continue
115+
}
116+
} else {
117+
lastRevision, exist, err := fs.revisionRepo.GetLastRevisionByFileURL(ctx, fileRecord.FileURL)
118+
if err != nil {
119+
log.Errorf("get last revision by file url error: %v", err)
120+
continue
121+
}
122+
if exist {
123+
// update the file record object id
124+
fileRecord.ObjectID = lastRevision.ObjectID
125+
if err := fs.fileRecordRepo.UpdateFileRecord(ctx, fileRecord); err != nil {
126+
log.Errorf("update file record object id error: %v", err)
127+
}
128+
continue
129+
}
130+
}
131+
// Delete and move the file record
132+
if err := fs.deleteAndMoveFileRecord(ctx, fileRecord); err != nil {
133+
log.Error(err)
134+
}
135+
}
136+
page++
137+
}
138+
}
139+
140+
func (fs *FileRecordService) PurgeDeletedFiles(ctx context.Context) {
141+
deletedPath := filepath.Join(fs.serviceConfig.UploadPath, constant.DeletedSubPath)
142+
log.Infof("purge deleted files: %s", deletedPath)
143+
err := os.RemoveAll(deletedPath)
144+
if err != nil {
145+
log.Errorf("purge deleted files error: %v", err)
146+
return
147+
}
148+
err = dir.CreateDirIfNotExist(deletedPath)
149+
if err != nil {
150+
log.Errorf("create deleted directory error: %v", err)
151+
}
152+
return
153+
}
154+
155+
func (fs *FileRecordService) deleteAndMoveFileRecord(ctx context.Context, fileRecord *entity.FileRecord) error {
156+
// Delete the file record
157+
if err := fs.fileRecordRepo.DeleteFileRecord(ctx, fileRecord.ID); err != nil {
158+
return fmt.Errorf("delete file record error: %v", err)
159+
}
160+
161+
// Move the file to the deleted directory
162+
oldFilename := filepath.Base(fileRecord.FilePath)
163+
oldFilePath := filepath.Join(fs.serviceConfig.UploadPath, fileRecord.FilePath)
164+
deletedPath := filepath.Join(fs.serviceConfig.UploadPath, constant.DeletedSubPath, oldFilename)
165+
166+
if err := writer.MoveFile(oldFilePath, deletedPath); err != nil {
167+
return fmt.Errorf("move file error: %v", err)
168+
}
169+
170+
log.Debugf("delete and move file: %s", fileRecord.FileURL)
171+
return nil
172+
}

‎internal/service/object_info/object_info.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ package object_info
2121

2222
import (
2323
"context"
24+
2425
"github.com/apache/answer/internal/base/constant"
2526
"github.com/apache/answer/internal/base/reason"
2627
"github.com/apache/answer/internal/schema"
@@ -279,7 +280,7 @@ func (os *ObjService) GetInfo(ctx context.Context, objectID string) (objInfo *sc
279280
ObjectID: tagInfo.ID,
280281
TagID: tagInfo.ID,
281282
ObjectType: objectType,
282-
Title: tagInfo.ParsedText,
283+
Title: tagInfo.SlugName,
283284
Content: tagInfo.ParsedText, // todo trim
284285
}
285286
}

‎internal/service/provider.go

+2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
"github.com/apache/answer/internal/service/dashboard"
3737
"github.com/apache/answer/internal/service/event_queue"
3838
"github.com/apache/answer/internal/service/export"
39+
"github.com/apache/answer/internal/service/file_record"
3940
"github.com/apache/answer/internal/service/follow"
4041
"github.com/apache/answer/internal/service/importer"
4142
"github.com/apache/answer/internal/service/meta"
@@ -126,4 +127,5 @@ var ProviderSetService = wire.NewSet(
126127
badge.NewBadgeAwardService,
127128
badge.NewBadgeGroupService,
128129
importer.NewImporterService,
130+
file_record.NewFileRecordService,
129131
)

‎internal/service/revision/revision.go

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ type RevisionRepo interface {
3131
AddRevision(ctx context.Context, revision *entity.Revision, autoUpdateRevisionID bool) (err error)
3232
GetRevisionByID(ctx context.Context, revisionID string) (revision *entity.Revision, exist bool, err error)
3333
GetLastRevisionByObjectID(ctx context.Context, objectID string) (revision *entity.Revision, exist bool, err error)
34+
GetLastRevisionByFileURL(ctx context.Context, fileURL string) (revision *entity.Revision, exist bool, err error)
3435
GetRevisionList(ctx context.Context, revision *entity.Revision) (revisionList []entity.Revision, err error)
3536
UpdateObjectRevisionId(ctx context.Context, revision *entity.Revision, session *xorm.Session) (err error)
3637
ExistUnreviewedByObjectID(ctx context.Context, objectID string) (revision *entity.Revision, exist bool, err error)

‎internal/service/service_config/service_config.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,8 @@
2020
package service_config
2121

2222
type ServiceConfig struct {
23-
UploadPath string `json:"upload_path" mapstructure:"upload_path" yaml:"upload_path"`
23+
UploadPath string `json:"upload_path" mapstructure:"upload_path" yaml:"upload_path"`
24+
CleanUpUploads bool `json:"clean_up_uploads" mapstructure:"clean_up_uploads" yaml:"clean_up_uploads"`
25+
CleanOrphanUploadsPeriodHours int `json:"clean_orphan_uploads_period_hours" mapstructure:"clean_orphan_uploads_period_hours" yaml:"clean_orphan_uploads_period_hours"`
26+
PurgeDeletedFilesPeriodDays int `json:"purge_deleted_files_period_days" mapstructure:"purge_deleted_files_period_days" yaml:"purge_deleted_files_period_days"`
2427
}

‎internal/service/tag_common/tag_common.go

+8-1
Original file line numberDiff line numberDiff line change
@@ -349,10 +349,17 @@ func (ts *TagCommonService) AddTag(ctx context.Context, req *schema.AddTagReq) (
349349
}
350350
tagInfoJson, _ := json.Marshal(tagInfo)
351351
revisionDTO.Content = string(tagInfoJson)
352-
_, err = ts.revisionService.AddRevision(ctx, revisionDTO, true)
352+
revisionID, err := ts.revisionService.AddRevision(ctx, revisionDTO, true)
353353
if err != nil {
354354
return nil, err
355355
}
356+
ts.activityQueueService.Send(ctx, &schema.ActivityMsg{
357+
UserID: req.UserID,
358+
ObjectID: tagInfo.ID,
359+
OriginalObjectID: tagInfo.ID,
360+
ActivityTypeKey: constant.ActTagCreated,
361+
RevisionID: revisionID,
362+
})
356363
return &schema.AddTagResp{SlugName: tagInfo.SlugName}, nil
357364
}
358365

‎internal/service/uploader/upload.go

+35-17
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import (
3131
"path/filepath"
3232
"strings"
3333

34+
"github.com/apache/answer/internal/service/file_record"
35+
3436
"github.com/apache/answer/internal/base/constant"
3537
"github.com/apache/answer/internal/base/reason"
3638
"github.com/apache/answer/internal/service/service_config"
@@ -53,6 +55,7 @@ var (
5355
constant.PostSubPath,
5456
constant.BrandingSubPath,
5557
constant.FilesPostSubPath,
58+
constant.DeletedSubPath,
5659
}
5760
supportedThumbFileExtMapping = map[string]imaging.Format{
5861
".jpg": imaging.JPEG,
@@ -63,36 +66,41 @@ var (
6366
)
6467

6568
type UploaderService interface {
66-
UploadAvatarFile(ctx *gin.Context) (url string, err error)
67-
UploadPostFile(ctx *gin.Context) (url string, err error)
68-
UploadPostAttachment(ctx *gin.Context) (url string, err error)
69-
UploadBrandingFile(ctx *gin.Context) (url string, err error)
69+
UploadAvatarFile(ctx *gin.Context, userID string) (url string, err error)
70+
UploadPostFile(ctx *gin.Context, userID string) (url string, err error)
71+
UploadPostAttachment(ctx *gin.Context, userID string) (url string, err error)
72+
UploadBrandingFile(ctx *gin.Context, userID string) (url string, err error)
7073
AvatarThumbFile(ctx *gin.Context, fileName string, size int) (url string, err error)
7174
}
7275

7376
// uploaderService uploader service
7477
type uploaderService struct {
75-
serviceConfig *service_config.ServiceConfig
76-
siteInfoService siteinfo_common.SiteInfoCommonService
78+
serviceConfig *service_config.ServiceConfig
79+
siteInfoService siteinfo_common.SiteInfoCommonService
80+
fileRecordService *file_record.FileRecordService
7781
}
7882

7983
// NewUploaderService new upload service
80-
func NewUploaderService(serviceConfig *service_config.ServiceConfig,
81-
siteInfoService siteinfo_common.SiteInfoCommonService) UploaderService {
84+
func NewUploaderService(
85+
serviceConfig *service_config.ServiceConfig,
86+
siteInfoService siteinfo_common.SiteInfoCommonService,
87+
fileRecordService *file_record.FileRecordService,
88+
) UploaderService {
8289
for _, subPath := range subPathList {
8390
err := dir.CreateDirIfNotExist(filepath.Join(serviceConfig.UploadPath, subPath))
8491
if err != nil {
8592
panic(err)
8693
}
8794
}
8895
return &uploaderService{
89-
serviceConfig: serviceConfig,
90-
siteInfoService: siteInfoService,
96+
serviceConfig: serviceConfig,
97+
siteInfoService: siteInfoService,
98+
fileRecordService: fileRecordService,
9199
}
92100
}
93101

94102
// UploadAvatarFile upload avatar file
95-
func (us *uploaderService) UploadAvatarFile(ctx *gin.Context) (url string, err error) {
103+
func (us *uploaderService) UploadAvatarFile(ctx *gin.Context, userID string) (url string, err error) {
96104
url, err = us.tryToUploadByPlugin(ctx, plugin.UserAvatar)
97105
if err != nil {
98106
return "", err
@@ -174,7 +182,7 @@ func (us *uploaderService) AvatarThumbFile(ctx *gin.Context, fileName string, si
174182
return saveFilePath, nil
175183
}
176184

177-
func (us *uploaderService) UploadPostFile(ctx *gin.Context) (
185+
func (us *uploaderService) UploadPostFile(ctx *gin.Context, userID string) (
178186
url string, err error) {
179187
url, err = us.tryToUploadByPlugin(ctx, plugin.UserPost)
180188
if err != nil {
@@ -202,10 +210,15 @@ func (us *uploaderService) UploadPostFile(ctx *gin.Context) (
202210
fileExt := strings.ToLower(path.Ext(fileHeader.Filename))
203211
newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt)
204212
avatarFilePath := path.Join(constant.PostSubPath, newFilename)
205-
return us.uploadImageFile(ctx, fileHeader, avatarFilePath)
213+
url, err = us.uploadImageFile(ctx, fileHeader, avatarFilePath)
214+
if err != nil {
215+
return "", err
216+
}
217+
us.fileRecordService.AddFileRecord(ctx, userID, avatarFilePath, url, string(plugin.UserPost))
218+
return url, nil
206219
}
207220

208-
func (us *uploaderService) UploadPostAttachment(ctx *gin.Context) (
221+
func (us *uploaderService) UploadPostAttachment(ctx *gin.Context, userID string) (
209222
url string, err error) {
210223
url, err = us.tryToUploadByPlugin(ctx, plugin.UserPostAttachment)
211224
if err != nil {
@@ -232,11 +245,16 @@ func (us *uploaderService) UploadPostAttachment(ctx *gin.Context) (
232245

233246
fileExt := strings.ToLower(path.Ext(fileHeader.Filename))
234247
newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt)
235-
avatarFilePath := path.Join(constant.FilesPostSubPath, newFilename)
236-
return us.uploadAttachmentFile(ctx, fileHeader, fileHeader.Filename, avatarFilePath)
248+
attachmentFilePath := path.Join(constant.FilesPostSubPath, newFilename)
249+
url, err = us.uploadAttachmentFile(ctx, fileHeader, fileHeader.Filename, attachmentFilePath)
250+
if err != nil {
251+
return "", err
252+
}
253+
us.fileRecordService.AddFileRecord(ctx, userID, attachmentFilePath, url, string(plugin.UserPostAttachment))
254+
return url, nil
237255
}
238256

239-
func (us *uploaderService) UploadBrandingFile(ctx *gin.Context) (
257+
func (us *uploaderService) UploadBrandingFile(ctx *gin.Context, userID string) (
240258
url string, err error) {
241259
url, err = us.tryToUploadByPlugin(ctx, plugin.AdminBranding)
242260
if err != nil {

‎pkg/writer/writer.go

+5
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,8 @@ func WriteFile(filePath, content string) error {
4848
}
4949
return nil
5050
}
51+
52+
// MoveFile move file to new path
53+
func MoveFile(oldPath, newPath string) error {
54+
return os.Rename(oldPath, newPath)
55+
}

0 commit comments

Comments
 (0)
Please sign in to comment.