В процессе решения задачи с загрузкой на сервер файла логотипа встал вопрос: как определить действительный 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 существенно больше, чем кусок скрипта!

Опубликовать комментарий

XHTML: Вы можете использовать следующие HTML теги: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Tags Связь с комментариями статьи:
RSS комментарии
Обратная ссылка