├── .sandstorm ├── stack ├── pgp-keyring ├── pgp-signature ├── screenshots │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ └── 7.png ├── new-instance.sh ├── debug.sh ├── description.md ├── setup.sh ├── build.sh ├── launcher.sh ├── logo.svg ├── global-setup.sh ├── changelog.md ├── Vagrantfile └── sandstorm-pkgdef.capnp ├── README.md ├── requirements.txt ├── .gitignore ├── radicale.config ├── HACKING.md ├── infcloud ├── css │ ├── hideresources.css │ └── sandstorm-integration.css ├── images │ ├── banner_cloud.svg │ └── banner_import_export.svg ├── cache_handler.js ├── cache.manifest ├── sandstorm-integration.js ├── caldavzap.config.js ├── lib │ └── jquery-ui-dialog-only.js ├── carddavmate.config.js └── addressbook.js ├── main.py ├── webassets_config └── __init__.py ├── nginx.conf.tmpl ├── issue-21.diff ├── importapp └── __init__.py └── assets.py /.sandstorm/stack: -------------------------------------------------------------------------------- 1 | uwsgi 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | .sandstorm/description.md -------------------------------------------------------------------------------- /.sandstorm/pgp-keyring: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synchrone/sandstorm-radicale/HEAD/.sandstorm/pgp-keyring -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | radicale==1.1.1 2 | webassets 3 | jsmin 4 | cssmin 5 | flask 6 | atomicwrites==1.1.0 7 | -------------------------------------------------------------------------------- /.sandstorm/pgp-signature: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synchrone/sandstorm-radicale/HEAD/.sandstorm/pgp-signature -------------------------------------------------------------------------------- /.sandstorm/screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synchrone/sandstorm-radicale/HEAD/.sandstorm/screenshots/1.png -------------------------------------------------------------------------------- /.sandstorm/screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synchrone/sandstorm-radicale/HEAD/.sandstorm/screenshots/2.png -------------------------------------------------------------------------------- /.sandstorm/screenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synchrone/sandstorm-radicale/HEAD/.sandstorm/screenshots/3.png -------------------------------------------------------------------------------- /.sandstorm/screenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synchrone/sandstorm-radicale/HEAD/.sandstorm/screenshots/4.png -------------------------------------------------------------------------------- /.sandstorm/screenshots/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synchrone/sandstorm-radicale/HEAD/.sandstorm/screenshots/5.png -------------------------------------------------------------------------------- /.sandstorm/screenshots/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synchrone/sandstorm-radicale/HEAD/.sandstorm/screenshots/6.png -------------------------------------------------------------------------------- /.sandstorm/screenshots/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/synchrone/sandstorm-radicale/HEAD/.sandstorm/screenshots/7.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .sandstorm/.vagrant 3 | .sandstorm/sandstorm-files.list 4 | env 5 | */__pycache__/ 6 | infcloud 7 | *nginx.conf 8 | *.zip 9 | *.spk -------------------------------------------------------------------------------- /.sandstorm/new-instance.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Starting new instance ($SUBAPP)" 4 | 5 | FLAG_FILE=/var/action.txt 6 | 7 | if [ ! -f $FLAG_FILE ] ; then 8 | echo $SUBAPP > $FLAG_FILE 9 | fi 10 | /opt/app/.sandstorm/launcher.sh -------------------------------------------------------------------------------- /.sandstorm/debug.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ## Debug flow: 3 | 4 | /opt/app/.sandstorm/build.sh --debug 5 | 6 | source /opt/app/env/bin/activate 7 | pip install pydevd 8 | 9 | HOME=/var uwsgi \ 10 | --socket /var/run/uwsgi.sock \ 11 | --plugin python3 \ 12 | --virtualenv /opt/app/env \ 13 | --pythonpath /opt/app \ 14 | --wsgi-file /opt/app/main.py \ 15 | --chmod-socket=777 & 16 | UWSGIPID=$! 17 | 18 | /usr/sbin/nginx -c /opt/app/${1:-"caldav"}.nginx.conf 19 | 20 | kill $UWSGIPID -------------------------------------------------------------------------------- /.sandstorm/description.md: -------------------------------------------------------------------------------- 1 | **A calendars and contacts application, which lets you sync your devices using a standard protocol.** 2 | 3 | ## This App 4 | This is a combination of Calendar and Contacts management [GUI applications](https://www.inf-it.com/open-source/clients/infcloud/) and a [server](http://radicale.org/), wrapped together for bundling as a Sandstorm.io app. 5 | 6 | ## Radicale 7 | The Radicale Project is mainly a calendar and contact server, giving local and distant access for reading, creating, modifying and deleting multiple calendars through simplified CalDAV and CardDAV protocols. -------------------------------------------------------------------------------- /.sandstorm/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | INF_IT_VER=0.13.1 3 | set -euo pipefail 4 | export DEBIAN_FRONTEND=noninteractive 5 | 6 | apt-get update 7 | apt-get install -y nginx uwsgi uwsgi-plugin-python3 virtualenv 8 | service nginx stop 9 | systemctl disable nginx 10 | 11 | #configure radicale 12 | mkdir -p /etc/radicale 13 | mkdir -p /var/lib/radicale 14 | ln -sf /opt/app/radicale.config /etc/radicale/config 15 | ln -sf /opt/app/radicale.rights /etc/radicale/rights 16 | 17 | echo "Downloading and configuring InfCloud" 18 | curl -s https://www.inf-it.com/InfCloud_$INF_IT_VER.zip > /opt/app/infcloud.zip 19 | unzip -q -n /opt/app/infcloud.zip -d /opt/app 20 | -------------------------------------------------------------------------------- /radicale.config: -------------------------------------------------------------------------------- 1 | [logging] 2 | debug = False 3 | [server] 4 | base_prefix = /radicale/ 5 | [well-known] 6 | caldav = /radicale/%(user)s/calendar.ics/ 7 | carddav = /radicale/%(user)s/addressbook.vcf/ 8 | [auth] 9 | type = remote_user 10 | [rights] 11 | type = owner_write 12 | [storage] 13 | filesystem_folder=/var/lib/radicale 14 | [headers] 15 | Access-Control-Allow-Origin = * 16 | Access-Control-Allow-Methods = GET, POST, OPTIONS, PROPFIND, PROPPATCH, REPORT, PUT, MOVE, DELETE, LOCK, UNLOCK 17 | Access-Control-Allow-Headers = User-Agent, Authorization, Content-type, Depth, If-match, If-None-Match, Lock-Token, Timeout, Destination, Overwrite, X-client, X-Requested-With 18 | Access-Control-Expose-Headers = Etag -------------------------------------------------------------------------------- /HACKING.md: -------------------------------------------------------------------------------- 1 | ##### Debugging Inf-IT frontend 2 | Since default flow uses FE resources bundling and grains cannot be changed in runtime - we need something more direct: 3 | 1. Don't use `vagrant-spk dev` 4 | 1. cd .sandstorm && vagrant ssh -c "sudo /opt/app/.sandstorm/debug.sh \[caldav|carddav\]" (defaults to caldav) 5 | 1. access [http://192.168.55.4:8000/] (notice how this is not using any grains, so the storage is /var/lib/radicale directly in vagrant) 6 | 1. _Note_: if you want to debug sandstorm interaction like sharing or API sync, you still need to use `vagrant-spk dev` 7 | 8 | ##### Building 9 | 1. `vagrant-spk dev`, so that build.sh is called without a --debug flag to re-build FE resources in production mode 10 | 1. `vagrant-spk build radicale-sandstorm-$( -------------------------------------------------------------------------------- /infcloud/css/hideresources.css: -------------------------------------------------------------------------------- 1 | /** Hiding the CalDAV resources, since we only have one per grain **/ 2 | #resourceCalDAV_h, #ResourceCalDAVList, #resourceCalDAVTODO_h, #ResourceCalDAVTODOList, #timezoneWrapper { 3 | width: 0px; 4 | } 5 | #CalendarLoader, #CalendarLoaderTODO { 6 | left: 50px; 7 | } 8 | #main_h, #searchForm, #main, #main_h_TODO, #searchFormTODO, #mainTODO { 9 | left: 49px; 10 | } 11 | #calendarLine, #calendarLineTODO { 12 | display: none; 13 | } 14 | 15 | /** CardDAV **/ 16 | .resourcesCardDAV_d, #ResourceCardDAVList, #ResourceCardDAVListOverlay { 17 | width: 0px; 18 | } 19 | .collection_d, #SearchBox, #ABList, #ABListOverlay, #AddressbookOverlay { 20 | left: 50px; 21 | } 22 | #vCardEditor tr[data-type="DEST"] { 23 | display: none; 24 | } -------------------------------------------------------------------------------- /.sandstorm/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | VENV=/opt/app/env 4 | if [ ! -d $VENV ] ; then 5 | virtualenv -p python3 $VENV 6 | else 7 | echo "$VENV exists, moving on" 8 | fi 9 | 10 | if [ -f /opt/app/requirements.txt ] ; then 11 | $VENV/bin/pip install -r /opt/app/requirements.txt 12 | fi 13 | 14 | # See: https://github.com/synchrone/sandstorm-radicale/issues/21 15 | bash -c "patch -p1 -i /opt/app/issue-21.diff --forward -d $VENV/lib/python3.4/site-packages/; exit 0" 16 | 17 | echo "Generating NGINX configs..." 18 | #rebuilding nginx config for dev 19 | sed --expression 's/_subapp_/caldavzap/' /opt/app/nginx.conf.tmpl > /opt/app/caldav.nginx.conf; 20 | sed --expression 's/_subapp_/carddavmate/' /opt/app/nginx.conf.tmpl > /opt/app/carddav.nginx.conf; 21 | 22 | cd /opt/app/ && $VENV/bin/python assets.py build $* -------------------------------------------------------------------------------- /.sandstorm/launcher.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | # something something folders 4 | mkdir -p /var/lib/nginx 5 | mkdir -p /var/lib/radicale 6 | mkdir -p /var/log 7 | mkdir -p /var/log/nginx 8 | mkdir -p /var/log/radicale 9 | # Wipe /var/run, since pidfiles and socket files from previous launches should go away 10 | # TODO someday: I'd prefer a tmpfs for these. 11 | rm -rf /var/run 12 | mkdir -p /var/run 13 | 14 | UWSGI_SOCKET_FILE=/var/run/uwsgi.sock 15 | 16 | # Spawn uwsgi 17 | HOME=/var uwsgi \ 18 | --socket $UWSGI_SOCKET_FILE \ 19 | --plugin python3 \ 20 | --virtualenv /opt/app/env \ 21 | --pythonpath /opt/app \ 22 | --wsgi-file /opt/app/main.py & 23 | 24 | # Wait for uwsgi to bind its socket 25 | while [ ! -e $UWSGI_SOCKET_FILE ] ; do 26 | echo "waiting for uwsgi to be available at $UWSGI_SOCKET_FILE" 27 | sleep .2 28 | done 29 | 30 | SUBAPP=$(cat /var/action.txt) 31 | echo "Starting NGINX ($SUBAPP)" 32 | # Start nginx. 33 | /usr/sbin/nginx -c /opt/app/$SUBAPP.nginx.conf 34 | 35 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # import pydevd 2 | import radicale 3 | from importapp import wsgi_app as importApp 4 | from radicale import config, Application as Radicale 5 | 6 | # pydevd.settrace('10.0.2.2', port=9009, stdoutToServer=True, stderrToServer=True, suspend=False) 7 | radicale.log.start() 8 | 9 | 10 | class RadicaleWithImportSupport(object): 11 | def __init__(self, radicale, importapp): 12 | self.radicale = radicale 13 | self.importapp = importapp 14 | 15 | def __call__(self, environ, start_response): 16 | prefix = config.get("server", "base_prefix").rstrip('/') 17 | if environ['PATH_INFO'] in [prefix+'/import', prefix+'/export']: 18 | radicale.log.LOGGER.debug('Handing over to import app (stripped %s)' % prefix) 19 | environ['PATH_INFO'] = environ['PATH_INFO'][len(prefix):] 20 | return self.importapp.__call__(environ, start_response) 21 | 22 | return self.radicale.__call__(environ, start_response) 23 | 24 | application = RadicaleWithImportSupport(Radicale(), importApp) 25 | -------------------------------------------------------------------------------- /infcloud/css/sandstorm-integration.css: -------------------------------------------------------------------------------- 1 | .ui-widget-overlay { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | background: #aaaaaa; 8 | opacity: .3; 9 | z-index: 26; 10 | } 11 | 12 | #intSync { 13 | display: block; 14 | background: url(../images/banner_cloud.svg) no-repeat center; 15 | background-size: 36px 36px; 16 | } 17 | 18 | #intImport { 19 | display: block; 20 | background: url(../images/banner_import_export.svg) no-repeat center; 21 | background-size: 36px 36px; 22 | } 23 | 24 | .infIt-dialog { 25 | z-index: 27; /** higher than inf-it system **/ 26 | } 27 | .infIt-dialog .ui-icon{ 28 | height: 0; /** No css for jQuery Dialog plugin, so won't show icons **/ 29 | } 30 | .infIt-dialog .ui-dialog-titlebar-close{ 31 | margin: 5px 0; 32 | } 33 | 34 | .infIt-dialog .ui-dialog-content{ 35 | padding: 0 15px; 36 | } 37 | 38 | .infIt-dialog .iframish{ 39 | color: #000; 40 | font-size: medium; 41 | font-family: monospace; 42 | } 43 | 44 | .infIt-dialog iframe { 45 | height: 32px; 46 | width: 100%; 47 | } 48 | -------------------------------------------------------------------------------- /infcloud/images/banner_cloud.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /webassets_config/__init__.py: -------------------------------------------------------------------------------- 1 | from webassets import Environment, Bundle 2 | environment = Environment(directory='infcloud', url='/') 3 | environment.register('js', Bundle( 4 | 'cache_handler.js', 5 | Bundle('lib/jquery-2.1.4.min.js'), 6 | 'lib/jquery.browser.js', 7 | 'lib/jquery.autosize.js', 8 | 'lib/jquery-ui-1.11.4.custom.js', 9 | 'lib/jquery.quicksearch.js', 10 | 'lib/jquery.placeholder-1.1.9.js', 11 | 'lib/jshash-2.2_sha256.js', 12 | 'lib/jquery.tagsinput.js', 13 | 'lib/spectrum.js', 14 | 'lib/fullcalendar.js', 15 | 'lib/jquery-ui-dialog-only.js', 16 | 'common.js', 17 | 'webdav_protocol.js', 18 | 'localization.js', 19 | 'interface.js', 20 | 'vcalendar_rfc_regex.js', 21 | 'vcard_rfc_regex.js', 22 | 'resource.js', 23 | 'vcalendar.js', 24 | 'vtodo.js', 25 | 'lib/rrule.js', 26 | 'addressbook.js', 27 | 'data_process.js', 28 | 'main.js', 29 | 'forms.js', 30 | 'timezones.js', 31 | 'sandstorm-integration.js', 32 | filters='jsmin', output='compressed.js' 33 | )) 34 | 35 | environment.register('css', Bundle( 36 | 'css/jquery-ui.custom.css', 37 | 'css/jquery.tagsinput.css', 38 | 'css/spectrum.custom.css', 39 | 'css/default.css', 40 | 'css/fullcalendar.css', 41 | 'css/default_integration.css', 42 | 'css/hideresources.css', 43 | 'css/sandstorm-integration.css', 44 | filters='cssmin', output='compressed.css' 45 | )) 46 | -------------------------------------------------------------------------------- /nginx.conf.tmpl: -------------------------------------------------------------------------------- 1 | worker_processes 4; 2 | pid /var/run/nginx.pid; 3 | 4 | daemon off; 5 | 6 | events { 7 | worker_connections 768; 8 | # multi_accept on; 9 | } 10 | 11 | http { 12 | sendfile on; 13 | tcp_nopush on; 14 | tcp_nodelay on; 15 | keepalive_timeout 65; 16 | types_hash_max_size 2048; 17 | include /etc/nginx/mime.types; 18 | default_type application/octet-stream; 19 | 20 | access_log off; #/dev/stderr; 21 | error_log stderr; # debug; 22 | gzip off; 23 | 24 | server { 25 | listen 8000 default_server; 26 | listen [::]:8000 default_server ipv6only=on; 27 | client_max_body_size 0; 28 | 29 | server_name localhost; 30 | root /opt/app/infcloud; 31 | 32 | location = /config.js { 33 | rewrite ^ /_subapp_.config.js break; 34 | break; 35 | } 36 | 37 | # radicale setup 38 | location /radicale { 39 | if ($http_x_sandstorm_permissions ~* (readonly)) { set $user $1; } 40 | if ($http_x_sandstorm_permissions ~* (owner)) { set $user $1; } 41 | 42 | uwsgi_pass unix:///var/run/uwsgi.sock; 43 | include /etc/nginx/uwsgi_params; 44 | uwsgi_param REMOTE_USER $user; 45 | uwsgi_param HTTP_AUTHORIZATION ''; 46 | } 47 | #dav discovery as per RFC6764 48 | location ~ ^/.well-known/(caldav|carddav) { 49 | rewrite ^ /radicale$request_uri last; 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /infcloud/images/banner_import_export.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 10 | 11 | 13 | 14 | 15 | 16 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /.sandstorm/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.sandstorm/global-setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | CURL_OPTS="--silent --show-error" 5 | echo localhost > /etc/hostname 6 | hostname localhost 7 | curl $CURL_OPTS https://install.sandstorm.io/ > /host-dot-sandstorm/caches/install.sh 8 | SANDSTORM_CURRENT_VERSION=$(curl $CURL_OPTS -f "https://install.sandstorm.io/dev?from=0&type=install") 9 | SANDSTORM_PACKAGE="sandstorm-$SANDSTORM_CURRENT_VERSION.tar.xz" 10 | if [[ ! -f /host-dot-sandstorm/caches/$SANDSTORM_PACKAGE ]] ; then 11 | echo -n "Downloading Sandstorm version ${SANDSTORM_CURRENT_VERSION}..." 12 | curl $CURL_OPTS --output "/host-dot-sandstorm/caches/$SANDSTORM_PACKAGE.partial" "https://dl.sandstorm.io/$SANDSTORM_PACKAGE" 13 | mv "/host-dot-sandstorm/caches/$SANDSTORM_PACKAGE.partial" "/host-dot-sandstorm/caches/$SANDSTORM_PACKAGE" 14 | echo "...done." 15 | fi 16 | if [ ! -e /opt/sandstorm/latest/sandstorm ] ; then 17 | echo -n "Installing Sandstorm version ${SANDSTORM_CURRENT_VERSION}..." 18 | bash /host-dot-sandstorm/caches/install.sh -d -e "/host-dot-sandstorm/caches/$SANDSTORM_PACKAGE" >/dev/null 19 | echo "...done." 20 | fi 21 | modprobe ip_tables 22 | # Make the vagrant user part of the sandstorm group so that commands like 23 | # `spk dev` work. 24 | usermod -a -G 'sandstorm' 'vagrant' 25 | # Bind to all addresses, so the vagrant port-forward works. 26 | sudo sed --in-place='' \ 27 | --expression='s/^BIND_IP=.*/BIND_IP=0.0.0.0/' \ 28 | /opt/sandstorm/sandstorm.conf 29 | sudo service sandstorm restart 30 | # Enable apt-cacher-ng proxy to make things faster if one appears to be running on the gateway IP 31 | GATEWAY_IP=$(ip route | grep ^default | cut -d ' ' -f 3) 32 | if nc -z "$GATEWAY_IP" 3142 ; then 33 | echo "Acquire::http::Proxy \"http://$GATEWAY_IP:3142\";" > /etc/apt/apt.conf.d/80httpproxy 34 | fi 35 | -------------------------------------------------------------------------------- /.sandstorm/changelog.md: -------------------------------------------------------------------------------- 1 | # v1 2 | * Initial packaging. 3 | * Supports sharing. 4 | * Does not support DAV access for all clients due to Sandstorm's limitations for HTTP Basic auth (see https://github.com/synchrone/sandstorm-radicale/issues/3) 5 | 6 | # v2 7 | * Initial CalDAV\CardDAV UI 8 | 9 | # v3 10 | * Resource pipelining, performance boost 11 | * CalDAV API now actually works 12 | * Switched to Python 3 13 | * Basic DAV discovery support 14 | * Hiding resource selection, since we have 1 per grain 15 | 16 | # v4 17 | * Fixing python virtualenv issue 18 | * Hiding resource selection in CardDAV as well 19 | 20 | # v5 21 | * Better UI for synchronization 22 | * Refresh button enabled 23 | * Web auto-refresh interval set to 30s for calendars and 60s for contacts 24 | 25 | # v6 26 | * Fixing refresh button not working bug 27 | 28 | # v7 29 | * Fixing [issue #7](https://github.com/synchrone/sandstorm-radicale/issues/7) 30 | * Asset auto-referencing from index.html 31 | 32 | # v8 33 | * Fixed .well-known support 34 | * Fixed an unauthenticated setup check support (issue #8) 35 | 36 | # v9 37 | * Import/Export feature 38 | * Sync and Import/Export icons changed 39 | * Sync and Import/Export dialog modal overlay fixed 40 | * Refresh [issue (#12)](https://github.com/synchrone/sandstorm-radicale/issues/7) solved 41 | * Copy button inside credential fields' iframes 42 | 43 | # v10 44 | * Fixing CardDavMATE UI bug 45 | * Fixing issue #15 46 | * Demo-mode is now calendar, not contacts 47 | * Not storing app settings inside DAV, since they're hardcoded anyway 48 | 49 | # v11 50 | * Fixing issue #16 51 | * Fixing file-list packaging issues 52 | 53 | # v12 54 | * Fixing issue #21 55 | 56 | # v13 57 | * Hotfixing atomic writes patch backport due to mis-backported issue #21 patch in v12 58 | 59 | # v14 60 | * Finally fixing OS X sync set up in Manual mode. 61 | * Fixing broken sync how-to URLs -------------------------------------------------------------------------------- /issue-21.diff: -------------------------------------------------------------------------------- 1 | diff --git a/radicale/storage/filesystem.py b/radicale/storage/filesystem.py 2 | index deeba20..32a26a7 100644 3 | --- a/radicale/storage/filesystem.py 4 | +++ b/radicale/storage/filesystem.py 5 | @@ -28,6 +28,7 @@ import json 6 | import time 7 | import sys 8 | from contextlib import contextmanager 9 | +from atomicwrites import AtomicWriter 10 | 11 | from .. import config, ical, log, pathutils 12 | 13 | @@ -42,6 +43,14 @@ except: 14 | GIT_REPOSITORY = None 15 | 16 | 17 | +class _EncodedAtomicWriter(AtomicWriter): 18 | + def __init__(self, path, encoding, mode="w", overwrite=True): 19 | + self._encoding = encoding 20 | + super().__init__(path, mode, overwrite) 21 | + 22 | + def get_fileobject(self, **kwargs): 23 | + return super().get_fileobject(encoding=self._encoding, **kwargs) 24 | + 25 | # This function overrides the builtin ``open`` function for this module 26 | # pylint: disable=W0622 27 | @contextmanager 28 | @@ -49,8 +58,12 @@ def open(path, mode="r"): 29 | """Open a file at ``path`` with encoding set in the configuration.""" 30 | # On enter 31 | abs_path = os.path.join(FOLDER, path.replace("/", os.sep)) 32 | - with codecs.open(abs_path, mode, config.get("encoding", "stock")) as fd: 33 | - yield fd 34 | + if mode == "w": 35 | + with _EncodedAtomicWriter(abs_path, config.get("encoding", "stock"), mode).open() as fd: 36 | + yield fd 37 | + else: 38 | + with codecs.open(abs_path, mode, config.get("encoding", "stock")) as fd: 39 | + yield fd 40 | # On exit 41 | if GIT_REPOSITORY and mode == "w": 42 | path = os.path.relpath(abs_path, FOLDER) 43 | @@ -60,7 +73,6 @@ def open(path, mode="r"): 44 | path.encode("utf-8"), committer=committer.encode("utf-8")) 45 | # pylint: enable=W0622 46 | 47 | - 48 | class Collection(ical.Collection): 49 | """Collection stored in a flat ical file.""" 50 | @property 51 | -------------------------------------------------------------------------------- /importapp/__init__.py: -------------------------------------------------------------------------------- 1 | from werkzeug.datastructures import FileStorage 2 | from flask import Flask, request, jsonify, make_response 3 | from radicale import config, ical, Application as Radicale 4 | app = Flask(__name__) 5 | radicale_app = Radicale() # uses /etc/config/radicale 6 | 7 | 8 | @app.route('/import', methods=['POST']) 9 | def post_import(): 10 | imported = handle_import(select_collection(get_path())) 11 | return jsonify(status='ok', imported=imported) 12 | 13 | 14 | @app.route('/export') 15 | def get_export(): 16 | collection = select_collection(get_path()) 17 | response = make_response(collection.text) 18 | response.headers["Content-Disposition"] = "attachment; filename=%s" % collection.name 19 | response.headers["Content-Type"] = collection.mimetype 20 | return response 21 | 22 | 23 | def get_path(): 24 | from werkzeug.exceptions import BadRequestKeyError 25 | try: 26 | path = request.args['path'] 27 | except BadRequestKeyError: 28 | path = request.form['path'] 29 | return path[len(config.get("server", "base_prefix").rstrip('/')):] if isinstance(path, str) else None 30 | 31 | 32 | def select_collection(path): 33 | user = request.environ.get("REMOTE_USER") # only supporting sandstorm 34 | items = ical.Collection.from_path(path, request.environ.get("HTTP_DEPTH", "0")) 35 | 36 | collection_to_use = \ 37 | radicale_app.collect_allowed_items(items, user)[1] # second item is writable collections 38 | assert len(collection_to_use) > 0 39 | 40 | collection_to_use = collection_to_use[0] 41 | assert isinstance(collection_to_use, ical.Collection) 42 | 43 | return collection_to_use 44 | 45 | 46 | def handle_import(collection_to_import): 47 | importFile = request.files['file'] 48 | assert isinstance(importFile, FileStorage) 49 | fileContents = importFile.stream.read().decode('utf-8') 50 | 51 | items_in_collection = len(collection_to_import.items) 52 | collection_to_import.append(None, fileContents) 53 | 54 | return len(collection_to_import.items) - items_in_collection 55 | 56 | wsgi_app = app.wsgi_app 57 | if __name__ == '__main__': 58 | app.run() -------------------------------------------------------------------------------- /assets.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import logging 3 | import argparse 4 | from webassets.script import CommandLineEnvironment 5 | from webassets.loaders import PythonLoader 6 | 7 | parser = argparse.ArgumentParser(description='Build assets') 8 | parser.add_argument('--loglevel', dest='loglevel', default='DEBUG', help='DEBUG, INFO, WARN, ERROR or CRITICAL') 9 | parser.add_argument('--debug', action='store_true', default=False, help='debug resource pipelining') 10 | parser.add_argument('--reference', action='store_true', default=True, help='Reference resources built from --reference-file') 11 | parser.add_argument('--reference-tmpl', dest='reference_tmpl', default='infcloud/index.html.tmpl', help='The html template to patch with resource links') 12 | parser.add_argument('--reference-output', dest='reference_output', default='infcloud/index.html', help='The html file to output') 13 | parser.add_argument('command', metavar='command', default='build', help='build, watch or clean') 14 | args = parser.parse_args() 15 | 16 | # Setup a logger 17 | log = logging.getLogger('webassets') 18 | log.addHandler(logging.StreamHandler()) 19 | log.setLevel(logging.__dict__[args.loglevel]) 20 | 21 | loader = PythonLoader('webassets_config') 22 | assets_env = loader.load_environment() 23 | assets_env.debug = args.debug 24 | 25 | cmdenv = CommandLineEnvironment(assets_env, log) 26 | cmdenv.build() 27 | 28 | if args.debug: 29 | print("The following files are produced by assets pipeline:") 30 | print(assets_env['js'].urls()) 31 | print(assets_env['css'].urls()) 32 | 33 | if args.command != 'build': 34 | cmdenv.invoke(args.command, {}) 35 | 36 | if args.reference: 37 | index = open(args.reference_tmpl, 'r') 38 | index_contents = index.read() 39 | index.close() 40 | 41 | js_prepped = ['' % url for url in assets_env['css'].urls()] 42 | css_prepped = ['' % url for url in assets_env['js'].urls()] 43 | index_contents = index_contents\ 44 | .replace('', "\n".join(css_prepped))\ 45 | .replace('', "\n".join(js_prepped)) 46 | 47 | patched = open(args.reference_output ,'w+') 48 | patched.write(index_contents) 49 | patched.close() 50 | -------------------------------------------------------------------------------- /infcloud/cache_handler.js: -------------------------------------------------------------------------------- 1 | // OFFLINE CACHE DEBUGGING 2 | 3 | /*var cacheStatusValues=[]; 4 | cacheStatusValues[0]='uncached'; 5 | cacheStatusValues[1]='idle'; 6 | cacheStatusValues[2]='checking'; 7 | cacheStatusValues[3]='downloading'; 8 | cacheStatusValues[4]='updateready'; 9 | cacheStatusValues[5]='obsolete'; 10 | 11 | var cache=window.applicationCache; 12 | cache.addEventListener('cached', logEvent, false); 13 | cache.addEventListener('checking', logEvent, false); 14 | cache.addEventListener('downloading', logEvent, false); 15 | cache.addEventListener('error', logEvent, false); 16 | cache.addEventListener('noupdate', logEvent, false); 17 | cache.addEventListener('obsolete', logEvent, false); 18 | cache.addEventListener('progress', logEvent, false); 19 | cache.addEventListener('updateready', logEvent, false); 20 | 21 | function logEvent(e) 22 | { 23 | var online, status, type, message; 24 | online=(navigator.onLine) ? 'yes' : 'no'; 25 | status=cacheStatusValues[cache.status]; 26 | type=e.type; 27 | message='online: '+online; 28 | message+=', event: '+type; 29 | message+=', status: '+status; 30 | if(type=='error' && navigator.onLine) 31 | message+=' (prolly a syntax error in manifest)'; 32 | console.log(message); 33 | } 34 | 35 | window.applicationCache.addEventListener('updateready', function(){ 36 | window.applicationCache.swapCache(); 37 | console.log('swap cache has been called'); 38 | }, false 39 | ); 40 | 41 | //setInterval(function(){cache.update()}, 10000);*/ 42 | 43 | // Check if a new cache is available on page load. 44 | window.addEventListener('load', function(e) 45 | { 46 | window.applicationCache.addEventListener('cached', function(e) 47 | { 48 | if(!isUserLogged) 49 | window.location.reload(); 50 | else 51 | $('#cacheDialog').css('display','none'); 52 | }, false); 53 | 54 | window.applicationCache.addEventListener('updateready', function(e) 55 | { 56 | if(!isUserLogged) 57 | window.location.reload(); 58 | else 59 | $('#cacheDialog').css('display','none'); 60 | }, false); 61 | 62 | window.applicationCache.addEventListener('obsolete', function(e) 63 | { 64 | if(!isUserLogged) 65 | window.location.reload(); 66 | else 67 | $('#cacheDialog').css('display','none'); 68 | }, false); 69 | 70 | window.applicationCache.addEventListener('noupdate', function(e) 71 | { 72 | if(!isUserLogged) 73 | { 74 | clearInterval(globalCacheUpdateInterval); 75 | globalCacheUpdateInterval=setInterval(function(){window.applicationCache.update();}, 300000); 76 | //$('#LoginPage .window').css('display', 'inline-none'); 77 | } 78 | }, false); 79 | }, false); 80 | -------------------------------------------------------------------------------- /infcloud/cache.manifest: -------------------------------------------------------------------------------- 1 | CACHE MANIFEST 2 | #V 20160210004437 3 | 4 | CACHE: 5 | compressed.js 6 | compressed.css 7 | fonts/Roboto-BoldItalic-webfont.eot 8 | fonts/Roboto-BoldItalic-webfont.svg 9 | fonts/Roboto-BoldItalic-webfont.ttf 10 | fonts/Roboto-BoldItalic-webfont.woff 11 | fonts/Roboto-Bold-webfont.eot 12 | fonts/Roboto-Bold-webfont.svg 13 | fonts/Roboto-Bold-webfont.ttf 14 | fonts/Roboto-Bold-webfont.woff 15 | fonts/Roboto-Italic-webfont.eot 16 | fonts/Roboto-Italic-webfont.svg 17 | fonts/Roboto-Italic-webfont.ttf 18 | fonts/Roboto-Italic-webfont.woff 19 | fonts/Roboto-LightItalic-webfont.eot 20 | fonts/Roboto-LightItalic-webfont.svg 21 | fonts/Roboto-LightItalic-webfont.ttf 22 | fonts/Roboto-LightItalic-webfont.woff 23 | fonts/Roboto-Light-webfont.eot 24 | fonts/Roboto-Light-webfont.svg 25 | fonts/Roboto-Light-webfont.ttf 26 | fonts/Roboto-Light-webfont.woff 27 | fonts/Roboto-MediumItalic-webfont.eot 28 | fonts/Roboto-MediumItalic-webfont.svg 29 | fonts/Roboto-MediumItalic-webfont.ttf 30 | fonts/Roboto-MediumItalic-webfont.woff 31 | fonts/Roboto-Medium-webfont.eot 32 | fonts/Roboto-Medium-webfont.svg 33 | fonts/Roboto-Medium-webfont.ttf 34 | fonts/Roboto-Medium-webfont.woff 35 | fonts/Roboto-Regular-webfont.eot 36 | fonts/Roboto-Regular-webfont.svg 37 | fonts/Roboto-Regular-webfont.ttf 38 | fonts/Roboto-Regular-webfont.woff 39 | images/add_cal.svg 40 | images/add_cal_white.svg 41 | images/arrow_next_red.svg 42 | images/arrow_next.svg 43 | images/arrow_prev_red.svg 44 | images/arrow_prev.svg 45 | images/arrow.svg 46 | images/banner_addressbook.svg 47 | images/banner_calendar.svg 48 | images/banner_logout.svg 49 | images/banner_refresh.svg 50 | images/banner_todo.svg 51 | images/calendarB.svg 52 | images/cdm_logo.svg 53 | images/cdz_logo.svg 54 | images/cloud.svg 55 | images/company_s_b.svg 56 | images/company.svg 57 | images/company_s_w.svg 58 | images/delegation.svg 59 | images/dp_left.svg 60 | images/dp_right.svg 61 | images/drag.svg 62 | images/error_badge.svg 63 | images/error_b.svg 64 | images/error_w.svg 65 | images/infcloud_logo.svg 66 | images/in_progress_b.svg 67 | images/in_progress_dr.svg 68 | images/in_progress_r.svg 69 | images/in_progress_w.svg 70 | images/jumper_bottom_b.svg 71 | images/jumper_bottom_w.svg 72 | images/jumper_top_b.svg 73 | images/jumper_top_w.svg 74 | images/loadinfo.gif 75 | images/loadinfo_s1.gif 76 | images/loadinfo_s2.gif 77 | images/loadinfo_s3.gif 78 | images/loadinfo_s4.gif 79 | images/login.svg 80 | images/logout.svg 81 | images/needs_action_b.svg 82 | images/needs_action_dr.svg 83 | images/needs_action_r.svg 84 | images/needs_action_w.svg 85 | images/new_item.svg 86 | images/op_add.svg 87 | images/op_del.svg 88 | images/popupArrow.svg 89 | images/priority-1-dr.svg 90 | images/priority-1-r.svg 91 | images/priority-1.svg 92 | images/priority-1-w.svg 93 | images/priority-2-dr.svg 94 | images/priority-2-r.svg 95 | images/priority-2.svg 96 | images/priority-2-w.svg 97 | images/priority-3-dr.svg 98 | images/priority-3-r.svg 99 | images/priority-3.svg 100 | images/priority-3-w.svg 101 | images/read_only_b.svg 102 | images/read_only_w.svg 103 | images/remove_cal.svg 104 | images/remove_cal_white.svg 105 | images/reset_b.svg 106 | images/reset_dr.svg 107 | images/reset_drw.svg 108 | images/reset_r.svg 109 | images/reset_rw.svg 110 | images/reset_w.svg 111 | images/resource_arrow_down.svg 112 | images/resource_arrow_right.svg 113 | images/resource_arrow_up.svg 114 | images/resources.svg 115 | images/search.svg 116 | images/searchWhiteNew.svg 117 | images/select_bg_black.svg 118 | images/select_bg_dis.svg 119 | images/select_bg.svg 120 | images/select_black.svg 121 | images/select_dis.svg 122 | images/select_inv.svg 123 | images/select_login.svg 124 | images/select.svg 125 | images/success_b.svg 126 | images/success_dr.svg 127 | images/success_drw.svg 128 | images/success_r.svg 129 | images/success_rw.svg 130 | images/success_w.svg 131 | images/todoB.svg 132 | images/user.svg 133 | 134 | NETWORK: 135 | * 136 | -------------------------------------------------------------------------------- /.sandstorm/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | # Guess at a reasonable name for the VM based on the folder vagrant-spk is 5 | # run from. The timestamp is there to avoid conflicts if you have multiple 6 | # folders with the same name. 7 | VM_NAME = File.basename(File.dirname(File.dirname(__FILE__))) + "_sandstorm_#{Time.now.utc.to_i}" 8 | 9 | # Vagrantfile API/syntax version. Don't touch unless you know what you're doing! 10 | VAGRANTFILE_API_VERSION = "2" 11 | 12 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 13 | # Base on the Sandstorm snapshots of the official Debian 8 (jessie) box. 14 | config.vm.box = "sandstorm/debian-jessie64" 15 | 16 | if Vagrant.has_plugin?("vagrant-vbguest") then 17 | # vagrant-vbguest is a Vagrant plugin that upgrades 18 | # the version of VirtualBox Guest Additions within each 19 | # guest. If you have the vagrant-vbguest plugin, then it 20 | # needs to know how to compile kernel modules, etc., and so 21 | # we give it this hint about operating system type. 22 | config.vm.guest = "debian" 23 | end 24 | 25 | # We forward port 6080, the Sandstorm web port, so that developers can 26 | # visit their sandstorm app from their browser as local.sandstorm.io:6080 27 | # (aka 127.0.0.1:6080). 28 | config.vm.network :forwarded_port, guest: 6080, host: 6080, auto_correct: true 29 | config.vm.network :forwarded_port, guest: 8000, host: 8000, auto_correct: true 30 | config.vm.network "private_network", ip: "192.168.55.4" 31 | 32 | # Use a shell script to "provision" the box. This installs Sandstorm using 33 | # the bundled installer. 34 | config.vm.provision "shell", inline: "sudo bash /opt/app/.sandstorm/global-setup.sh", keep_color: true 35 | # Then, do stack-specific and app-specific setup. 36 | config.vm.provision "shell", inline: "sudo bash /opt/app/.sandstorm/setup.sh", keep_color: true 37 | 38 | # Shared folders are configured per-provider since vboxsf can't handle >4096 open files, 39 | # NFS requires privilege escalation every time you bring a VM up, 40 | # and 9p is only available on libvirt. 41 | 42 | # Calculate the number of CPUs and the amount of RAM the system has, 43 | # in a platform-dependent way; further logic below. 44 | cpus = nil 45 | total_kB_ram = nil 46 | 47 | host = RbConfig::CONFIG['host_os'] 48 | if host =~ /darwin/ 49 | cpus = `sysctl -n hw.ncpu`.to_i 50 | total_kB_ram = `sysctl -n hw.memsize`.to_i / 1024 51 | elsif host =~ /linux/ 52 | cpus = `nproc`.to_i 53 | total_kB_ram = `grep MemTotal /proc/meminfo | awk '{print $2}'`.to_i 54 | elsif host =~ /mingw/ 55 | # powershell may not be available on Windows XP and Vista, so wrap this in a rescue block 56 | begin 57 | cpus = `powershell -Command "(Get-WmiObject Win32_Processor -Property NumberOfLogicalProcessors | Select-Object -Property NumberOfLogicalProcessors | Measure-Object NumberOfLogicalProcessors -Sum).Sum"`.to_i 58 | total_kB_ram = `powershell -Command "Get-CimInstance -class cim_physicalmemory | % $_.Capacity}"`.to_i / 1024 59 | rescue 60 | end 61 | end 62 | # Use the same number of CPUs within Vagrant as the system, with 1 63 | # as a default. 64 | # 65 | # Use at least 512MB of RAM, and if the system has more than 2GB of 66 | # RAM, use 1/4 of the system RAM. This seems a reasonable compromise 67 | # between having the Vagrant guest operating system not run out of 68 | # RAM entirely (which it basically would if we went much lower than 69 | # 512MB) and also allowing it to use up a healthily large amount of 70 | # RAM so it can run faster on systems that can afford it. 71 | if cpus.nil? or cpus.zero? 72 | cpus = 1 73 | end 74 | if total_kB_ram.nil? or total_kB_ram < 2048000 75 | assign_ram_mb = 512 76 | else 77 | assign_ram_mb = (total_kB_ram / 1024 / 4) 78 | end 79 | # Actually apply these CPU/memory values to the providers. 80 | config.vm.provider :virtualbox do |vb, override| 81 | vb.cpus = cpus 82 | vb.memory = assign_ram_mb 83 | vb.name = VM_NAME 84 | 85 | override.vm.synced_folder "..", "/opt/app", type: "nfs" 86 | override.vm.synced_folder ENV["HOME"] + "/.sandstorm", "/host-dot-sandstorm" 87 | override.vm.synced_folder "..", "/vagrant" 88 | end 89 | config.vm.provider :libvirt do |libvirt, override| 90 | libvirt.cpus = cpus 91 | libvirt.memory = assign_ram_mb 92 | libvirt.default_prefix = VM_NAME 93 | 94 | override.vm.synced_folder "..", "/opt/app", type: "9p", accessmode: "passthrough" 95 | override.vm.synced_folder ENV["HOME"] + "/.sandstorm", "/host-dot-sandstorm", type: "9p", accessmode: "passthrough" 96 | override.vm.synced_folder "..", "/vagrant", type: "9p", accessmode: "passthrough" 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /infcloud/sandstorm-integration.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | var subpath = $('').attr('href', globalAccountSettings[0].href)[0].pathname; 3 | var isCalDav = subpath.indexOf('.ics') > -1; 4 | 5 | function setupSync(){ 6 | var petname = (isCalDav ? 'Cal' : 'Card') + 'DAV Sync'; 7 | 8 | var dlg = $('
'+ 9 | '

For '+petname+' please use this URL:

'+ 10 | '