code.gitea.io/gitea@v1.21.7/models/migrations/v1_11/v115.go (about) 1 // Copyright 2019 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package v1_11 //nolint 5 6 import ( 7 "crypto/md5" 8 "fmt" 9 "io" 10 "math" 11 "os" 12 "path/filepath" 13 "time" 14 15 "code.gitea.io/gitea/modules/container" 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 23 func 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(container.Set[string]) 43 start := 0 44 migrated := 0 45 for { 46 if err := sess.Begin(); err != nil { 47 return fmt.Errorf("session.Begin: %w", 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]: %w", 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.Storage.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] %w", 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: %w", user.LowerName, err) 87 } 88 89 deleteList.Add(filepath.Join(setting.Avatar.Storage.Path, oldAvatar)) 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: %w", 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 137 func copyOldAvatarToNewLocation(userID int64, oldAvatar string) (string, error) { 138 fr, err := os.Open(filepath.Join(setting.Avatar.Storage.Path, oldAvatar)) 139 if err != nil { 140 return "", fmt.Errorf("os.Open: %w", err) 141 } 142 defer fr.Close() 143 144 data, err := io.ReadAll(fr) 145 if err != nil { 146 return "", fmt.Errorf("io.ReadAll: %w", 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.Storage.Path, newAvatar), data, 0o666); err != nil { 155 return "", fmt.Errorf("os.WriteFile: %w", err) 156 } 157 158 return newAvatar, nil 159 }