diff --git a/phpBB/adm/style/acp_storage.html b/phpBB/adm/style/acp_storage.html index 49535c1a74..999d5622ea 100644 --- a/phpBB/adm/style/acp_storage.html +++ b/phpBB/adm/style/acp_storage.html @@ -4,59 +4,83 @@

{{ lang('STORAGE_TITLE') }}

-

{{ lang('STORAGE_TITLE_EXPLAIN') }}

+ + - - - - - - - - - - - {% for storage in STORAGE_STATS %} - - - - - - - {% endfor %} - -
{{ lang('STORAGE_NAME') }}{{ lang('STORAGE_NUM_FILES') }}{{ lang('STORAGE_SIZE') }}{{ lang('STORAGE_FREE') }}
{{ storage.name }}{{ storage.files }}{{ storage.size }}{{ storage.free_space }}
+

{L_CONTINUE_EXPLAIN}

-{% if S_ERROR %} +
+
+ {L_SUBMIT} +   + + {S_FORM_TOKEN} +
+
+ + +

{{ lang('STORAGE_TITLE_EXPLAIN') }}

+ + + + + + + + + + + + {% for storage in STORAGE_STATS %} + + + + + + + {% endfor %} + +
{{ lang('STORAGE_NAME') }}{{ lang('STORAGE_NUM_FILES') }}{{ lang('STORAGE_SIZE') }}{{ lang('STORAGE_FREE') }}
{{ storage.name }}{{ storage.files }}{{ storage.size }}{{ storage.free_space }}
+ + {% if S_ERROR %}

{{ lang('WARNING') }}

{{ ERROR_MSG }}

-{% endif %} + {% endif %} -
+ - {% for storage in STORAGES %} -
- {{ lang('STORAGE_' ~ storage.get_name | upper ~ '_TITLE') }} -
-

{{ lang('STORAGE_SELECT_DESC') }}
-
- + {% for provider in PROVIDERS if provider.is_available %} - {% endif %} - {% endfor %} - -
-
-
+ {% endfor %} + + + + - {% for provider in PROVIDERS %} - {% if provider.is_available %} + {% for provider in PROVIDERS if provider.is_available %}
{{ lang('STORAGE_' ~ storage.get_name | upper ~ '_TITLE') }} - {{ lang('STORAGE_ADAPTER_' ~ provider.get_name | upper ~ '_NAME') }} {% for name, options in provider.get_options %} @@ -93,25 +117,25 @@ {% endfor %}
- {% endif %} + {% endfor %} {% endfor %} - {% endfor %} -
-
-

{{ lang('STORAGE_REMOVE_OLD_FILES_EXPLAIN') }}
-
- -
-
-
+
+
+

{{ lang('STORAGE_REMOVE_OLD_FILES_EXPLAIN') }}
+
+ +
+
+
-
- {{ lang('SUBMIT') }} -   - - {{ S_FORM_TOKEN }} -
-
+
+ {{ lang('SUBMIT') }} +   + + {{ S_FORM_TOKEN }} +
+ + {% include 'overall_footer.html' %} diff --git a/phpBB/includes/acp/acp_storage.php b/phpBB/includes/acp/acp_storage.php index 50d18c251e..af7141dc92 100644 --- a/phpBB/includes/acp/acp_storage.php +++ b/phpBB/includes/acp/acp_storage.php @@ -24,11 +24,14 @@ class acp_storage /** @var \phpbb\config\config $config */ protected $config; + /** @var \phpbb\db_text $config_text */ + protected $config_text; + /** @var \db\driver\driver_interface $db */ protected $db; - /** @var \phpbb\language\language $lang */ - protected $lang; + /** @var \phpbb\path_helper $path_helper */ + protected $path_helper; /** @var \phpbb\request\request */ protected $request; @@ -60,6 +63,9 @@ class acp_storage /** @var string */ public $u_action; + /** @var mixed */ + protected $state; + /** * @param string $id * @param string $mode @@ -69,9 +75,10 @@ class acp_storage global $phpbb_container, $phpbb_dispatcher, $phpbb_root_path; $this->config = $phpbb_container->get('config'); + $this->config_text = $phpbb_container->get('config_text'); $this->db = $phpbb_container->get('dbal.conn'); $this->filesystem = $phpbb_container->get('filesystem'); - $this->lang = $phpbb_container->get('language'); + $this->path_helper = $phpbb_container->get('path_helper'); $this->request = $phpbb_container->get('request'); $this->template = $phpbb_container->get('template'); $this->user = $phpbb_container->get('user'); @@ -81,7 +88,7 @@ class acp_storage $this->phpbb_root_path = $phpbb_root_path; // Add necesary language files - $this->lang->add_lang(['acp/storage']); + $this->user->add_lang(['acp/storage']); /** * Add language strings @@ -112,14 +119,172 @@ class acp_storage // Set page title $this->page_title = 'STORAGE_TITLE'; + $action = $this->request->variable('action', ''); + $this->load_state(); + + // If user cancelled to continue, remove state + if ($this->request->is_set_post('cancel', false)) + { + if (!check_form_key($form_key) || !check_link_hash($this->request->variable('hash', ''), 'acp_storage')) + { + trigger_error($this->user->lang('FORM_INVALID') . adm_back_link($this->u_action), E_USER_WARNING); + } + + if ($this->request->variable('cancel', false)) + { + $action = ''; + $this->state = false; + $this->save_state(); + } + } + + if ($action) + { + switch ($action) + { + case 'progress_bar': + $this->display_progress_bar(); + break; + case 'update': + // Just continue + break; + default: + trigger_error('NO_ACTION', E_USER_ERROR); + break; + } + + if (!check_form_key($form_key) || !check_link_hash($this->request->variable('hash', ''), 'acp_storage')) + { + trigger_error($this->user->lang('FORM_INVALID') . adm_back_link($this->u_action), E_USER_WARNING); + } + + // TODO: If both providers are the same, and remove + // old files is checked, the files could be only moved + + // Copy files from the old to the new storage + $i = 0; + foreach ($this->state['storages'] as $storage_name => $storage_options) + { + // Skip storages that have already moved files + if ($this->state['storage_index'] > $i++) + { + continue; + } + + $current_adapter = $this->get_current_adapter($storage_name); + $new_adapter = $this->get_new_adapter($storage_name); + + $sql = 'SELECT file_id, file_path + FROM ' . STORAGE_TABLE . " + WHERE storage = '" . $this->db->sql_escape($storage_name) . "' + AND file_id > " . $this->state['file_index']; + $result = $this->db->sql_query($sql); + + $starttime = microtime(true); + while ($row = $this->db->sql_fetchrow($result)) + { + if (!still_on_time()) + { + $this->save_state(); + meta_refresh(1, append_sid($this->u_action . '&action=update&hash=' . generate_link_hash('acp_storage'))); + trigger_error($this->user->lang('STORAGE_UPDATE_REDIRECT', $this->user->lang('STORAGE_' . strtoupper($storage_name) . '_TITLE'), $i + 1, count($this->state['storages']))); + } + + $stream = $current_adapter->read_stream($row['file_path']); + $new_adapter->write_stream($row['file_path'], $stream); + fclose($stream); + + $this->state['file_index'] = $row['file_id']; // Set last uploaded file + } + + // Copied all files of a storage, increase storage index and reset file index + $this->state['storage_index']++; + $this->state['file_index'] = 0; + } + + if ($this->state['remove_old']) + { + $i = 0; + foreach ($this->state['storages'] as $storage_name => $storage_options) + { + // Skip storages that have already moved files + if ($this->state['remove_storage_index'] > $i++) + { + continue; + } + + $current_adapter = $this->get_current_adapter($storage_name); + + $sql = 'SELECT file_id, file_path + FROM ' . STORAGE_TABLE . " + WHERE storage = '" . $this->db->sql_escape($storage_name) . "' + AND file_id > " . $this->state['file_index']; + $result = $this->db->sql_query($sql); + + $starttime = microtime(true); + while ($row = $this->db->sql_fetchrow($result)) + { + if (!still_on_time()) + { + $this->save_state(); + meta_refresh(1, append_sid($this->u_action . '&action=update&hash=' . generate_link_hash('acp_storage'))); + trigger_error($this->user->lang('STORAGE_UPDATE_REMOVE_REDIRECT', $this->user->lang('STORAGE_' . strtoupper($storage_name) . '_TITLE'), $i + 1, count($this->state['storages']))); + } + + $current_adapter->delete($row['file_path']); + + $this->state['file_index'] = $row['file_id']; // Set last uploaded file + } + + // Remove all files of a storage, increase storage index and reset file index + $this->state['remove_storage_index']++; + $this->state['file_index'] = 0; + } + } + + // Here all files have been copied/moved, so save new configuration + foreach (array_keys($this->state['storages']) as $storage_name) + { + $this->update_storage_config($storage_name); + } + + $this->state = false; + $this->save_state(); + + //$phpbb_log->add('admin', $this->user->data['user_id'], $this->user->ip, 'LOG_STORAGE_UPDATE', false); // todo + trigger_error($this->user->lang('STORAGE_UPDATE_SUCCESSFUL') . adm_back_link($this->u_action) . $this->close_popup_js()); + } + + // There is an updating in progress, show the form to continue or cancel + if ($this->state != false) + { + $this->template->assign_vars(array( + 'UA_PROGRESS_BAR' => addslashes(append_sid($this->path_helper->get_phpbb_root_path() . $this->path_helper->get_adm_relative_path() . "index." . $this->path_helper->get_php_ext(), "i=$id&mode=$mode&action=progress_bar")), + 'S_CONTINUE_UPDATING' => true, + 'U_CONTINUE_UPDATING' => $this->u_action . '&action=update&hash=' . generate_link_hash('acp_storage'), + 'L_CONTINUE' => $this->user->lang('CONTINUE_UPDATING'), + 'L_CONTINUE_EXPLAIN' => $this->user->lang('CONTINUE_UPDATING_EXPLAIN'), + )); + + return; + } + + // Process form and create a "state" for the update, + // then show a confirm form $messages = []; + if ($this->request->is_set_post('submit')) { + if (!check_form_key($form_key) || !check_link_hash($this->request->variable('hash', ''), 'acp_storage')) + { + trigger_error($this->user->lang('FORM_INVALID') . adm_back_link($this->u_action), E_USER_WARNING); + } + $modified_storages = []; if (!check_form_key($form_key)) { - $messages[] = $this->lang->lang('FORM_INVALID'); + $messages[] = $this->user->lang('FORM_INVALID'); } foreach ($this->storage_collection as $storage) @@ -133,7 +298,7 @@ class acp_storage $modified = false; // Check if provider have been modified - if ($this->get_new_provider($storage_name) != $this->get_current_provider($storage_name)) + if ($this->request->variable([$storage_name, 'provider'], '') != $this->get_current_provider($storage_name)) { $modified = true; } @@ -143,7 +308,7 @@ class acp_storage { foreach (array_keys($options) as $definition) { - if ($this->get_new_definition($storage_name, $definition) != $this->get_current_definition($storage_name, $definition)) + if ($this->request->variable([$storage_name, $definition], '') != $this->get_current_definition($storage_name, $definition)) { $modified = true; break; @@ -163,37 +328,41 @@ class acp_storage { if (empty($messages)) { + // Create state + $this->state = [ + // Save the value of the checkbox, to remove all files from the + // old storage once they have been successfully moved + 'remove_old' => $this->request->variable('remove_old', false), + 'storage_index' => 0, + 'file_index' => 0, + 'remove_storage_index' => 0, + ]; + + // Save in the state the selected storages and their configuration foreach ($modified_storages as $storage_name) { - $current_adapter = $this->get_current_adapter($storage_name); - $new_adapter = $this->get_new_adapter($storage_name); + $this->state['storages'][$storage_name]['provider'] = $this->request->variable([$storage_name, 'provider'], ''); - $sql = 'SELECT file_path - FROM ' . STORAGE_TABLE . " - WHERE storage = '" . $storage_name . "'"; - $result = $this->db->sql_query($sql); + $options = $this->get_provider_options($this->request->variable([$storage_name, 'provider'], '')); - while ($row = $this->db->sql_fetchrow($result)) + foreach (array_keys($options) as $definition) { - $stream = $current_adapter->read_stream($row['file_path']); - $new_adapter->write_stream($row['file_path'], $stream); - fclose($stream); + $this->state['storages'][$storage_name]['config'][$definition] = $this->request->variable([$storage_name, $definition], ''); } - - if ($this->request->variable('remove_old', false)) - { - $this->db->sql_rowseek(0, $result); - - while ($row = $this->db->sql_fetchrow($result)) - { - $current_adapter->delete($row['file_path']); - } - } - - $this->update_storage_config($storage_name); } - trigger_error($this->lang->lang('STORAGE_UPDATE_SUCCESSFUL') . adm_back_link($this->u_action), E_USER_NOTICE); + $this->save_state(); // A storage update is going to be done here + + // Show the confirmation form to start the process + $this->template->assign_vars(array( + 'UA_PROGRESS_BAR' => addslashes(append_sid($this->path_helper->get_phpbb_root_path() . $this->path_helper->get_adm_relative_path() . "index." . $this->path_helper->get_php_ext(), "i=$id&mode=$mode&action=progress_bar")), // same + 'S_CONTINUE_UPDATING' => true, + 'U_CONTINUE_UPDATING' => $this->u_action . '&action=update&hash=' . generate_link_hash('acp_storage'), + 'L_CONTINUE' => $this->user->lang('START_UPDATING'), + 'L_CONTINUE_EXPLAIN' => $this->user->lang('START_UPDATING_EXPLAIN'), + )); + + return; } else { @@ -202,9 +371,10 @@ class acp_storage } // If there is no changes - trigger_error($this->lang->lang('STORAGE_NO_CHANGES') . adm_back_link($this->u_action), E_USER_WARNING); + trigger_error($this->user->lang('STORAGE_NO_CHANGES') . adm_back_link($this->u_action), E_USER_WARNING); } + // Top table with stats of each storage $storage_stats = []; foreach ($this->storage_collection as $storage) { @@ -219,11 +389,11 @@ class acp_storage } catch (\phpbb\storage\exception\exception $e) { - $free_space = $this->lang->lang('STORAGE_UNKNOWN'); + $free_space = $this->user->lang('STORAGE_UNKNOWN'); } $storage_stats[] = [ - 'name' => $this->lang->lang('STORAGE_' . strtoupper($storage->get_name()) . '_TITLE'), + 'name' => $this->user->lang('STORAGE_' . strtoupper($storage->get_name()) . '_TITLE'), 'files' => $storage->get_num_files(), 'size' => get_formatted_filesize($storage->get_size()), 'free_space' => $free_space, @@ -231,15 +401,63 @@ class acp_storage } $this->template->assign_vars([ - 'STORAGES' => $this->storage_collection, - 'STORAGE_STATS' => $storage_stats, - 'PROVIDERS' => $this->provider_collection, + 'STORAGES' => $this->storage_collection, + 'STORAGE_STATS' => $storage_stats, + 'PROVIDERS' => $this->provider_collection, - 'ERROR_MSG' => implode('
', $messages), - 'S_ERROR' => !empty($messages), + 'ERROR_MSG' => implode('
', $messages), + 'S_ERROR' => !empty($messages), + + 'U_ACTION' => $this->u_action . '&hash=' . generate_link_hash('acp_storage'), ]); } + protected function display_progress_bar() + { + adm_page_header($this->user->lang('STORAGE_UPDATE_IN_PROGRESS')); + $this->template->set_filenames(array( + 'body' => 'progress_bar.html') + ); + $this->template->assign_vars(array( + 'L_PROGRESS' => $this->user->lang('STORAGE_UPDATE_IN_PROGRESS'), + 'L_PROGRESS_EXPLAIN' => $this->user->lang('STORAGE_UPDATE_IN_PROGRESS_EXPLAIN')) + ); + adm_page_footer(); + } + + function close_popup_js() + { + return "\n"; + } + + protected function save_state() + { + $state = $this->state; + + if ($state == false) + { + $state = []; + } + + $this->config_text->set('storage_update_state', json_encode($state)); + } + + protected function load_state() + { + $state = json_decode($this->config_text->get('storage_update_state'), true); + + if ($state == null || empty($state)) + { + $state = false; + } + + $this->state = $state; + } + /** * Get the current provider from config * @@ -251,17 +469,6 @@ class acp_storage return $this->config['storage\\' . $storage_name . '\\provider']; } - /** - * Get the new provider from the request - * - * @param string $storage_name Storage name - * @return string The new provider - */ - protected function get_new_provider($storage_name) - { - return $this->request->variable([$storage_name, 'provider'], ''); - } - /** * Get adapter definitions from a provider * @@ -285,18 +492,6 @@ class acp_storage return $this->config['storage\\' . $storage_name . '\\config\\' . $definition]; } - /** - * Get the new value of the definition of a storage from the request - * - * @param string $storage_name Storage name - * @param string $definition Definition - * @return string Definition value - */ - protected function get_new_definition($storage_name, $definition) - { - return $this->request->variable([$storage_name, $definition], ''); - } - /** * Validates data * @@ -305,56 +500,56 @@ class acp_storage */ protected function validate_data($storage_name, &$messages) { - $storage_title = $this->lang->lang('STORAGE_' . strtoupper($storage_name) . '_TITLE'); + $storage_title = $this->user->lang('STORAGE_' . strtoupper($storage_name) . '_TITLE'); // Check if provider exists try { - $new_provider = $this->provider_collection->get_by_class($this->get_new_provider($storage_name)); + $new_provider = $this->provider_collection->get_by_class($this->request->variable([$storage_name, 'provider'], '')); } catch (\Exception $e) { - $messages[] = $this->lang->lang('STORAGE_PROVIDER_NOT_EXISTS', $storage_title); + $messages[] = $this->user->lang('STORAGE_PROVIDER_NOT_EXISTS', $storage_title); return; } // Check if provider is available if (!$new_provider->is_available()) { - $messages[] = $this->lang->lang('STORAGE_PROVIDER_NOT_AVAILABLE', $storage_title); + $messages[] = $this->user->lang('STORAGE_PROVIDER_NOT_AVAILABLE', $storage_title); return; } // Check options - $new_options = $this->get_provider_options($this->get_new_provider($storage_name)); + $new_options = $this->get_provider_options($this->request->variable([$storage_name, 'provider'], '')); foreach ($new_options as $definition_key => $definition_value) { - $provider = $this->provider_collection->get_by_class($this->get_new_provider($storage_name)); - $definition_title = $this->lang->lang('STORAGE_ADAPTER_' . strtoupper($provider->get_name()) . '_OPTION_' . strtoupper($definition_key)); + $provider = $this->provider_collection->get_by_class($this->request->variable([$storage_name, 'provider'], '')); + $definition_title = $this->user->lang('STORAGE_ADAPTER_' . strtoupper($provider->get_name()) . '_OPTION_' . strtoupper($definition_key)); - $value = $this->get_new_definition($storage_name, $definition_key); + $value = $this->request->variable([$storage_name, $definition_key], ''); switch ($definition_value['type']) { case 'email': if (!filter_var($value, FILTER_VALIDATE_EMAIL)) { - $messages[] = $this->lang->lang('STORAGE_FORM_TYPE_EMAIL_INCORRECT_FORMAT', $definition_title, $storage_title); + $messages[] = $this->user->lang('STORAGE_FORM_TYPE_EMAIL_INCORRECT_FORMAT', $definition_title, $storage_title); } case 'text': case 'password': $maxlength = isset($definition_value['maxlength']) ? $definition_value['maxlength'] : 255; if (strlen($value) > $maxlength) { - $messages[] = $this->lang->lang('STORAGE_FORM_TYPE_TEXT_TOO_LONG', $definition_title, $storage_title); + $messages[] = $this->user->lang('STORAGE_FORM_TYPE_TEXT_TOO_LONG', $definition_title, $storage_title); } break; case 'radio': case 'select': if (!in_array($value, array_values($definition_value['options']))) { - $messages[] = $this->lang->lang('STORAGE_FORM_TYPE_SELECT_NOT_AVAILABLE', $definition_title, $storage_title); + $messages[] = $this->user->lang('STORAGE_FORM_TYPE_SELECT_NOT_AVAILABLE', $definition_title, $storage_title); } break; } @@ -377,14 +572,14 @@ class acp_storage } // Update provider - $this->config->set('storage\\' . $storage_name . '\\provider', $this->get_new_provider($storage_name)); + $this->config->set('storage\\' . $storage_name . '\\provider', $this->state['storages'][$storage_name]['provider']); // Set new storage config - $new_options = $this->get_provider_options($this->get_new_provider($storage_name)); + $new_options = $this->get_provider_options($this->state['storages'][$storage_name]['provider']); foreach (array_keys($new_options) as $definition) { - $this->config->set('storage\\' . $storage_name . '\\config\\' . $definition, $this->get_new_definition($storage_name, $definition)); + $this->config->set('storage\\' . $storage_name . '\\config\\' . $definition, $this->state['storages'][$storage_name]['config'][$definition]); } } @@ -434,7 +629,7 @@ class acp_storage protected function get_new_adapter($storage_name) { - $provider = $this->get_new_provider($storage_name); + $provider = $this->state['storages'][$storage_name]['provider']; $provider_class = $this->provider_collection->get_by_class($provider); $adapter = $this->adapter_collection->get_by_class($provider_class->get_adapter_class()); @@ -443,7 +638,7 @@ class acp_storage $options = []; foreach (array_keys($definitions) as $definition) { - $options[$definition] = $this->get_new_definition($storage_name, $definition); + $options[$definition] = $this->state['storages'][$storage_name]['config'][$definition]; } $adapter->configure($options); diff --git a/phpBB/language/en/acp/common.php b/phpBB/language/en/acp/common.php index bcbbe5d931..729888d509 100644 --- a/phpBB/language/en/acp/common.php +++ b/phpBB/language/en/acp/common.php @@ -768,6 +768,8 @@ $lang = array_merge($lang, array( 'LOG_STYLE_EDIT_DETAILS' => 'Edited style
» %s', 'LOG_STYLE_EXPORT' => 'Exported style
» %s', + 'LOG_STORAGE_UPDATE' => 'Storage updated', + 'LOG_UPDATE_DATABASE' => 'Updated Database from version %1$s to version %2$s', 'LOG_UPDATE_PHPBB' => 'Updated phpBB from version %1$s to version %2$s', diff --git a/phpBB/language/en/acp/storage.php b/phpBB/language/en/acp/storage.php index ae3c007c4a..7c921b46e2 100644 --- a/phpBB/language/en/acp/storage.php +++ b/phpBB/language/en/acp/storage.php @@ -50,6 +50,16 @@ $lang = array_merge($lang, array( 'STORAGE_UNKNOWN' => 'Unknown', 'STORAGE_REMOVE_OLD_FILES' => 'Remove old files', 'STORAGE_REMOVE_OLD_FILES_EXPLAIN' => 'Remove old files after they are copied to the new storage system.', + 'START_UPDATING' => 'Start update process', + 'START_UPDATING_EXPLAIN' => 'Start the storage update process', + 'CONTINUE_UPDATING' => 'Continue previous update process', + 'CONTINUE_UPDATING_EXPLAIN' => 'An update process has been started. In order to access the storage settings page you will have to complete it or cancel it.', + 'SORAGE_UPDATE_REDIRECT' => 'Files of %s (%d/%d) are being moved.
', + 'SORAGE_UPDATE_REMOVE_REDIRECT' => 'Files of old %s (%d/%d) are being removed.
', + + // Template progress bar + 'STORAGE_UPDATE_IN_PROGRESS' => 'Storage update in progress', + 'STORAGE_UPDATE_IN_PROGRESS_EXPLAIN' => 'Files are being moved between storages. This can take some minutes.', // Storage names 'STORAGE_ATTACHMENT_TITLE' => 'Attachments storage',