views:

1343

answers:

8

In the comments of this page:

http://msdn.microsoft.com/en-us/library/12s31dhy.aspx

..it says that TransmitFile() cannot be used with UNC shares. As far as I can tell, this is the case; I get this error in Event Log when I attempt it:

TransmitFile failed. File Name: \\myshare1\e$\file.zip, Impersonation Enabled: 0, Token Valid: 1, HRESULT: 0x8007052e

The suggested alternative is to use WriteFile(), however, this is problematic because it loads the file into memory. In my application, the files are >200MB, so this is not going to scale.

Is there a method in ASP.NET for streaming files to users that's:

  • scalable (doesn't read entire file into RAM or occupy ASP.NET threads)
  • works with UNC shares

Mapping a network drive as a virtual directory is not an option for us. I would like to avoid copying the file to the local web server as well.

Thanks

A: 

Something you could try is opening the network file using a FileStream, and using a loop to read a chunk of the file, transmit the chunk (using Response.Write(Char[], Int32, Int32)), dispose of the chunk, and repeat until the file has been completely read.

Adam Maras
One issue with this is that it occupies an ASP.NET worker process thread during the entire file transfer. So it may not be as scalable as TransmitFile().
frankadelic
True. But it might be the happy medium you have to settle for, given that there doesn't seem to be any built-in functionality for what you're looking for.
Adam Maras
There is also a risk of script timeout with this approach, no?
frankadelic
A: 

You could write the file to a local directory, and have a robocopy job monitoring the directory to do the copy.

Since you want to avoid writing to the local server, though, you may want to investigate putting a server (HTTP or FTP, e.g.) on the target server, and writing the file to that service.

brianary
This would be my solution also, but it isn't ideal for anything that may be load balanced. We have a client who actually uses this method and runs robo every 30 seconds across all folders in the site, and it fails a few times when one robo steps on another.
Jeremy
I am running in a load balanced environment. And as you mentioned, I want to avoid writing to the local web servers.
frankadelic
A: 

could you set up another IIS website which points to the UNC, then redirect them to the file on that other website?

Response.Redirect("http://files.somewhere.com/some/file.blah");

That way it will be running in a separate worker process and have no effect on your current site, and the files will be served directly by IIS, which is clearly best.

Michael Baldry
The issue with this is that the files would be available for open access if someone knew the URL. Serving the files from the same process means I can use forms authentication to restrict access to the file.
frankadelic
well, i don't think you are going to find an idea solution, you want to serve the file without any asp.net involvement, using asp.net autentication... you need to find a solution in between.
Michael Baldry
A: 

Have you tried setting up an IIS vroot using the remote UNC path as its home directory? If it works, this may be the easiest solution. You can still enforce authentication barriers on the files (e.g. via an HttpModule, or perhaps even via the out-of-box forms auth module) but you can rely on IIS to efficiently stream the content once your auth filters give the go-ahead.

A caveat: the last time I configured IIS in a UNC scenario was a long time ago (1998!!!) and I ran into intermittent problems with files being locked on the remote machine, making updating files sometimes problematic. Dealing with recovery after a UNC server reboot was also interesting. I assume in the 11 years since then those problems have been ironed out, but you can never be sure!

Another approach may be to install a web server on the UNC machine, and then install a reverse proxy server on your web server, like the new IIS7 Application Request Routing module.

If you're willing to tie up a server thread, you can use the approach recommended in KB812406 which deals with issues around RAM consumption, timeouts, client disconnection, etc. Make sure to turn off Response Buffering!

The ideal maximum-control solution would be a "streaming HttpHandler" where, instead of having to send the output all at once, you could return a stream to ASP.NET and let ASP.NET deal with the details about chunking results to the client, dealing with disconnects, etc. But I was unable to find a good way to do this. :-(

Justin Grant
A: 

The actual error you got back was a logon failure to the file share (which isn't massively surprising considering the user IIS is likely to be running as, as well as you trying to access an administrative share). Have you tried setting up a share which has no access restrictions at all just to check if that is the issue rather than any specific limit in TransmitFile.

If that fixes it then you need to login to that share one way or another as the current user, or impersonate a user which does have permissions.

It is also worth pointing out that after abit of looking around with reflector TransmitFile can potentially end up reading the file into memory anyway, and that WriteFile has another version which takes a boolean value which decides whether to read the file into memory or not (in fact the default WriteFile passes false for this parameter). Might be worth you poking around in the code.

tyranid
I have removed access restrictions on the share - does not help.
frankadelic
A: 

I would recommend the second site approach and implement a token-based authentication mechanism. Encode an authentication cookie in the URL passed to the client via Redirect. This could be an opaque value that is shared behind-the-scenes, or it could be something as simple as a hash of a secret password and the current date, for example.

One project I did used the hashing. One server generated a hash of a shared secret password and an extension number. The second server (which was on a different network) took the same password and extension and hashed them to confirm the user was allowed to place a call from that extension to the desired phone number.

Dark Falcon
Is the second site ASP.NET? How does it process the cookie?
frankadelic
The first site is PHP and the second is a custom C# service using HttpListener. This technique is applicable anywhere, however.
Dark Falcon
A: 

The code for TransmitFile is very simple, why not modify it to do what you need?

public void TransmitFile(string filename, long offset, long length)
{
    if (filename == null)
    {
        throw new ArgumentNullException("filename");
    }
    if (offset < 0L)
    {
        throw new ArgumentException(SR.GetString("Invalid_range"), "offset");
    }
    if (length < -1L)
    {
        throw new ArgumentException(SR.GetString("Invalid_range"), "length");
    }
    filename = this.GetNormalizedFilename(filename);
    using (FileStream stream = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read))
    {
        long num = stream.Length;
        if (length == -1L)
        {
            length = num - offset;
        }
        if (num < offset)
        {
            throw new ArgumentException(SR.GetString("Invalid_range"), "offset");
        }
        if ((num - offset) < length)
        {
            throw new ArgumentException(SR.GetString("Invalid_range"), "length");
        }
        if (!this.UsingHttpWriter)
        {
            this.WriteStreamAsText(stream, offset, length);
            return;
        }
    }
    if (length > 0L)
    {
        bool supportsLongTransmitFile = (this._wr != null) && this._wr.SupportsLongTransmitFile;
        this._httpWriter.TransmitFile(filename, offset, length, this._context.IsClientImpersonationConfigured || HttpRuntime.IsOnUNCShareInternal, supportsLongTransmitFile);
    }
}



private void WriteStreamAsText(Stream f, long offset, long size)
{
    if (size < 0L)
    {
        size = f.Length - offset;
    }
    if (size > 0L)
    {
        if (offset > 0L)
        {
            f.Seek(offset, SeekOrigin.Begin);
        }
        byte[] buffer = new byte[(int) size];
        int count = f.Read(buffer, 0, (int) size);
        this._writer.Write(Encoding.Default.GetChars(buffer, 0, count));
    }
}


internal void TransmitFile(string filename, long offset, long size, bool isImpersonating, bool supportsLongTransmitFile)
{
    if (this._charBufferLength != this._charBufferFree)
    {
        this.FlushCharBuffer(true);
    }
    this._lastBuffer = null;
    this._buffers.Add(new HttpFileResponseElement(filename, offset, size, isImpersonating, supportsLongTransmitFile));
    if (!this._responseBufferingOn)
    {
        this._response.Flush();
    }
}

Thanks,

Phil.

Plip