1// Copyright 2019 Gitea. All rights reserved.
2// SPDX-License-Identifier: MIT
3
4package admin
5
6import (
7 "context"
8 "fmt"
9
10 "forgejo.org/models/db"
11 repo_model "forgejo.org/models/repo"
12 user_model "forgejo.org/models/user"
13 "forgejo.org/modules/json"
14 "forgejo.org/modules/migration"
15 "forgejo.org/modules/secret"
16 "forgejo.org/modules/setting"
17 "forgejo.org/modules/structs"
18 "forgejo.org/modules/timeutil"
19 "forgejo.org/modules/util"
20)
21
22// Task represents a task
23type Task struct {
24 ID int64
25 DoerID int64 `xorm:"index"` // operator
26 Doer *user_model.User `xorm:"-"`
27 OwnerID int64 `xorm:"index"` // repo owner id, when creating, the repoID maybe zero
28 Owner *user_model.User `xorm:"-"`
29 RepoID int64 `xorm:"index"`
30 Repo *repo_model.Repository `xorm:"-"`
31 Type structs.TaskType
32 Status structs.TaskStatus `xorm:"index"`
33 StartTime timeutil.TimeStamp
34 EndTime timeutil.TimeStamp
35 PayloadContent string `xorm:"TEXT"`
36 Message string `xorm:"TEXT"` // if task failed, saved the error reason, it could be a JSON string of TranslatableMessage or a plain message
37 Created timeutil.TimeStamp `xorm:"created"`
38}
39
40func init() {
41 db.RegisterModel(new(Task))
42}
43
44// TranslatableMessage represents JSON struct that can be translated with a Locale
45type TranslatableMessage struct {
46 Format string
47 Args []any `json:",omitempty"`
48}
49
50// LoadRepo loads repository of the task
51func (task *Task) LoadRepo(ctx context.Context) error {
52 if task.Repo != nil {
53 return nil
54 }
55 var repo repo_model.Repository
56 has, err := db.GetEngine(ctx).ID(task.RepoID).Get(&repo)
57 if err != nil {
58 return err
59 } else if !has {
60 return repo_model.ErrRepoNotExist{
61 ID: task.RepoID,
62 }
63 }
64 task.Repo = &repo
65 return nil
66}
67
68// LoadDoer loads do user
69func (task *Task) LoadDoer(ctx context.Context) error {
70 if task.Doer != nil {
71 return nil
72 }
73
74 var doer user_model.User
75 has, err := db.GetEngine(ctx).ID(task.DoerID).Get(&doer)
76 if err != nil {
77 return err
78 } else if !has {
79 return user_model.ErrUserNotExist{
80 UID: task.DoerID,
81 }
82 }
83 task.Doer = &doer
84
85 return nil
86}
87
88// LoadOwner loads owner user
89func (task *Task) LoadOwner(ctx context.Context) error {
90 if task.Owner != nil {
91 return nil
92 }
93
94 var owner user_model.User
95 has, err := db.GetEngine(ctx).ID(task.OwnerID).Get(&owner)
96 if err != nil {
97 return err
98 } else if !has {
99 return user_model.ErrUserNotExist{
100 UID: task.OwnerID,
101 }
102 }
103 task.Owner = &owner
104
105 return nil
106}
107
108// UpdateCols updates some columns
109func (task *Task) UpdateCols(ctx context.Context, cols ...string) error {
110 _, err := db.GetEngine(ctx).ID(task.ID).Cols(cols...).Update(task)
111 return err
112}
113
114// MigrateConfig returns task config when migrate repository
115func (task *Task) MigrateConfig() (*migration.MigrateOptions, error) {
116 if task.Type == structs.TaskTypeMigrateRepo {
117 var opts migration.MigrateOptions
118 err := json.Unmarshal([]byte(task.PayloadContent), &opts)
119 if err != nil {
120 return nil, err
121 }
122
123 // decrypt credentials
124 if opts.CloneAddrEncrypted != "" {
125 if opts.CloneAddr, err = secret.DecryptSecret(setting.SecretKey, opts.CloneAddrEncrypted); err != nil {
126 return nil, err
127 }
128 }
129 if opts.AuthPasswordEncrypted != "" {
130 if opts.AuthPassword, err = secret.DecryptSecret(setting.SecretKey, opts.AuthPasswordEncrypted); err != nil {
131 return nil, err
132 }
133 }
134 if opts.AuthTokenEncrypted != "" {
135 if opts.AuthToken, err = secret.DecryptSecret(setting.SecretKey, opts.AuthTokenEncrypted); err != nil {
136 return nil, err
137 }
138 }
139
140 return &opts, nil
141 }
142 return nil, fmt.Errorf("Task type is %s, not Migrate Repo", task.Type.Name())
143}
144
145// ErrTaskDoesNotExist represents a "TaskDoesNotExist" kind of error.
146type ErrTaskDoesNotExist struct {
147 ID int64
148 RepoID int64
149 Type structs.TaskType
150}
151
152// IsErrTaskDoesNotExist checks if an error is a ErrTaskDoesNotExist.
153func IsErrTaskDoesNotExist(err error) bool {
154 _, ok := err.(ErrTaskDoesNotExist)
155 return ok
156}
157
158func (err ErrTaskDoesNotExist) Error() string {
159 return fmt.Sprintf("task does not exist [id: %d, repo_id: %d, type: %d]",
160 err.ID, err.RepoID, err.Type)
161}
162
163func (err ErrTaskDoesNotExist) Unwrap() error {
164 return util.ErrNotExist
165}
166
167// GetMigratingTask returns the migrating task by repo's id
168func GetMigratingTask(ctx context.Context, repoID int64) (*Task, error) {
169 task := Task{
170 RepoID: repoID,
171 Type: structs.TaskTypeMigrateRepo,
172 }
173 has, err := db.GetEngine(ctx).Get(&task)
174 if err != nil {
175 return nil, err
176 } else if !has {
177 return nil, ErrTaskDoesNotExist{0, repoID, task.Type}
178 }
179 return &task, nil
180}
181
182// GetMigratingTaskByID returns the migrating task by repo's id
183func GetMigratingTaskByID(ctx context.Context, id, doerID int64) (*Task, *migration.MigrateOptions, error) {
184 task := Task{
185 ID: id,
186 DoerID: doerID,
187 Type: structs.TaskTypeMigrateRepo,
188 }
189 has, err := db.GetEngine(ctx).Get(&task)
190 if err != nil {
191 return nil, nil, err
192 } else if !has {
193 return nil, nil, ErrTaskDoesNotExist{id, 0, task.Type}
194 }
195
196 var opts migration.MigrateOptions
197 if err := json.Unmarshal([]byte(task.PayloadContent), &opts); err != nil {
198 return nil, nil, err
199 }
200 return &task, &opts, nil
201}
202
203// CreateTask creates a task on database
204func CreateTask(ctx context.Context, task *Task) error {
205 return db.Insert(ctx, task)
206}
207
208// FinishMigrateTask updates database when migrate task finished
209func FinishMigrateTask(ctx context.Context, task *Task) error {
210 task.Status = structs.TaskStatusFinished
211 task.EndTime = timeutil.TimeStampNow()
212
213 // delete credentials when we're done, they're a liability.
214 conf, err := task.MigrateConfig()
215 if err != nil {
216 return err
217 }
218 conf.AuthPassword = ""
219 conf.AuthToken = ""
220 conf.CloneAddr = util.SanitizeCredentialURLs(conf.CloneAddr)
221 conf.AuthPasswordEncrypted = ""
222 conf.AuthTokenEncrypted = ""
223 conf.CloneAddrEncrypted = ""
224 confBytes, err := json.Marshal(conf)
225 if err != nil {
226 return err
227 }
228 task.PayloadContent = string(confBytes)
229
230 _, err = db.GetEngine(ctx).ID(task.ID).Cols("status", "end_time", "payload_content").Update(task)
231 return err
232}