Print This Post

ASP.Net File Upload/Download Module Version 2 (beta 2)

I’ve now uploaded beta 2 of the file upload module to the downloads section of the site. A special thanks goes to Dean Brettle who is the author of Neat Upload for his help with this version. In particular the encryption logic for storing the settings of upload controls in form fields is taken from Neat Upload (although the actual implementation is different and handled by the form parser).

More thanks go to Vince for pointing out a bug and providing the fix.

There are a few new features in this version:

  • Bug fix: two bytes extra appended to uploaded files.
  • Bug fix: progress bar does not now appear if the page fails client side validation.
  • Feature: you can now set the RequiredField property of a DJFileUpload control to true to provide client and server validation that at least one upload is provided in the control prior to submission.
  • Bug fix: the worker process is now retrieved after asserting the UnmanagedCode permission. This means that the module will now operate correctly under medium trust if the FileUploadLibrary.dll assembly is in the GAC.
  • Feature: DJFileUpload controls now have a FileProcessor property. When this property is set to a valid IFileProcessor implementation the properties of the processor will be saved and the processor used for uploads in that control only. The DJUploadController control has a similar DefaultFileProcessor property which can be used to set the default processor if one is not specified in the upload control. All normal ASP.Net file input controls will use this processor. The original method of selecting and configuring the processor in global.asax is still available.
  • Feature: if the upload module is not installed the upload controls will still operate correctly. In this instance the DJUploadController control will inspect all file inputs and pass the uploaded files directly to the appropriate file processors.

The most significant change in this version is the ability to configure file processors individually. You can see how this works in the sample default.aspx.cs page which configures a number of different processors in the page load.

protected void Page_Load(object sender, EventArgs e)
{
    // Set the default processor
    FileSystemProcessor fsd = new FileSystemProcessor();
    fsd.OutputPath = Server.MapPath("~/uploadsdefault");
    DJUploadController1.DefaultFileProcessor = fsd;

    // Change the file processor and set it's properties.
    FileSystemProcessor fs = new FileSystemProcessor();
    fs.OutputPath = Server.MapPath("~/uploads");
    DJFileUpload1.FileProcessor = fs;

    SQLProcessor sql = new SQLProcessor();
    sql.ConnectionConfig = "FUDatabase";
    DJFileUpload2.FileProcessor = sql;
}

This feature allows the use of different controllers on each page and also allows them to be configured later in the page loading cycle than in the previous global.asax implementation. This should address the requirements many users had of configuring file paths based on individual users.

Known issues:

  • There are minor styling issues in IE6.
  • The progress bar does not update correctly in Safari on OSX

These issues will be resolved in the next release.

There Are 22 Responses So Far. »

  1. Darren,

    Great article!
    I think I found a minor typo in the upload.CSS file that was causing the “select” ImageButton to be superimposed over the hidden “Browse” button. I believe that the “.upFileInputs>input.upFile” class should be “.upFileInputs.input.upFile” to fix it up.

    Also, have you tried your control in DNN v4.8.4? I’m trying to port your controls to a DNN module, but I’m having some problems with the DJFileUpload.GetController() method always returning null.

    Thanks,

    Mark

  2. Cheers for the info Mark. I’ll see if you’re right and possibly update the css in the next version.

    I haven’t tried the module in DNN but I don’t see any reason it wouldn’t work. DJFileUpload.GetController walks through the control collection looking for a control of type DJUploadController. If it doesn’t find this then it’ll return null. Have you added the controller to the page before the upload controls?

    Cheers
    Darren

  3. Darren,

    I took a step back and created a ASP.NET user control (.ascx) file outside of DNN with the controls in it (DJUploadControl appears first) and then placed the .ascx within a test .aspx file. I was still getting a failure. I then added some code to the GetController() method within the first “foreach” to check to see if the control is a UserControl and if so, loop through the UserControl.Controls collection looking for the DJUploadController. That worked, but when I went back to DNN I’m still getting the null. I’m pretty sure it has to do with the dynamic page creation in DNN.

  4. Hi Mark,

    Off the top of my head I think that the controller is probably buried in a control collection due to the way DNN constructs the pages.

    Try replacing the GetController method with the following code:

    DJUploadController GetControllerFromControls(ControlCollection cc)
    {
        DJUploadController res = null;
    
        foreach (object o in cc)
        {
            res = o as DJUploadController;
    
            if (res != null)
            {
                break;
            }
    
            Control c = o as Control;
    
            if (c != null && c.HasControls())
            {
                res = GetControllerFromControls(c.Controls);
    
                if (res != null)
                {
                    break;
                }
            }
        }
    
        return res;
    }
    
    DJUploadController GetController()
    {
        DJUploadController res = null;
    
        res = GetControllerFromControls(Page.Controls);
    
        if (res == null)
        {
            throw new Exception("An instance of the DJUploadController control must be placed at the beginning of the page before other controls.");
        }
    
        return res;
    }
    
  5. Darren,

    I’ll give it a try. After some further research I too found that the control is indeed buried within other controls in the DNN control hierarchy.

    I like your recursive approach. I was going down the road of getting the Parent of the DJFileUpload control (i.e. effectively changing Page.Form.Controls to “this.Parent.Controls”) and then searching the control collection at that level (assuming there is one). I don’t think that is quite as flexible as your approach though.

    I’ll experiment with both and let you know how it goes.

    Thanks,
    Mark

  6. Darren,

    I did a bit of a “hybrid approach”. Essentially I’m using your recursive approach, but instead of starting at the Page.Controls collection, I’m starting with the DJFileUpload object’s Parent controls collection. I believe that way there should be fewer controls (especially true for DNN) to slog through to find the UploadController.

    I made it past that hurdle. Now I’m running into a strange problem on upload. On the first upload attempt, the progress bar stays at “Waiting for uploads to start” and I get a script error in fileupload.js line 176 because the res.documentElement is null. Examination of the response (res) shows the XML is an empty string.
    If I ignore the script error, the modal window goes away and usually on a second attempt the progress bar works fine. I have seen a couple of instances where a successful file transfer completion logged me out of DNN.

    I’ll keep plugging away.

    Cheers,
    Mark

  7. Hi Mark,

    What browser are you using? I’m not finished with complete browser testing yet but in the next release I plan to support IE6/7, Firfox Win/Mac, Safari Win/Mac and Opera.

    I think your problem is probalbly security related. You need to make sure that the users of the site can call the UploadProgress.ashx handler.

    Cheers,
    Darren

    Cheers,
    Darren

  8. Darren,

    I’m using IE6.

    I’ll have a look from the security angle. Would a security issue allow some successful uploads (including the expected progress bar operation)? I didn’t think of this possibly being a security issue because of the intermittent successful runs. Typically the first upload fails, the second works fine, except I seemingly randomly get booted from DNN after the upload completes.

    Thank you,
    Mark

  9. Mark,

    I’ve just taken a look at it in IE6. Apart from the styling issues that are noted with the release it uploads ok and the progress bar displays. Can you run it in firefox and see if the progress bar is still missing? If so check the error console and see what the errors are- then I might be able to help you more.

    Security could stop the progress bar from working, although I would not have expected it to be intermitent. Can you check that the download module is the first registered module in web.config? Also, can you try running DownloadProgressHandler.ashx manually from your browser. With no parameters it should return an “empty” tag.

    Cheers,
    Darren

  10. Darren,

    I moved the download module to be the first module httpModules section in web.config. I also moved the download progress handler to be first in the httpHandlers section. I’m still seeing the intermittent failures.

    I’m not sure if DNN will allow me to request the DownloadHandler.ashx directly. So far, not much luck.

    I grabbed a copy of Firefox 3.0.1.
    When the upload progress control works, I get only one error:
    Error: source.parentElement is undefined
    Source File: http://localhost/DNN484/DesktopModules/ODPCostReporting/upload_scripts/fileupload.js
    Line: 45

    When the upload progress fails, I see two errors (in order of occurence):
    Error: source.parentElement is undefined
    Source File: http://localhost/DNN484/DesktopModules/ODPCostReporting/upload_scripts/fileupload.js
    Line: 45

    Error: res is null
    Source File: http://localhost/DNN484/DesktopModules/ODPCostReporting/upload_scripts/fileupload.js
    Line: 176

    Thanks,
    Mark

  11. Darren,

    More info…

    I found that I am getting a back via this request: http://localhost/dnn484/UploadProgress.ashx

    If I do http://localhost/dnn484/DownloadFile.ashx I get an exception (as I’d expect) that the default processor must be of type SQLProcessor for downloads.

    Thank you,

    Mark

  12. Darren,

    Thanks for your patience with this.
    Here is some more info.

    Logging out of DNN with the upload module displaying appears to trigger the upload progress modal.

    Thanks,
    Mark

  13. Hi Darren,

    This is excellent work. Thanks for sharing it.

    Sam

  14. Hi Mark,

    I think you should try beta 3 of the module, which I have just released. This fixes some issues with the progress bar that might be affecting you.

    If you want you can send me your code for the DNN module and I’ll take a look at it for you to see if I can help.

    Cheers,
    Darren

  15. Darren,

    I’ve downloaded Beta 3. I’m working on some other components right now, but I will need to return to this shortly.

    I’ll let you know how it goes.

    Thank you,
    Mark

  16. I downloaded your module and reworked it to meet my needs. I am implementing varbinary(MAX) in SQL 2005 and password protected items. Your built in write method did not work for varbinary(MAX) so I had to rework the code. I spent 1.5 weeks researching and recoding some of your code. It now works in IE and uploads fine but when I try uploading with Firefox, it always fails with ‘Exception = Could not find file’. Do you know what could be causing this? Is it Firefox compatible?

  17. ylee-

    Yes. The module has been tested in Firefox and so far seems to work fine. Beta 3 has now been released which incorporated much more browser testing. It’s dificult for me to say what your issue may be as you’ve reworked the code, although I’m not aware of any outstanding issues with Firefox.

    The SQL processor is designed to be inherited and changed through overriding the appropriate command generation methods. If you follow this approach then you can change the way it operates whilst maintaining compatibility with future releases.

    Read the beta 3 post http://darrenjohnstone.net/2008/08/10/aspnet-file-uploaddownload-module-version-2-beta-3/ and try the demo site http://demo.darrenjohnstone.net/fileupload.

    Is it the beta 1 code you are using? I say this as the beta 2 code was only released a week ago and you’ve obviously been working on it longer than that. If you send me the full exception details and information on what you’ve changed I’ll try and help you.

    Thanks,
    Darren

  18. darren,

    Thanks for responding. I used the beta 2 code which was less work to setup. I had to modify some base codes because of the methods that was required to write to the database. I am not too familiar with overriding virtual methods so I opted to change the code in the base class of sqlprocessor.cs. I added an additional parameter to the StartNewFile method to include a physical file path for the local file. I just recently found that the filename is the physical path that I need so I may change that back. I will test your original code to make sure that it wasn’t something I did to break functionality in Firefox. Also, I’ll look over beta3 to see if the improvements will help me out. If you ever get chance to do a write up of the new module in detail, I would really appreciate it. The old write up really helped me to understand http modules. Thanks a bunch.

  19. hi darren,

    I will try to up my code. Thanks again.

  20. Here’s my code. I’ll try to explain what I did.

    In SQLProcessor.cs I added declarations, properties and modified methods.

    SQLProcessor.cs:

    1. Included Crypto library for encrypting passwords.

    using Security.Crypto;

    2. Declaration section:

    string _passWord = “”;

    3. Data Access Section:

    Modified CreateInitialInsertCommand ->

    protected virtual SqlCommand CreateInitialInsertCommand(string fileName, string contentType)
    {
    if (!string.IsNullOrEmpty(HttpContext.Current.Request.Cookies["secpass"].Value))
    {
    StringCrypto strEncrypt = new StringCrypto();

    _passWord = strEncrypt.psEncrypt(HttpContext.Current.Request.Cookies["secpass"].Value.ToString());
    }

    SqlCommand cmd = new SqlCommand();

    cmd.CommandText = “Declare @gid uniqueidentifier; Set @gid = newid(); INSERT INTO ” + TableName + ” (fileGuid, fileName, fileType, fileObject, dateCreated, userId, password) ” +
    “Values(@gid, @fileName, @fileType, 0x, getdate(), @userId, @password); Set @fid = @gid”;

    cmd.Parameters.Add(new SqlParameter(”@userId”, _userId));
    cmd.Parameters.Add(new SqlParameter(”@password”, _passWord));
    cmd.Parameters.Add(new SqlParameter(”@fileName”, System.IO.Path.GetFileName(fileName)));
    cmd.Parameters.Add(new SqlParameter(”@fileType”, contentType));

    SqlParameter idParm = cmd.Parameters.Add(”@fid”, SqlDbType.UniqueIdentifier);
    idParm.Direction = ParameterDirection.Output;

    return cmd;
    }

    3. IFileProcessor Members

    Added variable _fPath ->

    string _fPath = null;

    Modified StartNewFile and Write method ->

    public object StartNewFile(string fileName, string filePath, string contentType, Dictionary headerItems)
    {
    SqlCommand insertCommand;

    // _rowId = -1;
    _rowId = null;
    _errorState = false;

    _fileName = fileName;
    _headerItems = headerItems;
    _contentType = contentType;
    _blobOffset = 0;

    try
    {
    _connection = new SqlConnection(GetConnectionString());
    _connection.Open();
    _tran = _connection.BeginTransaction();

    insertCommand = CreateInitialInsertCommand(fileName, contentType);
    insertCommand.Connection = _connection;
    insertCommand.Transaction = _tran;
    insertCommand.ExecuteNonQuery();

    //_pointer = (byte[])insertCommand.Parameters["@Pointer"].Value;
    _rowId = insertCommand.Parameters["@fid"].Value.ToString();
    _fPath = filePath;
    _tran.Commit();
    }
    catch(Exception ex)
    {
    _errorState = true;
    CleanUp(false);
    throw ex;
    }

    return _rowId;
    }

    ———

    public void Write(byte[] buffer, int offset, int count)
    {
    const int buffer_size = 8040 * 4; // ~32KB

    int totalLength = 0; // ~bytes

    string initString = @”SET NOCOUNT ON;UPDATE dbo.bc_Files SET fileObject.Write(@Bytes, 0, @Length) WHERE [fileGuid] = @fGuid”;
    string updtString = @”SET NOCOUNT ON;UPDATE dbo.bc_Files SET fileSize = @TotalLength WHERE [fileGuid] = @fGuid”;
    string cmdString = @”SET NOCOUNT ON;UPDATE dbo.bc_Files SET fileObject.Write(@Bytes, NULL, 0) WHERE [fileGuid] = @fGuid”;

    if (_errorState) return;

    // blobCommand = CreateBlobAppendCommand(_pointer, _blobOffset, toWrite);
    _connection = new SqlConnection(GetConnectionString());
    SqlCommand blobCommand = new SqlCommand(initString, _connection);

    blobCommand.Parameters.AddWithValue(”@fGuid”, new Guid(_rowId));
    // blobCommand.Parameters.Add(”@Bytes”, SqlDbType.VarBinary, buffer_size);
    // blobCommand.Parameters.AddWithValue(”@Bytes”, null);

    _tran = null;

    try
    {
    blobCommand.Connection.Open();

    // _tran = _connection.BeginTransaction();

    // blobCommand.Transaction = _tran;

    FileStream fs = new FileStream(_fPath, FileMode.Open, FileAccess.Read);

    BinaryReader br = new BinaryReader(fs);

    // Write initial file value to database column
    byte[] toWrite = br.ReadBytes(buffer_size);

    totalLength = toWrite.Length;

    blobCommand.Parameters.AddWithValue(”@Bytes”, toWrite);
    blobCommand.Parameters.AddWithValue(”@Length”, toWrite.Length);

    blobCommand.ExecuteNonQuery();

    blobCommand.CommandText = cmdString;

    toWrite = br.ReadBytes(buffer_size);

    while (toWrite.Length > 0)
    {
    blobCommand.Parameters["@Bytes"].Value = toWrite;

    totalLength += toWrite.Length;

    blobCommand.ExecuteNonQuery();

    toWrite = br.ReadBytes(buffer_size);
    }

    blobCommand.Parameters.AddWithValue(”@TotalLength”, totalLength);
    blobCommand.CommandText = updtString;
    blobCommand.ExecuteNonQuery();
    }
    catch (Exception ex)
    {
    _errorState = true;
    CleanUp(false);
    throw ex;
    }
    }

    4. Because of the extra parameter in StartNewFile I had to make changes in the base class IFileProcessor.cs. Which in turn I had to include those changes in the other classes inheriting IFileProcessor. i.e. DummyProcessor.cs,FormStream.cs,FileSystemProcessor.cs, etc…

    5. Lastly, in order for me to pickup the password set by the user from the form, I had to set a cookie before the postback of the upload module. During the CreateInitialInsertCommand() method call, I tested for the cookie and applied any encryption via the Crypto library. All values then were applied and the sql command executed.

  21. Hi Darren,

    I downloaded beta3 and reworked my logic to fit app. I still had to modify the write() method in SqlProceesor but it now works on IE and Firefox. Thanks for your help.

  22. Cheers. Glad you got it working.

Post a Response