diff --git a/appinfo/info.xml b/appinfo/info.xml
index 6aba016522..9ae36f4e63 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -52,4 +52,9 @@
5
+
+
+ OCA\Calendar\Command\Export
+ OCA\Calendar\Command\Import
+
diff --git a/lib/Command/Export.php b/lib/Command/Export.php
new file mode 100644
index 0000000000..9d18f8316a
--- /dev/null
+++ b/lib/Command/Export.php
@@ -0,0 +1,82 @@
+
+ *
+ * @author Christopher Ng
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * 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 .
+ *
+ */
+
+namespace OCA\Calendar\Command;
+
+use OC\Core\Command\Base;
+use OCA\Calendar\UserMigration\CalendarMigrator;
+use OCP\IUser;
+use OCP\IUserManager;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class Export extends Base {
+
+ /** @var IUserManager */
+ private $userManager;
+
+ /** @var CalendarMigrator */
+ private $calendarMigrator;
+
+ public function __construct(
+ IUserManager $userManager,
+ CalendarMigrator $calendarMigrator
+ ) {
+ parent::__construct();
+ $this->userManager = $userManager;
+ $this->calendarMigrator = $calendarMigrator;
+ }
+
+ protected function configure() {
+ $this
+ ->setName('calendar:export')
+ ->setDescription('Export the calendars of a user')
+ ->addArgument(
+ 'user',
+ InputArgument::REQUIRED,
+ 'User to export',
+ );
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $user = $this->userManager->get($input->getArgument('user'));
+
+ if (!$user instanceof IUser) {
+ $output->writeln('User ' . $input->getArgument('user') . ' does not exist');
+ return 1;
+ }
+
+ try {
+ $this->calendarMigrator->export($user, $output);
+ } catch (\Exception $e) {
+ $output->writeln('' . $e->getMessage() . '');
+ return $e->getCode() !== 0 ? $e->getCode() : 1;
+ }
+
+ return 0;
+ }
+}
diff --git a/lib/Command/Import.php b/lib/Command/Import.php
new file mode 100644
index 0000000000..d11753acd2
--- /dev/null
+++ b/lib/Command/Import.php
@@ -0,0 +1,93 @@
+
+ *
+ * @author Christopher Ng
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * 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 .
+ *
+ */
+
+namespace OCA\Calendar\Command;
+
+use OC\Core\Command\Base;
+use OCA\Calendar\UserMigration\CalendarMigrator;
+use OCP\IUser;
+use OCP\IUserManager;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class Import extends Base {
+
+ /** @var IUserManager */
+ private $userManager;
+
+ /** @var CalendarMigrator */
+ private $calendarMigrator;
+
+ public function __construct(
+ IUserManager $userManager,
+ CalendarMigrator $calendarMigrator
+ ) {
+ parent::__construct();
+ $this->userManager = $userManager;
+ $this->calendarMigrator = $calendarMigrator;
+ }
+
+ protected function configure() {
+ $this
+ ->setName('calendar:import')
+ ->setDescription('Import a calendar to a user\'s account')
+ ->addArgument(
+ 'user',
+ InputArgument::REQUIRED,
+ 'User to import the calendar for',
+ )
+ ->addArgument(
+ 'path',
+ InputArgument::REQUIRED,
+ 'Path to the *.ics file',
+ );
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $user = $this->userManager->get($input->getArgument('user'));
+
+ [
+ 'basename' => $filename,
+ 'dirname' => $srcDir,
+ ] = pathinfo($input->getArgument('path'));
+
+
+ if (!$user instanceof IUser) {
+ $output->writeln('User ' . $input->getArgument('user') . ' does not exist');
+ return 1;
+ }
+
+ try {
+ $this->calendarMigrator->import($user, $srcDir, $filename, $output);
+ } catch (\Exception $e) {
+ $output->writeln('' . $e->getMessage() . '');
+ return $e->getCode() !== 0 ? $e->getCode() : 1;
+ }
+
+ return 0;
+ }
+}
diff --git a/lib/UserMigration/CalendarMigrator.php b/lib/UserMigration/CalendarMigrator.php
new file mode 100644
index 0000000000..3e59b1cd4f
--- /dev/null
+++ b/lib/UserMigration/CalendarMigrator.php
@@ -0,0 +1,227 @@
+
+ *
+ * @author Christopher Ng
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * 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 .
+ *
+ */
+
+namespace OCA\Calendar\UserMigration;
+
+use Exception;
+use OC\Files\Filesystem;
+use OC\Files\View;
+use OCA\Calendar\AppInfo\Application;
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\CalDAV\ICSExportPlugin\ICSExportPlugin;
+use OCA\DAV\CalDAV\Plugin as CalDAVPlugin;
+use OCA\DAV\Connector\Sabre\CachingTree;
+use OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin;
+use OCA\DAV\Connector\Sabre\Server as SabreDavServer;
+use OCA\DAV\RootCollection;
+use OCP\Calendar\ICalendar;
+use OCP\Calendar\IManager as ICalendarManager;
+use OCP\IConfig;
+use OCP\IL10N;
+use OCP\IUser;
+use Psr\Log\LoggerInterface;
+use Sabre\CalDAV\Plugin;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class CalendarMigrator {
+
+ /** @var ICalendarManager */
+ private $calendarManager;
+
+ /** @var SabreDavServer */
+ private $sabreDavServer;
+
+ /** @var CalDavBackend */
+ private $calDavBackend;
+
+ // ICSExportPlugin is injected to use the mergeObjects() method and is not to be used as a SabreDAV server plugin
+ /** @var ICSExportPlugin */
+ private $icsExportPlugin;
+
+ /** @var IConfig */
+ private $config;
+
+ /** @var LoggerInterface */
+ private $logger;
+
+ /** @var IL10N */
+ private $l10n;
+
+ /** @var string */
+ public const USERS_URI_ROOT = 'principals/users/';
+
+ /** @var string */
+ public const FILENAME_EXT = '.ics';
+
+ public function __construct(
+ ICalendarManager $calendarManager,
+ CalDavBackend $calDavBackend,
+ ICSExportPlugin $icsExportPlugin,
+ IConfig $config,
+ LoggerInterface $logger,
+ IL10N $l10n
+ ) {
+ $this->calendarManager = $calendarManager;
+ $this->calDavBackend = $calDavBackend;
+ $this->icsExportPlugin = $icsExportPlugin;
+ $this->config = $config;
+ $this->logger = $logger;
+ $this->l10n = $l10n;
+
+ $root = new RootCollection();
+ $this->sabreDavServer = new SabreDavServer(new CachingTree($root));
+ $this->sabreDavServer->addPlugin(new CalDAVPlugin());
+ $this->sabreDavServer->addPlugin(new ExceptionLoggerPlugin(Application::APP_ID, \OC::$server->getLogger()));
+ }
+
+ // TODO exract logic into this helper function
+ protected function getCalendarExports(): array {
+ return ['name' => '', 'data' => ''];
+ }
+
+ // TODO drop all unnecessary code for testing purposes and integrate with user_migration app when API is ready
+ public function export(IUser $user, OutputInterface $output): void {
+ $userId = $user->getUID();
+ $principalUri = CalendarMigrator::USERS_URI_ROOT . $userId;
+
+ /** @var array $calendars */
+ $calendars = array_values(array_filter(array_map(
+ /** @return array{name: string, data: string} */
+ function (ICalendar $calendar) use ($userId, $output) {
+ $calendarId = $calendar->getKey();
+ $calendarInfo = $this->calDavBackend->getCalendarById($calendarId);
+
+ if (!empty($calendarInfo)) {
+ $uri = $calendarInfo['uri'];
+ $path = Plugin::CALENDAR_ROOT . "/$userId/$uri";
+
+ // NOTE implementation below based on \Sabre\CalDAV\ICSExportPlugin::httpGet()
+
+ $properties = $this->sabreDavServer->getProperties($path, [
+ '{DAV:}resourcetype',
+ '{DAV:}displayname',
+ '{http://sabredav.org/ns}sync-token',
+ '{DAV:}sync-token',
+ '{http://apple.com/ns/ical/}calendar-color',
+ ]);
+
+ // Filter out deleted calendars
+ if (!isset($properties['{DAV:}resourcetype']) || !$properties['{DAV:}resourcetype']->is('{' . Plugin::NS_CALDAV . '}calendar')) {
+ return;
+ }
+
+ if (!isset($properties['{http://nextcloud.com/ns}refresh-interval'])) {
+ $value = $this->config->getAppValue('dav', 'defaultRefreshIntervalExportedCalendars', ICSExportPlugin::DEFAULT_REFRESH_INTERVAL);
+ $properties['{http://nextcloud.com/ns}refresh-interval'] = $value;
+ }
+
+ // NOTE implementation below based on \Sabre\CalDAV\ICSExportPlugin::generateResponse()
+
+ $calDataProp = '{' . Plugin::NS_CALDAV . '}calendar-data';
+ $calendarNode = $this->sabreDavServer->tree->getNodeForPath($path);
+ $nodes = $this->sabreDavServer->getPropertiesForPath($path, [$calDataProp], 1);
+
+ $blobs = [];
+ foreach ($nodes as $node) {
+ if (isset($node[200][$calDataProp])) {
+ $blobs[$node['href']] = $node[200][$calDataProp];
+ }
+ }
+ unset($nodes);
+
+ $mergedCalendar = $this->icsExportPlugin->mergeObjects(
+ $properties,
+ $blobs,
+ );
+
+ return [
+ 'name' => $calendarNode->getName(),
+ 'data' => $mergedCalendar->serialize(),
+ ];
+ }
+
+ $output->writeln("Calendar $calendarId does not exist");
+ return 1;
+ },
+ $this->calendarManager->getCalendarsForPrincipal($principalUri),
+ )));
+
+ foreach ($calendars as ['name' => $name, 'data' => $data]) {
+ // Set filename to sanitized calendar name appended with the date
+ $filename = preg_replace('/[^a-zA-Z0-9-_ ]/um', '', $name) . '-' . date('Y-m-d') . CalendarMigrator::FILENAME_EXT;
+
+ $this->writeExport(
+ $user,
+ $data,
+ // TESTING directory does not automatically get created so just write to user directory, this will be put in a zip with all other data anyways
+ // "/$userId/export/$appId",
+ "/$userId",
+ $filename,
+ $output,
+ );
+ }
+ }
+
+ /**
+ * @param string $destDir relative path from the root of the user's calendar export folder
+ * @throws Exception
+ */
+ protected function writeExport(IUser $user, string $data, string $destDir, string $filename, OutputInterface $output): void {
+ $userId = $user->getUID();
+
+ // setup filesystem
+ // Requesting the user folder will set it up if the user hasn't logged in before
+ \OC::$server->getUserFolder($userId);
+ Filesystem::initMountPoints($userId);
+
+ $view = new View();
+
+ if ($view->file_put_contents("$destDir/$filename", $data) === false) {
+ throw new Exception('Could not export calendar');
+ }
+
+ $output->writeln("✅ Exported calendar of user $userId into $destDir/$filename");
+ }
+
+ /**
+ * @throws Exception
+ */
+ // TODO add import calendar functionality
+ public function import(IUser $user, string $srcDir, string $filename, OutputInterface $output): void {
+ $userId = $user->getUID();
+
+ $view = new View();
+
+ $output->writeln("Importing calendar of user $userId from $srcDir/$filename …");
+
+ $fileContents = $view->file_get_contents("$srcDir/$filename");
+ if ($fileContents === false) {
+ throw new Exception('Could not import calendar');
+ }
+
+ $output->writeln("✅ Imported calendar $srcDir/$filename to account of $userId");
+ }
+}