-
-
Notifications
You must be signed in to change notification settings - Fork 4.6k
feat: Calendar Import #51925
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Calendar Import #51925
Conversation
|
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. |
|
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? |
Absolutely! In my case, it is necessary to delete outdated events that are not contained in the calendar files. |
|
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 👍 |
aa32cd8 to
be42c1e
Compare
There was a problem hiding this 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
| if (!in_array($errors, CalendarImportOptions::ERROR_OPTIONS, true)) { | ||
| throw new InvalidArgumentException('Invalid errors option specified'); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| if (!in_array($errors, CalendarImportOptions::ERROR_OPTIONS, true)) { | |
| throw new InvalidArgumentException('Invalid errors option specified'); | |
| } |
Validation is done in setErrors
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| if (!in_array($validation, CalendarImportOptions::VALIDATE_OPTIONS, true)) { | ||
| throw new InvalidArgumentException('Invalid validation option specified'); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| if (!in_array($validation, CalendarImportOptions::VALIDATE_OPTIONS, true)) { | |
| throw new InvalidArgumentException('Invalid validation option specified'); | |
| } |
Validation is done in setValidation
| if (!in_array($format, ImportService::FORMATS, true)) { | ||
| throw new InvalidArgumentException("Format <$format> is not valid."); | ||
| } |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
$options seems unused
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
$options seems unused
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See above comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
$options seems unused
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See above comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay
apps/dav/lib/CalDAV/CalendarImpl.php
Outdated
| * | ||
| * @return array<string,array<string,string|array<string>>> | ||
| */ | ||
| public function import(CalendarImportOptions $options, callable $generator): array { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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') |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
| if (!in_array($errors, CalendarImportOptions::ERROR_OPTIONS, true)) { | ||
| throw new InvalidArgumentException('Invalid errors option specified'); | ||
| } |
There was a problem hiding this comment.
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.
apps/dav/lib/CalDAV/CalendarImpl.php
Outdated
| * | ||
| * @return array<string,array<string,string|array<string>>> | ||
| */ | ||
| public function import(CalendarImportOptions $options, callable $generator): array { |
There was a problem hiding this comment.
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.
d72a25d to
0bfb9cc
Compare
|
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)) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
6cf58f4 to
9037de4
Compare
There was a problem hiding this 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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| foreach ($generator($options) as $vObject) { | |
| foreach ($generator() as $vObject) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops. Fixed
| */ | ||
| class ImportService { | ||
|
|
||
| private $source; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| private $source; | |
| /** @var resource */ | |
| private $source; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
|
Scenario: Exporting a calendar for alice and then importing it into a different calendar under the same account does not work. 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 |
Good find, I didn't even think of this, I've corrected the issue |
Signed-off-by: SebastianKrupinski <[email protected]>
43d3c31 to
46e624a
Compare
|
Documentation nextcloud/documentation#13691 |
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:
Import from pipe:
Import from file and override existing:
Checklist