diff --git a/include/class.file.php b/include/class.file.php index 36f013a6a0e78fedb302a37a1897b41e004a7755..761aba16465ce8774c2fbcdde5c082720da75195 100644 --- a/include/class.file.php +++ b/include/class.file.php @@ -91,17 +91,14 @@ class AttachmentFile { return $this->ht['hash']; } + function open() { + return new AttachmentChunkedData($this->id); + } + function sendData() { - $chunk_size = 256 * 1024; - $start = 1; - for (;;) { - list($data) = db_fetch_row(db_query( - 'SELECT SUBSTRING(filedata,'.$start.','.$chunk_size - .') FROM '.FILE_TABLE.' WHERE id='.db_input($this->getId()))); - if (!$data) break; - echo $data; - $start += $chunk_size; - } + $file = $this->open(); + while ($chunk = $file->read()) + echo $chunk; } function getData() { @@ -184,28 +181,9 @@ class AttachmentFile { if (!(db_query($sql) && ($id=db_insert_id()))) return false; - $chunk_size = 256 * 1024; - $start = 0; - # This boils down to a disagreement between the MySQL community and - # developers. I'll refrain from a soapbox discussion here, but MySQL - # will truncate the field to '' when the length of a CONCAT expression - # exceeds the value of max_allowed_packet. See the following bugs for - # more information. The easiest fix is to expand the parameter. - # http://bugs.mysql.com/bug.php?id=22853 - # http://bugs.mysql.com/bug.php?id=34782 - # http://bugs.mysql.com/bug.php?id=63919 - if (db_get_variable('max_allowed_packet') < strlen($file['data'])) - db_set_variable('max_allowed_packet', strlen($file['data']) + $chunk_size); - while ($chunk = substr($file['data'], $start, $chunk_size)) { - $sql='UPDATE '.FILE_TABLE - .' SET filedata = CONCAT(filedata, 0x'.bin2hex($chunk).')' - .' WHERE id='.db_input($id); - if(!db_query($sql)) { - db_query('DELETE FROM '.FILE_TABLE.' WHERE id='.db_input($id).' LIMIT 1'); - return false; - } - $start += $chunk_size; - } + $data = new AttachmentChunkedData($id); + if (!$data->write($file['data'])) + return false; return $id; } @@ -242,44 +220,83 @@ class AttachmentFile { .'SELECT file_id FROM '.FAQ_ATTACHMENT_TABLE .') still_loved' .')'); + AttachmentChunkedData::deleteOrphans(); return db_affected_rows(); } } -class AttachmentList { - function AttachmentList($table, $key) { - $this->table = $table; - $this->key = $key; +/** + * Attachments stored in the database are cut into 256kB chunks and stored + * in the FILE_CHUNK_TABLE to overcome the max_allowed_packet limitation of + * LOB fields in the MySQL database + */ +define('CHUNK_SIZE', 256*1024); # Beware if you change this... +class AttachmentChunkedData { + function AttachmentChunkedData($file) { + $this->_file = $file; + $this->_pos = 0; } - function all() { - if (!isset($this->list)) { - $this->list = array(); - $res=db_query('SELECT file_id FROM '.$this->table - .' WHERE '.$this->key); - while(list($id) = db_fetch_row($res)) { - $this->list[] = new AttachmentFile($id); - } - } - return $this->list; + function length() { + list($length) = db_fetch_row(db_query( + 'SELECT SUM(LENGTH(filedata)) FROM '.FILE_CHUNK_TABLE + .' WHERE file_id='.db_input($this->_file))); + return $length; } - - function getCount() { - return count($this->all()); + + function seek($location) { + $this->_pos=$location; } - function add($fileId) { - db_query( - 'INSERT INTO '.$this->table - .' SET '.$this->key - .' file_id='.db_input($fileId)); + function tell() { + return $this->_pos; } - function remove($fileId) { + function read($length=CHUNK_SIZE) { + # Read requested length of data from attachment chunks + $buffer=''; + while ($length > 0) { + $chunk_id = floor($this->_pos / CHUNK_SIZE); + $start = $this->_pos % CHUNK_SIZE; + $size = min($length, CHUNK_SIZE - $start); + list($block) = @db_fetch_row(db_query( + 'SELECT SUBSTR(filedata, '.($start+1).', '.$size + .') FROM '.FILE_CHUNK_TABLE.' WHERE file_id=' + .db_input($this->_file).' AND chunk_id='.$chunk_id)); + if (!$block) return false; + $buffer .= $block; + $this->_pos += $size; + $length -= $size; + } + return $buffer; + } + + function write($what) { + # Figure out the remaining part of the current chunk (use CHUNK_SIZE + # and $this->_pos, increment pointer into $what and continue to end + # of what + $offset=0; + for (;;) { + $start = $this->_pos % CHUNK_SIZE; + $size = CHUNK_SIZE - $start; + $block = substr($what, $offset, $size); + if (!$block) break; + if (!db_query('REPLACE INTO '.FILE_CHUNK_TABLE + .' SET filedata=INSERT(filedata, '.($start+1).','.$size + .', 0x'.bin2hex($block) + .'), file_id='.db_input($this->_file) + .', chunk_id='.floor($this->_pos / CHUNK_SIZE))) + return false; + $offset += $size; + $this->_pos += strlen($block); + } + return true; + } + + function deleteOrpans() { db_query( - 'DELETE FROM '.$this->table - .' WHERE '.$this->key - .' AND file_id='.db_input($fileId)); + 'DELETE FROM '.FILE_CHUNK_TABLE.' WHERE file_id NOT IN ' + .'( SELECT id FROM '.FILE_TABLE.') still_loved'); + return db_affected_rows(); } } -?> diff --git a/include/upgrader/sql/15b30765-dd0022fb.patch.sql b/include/upgrader/sql/15b30765-dd0022fb.patch.sql new file mode 100644 index 0000000000000000000000000000000000000000..31307d655c731e2007046dad7f069892c8443288 --- /dev/null +++ b/include/upgrader/sql/15b30765-dd0022fb.patch.sql @@ -0,0 +1,39 @@ +/** + * @version v1.7 RC2+ + * @signature dd0022fb14892c0bb6a9700392df2de7 + * + * Migrate file attachment data from %file to %file_chunk + * + */ + +CREATE TABLE `%TABLE_PREFIX%T_file_chunk_id` ( `id` int(11) ); +-- Support up to 16MB attachments +INSERT INTO `%TABLE_PREFIX%T_file_chunk_id` VALUES (0), (1), (2), (3), (4), +(5), (6), (7), (8), (9), (10), (11), (12), (13), (14), (15), (16), (17), +(18), (19), (20), (21), (22), (23), (24), (25), (26), (27), (28), (29), +(30), (31), (32), (33), (34), (35), (36), (37), (38), (39), (40), (41), +(42), (43), (44), (45), (46), (47), (48), (49), (50), (51), (52), (53), +(54), (55), (56), (57), (58), (59), (60), (61), (62), (63); + +DROP TABLE IF EXISTS `%TABLE_PREFIX%file_chunk`; +CREATE TABLE `%TABLE_PREFIX%file_chunk` ( + `file_id` int(11) NOT NULL, + `chunk_id` int(11) NOT NULL, + `filedata` longblob NOT NULL, + PRIMARY KEY (`file_id`, `chunk_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +INSERT INTO `%TABLE_PREFIX%file_chunk` (`file_id`, `chunk_id`, `filedata`) + SELECT T1.`id`, T2.`id`, + SUBSTR(T1.`filedata`, T2.`id` * 256 * 1024 + 1, 256 * 1024) + FROM `%TABLE_PREFIX%file` T1, `%TABLE_PREFIX%T_file_chunk_id` T2 + WHERE T2.`id` * 256 * 1024 < LENGTH(T1.`filedata`); + +ALTER TABLE `%TABLE_PREFIX%file` DROP COLUMN `filedata`; +OPTIMIZE TABLE `%TABLE_PREFIX%file`; + +DROP TABLE `%TABLE_PREFIX%T_file_chunk_id`; + +-- Finished with patch +UPDATE `%TABLE_PREFIX%config` + SET `schema_signature`='dd0022fb14892c0bb6a9700392df2de7'; diff --git a/main.inc.php b/main.inc.php index 8ce3946696c8bb959200e3ea2366805eff58efa5..235a6341a2db15e7be7b5c1e313e6b85e9b678f3 100644 --- a/main.inc.php +++ b/main.inc.php @@ -63,7 +63,7 @@ #Current version && schema signature (Changes from version to version) define('THIS_VERSION','1.7-RC2+'); //Shown on admin panel - define('SCHEMA_SIGNATURE','15b3076533123ff617801d89861136c8'); //MD5 signature of the db schema. (used to trigger upgrades) + define('SCHEMA_SIGNATURE','dd0022fb14892c0bb6a9700392df2de7'); //MD5 signature of the db schema. (used to trigger upgrades) #load config info $configfile=''; if(file_exists(ROOT_DIR.'ostconfig.php')) //Old installs prior to v 1.6 RC5 @@ -131,6 +131,7 @@ define('SYSLOG_TABLE',TABLE_PREFIX.'syslog'); define('SESSION_TABLE',TABLE_PREFIX.'session'); define('FILE_TABLE',TABLE_PREFIX.'file'); + define('FILE_CHUNK_TABLE',TABLE_PREFIX.'file_chunk'); define('STAFF_TABLE',TABLE_PREFIX.'staff'); define('DEPT_TABLE',TABLE_PREFIX.'department'); diff --git a/setup/inc/sql/osTicket-mysql.sql b/setup/inc/sql/osTicket-mysql.sql index 6ac95bb523a74be12a537d09a5983e550e49e473..53e3c551999567641bd33c71b7056f45de546c6c 100644 --- a/setup/inc/sql/osTicket-mysql.sql +++ b/setup/inc/sql/osTicket-mysql.sql @@ -327,15 +327,24 @@ CREATE TABLE `%TABLE_PREFIX%file` ( `size` varchar(25) NOT NULL default '', `hash` varchar(125) NOT NULL, `name` varchar(255) NOT NULL default '', - `filedata` longblob NOT NULL, `created` datetime NOT NULL, PRIMARY KEY (`id`), KEY `hash` (`hash`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; -INSERT INTO `%TABLE_PREFIX%file` (`id`, `type`, `size`, `hash`, `name`, `filedata`, `created`) VALUES -(1, 'text/plain', '25', '670c6cc1d1dfc97fad20e5470251b255', 'osTicket.txt', 0x43616e6e6564206174746163686d656e747320726f636b210a, NOW()); +INSERT INTO `%TABLE_PREFIX%file` (`id`, `type`, `size`, `hash`, `name`, `created`) VALUES +(1, 'text/plain', '25', '670c6cc1d1dfc97fad20e5470251b255', 'osTicket.txt', NOW()); +DROP TABLE IF EXISTS `%TABLE_PREFIX%file_chunk`; +CREATE TABLE `%TABLE_PREFIX%file_chunk` ( + `file_id` int(11) NOT NULL, + `chunk_id` int(11) NOT NULL, + `filedata` longblob NOT NULL, + PRIMARY KEY (`file_id`, `chunk_id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +INSERT INTO `%TABLE_PREFIX%file_chunk` (`file_id`, `chunk_id`, `filedata`) +VALUES (1, 0, 0x43616e6e6564206174746163686d656e747320726f636b210a); DROP TABLE IF EXISTS `%TABLE_PREFIX%groups`; CREATE TABLE `%TABLE_PREFIX%groups` ( diff --git a/setup/inc/sql/osTicket-mysql.sql.md5 b/setup/inc/sql/osTicket-mysql.sql.md5 index 11c872a50b9a27b95388705d17ad0d7a43566ad1..e96b33654d6d528ac92f4613636491d6ffee3e0a 100644 --- a/setup/inc/sql/osTicket-mysql.sql.md5 +++ b/setup/inc/sql/osTicket-mysql.sql.md5 @@ -1 +1 @@ -15b3076533123ff617801d89861136c8 +dd0022fb14892c0bb6a9700392df2de7