PHP a MS SQL – vkládání a načítání souborů

18. března 2005

Když jsem potřeboval napsat program v PHP, který by ukládal soubory přímo do databáze, zjistil jsem, že nelze na internetu najít popsané řešení. Buď bylo pro MySQL nebo pro ASP. A tak vám tady ukážu řešení, které mě stálo hezkých pár hodin hledání. Řešení je provedeno (a hlavně odzkoušeno) nad MS SQL 2000 server

MS SQL – trocha teorie

MS SQL server podporuje takzvané BLOB (Binary Large OBject) objekty. Chceme-li ukládat soubory přímo do databáze, musí být datový typ sloupce buď:

  • ntext – textový datový typ podporující Unicode; max. počet znaků 230-1
  • text – textový datový typ nepodporující Unicode; max. počet znaků 231-1
  • image – binární datový typ umožňující pojmout až 231-1 bytů.

Datový sloupec, do kterého budeme soubory ukládat, nastavíme na typ „image“. Jak se ale později ukáže, bude nutné použít i datový typ „text“. Server navíc s BLOB daty pracuje tak, že si je ukládá do speciálního prostoru. V samotné tabulce uchovává jen ukazatel na uložený blok dat.

Navíc kvůli nutnosti používat datové typy BLOB jsme vystaveni ze strany databáze docela zásadním omezením:

  • V proceduře na straně databáze nelze deklarovat datový typ BLOB.
  • Jakékoli změny s BLOB lze provést jen přes speciální procedury.
  • SQL Server pracuje s BLOB tak, že používá takzvaný ukazatel (textpointer). Před jakoukoli změnou s BLOB je nutná inicializace ukazatele. Ukazatel je 16 bitové binární číslo.
  • Data lze načítat jen po určitém bloku, jehož velikost může být max. 2kB (lze nastavit vyšší velikost bloku, ale víc jak 2 kB mi SQL server nevracel).

Zakládáme potřebné objekty v databázi

Nejdříve vytvoříme tabulku:

CREATE TABLE [dbo].[SOUBORY] (
 [ID] INT IDENTITY (1, 1) NOT NULL ,
 [POPIS] NVARCHAR (250) NOT NULL ,
 [SOUBOR_VELIKOST] BIGINT NOT NULL ,
 [SOUBOR_TYP] NVARCHAR (100) NOT NULL ,
 [SOUBOR_NAZEV] NVARCHAR (100) NOT NULL ,
 [DATA] IMAGE NOT NULL ,
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO

Význam parametrů:

  • ID – jednoznačný číselný identifikátor souboru; číslo si generuje databáze sama
  • POPIS – krátký textový popis souboru
  • SOUBOR_VELIKOST – určuje velikost přílohy v bitech
  • SOUBOR_TYP – uchovává mimetype souboru
  • SOUBOR_NAZEV – jméno souboru
  • DATA – obsahuje data

Nyní máme vytvořenou tabulku a můžeme přejít k vytvoření procedury, která obstará uložení dat:

CREATE PROCEDURE [dbo].[ULOZ_SOUBOR]
(
   @file_name NVARCHAR (100), — jméno souboru
   @file_size BIGINT, –velikost souboru
   @file_data TEXT, –samotná data souboru (v binárním tvaru)
   @file_mimetype NVARCHAR(100), –mimetype souboru
   @popis NVARCHAR (250) — krátký popis souboru
)
AS
begin
declare @id binary(16) –toto je ukazatel na BLOB objekt
declare @get_id int –použijeme jako pomocnou proměnnou
–nejdříve je nutné vložit záznam do tabulky (bez dat)
–pak se přidají binární data
insert into SOUBORY (POPIS, SOUBOR_VELIKOST, SOUBOR_TYP, SOUBOR_NAZEV, DATA)
values (@popis, @file_size, @file_mimetype, @file_name, 0x)
–hodnota 0x není ničím jiným, než uložením „žádného“ bitu do dat. sloupce typu BLOB
— zjistíme si id soubory, které databáze vloženému záznamu přidělila
set @get_id=@@identity
–zjištění kurzoru
set @id=(select textptr(DATA) from SOUBORY where ID= @get_id)
writetext HC_PRILOHA.DATA @id @file_data
–writetext je vhodnější než updatetext => zruší stávající data
end
GO

Vtipem tohoto řešení je použití datového typu „text“ jako jednoho ze vstupních parametrů. Při použití datového typu „image“ přestává ukládání souborů fungovat. Další nutností je uložení hodnoty „0x“ do datového sloupce DATA při ukládání nebinárních informací.

PHP – ukládání binárních dat do MS SQL

Nyní již můžeme vytvořit skript, který provede upload dat. Do databáze se přihlašuji pomocí NTLM (windows autenticate) a ODBC. V příkladu neřeším kontrolu velikosti ukládaného souboru. Zde je nutné vzít potaz, že v php.ini musí být upload souboru povolen a také musí být nastavena vhodně maximální velikost nahrávaného souboru.

<?php
// hlavička HTML/XHTML souboru
//napojení na databázi pomocí ODBC, autentifikace pomocí NTLM
$dbconnect=odbc_connect(„uloz_soubor“);
      or die(„cannot connect to database (err.1a) \n“);
//=======================
if($_POST[„action“]==“upload“):    //uživatel se rozhodl uložit soubor
   $fp=FOpen($_FILES[„userfile“][tmp_name],“r“); //otevřeme si načtený soubor pro čtení
   $data=(Fread($fp,$_FILES[„userfile“][size]));
   $data=bin2hex($data);  //data je nutné z bináru převést do hexadecimálního tvaru
   FClose($fp);
   $popis=addslashes($_POST[„popis“]);
   $query=“ULOZ_SOUBOR ‚“.$_FILES[„userfile“][name].“‚,“.$_FILES[„userfile“][size].“, ‚“.$data.“‚,
     ‚“.$_FILES[„userfile“][type].“‚“;
   ODBC_exec($dbconnect,$query);
endif;
?>
<table cellpadding=“8″ align=“center“ border=“0″>
  <form action=““ method=“post“ enctype=“multipart/form-data“ action=““>
    <tr><td align=“center“>
      <strong>Vyberte soubor</strong>
    </td></tr>
    <tr><td>
      <input type=“file“ name=“userfile“>
  <tr>
    <td>popis:
      <input type=“text“ name=“popis“>
    </td>
  </tr>
  <tr>
    <td align=“right“>
      <input type=“submit“ value=“ulož“>
    </td>
  </tr>
      <input type=“hidden“ name=“action“ value=“upload“>
  </form>
</table>
<?php
//zápatí souboru
?>

V tomto souboru je jednou z nejdůležitějších věcí převod binárně načteného souboru do hexadecimálního tvaru. Ostatní „know-how“ je ve výše popsané proceduře ULOZ_SOUBOR ukládající data.

PHP – stažení souboru z MS SQL

Čtení souboru z databáze bude již plně obstarávat PHP. Mnou vytvořený příklad na základě zadaného parametru ID (tedy číselného identifikátoru přílohy) vyvolá v prohlížeči dialog k uložení souboru. Do databáze budu opět přistupovat pomocí ODBC.

<?php
//napojení na databázi pomocí ODBC, autentifikace pomocí NTLM
$dbconnect=odbc_connect(„uloz_soubor“);
    or die(„cannot connect to database (err.1a) \n“);
//=======================
$id_priloha=$_GET[„id“];
$blok_dat=512; //data budeme z databáze číst po 512 b
if (isset($id_priloha) and is_numeric($id_priloha)):
//je známo id přílohy, které je navíc číslem
  $query=“select * from SOUBORY where id=“.$id_priloha;
//potřeba zjistit informace o záznamu dané přílohy
  $soubor=ODBC_exec($dbconnect,$query);
  if (ODBC_fetch_row($soubory)): //jestliže je nalezen záznam o souboru
    $velikost=ODBC_result($soubory,“SOUBOR_VELIKOST“);
    $jmeno=ODBC_result($soubory,“SOUBOR_NAZEV“);
    $mime_type=ODBC_result($soubory,“SOUBOR_TYP“);
    $p=0; //$p označuje pozici, odkud se začneme binárně soubor z databáze číst (pokud 0, tak od začátku)
    //zjištění ukazatele a dalších pomocných proměnných
    $sql=“SELECT TEXTPTR(DATA) as ‚textptr‘, DATALENGTH(DATA) as ‚blobsize‘,
      $p as ‚chunkindex‘,
    CASE WHEN $blok_dat< DATALENGTH(DATA) THEN 512 ELSE DATALENGTH(DATA) END as ‚chunksize‘
    FROM SOUBORY where id=“.$id_priloha;
    $rs=ODBC_exec($dbconnect,$sql);
    //načtení výsledků z dtb do proměnných
    $textptr=bin2hex(ODBC_result($rs,“textptr“)); //ukazatel (musí být binární) na pozici souboru
    $blobsize=ODBC_result($rs,“BLOBSIZE“); //definuje celou velikost souboru
    $chunksize=ODBC_result($rs,“CHUNKSIZE“); //definuje po kolika bitech se bude načítat soubor
//Soubor je nutné načítat po blocích! Nelze najednou… chunksize je velkost bloku
    $chunkindex=ODBC_result($rs,“CHUNKINDEX“); //def. pozici, odkud se začíná číst blok dat (jehož velikost def. $chunksize
//=================
    if ($textptr<>““):  //jestliže je nalezen ukazatel
      while (($chunkindex<$blobsize)): //tento cyklus postupně načte všechna data
        if (($chunkindex+$chunksize)>$blobsize):
        /*
        kdybychom chtěli načíst větší blok, než jaký můžeme => pak je nutné vzít jako blok přesnou velikost toto typicky nastane tehdy, je-li max. možná délka načítaných dat menší, než udává proměnná $blok_dat
        */
          $chunksize=$blobsize-$chunkindex;
        endif;
      //celý princip je o tom, že SQL server není schopen vrátit víc jak 2048bitů.
      //Proto tedy /po námi stanoveném kroku v proměnné proměnná $blok_dat (512b)/
      //čteme celý soubor část po části a následně tyto bloky spojujeme do celku
        $sql2=“set textsize 512 READTEXT HC_PRILOHA.DATA 0x“.$textptr.“ $chunkindex „.($chunksize-$p).““;
      //nezapomeňme, že proměnná $p označuje počáteční pozici čtení dat;
        $rs2=ODBC_exec($dbconnect,$sql2);
      //přilepení bloku dat ke stávajícím
        $data.=ODBC_result($rs2,“DATA“);
      //navýšení indexu =>abychom četli následující blok dat
        $chunkindex=$chunkindex+$chunksize;
      endwhile;
    endif;
//a máme načtená data
//data je nutné převést s hexadecimálního tvaru na binární
    $data=pack(„H*“,$data);
//odešleme HTTP hlavičku
    header(„Pragma: public“); //vypnutí cache pro proxy servery atp. pro HTTPverze 1.0
    header(„Expires: 0“); //datum expirace – nastavení na ihned
    header(„Cache-Control: must-revalidate, post-check=0, pre-check=0“); //opět vypnutí cache; pro HTTP verze 1.1
    header(‚Content-Description: File Transfer‘);
    header(‚Content-Type: $mime_type‘);
    header(‚Content-Length: ‚ . $velikost);
    header(‚Content-Disposition: attachment; filename=‘ . $jmeno);
// a nyní odešleme data prohlížeči
    echo $data;
    exit();
  endif;
endif;
?>

Jednotlivé části skriptu jsou popsány dost dobře v komentářích, pro přehlednost uvádím souhrn akcí skriptu:

  • Zjistí si, zda existuje soubor s daným ID (hledá v URL proměnné id).
  • Pokud ID existuje, zjistí si informace o daném záznamu včetně ukazatele na BLOB objekt.
  • V cyklu si načte postupně blok po bloku binární data a spojí je. (Bohužel, elegantnější řešení bez nutnosti cyklického načítání buď nefungovalo, nebo se mi jej nepodařilo najít – pokaždé jsem narazil na různá omezení.)
  • Jakmile je načten celý blok dat, odešle HTTP hlavičky vypínající cache, informující o velikosti odesílaných dat, názvu souboru a podobně.
  • Odešle získaná binární data.

Starší komentáře ke článku

Pokud máte zájem o starší komentáře k tomuto článku, naleznete je zde.

Předchozí článek hazena
Další článek moonlake.cz
Štítky: Články

Mohlo by vás také zajímat

Nejnovější

4 komentářů

  1. rada

    Říj 8, 2009 v 1:37

    Preji pekny den,
    poustim se do php programovani, nyni s vyuzitim db mysql. Chtel bych se zeptat, jak resit zabezpeceni pro pripojeni k mysql db-jak vyresit bezpecnost prihlasovacich udaju(jmeno, heslo). Na strankach chci vypis nekterych udaju z databaze- na db se chci atuomaticky pripojit, ale vyplneni prihl. udaju v php kodu(mysql_connect („host“,“user“,“pass“)) neni pro me moc uklidnujici. Jak resit tento bezpecnostni problem? Predem dekuji za odpoved. R

    Odpovědět
  2. petr

    Říj 19, 2010 v 13:31

    Taky peknej den. Co se tyce pripojeni k databazi tak pouzij INCLUDE… to uz budes mit svedomi cistejsi… jinak doporucuji web „linuxsoft“ ktery pomaha zacatecnikum

    Odpovědět
  3. Jarda

    Říj 20, 2010 v 6:26

    A tim „INCLUDE“ vyresis jako co?

    Odpovědět
  4. Martin

    Dub 14, 2021 v 16:58

    Nebude to v hlavním kódu, jde to uložit jako globální proměnná.

    Odpovědět

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *