github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/kbfs/simplefs/upload_manager.go (about)

     1  // Copyright 2020 Keybase Inc. All rights reserved.
     2  // Use of this source code is governed by a BSD
     3  // license that can be found in the LICENSE file.
     4  
     5  package simplefs
     6  
     7  import (
     8  	"encoding/base64"
     9  	"fmt"
    10  	"os"
    11  	"path"
    12  	"path/filepath"
    13  	"sync"
    14  
    15  	"github.com/keybase/client/go/kbfs/libkbfs"
    16  	"github.com/keybase/client/go/protocol/keybase1"
    17  	"github.com/pkg/errors"
    18  	"golang.org/x/net/context"
    19  )
    20  
    21  const (
    22  	// ulCtxOpID is the display name for the unique operation SimpleFS ID tag.
    23  	ulCtxOpID = "SFSULID"
    24  )
    25  
    26  // ulCtxTagKey is the type used for unique context tags
    27  type ulCtxTagKey int
    28  
    29  const (
    30  	// ulCtxIDKey is the type of the tag for unique operation IDs.
    31  	ulCtxIDKey ulCtxTagKey = iota
    32  )
    33  
    34  type upload struct {
    35  	dirToDelete *string
    36  	uploadID    string
    37  	opid        keybase1.OpID
    38  
    39  	state keybase1.UploadState
    40  }
    41  
    42  type uploadManager struct {
    43  	k         *SimpleFS
    44  	publisher libkbfs.SubscriptionManagerPublisher
    45  	cacheDir  string
    46  
    47  	lock      sync.RWMutex
    48  	tempPaths map[string]bool
    49  	uploads   map[string]upload
    50  }
    51  
    52  func newUploadManager(simpleFS *SimpleFS) *uploadManager {
    53  	return &uploadManager{
    54  		k:         simpleFS,
    55  		publisher: simpleFS.config.SubscriptionManagerPublisher(),
    56  		cacheDir:  simpleFS.config.KbEnv().GetCacheDir(),
    57  		tempPaths: make(map[string]bool),
    58  		uploads:   make(map[string]upload),
    59  	}
    60  }
    61  
    62  func (m *uploadManager) makeContext() (ctx context.Context) {
    63  	return libkbfs.CtxWithRandomIDReplayable(context.Background(), ulCtxIDKey, ulCtxOpID, m.k.log)
    64  }
    65  
    66  func (m *uploadManager) makeTempDir() (tempDirPath string, err error) {
    67  	cacheDirAbs, err := filepath.Abs(m.cacheDir)
    68  	if err != nil {
    69  		return "", err
    70  	}
    71  	tempDirPath, err = os.MkdirTemp(cacheDirAbs, "uploads-")
    72  	if err != nil {
    73  		return "", err
    74  	}
    75  
    76  	m.lock.Lock()
    77  	defer m.lock.Unlock()
    78  	m.tempPaths[tempDirPath] = true
    79  
    80  	return tempDirPath, nil
    81  }
    82  
    83  func (m *uploadManager) doWhileLocked(action func()) {
    84  	m.lock.Lock()
    85  	defer m.lock.Unlock()
    86  	action()
    87  }
    88  
    89  func (m *uploadManager) getUpload(uploadID string) (upload, bool) {
    90  	m.lock.RLock()
    91  	defer m.lock.RUnlock()
    92  	u, ok := m.uploads[uploadID]
    93  	return u, ok
    94  }
    95  
    96  func (m *uploadManager) waitForCopy(uploadID string) {
    97  	upload, ok := m.getUpload(uploadID)
    98  	if !ok {
    99  		return
   100  	}
   101  	defer func() {
   102  		m.publisher.PublishChange(keybase1.SubscriptionTopic_UPLOAD_STATUS)
   103  		if upload.dirToDelete == nil {
   104  			return
   105  		}
   106  		if err := os.RemoveAll(*upload.dirToDelete); err != nil {
   107  			m.k.log.CDebugf(m.makeContext(), "remove temp dir error %s", err)
   108  		}
   109  
   110  	}()
   111  
   112  	err := m.k.SimpleFSWait(m.makeContext(), upload.opid)
   113  	if err != nil {
   114  		m.doWhileLocked(func() {
   115  			upload, ok := m.uploads[uploadID]
   116  			if !ok {
   117  				return
   118  			}
   119  			if errors.Cause(err) == context.Canceled {
   120  				upload.state.Canceled = true
   121  			} else {
   122  				errStr := err.Error()
   123  				upload.state.Error = &errStr
   124  			}
   125  			m.uploads[uploadID] = upload
   126  		})
   127  		return
   128  	}
   129  	m.doWhileLocked(func() {
   130  		delete(m.uploads, uploadID)
   131  	})
   132  }
   133  
   134  const uploadSuffixMax = 1024
   135  
   136  func (m *uploadManager) doStart(ctx context.Context,
   137  	sourceLocalPath string, dstParentPath string) (opid keybase1.OpID, dstPath keybase1.KBFSPath, err error) {
   138  	opid, err = m.k.SimpleFSMakeOpid(ctx)
   139  	if err != nil {
   140  		return keybase1.OpID{}, keybase1.KBFSPath{}, err
   141  	}
   142  	basename := filepath.Base(sourceLocalPath)
   143  
   144  renameLoop:
   145  	for i := 0; i < uploadSuffixMax; i++ {
   146  		name := basename
   147  		if i > 0 {
   148  			name = fmt.Sprintf("%s (%d)", basename, i)
   149  		}
   150  		dstPath = keybase1.NewPathWithKbfsPath(
   151  			path.Join(dstParentPath, name)).Kbfs()
   152  
   153  		// First check with stat. This should cover most cases, and is
   154  		// the last resort for avoiding merging directories where we
   155  		// don't get something like O_CREATE for free.
   156  		_, err = m.k.SimpleFSStat(ctx, keybase1.SimpleFSStatArg{
   157  			Path: keybase1.NewPathWithKbfs(dstPath),
   158  		})
   159  		switch {
   160  		case err == nil:
   161  			continue renameLoop
   162  		case err == errNotExist:
   163  		default:
   164  			return keybase1.OpID{}, keybase1.KBFSPath{}, err
   165  		}
   166  
   167  		err = m.k.SimpleFSCopyRecursive(ctx, keybase1.SimpleFSCopyRecursiveArg{
   168  			OpID:                   opid,
   169  			Src:                    keybase1.NewPathWithLocal(sourceLocalPath),
   170  			Dest:                   keybase1.NewPathWithKbfs(dstPath),
   171  			OverwriteExistingFiles: false,
   172  		})
   173  		switch errors.Cause(err) {
   174  		case errNameExists:
   175  			continue renameLoop
   176  		case nil:
   177  			return opid, dstPath, nil
   178  		default:
   179  			return keybase1.OpID{}, keybase1.KBFSPath{}, err
   180  		}
   181  	}
   182  
   183  	return keybase1.OpID{}, keybase1.KBFSPath{},
   184  		errors.New("too many rename attempts")
   185  }
   186  
   187  func (m *uploadManager) start(ctx context.Context, sourceLocalPath string,
   188  	targetParentPath keybase1.KBFSPath) (uploadID string, err error) {
   189  	opid, dstPath, err := m.doStart(ctx, sourceLocalPath, targetParentPath.Path)
   190  	if err != nil {
   191  		return "", err
   192  	}
   193  	uploadID = base64.RawURLEncoding.EncodeToString(opid[:])
   194  
   195  	defer m.publisher.PublishChange(keybase1.SubscriptionTopic_UPLOAD_STATUS)
   196  
   197  	m.doWhileLocked(func() {
   198  		var dirToDelete *string
   199  		sourceParent := filepath.Dir(sourceLocalPath)
   200  		if m.tempPaths[sourceParent] {
   201  			dirToDelete = &sourceParent
   202  		}
   203  		m.uploads[uploadID] = upload{
   204  			dirToDelete: dirToDelete,
   205  			uploadID:    uploadID,
   206  			opid:        opid,
   207  			state: keybase1.UploadState{
   208  				UploadID:   uploadID,
   209  				TargetPath: dstPath,
   210  			},
   211  		}
   212  	})
   213  
   214  	// If we start having tests on this we'll need proper shutdown mechanism
   215  	// for these goroutines.
   216  	go m.waitForCopy(uploadID)
   217  
   218  	return uploadID, nil
   219  }
   220  
   221  func (m *uploadManager) cancel(ctx context.Context, uploadID string) error {
   222  	upload, ok := m.getUpload(uploadID)
   223  	if !ok {
   224  		return nil
   225  	}
   226  	return m.k.SimpleFSCancel(ctx, upload.opid)
   227  }
   228  
   229  func (m *uploadManager) dismiss(uploadID string) error {
   230  	upload, ok := m.getUpload(uploadID)
   231  	if !ok {
   232  		return nil
   233  	}
   234  	if !upload.state.Canceled && upload.state.Error == nil {
   235  		return errors.New("dismiss called on ongoing upload")
   236  	}
   237  	m.doWhileLocked(func() {
   238  		delete(m.uploads, uploadID)
   239  	})
   240  	m.publisher.PublishChange(keybase1.SubscriptionTopic_UPLOAD_STATUS)
   241  	return nil
   242  }
   243  
   244  func (m *uploadManager) getUploads() (uploads []keybase1.UploadState) {
   245  	m.lock.RLock()
   246  	defer m.lock.RUnlock()
   247  	uploads = make([]keybase1.UploadState, 0, len(m.uploads))
   248  	for _, upload := range m.uploads {
   249  		uploads = append(uploads, upload.state)
   250  	}
   251  	return uploads
   252  }