diff --git a/bin/resync.php b/bin/resync.php new file mode 100755 --- /dev/null +++ b/bin/resync.php @@ -0,0 +1,116 @@ +#!/usr/bin/php + | + | | + | This program is free software: you can redistribute it and/or modify | + | it under the terms of the GNU Affero General Public License as published | + | by the Free Software Foundation, either version 3 of the License, or | + | (at your option) any later version. | + | | + | This program is distributed in the hope that it will be useful, | + | but WITHOUT ANY WARRANTY; without even the implied warranty of | + | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | + | GNU Affero General Public License for more details. | + | | + | You should have received a copy of the GNU Affero General Public License | + | along with this program. If not, see | + +--------------------------------------------------------------------------+ + | Author: Aleksander Machniak | + +--------------------------------------------------------------------------+ +*/ + +define('RCUBE_INSTALL_PATH', realpath(dirname(__FILE__) . '/../') . '/'); +define('RCUBE_PLUGINS_DIR', RCUBE_INSTALL_PATH . 'lib/plugins/'); + +// Define include path +$include_path = RCUBE_INSTALL_PATH . 'lib' . PATH_SEPARATOR; +$include_path .= RCUBE_INSTALL_PATH . 'lib/ext' . PATH_SEPARATOR; +$include_path .= ini_get('include_path'); +set_include_path($include_path); + +// include composer autoloader (if available) +if (@file_exists(RCUBE_INSTALL_PATH . 'vendor/autoload.php')) { + require RCUBE_INSTALL_PATH . 'vendor/autoload.php'; +} + +// include global functions from Roundcube Framework +require_once 'Roundcube/bootstrap.php'; + +$opts = rcube_utils::get_opt([ + 'o' => 'owner', + 'f' => 'folder', + 'd' => 'deviceid', + 't' => 'devicetype', // e.g. WindowsOutlook15 or iPhone +]); + +$rcube = \rcube::get_instance(); +$db = $rcube->get_dbh(); + +if (empty($opts['owner'])) { + rcube::raise_error("Owner not specified (--owner).", false, true); +} +if (empty($opts['folder'])) { + rcube::raise_error("Folder name not specified (--folder).", false, true); +} + +$select = $db->query( + "SELECT `user_id` FROM `users` WHERE `username` = ? ORDER BY `user_id` DESC", + \strtolower($opts['owner']) +); + +if ($data = $db->fetch_assoc($select)) { + $userid = $data['user_id']; +} else { + rcube::raise_error("User not found in Roundcube database.", false, true); +} + +$devices = []; +if (!empty($opts['deviceid'])) { + $select = $db->query( + "SELECT `id` FROM `syncroton_device` WHERE `owner_id` = ? AND `deviceid` = ?", + $userid, + $opts['deviceid'] + ); + while ($record = $db->fetch_assoc($select)) { + $devices[] = $record['id']; + } +} elseif (!empty($opts['devicetype'])) { + $select = $db->query( + "SELECT `id` FROM `syncroton_device` WHERE `owner_id` = ? AND `devicetype` = ?", + $userid, + $opts['devicetype'] + ); + while ($record = $db->fetch_assoc($select)) { + $devices[] = $record['id']; + } +} else { + $select = $db->query("SELECT `id` FROM `syncroton_device` WHERE `owner_id` = ?", $userid); + while ($record = $db->fetch_assoc($select)) { + $devices[] = $record['id']; + } +} + +if (empty($devices)) { + rcube::raise_error("Device not found.", false, true); +} + +// TODO: Support not only top-level folders + +$select = $db->query( + "SELECT `id`, `displayname`, `folderid` FROM `syncroton_folder`" + . " WHERE `device_id` IN (" . $db->array2list($devices) . ")" + . " AND `parentid` = '0' AND `displayname` = " . $db->quote($opts['folder']) +); + +while ($record = $db->fetch_assoc($select)) { + if (!empty($opts['dry-run'])) { + print("[DRY-RUN] {$record['displayname']} ({$record['id']}:{$record['folderid']})\n"); + } else { + $db->query("UPDATE `syncroton_folder` SET `resync` = 1 WHERE id = ?", $record['id']); + print("{$record['displayname']} ({$record['id']}:{$record['folderid']})\n"); + } +} diff --git a/docs/SQL/mysql/2024101700.sql b/docs/SQL/mysql/2024101700.sql new file mode 100644 --- /dev/null +++ b/docs/SQL/mysql/2024101700.sql @@ -0,0 +1 @@ +ALTER TABLE `syncroton_folder` ADD `resync` tinyint(1) DEFAULT NULL; diff --git a/lib/ext/Syncroton/Command/FolderSync.php b/lib/ext/Syncroton/Command/FolderSync.php --- a/lib/ext/Syncroton/Command/FolderSync.php +++ b/lib/ext/Syncroton/Command/FolderSync.php @@ -234,6 +234,12 @@ } } + // Handle folders set for forced re-sync, we'll send a delete action to the client, + // but because the folder is still existing and subscribed on the backend it should + // "immediately" be added again (and re-synced). + $forceDeleteIds = array_keys(array_filter($clientFolders, function ($f) { return !empty($f->resync); })); + $serverFoldersIds = array_diff($serverFoldersIds, $forceDeleteIds); + // calculate deleted entries $serverDiff = array_diff($clientFoldersIds, $serverFoldersIds); foreach ($serverDiff as $serverFolderId) { diff --git a/lib/ext/Syncroton/Model/Folder.php b/lib/ext/Syncroton/Model/Folder.php --- a/lib/ext/Syncroton/Model/Folder.php +++ b/lib/ext/Syncroton/Model/Folder.php @@ -34,6 +34,7 @@ 'class' => ['type' => 'string'], 'creationTime' => ['type' => 'datetime'], 'lastfiltertype' => ['type' => 'number'], + 'resync' => ['type' => 'number'], ], ]; } diff --git a/lib/kolab_sync_backend_folder.php b/lib/kolab_sync_backend_folder.php --- a/lib/kolab_sync_backend_folder.php +++ b/lib/kolab_sync_backend_folder.php @@ -88,7 +88,7 @@ $select = $this->db->query('SELECT * FROM `' . $this->table_name . '` WHERE ' . implode(' AND ', $where)); $folder = $this->db->fetch_assoc($select); - if (empty($folder)) { + if (empty($folder) || !empty($folder['resync'])) { throw new Syncroton_Exception_NotFound('Folder not found'); } @@ -117,6 +117,18 @@ Syncroton_Data_Factory::CLASS_TASKS, ]; + // Retrieve all folders already sent to the client + $select = $this->db->query("SELECT * FROM `{$this->table_name}` WHERE `device_id` = ?", $device->id); + + while ($folder = $this->db->fetch_assoc($select)) { + if (!empty($folder['resync'])) { + // Folder re-sync requested + return true; + } + + $client_folders[$folder['folderid']] = $this->get_object($folder); + } + // Reset imap cache so we work with up-to-date folders list rcube::get_instance()->get_storage()->clear_cache('mailboxes', true); @@ -132,13 +144,6 @@ } } - // retrieve all folders sent to the client - $select = $this->db->query("SELECT * FROM `{$this->table_name}` WHERE `device_id` = ?", $device->id); - - while ($folder = $this->db->fetch_assoc($select)) { - $client_folders[$folder['folderid']] = $this->get_object($folder); - } - ksort($client_folders); ksort($server_folders);