Virtual ics file

Hey there,
recently I was working with calendar files for events, and serving them by returning a snippet via route works fine.

However, I’d like to implement these ics files as virtual files which is much cleaner on the frontend and allows for stuff like niceSize() (custom file class etc), but how would I get the virtual file content (which is a string, basically) to represent the file? Has anybody done this before? I don’t see how new File() would be of any use there …

Would I implement something like public function content() … ?

Thanks!

I know, I could write my content to disk via File::create, but there has to be another way to achieve this …

I think it’s not that easy. Of course you could assign your string value to content, and then return that somehow, but you wouldn’t be able to do a file_get_contents unless you store the string in a data uri. And yes, you can find a way to estimate the future size of such a file through php://memory. But I wonder if there is really anything to be gained by this.

I once did this, a bit in a hurry:

site/models/event.php

<?php 
use Kirby\Cms\Page;
use Kirby\Cms\File;
use Jsvrcek\ICS\Model\Calendar;
use Jsvrcek\ICS\Model\CalendarEvent;
use Jsvrcek\ICS\Model\Description\Location;

use Jsvrcek\ICS\Utility\Formatter;
use Jsvrcek\ICS\CalendarStream;
use Jsvrcek\ICS\CalendarExport;


class ICSFile extends File {
  private $eventStart = null;
  private $eventEnd = null;
  private $eventName = null;
  private $wholeDay = false;
  private $eventLocation = null;
  private $timezone = null;


  // this needs to exist or deleting the page won't work
  public function delete(): bool
  {
    return true;
  }
  
  public function __construct($eventPage) {
    
    $this->eventStart = new DateTime($eventPage->date(), $this->timezone);
    if($eventPage->enddate()->isEmpty()) {
      $this->eventEnd = clone $this->eventStart;
      $this->eventEnd->setTime(23,59,59);
      if($this->eventStart->format('H:i:s') === '00:00:00') {
        $this->wholeDay = true;
      }
    } else {
      $this->eventEnd = new DateTime($eventPage->enddate(), $this->timezone);
    }


    $this->eventName = $eventPage->title();
    $this->eventLocation = $eventPage->place();
    $this->eventUrl = $eventPage->url() . '.ics';
    $this->timezone = new DateTimeZone('Europe/Zurich');

    parent::__construct([
      'filename' => 'calendar.ics',
      'url' => $this->eventUrl,
      'template' => 'attachment',
      'parent' => $eventPage,
      'content' => []
    ]);
  }

  public function writeContent(array $data, string $languageCode = null): bool {
    // not supported
  }

  public function read() {
    $event = new CalendarEvent();
    $location = new Location();

    $location
      ->setName($this->eventLocation);

    $event
      ->setStart($this->eventStart)
      ->setEnd($this->eventEnd)
      ->setAllDay($this->wholeDay)
      ->setSummary($this->eventName)
      ->addLocation($location);

    $calendar = new Calendar();
    $calendar
      ->setProdId('s1syphos')
      ->setTimezone($this->timezone)
      ->addEvent($event);
    $calendarExport = new CalendarExport(new CalendarStream, new Formatter());
    $calendarExport->addCalendar($calendar);

    return $calendarExport->getStream();
  }
}

class EventPage extends Page {
  public function files() {
    $files = parent::files();

    $virtualFile = new ICSFile($this);
    $files->add($virtualFile);
    return $files;
  }
}

It uses this GitHub - jasvrcek/ICS: Object-oriented php library for creating .ics iCal files to format the ICS file - you probably have your own solution, but I guess you’ll get the idea

2 Likes

Totally cool, thanks so much for sharing!

1 Like

I moved the calendar stream & export code from read() to __construct() and saved it as $this->calendar, then created these two (rather primitive) methods:

/**
 * Returns the raw size of the file
 *
 * @return int
 */
public function size(): int
{
    return strlen($this->calendar);
}


/**
 * Returns the file size in a human-readable format
 *
 * @return string
 */
public function niceSize(): string
{
    $size = $this->size();

    # the math magic
    $size = round($size / pow(1024, ($unit = floor(log($size, 1024)))), 2);

    return $size . ' ' . Kirby\Toolkit\F::$units[$unit];
}

… to provide low-level filesize. Thanks again, @rasteiner !

note that this creates the “file” (the export) every time the page is loaded. Even if you don’t access it, or its size. It’s probably not a problem, but if you want to be a bit more conservative, you could keep the code in read and instead cache it there. Like:

  public function read()
  {
    if($this->calendar) return $this->calendar;
    //else create $this->calendar and return it
  }

  public function size(): int
  {
    return strlen($this->read());
  }

  // etc...
1 Like

Didn’t think about that oO

Mhh, using an a tag (with download attribute, etc) doesn’t provide a working download link for $file->url() - what am I doing wrong? I didn’t change anything except adding the two functions as stated above - how did you manage to let people download your calendar file?

Uhm… It has been a while since I wrote that. Don’t really remember.

I’ll check it tomorrow and let you know

Oh… completely forgot about this. I have a content representation for the page the calendar event is bound to. That loads and echoes the file:

site/templates/event.ics.php

<?php
header('Content-type: text/calendar; charset=utf-8');
header('Content-Disposition: inline; filename=' . $page->slug() . '.ics');

echo $page->file('calendar.ics')->read();

You probably also could create a route that matches any “.ics” file, resolves and then loads it.

Yeah, I figured going this way would be feasible - @bnomei pointed me in that direction on Discord, thanks to you both!

1 Like

If you want the file to work in the panel, I guess you could overwrite the url function to make it point to the correct address

Nah, it works as it is, thx to your help - I’ll leave it as it is :wink:

hmmm sorry for the noob question, but how do I add the Jsvrcek php classes for the model/template to be able to use it? otherwise i am of course getting

Error
Class “Jsvrcek\ICS\Model\CalendarEvent” not found

See Plugin setup with Composer dependencies | Kirby CMS

thanks! the code above is not inside a plugin though, but ok, i will create a plugin just to load the library and keep the code above in the model. I just thought there was an easier way without having to create a plugin just for that

EDIT: it worked well, thanks! :slight_smile: