github.com/Files-com/files-sdk-go/v3@v3.1.81/file/downloader.go (about)

     1  package file
     2  
     3  import (
     4  	"context"
     5  	"io/fs"
     6  	"os"
     7  	"path/filepath"
     8  	"sync"
     9  	"time"
    10  
    11  	files_sdk "github.com/Files-com/files-sdk-go/v3"
    12  	"github.com/Files-com/files-sdk-go/v3/directory"
    13  	"github.com/Files-com/files-sdk-go/v3/file/status"
    14  	"github.com/Files-com/files-sdk-go/v3/lib"
    15  	"github.com/Files-com/files-sdk-go/v3/lib/direction"
    16  	"github.com/Files-com/files-sdk-go/v3/lib/keyvalue"
    17  )
    18  
    19  func downloader(ctx context.Context, fileSys fs.FS, params DownloaderParams) *Job {
    20  	job := (&Job{}).Init()
    21  	SetJobParams(job, direction.DownloadType, params, params.config.Logger, fileSys)
    22  	job.Config = params.config
    23  	jobCtx := job.WithContext(ctx)
    24  	remoteFs, ok := fileSys.(lib.FSWithContext)
    25  	if ok {
    26  		fileSys = remoteFs.WithContext(jobCtx)
    27  	}
    28  	if params.RemoteFile.Path != "" {
    29  		job.LocalPath = params.LocalPath
    30  		job.RemotePath = params.RemoteFile.Path
    31  
    32  		if params.RemoteFile.Type == "directory" {
    33  			job.Type = directory.Dir
    34  		} else {
    35  			job.Type = directory.File
    36  		}
    37  	} else {
    38  		job.LocalPath = params.LocalPath
    39  		job.RemotePath = lib.NewUrlPath(params.RemotePath).PruneStartingSlash().String()
    40  		if job.RemotePath == "" {
    41  			job.RemotePath = "."
    42  		}
    43  		var remoteType directory.Type
    44  		remoteFile, err := fileSys.Open(params.RemotePath)
    45  		remoteType = directory.Dir // default to Dir not found error will have to be dealt with downstream
    46  		if err == nil {
    47  			remoteStat, err := remoteFile.Stat()
    48  			if err == nil {
    49  				if remoteStat.IsDir() {
    50  					remoteType = directory.Dir
    51  				} else {
    52  					remoteType = directory.File
    53  				}
    54  			}
    55  		}
    56  		job.LocalPath = lib.ExpandTilde(job.LocalPath)
    57  		var localType directory.Type
    58  		stats, err := os.Stat(job.LocalPath)
    59  		if os.IsNotExist(err) {
    60  			if (lib.Path{Path: job.LocalPath}).EndingSlash() { // explicit directory
    61  				localType = directory.Dir
    62  			} else if remoteType == directory.File {
    63  				localType = directory.File
    64  			} else {
    65  				localType = directory.Dir // implicit directory
    66  			}
    67  		} else if err == nil {
    68  			if stats.IsDir() {
    69  				localType = directory.Dir
    70  			} else {
    71  				localType = directory.File
    72  			}
    73  		} else {
    74  			// Propagating this error is difficult, but this error will happen again in CodeStart.
    75  		}
    76  		if (!lib.NewUrlPath(params.RemotePath).EndingSlash() && localType == directory.Dir) || remoteType == directory.File && localType == directory.Dir {
    77  			job.LocalPath = filepath.Join(job.LocalPath, lib.NewUrlPath(job.RemotePath).SwitchPathSeparator(string(os.PathSeparator)).Pop())
    78  			if remoteType == directory.File {
    79  				localType = directory.File
    80  			}
    81  		}
    82  
    83  		// Use relative path
    84  		if job.LocalPath == "" {
    85  			job.LocalPath = lib.NewUrlPath(job.RemotePath).SwitchPathSeparator(string(os.PathSeparator)).Pop()
    86  		}
    87  
    88  		job.Type = localType
    89  		job.Logger.Printf(keyvalue.New(map[string]interface{}{
    90  			"LocalPath":  job.LocalPath,
    91  			"RemotePath": job.RemotePath,
    92  		}))
    93  	}
    94  	onComplete := make(chan *DownloadStatus)
    95  	job.CodeStart = func() {
    96  		job.Scan()
    97  		go enqueueIndexedDownloads(job, jobCtx, onComplete)
    98  		WaitTellFinished(job, onComplete, func() { RetryByPolicy(jobCtx, job, job.RetryPolicy.(RetryPolicy), false) })
    99  
   100  		it := (&lib.Walk[lib.DirEntry]{
   101  			FS:                 fileSys,
   102  			Root:               lib.UrlJoinNoEscape(job.RemotePath),
   103  			ConcurrencyManager: job.Manager.FilePartsManager,
   104  			WalkFile:           lib.DirEntryWalkFile,
   105  		}).Walk(jobCtx)
   106  
   107  		for it.Next() {
   108  			if it.Resource().Err() != nil {
   109  				createIndexedStatus(Entity{error: it.Resource().Err()}, params, job)
   110  			} else {
   111  				f, err := fileSys.Open(it.Resource().Path())
   112  				createIndexedStatus(Entity{error: err, File: f, FS: fileSys}, params, job)
   113  			}
   114  		}
   115  
   116  		if it.Err() != nil {
   117  			metaFile := &DownloadStatus{
   118  				job:        job,
   119  				status:     status.Errored,
   120  				localPath:  params.LocalPath,
   121  				remotePath: params.RemotePath,
   122  				Sync:       params.Sync,
   123  				Mutex:      &sync.RWMutex{},
   124  			}
   125  			metaFile.file = files_sdk.File{
   126  				DisplayName: filepath.Base(params.LocalPath),
   127  				Type:        job.Direction.Name(),
   128  				Path:        params.RemotePath,
   129  			}
   130  			job.Add(metaFile)
   131  			job.UpdateStatus(status.Errored, metaFile, it.Err())
   132  			onComplete <- metaFile
   133  		}
   134  
   135  		job.EndScan()
   136  	}
   137  
   138  	return job
   139  }
   140  
   141  func enqueueIndexedDownloads(job *Job, jobCtx context.Context, onComplete chan *DownloadStatus) {
   142  	for !job.EndScanning.Called() || job.Count(status.Indexed) > 0 {
   143  		if f, ok := job.EnqueueNext(); ok {
   144  			if job.FilesManager.WaitWithContext(jobCtx) {
   145  				go enqueueDownload(jobCtx, job, f.(*DownloadStatus), onComplete)
   146  			} else {
   147  				job.UpdateStatus(status.Canceled, f.(*DownloadStatus), nil)
   148  				onComplete <- f.(*DownloadStatus)
   149  			}
   150  		}
   151  	}
   152  }
   153  
   154  func normalizePath(rootDestination string) string {
   155  	if rootDestination != "" && rootDestination[len(rootDestination)-1:] == string(os.PathSeparator) {
   156  	} else {
   157  		rootDestination, _ = filepath.Abs(rootDestination)
   158  	}
   159  	return rootDestination
   160  }
   161  
   162  func createIndexedStatus(f Entity, params DownloaderParams, job *Job) {
   163  	s := &DownloadStatus{
   164  		error:         f.error,
   165  		fsFile:        f.File,
   166  		FS:            f.FS,
   167  		job:           job,
   168  		Sync:          params.Sync,
   169  		status:        status.Indexed,
   170  		Mutex:         &sync.RWMutex{},
   171  		PreserveTimes: params.PreserveTimes,
   172  		dryRun:        params.DryRun,
   173  	}
   174  	var err error
   175  	if f.error == nil {
   176  		s.FileInfo, err = f.File.Stat()
   177  		if err == nil {
   178  			s.file = s.FileInfo.Sys().(files_sdk.File)
   179  			s.localPath = localPath(s.file, *job)
   180  			s.remotePath = s.file.Path
   181  		} else {
   182  			s.SetStatus(status.Errored, err)
   183  		}
   184  	}
   185  
   186  	job.Add(s)
   187  }
   188  
   189  func enqueueDownload(ctx context.Context, job *Job, downloadStatus *DownloadStatus, signal chan *DownloadStatus) {
   190  	if downloadStatus.error != nil || downloadStatus.fsFile == nil {
   191  		job.UpdateStatus(status.Errored, downloadStatus, downloadStatus.RecentError())
   192  		signal <- downloadStatus
   193  		return
   194  	}
   195  
   196  	downloadFolderItem(ctx, signal, downloadStatus)
   197  }
   198  
   199  func downloadFolderItem(ctx context.Context, signal chan *DownloadStatus, s *DownloadStatus) {
   200  	func(ctx context.Context, reportStatus *DownloadStatus) {
   201  		defer func() {
   202  			s.job.FilesManager.Done()
   203  			signal <- reportStatus
   204  		}()
   205  		dir, _ := filepath.Split(reportStatus.LocalPath())
   206  		if dir != "" {
   207  			_, err := os.Stat(dir)
   208  			if os.IsNotExist(err) {
   209  				err = os.MkdirAll(dir, 0755)
   210  				if err != nil {
   211  					reportStatus.Job().UpdateStatus(status.Errored, reportStatus, err)
   212  					return
   213  				}
   214  			}
   215  		}
   216  
   217  		remoteStat, remoteStatErr := reportStatus.fsFile.Stat()
   218  		if remoteStatErr != nil {
   219  			reportStatus.Job().UpdateStatus(status.Errored, reportStatus, remoteStatErr)
   220  			return
   221  		}
   222  
   223  		if reportStatus.Job().Sync {
   224  			localStat, localStatErr := os.Stat(reportStatus.LocalPath())
   225  			if localStatErr != nil && !os.IsNotExist(localStatErr) {
   226  				reportStatus.Job().UpdateStatus(status.Errored, reportStatus, localStatErr)
   227  				return
   228  			}
   229  			// server is not after local
   230  			if !os.IsNotExist(localStatErr) && reportStatus.Job().Sync && remoteStat.Size() == localStat.Size() {
   231  				// Local version is the same or newer
   232  				reportStatus.Job().UpdateStatus(status.Skipped, reportStatus, nil)
   233  				return
   234  			}
   235  		}
   236  
   237  		if reportStatus.dryRun {
   238  			reportStatus.Job().UpdateStatus(status.Complete, reportStatus, nil)
   239  			return
   240  		}
   241  
   242  		tmpName, err := tmpDownloadPath(reportStatus.LocalPath())
   243  		if err != nil {
   244  			reportStatus.Job().UpdateStatus(status.Errored, reportStatus, err)
   245  			return
   246  		}
   247  		reportStatus.Job().Config.LogPath(
   248  			reportStatus.RemotePath(),
   249  			map[string]interface{}{
   250  				"LocalTempPath": tmpName,
   251  			},
   252  		)
   253  		writer := openFile(tmpName, reportStatus)
   254  		downloadParts := (&DownloadParts{}).Init(
   255  			reportStatus.fsFile,
   256  			remoteStat,
   257  			reportStatus.Job().Manager.FilePartsManager,
   258  			writer,
   259  			reportStatus.Job().Config,
   260  		)
   261  
   262  		lib.AnyError(func(err error) {
   263  			reportStatus.Job().UpdateStatus(status.Errored, reportStatus, err)
   264  		},
   265  			func() error { return downloadParts.Run(ctx) },
   266  			func() error { return downloadParts.CloseError },
   267  		)
   268  
   269  		if reportStatus.Status().Is(status.Valid...) {
   270  			reportStatus.SetFinalSize(downloadParts.FinalSize())
   271  			reportStatus.Job().Config.LogPath(
   272  				reportStatus.RemotePath(),
   273  				map[string]interface{}{
   274  					"LocalTempPath": tmpName,
   275  					"FinalSize":     downloadParts.FinalSize(),
   276  				},
   277  			)
   278  			err := finalizeTmpDownload(tmpName, reportStatus.LocalPath())
   279  
   280  			if err == nil && reportStatus.PreserveTimes {
   281  				var t time.Time
   282  				if s.file.ProvidedMtime != nil {
   283  					t = *s.file.ProvidedMtime
   284  				} else if s.file.Mtime != nil {
   285  					t = *s.file.Mtime
   286  				}
   287  				if !t.IsZero() {
   288  					err = os.Chtimes(reportStatus.LocalPath(), t.Local(), t.Local())
   289  				}
   290  			}
   291  
   292  			if err != nil {
   293  				reportStatus.Job().UpdateStatus(status.Errored, reportStatus, err)
   294  			} else if reportStatus.Status().Is(status.Downloading) {
   295  				reportStatus.Job().UpdateStatus(status.Complete, reportStatus, nil)
   296  			}
   297  		} else {
   298  			err := os.Remove(tmpName) // Clean up on invalid download
   299  			if err != nil {
   300  				reportStatus.Job().UpdateStatus(status.Errored, reportStatus, err)
   301  			}
   302  		}
   303  	}(ctx, s)
   304  }
   305  
   306  func openFile(partName string, reportStatus *DownloadStatus) lib.ProgressWriter {
   307  	out, createErr := os.Create(partName)
   308  	if createErr != nil {
   309  		reportStatus.Job().UpdateStatus(status.Errored, reportStatus, createErr)
   310  	}
   311  	writer := lib.ProgressWriter{WriterAndAt: out}
   312  	writer.ProgressWatcher = func(incDownloadedBytes int64) {
   313  		reportStatus.Job().UpdateStatusWithBytes(status.Downloading, reportStatus, incDownloadedBytes)
   314  	}
   315  	return writer
   316  }
   317  
   318  func localPath(file files_sdk.File, job Job) string {
   319  	var path string
   320  	if job.Type == directory.File {
   321  		path = job.LocalPath
   322  	} else {
   323  		path = filepath.Join(normalizePath(job.LocalPath), relativePath(job, file))
   324  	}
   325  
   326  	return path
   327  }
   328  
   329  func relativePath(job Job, file files_sdk.File) string {
   330  	relativePath, err := filepath.Rel(job.RemotePath, file.Path)
   331  	if err != nil {
   332  		panic(err)
   333  	}
   334  	return relativePath
   335  }