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  }