1// Copyright 2019 The Gitea Authors. All rights reserved.
2// Use of this source code is governed by a MIT-style
3// license that can be found in the LICENSE file.
4
5package migrations
6
7import (
8	"crypto/md5"
9	"fmt"
10	"io"
11	"math"
12	"os"
13	"path/filepath"
14	"time"
15
16	"code.gitea.io/gitea/modules/log"
17	"code.gitea.io/gitea/modules/setting"
18	"code.gitea.io/gitea/modules/util"
19
20	"xorm.io/xorm"
21)
22
23func renameExistingUserAvatarName(x *xorm.Engine) error {
24	sess := x.NewSession()
25	defer sess.Close()
26
27	type User struct {
28		ID        int64  `xorm:"pk autoincr"`
29		LowerName string `xorm:"UNIQUE NOT NULL"`
30		Avatar    string
31	}
32
33	ticker := time.NewTicker(5 * time.Second)
34	defer ticker.Stop()
35
36	count, err := x.Count(new(User))
37	if err != nil {
38		return err
39	}
40	log.Info("%d User Avatar(s) to migrate ...", count)
41
42	deleteList := make(map[string]struct{})
43	start := 0
44	migrated := 0
45	for {
46		if err := sess.Begin(); err != nil {
47			return fmt.Errorf("session.Begin: %v", err)
48		}
49		users := make([]*User, 0, 50)
50		if err := sess.Table("user").Asc("id").Limit(50, start).Find(&users); err != nil {
51			return fmt.Errorf("select users from id [%d]: %v", start, err)
52		}
53		if len(users) == 0 {
54			_ = sess.Rollback()
55			break
56		}
57
58		log.Info("select users [%d - %d]", start, start+len(users))
59		start += 50
60
61		for _, user := range users {
62			oldAvatar := user.Avatar
63
64			if stat, err := os.Stat(filepath.Join(setting.Avatar.Path, oldAvatar)); err != nil || !stat.Mode().IsRegular() {
65				if err == nil {
66					err = fmt.Errorf("Error: \"%s\" is not a regular file", oldAvatar)
67				}
68				log.Warn("[user: %s] os.Stat: %v", user.LowerName, err)
69				// avatar doesn't exist in the storage
70				// no need to move avatar and update database
71				// we can just skip this
72				continue
73			}
74
75			newAvatar, err := copyOldAvatarToNewLocation(user.ID, oldAvatar)
76			if err != nil {
77				_ = sess.Rollback()
78				return fmt.Errorf("[user: %s] %v", user.LowerName, err)
79			} else if newAvatar == oldAvatar {
80				continue
81			}
82
83			user.Avatar = newAvatar
84			if _, err := sess.ID(user.ID).Cols("avatar").Update(user); err != nil {
85				_ = sess.Rollback()
86				return fmt.Errorf("[user: %s] user table update: %v", user.LowerName, err)
87			}
88
89			deleteList[filepath.Join(setting.Avatar.Path, oldAvatar)] = struct{}{}
90			migrated++
91			select {
92			case <-ticker.C:
93				log.Info(
94					"%d/%d (%2.0f%%) User Avatar(s) migrated (%d old avatars to be deleted) in %d batches. %d Remaining ...",
95					migrated,
96					count,
97					float64(migrated)/float64(count)*100,
98					len(deleteList),
99					int(math.Ceil(float64(migrated)/float64(50))),
100					count-int64(migrated))
101			default:
102			}
103		}
104		if err := sess.Commit(); err != nil {
105			_ = sess.Rollback()
106			return fmt.Errorf("commit session: %v", err)
107		}
108	}
109
110	deleteCount := len(deleteList)
111	log.Info("Deleting %d old avatars ...", deleteCount)
112	i := 0
113	for file := range deleteList {
114		if err := util.Remove(file); err != nil {
115			log.Warn("util.Remove: %v", err)
116		}
117		i++
118		select {
119		case <-ticker.C:
120			log.Info(
121				"%d/%d (%2.0f%%) Old User Avatar(s) deleted. %d Remaining ...",
122				i,
123				deleteCount,
124				float64(i)/float64(deleteCount)*100,
125				deleteCount-i)
126		default:
127		}
128	}
129
130	log.Info("Completed migrating %d User Avatar(s) and deleting %d Old Avatars", count, deleteCount)
131
132	return nil
133}
134
135// copyOldAvatarToNewLocation copies oldAvatar to newAvatarLocation
136// and returns newAvatar location
137func copyOldAvatarToNewLocation(userID int64, oldAvatar string) (string, error) {
138	fr, err := os.Open(filepath.Join(setting.Avatar.Path, oldAvatar))
139	if err != nil {
140		return "", fmt.Errorf("os.Open: %v", err)
141	}
142	defer fr.Close()
143
144	data, err := io.ReadAll(fr)
145	if err != nil {
146		return "", fmt.Errorf("io.ReadAll: %v", err)
147	}
148
149	newAvatar := fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("%d-%x", userID, md5.Sum(data)))))
150	if newAvatar == oldAvatar {
151		return newAvatar, nil
152	}
153
154	if err := os.WriteFile(filepath.Join(setting.Avatar.Path, newAvatar), data, 0o666); err != nil {
155		return "", fmt.Errorf("os.WriteFile: %v", err)
156	}
157
158	return newAvatar, nil
159}
160