views:

2601

answers:

6

hey All,

I would like to provide downloadable files to website users, but want to hide the URL of the files from the user... I'm thinking an HTTPHanlder could do the trick, but is it possible to retrieve a file from an external server and stream it to the user?

Perhaps somebody can give me a hint at how to accomplish this, or point me to a resource where its been done before?

Thanks heaps Greg

+1  A: 

I recommend you look at the TransmitFile method: http://msdn.microsoft.com/en-us/library/12s31dhy.aspx

jwanagel
This is kind of what i was after, but can't find much information about it (for example, will it transmit a file from an external URL or must the file exist on the same machine as the code?)
Gregorius
A: 

I've done this before. First, and obviously, the files have to be in a share on the external server that the user process of the website has access to.

As far as the HTTPHandler goes, I handled this by giving the users zip files containing the files they want to download; this way my handler could intercept any call for .zip files and stream them the zip file I create.

Here's the code (quite a chunk; I use MVP, so it is split into Handler and Presenter): Handler:

public class ZipDownloadModule: IHttpHandler, ICompressFilesView, IErrorView 
{
 CompressFilesPresenter _presenter;

 public ZipDownloadModule()
 {
  _presenter = new CompressFilesPresenter(this, this);
 }
 #region IHttpHandler Members

 public bool IsReusable
 {
  get { return true; }
 }

 public void ProcessRequest(HttpContext context)
 {
  OnDownloadFiles();
 }

 private void OnDownloadFiles()
 {
  if(Compress != null)
   Compress(this, EventArgs.Empty);
 }

 #endregion

 #region IFileListDownloadView Members

 public IEnumerable<string> FileNames
 {
  get 
  {
   string files = HttpContext.Current.Request["files"] ?? string.Empty;

   return files.Split(new Char[] { ',' });
  }
 }

 public System.IO.Stream Stream
 {
  get
  {
   HttpContext.Current.Response.ContentType = "application/x-zip-compressed";
   HttpContext.Current.Response.AppendHeader("Content-Disposition", "attachment; filename=ads.zip");
   return HttpContext.Current.Response.OutputStream;
  }
 }

 public event EventHandler Compress;

 #endregion

 #region IErrorView Members

 public string errorMessage
 {
  set {  }
 }

 #endregion
}

Presenter:

public class CompressFilesPresenter: PresenterBase<ICompressFilesView>
{
 IErrorView _errorView;

 public CompressFilesPresenter(ICompressFilesView view, IErrorView errorView)
  : base(view)
 {
  _errorView = errorView;
  this.View.Compress += new EventHandler(View_Compress);
 }

 void View_Compress(object sender, EventArgs e)
 {
  CreateZipFile();
 }

 private void CreateZipFile()
 {
  MemoryStream stream = new MemoryStream();

  try
  {
   CreateZip(stream, this.View.FileNames);

   WriteZip(stream);
  }
  catch(Exception ex)
  {
   HandleException(ex);
  }
 }

 private void WriteZip(MemoryStream stream)
 {
  byte[] data = stream.ToArray();

  this.View.Stream.Write(data, 0, data.Length);
 }

 private void CreateZip(MemoryStream stream, IEnumerable<string> filePaths)
 {
  using(ZipOutputStream s = new ZipOutputStream(stream)) // this.View.Stream))
  {
   s.SetLevel(9); // 0 = store only to 9 = best compression

   foreach(string fullPath in filePaths)
    AddFileToZip(fullPath, s);

   s.Finish();
  }
 }

 private static void AddFileToZip(string fullPath, ZipOutputStream s)
 {
  byte[] buffer = new byte[4096];

  ZipEntry entry;

  // Using GetFileName makes the result compatible with XP
  entry = new ZipEntry(Path.GetFileName(fullPath));

  entry.DateTime = DateTime.Now;
  s.PutNextEntry(entry);

  using(FileStream fs = File.OpenRead(fullPath))
  {
   int sourceBytes;
   do
   {
    sourceBytes = fs.Read(buffer, 0, buffer.Length);
    s.Write(buffer, 0, sourceBytes);
   } while(sourceBytes > 0);
  }
 }

 private void HandleException(Exception ex)
 {
  switch(ex.GetType().ToString())
  {
   case "DirectoryNotFoundException":
    _errorView.errorMessage = "The expected directory does not exist.";
    break;
   case "FileNotFoundException":
    _errorView.errorMessage = "The expected file does not exist.";
    break;
   default:
    _errorView.errorMessage = "There has been an error. If this continues please contact AMG IT Support.";
    break;
  }
 }

 private void ClearError()
 {
  _errorView.errorMessage = "";
 }
}

Hope this helps!!

Carlton Jenke
A: 

Hey all,

Just to elaborate on what i'm trying to achieve... i'm building an asp.net website, which contains a music download link. I want to protect the actual URLs of the file, and i also want to store them on an external (php) server (MUCH MUCH cheaper)...

So what i need to do is setup a stream that can grab the file from a URL (points to another server), and stream it to the Response object without the user realising its coming from another server.

Will the TransmitFile method allow streaming of a file from a completely separate server? I dont want the file to be streamed "through" my server, as that defeats the purpose (saving bandwidth)... i want the client (browser) to download the file direct from the other server.

Do i need a handler on the file hosting server perhaps? maybe a php script on the other end is the way to go... ?

Gregorius
You should edit your original question with these additional details.
jwanagel
+1  A: 

With your clarification of wanting the bandwidth to come from the external server and not yours, it changes the question quite a bit.

In order to accomplish that, the external server would have to have a website on it you could send the user to. You cannot stream the file through your site but not get hit with the bandwidth, or control it from your site but streamed through the other server, so it must be completely handled through the other site. Problem with that is a normal URL based approach would show the user the URL, which you said is the second requirement that it not show the URL.

But, couldn't you just have a generic page that serves the files on the external site, and the specifics on which file to stream would be passed through a post from the page on the original site? That would remove the URL pointing to a specific file. It would show the domain, but users would not be able to pull files without knowing the post fields.

This would not need to be an HTTPHandler, just a normal page.

Carlton Jenke
thanks Carlton... This solution sounds do-able.. so just to clarify, you're suggesting passing a form post to the other server, with the file details to download as hidden form elements? While this will work, its not foolproof... and i still need to write some php code. will look into it thanks
Gregorius
A: 

Okay, seems my quest to avoid writing/ deploying some php code is in vain... here's what i'm going to run with on the file (php) server:

http://www.zubrag.com/scripts/download.php

Then the links from my asp.net web server will point to that script, which will then download the relevant file (hence avoiding direct downloads, and allowing tracking of downloads via google analytics)... i think that'll do the trick

Thanks all Greg

Gregorius
A: 

Yes, you can streaming from a remote stream (download from another server) to output stream. Assume serviceUrl is the location of file to stream:

HttpWebRequest webrequest = (HttpWebRequest)WebRequest.Create(serviceUrl);
            webrequest.AllowAutoRedirect = false;
            webrequest.Timeout = 30 * 1000;
            webrequest.ReadWriteTimeout = 30 * 1000;
            webrequest.KeepAlive = false;

            Stream remoteStream = null;
            byte[] buffer = new byte[4 * 1024];
            int bytesRead;

            try {
                WebResponse responce = webrequest.GetResponse();
                remoteStream = responce.GetResponseStream();
                bytesRead = remoteStream.Read(buffer, 0, buffer.Length);

                Server.ScriptTimeout = 30 * 60;
                Response.Buffer = false;
                Response.BufferOutput = false;
                Response.Clear();
                Response.ContentType = "application/octet-stream";
                Response.AppendHeader("Content-Disposition", "attachment; filename=" + Uid + ".EML");
                if (responce.ContentLength != -1)
                    Response.AppendHeader("Content-Length", responce.ContentLength.ToString());

                while (bytesRead > 0 && Response.IsClientConnected) {
                    Response.OutputStream.Write(buffer, 0, bytesRead);
                    bytesRead = remoteStream.Read(buffer, 0, buffer.Length);
                }

            } catch (Exception E) {
                Logger.LogErrorFormat(LogModules.DomainUsers, "Error transfering message from remote host: {0}", E.Message);
                Response.End();
                return;
            } finally {
                if (remoteStream != null) remoteStream.Close();
            }

            Response.End();