ASP.NET Mvc multi Upload - Uploadify

Als ich letzthin die Anforderung hatte, einen file upload in mvc zu machen, habe ich vergebens nach ein
@Html.FileUpload oder Ähnliches gesucht.
Die Erkenntnis: So etwas gibt es in MVC nicht, da man es auch nicht braucht.
Im Prinzip geht ein FileUpload sehr einfach und zwar so:
<form id="myForm" method="post" enctype="multipart/form-data">
<input type="file" id="FileInput" name="FileInput" />
</form>
Serverseitig kann man im Controller über
Request.Files["FileInput"] auf die Datei zugreifen.
Wichtig hierbei ist der enctype des form elements. Mit Angabe des Wertes
"multipart/form-data" wird der Inhalt einer jeden Datei in eine getrennte Sektion des multiparts Dokumentes gepackt. Mehr dazu gibt es in einem
Post von Scott Hanselman
Auf modernen Webseiten ist ein Upload mit ggf. mehreren Dateien und Fortschrittsbalken keine Seltenheit mehr.
Eigentlich gibt es viele Plugins, die etwas in der Art machen. Die meisten basieren im Hintergrund auf die Flash Komponente
swfupload, die es ermöglicht, mehrere Dateien in einem Schritt auszuwählen und hochzuladen.
Mit Hilfe eines einfachen (jQuery) Plugins -
uploadify, das mehr oder weniger alles abdeckt, was so verwendet wird, ist dies keine große Sache mehr.
Dafür einfach die
letzte Version herunterladen, die .js und .css Dateien einbinden
<link href="@Url.Content("~/Content/uploadify.css")" rel="stylesheet" type="text/css" />
<script src="@Url.Content("~/Scripts/jquery.uploadify.v2.1.4.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/swfobject.js")" type="text/javascript"></script>
und folgendes an der gewünschten Stelle in den HTML Markup schreiben:
<input type="file" id="FileInput" name="FileInput" />
Ein einfacher Aufruf des Plugins sieht in etwa so aus:
$('#FileInput').uploadify({
'uploader' : '/Content/swf/uploadify.swf',
'script' : '/Home/Upload',
'cancelImg' : '/Content/Images/cancel.png',
'folder' : '/Uploads',
'auto' : true
});
Hinweis zu den Paramtern:uploader: Das swf objekt.
script: Die MVC Controller Action.
folder: Der Pfad, in dem die Dateien gespeichert werden sollen.
Mehr dazu in der
uploadify Dokumentation.
Im Controller reicht folgende Methode aus:
public string Upload(HttpPostedFileBase fileData)
{
var fileName = Server.MapPath("~/uploads/" + System.IO.Path.GetFileName(FileData.FileName));
FileData.SaveAs(fileName);
return "ok";
}
Natürlich kann man als Rückgabe etwas schöneres mit Logik einbauen.
Der Übergabe Parameter muss als "fileData" benannt werden.
Ein weiterer Unterschied zu einem normalen Upload ist jener, dass mit
HttpPostedFileBase und nicht
HttpPostedFile gearbeitet wird.
Um ein wenig Sicherheit hinzuzufügen, wird die upload- Methode mit dem
[Authorize] Attribut dekoriert.
[Authorize]
public string Upload(...)
Das Plugin meldet nun einen Fehler.
Da es aber ohne dem Authorize Attribute im Prinzip funktioniert hat, wurde durch eine Suche im Internet schnell klar, dass es sich eigentlich um einen
Bug in Flash handelt der in dem Fall zur Folge führt, dass die ASP.NET Session und Autentifizierungs- Cookies nicht mitsendet werden.
Ein Workaround für dieses Problem bietet uploadify zum Glück mit dem "scriptData" Parameter, dem manuell das Authentifizierungstoken und die ASP.NET SessionId mitgegeben werden könen.
Folgendes wird im Aufruf des Plugins hinzugefügt/geändert:
var token = "@(Request.Cookies[FormsAuthentication.FormsCookieName]==null ? string.Empty : Request.Cookies[FormsAuthentication.FormsCookieName].Value)";
var sessionId = "@Session.SessionID";
$('#FileInputWithAuth').uploadify({
...
'scriptData': { SessionId: sessionId, Token: token }
});
Durch die
Diskussion auf StackOverFlow und dem
folgenden Blog Post bin ich auf
zwei Möglichkeiten gestoßen, um serverseitig die mitgegeben Parametern zu verarbeiten und in die Authentifizierungs- Logik einzubringen.
- Die SessionId und das Authentifizierungscookie in der Global.asax wiedererstellen
- Ein eigenes Authorize Attribut, das ebenfalls das Cookie ausliest und validiert.
Möglichkeit 1) Global.asaxIn der Begin Request methode folgenden code unterbringen:
protected void Application_BeginRequest(object sender, EventArgs e)
{
try
{
const string sessionParamName = "SessionId";
const string sessionCookieName = "ASP.NET_SessionId";
if (HttpContext.Current.Request.Form[sessionParamName] != null)
{
UpdateCookie(sessionCookieName, HttpContext.Current.Request.Form[sessionParamName]);
}
else if (HttpContext.Current.Request.QueryString[sessionParamName] != null)
{
UpdateCookie(sessionCookieName, HttpContext.Current.Request.QueryString[sessionParamName]);
}
}
catch
{
}
try
{
const string authParamName = "Token";
string authCookieName = FormsAuthentication.FormsCookieName;
if (HttpContext.Current.Request.Form[authParamName] != null)
{
UpdateCookie(authCookieName, HttpContext.Current.Request.Form[authParamName]);
}
else if (HttpContext.Current.Request.QueryString[authParamName] != null)
{
UpdateCookie(authCookieName, HttpContext.Current.Request.QueryString[authParamName]);
}
}
catch
{
}
}
private static void UpdateCookie(string cookieName, string cookieValue)
{
var cookie = HttpContext.Current.Request.Cookies.Get(cookieName) ?? new HttpCookie(cookieName);
cookie.Value = cookieValue;
HttpContext.Current.Request.Cookies.Set(cookie);
}
Möglichkeit B) Das eigene Attribut:
public class CustomAuthorizeAttribute : AuthorizeAttribute
{
private const string TokenKey = "token";
protected override bool AuthorizeCore(System.Web.HttpContextBase httpContext)
{
var token = httpContext.Request.Params[TokenKey];
if (token != null)
{
FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(token);
if (ticket != null)
{
var identity = new FormsIdentity(ticket);
var principal = ...
}
}
return base.AuthorizeCore(httpContext);
}
}
Im Prinzip funktionieren beide Möglichkeiten einwandfrei, wobei mir persönlich das Attribut besser gefällt. Zum einen, da der code in der Global.asax jedesmal ausgeführt wird und er eigentlich nur für die speziellen asynchronen upload requests benötigt wird, zum anderen kann man die Logik im Attribut besser auslagern.
Anbei gibt’s das Beispiel mit beiden Varianten.