I'm working on a site that I've inherited that's built with ASP.Net, which I'm only slightly familiar with. One of the pages allows for a link to a document (word or pdf) that, when clicked on, prompts the user to save or open the file, but does not reveal the true path of the file. This way it prevents users from pasting a url to a file - the link is to an aspx file that checks for a valid session, and then retrieves the file.
Anyway, because there's a lot of legacy code, I need to do this with a bunch of static htm files as well, however these files need to be displayed rather than prompting the user to save them, which is what happens now. I tried changing the content type to application/text, application/html, text/html, etc. and that didn't work, then tried adding a response header of content-disposition inline. When I do that, build, and try linking to the file, I get a couple of runtime exceptions:
[FormatException: Input string was not in a correct format.]
Microsoft.VisualBasic.CompilerServices.Conversions.ParseDecimal(String Value, NumberFormatInfo NumberFormat) +206
Microsoft.VisualBasic.CompilerServices.Conversions.ToLong(String Value) +110
[InvalidCastException: Conversion from string "inline; filename=\" + myFile + " to type 'Long' is not valid.]
Microsoft.VisualBasic.CompilerServices.Conversions.ToLong(String Value) +428
cmswebasp.CMSModdoclinks.DownloadFile(String file) +1704
cmswebasp.CMSModdoclinks.Page_Load(Object sender, EventArgs e) +625
System.Web.UI.Control.OnLoad(EventArgs e) +99
System.Web.UI.Control.LoadRecursive() +50
System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) +627
Here's a snippet of code from the page:
Response.AddHeader("Content-Disposition", "inline; filename=" & file)
Dim fi As New FileInfo(myFile)
Response.AddHeader("Content-Length", fi.Length)
Dim contentType As String = ""
Dim fileExt As String = file.Split(".")(1)
Select Case fileExt
Case "htm"
contentType = "application/text"
Case Else
contentType = "application/octet-stream"
End Select
Response.ContentType = contentType
Response.WriteFile(myFile)
Do I have to do something with an htmlwriter object or something? Can't I just have it open a new browser window with the file displaying or does it have to prompt the user if used in this way??
Scrap the full page (.aspx) approach in place of using a generic handler (.ashx). An .aspx page is going to do a lot of built-in loading to initialize all the state that would normally be used in an ASP.NET web page, while the generic handler only initializes the bare minimum to send output back out to the client.
You will need to also implement System.Web.SessionState.IRequiresSessionState to get your session state when using a generic handler, as it does not load the session state by default.
An example:
Public Class FileWrapper
Implements System.Web.IHttpHandler, System.Web.SessionState.IRequiresSessionState
Sub ProcessRequest(ByVal context As HttpContext) Implements IHttpHandler.ProcessRequest
' Validate session...
' Set output content type based on extension of requested file
Dim fileName As String = context.Request.QueryString("file")
Dim fileExt As String = fileName.Split("."c)(1).ToLowerInvariant()
Select Case fileExt
Case "htm", "html"
context.Response.ContentType = System.Net.Mime.MediaTypeNames.Text.Html
Case Else
context.Response.AddHeader("Content-Disposition", "attachment; filename=" & fileName)
context.Response.ContentType = System.Net.Mime.MediaTypeNames.Application.Octet
End Select
context.Response.WriteFile(fileName)
End Sub
ReadOnly Property IsReusable() As Boolean Implements IHttpHandler.IsReusable
Get
Return False
End Get
End Property
End Class
Note: If hosting on IIS 7, you will need to remove any <httpHandler> registrations from <system.web> and register them instead as <handler> registrations in <system.webServer>.
Such as (correct as necessary to use your namespace):
<system.webServer>
<!-- For IIS 7 -->
<handlers>
<add name="FileWrapperHandler" path="FileWrapper.ashx" verb="*" type="MyNamespace.FileWrapper"/>
</handlers>
</system.webServer>
Another Note: If you are developing for a IIS 7 host using Integrated pipeline (default for IIS 7), then I would suggest using (and installing if necessary) the IIS Express option for your web project. This would be found going into Properties for your web project, the Web tab from left, then in the Servers section, select Use Local IIS Web Server radio button, and check Use IIS Express below.
This will put your development environment more in sync with how your production environment will behave.
After some discussion, it appears that you may be best served using a lobotomized Page after all, instead of a generic handler, due to the possibility of users coming to the site via links within Word documents or other sources outside the browser.
We've found that the aspx page is able to recover session in this instance, whereas the ashx does not. Thus, I am providing code for an aspx solution:
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
' Validate session...
' Set output content type based on extension of requested file
Dim fileName As String = Request.QueryString("file")
Dim fileExt As String = fileName.Split("."c)(1).ToLowerInvariant()
Select Case fileExt
Case "htm", "html"
Response.ContentType = System.Net.Mime.MediaTypeNames.Text.Html
Case Else
Response.ClearHeaders()
Response.AppendHeader("Content-Disposition", "attachment; filename=" & fileName)
Response.ContentType = System.Net.Mime.MediaTypeNames.Application.Octet
End Select
Response.WriteFile(fileName)
Response.Flush()
Response.Close()
End Sub
It doesn't matter whatever is placed in the aspx markup itself, since none of that will get rendered anyway.
Just a note that this may cause some log entries on the web host about trying to "access a closed stream" since we have closed the response stream, but the page will continue processing as usual. That's one of the prices to pay for essentially hijacking the normal page flow.
The server might be throwing out your manually-set content-type header when you call Response.WriteFile(). Find out what the actual content-type header is when the client receives it. Firebug will tell you if you look under its Net tab.
If this is the case, try setting Response.ContentType after calling Response.WriteFile(). Alternatively, you could try reading the file into a string and use Response.Write() instead of WriteFile.
Related
I'm trying to call below code in a loop hundreds of times:
Sub ExportReport(ByVal en As MyReport)
Dim warnings As Warning() = Nothing
Dim streamids As String() = Nothing
Dim mimeType As String = Nothing
Dim encoding As String = Nothing
Dim extension As String = Nothing
Dim bytes As Byte()
bytes = aReport.ServerReport.Render("WORD", Nothing, mimeType, encoding, extension, streamids, warnings)
Response.Buffer = True
Response.Clear()
Response.ContentType = mimeType
Response.AddHeader("content-disposition", "attachment; filename=" & en.ToString() & "." + extension)
Response.BinaryWrite(bytes)
Response.Flush()
Response.End()
End Sub
And I'm getting this error :
Server cannot append header after HTTP headers have been sent.
How can I change the code so that I can loop this piece of code? Thanks.
EDIT :
I added this line after Response.End()
Response.Redirect(Request.Url.AbsoluteUri)
And I get this error :
Cannot redirect after HTTP headers have been sent.
WWW works on a request / response mechanism. For every request there is only 1 response. You cannot change that basic mechanism. When browser sends a request it is expecting one and only one response. So if it receives more than 1 response, it either issues a warning to the user to block this behaviour or may choose to ignore the extra responses by itself. Thus these extra responses may be lost.
Having said that you have 2 options with you:
Zip all the files that you want to download and download as a single file.
You can use Popular framework Ionic.Zip.
First, keep all your files in a local directory on the server.
Then use this library to zip the entire folder.
Pseudo code:
Imports (var zip = New Ionic.Zip.ZipFile())
{
zip.AddDirectory("DirectoryOnDisk", "rootInZipFile")
Response.Clear()
Response.AddHeader("Content-Disposition", "attachment; filename=DownloadedFile.zip")
Response.ContentType = "application/zip"
zip.Save(Response.OutputStream)
Response.End()
}
Add a mechanism to issue multiple request using Javascript to get multiple responses, so browser still treats this behaviour as normal.
A normal web page will have a load of (headers) stuff set up for you already, but you don't want any of that: you want complete control over what is sent to the browser. If you cause a redirect to something which sends the headers shown in code later here, the browser will (normally) download the data.
In the code-behind you can have something like
Protected Sub btn_click(ByVal sender As Object, ByVal e As EventArgs) Handles btn.Click
Response.Redirect("~/sendfile.ashx?ref=" & enReference, False)
Context.ApplicationInstance.CompleteRequest()
End Sub
You will also need to add a generic handler (right-click on the project in Solution Explorer, Add->New Item... -> Visual Basic--Web--General choose "Generic Handler"; give it a name like sendfile.ashx) which is somewhat like
Imports System.IO
Public Class sendfile
Implements System.Web.IHttpHandler, IReadOnlySessionState
Sub ProcessRequest(ByVal context As HttpContext) Implements IHttpHandler.ProcessRequest
Dim enReference = context.Request.QueryString("ref")
' do whatever is needed to get the report from enReference '
Dim bytes As Byte() = aReport.ServerReport.Render("WORD", Nothing, mimeType, encoding, extension, streamids, warnings)
Dim downloadName = yourfilename & "." & yourextension
context.Response.ContentType = "application/octet-stream"
context.Response.AddHeader("content-disposition", "attachment; filename=""" & downloadName & """" )
context.Response.BinaryWrite(bytes)
End Sub
ReadOnly Property IsReusable() As Boolean Implements IHttpHandler.IsReusable
Get
Return False
End Get
End Property
End Class
and you will need to work out the code to create the data to be sent.
If you don't need to use session state then you can remove the , IReadOnlySessionState part on the Implements line.
You might need to add context.Response.Flush(). If you find that the response does not have a Content-Length header, then you ought to add one so that the browser can show a meaningful download progress.
I've seen several questions relating to downloading a PDF from a Web browser using Response, but none seem to fit the mysterious issue I'm having.
I am working on a project that requires the user to be able to click a button (btnPDF) to instantly download a PDF of a Telerik report with a specific "ID" string to the Downloads folder. This process was originally located in an ASPX Page on an IIS separate from where the button is located. When btnPDF was clicked, I used Response.Redirect to download the PDF through that page. The code to download the PDF looked like this:
Response.Clear()
Response.ContentType = result.MimeType 'this is always "application/pdf"
Response.Cache.SetCacheability(HttpCacheability.Private)
Response.Expires = -1
Response.Buffer = True
Response.AddHeader("Content-Disposition", String.Format("{0};FileName={1}", "attachment", fileName))
Response.BinaryWrite(result.DocumentBytes)
Response.End()
Note that result.DocumentBytes is a byte array containing correct bytes for the PDF.
This code worked fine. Now, instead of having the process on a separate Page in a separate project, I need to merge the process onto the same page where btnPDFis located, so that when you click btnPDF, a subroutine is called that performs the same task. I thought this would be very easy, pretty much a copy and paste. With the same code added in a new subroutine, this is what my click event handler "ButtonPDF_Click" now looks like:
Protected Sub ButtonPDF_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnPDF.Click
DownloadReportPDF(Me.RadGrid1.SelectedValue.ToString())
Dim strMessage As String = "alert('Printed PDF Sheet.');"
ScriptManager.RegisterStartupScript(Me, Me.GetType, "MyScript", strMessage, True)
End Sub
Protected Sub DownloadReportPDF(ByVal releaseMasterId As String)
'Service call to generate report source
Dim service As New TelerikReportLibrary.ReportServices.PPSReportService
Dim source As Telerik.Reporting.TypeReportSource = service.GetReportSource(releaseMasterId)
'Render PDF and download
Dim reportProcessor As New ReportProcessor()
Dim result As RenderingResult = reportProcessor.RenderReport("PDF", source, Nothing)
Dim fileName As String = result.DocumentName + "_" + releaseMasterId + "." + result.Extension
Response.Clear()
Response.ContentType = result.MimeType 'this is always "application/pdf"
Response.Cache.SetCacheability(HttpCacheability.Private)
Response.Expires = -1
Response.Buffer = True
Response.AddHeader("Content-Disposition", String.Format("{0};FileName={1}", "attachment", fileName))
Response.BinaryWrite(result.DocumentBytes)
Response.End()
End Sub
But the PDF no longer downloads. An accurate byte array is still created, but the Response portion does not result in the PDF being downloaded from the browser. I've found that putting a call to DownloadReportPDF in the Page_Load handler on the same Page successfully generates and downloads a PDF as it did before.
I can't see any reason why this isn't working, but I'm new to ASP, and I'm not great in VB. I've tried using Response.OutputStream, Response.WriteFile, and making use of a MemoryStream, among several other things that I've lost track of. I'm hoping there's something simple, maybe some sort of property of the Page or btnPDF I could be missing. Here is the markup for btnPDF, just in case:
<asp:linkButton ID="btnPDF" CssClass="btn btn-default" runat="server" Width="115px">
<i class="fa fa-file-text" title="Edit"></i> PDF
</asp:linkButton>
What could be causing such a problem? Where should I look at this point?
Let me know if more information is needed.
Thanks,
Shane
EDIT:
I experimented with setting a session variable on btnPDF_Click, and handling the PDF download on postback. Again, a valid byte array was generated, but the HttpResponse did not cause the PDF to download from the browser.
EDIT:
Building on the last edit, this tells me that calling DownloadReportPDF from Page_Load works only when IsPostBack is false. I just tested this thought, and it holds true. In the above code, if I check IsPostBack at the moment I'm trying to download the PDF, it is true. Investigating further.
Alright, I finally found a solution I'm satisfied with (though I still don't understand why I can't download the PDF using Response while IsPostBack is true).
Inspired by this thread, I put the previously posted code in an HttpHandler called PDFDownloadHandler, then used Response.Redirect in the btnPDF_Click event handler to utilize PDFDownloadHandler. This article helped me a lot on that process, as it is something I have not done before.
In case anyone else runs into this problem, here is the new PDFDownloadHandler:
Imports Microsoft.VisualBasic
Imports System.Web
Imports Telerik.Reporting
Imports Telerik.Reporting.Processing
Public Class PDFDownloadHandler
Implements IHttpHandler
Public Sub ProcessRequest(ByVal context As _
System.Web.HttpContext) Implements _
System.Web.IHttpHandler.ProcessRequest
Dim request As HttpRequest = context.Request
Dim response As HttpResponse = context.Response
Dim path As String = request.Path
If path.Contains("pps.pdfdownload") Then
Dim releaseMasterId As String = request.QueryString("ID")
If releaseMasterId IsNot Nothing Then
'Service call to generate report source
Dim service As New TelerikReportLibrary.ReportServices.PPSReportService
Dim source As Telerik.Reporting.TypeReportSource = service.GetReportSource(releaseMasterId)
'Render PDF and save
Dim reportProcessor As New ReportProcessor()
Dim result As RenderingResult = reportProcessor.RenderReport("PDF", source, Nothing)
Dim fileName As String = result.DocumentName + "_" + releaseMasterId + "." + result.Extension
response.Clear()
response.ContentType = result.MimeType
response.Cache.SetCacheability(HttpCacheability.Private)
response.Expires = -1
response.Buffer = True
response.AddHeader("Content-Disposition", String.Format("{0};FileName={1}", "attachment", fileName))
response.BinaryWrite(result.DocumentBytes)
End If
End If
response.End()
End Sub
Public ReadOnly Property IsReusable() As Boolean _
Implements System.Web.IHttpHandler.IsReusable
Get
Return False
End Get
End Property
End Class
Any further insight on why the original technique did not work is greatly appreciated.
I hav an Asp.NET site that displays a PDF/Word/Excel file but I would like to hide the location of the file i.e. if the user requests a file via a link instead of displaying the path just open the filename, I've seen some other posts on other sites but since they are old they do not wok.
Any help is appreciated.
Thanks
Use ASP.NET ASHX Handler
Some ASP.NET files are dynamically generated. They are generated with C# code or disk resources. These files do not require web forms. Instead, an ASHX generic handler is ideal. It can dynamically return an image from a query string, write XML, or any other data.
One way to get around this is to write a custom .ashx handler, which implements IHttpHandler.
Implement the ProcessRequest(..) method, and pipe the file out in the response (here's an example from an application I wrote a while back:
Public Sub ProcessRequest(ByVal context As HttpContext) Implements IHttpHandler.ProcessRequest
Dim inline As Boolean = Boolean.Parse(context.Request.QueryString("Inline"))
Dim fileName As String = context.Request.QueryString("fileName")
If (fileName.Contains("\")) Then Throw New Exception(String.Format("Invalid filename {0}. Looks like a path was attempted", fileName))
Dim filePath = ConfigurationManager.AppSettings("FileDirectory") + "\" + fileName
With context.Response
.Buffer = True
.Clear()
If inline Then
.AddHeader("content-disposition", "inline; ; filename=" & IO.Path.GetFileName(filePath))
Else
.AddHeader("content-disposition", "attachment; ; filename=" & IO.Path.GetFileName(filePath))
End If
.WriteFile(filePath)
If fileName.ToUpper.EndsWith(".PDF") Then
.ContentType = "application/pdf"
ElseIf fileName.EndsWith(".htm") Or fileName.EndsWith(".html") Then
.ContentType = "text/html"
ElseIf fileName.EndsWith(".tif") Then
.ContentType = "image/tiff"
ElseIf fileName.EndsWith(".jpeg") Or fileName.EndsWith(".jpg") Then
.ContentType = "image/jpeg"
End If
.End()
End With
I am trying to make a file upload interface in ASP.NET webforms and am looking for some advice on how to proceed.
The file upload interface is part of a website I am making on which users can post adverts. The interface is part of the "create a new advert" and will enable the user to upload up to 6 images. I am using only the asp.net FileUpload server control as I am trying to make a control which will work when users have javascript disabled. That's the background.
The upload for all 6 files occurs on button click. This stores the files in a temp folder (/UserUploads/temp) until the user submits the form in which case the files are moved to the /UserUploads folder and the references in the database or until the user hits the cancel button or navigates away in which case the files are deleted.
First question is: Is storing the files in a temp directory the right way to go about this? Or is there some better way of keeping the temp files on the server until the parent form is submitted? The only alternative I can think about is saving the files to the session, but that seems like a recipe for killing the server...
Second question: I am unclear what to do when the user just closes the browser window. I want to avoid ending up with a mess of orphaned files in the temp directory. Is there some way to make sure that all the files will get cleared out if the user doesn't go through with the form submission? Or do I just have to perform a cleanup of the temp directory every so often?
Third question: Am I doing this completely wrong and there is in fact a much better approach to uploading multiple files?
1) If you are using SQL Server, I personally prefer to store uploaded files in a varbinary(max) field and work with them by their unique ID. Then you don't have to worry about name collisions or de-sync of your DB to your filesystem. This also allows your upload process to be independent of the insertion of the parent form.
The examples below show how to grab the file stream (and metadata) from a FileUpload control in a FormView and supply it as a parameter to a SQL stored procedure. Then, a class implementing IHTTPHandler is used to retrieve files from the DB.
2) As far as clearing out temp files, I would associate each uploaded file with a temp master record so they are tied together. When the real master is confirmed, delete the temp master (and reference files from the real master). Then run a SQL Agent job on a regular interval to delete temp masters and associated files that are older than X amount of time.
Saving:
Protected Sub DetailsView1_ItemInserting(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.DetailsViewInsertEventArgs) Handles DetailsView1.ItemInserting
Dim objUploader As FileUpload = DetailsView1.FindControl("fuFile")
If objUploader.HasFile Then
Dim strFileName As String = objUploader.PostedFile.FileName
strFileName = strFileName.Substring(strFileName.LastIndexOf("\") + 1)
Dim objFileStream As System.IO.Stream = objUploader.PostedFile.InputStream
Dim arrFileImageByteArray(objFileStream.Length) As Byte
objFileStream.Read(arrFileImageByteArray, 0, objFileStream.Length)
e.Values.Insert(0, "FileImage", arrFileImageByteArray)
e.Values.Insert(1, "FileName", strFileName)
e.Values.Insert(3, "PostingDate", Now)
e.Values.Insert(5, "Application", "CorpForms")
Else
e.Cancel = True
objMessages.Add(New StatusMessage(MessageType.Warning, "File Upload canceled. No file was selected."))
End If
End Sub
Retrieving:
Public Class FileServiceHandler : Implements IHttpHandler
Public Sub ProcessRequest(ByVal context As HttpContext) Implements IHttpHandler.ProcessRequest
Dim idFileID As Guid
If context.Request.QueryString("FileID") IsNot Nothing Then
Dim strFileID As String = context.Request.QueryString("FileID")
Try
idFileID = Guid.Parse(strFileID)
Catch ex As Exception
Throw New Exception("Unable to parse File ID")
End Try
End If
Dim objConnection As New SqlConnection(ConfigurationManager.ConnectionStrings("PublicWebConnectionString").ConnectionString)
Dim objCommand As SqlCommand = objConnection.CreateCommand
Dim objReader As SqlDataReader
objCommand.CommandType = Data.CommandType.StoredProcedure
objCommand.CommandText = "spGetUploadedFile"
objCommand.Parameters.AddWithValue("FileID", idFileID.ToString)
Dim arrFileImage() As Byte = Nothing
Dim strFileName As String = String.Empty
Try
objConnection.Open()
objReader = objCommand.ExecuteReader
While objReader.Read
If Not IsDBNull(objReader("FileImage")) Then
arrFileImage = objReader("FileImage")
End If
If Not IsDBNull(objReader("FileName")) Then
strFileName = objReader("FileName")
End If
End While
Catch ex As Exception
Throw New Exception("There was a problem retreiving the file: " & ex.Message)
End Try
If objConnection.State <> Data.ConnectionState.Closed Then
objConnection.Close()
End If
If arrFileImage IsNot Nothing Then
context.Response.Clear()
context.Response.AddHeader("content-disposition", "attachment;filename=" & strFileName)
context.Response.BinaryWrite(arrFileImage)
context.Response.End()
Else
context.Response.ContentType = "text/plain"
context.Response.Write("Unable to retrieve file ID# " & idFileID.ToString)
End If
End Sub
Public ReadOnly Property IsReusable() As Boolean Implements IHttpHandler.IsReusable
Get
Return True
End Get
End Property
End Class
Web.Config in file retrieval path:
<configuration>
<system.web>
<httpHandlers>
<add verb="GET" path="*" type="MyNamespace.FileServiceHandler" />
</httpHandlers>
</system.web>
<system.webServer>
<handlers>
<add name="MyNamespace.FileServiceHandler" path="*" verb="*" type="MyNamespace.FileServiceHandler" resourceType="Unspecified" preCondition="integratedMode" />
</handlers>
</system.webServer>
</configuration>
File upload is always annoying. I recently found a great component that does what I belive all upload componentes should do.
See at:
http://aquantum-demo.appspot.com/file-upload
And a sample in C# at:
https://github.com/BoSchatzberg/jfu-CSharp-Example
And, you should store your files in a temporary folder before creating the database row. To avoid a mess of files left useless, you can use a windows temporary folder to delagate to the windows when delete or not those files.
System.IO.Path.GetTempPath()
I would recommend storing the files in the database, rather than a temporary folder.
Don't store them in Session Db - too much information, but include the SessionId in the Files Database record.
So you'd have a Files database with a table along the lines of
Id (int identity field)
Data (varbinary(max))
MimeType (varchar(50))
SessionId (varchar(50))
UserId ??
Then you'd simply need to write a scheduled SQL task to clear images where the session had expired.
i have a folder in my asp.net app conatining doc files that can be accessed only (dowwload) by a certain type of connected users(admin account or permitted other accounts)
how can i do that?
any ideas?
thanks in advance.
The App_Data folder in .NET is protected, and therefore ideal for this very purpose. I normally put sensitive files in here then have a page "ViewDoc.aspx" that performs the security checks and then sends the file to the user (using Response.Write).
Put sensitive files outside of web site root, so they can not be accessed by URL.
After that, use this HttpHandler (written in VB.NET) to serve files:
Public NotInheritable Class FileHandler
Implements IHttpHandler
Public ReadOnly Property IsReusable() As Boolean Implements System.Web.IHttpHandler.IsReusable
Get
Return False
End Get
End Property
Public Sub ProcessRequest(ByVal context As System.Web.HttpContext) Implements System.Web.IHttpHandler.ProcessRequest
If Not String.IsNullOrEmpty(context.Request.QueryString("FileName")) Then
Dim fileName As String = context.Request.QueryString("FileName")
Try
Dim filesPath As String = "D:\TheFiles\"
Dim fileInfo As New IO.FileInfo(filesPath & fileName)
If fileInfo.Exists Then
Dim fileExt As String = fileInfo.Extension.Remove(0, 1).ToUpperInvariant
If fileExt = "JPG" Then
context.Response.ContentType = "image/jpeg"
Else
context.Response.ContentType = "image/" & fileExt
End If
context.Response.TransmitFile(fileInfo.FullName)
End If
Catch ex As Exception
End Try
End If
End Sub
End Class
and register this handler in your web.config like this:
<httpHandlers>
<add verb="*" path="secfile.axd" type="MyApp.FileHandler, MyApp" validate="false"/>
</httpHandlers>
use like this:
<a href="secfile.axd?pic=sample.jpg" />
Remember adding your file types to handler and change response.contenttype by type of your file.
Using a handler is not the only way, you can use context.Response.TransmitFile(fileInfo.FullName) in your aspx file.
A simple way to do this is to NOT put these documents inside a folder of your ASP.NET app and instead, put it somewhere else in the file system that can't be accessed directly from the browser. Then programmatically, you can serve the file to the user if s/he's authorized to do so.