diff --git a/include/class.orm.php b/include/class.orm.php index 12bc89072444fbbcb44032f2afb5818d9fe3619a..53ea936477c95464b25e66c737cf8222ab09af2d 100644 --- a/include/class.orm.php +++ b/include/class.orm.php @@ -206,9 +206,9 @@ class VerySimpleModel { foreach ($this->dirty as $field=>$old) { if ($this->__new__ or !in_array($field, $pk)) { if (@get_class($this->get($field)) == 'SqlFunction') - $fields[] = $field.' = '.$this->get($field)->toSql(); + $fields[] = "`$field` = ".$this->get($field)->toSql(); else - $fields[] = $field.' = '.db_input($this->get($field)); + $fields[] = "`$field` = ".db_input($this->get($field)); } } $sql .= ' SET '.implode(', ', $fields); diff --git a/setup/cli/modules/file.php b/setup/cli/modules/file.php index ee83169958c3e25a9c514e979564b7f6b699d588..e52a5ef1c9d0b4ecce5e66e1eaa1d4d2fbc36b0c 100644 --- a/setup/cli/modules/file.php +++ b/setup/cli/modules/file.php @@ -176,37 +176,197 @@ class FileManager extends Module { $this->stdout->write("Migrated $count files\n"); break; + /** + * export + * + * Export file contents to a stream file. The format of the stream + * will be a continuous stream of file information in the following + * format: + * + * FILE<header-length><data-length><header><data>EOF\x1c + * + * Where + * "FILE" is the literal text 'FILE' + * header-length is 'V' packed header length (bytes) + * data-length is 'V' packed data length (bytes) + * header is the file record, php serialized + * data is the raw content of the file + * "EOF" is the literal text 'EOF' + * \x1c is an ASCII 0x1c byte (file separator) + * + * Options: + * --file File to which to direct the stream output, default + * is stdout + */ case 'export': - // Create a temporary ZIP file $files = FileModel::objects(); $this->_applyCriteria($options, $files); - if (!$options['file']) - $this->fail('Please specify zip file with `-f`'); + if (!$options['file'] || $options['file'] == '-') + $options['file'] = 'php://stdout'; - $zip = new ZipArchive(); - if (true !== ($reason = $zip->open($options['file'], - ZipArchive::CREATE))) - $this->fail($reason.': Unable to create zip file'); + if (!($stream = fopen($options['file'], 'wb'))) + $this->fail($options['file'].': Unable to open file for export stream'); - $manifest = array(); foreach ($files as $m) { $f = AttachmentFile::lookup($m->id); - $zip->addFromString($f->getId(), $f->getData()); - $zip->setCommentName($f->getId(), $f->getName()); // TODO: Log %attachment and %ticket_attachment entries $info = array('file' => $f->getInfo()); foreach ($m->tickets as $t) $info['tickets'][] = $t->ht; + $header = serialize($info); + fwrite($stream, 'FILE'.pack('VV', strlen($header), $f->getSize())); + fwrite($stream, $header); + $FS = $f->open(); + while ($block = $FS->read()) + fwrite($stream, $block); + fwrite($stream, "EOF\x1c"); + } + fclose($stream); + break; + + /** + * import + * + * Import a collection of file contents exported by the `export`. + * See the export function above for details about the stream + * format. + * + * Options: + * --file File to which to direct the stream output, default + * is stdin + * --to Backend to receive the contents (@see `backends`) + * --verbose Show file names while importing + */ + case 'import': + if (!$options['file'] || $options['file'] == '-') + $options['file'] = 'php://stdin'; + + if (!($stream = fopen($options['file'], 'wb'))) + $this->fail($options['file'].': Unable to open file for export stream'); + + while (true) { + // Read the file header + $header = fread($stream, 12); + if (!$header) + // EOF + break; + list(, $mark, $hlen, $dlen) = unpack('V3', $header); + + // FILE written as little-endian 4-byte int is 0x454c4946 (ELIF) + if ($mark != 0x454c4946) + $this->fail('Bad file record'); + + // Read the header + $header = fread($stream, $hlen); + if (strlen($header) != $hlen) + $this->fail('Short read getting header info'); + + $header = unserialize($header); + if (!$header) + $this->fail('Unable to decipher file header'); + + // Find or create the file record + $finfo = $header['file']; + $f = AttachmentFile::lookup($finfo['id']); + if ($f) { + // Verify file information + if ($f->getSize() != $finfo['size'] + || $f->getSignature() != $finfo['signature'] + ) { + $this->fail(sprintf( + '%s: File data does not match existing file record', + $finfo['name'] + )); + } + // Drop existing file contents, if any + try { + if ($bk = $f->open()) + $bk->unlink(); + } + catch (Exception $e) {} + } + // Create a new file + else { + $fm = FileModel::create($finfo); + if (!$fm->save() || !($f = AttachmentFile::lookup($fm->id))) { + $this->fail(sprintf( + '%s: Unable to create new file record', + $finfo['name'])); + } + } + + // Determine the backend to recieve the file contents + if ($options['to']) { + $bk = FileStorageBackend::lookup($options['to'], $f); + } + // Use the system default + else { + $bk = AttachmentFile::getBackendForFile($f); + } + + if ($options['verbose']) + $this->stdout->write('Importing '.$f->getName()."\n"); + + // Write file contents to the backend + $md5 = hash_init('md5'); + $sha1 = hash_init('sha1'); + while ($dlen > 0) { + $read_size = min($dlen, $bk->getBlockSize()); + $contents = ''; + // reading from the stream will likely return an amount + // of data different from the backend requested block + // size. Loop until $read_size bytes are recieved. + while ($read_size > 0 && ($block = fread($stream, $read_size))) { + $contents .= $block; + $read_size -= strlen($block); + } + if ($read_size != 0) { + // short read + $this->fail(sprintf( + '%s: Some contents are missing from the stream', + $f->getName() + )); + } + // Calculate MD5 and SHA1 hashes of the file to verify + // contents after successfully written to backend + if (!$bk->write($contents)) + $this->fail('Unable to send file contents to backend'); + hash_update($md5, $contents); + hash_update($sha1, $contents); + $dlen -= strlen($contents); + } + if (!$bk->flush()) + $this->fail('Unable to commit file contents to backend'); + + // Check the signature hash + if ($finfo['signature']) { + $md5 = base64_encode(hash_final($md5, true)); + $sha1 = base64_encode(hash_final($sha1, true)); + $sig = str_replace( + array('=','+','/'), + array('','-','_'), + substr($sha1, 0, 16) . substr($md5, 0, 16)); + if ($sig != $finfo['signature']) { + $this->fail(sprintf( + '%s: Signature verification failed', + $f->getName() + )); + } + } - $manifest[$f->getId()] = $info; + // Read file record footer + $footer = fread($stream, 4); + if (strlen($footer) != 4) + $this->fail('Unable to read file EOF marker'); + list(,$footer) = unpack('N', $footer); + // Footer should be EOF\x1c as an int + if ($footer != 0x454f461c) + $this->fail('Incorrect file EOF marker'); } - $zip->addFromString('MANIFEST', serialize($manifest)); - $zip->close(); break; case 'expunge': - // Create a temporary ZIP file $files = FileModel::objects(); $this->_applyCriteria($options, $files);