Exemplo n.º 1
0
        /// <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);
                }
            }
        }