code.gitea.io/gitea@v1.22.3/services/repository/files/upload.go (about)

     1  // Copyright 2019 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package files
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"os"
    10  	"path"
    11  	"strings"
    12  
    13  	git_model "code.gitea.io/gitea/models/git"
    14  	repo_model "code.gitea.io/gitea/models/repo"
    15  	user_model "code.gitea.io/gitea/models/user"
    16  	"code.gitea.io/gitea/modules/git"
    17  	"code.gitea.io/gitea/modules/lfs"
    18  	"code.gitea.io/gitea/modules/setting"
    19  )
    20  
    21  // UploadRepoFileOptions contains the uploaded repository file options
    22  type UploadRepoFileOptions struct {
    23  	LastCommitID string
    24  	OldBranch    string
    25  	NewBranch    string
    26  	TreePath     string
    27  	Message      string
    28  	Files        []string // In UUID format.
    29  	Signoff      bool
    30  }
    31  
    32  type uploadInfo struct {
    33  	upload        *repo_model.Upload
    34  	lfsMetaObject *git_model.LFSMetaObject
    35  }
    36  
    37  func cleanUpAfterFailure(ctx context.Context, infos *[]uploadInfo, t *TemporaryUploadRepository, original error) error {
    38  	for _, info := range *infos {
    39  		if info.lfsMetaObject == nil {
    40  			continue
    41  		}
    42  		if !info.lfsMetaObject.Existing {
    43  			if _, err := git_model.RemoveLFSMetaObjectByOid(ctx, t.repo.ID, info.lfsMetaObject.Oid); err != nil {
    44  				original = fmt.Errorf("%w, %v", original, err) // We wrap the original error - as this is the underlying error that required the fallback
    45  			}
    46  		}
    47  	}
    48  	return original
    49  }
    50  
    51  // UploadRepoFiles uploads files to the given repository
    52  func UploadRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, opts *UploadRepoFileOptions) error {
    53  	if len(opts.Files) == 0 {
    54  		return nil
    55  	}
    56  
    57  	uploads, err := repo_model.GetUploadsByUUIDs(ctx, opts.Files)
    58  	if err != nil {
    59  		return fmt.Errorf("GetUploadsByUUIDs [uuids: %v]: %w", opts.Files, err)
    60  	}
    61  
    62  	names := make([]string, len(uploads))
    63  	infos := make([]uploadInfo, len(uploads))
    64  	for i, upload := range uploads {
    65  		// Check file is not lfs locked, will return nil if lock setting not enabled
    66  		filepath := path.Join(opts.TreePath, upload.Name)
    67  		lfsLock, err := git_model.GetTreePathLock(ctx, repo.ID, filepath)
    68  		if err != nil {
    69  			return err
    70  		}
    71  		if lfsLock != nil && lfsLock.OwnerID != doer.ID {
    72  			u, err := user_model.GetUserByID(ctx, lfsLock.OwnerID)
    73  			if err != nil {
    74  				return err
    75  			}
    76  			return git_model.ErrLFSFileLocked{RepoID: repo.ID, Path: filepath, UserName: u.Name}
    77  		}
    78  
    79  		names[i] = upload.Name
    80  		infos[i] = uploadInfo{upload: upload}
    81  	}
    82  
    83  	t, err := NewTemporaryUploadRepository(ctx, repo)
    84  	if err != nil {
    85  		return err
    86  	}
    87  	defer t.Close()
    88  
    89  	hasOldBranch := true
    90  	if err = t.Clone(opts.OldBranch, true); err != nil {
    91  		if !git.IsErrBranchNotExist(err) || !repo.IsEmpty {
    92  			return err
    93  		}
    94  		if err = t.Init(repo.ObjectFormatName); err != nil {
    95  			return err
    96  		}
    97  		hasOldBranch = false
    98  		opts.LastCommitID = ""
    99  	}
   100  	if hasOldBranch {
   101  		if err = t.SetDefaultIndex(); err != nil {
   102  			return err
   103  		}
   104  	}
   105  
   106  	var filename2attribute2info map[string]map[string]string
   107  	if setting.LFS.StartServer {
   108  		filename2attribute2info, err = t.gitRepo.CheckAttribute(git.CheckAttributeOpts{
   109  			Attributes: []string{"filter"},
   110  			Filenames:  names,
   111  			CachedOnly: true,
   112  		})
   113  		if err != nil {
   114  			return err
   115  		}
   116  	}
   117  
   118  	// Copy uploaded files into repository.
   119  	for i := range infos {
   120  		if err := copyUploadedLFSFileIntoRepository(&infos[i], filename2attribute2info, t, opts.TreePath); err != nil {
   121  			return err
   122  		}
   123  	}
   124  
   125  	// Now write the tree
   126  	treeHash, err := t.WriteTree()
   127  	if err != nil {
   128  		return err
   129  	}
   130  
   131  	// make author and committer the doer
   132  	author := doer
   133  	committer := doer
   134  
   135  	// Now commit the tree
   136  	commitHash, err := t.CommitTree(opts.LastCommitID, author, committer, treeHash, opts.Message, opts.Signoff)
   137  	if err != nil {
   138  		return err
   139  	}
   140  
   141  	// Now deal with LFS objects
   142  	for i := range infos {
   143  		if infos[i].lfsMetaObject == nil {
   144  			continue
   145  		}
   146  		infos[i].lfsMetaObject, err = git_model.NewLFSMetaObject(ctx, infos[i].lfsMetaObject.RepositoryID, infos[i].lfsMetaObject.Pointer)
   147  		if err != nil {
   148  			// OK Now we need to cleanup
   149  			return cleanUpAfterFailure(ctx, &infos, t, err)
   150  		}
   151  		// Don't move the files yet - we need to ensure that
   152  		// everything can be inserted first
   153  	}
   154  
   155  	// OK now we can insert the data into the store - there's no way to clean up the store
   156  	// once it's in there, it's in there.
   157  	contentStore := lfs.NewContentStore()
   158  	for _, info := range infos {
   159  		if err := uploadToLFSContentStore(info, contentStore); err != nil {
   160  			return cleanUpAfterFailure(ctx, &infos, t, err)
   161  		}
   162  	}
   163  
   164  	// Then push this tree to NewBranch
   165  	if err := t.Push(doer, commitHash, opts.NewBranch); err != nil {
   166  		return err
   167  	}
   168  
   169  	return repo_model.DeleteUploads(ctx, uploads...)
   170  }
   171  
   172  func copyUploadedLFSFileIntoRepository(info *uploadInfo, filename2attribute2info map[string]map[string]string, t *TemporaryUploadRepository, treePath string) error {
   173  	file, err := os.Open(info.upload.LocalPath())
   174  	if err != nil {
   175  		return err
   176  	}
   177  	defer file.Close()
   178  
   179  	var objectHash string
   180  	if setting.LFS.StartServer && filename2attribute2info[info.upload.Name] != nil && filename2attribute2info[info.upload.Name]["filter"] == "lfs" {
   181  		// Handle LFS
   182  		// FIXME: Inefficient! this should probably happen in models.Upload
   183  		pointer, err := lfs.GeneratePointer(file)
   184  		if err != nil {
   185  			return err
   186  		}
   187  
   188  		info.lfsMetaObject = &git_model.LFSMetaObject{Pointer: pointer, RepositoryID: t.repo.ID}
   189  
   190  		if objectHash, err = t.HashObject(strings.NewReader(pointer.StringContent())); err != nil {
   191  			return err
   192  		}
   193  	} else if objectHash, err = t.HashObject(file); err != nil {
   194  		return err
   195  	}
   196  
   197  	// Add the object to the index
   198  	return t.AddObjectToIndex("100644", objectHash, path.Join(treePath, info.upload.Name))
   199  }
   200  
   201  func uploadToLFSContentStore(info uploadInfo, contentStore *lfs.ContentStore) error {
   202  	if info.lfsMetaObject == nil {
   203  		return nil
   204  	}
   205  	exist, err := contentStore.Exists(info.lfsMetaObject.Pointer)
   206  	if err != nil {
   207  		return err
   208  	}
   209  	if !exist {
   210  		file, err := os.Open(info.upload.LocalPath())
   211  		if err != nil {
   212  			return err
   213  		}
   214  
   215  		defer file.Close()
   216  		// FIXME: Put regenerates the hash and copies the file over.
   217  		// I guess this strictly ensures the soundness of the store but this is inefficient.
   218  		if err := contentStore.Put(info.lfsMetaObject.Pointer, file); err != nil {
   219  			// OK Now we need to cleanup
   220  			// Can't clean up the store, once uploaded there they're there.
   221  			return err
   222  		}
   223  	}
   224  	return nil
   225  }