Сховище графіки на 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="open.community.media.room@gmail.com" /><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="open.community.media.room@gmail.com" /><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 = open@gmail.com
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+, що працють з графікою, описані ось тут - Загально графічні функції. Але все це лише невеличка (але найважливіша) частинка мого графічного фреймворка, до якого входить ще багато функцій, наприклад хандлери повороту світлин.
Цей графічний двигун постійно розвивається, наприклад, всі останні версіі цього графічного двигуна пишуть в базу розмірність світлин:
|