/// <summary> /// Stash the next chunk in the stream. /// </summary> /// <param name="chunkSize">The maximum size of the next chunk.</param> /// <param name="cancellationToken">The token used to cancel the operation.</param> /// <exception cref="ArgumentOutOfRangeException"><paramref name="chunkSize"/> is not between <see cref="SiteInfo.MinUploadChunkSize"/> and <see cref="SiteInfo.MaxUploadSize"/>.</exception> /// <exception cref="InvalidOperationException">A chunk is currently uploading - or - <see cref="TotalSize"/> is zero.</exception> /// <exception cref="OperationFailedException"> /// General operation failure - or - specified as follows /// <list type="table"> /// <listheader> /// <term><see cref="OperationFailedException.ErrorCode"/></term> /// <description>Description</description> /// </listheader> /// <item> /// <term>illegal-filename</term> /// <description>The filename is not allowed.</description> /// </item> /// <item> /// <term>stashfailed</term> /// <description>Stash failure. Can be caused by file verification failure. (e.g. Extension of the file name does not match the file content.)</description> /// </item> /// </list> /// </exception> /// <returns> /// <c>true</c> if a chunk has been uploaded; /// <c>false</c> if all the chunks has already been uploaded. /// </returns> /// <remarks> /// </remarks> public async Task <UploadResult> StashNextChunkAsync(int chunkSize, CancellationToken cancellationToken) { var minChunkSize = Site.SiteInfo.MinUploadChunkSize; var maxChunkSize = Site.SiteInfo.MaxUploadSize; if (chunkSize <= 0) { throw new ArgumentOutOfRangeException(nameof(chunkSize)); } // For Wikia (MW 1.19), it supports chunked uploading, // while SiteInfo.MinUploadChunkSize and SiteInfo.MaxUploadSize are missing. if (minChunkSize > 0 && chunkSize < minChunkSize || maxChunkSize > 0 && chunkSize > maxChunkSize) { throw new ArgumentOutOfRangeException(nameof(chunkSize), $"Chunk size should be between {minChunkSize} and {maxChunkSize} on this wiki site."); } var lastState = Interlocked.CompareExchange(ref state, STATE_CHUNK_STASHING, STATE_CHUNK_IMPENDING); switch (lastState) { case STATE_CHUNK_STASHING: throw new InvalidOperationException(Prompts.ExceptionConcurrentStashing); case STATE_ALL_STASHED: throw new InvalidOperationException(Prompts.ExceptionStashingComplete); } var startingPos = SourceStream.Position; using (Site.BeginActionScope(this, chunkSize)) { RETRY: try { UploadResult result; Site.Logger.LogDebug("Start uploading chunk of {Stream} from offset {Offset}/{TotalSize}.", SourceStream, UploadedSize, TotalSize); using (var chunkStream = new MemoryStream((int)Math.Min(chunkSize, SourceStream.Length - startingPos))) { var copiedSize = await SourceStream.CopyRangeToAsync(chunkStream, chunkSize, cancellationToken); // If someone has messed with the SourceStream, this can happen. if (copiedSize == 0) { throw new InvalidOperationException(Prompts.ExceptionUnexpectedStreamEof); } chunkStream.Position = 0; var jparams = new Dictionary <string, object> { { "action", "upload" }, { "token", WikiSiteToken.Edit }, { "filename", FileName }, { "filekey", lastStashingFileKey }, { "offset", UploadedSize }, { "filesize", TotalSize }, { "comment", "Chunked" }, { "stash", true }, { "ignorewarnings", true }, { "chunk", chunkStream }, }; var jresult = await Site.InvokeMediaWikiApiAsync(new MediaWikiFormRequestMessage(jparams, true), ChunkedUploadResponseParser.Default, false, cancellationToken); // Possible error: code=stashfailed, info=Invalid chunk offset // We will retry from the server-expected offset. var err = jresult["error"]; if (err != null && (string)err["code"] == "stashfailed" && err["offset"] != null) { Site.Logger.LogWarning("Server reported: {Message}. Will retry from offset {Offset}.", (string)err["info"], (int)err["offset"]); UploadedSize = (int)err["offset"]; goto RETRY; } result = jresult["upload"].ToObject <UploadResult>(Utility.WikiJsonSerializer); // Ignore warnings, as long as we have filekey to continue the upload. if (result.FileKey == null) { Debug.Assert(result.ResultCode != UploadResultCode.Warning); throw new UnexpectedDataException(Prompts.ExceptionStashingNoFileKey); } // Note the fileKey changes after each upload. lastStashingFileKey = result.FileKey; UploadedSize += copiedSize; if (result.Offset != null && result.Offset != UploadedSize) { Site.Logger.LogWarning( "Unexpected next chunk offset reported from server: {ServerUploadedSize}. Expect: {UploadedSize}. Will use the server-reported offset.", result.Offset, UploadedSize); UploadedSize = (int)result.Offset.Value; SourceStream.Position = originalSourceStreamPosition + UploadedSize; } } Site.Logger.LogDebug("Uploaded chunk of {Stream}. Offset: {UploadedSize}/{TotalSize}, Result: {Result}.", SourceStream, UploadedSize, TotalSize, result.ResultCode); if (result.ResultCode == UploadResultCode.Success) { state = STATE_ALL_STASHED; FileKey = result.FileKey; lastStashingFileKey = null; } return(result); } catch (Exception) { // Restore stream position upon error. SourceStream.Position = startingPos; throw; } finally { Interlocked.CompareExchange(ref state, STATE_CHUNK_IMPENDING, STATE_CHUNK_STASHING); } } }