ImageBehavior – uploaduj pliki prosto do bazy

Jakiś czas temu napisałem o pomyśle cake’owego Behavior (http://webbricks.blogspot.com/2009/02/pliki-w-formie-binarnej-w-bazie.html). Poniżej prezentuję pierwsze podejście do problemu:


/**
* ImageBehavior - take best from database blobs adn file image storage

* requires ’content’ field that is a blob (mediumblob or longblob), and
* ’ext’ varchar(10) field and

* ’modified’ datetime field
* @author Grzegorz Pawlik
* @version 1.0
*/
class ImageBehavior extends ModelBehavior {

/**
* directory in which cached files will be stored
*
* @var string
*/
var $cacheSubdir = ‘filecache’;

/**
* if set to false - never check if cached file is present (nor actual)
*

* @var bool
*/
var $usecache = true;

function setup(&$Model) {
// no setup at this time

}

/**
* Insert proper blob when standard data after upload is present
*
* @param object $Model

* @return bool true
*/
function beforeSave(&$Model) {

if(isset($Model->data[$Model->name]['file']['tmp_name']) && is_uploaded_file($Model->data[$Model->name]['file']['tmp_name'])) {

// podnieś wyżej parametry
$Model->data[$Model->name] = array_merge($Model->data[$Model->name], $Model->data[$Model->name]['file']);

// przygotuj blob
$this->_prepareBlob($Model);

$this->_getExt($Model);
}

return true;
}

/**
* prepares blob contents
*
* @param object $Model

*/
function _prepareBlob(&$Model) {
App::import(‘Core’, ‘File’);
$file = new File($Model->data['Medium']['tmp_name'], false);

$content = $this->addSlashes( $file->read() );
$Model->data[$Model->name]['content'] = $content;

}

/**
* Get uploaded file extension
*
* @param object $Model
*/
function _getExt(&$Model) {

$file = explode(‘.’, $Model->data['Medium']['name']);
$ext = array_pop($file);

$Model->data[$Model->name]['ext'] = $ext;
}

/**
* replace blob contents with file path

* After reading database checks if cached file is present. If not creates it (from blob contents) and

* returns a ’file’ field with path relative to /app/webroot/img
*
*
* @param object $model

* @param array $results
* @param unknown_type $primary
* @return unknown
*/
function afterFind(&$model, $results, $primary) {

foreach($results as $key => $val) {

$relpath = $this->cacheSubdir . DS .

$val[$model->name]['id'] . ‘_’ . $model->name . ‘_’ .

$val[$model->name]['modified'] . ‘.’ . $val[$model->name]['ext'];

$relpath = str_replace( array(‘ ’, ‘:’) , ‘_’, $relpath);

$fullpath = IMAGES . $relpath;

if(!file_exists($fullpath) || !$this->usecache ) {
file_put_contents($fullpath, $this->stripSlashes($results[$key][$model->name]['content']));

}

$results[$key][$model->name]['file'] = $relpath;
// remove blob from results (its messy when You want to output results in debug)

unset($results[$key][$model->name]['content']);
}
return $results;
}

/**
* add slashes (just wrapper)
*
* @param string $string
* @return string with slashes
*/

function addSlashes($string) {
return addslashes($string);
}

/**
* strip slashes (just wrapper)

*
* @param string $string
* @return string without slashes
*/
function stripSlashes($string) {

return stripslashes($string);
}
}

Zasada działania jest dość prosta. Wyjaśnię ją na przykładzie.
Tabela media:

CREATE TABLE IF NOT EXISTS `media` (
`id` int(11) NOT NULL auto_increment,
`name` varchar(50) NOT NULL,

`ext` varchar(10) NOT NULL,
`content` longblob NOT NULL,
`size` int(11) NOT NULL,
`created` datetime NOT NULL,
`modified` datetime NOT NULL,

`type` varchar(20) NOT NULL,
PRIMARY KEY  (`id`)
) ENGINE=MyISAM;

Model:

class Medium extends AppModel {

var $name = ‘Medium’;
var $actsAs = array(‘Image’);

}

Kontroler:

class MediaController extends AppController {

var $name = ‘Media’;
var $helpers = array(‘Html’, ‘Form’);
function index() {

$this->set(‘media’, $this->Medium->findAll());
}

function add() {
if(!empty($this->data)) {
$this->Medium->save($this->data);

}
}

}

Przy uploadzie ImageBehavior oczekuje, że plik będzie przekazany w poly ModelName.file (tutaj Media.file).

add.ctp:

create(
array(‘url’ => array(
‘controller’ => ‘media’,

‘action’ => ‘add’
),
‘enctype’ => ‘multipart/form-data’
)
);
?>

file(‘Medium.file’); ?>
end(’submit’); ?>

Przy odczycie dzieje się to co mnie najbardziej interesowało. Zamiast dostać zawartość (BLOB) pliku, dostajemy w polu file ścieżkę (relatywną do app/webroot/img). Domyślne ustawienia wymagają, żeby był tam katalog filecache (z możliwością zapisu). Przy operacji read behavior sprawdzi, czy istnieje aktualy plik w filecache, i jesli nie – utworzy go.

index.ctp:


image($medium['Medium']['file']); ?>

To rozwiązanie ma przynajmniej dwa zauważalne braki:

  1. Gdy dodamy taki plik do treści np. postu, po jego updacie – nie będą widoczne zmiany
  2. Dobrze byłoby, gdyby przy operacji read nie zwracał zawartości BLOB (ważne, gdy baza jest gdzieś dalej), ale odpytywał tylko wtedy, gdy jest potrzebne zaktualizowanie zawartości pliku w filecache.

To rzeczy do zrobienia w kolejnym podejściu ;)


Aktualizacja: po umieszczeniu tego pomysłu na bakery niejaki Travis Rowland wrzucił ciekawe poprawki. Nie miałem okazji ich wypróbować, ale dla zainteresowanych: http://bakery.cakephp.org/articles/view/imagebehavior-best-from-database-blobs-and-file-storage

Share Button

Co się dzieje, gdy dane są nie tylko w bazie?

Z tym problemem często spotykam się w pracy.

Standardowe zagadnienie – klient chce wrzucać obrazki na stronę, a my ze względu na bazę zapisujemy je jako pliki, a w bazie co najwyżej ścieżkę do niego.
No i niestety przeniesienie systemu na inny serwer (np. produkcyjny) to, oprócz kopiowania kodu i bazy, przenoszenie multimediów, które nie zawsze jest miłe i przyjemne.

Dlatego od jakiegoś czasu chodzi mi po głowie pewna koncepcja, która ten problem mogłaby rozwiązać.
Otóż pliki binarne, podczas uploadu należało by jednak zapisać w bazie. Do tego należałoby opracować komponent, który w przypadku żądania obrazka o id = 1 sprawdziłby odpowiedni katalog i po znalezieniu go – zwrócił jako odpowiedź. Z kolei przy jego braku w systemie plików – utworzył takowy na podstawie danych w bazie i w standardowy sposób zwrócił plik jako odpowiedź.

Świetnie by się do tego nadawały cake’owe behaviors. Do tego można by go sprząc z jeszcze jedną funkcjonalnością- plik mógłby być w bazie zapisywany (w razie podmiany zawartości) za każdym razem pod inną nazwą (np. id_kolejny_numer_wersji). Dzięki temu możnaby tym plikom ustawić nagłówki Expire z odległą przyszłością i korzystać z dobrodziejstw bufora przeglądarek…

Share Button