(MVC) MVC (2015)

Сховище графіки на SQL FileStream та канал браузеру multipart/form-data.

Я вще декілька років роблю Сховище графіки на SQL-FileStream. Вперше я описав цю свою технологію у 2012-му році - FileStream в MS SQL. Ця технологія себе повністю виправдала, особливо у суппорті - величезні масиви графікі бекапяться та ресторятся за декілька секунд.

Тому я знов повертаюся до ціеї технологіі, але на відміну від ціеє сторінці, де я описував Flex-клиент, працюючий на потоках браузера multipart/form-data - Пакетный загрузчик файлов на сайт - ні ціеї сторінці я опишу звичайній кліент - HTML & AJAX. Тобто мій хандлер придатний і до звичайного вікористовування, і до AJAX - він має два режима роботи.

Але спочатку декілька слов про технологію постбеку, якою може працювати кожний браузер - це звичайна технологія постбеку application/x-www-form-urlencoded - як раз у цьому році я описав невеличкий аналізатор таких постбеків - SiteRequestTracer - Система аналізу і трасування усіх реквестів до сайту. Та технологія multipart/form-data, яка візначена стандартом интернету RFC 2388.

Невеличка проблема цього стандарту була в тому, що хоча вся працювали на ньому повсюдно, але публічна бібліотека .NET Framework від Миксософту з'явилася досить недавно у составі Web Api 2.0 і тому до цього часу нам доводилося користуватися різноманітніми самостійно побудованнимі бібліотеками, як це описано у мене на ціеї сторінці - Пакетный загрузчик файлов на сайт. Тепер у цьому необхідності немає, бо з'явилася добре продумана бібліотека HttpMultipartParser.

Щоб зрозуміти, як це працює по-перше подивимося на конфіг сайту (або окремого графічного сервіса для сайту).


   1:  <?xml version="1.0"?>
   2:   
   3:  <configuration>
   4:    <appSettings>
   5:      <add key="TraceRequest" value="true"/>
   6:      <!-- имя директории с кешами рисунков -->
   7:      <add key="ImageCachePatch" value="H:\Temp1\"/>
   8:      <!--размеры рисунков в полях Cache1, Cache2-->
   9:      <add key="CacheImageSise1" value="138"/>
  10:      <add key="CacheImageSise2" value="540"/>
  11:      <!--режим приведения кеша к размеру -->
  12:      <!--(1) задана ширина, высота как получится -->
  13:      <!--(2) задана большая сторона, вторая как получится -->
  14:      <add key="CacheImageReiseMode1" value="1"/>
  15:      <add key="CacheImageReiseMode2" value="2"/>
  16:      <add key="webpages:Version" value="1.0.0.0"/>
  17:      <add key="ClientValidationEnabled" value="true"/>
  18:      <add key="UnobtrusiveJavaScriptEnabled" value="true"/>
  19:    </appSettings>
  20:    <connectionStrings>
  21:      <remove name="LocalSqlServer"/>
  22:      <add name="OCMR_ConnectionStrings" connectionString="server=111.22.33.444;Initial Catalog=Ocmr;User ID=Ocmr;Password=111111111;Max Pool Size=10000;" providerName="System.Data.SqlClient"/>
  23:      <add name="OCMR_FS_ConnectionStrings" connectionString="server=111.22.33.444;Initial Catalog=Ocmr_FS;User ID=Ocmr_FS;Password=222222222;Max Pool Size=10000;" providerName="System.Data.SqlClient"/>
  24:       </connectionStrings>
  25:    <system.web>
  26:      <compilation debug="true" targetFramework="4.0">
  27:   
  28:      </compilation>
  29:   
  30:      <authentication mode="Forms">
  31:        <forms loginUrl="~/Account/LogOn" timeout="2880" />
  32:      </authentication>
  33:   
  34:      <pages>
  35:        <namespaces>
  36:          <add namespace="System.Web.Helpers" />
  37:          <add namespace="System.Web.Mvc" />
  38:          <add namespace="System.Web.Mvc.Ajax" />
  39:          <add namespace="System.Web.Mvc.Html" />
  40:          <add namespace="System.Web.Routing" />
  41:          <add namespace="System.Web.WebPages"/>
  42:        </namespaces>
  43:      </pages>
  44:    </system.web>
  45:   
  46:    <system.webServer>
  47:      <validation validateIntegratedModeConfiguration="false"/>
  48:      <modules runAllManagedModulesForAllRequests="true"/>
  49:    </system.webServer>
  50:   
  51:    <runtime>
  52:      <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
  53:        <dependentAssembly>
  54:          <assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" />
  55:          <bindingRedirect oldVersion="1.0.0.0-2.0.0.0" newVersion="3.0.0.0" />
  56:        </dependentAssembly>
  57:      </assemblyBinding>
  58:    </runtime>
  59:  </configuration>

У чьому сайту OCMR графічний сервіс не у мене не відокремлений від самого сайту, а ось наприклад у сайті на скрині нище - графічній сервіс відокремлений у окремий віртуальний web-вузол.



Як ви бачите, сайт користується двома базами - головна база, де зберігаються усі дані сайта, та база графікі - за префіксом FileSrtream. Перша база нам для порозуміння данної сторінки нецікава, а як побудувати другу базу - у мене описано на сторінці FileStream в MS SQL - повторюватися не будемо, лише для себе відзначимо, що у проєкті я маю два класа-мапинга Linq-to-SQL.



А сама структура бази-сховища графікі у данному випадку має такий вигляд:


   1:  CREATE DATABASE [OCMR_FS] ON  PRIMARY 
   2:  ( NAME = N'OCMR_FS', FILENAME = N'I:\OCMR_FS\FileStream_OCMR.mdf' , SIZE = 61056KB , MAXSIZE = UNLIMITED, FILEGROWTH = 10240KB ), 
   3:   FILEGROUP [FS2] CONTAINS FILESTREAM  DEFAULT 
   4:  ( NAME = N'FS2', FILENAME = N'I:\OCMR_FS\FileStream_OCMR.File' )
   5:   LOG ON 
   6:  ( NAME = N'FileStream_log', FILENAME = N'I:\OCMR_FS\FileStream_OCMR.ldf' , SIZE = 42240KB , MAXSIZE = 2048GB , FILEGROWTH = 10%)
   7:  GO

Крім бази, я маю ще у проекті дві тестові сторінкі, одна з яких відправляє постбек до серверу по "multipart/form-data" , а друга по "application/x-www-form-urlencoded".



Код тестової сторінки загрузки світлин LoadImageTest - виглядаї ось так:


   1:  <%@ Page Title="" Language="VB" MasterPageFile="~/Views/Shared/M1.Master" Inherits="System.Web.Mvc.ViewPage" %>
   2:   
   3:  <asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
   4:  LoadImageTest
   5:  </asp:Content>
   6:   
   7:  <asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
   8:   
   9:  <h2>Load Image Test</h2>
  10:   
  11:  <form enctype="multipart/form-data" method="post" name="f1"  id="f1" action="../../LoadImage.ashx" >
  12:  фото<br />
  13:  <input type="file" id="file1" name="file1"  style="width:300px" /><br /><br />
  14:  подпись<br />
  15:  <input type="text" id="text1" name="text1"  style="width:300px"  value="Paul, Jessica, Nicolas, Paulo, Maria, Roland & James - Фото 1" /><br /><br />
  16:  User-email<br />
  17:  <input type="text" id="mail1" name="mail1"  style="width:300px"  value="[email protected]" /><br /><br />
  18:   
  19:  EventsID<br />
  20:  <input type="text" id="event1" name="event1"  style="width:300px"  value="303D0F95-F255-4154-875E-AC8BEC3C070E" /><br /><br />
  21:   
  22:  AJAX mode<br />
  23:  <input type="checkbox" id="ajax1" name="ajax1"  style="width:30px"   /><br /><br />
  24:   
  25:   
  26:   
  27:  <input type="submit" id="submit1" value="Загрузить" name="submit1"  style="width:300px" />
  28:  </form>
  29:  <br /><br />
  30:   
  31:   
  32:  </asp:Content>

А код другої сторінки для відображення світлин ShowImageTest - виглядає ось так:


   1:  <%@ Page Title="" Language="VB" MasterPageFile="~/Views/Shared/M1.Master" Inherits="System.Web.Mvc.ViewPage" %>
   2:   
   3:  <asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
   4:  LoadImageTest
   5:  </asp:Content>
   6:   
   7:  <asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
   8:   
   9:  <h2>Load Image Test</h2>
  10:   
  11:  <form enctype="multipart/form-data" method="post" name="f1"  id="f1" action="../../LoadImage.ashx" >
  12:  фото<br />
  13:  <input type="file" id="file1" name="file1"  style="width:300px" /><br /><br />
  14:  подпись<br />
  15:  <input type="text" id="text1" name="text1"  style="width:300px"  value="Paul, Jessica, Nicolas, Paulo, Maria, Roland & James - Фото 1" /><br /><br />
  16:  User-email<br />
  17:  <input type="text" id="mail1" name="mail1"  style="width:300px"  value="[email protected]" /><br /><br />
  18:   
  19:  EventsID<br />
  20:  <input type="text" id="event1" name="event1"  style="width:300px"  value="303D0F95-F255-4154-875E-AC8BEC3C070E" /><br /><br />
  21:   
  22:  AJAX mode<br />
  23:  <input type="checkbox" id="ajax1" name="ajax1"  style="width:30px"   /><br /><br />
  24:   
  25:   
  26:  <input type="submit" id="submit1" value="Загрузить" name="submit1"  style="width:300px" />
  27:  </form>
  28:  <br /><br />
  29:   
  30:   
  31:  </asp:Content>

Ну ось на цьому місці, коли вже контекст віконання хандлеру повністю зрозумілий - можна подивитися і на сам текст хандлеру.


   1:  Imports System.Web
   2:  Imports System.Web.Services
   3:   
   4:  Public Class LoadImage
   5:      Implements System.Web.IHttpHandler
   6:   
   7:      Sub ProcessRequest(ByVal context As HttpContext) Implements IHttpHandler.ProcessRequest
   8:   
   9:          Try
  10:              If context.Request.RequestType = "POST" Then
  11:                  'Dim Byte1 As Byte() = context.Request.BinaryRead(context.Request.InputStream.Length)
  12:                  'Dim Dir As String = HttpContext.Current.Server.MapPath("~")
  13:                  'My.Computer.FileSystem.WriteAllBytes(IO.Path.Combine(Dir, Guid.NewGuid.ToString & ".jpg"), Byte1, False)
  14:                  'Exit Sub
  15:                  '
  16:                  'Парсинг multipart/form-data
  17:                  '
  18:                  Dim Parser As New HttpMultipartParser.MultipartFormDataParser(context.Request.InputStream)
  19:                  Dim Buf(Parser.Files.First.Data.Length) As Byte
  20:                  Parser.Files.First.Data.Read(Buf, 0, Parser.Files.First.Data.Length)
  21:                  'My.Computer.FileSystem.WriteAllBytes(FullFileName, Buf, False)
  22:                  '
  23:                  'Проверка всех параметров реквеста
  24:                  '
  25:                  If Parser.Files.First.Data.Length = 0 Then
  26:                      context.Response.ContentType = "text/plain"
  27:                      context.Response.Write("empty file")
  28:                      Exit Sub
  29:                  End If
  30:                  Dim Text1 As String = Parser.Parameters("text1").Data
  31:                  Dim Mail1 As String = Parser.Parameters("mail1").Data
  32:                  If Mail1 = "" Or Not Mail1.Contains("@") Then
  33:                      context.Response.ContentType = "text/plain"
  34:                      context.Response.Write("bad parm")
  35:                      Exit Sub
  36:                  End If
  37:                  Dim Event1 As Guid
  38:                  Try
  39:                      Event1 = Guid.Parse(Parser.Parameters("event1").Data)
  40:                  Catch ex As Exception
  41:                      context.Response.ContentType = "text/plain"
  42:                      context.Response.Write("bad parm")
  43:                      Exit Sub
  44:                  End Try
  45:                  Dim AJAX_Mode As Boolean = False
  46:                  Try
  47:                      AJAX_Mode = IIf(Parser.Parameters("ajax1").Data = "on", True, False)
  48:                  Catch ex As System.Collections.Generic.KeyNotFoundException
  49:                      '
  50:                  End Try
  51:                  '
  52:                  Dim OCMR_DB As New OCMRDataContext
  53:                  Dim UserID = (From X In OCMR_DB.Users Select X Where X.Email.Trim = Mail1.Trim).ToList
  54:                  If UserID.Count = 0 Then
  55:                      context.Response.ContentType = "text/plain"
  56:                      context.Response.Write("bad parm")
  57:                      Exit Sub
  58:                  End If
  59:                  Dim EventID = (From X In OCMR_DB.Events Select X Where X.UserID = UserID(0).ID And X.ID = Event1).ToList
  60:                  If EventID.Count = 0 Then
  61:                      context.Response.ContentType = "text/plain"
  62:                      context.Response.Write("bad parm")
  63:                      Exit Sub
  64:                  End If
  65:                  '
  66:                  'Теперь запишем FileStream
  67:                  '
  68:                  Dim FS_DB As New OCMR_FSDataContext
  69:                  Dim NewEventFoto As New EventFoto
  70:                  NewEventFoto.RowGuid = Guid.NewGuid
  71:                  NewEventFoto.EventID = EventID(0).ID
  72:                  NewEventFoto.Comment = Text1
  73:                  NewEventFoto.Image = Buf
  74:                  FS_DB.EventFotos.InsertOnSubmit(NewEventFoto)
  75:                  FS_DB.SubmitChanges()
  76:                  Dim LastID As Integer = NewEventFoto.i
  77:                  '
  78:                  'И ответим на реквест
  79:                  '
  80:                  If AJAX_Mode Then
  81:                      context.Response.ContentType = "text/plain"
  82:                      context.Response.Write(NewEventFoto.RowGuid.ToString)
  83:                  Else
  84:                      context.Response.RedirectPermanent(context.Request.UrlReferrer.ToString)
  85:                  End If
  86:                  '
  87:                  'Кеши делаем позже ответа
  88:                  '
  89:                  Dim Dimension As ImageDimension.Dimension = (new ImageDimension).GetDimension(Buf)
  90:                  Dim Dir1 As String = System.Configuration.ConfigurationManager.AppSettings("ImageCachePatch")
  91:                  '
  92:                  Dim CacheImageSise1 As Integer = System.Configuration.ConfigurationManager.AppSettings("CacheImageSise1")
  93:                  Dim CacheImageSise2 As Integer = System.Configuration.ConfigurationManager.AppSettings("CacheImageSise2")
  94:                  '
  95:                  Dim Temp1 As String = Guid.NewGuid.ToString & ".jpg"
  96:                  Dim FullFileName1 = IO.Path.Combine(Dir1, Temp1)
  97:                  '
  98:                  Dim Temp2 As String = Guid.NewGuid.ToString & ".jpg"
  99:                  Dim FullFileName2 = IO.Path.Combine(Dir1, Temp2)
 100:                  '
 101:                  Dim Buf1 As Byte() = (new ImageDimension).StripSize(Buf, Dimension, CacheImageSise1)
 102:                  Dim Buf2 As Byte() = (new ImageDimension).StripSize(Buf, Dimension, CacheImageSise2)
 103:                  '
 104:                  My.Computer.FileSystem.WriteAllBytes(FullFileName1, Buf1, False)
 105:                  My.Computer.FileSystem.WriteAllBytes(FullFileName2, Buf2, False)
 106:                  '
 107:                  Dim FS_DB1 As New OCMR_FSDataContext
 108:                  Dim CurrentFoto = (From X In FS_DB1.EventFotos Select X Where X.i = LastID).ToList
 109:                  If CurrentFoto.Count = 0 Then
 110:                      context.Response.ContentType = "text/plain"
 111:                      context.Response.Write("unexpected error")
 112:                  End If
 113:                  CurrentFoto(0).Cache1 = Temp1
 114:                  CurrentFoto(0).Cache2 = Temp2
 115:                  FS_DB1.SubmitChanges()
 116:                  '
 117:              Else
 118:                  context.Response.ContentType = "text/plain"
 119:                  context.Response.Write("only post")
 120:              End If
 121:   
 122:   
 123:          Catch ex As Exception
 124:              SaveErrLog(ex.Message)
 125:              context.Response.ContentType = "text/plain"
 126:              context.Response.Write(ex.Message)
 127:          End Try
 128:      End Sub
 129:   
 130:      Sub SaveErrLog(TXT As String)
 131:          Dim CNW As New Data.SqlClient.SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings("FS_ConnectionStrings").ConnectionString)
 132:          CNW.Open()
 133:          Dim CMDW As New Data.SqlClient.SqlCommand("INSERT INTO [FileStream2].[dbo].[ErrLog] ([CrDate],[TXT])VALUES( GETDATE(),@TXT)", CNW)
 134:          CMDW.Parameters.Add("@TXT", Data.SqlDbType.NVarChar)
 135:          CMDW.Parameters("@TXT").Value = TXT
 136:          CMDW.ExecuteScalar()
 137:          CNW.Close()
 138:      End Sub
 139:   
 140:   
 141:      Public ReadOnly Property IsReusable() As Boolean Implements IHttpHandler.IsReusable
 142:          Get
 143:              Return False
 144:          End Get
 145:      End Property
 146:  End Class

Що можна сказати про цей мій код? По-перше, такий точно парсер вібпрацьовує у бекграунді якщо написаті просто у контроллері щос подібне:


Function AddStory(BinaryFile As HttpPostedFileBase, PRM As FormCollection) As ActionResult

По-друге, зверніть увагу на достатно тонку технологію, виконання решти кода, що потребує багато часу і ресурсів вже після редіректу браузера. Мабуть, це не дуже правільно, краще було б зробити окремий поток і в ньому виконати ці важки перетворювання. Але... працює і так!

Якихось інших особливостей код не має, все просто, дуже просто. Я навіть не став видяляти утворювання кеша у окрему функцію. Такий же простий і код відображення графіки у браузер. Мабуть і розповісти про нього нема чого:


   1:  Imports System.Web
   2:  Imports System.Web.Services
   3:   
   4:  Public Class GetImage
   5:      Implements System.Web.IHttpHandler
   6:   
   7:      Sub ProcessRequest(ByVal context As HttpContext) Implements IHttpHandler.ProcessRequest
   8:          'QueryString :
   9:          'ID = 0091738F-92DA-436A-B6C7-F74765E42ADE
  10:          'Mode = 0 - Original Size
  11:          'Mode = 1 - Cache Size 1
  12:          'Mode = 2 - Cache Size 2
  13:          'Mode = w - Special Width
  14:          'Mode = h - Special Height
  15:          Dim ID As Guid
  16:          Dim Mode As Char
  17:          Dim W As Integer
  18:          Dim H As Integer
  19:          Dim M As Integer
  20:          Try
  21:              'проверили параметры
  22:              If context.Request.QueryString("ID") Is Nothing Or context.Request.QueryString("Mode") Is Nothing Then
  23:                  context.Response.ContentType = "text/plain"
  24:                  context.Response.Write("bad parm")
  25:                  Exit Sub
  26:              End If
  27:              If context.Request.QueryString("ID").Trim = "" Or context.Request.QueryString("Mode").Trim = "" Then
  28:                  context.Response.ContentType = "text/plain"
  29:                  context.Response.Write("bad parm")
  30:                  Exit Sub
  31:              End If
  32:   
  33:              Try
  34:                  ID = Guid.Parse(context.Request.QueryString("ID"))
  35:                  Mode = context.Request.QueryString("Mode").ToLower
  36:              Catch ex As Exception
  37:                  context.Response.ContentType = "text/plain"
  38:                  context.Response.Write("bad parm")
  39:                  Exit Sub
  40:              End Try
  41:              If Mode = "w" Then
  42:                  If context.Request.QueryString("w") IsNot Nothing Then
  43:                      If context.Request.QueryString("w") <> "" Then
  44:                          If IsNumeric(context.Request.QueryString("w")) Then
  45:                              W = CInt(context.Request.QueryString("w"))
  46:                          Else
  47:                              context.Response.ContentType = "text/plain"
  48:                              context.Response.Write("bad parm")
  49:                              Exit Sub
  50:                          End If
  51:                      Else
  52:                          context.Response.ContentType = "text/plain"
  53:                          context.Response.Write("bad parm")
  54:                          Exit Sub
  55:                      End If
  56:                  Else
  57:                      context.Response.ContentType = "text/plain"
  58:                      context.Response.Write("bad parm")
  59:                      Exit Sub
  60:                  End If
  61:              End If
  62:              If Mode = "h" Then
  63:                  If context.Request.QueryString("h") IsNot Nothing Then
  64:                      If context.Request.QueryString("h") <> "" Then
  65:                          If IsNumeric(context.Request.QueryString("h")) Then
  66:                              H = CInt(context.Request.QueryString("h"))
  67:                          Else
  68:                              context.Response.ContentType = "text/plain"
  69:                              context.Response.Write("bad parm")
  70:                              Exit Sub
  71:                          End If
  72:                      Else
  73:                          context.Response.ContentType = "text/plain"
  74:                          context.Response.Write("bad parm")
  75:                          Exit Sub
  76:                      End If
  77:                  Else
  78:                      context.Response.ContentType = "text/plain"
  79:                      context.Response.Write("bad parm")
  80:                      Exit Sub
  81:                  End If
  82:              End If
  83:              If Mode = "m" Then
  84:                  If context.Request.QueryString("m") IsNot Nothing Then
  85:                      If context.Request.QueryString("m") <> "" Then
  86:                          If IsNumeric(context.Request.QueryString("m")) Then
  87:                              M = CInt(context.Request.QueryString("m"))
  88:                          Else
  89:                              context.Response.ContentType = "text/plain"
  90:                              context.Response.Write("bad parm")
  91:                              Exit Sub
  92:                          End If
  93:                      Else
  94:                          context.Response.ContentType = "text/plain"
  95:                          context.Response.Write("bad parm")
  96:                          Exit Sub
  97:                      End If
  98:                  Else
  99:                      context.Response.ContentType = "text/plain"
 100:                      context.Response.Write("bad parm")
 101:                      Exit Sub
 102:                  End If
 103:              End If
 104:              '
 105:              'проверили ID
 106:              Dim FS_DB As New OCMR_FSDataContext
 107:              Dim CurrentFoto = (From X In FS_DB.EventFotos Select X Where X.RowGuid = ID).ToList
 108:              If CurrentFoto.Count = 0 Then
 109:                  context.Response.ContentType = "text/plain"
 110:                  context.Response.Write("no foto")
 111:                  Exit Sub
 112:              End If
 113:              'отображаем
 114:              Dim Dir1 As String = System.Configuration.ConfigurationManager.AppSettings("ImageCachePatch")
 115:              Dim CacheImageSise1 As Integer = System.Configuration.ConfigurationManager.AppSettings("CacheImageSise1")
 116:              Dim CacheImageSise2 As Integer = System.Configuration.ConfigurationManager.AppSettings("CacheImageSise2")
 117:              Dim ImageBytes(CurrentFoto(0).Image.Length) As Byte
 118:              'System.Buffer.BlockCopy(CurrentFoto(0).Image.ToArray, 0, ImageBytes, 0, CurrentFoto(0).Image.Length)
 119:              Select Case Mode
 120:                  Case "0"
 121:                      context.Response.ContentType = "image/bmp"
 122:                      context.Response.BinaryWrite(CurrentFoto(0).Image.ToArray)
 123:                      Exit Sub
 124:   
 125:                  Case "1"
 126:                      Dim FileName1 As String = CurrentFoto(0).Cache1
 127:                      Dim FullFileName1 As String
 128:                      If FileName1 Is Nothing Then
 129:  CreateCache1:
 130:                          Dim Temp1 As String = Guid.NewGuid.ToString & ".jpg"
 131:                          FullFileName1 = IO.Path.Combine(Dir1, Temp1)
 132:                          ImageBytes = CurrentFoto(0).Image.ToArray
 133:                          Dim Dimension1 As ImageDimension.Dimension = (new ImageDimension).GetDimension(ImageBytes)
 134:                          Dim Buf1 As Byte() = (new  ImageDimension).StripSize(ImageBytes, Dimension1, CacheImageSise1)
 135:                          context.Response.ContentType = "image/bmp"
 136:                          context.Response.BinaryWrite(Buf1)
 137:                          'после записи в браузер - запись на диск и в базу
 138:                          'если в єтом хвосте будет ошибка - рисунок будет испорчен, тк сработает CATCH с ContentType = "text/plain"
 139:                          My.Computer.FileSystem.WriteAllBytes(FullFileName1, Buf1, False)
 140:                          CurrentFoto(0).Cache1 = Temp1
 141:                          FS_DB.SubmitChanges()
 142:                          Exit Sub
 143:                      Else
 144:                          FullFileName1 = IO.Path.Combine(Dir1, FileName1)
 145:                          If My.Computer.FileSystem.FileExists(FullFileName1) Then
 146:                              context.Response.ContentType = "image/bmp"
 147:                              context.Response.BinaryWrite(My.Computer.FileSystem.ReadAllBytes(FullFileName1))
 148:                          Else
 149:                              GoTo CreateCache1
 150:                          End If
 151:                          Exit Sub
 152:                      End If
 153:   
 154:   
 155:                  Case "2"
 156:                      Dim FileName2 As String = CurrentFoto(0).Cache2
 157:                      Dim FullFileName2 As String
 158:                      If FileName2 Is Nothing Then
 159:  CreateCache2:
 160:                          Dim Temp2 As String = Guid.NewGuid.ToString & ".jpg"
 161:                          FullFileName2 = IO.Path.Combine(Dir1, Temp2)
 162:                          ImageBytes = CurrentFoto(0).Image.ToArray
 163:                          Dim Dimension2 As ImageDimension.Dimension = (new ImageDimension).GetDimension(ImageBytes)
 164:                          Dim Buf2 As Byte() = (new ImageDimension).StripSize(ImageBytes, Dimension2, CacheImageSise2)
 165:                          context.Response.ContentType = "image/bmp"
 166:                          context.Response.BinaryWrite(Buf2)
 167:                          'хвост
 168:                          My.Computer.FileSystem.WriteAllBytes(FullFileName2, Buf2, False)
 169:                          CurrentFoto(0).Cache2 = Temp2
 170:                          FS_DB.SubmitChanges()
 171:                          Exit Sub
 172:                      Else
 173:                          FullFileName2 = IO.Path.Combine(Dir1, FileName2)
 174:                          If My.Computer.FileSystem.FileExists(FullFileName2) Then
 175:                              context.Response.ContentType = "image/bmp"
 176:                              context.Response.BinaryWrite(My.Computer.FileSystem.ReadAllBytes(FullFileName2))
 177:                          Else
 178:                              GoTo CreateCache2
 179:                          End If
 180:                          Exit Sub
 181:                      End If
 182:   
 183:                  Case "w"
 184:                      context.Response.ContentType = "image/bmp"
 185:                      ImageBytes = CurrentFoto(0).Image.ToArray
 186:                      context.Response.BinaryWrite(ImageService.StripSizeForWidth(ImageBytes, W))
 187:                      Exit Sub
 188:   
 189:                  Case "h"
 190:                      context.Response.ContentType = "image/bmp"
 191:                      ImageBytes = CurrentFoto(0).Image.ToArray
 192:                      context.Response.BinaryWrite(ImageService.StripSizeForHeight(ImageBytes, H))
 193:                      Exit Sub
 194:   
 195:                  Case "m"
 196:                      ImageBytes = CurrentFoto(0).Image.ToArray
 197:                      Dim Dimension As ImageDimension.Dimension = (new ImageDimension).GetDimension(ImageBytes)
 198:                      context.Response.ContentType = "image/bmp"
 199:                      If Dimension.Width > Dimension.Heigth Then
 200:                          context.Response.BinaryWrite(ImageService.StripSizeForWidth(ImageBytes, M))
 201:                      Else
 202:                          context.Response.BinaryWrite(ImageService.StripSizeForHeight(ImageBytes, M))
 203:                      End If
 204:                      Exit Sub
 205:              End Select
 206:          Catch ex As Exception
 207:              context.Response.ContentType = "text/plain"
 208:              context.Response.Write(ex.Message)
 209:          End Try
 210:      End Sub
 211:   
 212:      ReadOnly Property IsReusable() As Boolean Implements IHttpHandler.IsReusable
 213:          Get
 214:              Return False
 215:          End Get
 216:      End Property
 217:   
 218:  End Class

Треба ще трошки розповісти про структуру кешу. У цьому проєкті я зробив її плоску, але так я роблю не завжди. Ось подивиться на цей проєкт. Тут тільки коренних діректорій у кеші 6 тисяч. Якщо це спробувати відкрити у файловому єксплорері windows - він буде відкрівати корений каталог цілий тиждень!



Зрозуміло, що цю іерархію можна зробити як завгодно - по GUIDам юзерів, або по GUIDам об'ектів тощо.


Останне і найбільш цікаве питання сховища графікі - це видалення графічного об'єкту. Зрозуміло, що без кріптографіі тут не обойтись - бо не можна дозволити кому завгодно відалити світлину, і передати аутентификацію з браузеру не можливо без ризику стороннього втручання. Я моя добре пророблена технологія, яку я наприклад описвав тут Складська прога на WCF-сервісах зі сканером чи ось тут FlexStringObfuscator - Flame-преобразования во Flex - на підійде, бо де можна в браузері сховати без ризику пароль?

Тому я зробив для цього віпадку надійну аутентефікацію інакше. До кожного реквесту на відалення світлини додаешься SecurityToken, який можна передавати із браузера без будь якого ризику. Але зробити його можливо лише функцією SQL-серверу, до якої сайт має доступ, а стороння людина їз интернету не має. Тобто серверний код получає той SecurityToken на сервері шляхом виклика функціі SQL-серверу, а код хандлера DeleteImage перевіряє цю фукнцію. I якщо SecurityToken передан у параметрах виклику хандлеру DeleteImage добрий - стрічка з бази відаляїться і кеш вічищаеться від непотрібних світлин.

Як же зроблена та функція. Я, мабуть, реальні свох функции показувати тут не буду, щоб не наражати ризику до своїх сайтів, але покажу тут якусь умовну функцію, яка виробляє SecurityToken. Важливо такось зрозуміти, що зробити цей токен дійсним на будь-який час не просто - а дуже просто. Але я цього теж описувати тут не буду - сподиваюсь, що якзо ви дочитали до цього місця - зробити це - дасть вам масу задоволення.

Отож, умовна функція вироблення SecurityToken виглядає так:


   1:  ALTER FUNCTION [dbo].[SecurityTokenBody]
   2:  (
   3:    @ImageID as uniqueidentifier
   4:  )
   5:  RETURNS nvarchar(37)
   6:  AS
   7:  BEGIN
   8:   
   9:  Declare @Cache1 as nvarchar(40)
  10:  Declare @Cache2 as nvarchar(40)
  11:  select  @Cache1=Cache1, @Cache2=Cache2 from dbo.PointFoto where RowGuid=@ImageID
  12:  Select  @Cache1=ISNULL(@Cache1,'00000000-0000-0000-0000-000000000000'),
  13:          @Cache2=ISNULL(@Cache2,'00000000-0000-0000-0000-000000000000')
  14:          
  15:  Declare @Str1 as nvarchar(53)
  16:  Select  @Str1 = CAST (@ImageID as nvarchar(36)) 
  17:          + 'MySercetPassword1' + CAST (@Cache1 as nvarchar(36)) 
  18:          + 'MySercetPassword2' + CAST (@Cache2 as nvarchar(36))
  19:          
  20:  Return  CONVERT(NVARCHAR(32),HashBytes('MD5', @Str1),2) 
  21:          
  22:  END

   1:  ALTER FUNCTION [dbo].[CreateSecurityToken]
   2:  (
   3:    @ImageID as uniqueidentifier
   4:  )
   5:  RETURNS nvarchar(37)
   6:  AS
   7:  BEGIN
   8:   
   9:  Declare @Body as nvarchar(32)
  10:  Select  @Body = dbo.SecurityTokenBody (@ImageID)      
  11:          
  12:  Return  RIGHT(CAST(CAST(CURRENT_TIMESTAMP AS TIME(7)) as nvarchar(12)),3) +
  13:          @Body +
  14:          SUBSTRING(CAST(CAST(CURRENT_TIMESTAMP AS TIME(7)) as nvarchar(12)),7,2)
  15:  END

А функция DecryptSecurityToken, до якої звертається хандлер DeleteImage виглядае ось так - вона отримує ID-об'єкту, до якого прив'язана світлина і прораховуе односторонню функцию для кожного графічного обьекту (у моєму сайті світлин до кожного об'екту бути не може) - і повертає до хандлеру або ID святлини, що треба видалити, або NULL. Хандлер перевіряє - чи той ID збігається з тим, що він отримав iз браузеру - і якщо "так" - відаляє світлину - тобто до ханлеру прилетіли из браузера не будь-що, а те що потрібно - обидва публічно відомих параметра (ID та EventID) і SecurityToken вироблени саме сайтом, який користується данною базою, а не хто завгодно звертаєть до хандлеру из реквестом на відалення світлини.


   1:  ALTER FUNCTION [dbo].[DecryptSecurityToken]
   2:  (
   3:    @SecurityToken as nvarchar(37),
   4:    @ImageID uniqueidentifier
   5:  )
   6:  RETURNS uniqueidentifier
   7:  AS
   8:  BEGIN
   9:  Declare @ID uniqueidentifier
  10:  Select @ID=RowGuid from dbo.EventFoto where 
  11:  RowGuid=@ImageID and
  12:  dbo.SecurityTokenBody(dbo.EventFoto.RowGuid) = SUBSTRING(@SecurityToken,4,32)
  13:  Return  @ID
  14:  END

Зрозуміло, що слово Decrypt у імені функціі тут умовне, бо ніякого "Decrypt" по односторонній хеш-функціі MD5 принципово зробити неможливо.


Код хандлера DeleteImage дуже простий і ніякіх коментарів не потребує:


   1:  Imports System.Web
   2:  Imports System.Web.Services
   3:   
   4:  Public Class DeleteImage
   5:      Implements System.Web.IHttpHandler
   6:   
   7:      Sub ProcessRequest(ByVal context As HttpContext) Implements IHttpHandler.ProcessRequest
   8:          'QueryString :
   9:          'ID = 0091738F-92DA-436A-B6C7-F74765E42ADE
  10:          'securitytoken = 1074F99DFDDA4B8DAE44D4079BB8C2EDCF959
  11:          'events1 = B596348B-0107-4CF2-91F8-CB38B98F20DD
  12:          'mail1 = [email protected]
  13:          'ajax1 = "on" или опущено
  14:          '
  15:          'проверили все параметры
  16:          Dim ID1 As String = context.Request.QueryString("id1")
  17:          Dim Mail1 As String = context.Request.QueryString("mail1")
  18:          Dim Event1 As String = context.Request.QueryString("events1")
  19:          Dim SecurityToken As String = context.Request.QueryString("securitytoken1")
  20:          If ID1 Is Nothing Or Mail1 Is Nothing Or Event1 Is Nothing Or SecurityToken Is Nothing Then
  21:              context.Response.ContentType = "text/plain"
  22:              context.Response.Write("bad parm 1")
  23:              Exit Sub
  24:          End If
  25:          If ID1.Trim = "" Or Mail1.Trim = "" Or Event1.Trim = "" Or SecurityToken.Trim = "" Then
  26:              context.Response.ContentType = "text/plain"
  27:              context.Response.Write("bad parm 2")
  28:              Exit Sub
  29:          End If
  30:          Dim ID As Guid
  31:          Try
  32:              ID = Guid.Parse(ID1)
  33:          Catch ex As Exception
  34:              context.Response.ContentType = "text/plain"
  35:              context.Response.Write("bad parm 3")
  36:              Exit Sub
  37:          End Try
  38:          Dim Event2 As Guid
  39:          Try
  40:              Event2 = Guid.Parse(Event1)
  41:          Catch ex As Exception
  42:              context.Response.ContentType = "text/plain"
  43:              context.Response.Write("bad parm")
  44:              Exit Sub
  45:          End Try
  46:          If Not IsNumeric(Event1) Or Not Mail1.Contains("@") Then
  47:              context.Response.ContentType = "text/plain"
  48:              context.Response.Write("bad parm 4")
  49:              Exit Sub
  50:          End If
  51:          Dim OCMR_DB As New OCMRDataContext
  52:          Dim UserID = (From X In OCMR_DB.Users Select X Where X.Email.Trim = Mail1.Trim).ToList
  53:          If UserID.Count = 0 Then
  54:              context.Response.ContentType = "text/plain"
  55:              context.Response.Write("bad parm 5")
  56:              Exit Sub
  57:          End If
  58:          Dim EventsID = (From X In OCMR_DB.Events Select X Where X.UserID = UserID(0).ID And X.ID = Event2).ToList
  59:          If EventsID.Count = 0 Then
  60:              context.Response.ContentType = "text/plain"
  61:              context.Response.Write("bad parm 6")
  62:              Exit Sub
  63:          End If
  64:          '
  65:          'выбрали рисунок
  66:          Dim FS_DB As New OCMR_FSDataContext
  67:          Dim FS_Image = (From X In FS_DB.EventFotos Select X Where X.RowGuid = ID And X.EventID = Event2).ToList
  68:          If FS_Image.Count = 0 Then
  69:              context.Response.ContentType = "text/plain"
  70:              context.Response.Write("bad parm 7")
  71:              Exit Sub
  72:          End If
  73:          Dim Cache1 As String = FS_Image(0).Cache1 : If Cache1 Is Nothing Then Cache1 = ""
  74:          Dim Cache2 As String = FS_Image(0).Cache2 : If Cache2 Is Nothing Then Cache2 = ""
  75:          '
  76:          'проверили токен
  77:          Dim CheckSecurityToken = FS_DB.DecryptSecurityToken(SecurityToken, ID)
  78:          If CheckSecurityToken Is Nothing Then
  79:              context.Response.ContentType = "text/plain"
  80:              context.Response.Write("bad parm 8")
  81:              Exit Sub
  82:          End If
  83:          If CheckSecurityToken <> ID Then
  84:              context.Response.ContentType = "text/plain"
  85:              context.Response.Write("bad parm 9")
  86:              Exit Sub
  87:          End If
  88:          'удалили рисунок
  89:          FS_DB.EventFotos.DeleteOnSubmit(FS_Image(0))
  90:          FS_DB.SubmitChanges()
  91:          'ответили в браузер
  92:          Dim AJAX_Mode As Boolean = False
  93:          Try
  94:              AJAX_Mode = IIf(context.Request.QueryString("ajax1") = "on", True, False)
  95:          Catch ex As System.Collections.Generic.KeyNotFoundException
  96:              '
  97:          End Try
  98:          If AJAX_Mode Then
  99:              context.Response.ContentType = "text/plain"
 100:              context.Response.Write("OK")
 101:          Else
 102:              context.Response.RedirectPermanent(context.Request.UrlReferrer.ToString)
 103:          End If
 104:          'удалили кеши
 105:          Dim Dir1 As String = System.Configuration.ConfigurationManager.AppSettings("ImageCachePatch")
 106:          Dim FullFileName1 = IO.Path.Combine(Dir1, Cache1)
 107:          If My.Computer.FileSystem.FileExists(FullFileName1) Then
 108:              My.Computer.FileSystem.DeleteFile(FullFileName1)
 109:          End If
 110:          Dim FullFileName2 = IO.Path.Combine(Dir1, Cache2)
 111:          If My.Computer.FileSystem.FileExists(FullFileName2) Then
 112:              My.Computer.FileSystem.DeleteFile(FullFileName2)
 113:          End If
 114:      End Sub
 115:   
 116:   
 117:      ReadOnly Property IsReusable() As Boolean Implements IHttpHandler.IsReusable
 118:          Get
 119:              Return False
 120:          End Get
 121:      End Property
 122:   
 123:  End Class

Хандлер Crop описан у мене ось тут - Cropper світлин сайту, а функціі GDI+, що працють з графікою, описані ось тут - Загально графічні функції. Але все це лише невеличка (але найважливіша) частинка мого графічного фреймворка, до якого входить ще багато функцій, наприклад хандлери повороту світлин.

Цей графічний двигун постійно розвивається, наприклад, всі останні версіі цього графічного двигуна пишуть в базу розмірність світлин:




Comments ( )
<00>  <01>  <02>  <03>  <04>  <05>  <06>  <07>  <08>  <09>  <10>  <11>  <12>  <13>  <14>  <15>  <16>  <17>  <18>  <19>  <20>  <21>  <22>  <23
Link to this page: //www.vb-net.com/Multipart-form-data/index.htm
<SITEMAP>  <MVC>  <ASP>  <NET>  <DATA>  <KIOSK>  <FLEX>  <SQL>  <NOTES>  <LINUX>  <MONO>  <FREEWARE>  <DOCS>  <ENG>  <CHAT ME>  <ABOUT ME>  < THANKS ME>