2019-10-10 00:52:42 -04:00
using GitHub.Services.FileContainer.Client ;
using System ;
using System.Collections.Concurrent ;
using System.Collections.Generic ;
using System.IO ;
using System.Linq ;
using System.Threading ;
using System.Threading.Tasks ;
using System.Diagnostics ;
using GitHub.Services.WebApi ;
using System.Net.Http ;
using System.Net ;
using GitHub.Runner.Sdk ;
using GitHub.Services.FileContainer ;
using GitHub.Services.Common ;
namespace GitHub.Runner.Plugins.Artifact
{
public class FileContainerServer
{
private const int _defaultFileStreamBufferSize = 4096 ;
//81920 is the default used by System.IO.Stream.CopyTo and is under the large object heap threshold (85k).
private const int _defaultCopyBufferSize = 81920 ;
private readonly ConcurrentQueue < string > _fileUploadQueue = new ConcurrentQueue < string > ( ) ;
private readonly ConcurrentQueue < DownloadInfo > _fileDownloadQueue = new ConcurrentQueue < DownloadInfo > ( ) ;
private readonly ConcurrentDictionary < string , ConcurrentQueue < string > > _fileUploadTraceLog = new ConcurrentDictionary < string , ConcurrentQueue < string > > ( ) ;
private readonly ConcurrentDictionary < string , ConcurrentQueue < string > > _fileUploadProgressLog = new ConcurrentDictionary < string , ConcurrentQueue < string > > ( ) ;
private readonly FileContainerHttpClient _fileContainerHttpClient ;
private CancellationTokenSource _uploadCancellationTokenSource ;
private CancellationTokenSource _downloadCancellationTokenSource ;
private TaskCompletionSource < int > _uploadFinished ;
private TaskCompletionSource < int > _downloadFinished ;
private Guid _projectId ;
private long _containerId ;
private string _containerPath ;
private int _uploadFilesProcessed = 0 ;
private int _downloadFilesProcessed = 0 ;
private string _sourceParentDirectory ;
public FileContainerServer (
VssConnection connection ,
Guid projectId ,
long containerId ,
string containerPath )
{
_projectId = projectId ;
_containerId = containerId ;
_containerPath = containerPath ;
// default file upload/download request timeout to 600 seconds
var fileContainerClientConnectionSetting = connection . Settings . Clone ( ) ;
if ( fileContainerClientConnectionSetting . SendTimeout < TimeSpan . FromSeconds ( 600 ) )
{
fileContainerClientConnectionSetting . SendTimeout = TimeSpan . FromSeconds ( 600 ) ;
}
var fileContainerClientConnection = new VssConnection ( connection . Uri , connection . Credentials , fileContainerClientConnectionSetting ) ;
_fileContainerHttpClient = fileContainerClientConnection . GetClient < FileContainerHttpClient > ( ) ;
}
public async Task DownloadFromContainerAsync (
RunnerActionPluginExecutionContext context ,
String destination ,
CancellationToken cancellationToken )
{
// Find out all container items need to be processed
List < FileContainerItem > containerItems = new List < FileContainerItem > ( ) ;
int retryCount = 0 ;
while ( retryCount < 3 )
{
try
{
containerItems = await _fileContainerHttpClient . QueryContainerItemsAsync ( _containerId ,
_projectId ,
_containerPath ,
cancellationToken : cancellationToken ) ;
break ;
}
catch ( OperationCanceledException ) when ( cancellationToken . IsCancellationRequested )
{
context . Debug ( $"Container query has been cancelled." ) ;
throw ;
}
catch ( Exception ex ) when ( retryCount < 2 )
{
retryCount + + ;
context . Warning ( $"Fail to query container items under #/{_containerId}/{_containerPath}, Error: {ex.Message}" ) ;
context . Debug ( ex . ToString ( ) ) ;
}
var backOff = BackoffTimerHelper . GetRandomBackoff ( TimeSpan . FromSeconds ( 5 ) , TimeSpan . FromSeconds ( 15 ) ) ;
context . Warning ( $"Back off {backOff.TotalSeconds} seconds before retry." ) ;
await Task . Delay ( backOff ) ;
}
if ( containerItems . Count = = 0 )
{
context . Output ( $"There is nothing under #/{_containerId}/{_containerPath}" ) ;
return ;
}
// container items will include both folders, files and even file with zero size
// Create all required empty folders and emptry files, gather a list of files that we need to download from server.
int foldersCreated = 0 ;
int emptryFilesCreated = 0 ;
List < DownloadInfo > downloadFiles = new List < DownloadInfo > ( ) ;
foreach ( var item in containerItems . OrderBy ( x = > x . Path ) )
{
if ( ! item . Path . StartsWith ( _containerPath , StringComparison . OrdinalIgnoreCase ) )
{
throw new ArgumentOutOfRangeException ( $"Item {item.Path} is not under #/{_containerId}/{_containerPath}" ) ;
}
var localRelativePath = item . Path . Substring ( _containerPath . Length ) . TrimStart ( '/' ) ;
var localPath = Path . Combine ( destination , localRelativePath ) ;
if ( item . ItemType = = ContainerItemType . Folder )
{
context . Debug ( $"Ensure folder exists: {localPath}" ) ;
Directory . CreateDirectory ( localPath ) ;
foldersCreated + + ;
}
else if ( item . ItemType = = ContainerItemType . File )
{
if ( item . FileLength = = 0 )
{
context . Debug ( $"Create empty file at: {localPath}" ) ;
var parentDirectory = Path . GetDirectoryName ( localPath ) ;
Directory . CreateDirectory ( parentDirectory ) ;
IOUtil . DeleteFile ( localPath ) ;
using ( new FileStream ( localPath , FileMode . Create ) )
{
}
emptryFilesCreated + + ;
}
else
{
context . Debug ( $"Prepare download {item.Path} to {localPath}" ) ;
downloadFiles . Add ( new DownloadInfo ( item . Path , localPath ) ) ;
}
}
else
{
throw new NotSupportedException ( item . ItemType . ToString ( ) ) ;
}
}
if ( foldersCreated > 0 )
{
context . Output ( $"{foldersCreated} folders created." ) ;
}
if ( emptryFilesCreated > 0 )
{
context . Output ( $"{emptryFilesCreated} empty files created." ) ;
}
if ( downloadFiles . Count = = 0 )
{
context . Output ( $"There is nothing to download" ) ;
return ;
}
// Start multi-task to download all files.
using ( _downloadCancellationTokenSource = CancellationTokenSource . CreateLinkedTokenSource ( cancellationToken ) )
{
// try download all files for the first time.
DownloadResult downloadResult = await ParallelDownloadAsync ( context , downloadFiles . AsReadOnly ( ) , Math . Min ( downloadFiles . Count , Environment . ProcessorCount ) , _downloadCancellationTokenSource . Token ) ;
if ( downloadResult . FailedFiles . Count = = 0 )
{
// all files have been download succeed.
context . Output ( $"{downloadFiles.Count} files download succeed." ) ;
return ;
}
else
{
context . Output ( $"{downloadResult.FailedFiles.Count} files failed to download, retry these files after a minute." ) ;
}
// Delay 1 min then retry failed files.
for ( int timer = 60 ; timer > 0 ; timer - = 5 )
{
context . Output ( $"Retry file download after {timer} seconds." ) ;
await Task . Delay ( TimeSpan . FromSeconds ( 5 ) , _uploadCancellationTokenSource . Token ) ;
}
// Retry download all failed files.
context . Output ( $"Start retry {downloadResult.FailedFiles.Count} failed files upload." ) ;
DownloadResult retryDownloadResult = await ParallelDownloadAsync ( context , downloadResult . FailedFiles . AsReadOnly ( ) , Math . Min ( downloadResult . FailedFiles . Count , Environment . ProcessorCount ) , _downloadCancellationTokenSource . Token ) ;
if ( retryDownloadResult . FailedFiles . Count = = 0 )
{
// all files have been download succeed after retry.
context . Output ( $"{downloadResult.FailedFiles} files download succeed after retry." ) ;
return ;
}
else
{
throw new Exception ( $"{retryDownloadResult.FailedFiles.Count} files failed to download even after retry." ) ;
}
}
}
public async Task < long > CopyToContainerAsync (
RunnerActionPluginExecutionContext context ,
String source ,
CancellationToken cancellationToken )
{
//set maxConcurrentUploads up to 2 until figure out how to use WinHttpHandler.MaxConnectionsPerServer modify DefaultConnectionLimit
int maxConcurrentUploads = Math . Min ( Environment . ProcessorCount , 2 ) ;
//context.Output($"Max Concurrent Uploads {maxConcurrentUploads}");
List < String > files ;
if ( File . Exists ( source ) )
{
files = new List < String > ( ) { source } ;
_sourceParentDirectory = Path . GetDirectoryName ( source ) ;
}
else
{
files = Directory . EnumerateFiles ( source , "*" , SearchOption . AllDirectories ) . ToList ( ) ;
_sourceParentDirectory = source . TrimEnd ( Path . DirectorySeparatorChar , Path . AltDirectorySeparatorChar ) ;
}
context . Output ( $"Uploading {files.Count()} files" ) ;
using ( _uploadCancellationTokenSource = CancellationTokenSource . CreateLinkedTokenSource ( cancellationToken ) )
{
// hook up reporting event from file container client.
_fileContainerHttpClient . UploadFileReportTrace + = UploadFileTraceReportReceived ;
_fileContainerHttpClient . UploadFileReportProgress + = UploadFileProgressReportReceived ;
try
{
// try upload all files for the first time.
UploadResult uploadResult = await ParallelUploadAsync ( context , files , maxConcurrentUploads , _uploadCancellationTokenSource . Token ) ;
2019-10-23 12:48:10 -04:00
if ( uploadResult . RetryFiles . Count = = 0 )
2019-10-10 00:52:42 -04:00
{
// all files have been upload succeed.
2019-10-23 12:48:10 -04:00
context . Output ( "File upload complete." ) ;
2019-10-10 00:52:42 -04:00
return uploadResult . TotalFileSizeUploaded ;
}
else
{
2019-10-23 12:48:10 -04:00
context . Output ( $"{uploadResult.RetryFiles.Count} files failed to upload, retry these files after a minute." ) ;
2019-10-10 00:52:42 -04:00
}
// Delay 1 min then retry failed files.
for ( int timer = 60 ; timer > 0 ; timer - = 5 )
{
context . Output ( $"Retry file upload after {timer} seconds." ) ;
await Task . Delay ( TimeSpan . FromSeconds ( 5 ) , _uploadCancellationTokenSource . Token ) ;
}
// Retry upload all failed files.
2019-10-23 12:48:10 -04:00
context . Output ( $"Start retry {uploadResult.RetryFiles.Count} failed files upload." ) ;
UploadResult retryUploadResult = await ParallelUploadAsync ( context , uploadResult . RetryFiles , maxConcurrentUploads , _uploadCancellationTokenSource . Token ) ;
2019-10-10 00:52:42 -04:00
2019-10-23 12:48:10 -04:00
if ( retryUploadResult . RetryFiles . Count = = 0 )
2019-10-10 00:52:42 -04:00
{
// all files have been upload succeed after retry.
2019-10-23 12:48:10 -04:00
context . Output ( "File upload complete after retry." ) ;
2019-10-10 00:52:42 -04:00
return uploadResult . TotalFileSizeUploaded + retryUploadResult . TotalFileSizeUploaded ;
}
else
{
throw new Exception ( "File upload failed even after retry." ) ;
}
}
finally
{
_fileContainerHttpClient . UploadFileReportTrace - = UploadFileTraceReportReceived ;
_fileContainerHttpClient . UploadFileReportProgress - = UploadFileProgressReportReceived ;
}
}
}
private async Task < DownloadResult > ParallelDownloadAsync ( RunnerActionPluginExecutionContext context , IReadOnlyList < DownloadInfo > files , int concurrentDownloads , CancellationToken token )
{
// return files that fail to download
var downloadResult = new DownloadResult ( ) ;
// nothing needs to download
if ( files . Count = = 0 )
{
return downloadResult ;
}
// ensure the file download queue is empty.
if ( ! _fileDownloadQueue . IsEmpty )
{
throw new ArgumentOutOfRangeException ( nameof ( _fileDownloadQueue ) ) ;
}
// enqueue file into download queue.
foreach ( var file in files )
{
_fileDownloadQueue . Enqueue ( file ) ;
}
// Start download monitor task.
_downloadFilesProcessed = 0 ;
_downloadFinished = new TaskCompletionSource < int > ( ) ;
Task downloadMonitor = DownloadReportingAsync ( context , files . Count ( ) , token ) ;
// Start parallel download tasks.
List < Task < DownloadResult > > parallelDownloadingTasks = new List < Task < DownloadResult > > ( ) ;
for ( int downloader = 0 ; downloader < concurrentDownloads ; downloader + + )
{
parallelDownloadingTasks . Add ( DownloadAsync ( context , downloader , token ) ) ;
}
// Wait for parallel download finish.
await Task . WhenAll ( parallelDownloadingTasks ) ;
foreach ( var downloadTask in parallelDownloadingTasks )
{
// record all failed files.
downloadResult . AddDownloadResult ( await downloadTask ) ;
}
// Stop monitor task;
_downloadFinished . TrySetResult ( 0 ) ;
await downloadMonitor ;
return downloadResult ;
}
private async Task < UploadResult > ParallelUploadAsync ( RunnerActionPluginExecutionContext context , IReadOnlyList < string > files , int concurrentUploads , CancellationToken token )
{
// return files that fail to upload and total artifact size
var uploadResult = new UploadResult ( ) ;
// nothing needs to upload
if ( files . Count = = 0 )
{
return uploadResult ;
}
// ensure the file upload queue is empty.
if ( ! _fileUploadQueue . IsEmpty )
{
throw new ArgumentOutOfRangeException ( nameof ( _fileUploadQueue ) ) ;
}
// enqueue file into upload queue.
foreach ( var file in files )
{
_fileUploadQueue . Enqueue ( file ) ;
}
// Start upload monitor task.
_uploadFilesProcessed = 0 ;
_uploadFinished = new TaskCompletionSource < int > ( ) ;
_fileUploadTraceLog . Clear ( ) ;
_fileUploadProgressLog . Clear ( ) ;
Task uploadMonitor = UploadReportingAsync ( context , files . Count ( ) , _uploadCancellationTokenSource . Token ) ;
// Start parallel upload tasks.
List < Task < UploadResult > > parallelUploadingTasks = new List < Task < UploadResult > > ( ) ;
for ( int uploader = 0 ; uploader < concurrentUploads ; uploader + + )
{
parallelUploadingTasks . Add ( UploadAsync ( context , uploader , _uploadCancellationTokenSource . Token ) ) ;
}
// Wait for parallel upload finish.
await Task . WhenAll ( parallelUploadingTasks ) ;
foreach ( var uploadTask in parallelUploadingTasks )
{
// record all failed files.
uploadResult . AddUploadResult ( await uploadTask ) ;
}
// Stop monitor task;
_uploadFinished . TrySetResult ( 0 ) ;
await uploadMonitor ;
return uploadResult ;
}
private async Task < DownloadResult > DownloadAsync ( RunnerActionPluginExecutionContext context , int downloaderId , CancellationToken token )
{
List < DownloadInfo > failedFiles = new List < DownloadInfo > ( ) ;
Stopwatch downloadTimer = new Stopwatch ( ) ;
while ( _fileDownloadQueue . TryDequeue ( out DownloadInfo fileToDownload ) )
{
token . ThrowIfCancellationRequested ( ) ;
try
{
int retryCount = 0 ;
bool downloadFailed = false ;
while ( true )
{
try
{
context . Debug ( $"Start downloading file: '{fileToDownload.ItemPath}' (Downloader {downloaderId})" ) ;
downloadTimer . Restart ( ) ;
using ( FileStream fs = new FileStream ( fileToDownload . LocalPath , FileMode . Create , FileAccess . Write , FileShare . None , bufferSize : _defaultFileStreamBufferSize , useAsync : true ) )
using ( var downloadStream = await _fileContainerHttpClient . DownloadFileAsync ( _containerId , fileToDownload . ItemPath , token , _projectId ) )
{
await downloadStream . CopyToAsync ( fs , _defaultCopyBufferSize , token ) ;
await fs . FlushAsync ( token ) ;
downloadTimer . Stop ( ) ;
context . Debug ( $"File: '{fileToDownload.LocalPath}' took {downloadTimer.ElapsedMilliseconds} milliseconds to finish download (Downloader {downloaderId})" ) ;
break ;
}
}
catch ( OperationCanceledException ) when ( token . IsCancellationRequested )
{
context . Debug ( $"Download has been cancelled while downloading {fileToDownload.ItemPath}. (Downloader {downloaderId})" ) ;
throw ;
}
catch ( Exception ex )
{
retryCount + + ;
context . Warning ( $"Fail to download '{fileToDownload.ItemPath}', error: {ex.Message} (Downloader {downloaderId})" ) ;
context . Debug ( ex . ToString ( ) ) ;
}
if ( retryCount < 3 )
{
var backOff = BackoffTimerHelper . GetRandomBackoff ( TimeSpan . FromSeconds ( 10 ) , TimeSpan . FromSeconds ( 30 ) ) ;
context . Warning ( $"Back off {backOff.TotalSeconds} seconds before retry. (Downloader {downloaderId})" ) ;
await Task . Delay ( backOff ) ;
}
else
{
// upload still failed after 3 tries.
downloadFailed = true ;
break ;
}
}
if ( downloadFailed )
{
// tracking file that failed to download.
failedFiles . Add ( fileToDownload ) ;
}
Interlocked . Increment ( ref _downloadFilesProcessed ) ;
}
catch ( Exception ex )
{
// We should never
context . Error ( $"Error '{ex.Message}' when downloading file '{fileToDownload}'. (Downloader {downloaderId})" ) ;
2021-01-21 13:45:16 -05:00
throw ex ;
2019-10-10 00:52:42 -04:00
}
}
return new DownloadResult ( failedFiles ) ;
}
private async Task < UploadResult > UploadAsync ( RunnerActionPluginExecutionContext context , int uploaderId , CancellationToken token )
{
List < string > failedFiles = new List < string > ( ) ;
long uploadedSize = 0 ;
string fileToUpload ;
Stopwatch uploadTimer = new Stopwatch ( ) ;
while ( _fileUploadQueue . TryDequeue ( out fileToUpload ) )
{
token . ThrowIfCancellationRequested ( ) ;
try
{
using ( FileStream fs = File . Open ( fileToUpload , FileMode . Open , FileAccess . Read , FileShare . Read ) )
{
string itemPath = ( _containerPath . TrimEnd ( '/' ) + "/" + fileToUpload . Remove ( 0 , _sourceParentDirectory . Length + 1 ) ) . Replace ( '\\' , '/' ) ;
2019-10-23 12:48:10 -04:00
bool failAndExit = false ;
2019-10-10 00:52:42 -04:00
try
{
2019-10-23 12:48:10 -04:00
uploadTimer . Restart ( ) ;
using ( HttpResponseMessage response = await _fileContainerHttpClient . UploadFileAsync ( _containerId , itemPath , fs , _projectId , cancellationToken : token , chunkSize : 4 * 1024 * 1024 ) )
{
if ( response = = null | | response . StatusCode ! = HttpStatusCode . Created )
{
context . Output ( $"Unable to copy file to server StatusCode={response?.StatusCode}: {response?.ReasonPhrase}. Source file path: {fileToUpload}. Target server path: {itemPath}" ) ;
if ( response ? . StatusCode = = HttpStatusCode . Conflict )
{
// fail upload task but continue with any other files
context . Error ( $"Error '{fileToUpload}' has already been uploaded." ) ;
}
else if ( _fileContainerHttpClient . IsFastFailResponse ( response ) )
{
// Fast fail: we received an http status code where we should abandon our efforts
context . Output ( $"Cannot continue uploading files, so draining upload queue of {_fileUploadQueue.Count} items." ) ;
DrainUploadQueue ( context ) ;
failedFiles . Clear ( ) ;
failAndExit = true ;
throw new UploadFailedException ( $"Critical failure uploading '{fileToUpload}'" ) ;
}
else
{
context . Debug ( $"Adding '{fileToUpload}' to retry list." ) ;
failedFiles . Add ( fileToUpload ) ;
}
throw new UploadFailedException ( $"Http failure response '{response?.StatusCode}': '{response?.ReasonPhrase}' while uploading '{fileToUpload}'" ) ;
}
uploadTimer . Stop ( ) ;
context . Debug ( $"File: '{fileToUpload}' took {uploadTimer.ElapsedMilliseconds} milliseconds to finish upload" ) ;
uploadedSize + = fs . Length ;
OutputLogForFile ( context , fileToUpload , $"Detail upload trace for file: {itemPath}" , context . Debug ) ;
}
2019-10-10 00:52:42 -04:00
}
catch ( OperationCanceledException ) when ( token . IsCancellationRequested )
{
context . Output ( $"File upload has been cancelled during upload file: '{fileToUpload}'." ) ;
throw ;
}
catch ( Exception ex )
{
context . Output ( $"Fail to upload '{fileToUpload}' due to '{ex.Message}'." ) ;
context . Output ( ex . ToString ( ) ) ;
2019-10-23 12:48:10 -04:00
OutputLogForFile ( context , fileToUpload , $"Detail upload trace for file that fail to upload: {itemPath}" , context . Output ) ;
2019-10-10 00:52:42 -04:00
2019-10-23 12:48:10 -04:00
if ( failAndExit )
2019-10-10 00:52:42 -04:00
{
2019-10-23 12:48:10 -04:00
context . Debug ( "Exiting upload." ) ;
throw ;
2019-10-10 00:52:42 -04:00
}
}
}
Interlocked . Increment ( ref _uploadFilesProcessed ) ;
}
catch ( Exception ex )
{
context . Output ( $"File error '{ex.Message}' when uploading file '{fileToUpload}'." ) ;
2021-01-21 13:45:16 -05:00
throw ex ;
2019-10-10 00:52:42 -04:00
}
}
return new UploadResult ( failedFiles , uploadedSize ) ;
}
private async Task UploadReportingAsync ( RunnerActionPluginExecutionContext context , int totalFiles , CancellationToken token )
{
int traceInterval = 0 ;
while ( ! _uploadFinished . Task . IsCompleted & & ! token . IsCancellationRequested )
{
bool hasDetailProgress = false ;
foreach ( var file in _fileUploadProgressLog )
{
string message ;
while ( file . Value . TryDequeue ( out message ) )
{
hasDetailProgress = true ;
context . Output ( message ) ;
}
}
// trace total file progress every 25 seconds when there is no file level detail progress
if ( + + traceInterval % 2 = = 0 & & ! hasDetailProgress )
{
context . Output ( $"Total file: {totalFiles} ---- Processed file: {_uploadFilesProcessed} ({(_uploadFilesProcessed * 100) / totalFiles}%)" ) ;
}
await Task . WhenAny ( _uploadFinished . Task , Task . Delay ( 5000 , token ) ) ;
}
}
private async Task DownloadReportingAsync ( RunnerActionPluginExecutionContext context , int totalFiles , CancellationToken token )
{
int traceInterval = 0 ;
while ( ! _downloadFinished . Task . IsCompleted & & ! token . IsCancellationRequested )
{
// trace total file progress every 10 seconds when there is no file level detail progress
if ( + + traceInterval % 2 = = 0 )
{
context . Output ( $"Total file: {totalFiles} ---- Downloaded file: {_downloadFilesProcessed} ({(_downloadFilesProcessed * 100) / totalFiles}%)" ) ;
}
await Task . WhenAny ( _downloadFinished . Task , Task . Delay ( 5000 , token ) ) ;
}
}
2019-10-23 12:48:10 -04:00
private void DrainUploadQueue ( RunnerActionPluginExecutionContext context )
{
while ( _fileUploadQueue . TryDequeue ( out string fileToUpload ) )
{
context . Debug ( $"Clearing upload queue: '{fileToUpload}'" ) ;
Interlocked . Increment ( ref _uploadFilesProcessed ) ;
}
}
private void OutputLogForFile ( RunnerActionPluginExecutionContext context , string itemPath , string logDescription , Action < string > log )
{
// output detail upload trace for the file.
ConcurrentQueue < string > logQueue ;
if ( _fileUploadTraceLog . TryGetValue ( itemPath , out logQueue ) )
{
log ( logDescription ) ;
string message ;
while ( logQueue . TryDequeue ( out message ) )
{
log ( message ) ;
}
}
}
2019-10-10 00:52:42 -04:00
private void UploadFileTraceReportReceived ( object sender , ReportTraceEventArgs e )
{
ConcurrentQueue < string > logQueue = _fileUploadTraceLog . GetOrAdd ( e . File , new ConcurrentQueue < string > ( ) ) ;
logQueue . Enqueue ( e . Message ) ;
}
private void UploadFileProgressReportReceived ( object sender , ReportProgressEventArgs e )
{
ConcurrentQueue < string > progressQueue = _fileUploadProgressLog . GetOrAdd ( e . File , new ConcurrentQueue < string > ( ) ) ;
progressQueue . Enqueue ( $"Uploading '{e.File}' ({(e.CurrentChunk * 100) / e.TotalChunks}%)" ) ;
}
}
public class UploadResult
{
public UploadResult ( )
{
2019-10-23 12:48:10 -04:00
RetryFiles = new List < string > ( ) ;
2019-10-10 00:52:42 -04:00
TotalFileSizeUploaded = 0 ;
}
2019-10-23 12:48:10 -04:00
public UploadResult ( List < string > retryFiles , long totalFileSizeUploaded )
2019-10-10 00:52:42 -04:00
{
2019-10-23 12:48:10 -04:00
RetryFiles = retryFiles ? ? new List < string > ( ) ;
2019-10-10 00:52:42 -04:00
TotalFileSizeUploaded = totalFileSizeUploaded ;
}
2019-10-23 12:48:10 -04:00
public List < string > RetryFiles { get ; set ; }
2019-10-10 00:52:42 -04:00
public long TotalFileSizeUploaded { get ; set ; }
public void AddUploadResult ( UploadResult resultToAdd )
{
2019-10-23 12:48:10 -04:00
this . RetryFiles . AddRange ( resultToAdd . RetryFiles ) ;
2019-10-10 00:52:42 -04:00
this . TotalFileSizeUploaded + = resultToAdd . TotalFileSizeUploaded ;
}
}
public class DownloadInfo
{
public DownloadInfo ( string itemPath , string localPath )
{
this . ItemPath = itemPath ;
this . LocalPath = localPath ;
}
public string ItemPath { get ; set ; }
public string LocalPath { get ; set ; }
}
public class DownloadResult
{
public DownloadResult ( )
{
FailedFiles = new List < DownloadInfo > ( ) ;
}
public DownloadResult ( List < DownloadInfo > failedFiles )
{
FailedFiles = failedFiles ;
}
public List < DownloadInfo > FailedFiles { get ; set ; }
public void AddDownloadResult ( DownloadResult resultToAdd )
{
this . FailedFiles . AddRange ( resultToAdd . FailedFiles ) ;
}
}
2019-10-23 12:48:10 -04:00
public class UploadFailedException : Exception
{
public UploadFailedException ( )
: base ( )
{ }
public UploadFailedException ( string message )
: base ( message )
{ }
public UploadFailedException ( string message , Exception inner )
: base ( message , inner )
{ }
}
2021-01-21 13:45:16 -05:00
}