C# HTTP breakpoint resume download

In IIS, the file corresponding to the disk path can be downloaded directly, while native IIS does not require additional configuration to resume the download. The file download address used in Xiaozhu’s project does not correspond to the file address of the disk path. Instead, it is necessary to verify whether the user has permission to download and then use fileresult to provide file download. In this way, the entire download process needs to be completed by writing code yourself. In order to make the client experience better, the function of resuming the download must be provided.

The principle of resuming download from breakpoint

In fact, the principle of resumable downloading is very simple, it is just that the Http request is different from the general download.

For example, when a browser requests a file on the server, the request it makes is as follows:
Assume that the server domain name is wwww.smallerpig.com and the file name is down.zip.

GET /down.zip HTTP/1.1
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms-
excel, application/msword, application/vnd.ms-powerpoint, */*
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)
Connection: Keep-Alive

After the server receives the request, it searches for the requested file as required, extracts the file information, and then returns it to the browser. The returned information is as follows:

200
Content-Length=106786028
Accept-Ranges=bytes
Date=Mon, 30 Apr 2001 12:56:11 GMT
ETag=W/"02ca57e173c11:95b"
Content-Type=application/octet-stream
Server=Microsoft-IIS/5.0
Last-Modified=Mon, 30 Apr 2001 12:56:11 GMT

The so-called resume download means to continue downloading from the point where the file has been downloaded. So there is one more piece of information that the client browser passes to the web server – where to start.
The following is a “browser” compiled by myself to pass the request information to the Web server. The request starts from 2000070 bytes.

GET /down.zip HTTP/1.0
User-Agent: NetFox
RANGE: bytes=2000070-
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2

If you look carefully, you will find that there is an extra line RANGE: bytes=2000070-
The meaning of this line is to tell the server to transmit the file down.zip starting from 2000070 bytes, and the previous bytes do not need to be transmitted.
After the server receives this request, the information returned is as follows:

206
Content-Length=106786028
Content-Range=bytes 2000070-106786027/106786028
Date=Mon, 30 Apr 2001 12:55:20 GMT
ETag=W/"02ca57e173c11:95b"
Content-Type=application/octet-stream
Server=Microsoft-IIS/5.0
Last-Modified=Mon, 30 Apr 2001 12:55:20 GMT

Comparing it with the information returned by the previous server, you will find that a line has been added:

Content-Range=bytes 2000070-106786027/106786028

The return code is also changed to 206 instead of 200.

Knowing the above principles, you can program breakpoint resume downloading.

C# implements breakpoint resume download

/// <summary>
///Supports breakpoint resumption
/// </summary>
/// <param name="httpContext"></param>
/// <param name="filePath"></param>
/// <param name="speed"></param>
/// <returns></returns>
public static bool DownloadFile(HttpContext httpContext, string filePath, long speed)
{<!-- -->
    bool ret = true;
    try
    {<!-- -->
        #region--Verification: HttpMethod, whether the requested file exists
        switch (httpContext.Request.HttpMethod.ToUpper())
        {<!-- --> //Currently only supports GET and HEAD methods
            case "GET":
            case "HEAD":
                break;
            default:
                httpContext.Response.StatusCode = 501;
                return false;
        }
        if (!System.IO.File.Exists(filePath))
        {<!-- -->
            httpContext.Response.StatusCode = 404;
            return false;
        }
        #endregion
    
        #region defines local variables
        long startBytes = 0;
        int packSize = 1024 * 40; //Read in blocks, each block is 40K bytes
        string fileName = Path.GetFileName(filePath);
        FileStream myFile = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
        BinaryReader br = new BinaryReader(myFile);
        long fileLength = myFile.Length;
    
        int sleep = (int)Math.Ceiling(1000.0 * packSize / speed);//Milliseconds: the time interval for reading the next data block
        string lastUpdateTiemStr = System.IO.File.GetLastWriteTimeUtc(filePath).ToString("r");
        string eTag = HttpUtility.UrlEncode(fileName, Encoding.UTF8) + lastUpdateTiemStr;//Easy to extract the request header when resuming download;
        #endregion
    
        #region--Verify: whether the file is too large, whether it is a resumable transfer, and whether it has been repaired after the last requested date
        if (myFile.Length > Int32.MaxValue)
        {<!-- -->//-------The file is too large-------
            httpContext.Response.StatusCode = 413;//The request entity is too large
            return false;
        }
    
        if (httpContext.Request.Headers["If-Range"] != null)//Corresponding response header ETag: file name + file last modification time
        {<!-- -->
            //----------Modified since the last requested date--------------
            if (httpContext.Request.Headers["If-Range"].Replace(""", "") != eTag)
            {<!-- -->//File modified
                httpContext.Response.StatusCode = 412;//Preprocessing failed
                return false;
            }
        }
        #endregion
    
        try
        {<!-- -->
            #region -------Add important response headers, parse request headers, and related verification------------------
            httpContext.Response.Clear();
            httpContext.Response.Buffer = false;
            httpContext.Response.AddHeader("Content-MD5",Common.ASE.GetMD5Hash(filePath));//Used to verify files
            httpContext.Response.AddHeader("Accept-Ranges", "bytes");//Important: Resuming the transmission is required
            httpContext.Response.AppendHeader("ETag", """ + eTag + """);//Important: Resuming the download is required
            httpContext.Response.AppendHeader("Last-Modified", lastUpdateTiemStr);//Write the last modified date into the response
            httpContext.Response.ContentType = "application/octet-stream";//MIME type: matches any file type
            httpContext.Response.AddHeader("Content-Disposition", "attachment;filename=" + HttpUtility.UrlEncode(fileName, Encoding.UTF8).Replace(" + ", " "));
            httpContext.Response.AddHeader("Content-Length", (fileLength - startBytes).ToString());
            httpContext.Response.AddHeader("Connection", "Keep-Alive");
            httpContext.Response.ContentEncoding = Encoding.UTF8;
            if (httpContext.Request.Headers["Range"] != null)
            {<!-- -->//------If it is a resumption request, get the starting position of the resumption, that is, the number of bytes that have been downloaded to the client------
                httpContext.Response.StatusCode = 206; //Important: Resume transmission is required, indicating a local scope response. Defaults to 200 on initial download
                string[] range = httpContext.Request.Headers["Range"].Split(new char[] {<!-- --> '=', '-' });//" bytes=1474560-"
                startBytes = Convert.ToInt64(range[1]);//The number of bytes that have been downloaded, that is, the starting position of this download
                if (startBytes < 0 || startBytes >= fileLength)
                {<!-- -->//Invalid starting position
                    return false;
                }
            }
            if (startBytes > 0)
            {<!-- -->//------If it is a resumption request, tell the client the starting number of bytes and the total length, so that the client can append the resuming data to the startBytes position-- --------
                httpContext.Response.AddHeader("Content-Range", string.Format(" bytes {0}-{1}/{2}", startBytes, fileLength - 1, fileLength));
            }
            #endregion
    
            #region -------Send data block to client-------------------
            br.BaseStream.Seek(startBytes, SeekOrigin.Begin);
            int maxCount = (int)Math.Ceiling((fileLength - startBytes + 0.0) / packSize);//Download in chunks, the number of chunks that the remaining part can be divided into
            for (int i = 0; i < maxCount & amp; & amp; httpContext.Response.IsClientConnected; i + + )
            {<!-- -->//The client interrupts the connection and then pauses
                httpContext.Response.BinaryWrite(br.ReadBytes(packSize));
                httpContext.Response.Flush();
                if (sleep > 1) Thread.Sleep(sleep);
            }
            #endregion
        }
        catch
        {<!-- -->
            ret = false;
        }
        finally
        {<!-- -->
            br.Close();
            myFile.Close();
        }
    }
    catch
    {<!-- -->
        ret = false;
    }
    return ret;
}

Reference article: http://blog.ncmem.com/wordpress/2023/11/13/c-http-Breakpoint Resume/
Welcome to join the group to discuss