Skip to content

Conversation

@SebastianKrupinski
Copy link
Contributor

@SebastianKrupinski SebastianKrupinski commented Apr 4, 2025

Summary

This adds the ability to import calendars

Split of #49995

OCC Import

Command: calendar:import
Arguments:
uid - mandatory
cid - mandatory
format - optional defaults to ical (ical, xcal, jcal)
filepath - optional defaults to stdin
--supersede - optional (force override of existing)
--show-created - optional (show uid of created objects)
--show-updated - optional (show uid of updated objects)
--show-skipped - optional (show uid of skipped objects)
--show-errors - optional (show uid of objects with errors)
--errors (int) - optional - how to handle errors (0 - continue, 1 - fail)
--validation (int) - optional - how to handle item validation (0 - no validation, 1 - validate and skip on issue, 2 - validate and fail on issue)

Import from file:

calendar:import user1 personal ical /tmp/personal.ics

Import from pipe:

cat /tmp/personal.ics | calendar:import user1 personal ical

Import from file and override existing:

calendar:import user1 personal ical /tmp/personal.ics --supersede

Checklist

@SebastianKrupinski SebastianKrupinski added the 3. to review Waiting for reviews label Apr 4, 2025
@SebastianKrupinski SebastianKrupinski self-assigned this Apr 4, 2025
@SebastianKrupinski SebastianKrupinski requested review from a team and tcitworld as code owners April 4, 2025 00:41
@SebastianKrupinski SebastianKrupinski requested review from Altahrim, nfebe and yemkareems and removed request for a team April 4, 2025 00:41
@github-project-automation github-project-automation bot moved this to 🏗️ In progress in 💌 📅 👥 Groupware team Apr 4, 2025
@susnux susnux added enhancement feature: caldav Related to CalDAV internals labels Apr 4, 2025
@susnux susnux added this to the Nextcloud 32 milestone Apr 4, 2025
@Dexus
Copy link

Dexus commented Apr 30, 2025

A small note from me: You should not only be able to overwrite entries but also delete all existent events in the same calendar that are no longer part of the ICAL files (multievents files). Otherwise such an importer makes no sense, as entries are added but never deleted if they are no longer contained in the ICS/ICALs.

@SebastianKrupinski
Copy link
Contributor Author

Hi @Dexus

You mean like adding an option, --purge or --reset to empty the calendar before importing?

We can add that after this is merged, I don't want to delay this feature any more that it has been already, I will just need the team to approve it.

@ChristophWurst do you have any thoughts?

@Dexus
Copy link

Dexus commented Apr 30, 2025

You mean like adding an option, --purge or --reset to empty the calendar before importing?

Absolutely! In my case, it is necessary to delete outdated events that are not contained in the calendar files.

@ChristophWurst
Copy link
Member

The import/export feature was requested for Migration projects AFAIK, so the current capabilities are enough. Let's merge this.

The purge can be helpful for manual syncing. This can be done too 👍

@SebastianKrupinski SebastianKrupinski force-pushed the feat/issue-563-calendar-import branch from aa32cd8 to be42c1e Compare May 8, 2025 18:17
Copy link
Contributor Author

@SebastianKrupinski SebastianKrupinski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried to rebase merge this (hopefully it does no make you lose your review position) so that you can test it with the export command in place.

Also went through and made some of the changes you recommended in the export PR to limit the review changes

Comment on lines 96 to 98
if (!in_array($errors, CalendarImportOptions::ERROR_OPTIONS, true)) {
throw new InvalidArgumentException('Invalid errors option specified');
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!in_array($errors, CalendarImportOptions::ERROR_OPTIONS, true)) {
throw new InvalidArgumentException('Invalid errors option specified');
}

Validation is done in setErrors

Copy link
Contributor Author

@SebastianKrupinski SebastianKrupinski May 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest, I would prefer to remove the validation all together from here and the CalendarImportOptions. This would allow additional error options that are specific to each app verses forcing specific ones.

What do you think? Lets have a call to discuss the benefits.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned in our call, CalendarImportOptions.setErrors already validates the input. Whether you validate it in the command or in the options object doesn’t really matter, but please don’t do it in both places 😉 My preference would be to keep the validation in the options object.

Comment on lines 102 to 104
if (!in_array($validation, CalendarImportOptions::VALIDATE_OPTIONS, true)) {
throw new InvalidArgumentException('Invalid validation option specified');
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!in_array($validation, CalendarImportOptions::VALIDATE_OPTIONS, true)) {
throw new InvalidArgumentException('Invalid validation option specified');
}

Validation is done in setValidation

Comment on lines 108 to 110
if (!in_array($format, ImportService::FORMATS, true)) {
throw new InvalidArgumentException("Format <$format> is not valid.");
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to also move the validation to the options object like for errors and validate.

*
* @return Generator<\Sabre\VObject\Component\VCalendar>
*/
private function importText(CalendarImportOptions $options): Generator {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$options seems unused

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done on purpose, so that server code can be changed in the future with additional options but the function signature stays the same for any application that implements this.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As the ability to overwrite the import functionality is gone, there's also no need any longer to pass the unused options arguments. If we need it later, we can just add it again without breaking anything, as the implementation is now private.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay

*
* @return Generator<\Sabre\VObject\Component\VCalendar>
*/
private function importXml(CalendarImportOptions $options): Generator {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$options seems unused

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above comment

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay

*
* @return Generator<\Sabre\VObject\Component\VCalendar>
*/
private function importJson(CalendarImportOptions $options): Generator {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$options seems unused

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above comment

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay

*
* @return array<string,array<string,string|array<string>>>
*/
public function import(CalendarImportOptions $options, callable $generator): array {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not super happy with the import method on the calendar object. I know we did the same for export, my bad for not catching it earlier. The issue is that the ImportService and the import method in the calendar are quite tightly coupled. I think it would be nicer to inject the CalDavBackend into the ImportService and let the service handle the import directly, without going through CalendarImpl.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did this on purpose, so that any app that supports ICalendarImport can import data. If the service imports directly to the caldavbackend then a 3rd party apps can not use this code to import data, with the current design the DECK app for instance, can extend this own calendar object to import data, and save it, in anyway or format that it wants.

To be honest, now that I think about this, I think the outcome should also be passed in as a callback and the outcome result should be created in the service or command/controller layer, this would allow for streaming output in the command line or even api end point.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kesselb but the import service is not exposed, is it? Wouldn't that be necessary to make the import service usable as entry point?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The import service is not exposed.

Right now, the import method doesn't use any private methods or fields from the Calendar object that aren’t already exposed, so there’s no technical reason to implement import and export functionality directly in the Calendar object.

I understand the idea behind ICalendarImport, and that apps might be able to override it, but honestly, that’s a use case I just don’t see.

Take the Deck example: a deck card is much more than just a calendar event. I’d be really surprised if the Deck team started subclassing Calendar and implementing their own marshalling to wrap a deck card into an iCal object. Sure, that might technically be possible; but why would they do that? ;)

Same goes for the Tasks and Pools apps, they don’t even use a calendar provider and rely on our code for CalDAV. If something’s missing for them when importing/exporting, it needs to be fixed here.

So we’re introducing a more complex implementation to support extensibility that no one has asked for yet, and it’s unclear whether anyone ever will. That’s why my suggestion is: don’t expose the functionality, and remove the related exporter interface again before the 32 freeze.

Unless there’s a concrete use case, I’d rather avoid introducing complexity without a clear benefit.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, that makes sense. I agree that this extensibility is not on the horizon, so let's not prepare for it but cross that bridge when we get to it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also agree. I would prefer not having to maintain the public API for imports until we or some other app/team really needs it.

->addArgument('uid', InputArgument::REQUIRED, 'Id of system user')
->addArgument('cid', InputArgument::REQUIRED, 'Id of calendar')
->addOption('format', null, InputOption::VALUE_REQUIRED, 'Format of input (ical, jcal, xcal) defaults to ical', 'ical')
->addOption('location', null, InputOption::VALUE_REQUIRED, 'Location of where to write the input. defaults to stdin')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggested using an option for the location to keep it in line with the export command, but it feels a bit off here, since most people probably just want to save it to a file anyway.

Using an optional argument for the location makes more sense in this case, and I might update the export command later to match. The 'format' option should stay as is, though, because of the default.

Also, please don’t forget to update the execute function; it currently treats format and location as arguments, which causes an error when running the command. In most other commands, we usually define the arguments first and then the options, so please update that as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I can make this an argument instead of a option, but it would be optional in either case, as the code supports piping a file directly in to the command, this can't be enforced. So does it really make a difference if this an argument or a option?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’d agree, it basically comes down to:

A: occ calendar:import alice personal --location /mnt/testfiles/lustige_feiertage.ics
B: occ calendar:import alice personal /mnt/testfiles/lustige_feiertage.ics

I assume most people will use the command to import from a file, such as a backup, so my preference is the version without --location.

That said, both versions are fine, and whether location is an option or an argument isn’t a blocker.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, most people will most likely define a file to import, so there is no point in making them type in --location, i've changed it to a optional argument

Comment on lines 96 to 98
if (!in_array($errors, CalendarImportOptions::ERROR_OPTIONS, true)) {
throw new InvalidArgumentException('Invalid errors option specified');
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned in our call, CalendarImportOptions.setErrors already validates the input. Whether you validate it in the command or in the options object doesn’t really matter, but please don’t do it in both places 😉 My preference would be to keep the validation in the options object.

*
* @return array<string,array<string,string|array<string>>>
*/
public function import(CalendarImportOptions $options, callable $generator): array {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The import service is not exposed.

Right now, the import method doesn't use any private methods or fields from the Calendar object that aren’t already exposed, so there’s no technical reason to implement import and export functionality directly in the Calendar object.

I understand the idea behind ICalendarImport, and that apps might be able to override it, but honestly, that’s a use case I just don’t see.

Take the Deck example: a deck card is much more than just a calendar event. I’d be really surprised if the Deck team started subclassing Calendar and implementing their own marshalling to wrap a deck card into an iCal object. Sure, that might technically be possible; but why would they do that? ;)

Same goes for the Tasks and Pools apps, they don’t even use a calendar provider and rely on our code for CalDAV. If something’s missing for them when importing/exporting, it needs to be fixed here.

So we’re introducing a more complex implementation to support extensibility that no one has asked for yet, and it’s unclear whether anyone ever will. That’s why my suggestion is: don’t expose the functionality, and remove the related exporter interface again before the 32 freeze.

Unless there’s a concrete use case, I’d rather avoid introducing complexity without a clear benefit.

@SebastianKrupinski SebastianKrupinski force-pushed the feat/issue-563-calendar-import branch 2 times, most recently from d72a25d to 0bfb9cc Compare July 21, 2025 15:14
@SebastianKrupinski
Copy link
Contributor Author

SebastianKrupinski commented Jul 21, 2025

@kesselb

I moved the code from the calendar to the service

* @param 'ical'|'jcal'|'xcal' $format
*/
public function setFormat(string $value): void {
if (!in_array($value, ImportService::FORMATS, true)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Psalm is raising a warning because \OCA\DAV\CalDAV\Import\ImportService is private, but referenced by a class part of the public interface. The easiest way to resolve the warning is to move the formats const over here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, this feels odd, the service should define the formats it supports. But I've changes it.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huhu,

If you’d rather not move the formats constant over here, I’m fine with a different approach too. The main thing is to keep psalm happy ;)

The psalm warning

lib/public/Calendar/CalendarImportOptions.php:56:25: UndefinedClass: Class, interface or enum named OCA\DAV\CalDAV\Import\ImportService does not exist

means that part of the public API is referencing an implementation detail.

For me, formats, validate_options, and error_options are similar in the sense that the service uses them to alter its behavior, so I’d prefer to keep them together in one place. Whether that’s in the Options object or in the service itself doesn’t matter.

If you prefer having them in the service, then you can’t use them in the Options object. I haven’t tested it, but moving the Options object to OCA\DAV\CalDAV\Import could be one solution. Another option would be, similar to what you did with the exporter, to avoid any validation in the Options object. That way, you wouldn’t run into issues with referencing private code in public.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point it does not matter, lets just keep it where it is.

*
* @return Generator<\Sabre\VObject\Component\VCalendar>
*/
private function importText(CalendarImportOptions $options): Generator {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As the ability to overwrite the import functionality is gone, there's also no need any longer to pass the unused options arguments. If we need it later, we can just add it again without breaking anything, as the implementation is now private.

*
* @return Generator<\Sabre\VObject\Component\VCalendar>
*/
private function importXml(CalendarImportOptions $options): Generator {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*
* @return Generator<\Sabre\VObject\Component\VCalendar>
*/
private function importJson(CalendarImportOptions $options): Generator {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SebastianKrupinski SebastianKrupinski force-pushed the feat/issue-563-calendar-import branch from 6cf58f4 to 9037de4 Compare August 20, 2025 04:20
Copy link
Collaborator

@kesselb kesselb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

php-cs is unhappy
please merge master

$calendarId = $calendar->getKey();
$principalUri = $calendar->getPrincipalUri();
$outcome = [];
foreach ($generator($options) as $vObject) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
foreach ($generator($options) as $vObject) {
foreach ($generator() as $vObject) {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops. Fixed

This was referenced Aug 22, 2025
*/
class ImportService {

private $source;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private $source;
/** @var resource */
private $source;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@kesselb
Copy link
Collaborator

kesselb commented Aug 27, 2025

Scenario: Exporting a calendar for alice and then importing it into a different calendar under the same account does not work.

occ calendar:export --format xcal --location /tmp/cal_export.xcal alice personal
occ dav:create-calendar alice test-123
occ calendar:import --format xcal alice test-123 /tmp/cal_export.xcal

My expectation would be that importing such a file into another calendar of the same user should work. Technically, we find the existing event through getCalendarObjectByUID and then either skip or update it. What’s missing is a check to see if the import is targeting the same calendar or a different one. I still approved the PR, but maybe we should consider adding such a check / making that case work too.

@SebastianKrupinski
Copy link
Contributor Author

Scenario: Exporting a calendar for alice and then importing it into a different calendar under the same account does not work.

Good find, I didn't even think of this, I've corrected the issue

Signed-off-by: SebastianKrupinski <[email protected]>
@SebastianKrupinski SebastianKrupinski force-pushed the feat/issue-563-calendar-import branch from 43d3c31 to 46e624a Compare August 27, 2025 12:31
@SebastianKrupinski SebastianKrupinski merged commit 9e9f3b9 into master Aug 27, 2025
215 of 219 checks passed
@SebastianKrupinski SebastianKrupinski deleted the feat/issue-563-calendar-import branch August 27, 2025 13:04
@github-project-automation github-project-automation bot moved this from 🏗️ In progress to ☑️ Done in 💌 📅 👥 Groupware team Aug 27, 2025
@nextcloud-bot nextcloud-bot mentioned this pull request Aug 28, 2025
@SebastianKrupinski
Copy link
Contributor Author

Documentation nextcloud/documentation#13691

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

3. to review Waiting for reviews enhancement feature: caldav Related to CalDAV internals

Projects

Development

Successfully merging this pull request may close these issues.

7 participants