github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/kbfs/simplefs/downloads.go (about) 1 // Copyright 2019 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 "os" 9 "path" 10 "path/filepath" 11 "sort" 12 "strconv" 13 "sync" 14 "syscall" 15 "time" 16 17 "github.com/keybase/client/go/kbfs/libkbfs" 18 "github.com/keybase/client/go/libkb" 19 "github.com/keybase/client/go/protocol/keybase1" 20 "github.com/pkg/errors" 21 "golang.org/x/net/context" 22 ) 23 24 const ( 25 // dlCtxOpID is the display name for the unique operation SimpleFS ID tag. 26 dlCtxOpID = "SFSDLID" 27 ) 28 29 // dlCtxTagKey is the type used for unique context tags 30 type dlCtxTagKey int 31 32 const ( 33 // dlCtxIDKey is the type of the tag for unique operation IDs. 34 dlCtxIDKey dlCtxTagKey = iota 35 ) 36 37 type download struct { 38 info keybase1.DownloadInfo 39 safeFilename string 40 state keybase1.DownloadState 41 opid keybase1.OpID 42 } 43 44 // downloadManager manages "downloads" initiated from outside KBFS. To KBFS, 45 // this is more like "exporting". Currently this is only used by GUI, so its 46 // APIs are tailored to the GUI. 47 // 48 // We have regular downloads which are tracked in the app visually, and are 49 // moved into a "Downloads" folder after they're done, and non-regular 50 // downloads which are for "Save" and "Send to other apps" on mobile. When the 51 // user chooses to save a photo or a video, or share a file to another app, we 52 // download to a cache folder and have GUI call some APIs to actually add them 53 // to the photo library or send to other apps. 54 type downloadManager struct { 55 k *SimpleFS 56 publisher libkbfs.SubscriptionManagerPublisher 57 58 lock sync.RWMutex 59 cacheDir string 60 downloadDir string 61 downloads map[string]download // download ID -> download 62 } 63 64 func newDownloadManager(simpleFS *SimpleFS) *downloadManager { 65 return &downloadManager{ 66 k: simpleFS, 67 publisher: simpleFS.config.SubscriptionManagerPublisher(), 68 cacheDir: simpleFS.config.KbEnv().GetCacheDir(), 69 downloadDir: simpleFS.config.KbEnv().GetDownloadsDir(), 70 downloads: make(map[string]download), 71 } 72 } 73 74 func (m *downloadManager) makeContext() (ctx context.Context, cancel func()) { 75 return context.WithCancel(libkbfs.CtxWithRandomIDReplayable(context.Background(), dlCtxIDKey, dlCtxOpID, m.k.log)) 76 } 77 78 func (m *downloadManager) getDownload(downloadID string) (download, error) { 79 m.lock.RLock() 80 defer m.lock.RUnlock() 81 d, ok := m.downloads[downloadID] 82 if !ok { 83 return download{}, errors.New("unknown downloadID") 84 } 85 return d, nil 86 } 87 88 func (m *downloadManager) updateDownload(downloadID string, f func(original download) download) (err error) { 89 defer m.publisher.PublishChange(keybase1.SubscriptionTopic_DOWNLOAD_STATUS) 90 m.lock.Lock() 91 defer m.lock.Unlock() 92 download, ok := m.downloads[downloadID] 93 if !ok { 94 return errors.New("unknown downloadID") 95 } 96 m.downloads[downloadID] = f(download) 97 return nil 98 } 99 100 const monitorDownloadTickerInterval = time.Second 101 102 func (m *downloadManager) monitorDownload( 103 ctx context.Context, opid keybase1.OpID, downloadID string, 104 done func(error)) { 105 ticker := time.NewTicker(monitorDownloadTickerInterval) 106 defer ticker.Stop() 107 for { 108 select { 109 case <-ticker.C: 110 resp, err := m.k.SimpleFSCheck(ctx, opid) 111 switch errors.Cause(err) { 112 case nil: 113 if err := m.updateDownload(downloadID, func(d download) download { 114 d.state.EndEstimate = resp.EndEstimate 115 d.state.Progress = float64( 116 resp.BytesWritten) / float64(resp.BytesTotal) 117 return d 118 }); err != nil { 119 done(err) 120 return 121 } 122 case errNoResult: 123 // This is from simpleFS. Likely download has finished, but 124 // wait for ctx.Done(). 125 default: 126 done(err) 127 return 128 } 129 case <-ctx.Done(): 130 return 131 } 132 } 133 } 134 135 func (m *downloadManager) getCacheDir() string { 136 m.lock.RLock() 137 defer m.lock.RUnlock() 138 return m.cacheDir 139 } 140 141 func (m *downloadManager) getDownloadDir() string { 142 m.lock.RLock() 143 defer m.lock.RUnlock() 144 return m.downloadDir 145 } 146 147 func (m *downloadManager) getFilenames( 148 kbfsPath keybase1.KBFSPath) (filename, safeFilename string) { 149 _, filename = path.Split(path.Clean(kbfsPath.Path)) 150 return filename, libkb.GetSafeFilename(filename) 151 } 152 153 func (m *downloadManager) getDownloadPath( 154 ctx context.Context, filename, downloadID string) ( 155 downloadPath string, err error) { 156 parentDir := filepath.Join(m.getCacheDir(), "simplefsdownload") 157 if err = os.MkdirAll(parentDir, 0700); err != nil { 158 return "", err 159 } 160 downloadPath = filepath.Join(parentDir, downloadID+path.Ext(filename)) 161 return downloadPath, nil 162 } 163 164 func (m *downloadManager) moveToDownloadFolder( 165 ctx context.Context, srcPath string, filename string) (localPath string, err error) { 166 // There's no download on iOS; just saving to the photos library and 167 // sharing to other apps, both of which are handled in JS after the 168 // download (to the cache dir) finishes. 169 if libkb.GetPlatformString() == "ios" || libkb.GetPlatformString() == "ipad" { 170 return "", errors.New("MoveToDownloadFolder is not supported on iOS") 171 } 172 parentDir := m.getDownloadDir() 173 if err = os.MkdirAll(parentDir, 0700); err != nil { 174 return "", err 175 } 176 filename = limitFilenameLengthForWindowsDownloads(filename) 177 destPath, err := libkb.FindFilePathWithNumberSuffix(parentDir, filename, false) 178 if err != nil { 179 return "", err 180 } 181 182 err = os.Rename(srcPath, destPath) 183 switch er := err.(type) { 184 case nil: 185 return destPath, nil 186 case *os.LinkError: 187 if er.Err != syscall.EXDEV { 188 return "", err 189 } 190 // Rename failed because dest and src are on different devices. So 191 // use SimpleFSMove which copies then deletes. 192 opid, err := m.k.SimpleFSMakeOpid(ctx) 193 if err != nil { 194 return "", err 195 } 196 err = m.k.SimpleFSMove(ctx, keybase1.SimpleFSMoveArg{ 197 OpID: opid, 198 Src: keybase1.NewPathWithLocal(srcPath), 199 Dest: keybase1.NewPathWithLocal(destPath), 200 }) 201 if err != nil { 202 return "", err 203 } 204 err = m.k.SimpleFSWait(ctx, opid) 205 if err != nil { 206 return "", err 207 } 208 return destPath, nil 209 default: 210 return "", err 211 } 212 } 213 214 func (m *downloadManager) waitForDownload(ctx context.Context, 215 downloadID string, downloadPath string, done func(error)) { 216 d, err := m.getDownload(downloadID) 217 if err != nil { 218 done(err) 219 return 220 } 221 err = m.k.SimpleFSWait(ctx, d.opid) 222 if err != nil { 223 done(err) 224 return 225 } 226 227 var localPath string 228 if d.info.IsRegularDownload { 229 localPath, err = m.moveToDownloadFolder( 230 ctx, downloadPath, d.safeFilename) 231 if err != nil { 232 done(err) 233 return 234 } 235 } else { 236 localPath = downloadPath 237 } 238 239 done(m.updateDownload(downloadID, func(d download) download { 240 d.state.LocalPath = localPath 241 return d 242 })) 243 } 244 245 func (m *downloadManager) startDownload( 246 ctx context.Context, arg keybase1.SimpleFSStartDownloadArg) ( 247 downloadID string, err error) { 248 opid, err := m.k.SimpleFSMakeOpid(ctx) 249 if err != nil { 250 return "", err 251 } 252 downloadID = strconv.FormatInt(time.Now().UnixNano(), 16) 253 filename, safeFilename := m.getFilenames(arg.Path) 254 downloadPath, err := m.getDownloadPath(ctx, filename, downloadID) 255 if err != nil { 256 return "", err 257 } 258 // TODO for dirs maybe we want zip instead? 259 err = m.k.SimpleFSCopyRecursive(ctx, keybase1.SimpleFSCopyRecursiveArg{ 260 OpID: opid, 261 Src: keybase1.NewPathWithKbfs(arg.Path), 262 Dest: keybase1.NewPathWithLocal(downloadPath), 263 }) 264 if err != nil { 265 return "", err 266 } 267 268 func() { 269 defer m.publisher.PublishChange(keybase1.SubscriptionTopic_DOWNLOAD_STATUS) 270 m.lock.Lock() 271 defer m.lock.Unlock() 272 m.downloads[downloadID] = download{ 273 info: keybase1.DownloadInfo{ 274 DownloadID: downloadID, 275 Path: arg.Path, 276 Filename: filename, 277 StartTime: keybase1.ToTime(time.Now()), 278 IsRegularDownload: arg.IsRegularDownload, 279 }, 280 opid: opid, 281 safeFilename: safeFilename, 282 state: keybase1.DownloadState{ 283 DownloadID: downloadID, 284 }, 285 } 286 }() 287 288 bgCtx, cancelBtCtx := m.makeContext() 289 done := func(err error) { 290 _ = m.updateDownload(downloadID, func(d download) download { 291 if d.state.Done || d.state.Canceled || len(d.state.Error) > 0 { 292 return d 293 } 294 if errors.Cause(err) == context.Canceled { 295 d.state.Canceled = true 296 } else if err != nil { 297 d.state.Error = err.Error() 298 } else { 299 d.state.EndEstimate = keybase1.ToTime(time.Now()) 300 d.state.Progress = 1 301 d.state.Done = true 302 } 303 return d 304 }) 305 cancelBtCtx() 306 } 307 go m.monitorDownload(bgCtx, opid, downloadID, done) 308 go m.waitForDownload(bgCtx, downloadID, downloadPath, done) 309 310 return downloadID, nil 311 } 312 313 func (m *downloadManager) getDownloadStatus(ctx context.Context) ( 314 status keybase1.DownloadStatus) { 315 m.lock.RLock() 316 defer m.lock.RUnlock() 317 for _, download := range m.downloads { 318 status.States = append(status.States, download.state) 319 if download.info.IsRegularDownload { 320 status.RegularDownloadIDs = append( 321 status.RegularDownloadIDs, download.info.DownloadID) 322 } 323 } 324 sort.Slice(status.RegularDownloadIDs, func(i, j int) bool { 325 d1, ok := m.downloads[status.RegularDownloadIDs[i]] 326 if !ok { 327 return false 328 } 329 d2, ok := m.downloads[status.RegularDownloadIDs[j]] 330 if !ok { 331 return false 332 } 333 return d1.info.StartTime.After(d2.info.StartTime) 334 }) 335 return status 336 } 337 338 func (m *downloadManager) cancelDownload( 339 ctx context.Context, downloadID string) error { 340 d, err := m.getDownload(downloadID) 341 if err != nil { 342 return err 343 } 344 return m.k.SimpleFSCancel(ctx, d.opid) 345 } 346 347 func (m *downloadManager) dismissDownload( 348 ctx context.Context, downloadID string) { 349 // make sure it's canceled, but don't error if it's already dismissed. 350 _ = m.cancelDownload(ctx, downloadID) 351 defer m.publisher.PublishChange(keybase1.SubscriptionTopic_DOWNLOAD_STATUS) 352 m.lock.Lock() 353 defer m.lock.Unlock() 354 delete(m.downloads, downloadID) 355 } 356 357 func (m *downloadManager) getDownloadInfo(downloadID string) (keybase1.DownloadInfo, error) { 358 d, err := m.getDownload(downloadID) 359 if err != nil { 360 return keybase1.DownloadInfo{}, err 361 } 362 return d.info, nil 363 } 364 365 func (m *downloadManager) configureDownload(cacheDirOverride string, downloadDirOverride string) { 366 m.lock.Lock() 367 defer m.lock.Unlock() 368 if len(cacheDirOverride) > 0 { 369 m.cacheDir = cacheDirOverride 370 } 371 if len(downloadDirOverride) > 0 { 372 m.downloadDir = downloadDirOverride 373 } 374 }