PowerShell: определение MIME для файла или неочевидные проблемы при вызове FindMimeFromData
В процессе решения задачи с загрузкой на сервер файла логотипа встал вопрос: как определить действительный MIME для графического (да и не только) файла? Ориентироваться только на расширение – не стоит (неоднократно встречал .png файлы с jpeg содержанием). Да и в случае определения MIME по расширению – стоит ли лезть самому в реестр для поисков ответа (я уж молчу про самостоятельное установление соответствия расширений и MIME-типов)? Ответ – нет. Для этих целей есть Windows API. Но задача, как выяснилось, не совсем уж тривиальная…
Решение “в лоб”
Итак, использовать мы будем API FindMimeFromData. Приведу обёртку для использования этой функции в PowerShell:
Add-Type @" using System; using System.IO; using System.Runtime.InteropServices; namespace ITG { public class WinAPI { // http://msdn.microsoft.com/en-us/library/ms775107(VS.85).aspx // http://social.msdn.microsoft.com/Forums/en-US/Vsexpressvcs/thread/d79e76e3-b8c9-4fce-a97d-94ded18ea4dd [DllImport("urlmon.dll", CharSet = CharSet.Auto)] public static extern System.UInt32 FindMimeFromData( System.UInt32 pBC, [MarshalAs(UnmanagedType.LPWStr)] System.Text.StringBuilder pwzUrl, [MarshalAs(UnmanagedType.LPArray)] byte[] pBuffer, System.UInt32 cbSize, [MarshalAs(UnmanagedType.LPWStr)] System.Text.StringBuilder pwzMimeProposed, System.UInt32 dwMimeFlags, out System.UInt32 ppwzMimeOut, System.UInt32 dwReserverd ); } } "@;
Обращу внимание на один немаловажный момент, с которым потерял сам полчаса. Обратите внимание на тип параметров pwzUrl
, pwzMimeProposed
. Он не [System.String]
, а [System.Text.StringBuilder
]. Именно этот тип следует использовать в обёртках, если существует необходимость иметь возможность передать и null в качестве значения параметра, а не пустую строку! Если Вы используете тип [System.String]
– в API будет передан указатель на пустую строку, что не одно и то же для многих функций API Windows.
Ниже – непосредственно код на PowerShell для определения MIME файла (если файл не существует, MIME будет определён по его имени, точнее – по расширению, что нам и надо):
$fileInfo = [System.IO.FileInfo]'C:\temp\logo.jpg'; $length = 0; [Byte[]] $buffer = $null; if ( [System.IO.File]::Exists( $fileInfo ) ) { $fs = New-Object System.IO.FileStream ( $fileInfo, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [system.IO.FileShare]::Read, 256 ); try { $buffer = New-Object Byte[] (256); if ( $fs.Length -ge 256) { $length = 256; } else { $length = $fs.Length; }; if ( $length ) { $length = $fs.Read( $buffer, 0, $length ); }; } finally { $fs.Dispose(); }; }; [System.UInt32] $mimeType = 0; $err = [ITG.WinAPI]::FindMimeFromData( 0, $fileInfo.FullName, $buffer, $length, $null, 0x00000001 + 0x00000020, [ref]$mimetype, 0 ); $mimeTypePtr = [System.IntPtr]$mimeType; $mime = [System.Runtime.InteropServices.Marshal]::PtrToStringUni( $mimeTypePtr ); [System.Runtime.InteropServices.Marshal]::FreeCoTaskMem( $mimeTypePtr ); $mime;
Приведённый выше код прекрасно определяет MIME по содержимому файла, а если файл не существует – по расширению его имени.
Оформим командлет Get-MIME
Как Вы понимаете, маршалинг параметров в приведённом выше коде подглядел в сети. Однако, смутила обработка выходного параметра и закрались сомнения. И на самом деле, c# предлагает куда более “красивый” маршалинг, в том числе и для “out” параметров. Итак, сначала c# код нашей обёртки с новым маршалингом:
using System; using System.IO; using System.Runtime.InteropServices; namespace ITG.WinAPI.UrlMon { [Flags] public enum FMFD { Default = 0x00000000, // No flags specified. Use default behavior for the function. URLAsFileName = 0x00000001, // Treat the specified pwzUrl as a file name. EnableMIMESniffing = 0x00000002, // Internet Explorer 6 for Windows XP SP2 and later. Use MIME-type detection even if FEATURE_MIME_SNIFFING is detected. Usually, this feature control key would disable MIME-type detection. IgnoreMIMETextPlain = 0x00000004, // Internet Explorer 6 for Windows XP SP2 and later. Perform MIME-type detection if "text/plain" is proposed, even if data sniffing is otherwise disabled. Plain text may be converted to text/html if HTML tags are detected. ServerMIME = 0x00000008, // Internet Explorer 8. Use the authoritative MIME type specified in pwzMimeProposed. Unless FMFD_IGNOREMIMETEXTPLAIN is specified, no data sniffing is performed. RespectTextPlain = 0x00000010, // Internet Explorer 9. Do not perform detection if "text/plain" is specified in pwzMimeProposed. RetrunUpdatedImgMIMEs = 0x00000020 // Internet Explorer 9. Returns image/png and image/jpeg instead of image/x-png and image/pjpeg. }; public class API { // http://msdn.microsoft.com/en-us/library/ms775107(VS.85).aspx // http://www.rsdn.ru/article/dotnet/netTocom.xml [DllImport( "urlmon.dll" , CharSet=CharSet.Unicode , SetLastError=false )] public static extern System.UInt32 FindMimeFromData( System.UInt32 pBC, [In, MarshalAs(UnmanagedType.LPWStr)] System.Text.StringBuilder pwzUrl, [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex=3)] byte[] pBuffer, [In, MarshalAs(UnmanagedType.U4)] System.UInt32 cbSize, [In, MarshalAs(UnmanagedType.LPWStr)] System.Text.StringBuilder pwzMimeProposed, [In, MarshalAs(UnmanagedType.U4)] System.UInt32 dwMimeFlags, [Out, MarshalAs(UnmanagedType.LPWStr)] out String ppwzMimeOut, [In, MarshalAs(UnmanagedType.U4)] System.UInt32 dwReserverd ); } }
Обратили внимание на атрибуты, определяющие маршалинг выходного параметра? А теперь код нашего командлета:
Import-Module ` (join-path ` -path $PSScriptRoot ` -childPath 'ITG.WinAPI.Common' ` ) ` ; 'ITG.WinAPI.UrlMon.cs' ` | % { Add-CSharpType ` -Path (join-path ` -path $PSScriptRoot ` -childPath $_ ` ) ` ; } ` ; function Get-MIME { <# .Synopsis Функция определяет MIME тип файла по его содержимому и расширению имени файла. .Description Функция определяет MIME тип файла по его содержимому и расширению имени файла (если файл недоступен). Обёртка для API FindMimeFromData. .Link http://msdn.microsoft.com/en-us/library/ms775107(VS.85).aspx http://social.msdn.microsoft.com/Forums/en-US/Vsexpressvcs/thread/d79e76e3-b8c9-4fce-a97d-94ded18ea4dd .Example "logo.jpg" | Get-MIME #> [CmdletBinding( )] param ( # путь к файлу, MIME для которого необходимо определить [Parameter( Mandatory=$true, Position=0, ValueFromPipeline=$true )] [System.IO.FileInfo]$Path ) process { try { $length = 0; [Byte[]] $buffer = $null; if ( [System.IO.File]::Exists( $Path ) ) { Write-Verbose "Определяем MIME тип файла по его содержимому (файл $($Path.FullName) доступен)."; $fs = New-Object System.IO.FileStream ( $Path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [system.IO.FileShare]::Read, 256 ); try { if ( $fs.Length -ge 256) { $length = 256; } else { $length = $fs.Length; }; if ( $length ) { $buffer = New-Object Byte[] (256); $length = $fs.Read( $buffer, 0, $length ); }; } finally { $fs.Dispose(); }; } else { Write-Verbose "Определяем MIME тип файла по расширению его имени (файл $($Path.FullName) недоступен)."; }; [string]$mime = ''; $err = [ITG.WinAPI.UrlMon.API]::FindMimeFromData( 0, $Path.FullName, $buffer, $length, $null, [ITG.WinAPI.UrlMon.FMFD]::URLAsFileName -bor [ITG.WinAPI.UrlMon.FMFD]::RetrunUpdatedImgMIMEs, [ref]$mime, 0 ); [System.Runtime.InteropServices.Marshal]::ThrowExceptionForHR( $err ); $mime; } catch [Exception] { Write-Error ` -Exception $_.Exception ` -Message "Возникла ошибка при попытке определения MIME типа для файла $($Path.FullName)" ` ; }; } } Export-ModuleMember ` Get-MIME ` ;
Ну а теперь определить MIME тип для файлов элементарно просто:
Import-Module ` (join-path ` -path ( ( [System.IO.FileInfo] ( $myinvocation.mycommand.path ) ).directory ) ` -childPath 'ITG.WinAPI.UrlMon' ` ) ` ; 'test\logo.jpg' ` , 'test\logo2.jpg' ` | % { (join-path ` -path ( ( [System.IO.FileInfo] ( $myinvocation.mycommand.path ) ).directory ) ` -childPath $_ ` ) ` } ` | Get-MIME -ErrorAction Continue ` ;
Приведённый выше код для существующего файла определит MIME по содержимому, для недоступного – по расширению имени файла.
P.S. С недавних пор перешёл на git и github. Описанный выше модуль доступен на github.com через репозиторий проекта ITG.WinAPI.UrlMon.
Расширяем класс System.IO.FileInfo
Изучив возможности манифеста модулей PowerShell, решил довести дело до логического финала – добавить свойство ContentType
в класс System.IO.FileInfo
. Приведу выдержку манифеста модуля, а следом – файл с описаниями типов для данного модуля:
# # Манифест модуля для модуля "ITG.WinAPI.UrlMon". # # Создано: Sergey S. Betke # # Дата создания: 08.10.2012 # # Архив проекта: https://github.com/sergey-s-betke/ITG.WinAPI # @{ # Файл модуля скрипта или двоичного модуля, связанный с данным манифестом ModuleToProcess = 'ITG.WinAPI.UrlMon.psm1' # Номер версии данного модуля. ModuleVersion = '2.0' # Уникальный идентификатор данного модуля GUID = 'd458bfbb-1a1a-4b36-aa39-d35018456959' # Автор данного модуля Author = 'Sergey S. Betke' # Компания, создавшая данный модуль, или его поставщик CompanyName = 'IT-Service.Nov.RU' # Заявление об авторских правах на модуль Copyright = '(c) 2012 Sergey S. Betke. All rights reserved.' # Описание функций данного модуля Description = 'Обёртки для Windows API UrlMon.dll и командлеты на их основе' ... # Файлы типа (.ps1xml), которые загружаются при импорте данного модуля TypesToProcess = @( 'ITG.WinAPI.UrlMon.Types.ps1xml' ) ... }
И файл ITG.WinAPI.UrlMon.Types.ps1xml:
<?xml version="1.0" encoding="utf-8" ?> <Types> <Type> <Name>System.IO.FileInfo</Name> <Members> <ScriptProperty> <Name>ContentType</Name> <GetScriptBlock> Get-MIME -Path $this; </GetScriptBlock> </ScriptProperty> <AliasProperty> <Name>MIMEContentType</Name> <ReferencedMemberName>ContentType</ReferencedMemberName> </AliasProperty> </Members> </Type> </Types>
Теперь можем воспользоваться нашим новым свойством:
Import-Module ` 'ITG.WinAPI.UrlMon' ` -Force ` ; # демонстрация свойства, добавленного через types.ps1xml $a = ( [System.IO.FileInfo] 'test\logo3.jpg' ); $a.ContentType;
Теперь уже точно всё с этим вопросом. Резюме: модули PowerShell существенно больше, чем кусок скрипта!
RSS комментарии
Обратная ссылка