Skip to content

Commit 5cffab8

Browse files
committed
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

Diff for: cmd/wire_gen.go

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

Diff for: 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: '/'

Diff for: 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

Diff for: 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
)

Diff for: 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
}

Diff for: 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

Diff for: 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+
}

Diff for: 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 {

Diff for: 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+
}

Diff for: 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
}

Diff for: internal/repo/file_record/file_record_repo.go

+84
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+
}

0 commit comments

Comments
 (0)