* @license PHP License * @package WB * @subpackage vfs */ WBClass::load('WBLog' , 'WBVFS' ); /** * Virtual File System: ChunkUpload * * Implement chunk upload feature backend * * @version 1.0.0 * @package WB * @subpackage vfs */ class WBVFS_ChunkUpload extends WBVFS { /** * location of chunks */ const CHUNK_DIR = '%s/var/spool/vfs/chunk/%s'; /** * table * @var WBDatasource_Table */ private $table; /** * current upload id * @var string */ private $id; /** * md5 checksum, if available * @var string */ private $md5 = ''; /** * expected file size * @var int */ private $size; /** * id of user that uploads files * @var string */ private $uid; /** * number of uploaded chunks * @var array */ private $chunks = 0; /** * Upload creation timestamp * @var string */ private $created = '0000-00-00 00:00:00'; /** * Upload last change timestamp * @var string */ private $changed = '0000-00-00 00:00:00'; /** * logger * @var WBLog */ private $log; /** * constructor * * Initialize table access */ public function __construct() { $this->table = WBClass::create('WBDatasource_Table'); $this->log = WBLog::start(__CLASS__); } /** * Set user * * User id is required to dell who own the upload chunks * * @param string $uid */ public function setUser($uid) { $this->uid = $uid; } /** * Get current upload id * * @return string $id */ public function getId() { return $this->id; } /** * Check user id * * Make sure use id is set * @throws WBException_Call */ private function checkUser() { if (empty($this->uid)) { WBClass::load('WBException_Call'); throw new WBException_Call('User is required, call setUser() first', 1, __CLASS__); } } /** * Create new upload id * * Check upload data and create an upload slot for it. * * @param array $data */ public function create($data) { $this->checkUser(); $this->id = null; $this->md5 = ''; $this->size = 0; $this->chunks = 0; $this->created = gmdate('Y-m-d H:i:s'); $this->changed = gmdate('Y-m-d H:i:s'); // verify size if (!isset($data['size']) || empty($data['size']) || 0 > $data['size']) { WBClass::load('WBException_Argument'); throw new WBException_Argument('Proper file size is required', 2, __CLASS__); } $this->size = $data['size']; $uPrimary = $this->table->getIdentifier(WBVFS::TABLE_USER); $save = array( $uPrimary => $this->uid, 'chunks' => $this->chunks, 'size' => $this->size, 'md5' => '', 'created' => $this->created, 'changed' => $this->changed ); // check for same md5 sum if (isset($data['md5']) && !emty($data['md5'])) { $clause = array(); $clause[] = array( 'field' => $uPrimary, 'value' => $this->uid() ); $clause[] = array( 'field' => 'md5', 'value' => $data['md5'] ); if ($this->table->count(WBVFS::TABLE_UPLOAD, null, null, $clause)) { WBClass::load('WBException_Argument'); throw new WBException_Argument('It is not allowed to create several uploads with the same md5-checksum', 3, __CLASS__); } $save['md5'] = $data['md5']; } $this->id = $this->table->save(WBVFS::TABLE_UPLOAD, '__new', $save); $log = array( 'action' => 'create', 'uploadid' => $this->id, 'user' => $this->uid, 'size' => $this->size, 'md5' => $this->md5 ); $this->log->notice($log); return $this->id; } /** * Load upload chunks by id * * Select current upload by id * * @param string $id * @param string $id */ public function loadById($id) { if ($this->id == $id) { return $this->id; } $this->checkUser(); $upload = $this->table->get(WBVFS::TABLE_UPLOAD, $id, $this->uid); return $this->loadByRaw($upload); } /** * Load upload chunks by md5 checksum * * Try to find upload by MD5 sum * * @param string $md5 * @param string $id */ public function loadByMD5($md5) { $this->checkUser(); $clause = array(); $clause[] = array( 'field' => 'md5', 'value' => $md5 ); $upload = $this->table->get(WBVFS::TABLE_UPLOAD, null, $this->uid, $clause); return $this->loadByRaw($upload); } /** * Load upload by raw data * * Use database result set tom populate object * @param array $upload * @return string */ private function loadByRaw($upload) { $this->id = null; $this->md5 = null; $this->size = null; if (1 != count($upload)) { return null; } $uPrimary = $this->table->getIdentifier(WBVFS::TABLE_USER); if ($this->uid != $upload[0][$uPrimary]) { return null; } $this->id = $upload[0][$this->table->getIdentifier(WBVFS::TABLE_UPLOAD)]; $this->md5 = $upload[0]['md5']; $this->size = $upload[0]['size']; $this->chunks = $upload[0]['chunks']; $this->created = $upload[0]['created']; $this->changed = $upload[0]['changed']; return $this->id; } /** * Store chunk * * Add chunk to current upload * * @param int $position * @param string $cnt chunks content */ public function add($position, $cnt) { if (!$this->id) { WBClass::load('WBException_Call'); throw new WBException_Call('Upload id is required, call loadById() or create() first', 4, __CLASS__); } $dir = sprintf(self::CHUNK_DIR, WBParam::get('wb/dir/base'), $this->id); if (!is_dir($dir)) { mkdir($dir, 0777, true); } // calculate position by adding file sizes if (0 > $position) { $position = 0; foreach (new DirectoryIterator($dir) as $file) { if ($file->isDot()) { continue; } if (!$file->isFile()) { continue; } $position += $file->getSize(); } } $file = $dir . '/' . $position; file_put_contents($file, $cnt); chmod($file, 0666); // update ++$this->chunks; $save = array( 'chunks' => $this->chunks, 'changed' => gmdate('Y-m-d H:i:s') ); $this->table->save(WBVFS::TABLE_UPLOAD, $this->id, $save); $log = array( 'action' => 'upload', 'uploadid' => $this->id, 'user' => $this->uid, 'chunks' => $this->chunks, 'position' => $position, 'size' => strlen($cnt) ); $this->log->info($log); return $position; } /** * Remove uploaded chunks * * Remove upload record from database and uploaded chunk files */ public function rm() { if (!$this->id) { return null; } $this->table->delete(WBVFS::TABLE_UPLOAD, $this->id); $this->unlinkUpload($this->id); $log = array( 'action' => 'rm', 'uploadid' => $this->id, 'user' => $this->uid, 'chunks' => $this->chunks ); $this->log->info($log); $this->id = null; $this->md5 = ''; $this->size = 0; $this->chunks = 0; } /** * Import chunks into WBVFS_File * * @param WBVFS_File $file * @param string name */ public function import($file, $name) { if (!$this->id) { return; } // record import time $start = time(); $this->checkUser(); $dir = sprintf(self::CHUNK_DIR, WBParam::get('wb/dir/base'), $this->id); if (!is_dir($dir)) { WBClass::create('WBException_File'); throw new WBException_File('Chunk folder does not exist', 5, __CLASS__); } // temporary file $tmp = WBClass::create('WBFile'); $tmp->tempnam(); $fh = fopen($tmp->realpath(), 'w'); // find chunks $chunks = array(); foreach (new DirectoryIterator($dir) as $chunk) { if ($chunk->isDot() || !$chunk->isFile()) { continue; } $chunks[] = $chunk->getFilename(); } // sort to avoid fseek operations natsort($chunks); // assamble file from chunks foreach ($chunks as $chunk) { fseek($fh, $chunk); fwrite($fh, file_get_contents($dir . '/' . $chunk), filesize($dir . '/' . $chunk)); } fclose($fh); // validate filesize clearstatcache(); if (filesize($tmp->realpath()) != $this->size) { $tmp->unlink(); WBClass::load('WBException_File'); throw new WBException_File('Different file size excpeted, maybe chunk upload was incomplete', 6, __CLASS__); } // validate md5 sum - optionally if (!empty($this->md5) && md5_file($tmp->realpath()) != $this->md5) { $tmp->unlink(); WBClass::load('WBException_File'); throw new WBException_File('MD5 sum does not match', 7, __CLASS__); } $file = $file->import($tmp->realpath(), $name); $log = array( 'action' => 'import', 'uploadid' => $this->id, 'user' => $this->uid, 'fileid' => $file->getId(), 'chunks' => $this->chunks, 'size' => $this->size, 'uploadtime' => strtotime($this->changed) - strtotime($this->created), 'importtime' => time() - $start ); $this->log->notice($log); } /** * remove temporary upload folder * * Remove folder and all chunks * * @param string $id */ private function unlinkUpload() { $dir = sprintf(self::CHUNK_DIR, WBParam::get('wb/dir/base'), $this->id); if (!is_dir($dir)) { return; } foreach (new DirectoryIterator($dir) as $file) { if ($file->isDot()) { continue; } if ($file->isDir()) { WBClass::load('WBException_File'); throw new WBException_File('File expected, but got directory!', 8, __CLASS__); } unlink($file->getPathName()); } rmdir($dir); } }