smithvoice.com
 Y'herd thisun? 

“! could either watch it happen, or be part of it.”
-Elon Musk



Redirected Downloads

TaggedCoding, CSharp, ASP.Net

We have files for download on a NAS.  We don't want to expose them directly to everyone hitting the site and the idea of just mapping the drives makes people go nutty so, got a fast idea?  We know that by this time it's got to be so simple that we're missing the obvious but we're going in circles and a vendor wants too much money for a bridge product.


Huh.  Does the file server run IIS or Apache/Mono?


It's a Windows server so IIS is ok but we don't want to expose it to the internet.


You don't have to, if it's on your company network just expose it internally so the web servers can hit it.  That possible?


Yes.  Thankfully we're doing a house server for this feature.  Eventually it will go to an external farm but not yet.


Deal with that when it comes, you'll probably plop a file server in the same group externally at that point. The answer is going to slay you... open up notepad and copy this in:


<%@ WebService Language="C#" Class="DLService" %>
using System;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.IO;

    [WebService(Namespace = http://InsertYourURIHere.com/)]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    public class DLService : System.Web.Services.WebService
    {
        [WebMethod]
        public void dlfile(string filename)
        {
            HttpContext context = this.Context;
            Stream stream = null;
            var buffer = new byte[10000];
            String fileName = filename;

        //TODO: Protect the input such as...
            //if (string.IsNullOrEmpty(fileName))
            //    throw new Exception("Filename argument is required");


            String fullFileName = Path.Combine(string.Format("{0}",
                context.Request.PhysicalApplicationPath), "FileHolder", fileName);
            try
            {
                stream = new FileStream(fullFileName, FileMode.Open, FileAccess.Read, FileShare.Read);
                var dataToRead = stream.Length;
                while (dataToRead > 0)
                {
                    if (context.Response.IsClientConnected)
                    {
                        int length = stream.Read(buffer, 0, 10000);
                        context.Response.OutputStream.Write(buffer, 0, length);
                        context.Response.Flush();
                        buffer = new Byte[10000];
                        dataToRead = dataToRead - length;
                    }
                    else
                    {
                        dataToRead = -1;
                    }
                }
            }
            catch (Exception ex)
            {
                //TODO: log & alert
            }
            finally
            {
                if (stream != null)
                {
                    stream.Close();
                }
            }
        }
    }



Change the Path builder to point to the physical folder on the machine where the files are, giving IIS/ASPNET/NetworkServce the permissions for file reading if not in the same vdir as the web application.  Save the code as DLService.asmx and drop it in web application folder on the file server.  

Go to the web.config of the web app and add the Get ability to system.web:


      <webServices>
        <protocols>
          <add name="HttpGet"/>
        </protocols>
      </webServices>
  

Now call it from a client page in the internet-exposed web site like this...  I've got a page with a button and a textBox.  The textBox is for that file name.

        protected void butGetFromSVC_Click(object sender, EventArgs e)
        {
            string fileName = txtFile.Text;
            string queryString = "filename=" + fileName;
            string fsURL = "http://192.168.1.403:8044/DLService.asmx/dlfile"; //replace with your fileserver url
            string url = fsURL + "?" + queryString;
            try
            {
               CallToSaveFile(url, fileName) ; // if you know that exact file size then include it... , 442226370);
            }
            catch (Exception ex)
            {
                Response.Write("error getting file.");
            } 
        }


        private void CallToSaveFile(string url, string fileName, int fileSize = 0)
        {
            try
            {
                HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(url);
                webRequest.KeepAlive = false;

                //TODO:  If you want it secure, add authentication code
                //webRequest.UnsafeAuthenticatedConnectionSharing = true;
                //webRequest.Credentials = CredentialCache.DefaultCredentials;

                HttpWebResponse webResponse = (HttpWebResponse)webRequest.GetResponse();

                var streamResponse = webResponse.GetResponseStream();
                Response.Clear();
                Response.AddHeader("Content-Disposition", "attachment; filename=" + fileName);

                //Notes on Content_Length header:
                //if you know the file size, perhaps from grabbing it during the original upload, then
                //  use it in the Content-Length header to tell the browser how to calculate percentages

                //if you don't truly know the exact size, don't undervalue it or the browser will
                //  stop when it hits the specified value
                // and don't overestimate or the browser won't know to end the download and could simply
                //  stall with a full file that is still flagged as a partial
                //leaving this header out is ok, the user just wont get a percentage and/or estimated time from the browser
                //for manual hard-code testing, remember to use a string of the actual file size in bytes, not
                //  the compression gerneralized SizeOnDisk value

                if (fileSize > 0)
                {
                   Response.AddHeader("Content-Length", fileSize.ToString());
                }       

                Response.ContentType = "application/octet-stream";
                Response.Buffer = false;
                Response.BufferOutput = false;
                streamResponse.CopyTo(Response.OutputStream);

                Response.End();
            }
            catch (Exception ex)
            {
            //TODO: mainly you'll just get 500s on all problems, have to use your fave way to trap others
               throw ex;

            }
        }
    }


Replace the url to suit your servers.

All you're doing is having the internet-available web page call over to the file server for the chunks and redirecting the response stream back out to the internet caller's response stream.

That easy enough?


It works. There is some push against it because ASMX is so old school.  


Like you said, it works.  There's not much easier than notepad code, and leveraging IIS's understanding of the ASMX extension allows that to be true. 

Point them at the return, it's void;  The ASMX just a container writing out to the Response,  there's no WSDL proxy required and it's not the SOAP return that everyone is so rote-afraid of.  And even if this were spitting SOAP, when you think about it, with machines on both side of the wire - and the size of the wires - being better than they were in 2001 does it really make sense to have that "old-school" fear any more?  Compared to the hidden one size fits all bulk-up of calls and layers in hipper-right-now technologies this is like working at the metal.  Alas, perception rules. 

But, again, this is not your boss's boss's era's SOAP ASMX - it's a stream responder.


If it's just the ".asmx" that's creating the human problem, put it in a handler :).  Create a project, Add new item >> Generic handler, name it Downloader.ashx and copy in this code (with your namespace).



using System;
using System.Web;
using System.IO;

namespace FileStreamer
{
    public class Downloader : IHttpHandler
    {
        public void ProcessRequest(HttpContext context)
        {
            HttpRequest request = context.Request;
            Stream stream = null;
            var buffer = new byte[10000];
            String fileName = request["FileName"];


            //TODO: validate the argument


            String fullFileName = Path.Combine(string.Format("{0}",
                request.PhysicalApplicationPath), "FileHolder", fileName);
            try
{            {
                stream = new FileStream(fullFileName, FileMode.Open, FileAccess.Read, FileShare.Read);
                var dataToRead = stream.Length;
                while (dataToRead > 0)
                {
                    if (context.Response.IsClientConnected)
                    {
                        int length = stream.Read(buffer, 0, 10000);
                        context.Response.OutputStream.Write(buffer, 0, length);
                        context.Response.Flush();
                        buffer = new Byte[10000];

                        dataToRead = dataToRead - length;
                    }
                    else
                    {
                        dataToRead = -1;
                    }
                }
            }
            catch (Exception ex)
            {
                context.Response.Write(ex);
            }

            finally
            {
                if (stream != null)
                {
                    stream.Close();
                }
            }
        }


        public bool IsReusable
        {
            get
            {
                return false;
            }
        }
    }
}



Same thing, different file extension.  Call it from the button the same way:


string fileName = txtFile.Text;
string queryString = "FileName=" + fileName ;
string fsURL = "http://localhost:65775/Downloader.ashx"; //replace with your fileserver ashx url
string url = fsURL + "?" + queryString;

try
{
     CallToSaveFile(url, fileName);
}
catch (Exception ex)
{
    Response.Write("error getting file.");
}


Funny!  No one cared about it being an HTTPHandler :-)

Devs are so darmed smart :0.  You'll want to make the IO and client call asynchronous to spin out the blocking ASP.Net threads but that's pretty much how hard it has to be.



home     who is smith    contact smith     rss feed π
Since 1997 a place for my stuff, and it if helps you too then all the better smithvoice.com