Index: kernel/constants.php =================================================================== --- kernel/constants.php (revision 14858) +++ kernel/constants.php (working copy) @@ -183,4 +183,13 @@ class PromoBlockType { const INTERNAL = 1; const EXTERNAL = 2; + } + + class StorageEngine { + const HASH = 1; + const TIMESTAMP = 2; + + const PS_DATE_TIME = 'DATE-TIME'; + const PS_PREFIX = 'PREFIX'; + const PS_USER = 'USER'; } \ No newline at end of file Index: kernel/globals.php =================================================================== --- kernel/globals.php (revision 14858) +++ kernel/globals.php (working copy) @@ -660,6 +660,30 @@ return $default; } + + /** + * Generate subpath from hashed value + * + * @param string $name + * @param int $levels + * @return string + */ + public static function getHashPathForLevel($name, $levels = 2) + { + if ( $levels == 0 ) { + return ''; + } + else { + $path = ''; + $hash = md5($name); + + for ($i = 0; $i < $levels; $i++) { + $path .= substr($hash, $i, 1) . '/'; + } + + return $path; + } + } } /** Index: kernel/utility/formatters/upload_formatter.php =================================================================== --- kernel/utility/formatters/upload_formatter.php (revision 14858) +++ kernel/utility/formatters/upload_formatter.php (working copy) @@ -56,8 +56,6 @@ $this->FullPath = FULL_PATH.$this->DestinationPath; } - $this->fileHelper->CheckFolder($this->FullPath); - // SWF Uploader if (is_array($value) && isset($value['tmp_ids'])) { if ($value['tmp_deleted']) { @@ -100,19 +98,21 @@ } } - for ($i=0; $igetStorageEngineFile($swf_uploaded_names[$i], $options, $object->Prefix); + $real_name = $this->getStorageEngineFolder($real_name, $options) . $real_name; + $real_name = $this->fileHelper->ensureUniqueFilename($this->FullPath, $real_name, $files2delete); - $file_name = $this->FullPath.$real_name; + $file_name = $this->FullPath . $real_name; - $tmp_file = WRITEABLE . '/tmp/' . $swf_uploaded_ids[$i].'_'.$swf_uploaded_names[$i]; + $tmp_file = WRITEABLE . '/tmp/' . $swf_uploaded_ids[$i] . '_' . $swf_uploaded_names[$i]; rename($tmp_file, $file_name); @chmod($file_name, 0666); - $fret[] = getArrayValue($options, 'upload_dir') ? $real_name : $this->DestinationPath.$real_name; + $fret[] = getArrayValue($options, 'upload_dir') ? $real_name : $this->DestinationPath . $real_name; } - $fret = array_merge($existing, $fret); - return implode('|', $fret); + + return implode('|', array_merge($existing, $fret)); } // SWF Uploader END @@ -154,9 +154,12 @@ $object->SetError($field_name, 'cant_save_file', 'la_error_cant_save_file'); } else { - $real_name = $this->fileHelper->ensureUniqueFilename($this->FullPath, $value['name']); + $real_name = $this->getStorageEngineFile($value['name'], $options, $object->Prefix); + $real_name = $this->getStorageEngineFolder($real_name, $options) . $real_name; + $real_name = $this->fileHelper->ensureUniqueFilename($this->FullPath, $real_name); $file_name = $this->FullPath . $real_name; + if ( !move_uploaded_file($value['tmp_name'], $file_name) ) { $object->SetError($field_name, 'cant_save_file', 'la_error_cant_save_file'); } @@ -348,6 +351,81 @@ return sprintf($format, $value); } + + /** + * Creates & returns folder, based on storage engine specified in field options + * + * @param string $file_name + * @param array $options + * @return string + * @access protected + */ + protected function getStorageEngineFolder($file_name, $options) + { + $storage_engine = (string)getArrayValue($options, 'storage_engine'); + + if ( !$storage_engine ) { + return ''; + } + + switch ($storage_engine) { + case StorageEngine::HASH: + $folder_path = kUtil::getHashPathForLevel($file_name); + break; + + case StorageEngine::TIMESTAMP: + $folder_path = adodb_date('Y-m/d/'); + break; + + default: + throw new Exception('Unknown storage engine "' . $storage_engine . '".'); + break; + } + + return $folder_path; + } + + /** + * Applies prefix & suffix to uploaded filename, based on storage engine in field options + * + * @param string $name + * @param array $options + * @param string $unit_prefix + * @return string + * @access protected + */ + protected function getStorageEngineFile($name, $options, $unit_prefix) + { + $prefix = $this->getStorageEngineFilePart(getArrayValue($options, 'filename_prefix'), $unit_prefix); + $suffix = $this->getStorageEngineFilePart(getArrayValue($options, 'filename_suffix'), $unit_prefix); + + $parts = pathinfo($name); + + return ($prefix ? $prefix . '_' : '') . $parts['filename'] . ($suffix ? '_' . $suffix : '') . '.' . $parts['extension']; + } + + /** + * Creates prefix/suffix to join with uploaded file + * + * Added "u" before user_id to keep this value after FileHelper::ensureUniqueFilename method call + * + * @param string $option + * @param string $unit_prefix + * @return string + * @access protected + */ + protected function getStorageEngineFilePart($option, $unit_prefix) + { + $replace_from = Array ( + StorageEngine::PS_DATE_TIME, StorageEngine::PS_PREFIX, StorageEngine::PS_USER + ); + + $replace_to = Array ( + adodb_date('Ymd-His'), $unit_prefix, 'u' . $this->Application->RecallVar('user_id') + ); + + return str_replace($replace_from, $replace_to, $option); + } } class kPictureFormatter extends kUploadFormatter Index: units/helpers/file_helper.php =================================================================== --- units/helpers/file_helper.php (revision 14858) +++ units/helpers/file_helper.php (working copy) @@ -117,9 +117,11 @@ public function PreserveItemFiles(&$field_values) { foreach ($field_values as $field_name => $field_value) { - if (!is_array($field_value)) continue; + if ( !is_array($field_value) ) { + continue; + } - if (isset($field_value['upload']) && ($field_value['error'] == UPLOAD_ERR_NO_FILE)) { + if ( isset($field_value['upload']) && ($field_value['error'] == UPLOAD_ERR_NO_FILE) ) { // this is upload field, but nothing was uploaded this time unset($field_values[$field_name]); } @@ -425,9 +427,17 @@ { $parts = pathinfo($name); $ext = '.' . $parts['extension']; - $filename = mb_substr($parts['basename'], 0, -mb_strlen($ext)); + $filename = $parts['filename']; $new_name = $filename . $ext; + if ( $parts['dirname'] != '.' ) { + $path = rtrim($path, '/') . '/' . ltrim($parts['dirname'], '/'); + } + + // make sure target folder always exists, especially for cases, + // when storage engine folder is supplied as a part of $name + $this->CheckFolder($path); + while ( file_exists($path . '/' . $new_name) || in_array(rtrim($path, '/') . '/' . $new_name, $forbidden_names) ) { if ( preg_match('/(' . preg_quote($filename, '/') . '_)([0-9]*)(' . preg_quote($ext, '/') . ')/', $new_name, $regs) ) { $new_name = $regs[1] . ($regs[2] + 1) . $regs[3]; @@ -437,6 +447,10 @@ } } + if ( $parts['dirname'] != '.' ) { + $new_name = $parts['dirname'] . '/' . $new_name; + } + return $new_name; } } \ No newline at end of file Index: units/images/image_event_handler.php =================================================================== --- units/images/image_event_handler.php (revision 14858) +++ units/images/image_event_handler.php (working copy) @@ -137,13 +137,13 @@ { $id = $event->getEventParam('id'); - $object =& $this->Application->recallObject($event->Prefix.'.-item', $event->Prefix, Array ('skip_autoload' => true)); + $object =& $this->Application->recallObject($event->Prefix . '.-item', $event->Prefix, Array ('skip_autoload' => true)); /* @var $object kDBItem */ - if (in_array($event->Name, Array('OnBeforeDeleteFromLive','OnAfterClone')) ) { + if ( in_array($event->Name, Array ('OnBeforeDeleteFromLive', 'OnAfterClone')) ) { $object->SwitchToLive(); } - elseif ($event->Name == 'OnBeforeItemDelete') { + elseif ( $event->Name == 'OnBeforeItemDelete' ) { // keep current table } else { @@ -152,78 +152,80 @@ $object->Load($id); - $fields = Array('LocalPath' => 'LocalImage', 'ThumbPath' => 'LocalThumb'); + $file_helper =& $this->Application->recallObject('FileHelper'); + /* @var $file_helper FileHelper */ + + $fields = Array ('LocalPath' => 'LocalImage', 'ThumbPath' => 'LocalThumb'); + foreach ($fields as $a_field => $mode_field) { - $file = $object->GetField($a_field); - if (!$file) continue; - $source_file = FULL_PATH.$file; + $file = $object->GetDBField($a_field); + if ( !$file ) { + continue; + } + + $source_file = FULL_PATH . $file; + switch ($event->Name) { // Copy image files to pending dir and update corresponding fields in temp record - // Checking for existing files and renaming if nessessary - two users may upload same pending files at the same time! + // Checking for existing files and renaming if necessary - two users may upload same pending files at the same time! case 'OnAfterCopyToTemp': - $new_file = IMAGES_PENDING_PATH . $this->ValidateFileName(FULL_PATH.IMAGES_PENDING_PATH, basename($file)); - $dest_file = FULL_PATH.$new_file; - copy($source_file, $dest_file); + $file = preg_replace('/^' . preg_quote(IMAGES_PATH, '/') . '/', IMAGES_PENDING_PATH, $file, 1); + $new_file = $file_helper->ensureUniqueFilename(FULL_PATH, $file); + + $dst_file = FULL_PATH . $new_file; + copy($source_file, $dst_file); + $object->SetFieldOption($a_field, 'skip_empty', false); $object->SetDBField($a_field, $new_file); break; - // Copy image files to live dir (checking if fileexists and renameing if nessessary) + // Copy image files to live dir (checking if file exists and renaming if necessary) // and update corresponding fields in temp record (which gets copied to live automatically) case 'OnBeforeCopyToLive': - if ( $object->GetDBField($mode_field) ) { // if image is local - // rename file if it exists in live folder - $new_file = IMAGES_PATH . $this->ValidateFileName(FULL_PATH.IMAGES_PATH, basename($file)); - $dest_file = FULL_PATH.$new_file; - rename($source_file, $dest_file); + if ( $object->GetDBField($mode_field) ) { + // if image is local -> rename file if it exists in live folder + $file = preg_replace('/^' . preg_quote(IMAGES_PENDING_PATH, '/') . '/', IMAGES_PATH, $file, 1); + $new_file = $file_helper->ensureUniqueFilename(FULL_PATH, $file); + + $dst_file = FULL_PATH . $new_file; + rename($source_file, $dst_file); } - else { // if image is remote url - remove local file (if any), update local file field with empty value - if (file_exists($source_file)) @unlink($source_file); + else { + // if image is remote url - remove local file (if any), update local file field with empty value + if ( file_exists($source_file) ) { + @unlink($source_file); + } + $new_file = ''; } + $object->SetFieldOption($a_field, 'skip_empty', false); $object->SetDBField($a_field, $new_file); break; case 'OnBeforeDeleteFromLive': // Delete image files from live folder before copying over from temp - case 'OnBeforeItemDelete': // Delete image files when deleteing Image object - @unlink(FULL_PATH.$file); + case 'OnBeforeItemDelete': // Delete image files when deleting Image object + @unlink(FULL_PATH . $file); break; - case 'OnAfterClone': // Copy files when cloning objects, renaming it on the fly - $path_info = pathinfo($file); - $new_file = $path_info['dirname'].'/'.$this->ValidateFileName(FULL_PATH.$path_info['dirname'], $path_info['basename']); - $dest_file = FULL_PATH . $new_file; - copy($source_file, $dest_file); + case 'OnAfterClone': + // Copy files when cloning objects, renaming it on the fly + $new_file = $file_helper->ensureUniqueFilename(FULL_PATH, $file); + $dst_file = FULL_PATH . $new_file; + copy($source_file, $dst_file); + $object->SetFieldOption($a_field, 'skip_empty', false); $object->SetDBField($a_field, $new_file); break; } } - if ( in_array($event->Name, Array('OnAfterClone', 'OnBeforeCopyToLive', 'OnAfterCopyToTemp')) ) { + + if ( in_array($event->Name, Array ('OnAfterClone', 'OnBeforeCopyToLive', 'OnAfterCopyToTemp')) ) { $object->Update(null, true); } } - function ValidateFileName($path, $name) - { - $parts = pathinfo($name); - $ext = '.'.$parts['extension']; - $filename = mb_substr($parts['basename'], 0, -mb_strlen($ext)); - $new_name = $filename.$ext; - while ( file_exists($path.'/'.$new_name) ) - { - if ( preg_match('/('.preg_quote($filename, '/').'_)([0-9]*)('.preg_quote($ext, '/').')/', $new_name, $regs) ) { - $new_name = $regs[1].($regs[2]+1).$regs[3]; - } - else { - $new_name = $filename.'_1'.$ext; - } - } - return $new_name; - } - /** * Sets primary image of user/category/category item *