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 }