= sfMogileFSPlugin = sfMogileFS enables symfony applications to interact with [http://en.wikipedia.org/wiki/MogileFS MogileFS]. MogileFS is an anagram for "OMG Files" and was created by [http://www.livejournal.com LiveJournal] to handle the storage, replication and retrieval of the large amount of file uploads they were, and continue to, experience. Many of the web's most popular sites use MogileFS as their file store. This includes [http://www.digg.com Digg], [http://www.last.fm Last.fm], [http://www.guba.com Guba], and others. == Prerequisites == The following must be [http://durrett.net/mogilefs_setup.html installed and setup] before using this plugin. * [http://www.danga.com/perlbal/ Perlbal] * [http://www.danga.com/mogilefs/ MogileFS] * [http://www.php.net/curl CURL] == How It Works == Perlbal is a load balancer that runs on port 80 with a pool of apache servers running behind it. In addition to dispatching requests to the internal servers, Perlbal also sends the critical X-REPROXY-URL header to MogileFS. MogileFS sends the file back based on the key given. == MogileFS Terminology == There are four main components to a MogileFS setup: * tracker(s): talk to the MogileFS database. By default run on port 6001. * database (a ''MySQL Cluster'' is recommended but simply one db server will work): maintains a list of storage nodes, files, etc. * storage node(s). Physical disks (hard drives) where files are stored. Mogstored defaults to port 7500. * client library (aka sfMogileFSPlugin) - talks to trackers (mogilefsd). Storing files: * domain: Top level separation of files. File keys are unique within domains. The Socialhouse.com domain (socialhouse) is defined within the socialhouse application in app.yml. A domain consists of a set of classes that define the files within the domain. class * key: A unique textual string that identifies a file. Keys are unique within domains. Examples of keys: userpicture:34:39, phonepost:93:3834, userbackup:15. * class: Part of exactly one domain. A class, in effect, only specifies the minimum replica count of a file. Examples of Socialhouse classes: image, thumbnail, video. * minimum replica count: Property of a class. This defines how many times the files in that class need to be replicated onto different devices in order to ensure redundancy among the data and prevent loss. * file: Every file is part of exactly one class. A file is a defined collection of bits uploaded to MogileFS to store. Files are replicated according to their minimum replica count. Each file has a key, is a part of one class, and is located in one domain. == Plugin Installation == {{{ $ php symfony plugin-install http://plugins.symfony-project.com/sfMogileFSPlugin }}} In your app.yml file add: {{{ dev: sfMogileFSPlugin: domain: mogilefs_domain timeout: 5 trackers: [111.111.111.1:6001, 222.222.222.2:6001] }}} You will also need to define settings for your production environment. Modify your routing.yml (feel free to change "i", module, and action to whatever you'd like). {{{ @mfs url: /i/:mkey param: { module: file, action: showMogileFSFile } }}} This plugin was designed to primarily add and retrieve files from MogileFS. You will need to create your own media handling model(s), module(s) and action(s), though an example setup is given below. Installation complete. Clear your cache. == Plugin Files == The plugin contains 4 classes and one helper. They are phpdocumented and unit tests are included in the tests/ folder. * sfMogileFSBaseFile.class.php - contains methods shared by both sfMogileFSFile and sfMogileFSRemote file. * sfMogileFSFile.class.php - represents a file before it's saved to MogileFS. * sfMogileFSRemoteFile.class.php - represents a file retrieved from MogileFS. * sfMogileFS.class.php - contains static methods for interacting with the MogileFS trackers. * MogileFSAssetsHelper.php - wrapper for existing symfony helper's suchc as url_for and image_tag. To output an image file from your template: {{{ <?php use_helper('MogileFSAsset') ?> <?php echo mfs_image_tag($mogile_key.$ext) ?> }}} To add a file to MogileFS: {{{ $mfile = new sfMogileFSFile(); $mfile->setKey($mfs_key); $mfile->setClass($mfs_class); $mfile->setFile($local_file); $mfile->save(); // saves to MogileFS }}} Alternatively: {{{ $mfile = new sfMogileFSFile($key, $class, $file); $mfile->save(); }}} To retrieve a file directly from MogileFS as a string: {{{ $mfile = sfMogileFS::loadFile($mfs_key); }}} To get a valid tracker path: {{{ $mfile = sfMogileFS::loadFile($mfs_key); $mfile->getPath(); }}} == Example Setup == This is a complete example. However, You should only use this as a guide and should make changes based on your own application and needs. The example assumes you already have a file in MogileFS with the key "cab9f3c712d04de874dafb0af0a0bf03e303e6e0". schema.yml: {{{ files: _attributes: { phpName: File } id: user_id: { type: integer, foreignTable: users, foreignReference: id, onDelete: cascade } file_type_id: { type: integer, foreignTable: file_types, foreignReference: id, onDelete: cascade } mime_type: varchar(128) extension: varchar(8) title: varchar(128) description: varchar(255) created_at: updated_at: photos: _attributes: { phpName: Photo } id: file_id: { type: integer, foreignTable: files, foreignReference: id, onDelete: cascade } photo_type_id: { type: integer, foreignTable: photo_types, foreignReference: id, onDelete: cascade } photo_status_type_id: { type: integer, foreignTable: photo_status_types, foreignReference: id, onDelete: cascade } mogilefs_key: { type: varchar(40), phpName: MogileFSKey, required: true } width: integer height: integer filesize: integer created_at: }}} routing.yml: {{{ mfs: url: /i/:mkey param: { module: file, action: showMogileFSFile } }}} /apps/<app>/modules/file/actions/uploadAction.class.php: {{{ class uploadAction extends sfAction { public function execute() { if ($this->getRequest()->getMethod() == sfRequest::POST && $this->getRequest()->hasFiles()) { if ($this->getRequest()->hasFileErrors()) { $this->setFlash('notice', 'An error occurred while uploading ('.$this->getRequest->getFileError('file').')'); $this->redirect('@upload'); } // save() method adds file to File and Photo tables as well as MogileFS $file = new File(); $file->setUserId($this->getUser()->getId()); $file->setFileTypeId(1); // image $file->setMimeType($this->getRequest()->getFileType('file')); $file->setExtension($this->getRequest()->getFileExtension('file')); $file->setTitle($this->getRequestParameter('title')); $file->setDescription($this->getRequestParameter('description')); $file->setTmpFile($this->getRequest()->getFilePath('file')); // not actually part of model but used by it $file->setFilesize($this->getRequest()->getFileSize('file')); // not actually part of the model but used by it $file->save(); // delete upload unlink($file->getTmpFile()); // redirect to upload form $this->setFlash('notice', 'Your file has been uploaded'); $this->redirect('@upload'); } else { // display upload form return sfView::SUCCESS; } } }}} /<project>/lib/model/Photo.php: {{{ class Photo extends BasePhoto { public function save($con = null) { if ($this->isNew()) { $this->setMogileFSKey(hash('sha1', $this->getFileId().':'.$this->getId())); } parent::save($con); } } }}} /<project>/lib/model/File.php: {{{ class File extends BaseFile { protected $filesize; protected $tmp_file; public function setFilesize($fileSize) { $this->filesize = $fileSize; } public function getFilesize() { return $this->filesize; } public function setTmpFile($filePath) { $this->tmp_file = $filePath; } public function getTmpFile() { return $this->tmp_file; } /** * save * adds original file to file table, sub-file (photo, etc) table and MogileFS * All steps are wrapped in a transaction. * * @param mixed $con * @access public * @return void */ public function save($con = null) { if ($this->isNew()) { list($width, $height) = getimagesize($this->getTmpFile()); // store it $con = Propel::getConnection(); try { $con->begin(); // save to File table parent::save($con); // add to photo table if ($this->getFileTypeId() == 1) { $photo = new Photo(); $photo->setFileId($this->getId()); // parent file $photo->setPhotoTypeId(1); // original photo $photo->setPhotoStatusTypeId(1); // uploaded but not converted $photo->setWidth($width); $photo->setHeight($height); $photo->setFilesize($this->getFilesize()); $photo->save(); } // save to MogileFS $sfMogileFSFile = new sfMogileFSFile(); $sfMogileFSFile->setKey($photo->getMogileFSKey()); $sfMogileFSFile->setClass('orig_photo'); $sfMogileFSFile->setFile($this->getTmpFile()); $sfMogileFSFile->setFilesize($photo->getFilesize()); $sfMogileFSFile->save(); $con->commit(); } catch (Exception $e) { $con->rollback(); throw $e; } } } } }}} /apps/<app>/modules/file/actions/showMogileFSFileAction.class.php: {{{ class showMogileFSFileAction extends sfAction { public function execute() { // remove file's extension $mogileFSKey = preg_replace('@\.[\w\d]+$@', '', $this->getRequestParameter('mkey')); // get mogilefs key from photo table $c = new Criteria(); $c->add(PhotoPeer::MOGILEFS_KEY, $mogileFSKey); $photo = PhotoPeer::doSelectOne($c); // load path from mogilefs $sfMogileFSRemoteFile = sfMogileFS::loadFile($mogileFSKey); // call perlbal's x-reproxy-url and set mime type $this->getResponse()->setHttpHeader('Content-type', $photo->getFile()->getMimeType(), true); $this->getResponse()->setHttpHeader('X-REPROXY-URL', $sfMogileFSRemoteFile->getPath(), true); return sfView::HEADER_ONLY; } } }}} layout.php (or any template): {{{ <?php use_helper('MogileFSAsset') ?> <?php echo mfs_image_tag('cab9f3c712d04de874dafb0af0a0bf03e303e6e0.jpg') ?> }}} This results in the following HTML rendering: {{{ <img src="/index.php/i/cab9f3c712d04de874dafb0af0a0bf03e303e6e0.jpg" /> }}} == Todo == * More detailed How It Works explanation * Example of calling MogileFS using a separate php script and mod_rewrite (instead of having to launch symfony for every image call) * Backend module with MogileFS stats, etc. == Changelog == === trunk === 2007-07-19 | 0.6.0 Alpha * bmeynell: Initial release