github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/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  		// LocalPath should always use forward slash.
   263  		// Context: https://github.com/keybase/kbfs/pull/1708#issuecomment-408551015
   264  		Dest: keybase1.NewPathWithLocal(filepath.ToSlash(downloadPath)),
   265  	})
   266  	if err != nil {
   267  		return "", err
   268  	}
   269  
   270  	func() {
   271  		defer m.publisher.PublishChange(keybase1.SubscriptionTopic_DOWNLOAD_STATUS)
   272  		m.lock.Lock()
   273  		defer m.lock.Unlock()
   274  		m.downloads[downloadID] = download{
   275  			info: keybase1.DownloadInfo{
   276  				DownloadID:        downloadID,
   277  				Path:              arg.Path,
   278  				Filename:          filename,
   279  				StartTime:         keybase1.ToTime(time.Now()),
   280  				IsRegularDownload: arg.IsRegularDownload,
   281  			},
   282  			opid:         opid,
   283  			safeFilename: safeFilename,
   284  			state: keybase1.DownloadState{
   285  				DownloadID: downloadID,
   286  			},
   287  		}
   288  	}()
   289  
   290  	bgCtx, cancelBtCtx := m.makeContext()
   291  	done := func(err error) {
   292  		_ = m.updateDownload(downloadID, func(d download) download {
   293  			if d.state.Done || d.state.Canceled || len(d.state.Error) > 0 {
   294  				return d
   295  			}
   296  			if errors.Cause(err) == context.Canceled {
   297  				d.state.Canceled = true
   298  			} else if err != nil {
   299  				d.state.Error = err.Error()
   300  			} else {
   301  				d.state.EndEstimate = keybase1.ToTime(time.Now())
   302  				d.state.Progress = 1
   303  				d.state.Done = true
   304  			}
   305  			return d
   306  		})
   307  		cancelBtCtx()
   308  	}
   309  	go m.monitorDownload(bgCtx, opid, downloadID, done)
   310  	go m.waitForDownload(bgCtx, downloadID, downloadPath, done)
   311  
   312  	return downloadID, nil
   313  }
   314  
   315  func (m *downloadManager) getDownloadStatus(ctx context.Context) (
   316  	status keybase1.DownloadStatus) {
   317  	m.lock.RLock()
   318  	defer m.lock.RUnlock()
   319  	for _, download := range m.downloads {
   320  		status.States = append(status.States, download.state)
   321  		if download.info.IsRegularDownload {
   322  			status.RegularDownloadIDs = append(
   323  				status.RegularDownloadIDs, download.info.DownloadID)
   324  		}
   325  	}
   326  	sort.Slice(status.RegularDownloadIDs, func(i, j int) bool {
   327  		d1, ok := m.downloads[status.RegularDownloadIDs[i]]
   328  		if !ok {
   329  			return false
   330  		}
   331  		d2, ok := m.downloads[status.RegularDownloadIDs[j]]
   332  		if !ok {
   333  			return false
   334  		}
   335  		return d1.info.StartTime.After(d2.info.StartTime)
   336  	})
   337  	return status
   338  }
   339  
   340  func (m *downloadManager) cancelDownload(
   341  	ctx context.Context, downloadID string) error {
   342  	d, err := m.getDownload(downloadID)
   343  	if err != nil {
   344  		return err
   345  	}
   346  	return m.k.SimpleFSCancel(ctx, d.opid)
   347  }
   348  
   349  func (m *downloadManager) dismissDownload(
   350  	ctx context.Context, downloadID string) {
   351  	// make sure it's canceled, but don't error if it's already dismissed.
   352  	_ = m.cancelDownload(ctx, downloadID)
   353  	defer m.publisher.PublishChange(keybase1.SubscriptionTopic_DOWNLOAD_STATUS)
   354  	m.lock.Lock()
   355  	defer m.lock.Unlock()
   356  	delete(m.downloads, downloadID)
   357  }
   358  
   359  func (m *downloadManager) getDownloadInfo(downloadID string) (keybase1.DownloadInfo, error) {
   360  	d, err := m.getDownload(downloadID)
   361  	if err != nil {
   362  		return keybase1.DownloadInfo{}, err
   363  	}
   364  	return d.info, nil
   365  }
   366  
   367  func (m *downloadManager) configureDownload(cacheDirOverride string, downloadDirOverride string) {
   368  	m.lock.Lock()
   369  	defer m.lock.Unlock()
   370  	if len(cacheDirOverride) > 0 {
   371  		m.cacheDir = cacheDirOverride
   372  	}
   373  	if len(downloadDirOverride) > 0 {
   374  		m.downloadDir = downloadDirOverride
   375  	}
   376  }