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 }