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