2016-11-15 1 views
0

Dies ist eine Follow-up zu einer Frage, die ich zuvor gefragt hatte, dass zu weit geschlossen wurde geschlossen. Previous QuestionGroße Datei Download von SQL über WebApi nach benutzerdefinierten MultipartFormDataStreamProvider Upload

In dieser Frage habe ich erklärt, dass ich eine große Datei (1-3 GB) in die Datenbank hochladen musste, indem ich Stücke als einzelne Zeilen speicherte. Ich habe dies getan, indem ich die MultipartFormDataStreamProvider.GetStream-Methode überschrieben habe. Diese Methode hat einen benutzerdefinierten Stream zurückgegeben, der die gepufferten Chunks in die Datenbank geschrieben hat.

Das Problem besteht darin, dass die überschriebene GetStream-Methode die gesamte Anforderung in die Datenbank schreibt (einschließlich der Header). Es schreibt erfolgreich diese Daten, während die Speicherlevel flach bleiben, aber wenn ich die Datei herunterlade, gibt es zusätzlich zu den Dateiinhalten alle Headerinformationen in den heruntergeladenen Dateiinhalten zurück, so dass die Datei nicht geöffnet werden kann.

Gibt es eine Möglichkeit, in der überschriebenen GetStream-Methode nur den Inhalt der Datei in die Datenbank zu schreiben, ohne die Header zu schreiben?

API

[HttpPost] 
    [Route("file")] 
    [ValidateMimeMultipartContentFilter] 
    public Task<HttpResponseMessage> PostFormData() 
    { 
     var provider = new CustomMultipartFormDataStreamProvider(); 

     // Read the form data and return an async task. 
     var task = Request.Content.ReadAsMultipartAsync(provider).ContinueWith<HttpResponseMessage>(t => 
     { 
      if (t.IsFaulted || t.IsCanceled) 
      { 
       Request.CreateErrorResponse(HttpStatusCode.InternalServerError, t.Exception); 
      } 

      return Request.CreateResponse(HttpStatusCode.OK); 
     }); 

     return task; 
    } 

    [HttpGet] 
    [Route("file/{id}")] 
    public async Task<HttpResponseMessage> GetFile(string id) 
    { 
         var result = new HttpResponseMessage() 
      { 
       Content = new PushStreamContent(async (outputStream, httpContent, transportContext) => 
       { 
        await WriteDataChunksFromDBToStream(outputStream, httpContent, transportContext, id); 
       }), 
       StatusCode = HttpStatusCode.OK 
      }; 


      result.Content.Headers.ContentType = new MediaTypeHeaderValue("application/zipx"); 
      result.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment") { FileName = "test response.zipx" }; 

      return result; 
     } 

     return new HttpResponseMessage(HttpStatusCode.BadRequest); 
    } 

    private async Task WriteDataChunksFromDBToStream(Stream responseStream, HttpContent httpContent, TransportContext transportContext, string fileIdentifier) 
    { 
     // PushStreamContent requires the responseStream to be closed 
     // for signaling it that you have finished writing the response. 
     using (responseStream) 
     { 
      using (var myConn = new SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings["TestDB"].ConnectionString)) 
      { 
       await myConn.OpenAsync(); 

       using (var myCmd = new SqlCommand("ReadAttachmentChunks", myConn)) 
       { 
        myCmd.CommandType = System.Data.CommandType.StoredProcedure; 

        var fileName = new SqlParameter("@Identifier", fileIdentifier); 

        myCmd.Parameters.Add(fileName); 


        // Read data back from db in async call to avoid OutOfMemoryException when sending file back to user 
        using (var reader = await myCmd.ExecuteReaderAsync(CommandBehavior.SequentialAccess)) 
        { 
         while (await reader.ReadAsync()) 
         { 
          if (!(await reader.IsDBNullAsync(3))) 
          { 
           using (var data = reader.GetStream(3)) 
           { 
            // Asynchronously copy the stream from the server to the response stream 
            await data.CopyToAsync(responseStream); 
           } 
          } 
         } 
        } 
       } 
      } 
     }// close response stream 
    } 

Benutzerdefinierte MultipartFormDataStreamProvider GetStream Methode Implementierung

public override Stream GetStream(HttpContent parent, HttpContentHeaders headers) 
    { 
     // For form data, Content-Disposition header is a requirement 
     ContentDispositionHeaderValue contentDisposition = headers.ContentDisposition; 
     if (contentDisposition != null) 
     { 
      // If we have a file name then write contents out to AWS stream. Otherwise just write to MemoryStream 
      if (!String.IsNullOrEmpty(contentDisposition.FileName)) 
      { 
       var identifier = Guid.NewGuid().ToString(); 
       var fileName = contentDisposition.FileName;// GetLocalFileName(headers); 

       if (fileName.Contains("\\")) 
       { 
        fileName = fileName.Substring(fileName.LastIndexOf("\\") + 1).Replace("\"", ""); 
       } 

       // We won't post process files as form data 
       _isFormData.Add(false); 

       var stream = new CustomSqlStream(); 
       stream.Filename = fileName; 
       stream.Identifier = identifier; 
       stream.ContentType = headers.ContentType.MediaType; 
       stream.Description = (_formData.AllKeys.Count() > 0 && _formData["description"] != null) ? _formData["description"] : ""; 

       return stream; 
       //return new CustomSqlStream(contentDisposition.Name); 
      } 

      // We will post process this as form data 
      _isFormData.Add(true); 

      // If no filename parameter was found in the Content-Disposition header then return a memory stream. 
      return new MemoryStream(); 
     } 

     throw new InvalidOperationException("Did not find required 'Content-Disposition' header field in MIME multipart body part.."); 
     #endregion 
    } 

Implementiert Write-Methode Stream genannt durch CustomSqlStream

public override void Write(byte[] buffer, int offset, int count) 
    { 
        //write buffer to database 
     using (var myConn = new SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings["TestDB"].ConnectionString)) { 
      using (var myCmd = new SqlCommand("WriteAttachmentChunk", myConn)) { 
       myCmd.CommandType = System.Data.CommandType.StoredProcedure; 

            var pContent = new SqlParameter("@Content", buffer); 

       myCmd.Parameters.Add(pContent); 

       myConn.Open(); 
       myCmd.ExecuteNonQuery(); 

       if (myConn.State == System.Data.ConnectionState.Open) 
       { 
        myConn.Close(); 
       } 
      } 
     } 
      ((ManualResetEvent)_dataAddedEvent).Set(); 
    } 

Die gespeicherte Prozedur "ReadAttachmentChunks" ruft die entsprechenden Zeilen von der Datenbank ab, die zum Zeitpunkt des Einfügens in die Datenbank bestellt wurde. Die Art und Weise, wie der Code funktioniert, ist, dass er diese Chunks zurückzieht und dann async zurück in den PushStreamContent schreibt, um zum Benutzer zurückzukehren.

Also meine Frage ist:

Gibt es eine Möglichkeit nur den Inhalt der Datei auf die Header zusätzlich zu dem Inhalt im Gegensatz hochgeladen werden zu schreiben?

Jede Hilfe würde sehr geschätzt werden. Vielen Dank.

Antwort

1

Ich habe es endlich herausgefunden.Ich habe den Schreibprozess übermäßig kompliziert gemacht, der den größten Teil des Kampfes verursacht hat. Hier ist meine Lösung für mein erstes Problem:

Um zu verhindern, dass .net die Datei im Speicher zwischenspeichert (so dass Sie große Dateiuploads verarbeiten können), müssen Sie zunächst den WebHostBufferPolicySelector überschreiben, so dass es nicht den Eingabestream für Ihr Controller und ersetzen Sie dann den BufferPolicy Selector.

public class NoBufferPolicySelector : WebHostBufferPolicySelector 
{ 
    public override bool UseBufferedInputStream(object hostContext) 
    { 
     var context = hostContext as HttpContextBase; 

     if (context != null) 
     { 
      if (context.Request.RequestContext.RouteData.Values["controller"] != null) 
      { 
       if (string.Equals(context.Request.RequestContext.RouteData.Values["controller"].ToString(), "upload", StringComparison.InvariantCultureIgnoreCase)) 
        return false; 
      } 
     } 

     return true; 
    } 

    public override bool UseBufferedOutputStream(HttpResponseMessage response) 
    { 
     return base.UseBufferedOutputStream(response); 
    } 
} 

dann die BufferPolicy Selector für den Ersatz

GlobalConfiguration.Configuration.Services.Replace(typeof(IHostBufferPolicySelector), new NoBufferPolicySelector()); 

Dann das Standardverhalten von mit dem Datei-Stream auf der Platte geschrieben, um zu vermeiden, müssen Sie einen Stream-Anbieter schaffen, anstatt in die Datenbank geschrieben werden. Dazu erben Sie MultipartStreamProvider und überschreiben die GetStream-Methode, um den Stream zurückzugeben, der in Ihre Datenbank schreibt.

public override Stream GetStream(HttpContent parent, HttpContentHeaders headers) 
    { 
     // For form data, Content-Disposition header is a requirement 
     ContentDispositionHeaderValue contentDisposition = headers.ContentDisposition; 
     if (contentDisposition != null && !String.IsNullOrEmpty(contentDisposition.FileName)) 
     { 
      // We won't post process files as form data 
      _isFormData.Add(false); 

      //create unique identifier for this file upload 
      var identifier = Guid.NewGuid(); 
      var fileName = contentDisposition.FileName; 

      var boundaryObj = parent.Headers.ContentType.Parameters.SingleOrDefault(a => a.Name == "boundary"); 

      var boundary = (boundaryObj != null) ? boundaryObj.Value : ""; 

      if (fileName.Contains("\\")) 
      { 
       fileName = fileName.Substring(fileName.LastIndexOf("\\") + 1).Replace("\"", ""); 
      } 

      //write parent container for the file chunks that are being stored 
      WriteLargeFileContainer(fileName, identifier, headers.ContentType.MediaType, boundary); 

      //create an instance of the custom stream that will write the chunks to the database 
      var stream = new CustomSqlStream(); 
      stream.Filename = fileName; 
      stream.FullFilename = contentDisposition.FileName.Replace("\"", ""); 
      stream.Identifier = identifier.ToString(); 
      stream.ContentType = headers.ContentType.MediaType; 
      stream.Boundary = (!string.IsNullOrEmpty(boundary)) ? boundary : ""; 

      return stream; 
     } 
     else 
     { 
      // We will post process this as form data 
      _isFormData.Add(true); 

      // If no filename parameter was found in the Content-Disposition header then return a memory stream. 
      return new MemoryStream(); 
     } 
    } 

Der benutzerdefinierte Stream, den Sie erstellen, muss Stream erben und die Write-Methode überschreiben. Dies ist, wo ich das Problem überlegte und dachte, dass ich die Grenzüberschriften analysieren musste, die über den Pufferparameter übergeben wurden. Dies wird jedoch für Sie erledigt, indem Sie die Parameter offset und count nutzen.

Von dort ist es nur einstecken die API-Methoden für den Upload und Download. für den Upload:

public Task<HttpResponseMessage> PostFormData() 
    { 
     var provider = new CustomMultipartLargeFileStreamProvider(); 

     // Read the form data and return an async task. 
     var task = Request.Content.ReadAsMultipartAsync(provider).ContinueWith<HttpResponseMessage>(t => 
     { 
      if (t.IsFaulted || t.IsCanceled) 
      { 
       Request.CreateErrorResponse(HttpStatusCode.InternalServerError, t.Exception); 
      } 

      return Request.CreateResponse(HttpStatusCode.OK); 
     }); 

     return task; 
    } 

Zum Herunterladen und um den Speicherbedarf gering zu halten, nutzte ich die PushStreamContent die Stücke an den Benutzer zurück zu schieben:

[HttpGet] 
    [Route("file/{id}")] 
    public async Task<HttpResponseMessage> GetFile(string id) 
    { 
     string mimeType = string.Empty; 
     string filename = string.Empty; 
     if (!string.IsNullOrEmpty(id)) 
     { 
      //get the headers for the file being sent back to the user 
      using (var myConn = new SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings["PortalBetaConnectionString"].ConnectionString)) 
      { 
       using (var myCmd = new SqlCommand("ReadLargeFileInfo", myConn)) 
       { 
        myCmd.CommandType = System.Data.CommandType.StoredProcedure; 

        var pIdentifier = new SqlParameter("@Identifier", id); 

        myCmd.Parameters.Add(pIdentifier); 

        myConn.Open(); 

        var dataReader = myCmd.ExecuteReader(); 

        if (dataReader.HasRows) 
        { 
         while (dataReader.Read()) 
         { 
          mimeType = dataReader.GetString(0); 
          filename = dataReader.GetString(1); 
         } 
        } 
       } 
      } 


      var result = new HttpResponseMessage() 
      { 
       Content = new PushStreamContent(async (outputStream, httpContent, transportContext) => 
       { 
        //pull the data back from the db and stream the data back to the user 
        await WriteDataChunksFromDBToStream(outputStream, httpContent, transportContext, id); 
       }), 
       StatusCode = HttpStatusCode.OK 
      }; 

      result.Content.Headers.ContentType = new MediaTypeHeaderValue(mimeType);// "application/octet-stream"); 
      result.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment") { FileName = filename }; 

      return result; 
     } 

     return new HttpResponseMessage(HttpStatusCode.BadRequest); 
    } 

    private async Task WriteDataChunksFromDBToStream(Stream responseStream, HttpContent httpContent, TransportContext transportContext, string fileIdentifier) 
    { 
     // PushStreamContent requires the responseStream to be closed 
     // for signaling it that you have finished writing the response. 
     using (responseStream) 
     { 
      using (var myConn = new SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings["PortalBetaConnectionString"].ConnectionString)) 
      { 
       await myConn.OpenAsync(); 

       //stored proc to pull the data back from the db 
       using (var myCmd = new SqlCommand("ReadAttachmentChunks", myConn)) 
       { 
        myCmd.CommandType = System.Data.CommandType.StoredProcedure; 

        var fileName = new SqlParameter("@Identifier", fileIdentifier); 

        myCmd.Parameters.Add(fileName); 

        // The reader needs to be executed with the SequentialAccess behavior to enable network streaming 
        // Otherwise ReadAsync will buffer the entire BLOB into memory which can cause scalability issues or even OutOfMemoryExceptions 
        using (var reader = await myCmd.ExecuteReaderAsync(CommandBehavior.SequentialAccess)) 
        { 
         while (await reader.ReadAsync()) 
         { 
          //confirm the column that has the binary data of the file returned is not null 
          if (!(await reader.IsDBNullAsync(0))) 
          { 
           //read the binary data of the file into a stream 
           using (var data = reader.GetStream(0)) 
           { 
            // Asynchronously copy the stream from the server to the response stream 
            await data.CopyToAsync(responseStream); 
            await data.FlushAsync(); 
           } 
          } 
         } 
        } 
       } 
      } 
     }// close response stream 
    } 
0

Ugh. Das ist eklig. Beim Upload müssen Sie sicherstellen, dass

  • die Header aus dem Inhaltsbereich trennen - Sie müssen den Anforderungen RFC-Dokumente für HTTP folgen.
  • Chunked-Übertragungen zulassen
  • Natürlich wird der Inhaltsteil (sofern Sie keinen Text übertragen) binär in Strings codiert.
  • Erlauben Übertragungen, die komprimiert sind, d. H. GZIP oder DEFLATE.
  • Vielleicht - nur vielleicht - nehmen Sie die Codierung in Betracht (ASCII, Unicode, UTF8, etc).
  • Sie können nicht wirklich sicherstellen, dass Sie die richtigen Informationen zur Datenbank beibehalten, ohne auf all diese zu schauen. Bei den letztgenannten Elementen befinden sich all Ihre Metadaten, was zu tun ist, irgendwo in der Kopfzeile, also ist es nicht nur ein Wegwerfprodukt.

    +0

    Ist es böse, weil es eine ist bessere Möglichkeit, dies zu tun, oder weil Sie dies im Allgemeinen nicht tun? Jedes Mal, wenn ich eine große Dateiübertragung gemacht habe, habe ich es auf die Festplatte geschafft und den Dateispeicherbereich gesperrt, so dass ich mich nie mit so etwas herumschlagen musste, also verzeih mir, wenn das, was ich mache, einfach dumm ist . Die Anforderungen sind, dass ich die große Datei in der Datenbank speichern muss (Filestream kann nicht verwendet werden), und ich muss es verschlüsseln, bevor es dort ankommt, während der Speicherbedarf gering gehalten wird. Diese chunkende Idee war die einzige Möglichkeit, die ich mir vorstellen konnte. – JakeHova

    +0

    Der unangenehme Teil ist die Tatsache, dass Sie im Framework keine Sachen verwenden können, um all die schmutzige Arbeit für Sie zu erledigen. Vielleicht finden Sie auf Nuget eine http-Client-Bibliothek eines Drittanbieters, sehen sich die Quelle an und sehen, wie sie sich im Falle eines Hochladens eines Upload-Streams auswirkt. –

    +0

    Es scheint, dass mein einziges Problem jetzt ist, dass die Anfrage Header in meinem Schreiben in die db enthalten sind. Ich habe verschiedene Möglichkeiten ausprobiert, die Header manuell zu entfernen, indem ich den Grenzwert nutze, um zu erkennen, wo ein Header ist/endet, aber 1) er funktioniert nicht auf Nicht-Text-Dateien 2) es fühlt sich an wie eine hackische und zerbrechliche Lösung für was sollte sei ein einfaches Problem. Irgendwelche Gedanken darüber, wie ich die Header herausziehen kann, bevor ich sie schreibe? – JakeHova