├── bootstrap.sh
├── .gitignore
├── requirements.txt
├── chef
├── flask
│ └── recipes
│ │ └── default.rb
├── solo.rb
├── roles.json
├── github-pages
│ └── recipes
│ │ └── default.rb
├── requests
│ └── recipes
│ │ └── default.rb
├── directories
│ └── recipes
│ │ └── default.rb
├── apache
│ └── recipes
│ │ ├── apache-jekit.conf
│ │ ├── default.rb
│ │ └── apache-ssl.conf
└── gunicorn
│ └── recipes
│ └── default.rb
├── templates
├── error-oauth.html
├── no-such-repo.html
├── error-runtime.html
├── no-such-ref.html
├── error-404.html
├── page-layout.html
└── index.html
├── install.sh
├── askpass.py
├── scripts
└── bookmarklet.js
├── jekyll.py
├── cull-dirs.py
├── README.md
├── href.py
├── util.py
├── git.py
└── make-it-so.py
/bootstrap.sh:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | checkouts
3 | repos
4 | venv-*
5 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | flask
2 | requests
3 | requests-oauthlib
4 |
--------------------------------------------------------------------------------
/chef/flask/recipes/default.rb:
--------------------------------------------------------------------------------
1 | package 'python-pip'
2 |
3 | bash 'install python-flask' do
4 | not_if 'python -c "import flask"'
5 | code 'pip install flask'
6 | end
7 |
--------------------------------------------------------------------------------
/chef/solo.rb:
--------------------------------------------------------------------------------
1 | #
2 | # http://docs.opscode.com/config_rb_solo.html
3 | #
4 | file_cache_path "/tmp/chef-solo"
5 | cookbook_path File.dirname(__FILE__)
6 | log_level :info
7 | log_location "/var/log/cheflog"
8 | ssl_verify_mode :verify_none
9 |
--------------------------------------------------------------------------------
/templates/error-oauth.html:
--------------------------------------------------------------------------------
1 | {% extends "page-layout.html" %}
2 | {% block content %}
3 |
4 |
Jekit Error
5 |
6 |
7 | Couldn’t authenticate with Github, sorry: {{reason}}
8 |
9 |
10 | {% endblock %}
11 |
--------------------------------------------------------------------------------
/chef/roles.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ubuntu 12.04",
3 | "ruby": "2.0.0-p353",
4 | "run_list":
5 | [
6 | "github-pages",
7 | "flask", "requests",
8 | "directories",
9 | "apache", "gunicorn"
10 | ],
11 | "user": "ubuntu"
12 | }
13 |
--------------------------------------------------------------------------------
/chef/github-pages/recipes/default.rb:
--------------------------------------------------------------------------------
1 | package 'ruby1.9.3'
2 |
3 | gem_package 'github-pages' do
4 | gem_binary '/usr/bin/gem1.9.3'
5 | options "--no-rdoc --no-ri"
6 | action :install
7 | end
8 |
9 | # Jekyll wants a JS runtime
10 | package 'nodejs'
11 |
--------------------------------------------------------------------------------
/templates/no-such-repo.html:
--------------------------------------------------------------------------------
1 | {% extends "page-layout.html" %}
2 | {% block content %}
3 |
4 | Jekit Error
5 |
6 |
7 | Couldn’t find {{account}}/{{repo}} .
8 | Does it exist?
9 |
10 |
11 | {% endblock %}
12 |
--------------------------------------------------------------------------------
/templates/error-runtime.html:
--------------------------------------------------------------------------------
1 | {% extends "page-layout.html" %}
2 | {% block content %}
3 |
4 | Jekit Error
5 |
6 |
7 | Something went wrong building the preview. Here’s what I know:
8 |
9 |
10 | {{error}}
11 |
12 | {% endblock %}
13 |
--------------------------------------------------------------------------------
/templates/no-such-ref.html:
--------------------------------------------------------------------------------
1 | {% extends "page-layout.html" %}
2 | {% block content %}
3 |
4 | Jekit Error
5 |
6 |
7 | Couldn’t find {{ref}} under
8 | {{account}}/{{repo}} .
9 | Does it exist?
10 |
11 |
12 | {% endblock %}
13 |
--------------------------------------------------------------------------------
/chef/requests/recipes/default.rb:
--------------------------------------------------------------------------------
1 | package 'python-pip'
2 |
3 | bash 'install python-requests' do
4 | not_if 'python -c "from requests import get"'
5 | code 'pip install requests'
6 | end
7 |
8 | bash 'install python-requests-oauthlib' do
9 | not_if 'python -c "from requests_oauthlib import OAuth2Session"'
10 | code 'pip install requests-oauthlib'
11 | end
12 |
--------------------------------------------------------------------------------
/install.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -ex
2 | #
3 | # Install the chef ruby gem if chef-solo is not in the path.
4 | # This script is safe to run multiple times.
5 | #
6 | if [ ! `which chef-solo` ]; then
7 | apt-get install -y ruby1.9.3 build-essential
8 | gem1.9.3 install chef ohai --no-rdoc --no-ri
9 | fi
10 |
11 | BASE=`dirname $0`
12 | PREFIX="${BASE:0:1}"
13 |
14 | if [ $PREFIX != '/' ]; then
15 | BASE="`pwd`/${BASE}"
16 | fi
17 |
18 | chef-solo -c $BASE/chef/solo.rb \
19 | -j $BASE/chef/roles.json
20 |
--------------------------------------------------------------------------------
/templates/error-404.html:
--------------------------------------------------------------------------------
1 | {% extends "page-layout.html" %}
2 | {% block content %}
3 |
4 | Jekit Error
5 |
6 |
7 | Couldn’t find {{path}} under
8 | {{account}}/{{repo}}/{{ref}} .
9 |
10 | Maybe it exists ,
11 | but Jekyll put it at some other location ?
12 |
13 |
14 | {% endblock %}
15 |
--------------------------------------------------------------------------------
/askpass.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | #
3 | # Short & sweet script for use with git clone and fetch credentials.
4 | # Requires GIT_USERNAME and GIT_PASSWORD environment variables,
5 | # intended to be called by Git via GIT_ASKPASS.
6 | #
7 |
8 | from sys import argv
9 | from os import environ
10 |
11 | if argv[1] == "Username for 'https://github.com': ":
12 | print environ['GIT_USERNAME']
13 | exit()
14 |
15 | if argv[1] == "Password for 'https://%(GIT_USERNAME)s@github.com': " % environ:
16 | print environ['GIT_PASSWORD']
17 | exit()
18 |
19 | exit(1)
20 |
--------------------------------------------------------------------------------
/scripts/bookmarklet.js:
--------------------------------------------------------------------------------
1 | var href = location.href,
2 | url = false;
3 |
4 | if(href.match(/^https:\/\/github.com\/.+\/.+\/(edit|blob)\//))
5 | {
6 | if(href.match(/^.+\/_posts\/....-..-..-.+$/))
7 | {
8 | var url = href.replace(/^.+\/(.+\/.+)\/(edit|blob)\/(.+)\/_posts\/(....)-(..)-(..)-(.+)$/,
9 | 'http://host:port/$1/$3/$4/$5/$6/$7');
10 | }
11 | else
12 | {
13 | var url = href.replace(/^.+\/(.+\/.+)\/(edit|blob)\/([^\/]+\/.+)$/,
14 | 'http://host:port/$1/$3');
15 | }
16 | }
17 | else if(href.match(/^http:\/\/prose.io\/#.+\/.+\/edit\//))
18 | {
19 | if(href.match(/^.+\/_posts\/....-..-..-.+$/))
20 | {
21 | var url = href.replace(/^.+\/#(.+\/.+)\/edit\/(.+)\/_posts\/(....)-(..)-(..)-(.+)$/,
22 | 'http://host:port/$1/$2/$3/$4/$5/$6');
23 | }
24 | else
25 | {
26 | var url = href.replace(/^.+\/#(.+\/.+)\/edit\/([^\/]+\/.+)$/,
27 | 'http://host:port/$1/$2');
28 | }
29 | }
30 |
31 | if(url)
32 | {
33 | if(url && url.match(/\.(md|markdown)$/)) {
34 | url = url.replace(/\.(md|markdown)$/, '.html');
35 | }
36 |
37 | window.open(url);
38 | }
39 | else
40 | {
41 | alert("Jekit doesn't understand this URL:\n" + href);
42 | }
43 |
--------------------------------------------------------------------------------
/jekyll.py:
--------------------------------------------------------------------------------
1 | from os.path import join, exists
2 | from logging import getLogger
3 | from shutil import copyfile
4 |
5 | from util import run_cmd, touch, is_fresh, locked_file
6 |
7 | jlogger = getLogger('jekit')
8 |
9 | def jekyll_build(checkout_path):
10 | '''
11 | '''
12 | checkout_lock = checkout_path + '.jekyll-lock'
13 | jekyll_path = join(checkout_path, '_site')
14 | built_hash_file = checkout_path + '.built-hash'
15 | hash_file = checkout_path + '.commit-hash'
16 |
17 | if exists(jekyll_path) and is_fresh(jekyll_path):
18 | return jekyll_path
19 |
20 | with locked_file(checkout_lock):
21 | do_build = True
22 |
23 | if exists(built_hash_file):
24 | built_hash = open(built_hash_file).read().strip()
25 | commit_hash = open(hash_file).read().strip()
26 |
27 | if built_hash == commit_hash:
28 | jlogger.debug('Skipping build to ' + jekyll_path)
29 | do_build = False
30 |
31 | if do_build:
32 | jlogger.info('Building jekyll ' + jekyll_path)
33 | run_cmd(('jekyll', 'build'), checkout_path)
34 |
35 | if exists(hash_file):
36 | copyfile(hash_file, built_hash_file)
37 |
38 | touch(jekyll_path)
39 |
40 | return jekyll_path
41 |
--------------------------------------------------------------------------------
/chef/directories/recipes/default.rb:
--------------------------------------------------------------------------------
1 | link '/opt/git-jekyll-preview' do
2 | to File.absolute_path(File.join(File.dirname(__FILE__), '..', '..', '..'))
3 | end
4 |
5 | directory '/opt/git-jekyll-preview/repos' do
6 | mode 0777
7 | action :create
8 | end
9 |
10 | directory '/opt/git-jekyll-preview/checkouts' do
11 | mode 0777
12 | action :create
13 | end
14 |
15 | directory '/var/log/jekit' do
16 | owner 'www-data'
17 | group 'www-data'
18 | mode '0775'
19 | action :create
20 | end
21 |
22 | file '/var/log/jekit/cull-dirs.log' do
23 | owner 'www-data'
24 | group 'www-data'
25 | mode '0644'
26 | action :create
27 | end
28 |
29 | file '/etc/cron.hourly/cull-dirs' do
30 | owner 'root'
31 | group 'root'
32 | mode '0755'
33 | action :create
34 | content <<-cron
35 | #!/bin/sh
36 | cd /opt/git-jekyll-preview
37 | sudo -u www-data python cull-dirs.py checkouts repos >> /var/log/jekit/cull-dirs.log
38 | cron
39 | end
40 |
41 | file '/etc/logrotate.d/cull-dirs' do
42 | owner 'root'
43 | group 'root'
44 | mode '0755'
45 | action :create
46 | content <<-conf
47 | /var/log/jekit/cull-dirs.log
48 | {
49 | copytruncate
50 | rotate 4
51 | weekly
52 | missingok
53 | notifempty
54 | compress
55 | delaycompress
56 | endscript
57 | }
58 | conf
59 | end
60 |
--------------------------------------------------------------------------------
/cull-dirs.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | from __future__ import division
3 | from sys import argv
4 | from os.path import join, isdir
5 | from os import statvfs, listdir, stat, remove
6 | from shutil import rmtree
7 |
8 | if __name__ == '__main__':
9 |
10 | checkouts, repos = argv[1:]
11 |
12 | # Look at checkouts and repos in turn.
13 | for dirname in (checkouts, repos):
14 |
15 | # Calculate free space
16 | info = statvfs(dirname)
17 | free = info.f_bavail / info.f_blocks
18 |
19 | # Skip if free space is over 20%
20 | if free > .2:
21 | continue
22 |
23 | # Order directory contents by age, oldest-first
24 | paths = [join(dirname, name) for name in listdir(dirname)]
25 | times = [stat(path).st_mtime for path in paths]
26 | infos = zip(times, paths)
27 | infos.sort()
28 |
29 | if dirname == checkouts:
30 | # Remove twenty-five checkouts at a time
31 | removals = infos[:25]
32 |
33 | elif dirname == repos:
34 | # Remove one repo at a time
35 | removals = infos[:1]
36 |
37 | # Delete things
38 | for (time, path) in removals:
39 | print 'Removing', path
40 |
41 | if isdir(path):
42 | rmtree(path)
43 | else:
44 | remove(path)
45 |
--------------------------------------------------------------------------------
/chef/apache/recipes/apache-jekit.conf:
--------------------------------------------------------------------------------
1 |
2 | ServerAdmin webmaster@localhost
3 |
4 | DocumentRoot /var/www
5 |
6 |
7 | Options FollowSymLinks
8 | AllowOverride None
9 |
10 |
11 |
12 | Options Indexes FollowSymLinks MultiViews
13 | AllowOverride None
14 | Order allow,deny
15 | allow from all
16 |
17 |
18 | ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/
19 |
20 |
21 | AllowOverride None
22 | Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch
23 | Order allow,deny
24 | Allow from all
25 |
26 |
27 | ErrorLog ${APACHE_LOG_DIR}/error.log
28 |
29 | # Possible values include: debug, info, notice, warn, error, crit,
30 | # alert, emerg.
31 | LogLevel warn
32 |
33 | CustomLog ${APACHE_LOG_DIR}/access.log combined
34 |
35 | Alias /doc/ "/usr/share/doc/"
36 |
37 |
38 | Options Indexes MultiViews FollowSymLinks
39 | AllowOverride None
40 | Order deny,allow
41 | Deny from all
42 | Allow from 127.0.0.0/255.0.0.0 ::1/128
43 |
44 |
45 |
46 | ProxyPass http://127.0.0.1:8080/
47 | ProxyPassReverse http://127.0.0.1:8080/
48 |
49 |
50 |
51 | Allow from all
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Git-Jekyll Preview
2 | ==================
3 |
4 | Preview your Github-hosted websites before making them live. Use it to check
5 | your plain static or [Jekyll](http://jekyllrb.com/)-generated websites before
6 | you make them live to [Github Pages](http://pages.github.com/) or to your own
7 | server.
8 |
9 | Try it live at [jekit.codeforamerica.org](http://jekit.codeforamerica.org).
10 |
11 | Status, Contact
12 | ---------------
13 |
14 | Git-Jekyll Preview is mostly a singleton-app, built only to be run at a single
15 | location. For the time being, it's not intended for general redeployment but
16 | improvements for [jekit.codeforamerica.org](http://jekit.codeforamerica.org)
17 | are welcomed.
18 |
19 | [Michal Migurski](https://github.com/migurski) is currently maintainer.
20 |
21 | Install
22 | -------
23 |
24 | Git-Jekyll Preview is intended to be run on its own Ubuntu server, and will
25 | not currently work on a managed system like Heroku. Installation dependencies
26 | are managed by [Chef](https://wiki.opscode.com/display/chef/Home). It should
27 | be possible to install Chef and run all required recipes with the script
28 | [install.sh](install.sh). Note the world-writeable directories created in
29 | [chef/directories](chef/directories/recipes/default.rb).
30 |
31 | The application is a [Flask](http://flask.pocoo.org)-based Python server which
32 | shells out to [Git](https://www.kernel.org/pub/software/scm/git/docs/) for
33 | interaction with Github. [OAuth](http://developer.github.com/v3/oauth/) is
34 | used for authentication; put your client ID and secret in [git.py](git.py).
35 |
36 | To run for testing:
37 |
38 | python make-it-so.py
39 |
40 | To run in production, with [Gunicorn](http://gunicorn.org):
41 |
42 | gunicorn make-it-so:app
43 |
--------------------------------------------------------------------------------
/chef/gunicorn/recipes/default.rb:
--------------------------------------------------------------------------------
1 | package 'python-pip'
2 |
3 | bash 'install python-gunicorn' do
4 | not_if 'python -c "import gunicorn"'
5 | code 'pip install gunicorn'
6 | end
7 |
8 | directory '/var/log/jekit' do
9 | owner 'www-data'
10 | group 'www-data'
11 | mode '0775'
12 | action :create
13 | end
14 |
15 | file '/etc/init/jekit.conf' do
16 | owner 'root'
17 | group 'root'
18 | mode '0755'
19 | action :create
20 | content <<-conf
21 | description "jekit"
22 |
23 | start on (filesystem)
24 | stop on runlevel [016]
25 |
26 | respawn
27 | console log
28 | setuid www-data
29 | setgid www-data
30 | chdir /opt/git-jekyll-preview
31 |
32 | exec /usr/local/bin/gunicorn -b 0.0.0.0:8080 -w 4 -t 300 -e app-logfile=/var/log/jekit/app.log --access-logfile /var/log/jekit/access.log make-it-so:app
33 | conf
34 | end
35 |
36 | file '/etc/logrotate.d/jekit-access' do
37 | owner 'root'
38 | group 'root'
39 | mode '0755'
40 | action :create
41 | content <<-conf
42 | /var/log/jekit/access.log
43 | {
44 | copytruncate
45 | rotate 4
46 | weekly
47 | missingok
48 | notifempty
49 | compress
50 | delaycompress
51 | endscript
52 | }
53 | conf
54 | end
55 |
56 | file '/etc/logrotate.d/jekit-app' do
57 | owner 'root'
58 | group 'root'
59 | mode '0755'
60 | action :create
61 | content <<-conf
62 | /var/log/jekit/app.log
63 | {
64 | copytruncate
65 | rotate 4
66 | weekly
67 | missingok
68 | notifempty
69 | compress
70 | delaycompress
71 | endscript
72 | }
73 | conf
74 | end
75 |
76 | #
77 | # Make it go.
78 | #
79 | execute "stop jekit" do
80 | returns [0, 1]
81 | end
82 |
83 | execute "start jekit"
84 |
--------------------------------------------------------------------------------
/chef/apache/recipes/default.rb:
--------------------------------------------------------------------------------
1 | package 'apache2'
2 |
3 | link '/etc/apache2/mods-enabled/proxy.conf' do
4 | to '/etc/apache2/mods-available/proxy.conf'
5 | action :create
6 | end
7 |
8 | link '/etc/apache2/mods-enabled/proxy.load' do
9 | to '/etc/apache2/mods-available/proxy.load'
10 | action :create
11 | end
12 |
13 | link '/etc/apache2/mods-enabled/ssl.conf' do
14 | to '/etc/apache2/mods-available/ssl.conf'
15 | action :create
16 | end
17 |
18 | link '/etc/apache2/mods-enabled/ssl.load' do
19 | to '/etc/apache2/mods-available/ssl.load'
20 | action :create
21 | end
22 |
23 | link '/etc/apache2/mods-enabled/proxy_http.load' do
24 | to '/etc/apache2/mods-available/proxy_http.load'
25 | action :create
26 | end
27 |
28 | link '/etc/apache2/mods-enabled/headers.load' do
29 | to '/etc/apache2/mods-available/headers.load'
30 | action :create
31 | end
32 |
33 | file '/etc/apache2/sites-available/jekit' do
34 | owner 'root'
35 | group 'root'
36 | mode '0755'
37 | action :create
38 | content File.open(File.dirname(__FILE__) + "/apache-jekit.conf").read()
39 | end
40 |
41 | link '/etc/apache2/sites-enabled/000-default' do
42 | action :delete
43 | end
44 |
45 | link '/etc/apache2/sites-enabled/jekit' do
46 | to '/etc/apache2/sites-available/jekit'
47 | action :create
48 | end
49 |
50 | link '/etc/apache2/sites-enabled/default-ssl' do
51 | to '/etc/apache2/sites-available/default-ssl'
52 | action :delete
53 | end
54 |
55 | file '/etc/apache2/sites-available/jekit-ssl' do
56 | owner 'root'
57 | group 'root'
58 | mode '0755'
59 | action :create
60 | content File.open(File.dirname(__FILE__) + "/apache-ssl.conf").read()
61 | end
62 |
63 | file '/etc/ssl/certs/cfa-2014-ssl.crt' do
64 | owner 'root'
65 | group 'root'
66 | mode '0644'
67 | action :create
68 | end
69 |
70 | file '/etc/ssl/private/cfa-2014-ssl.key' do
71 | owner 'root'
72 | group 'root'
73 | mode '0644'
74 | action :create
75 | end
76 |
77 | link '/etc/apache2/sites-enabled/jekit-ssl' do
78 | to '/etc/apache2/sites-available/jekit-ssl'
79 | action :create
80 | end
81 |
82 | #
83 | # Make it go.
84 | #
85 | execute "apache2ctl restart"
86 |
--------------------------------------------------------------------------------
/templates/page-layout.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Jekit
6 |
7 |
8 |
9 |
10 |
13 |
16 |
17 |
18 |
47 |
48 |
49 |
50 |
51 |
52 |
55 |
56 |
57 |
58 |
59 |
60 | {% block content %}{% endblock %}
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/href.py:
--------------------------------------------------------------------------------
1 | from urlparse import urlparse
2 | from re import match
3 |
4 | def get_redirect(req_path, ref_url):
5 | '''
6 | >>> get_redirect('/style.css', 'http://preview.local/foo/bar/baz/')
7 | '/foo/bar/baz/style.css'
8 |
9 | >>> get_redirect('/style.css', 'http://preview.local/foo/bar/baz/quux.html')
10 | '/foo/bar/baz/style.css'
11 |
12 | >>> get_redirect('/quux/style.css', 'http://preview.local/foo/bar/baz/')
13 | '/foo/bar/baz/quux/style.css'
14 | '''
15 | _, ref_host, ref_path, _, _, _ = urlparse(ref_url)
16 | ref_git_preamble_match = match(r'((/[^/]+){3})', ref_path)
17 |
18 | return ref_git_preamble_match.group(1) + req_path
19 |
20 | def needs_redirect(req_host, req_path, ref_url):
21 | '''
22 | Don't redirect when the request and referer hosts don't match:
23 | >>> needs_redirect('preview.local', '/style.css', 'http://example.com/foo/bar/baz/')
24 | False
25 |
26 | Don't redirect when the referer doesn't appear to include a git path.
27 | >>> needs_redirect('preview.local', '/style.css', 'http://preview.local/about/')
28 | False
29 |
30 | Don't redirect when the request path already includes the git preamble.
31 | >>> needs_redirect('preview.local', '/foo/bar/baz/style.css', 'http://preview.local/foo/bar/baz/')
32 | False
33 |
34 | >>> needs_redirect('preview.local', '/', 'http://preview.local/foo/bar/baz/')
35 | True
36 |
37 | >>> needs_redirect('preview.local', '/style.css', 'http://preview.local/foo/bar/baz/')
38 | True
39 |
40 | >>> needs_redirect('preview.local', '/fee/fi/fo/fum/style.css', 'http://preview.local/foo/bar/baz/')
41 | True
42 | '''
43 | _, ref_host, ref_path, _, _, _ = urlparse(ref_url)
44 |
45 | #
46 | # Don't redirect when the request and referer hosts don't match.
47 | #
48 | if req_host != ref_host:
49 | return False
50 |
51 | ref_git_preamble_match = match(r'(/([^/]+/){3})', ref_path)
52 |
53 | #
54 | # Don't redirect when the referer doesn't appear to include a git path.
55 | #
56 | if not ref_git_preamble_match:
57 | return False
58 |
59 | #
60 | # Don't redirect when the request path already includes the git preamble.
61 | #
62 | if req_path.startswith(ref_git_preamble_match.group(1)):
63 | return False
64 |
65 | return True
66 |
67 | if __name__ == '__main__':
68 | import doctest
69 | doctest.testmod()
70 |
--------------------------------------------------------------------------------
/util.py:
--------------------------------------------------------------------------------
1 | from os import utime, stat, listdir
2 | from fcntl import flock, LOCK_EX, LOCK_UN
3 | from contextlib import contextmanager
4 | from subprocess import Popen, PIPE
5 | from mimetypes import guess_type
6 | from traceback import format_exc
7 | from logging import getLogger
8 | from functools import wraps
9 | from os.path import join
10 | from time import time
11 |
12 | from flask import Response
13 |
14 | jlogger = getLogger('jekit')
15 |
16 | @contextmanager
17 | def locked_file(path):
18 | ''' Create a file, lock it, then unlock it. Use as a context manager.
19 |
20 | Yields nothing.
21 | '''
22 | jlogger.debug('Locking ' + path)
23 |
24 | try:
25 | file = open(path, 'a')
26 | flock(file, LOCK_EX)
27 |
28 | yield
29 |
30 | finally:
31 | jlogger.debug('Unlocking ' + path)
32 | flock(file, LOCK_UN)
33 |
34 | def is_fresh(path):
35 | ''' Return true if path is younger than 10 seconds.
36 | '''
37 | return stat(path).st_mtime > time() - 10
38 |
39 | def touch(path):
40 | ''' Touch the path to bring its modified timestamp to now.
41 | '''
42 | jlogger.debug('Touching ' + path)
43 | utime(path, None)
44 |
45 | def run_cmd(args, cwd=None):
46 | ''' Runs a single command in a new process, returns its stdout.
47 | '''
48 | command = Popen(args, stdout=PIPE, stderr=PIPE, cwd=cwd)
49 | command.wait()
50 |
51 | if command.returncode != 0:
52 | raise RuntimeError(command.stderr.read())
53 |
54 | return command.stdout.read()
55 |
56 | def get_file_response(path):
57 | ''' Return a flask Response for a simple file.
58 | '''
59 | mimetype, encoding = guess_type(path)
60 |
61 | with open(path) as file:
62 | return Response(file.read(), headers={'Content-Type': mimetype, 'Cache-Control': 'no-store private'})
63 |
64 | def get_directory_response(path):
65 | ''' Return a flask Response for a directory listing.
66 | '''
67 | names = sorted(listdir(path))
68 |
69 | if 'index.html' in names:
70 | return get_file_response(join(path, 'index.html'))
71 |
72 | items = ['%s ' % (n, n) for n in names]
73 | html = ''
74 |
75 | return Response(html, headers={'Content-Type': 'text/html', 'Cache-Control': 'no-store private'})
76 |
77 | def errors_logged(route_function):
78 | '''
79 | '''
80 | @wraps(route_function)
81 | def wrapper(*args, **kwargs):
82 | try:
83 | result = route_function(*args, **kwargs)
84 | except Exception, e:
85 | jlogger.error(format_exc())
86 | return Response('Nope.', headers={'Content-Type': 'text/plain'}, status=500)
87 | else:
88 | return result
89 |
90 | return wrapper
--------------------------------------------------------------------------------
/templates/index.html:
--------------------------------------------------------------------------------
1 | {% extends "page-layout.html" %}
2 | {% block content %}
3 |
4 | Jekit
5 |
6 | Preview your Github-hosted websites before making them live. Use it to
7 | check your plain static or Jekyll -generated
8 | websites before you make them live to Github Pages
9 | or to your own server.
10 |
11 |
12 | Written by Michal Migurski
13 | with Mick Thompson ,
14 | © 2013-2014 Code for America .
15 |
16 |
17 | You can view the source code for Jekit on GitHub at https://github.com/codeforamerica/git-jekyll-preview .
18 |
19 | Check it, before you wreck it.
20 |
21 |
22 |
23 | Preview pages in Jekit by adding your Github account name and repository
24 | name to a Jekit URL:
25 |
26 |
27 | {{request.host}}/<account>/<repo>/
28 |
29 |
30 | Example showing the Beyond Transparency book site :
31 | {{request.host}}/codeforamerica/beyondtransparency/
32 |
33 |
34 |
35 |
36 | To view a commit or branch other than master , add the ref to the
37 | end:
38 |
39 |
40 | {{request.host}}/<account>/<repo>/<ref>/
41 |
42 |
43 | Example showing a single Beyond Transparency commit :
44 | {{request.host}}/codeforamerica/beyondtransparency/08463604/
45 |
46 |
47 |
48 |
49 | To view a specific file or directory, add it after the ref:
50 |
51 |
52 | {{request.host}}/<account>/<repo>/<ref>/wow/such-path.html
53 |
54 |
55 | Example of the Beyond Transparency preface :
56 | {{request.host}}/codeforamerica/beyondtransparency/master/chapters/preface/
57 |
58 |
59 |
60 | Private Repositories
61 |
62 | Jekit will ask for access to your Github account to preview private
63 | repositories. You can revoke Jekit’s access via your
64 | Github Applications page .
65 | Jekit will keep a copy of each private repository you preview for an
66 | unknown period of time.
67 |
68 | {% if id %}
69 |
76 | {% endif %}
77 | Bookmarklet
78 |
79 | From Github
80 | or Prose.io ,
81 | drag this to your bookmarks bar to install
82 | the bookmarklet .
83 |
84 |
85 | Preview on {{request.host}}
86 |
87 |
88 | {% endblock %}
89 |
--------------------------------------------------------------------------------
/chef/apache/recipes/apache-ssl.conf:
--------------------------------------------------------------------------------
1 |
2 |
3 | ServerAdmin webmaster@localhost
4 |
5 | DocumentRoot /var/www
6 |
7 |
8 | Options FollowSymLinks
9 | AllowOverride None
10 |
11 |
12 |
13 | Options Indexes FollowSymLinks MultiViews
14 | AllowOverride None
15 | Order allow,deny
16 | allow from all
17 |
18 |
19 | ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/
20 |
21 |
22 | AllowOverride None
23 | Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch
24 | Order allow,deny
25 | Allow from all
26 |
27 |
28 |
29 | ProxyPass http://127.0.0.1:8080/
30 | ProxyPassReverse http://127.0.0.1:8080/
31 |
32 |
33 |
34 | Allow from all
35 |
36 |
37 | # Apache ProxyPassReverse doesn't seem to understand how to
38 | # rewrite headers to the correct URL schema, so we do it here.
39 | Header edit Location ^http://jekit.(.*)$ https://jekit.$1
40 |
41 | ErrorLog ${APACHE_LOG_DIR}/error.log
42 |
43 | # Possible values include: debug, info, notice, warn, error, crit,
44 | # alert, emerg.
45 | LogLevel warn
46 |
47 | CustomLog ${APACHE_LOG_DIR}/ssl_access.log combined
48 |
49 | # SSL Engine Switch:
50 | # Enable/Disable SSL for this virtual host.
51 | SSLEngine on
52 |
53 | # A self-signed (snakeoil) certificate can be created by installing
54 | # the ssl-cert package. See
55 | # /usr/share/doc/apache2.2-common/README.Debian.gz for more info.
56 | # If both key and certificate are stored in the same file, only the
57 | # SSLCertificateFile directive is needed.
58 | SSLCertificateFile /etc/ssl/certs/cfa-2014-ssl.crt
59 | SSLCertificateKeyFile /etc/ssl/private/cfa-2014-ssl.key
60 |
61 |
62 | SSLOptions +StdEnvVars
63 |
64 |
65 | SSLOptions +StdEnvVars
66 |
67 |
68 | # SSL Protocol Adjustments:
69 | # The safe and default but still SSL/TLS standard compliant shutdown
70 | # approach is that mod_ssl sends the close notify alert but doesn't wait for
71 | # the close notify alert from client. When you need a different shutdown
72 | # approach you can use one of the following variables:
73 | # o ssl-unclean-shutdown:
74 | # This forces an unclean shutdown when the connection is closed, i.e. no
75 | # SSL close notify alert is send or allowed to received. This violates
76 | # the SSL/TLS standard but is needed for some brain-dead browsers. Use
77 | # this when you receive I/O errors because of the standard approach where
78 | # mod_ssl sends the close notify alert.
79 | # o ssl-accurate-shutdown:
80 | # This forces an accurate shutdown when the connection is closed, i.e. a
81 | # SSL close notify alert is send and mod_ssl waits for the close notify
82 | # alert of the client. This is 100% SSL/TLS standard compliant, but in
83 | # practice often causes hanging connections with brain-dead browsers. Use
84 | # this only for browsers where you know that their SSL implementation
85 | # works correctly.
86 | # Notice: Most problems of broken clients are also related to the HTTP
87 | # keep-alive facility, so you usually additionally want to disable
88 | # keep-alive for those clients, too. Use variable "nokeepalive" for this.
89 | # Similarly, one has to force some clients to use HTTP/1.0 to workaround
90 | # their broken HTTP/1.1 implementation. Use variables "downgrade-1.0" and
91 | # "force-response-1.0" for this.
92 | BrowserMatch "MSIE [2-6]" \
93 | nokeepalive ssl-unclean-shutdown \
94 | downgrade-1.0 force-response-1.0
95 | # MSIE 7 and newer should be able to use keepalive
96 | BrowserMatch "MSIE [17-9]" ssl-unclean-shutdown
97 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/git.py:
--------------------------------------------------------------------------------
1 | from os.path import join, exists, dirname
2 | from os import getcwd, mkdir, environ
3 | from logging import getLogger
4 |
5 | from util import locked_file, is_fresh, touch, run_cmd
6 | from requests_oauthlib import OAuth2Session
7 | from requests import get
8 |
9 | github_client_id = r'e62e0d541bb6d0125b62'
10 | github_client_secret = r'1f488407e92a59beb897814e9240b5a06a2020e3'
11 |
12 | jlogger = getLogger('jekit')
13 |
14 | class PrivateRepoException (Exception): pass
15 |
16 | class MissingRepoException (Exception): pass
17 |
18 | class MissingRefException (Exception): pass
19 |
20 | def prepare_git_checkout(account, repo, ref, token):
21 | '''
22 | '''
23 | repo_href = 'https://github.com/%s/%s.git' % (account, repo)
24 | repo_path = join(getcwd(), 'repos/%s-%s' % (account, repo))
25 | repo_refs = 'https://api.github.com/repos/%s/%s/branches' % (account, repo)
26 | repo_sha = 'https://api.github.com/repos/%s/%s/commits/%s' % (account, repo, ref)
27 | checkout_path = join(getcwd(), 'checkouts/%s-%s-%s' % (account, repo, ref))
28 | checkout_lock = checkout_path + '.git-lock'
29 |
30 | if exists(checkout_path) and is_fresh(checkout_path):
31 | return checkout_path
32 |
33 | ref_check = OAuth2Session(github_client_id, token=token).get(repo_refs)
34 |
35 | if ref_check.status_code == 401:
36 | # Github wants authentication.
37 | raise PrivateRepoException()
38 |
39 | elif ref_check.status_code == 404:
40 | # This repository might not exist at all?
41 | raise MissingRepoException()
42 |
43 | branches = dict([(b['name'], b['commit']['sha']) for b in ref_check.json()])
44 | ref_sha = branches.get(ref, None)
45 |
46 | if ref_sha is None:
47 | # The ref is not a branch, but it may be a sha.
48 | sha_check = OAuth2Session(github_client_id, token=token).get(repo_sha)
49 |
50 | if sha_check.status_code == 200:
51 | # The ref must be a sha hash.
52 | ref_sha = sha_check.json()['sha']
53 | else:
54 | # The repository exists, but the branch does not?
55 | raise MissingRefException()
56 |
57 | if token:
58 | jlogger.debug('Adding Github credentials to environment')
59 | environ.update(dict(GIT_ASKPASS=join(dirname(__file__), 'askpass.py')))
60 | environ.update(dict(GIT_USERNAME=token['access_token'], GIT_PASSWORD=''))
61 |
62 | else:
63 | jlogger.debug('Clearing Github credentials from environment')
64 | environ.update(dict(GIT_ASKPASS='', GIT_USERNAME='', GIT_PASSWORD=''))
65 |
66 | with locked_file(checkout_lock):
67 | if not exists(repo_path):
68 | git_clone(repo_href, repo_path)
69 | else:
70 | git_fetch(repo_path, ref, ref_sha)
71 |
72 | git_checkout(repo_path, checkout_path, ref)
73 |
74 | # Make sure these are gone before we return.
75 | environ.update(dict(GIT_ASKPASS='', GIT_USERNAME='', GIT_PASSWORD=''))
76 |
77 | return checkout_path
78 |
79 | def git_clone(href, path):
80 | ''' Clone a git repository from its remote address to a local path.
81 | '''
82 | jlogger.info('Cloning to ' + path)
83 | run_cmd(('git', 'clone', '--mirror', href, path))
84 |
85 | def get_ref_sha(repo_path, ref):
86 | ''' Get the current SHA for a ref in the given repo path.
87 | '''
88 | return run_cmd(('git', 'show', '--pretty=%H', '--summary', ref), repo_path).strip()
89 |
90 | def git_fetch(repo_path, ref, sha):
91 | ''' Run `git fetch` inside a local git repository.
92 | '''
93 | jlogger.info('Fetching in ' + repo_path)
94 |
95 | try:
96 | found_sha = get_ref_sha(repo_path, ref)
97 | except RuntimeError:
98 | #
99 | # Account for a missing ref by performing a complete fetch.
100 | #
101 | jlogger.debug('Complete fetch in '+repo_path)
102 | run_cmd(('git', 'fetch'), repo_path)
103 | found_sha = get_ref_sha(repo_path, ref)
104 |
105 | if sha == found_sha:
106 | jlogger.debug('Skipping fetch in '+repo_path)
107 |
108 | else:
109 | run_cmd(('git', 'fetch'), repo_path)
110 |
111 | touch(repo_path)
112 |
113 | def git_checkout(repo_path, checkout_path, ref):
114 | ''' Check out a git repository to a given reference and path.
115 |
116 | This function is assumed to be run in a lock.
117 | '''
118 | jlogger.info('Checking out to ' + checkout_path)
119 |
120 | if not exists(checkout_path):
121 | mkdir(checkout_path)
122 |
123 | hash_file = checkout_path + '.commit-hash'
124 | commit_hash = get_ref_sha(repo_path, ref)
125 |
126 | do_checkout = True
127 |
128 | if exists(hash_file):
129 | previous_hash = open(hash_file).read().strip()
130 |
131 | if previous_hash == commit_hash:
132 | jlogger.debug('Skipping checkout to '+checkout_path)
133 | do_checkout = False
134 |
135 | if do_checkout:
136 | run_cmd(('git', '--work-tree='+checkout_path, 'checkout', ref, '--', '.'), repo_path)
137 |
138 | touch(checkout_path)
139 |
140 | with open(hash_file, 'w') as file:
141 | print >> file, commit_hash
142 |
--------------------------------------------------------------------------------
/make-it-so.py:
--------------------------------------------------------------------------------
1 | from logging import DEBUG, INFO, getLogger, FileHandler, StreamHandler, Formatter
2 | from os.path import join, isdir, isfile
3 | from traceback import format_exc
4 | from urllib import urlencode
5 | from os import environ
6 | from time import time
7 |
8 | from flask import Flask, redirect, request, make_response, render_template, session
9 |
10 | from requests import post
11 | from requests_oauthlib import OAuth2Session
12 | from git import prepare_git_checkout, PrivateRepoException
13 | from git import MissingRepoException, MissingRefException
14 | from href import needs_redirect, get_redirect
15 | from util import get_directory_response
16 | from util import get_file_response
17 | from util import errors_logged
18 | from jekyll import jekyll_build
19 |
20 | from git import github_client_id, github_client_secret
21 | flask_secret_key = 'poop'
22 |
23 | app = Flask(__name__)
24 | app.secret_key = flask_secret_key
25 |
26 | @app.before_first_request
27 | def adjust_log_level():
28 | getLogger('jekit').setLevel(DEBUG if app.debug else INFO)
29 |
30 | def should_redirect():
31 | ''' Return True if the current flask.request should redirect.
32 | '''
33 | if request.args.get('go') == u'\U0001f44c':
34 | return False
35 |
36 | referer_url = request.headers.get('Referer')
37 |
38 | if not referer_url:
39 | return False
40 |
41 | return needs_redirect(request.host, request.path, referer_url)
42 |
43 | def make_redirect():
44 | ''' Return a flask.redirect for the current flask.request.
45 | '''
46 | referer_url = request.headers.get('Referer')
47 |
48 | other = redirect(get_redirect(request.path, referer_url), 302)
49 | other.headers['Cache-Control'] = 'no-store private'
50 | other.headers['Vary'] = 'Referer'
51 |
52 | return other
53 |
54 | def get_auth():
55 | ''' Get (username, password) tuple from flask.request, or None.
56 | '''
57 | auth = request.authorization
58 |
59 | return (auth.username, auth.password) if auth else None
60 |
61 | def get_token():
62 | ''' Get OAuth token from flask.session, or a fake one guaranteed to fail.
63 | '''
64 | token = dict(token_type='bearer', access_token='')
65 | token.update(session.get('token', {}))
66 |
67 | return token
68 |
69 | def make_401_response():
70 | ''' Create an HTTP 401 Not Authorized response to trigger Github OAuth.
71 |
72 | Start by redirecting the user to Github OAuth authorization page:
73 | http://developer.github.com/v3/oauth/#redirect-users-to-request-github-access
74 | '''
75 | state_id = 'foobar' # fake.
76 | states = session.get('states', {})
77 | states[state_id] = dict(redirect=request.url, created=time())
78 | session['states'] = states
79 |
80 | data = dict(scope='user,repo', client_id=github_client_id, state=state_id)
81 |
82 | auth = redirect('https://github.com/login/oauth/authorize?' + urlencode(data), 302)
83 | auth.headers['Cache-Control'] = 'no-store private'
84 | auth.headers['Vary'] = 'Referer'
85 |
86 | return auth
87 |
88 | def make_404_response(template, vars):
89 | '''
90 | '''
91 | return make_response(render_template(template, **vars), 404)
92 |
93 | def make_500_response(error, traceback):
94 | '''
95 | '''
96 | try:
97 | message = unicode(error)
98 | except UnicodeDecodeError:
99 | message = str(error).decode('latin-1')
100 |
101 | vars = dict(error=message, traceback=traceback)
102 |
103 | return make_response(render_template('error-runtime.html', **vars), 500)
104 |
105 | @app.route('/')
106 | @errors_logged
107 | def hello_world():
108 | if should_redirect():
109 | return make_redirect()
110 |
111 | id = session.get('id', None)
112 |
113 | script = '''
114 | javascript:(
115 | function ()
116 | {
117 | document.getElementsByTagName('head')[0].appendChild(document.createElement('script')).src='http://host:port/bookmarklet.js';
118 | }()
119 | );
120 | '''
121 |
122 | script = script.replace('http', request.scheme)
123 | script = script.replace('host:port', request.host)
124 | script = script.replace(' ', '').replace('\n', '')
125 |
126 | return render_template('index.html', id=id, script=script, request=request)
127 |
128 | @app.route('/.well-known/status')
129 | @errors_logged
130 | def wellknown_status():
131 | if should_redirect():
132 | return make_redirect()
133 |
134 | status = '''
135 | {
136 | "status": "ok",
137 | "updated": %d,
138 | "dependencies": [ ],
139 | "resources": { }
140 | }
141 | ''' % time()
142 |
143 | resp = make_response(status, 200)
144 | resp.headers['Content-Type'] = 'application/json'
145 |
146 | return resp
147 |
148 | @app.route('/bookmarklet.js')
149 | @errors_logged
150 | def bookmarklet_script():
151 | if should_redirect():
152 | return make_redirect()
153 |
154 | js = open('scripts/bookmarklet.js').read()
155 |
156 | script = make_response(js.replace('host:port', request.host), 200)
157 | script.headers['Content-Type'] = 'text/javascript'
158 | script.headers['Cache-Control'] = 'no-store private'
159 |
160 | return script
161 |
162 | @app.route('/oauth/callback')
163 | @errors_logged
164 | def get_oauth_callback():
165 | ''' Handle Github's OAuth callback after a user authorizes.
166 |
167 | http://developer.github.com/v3/oauth/#github-redirects-back-to-your-site
168 | '''
169 | if 'error' in request.args:
170 | return render_template('error-oauth.html', reason="you didn't authorize access to your account.")
171 |
172 | try:
173 | code, state_id = request.args['code'], request.args['state']
174 | except:
175 | return render_template('error-oauth.html', reason='missing code or state in callback.')
176 |
177 | try:
178 | state = session['states'].pop(state_id)
179 | except:
180 | return render_template('error-oauth.html', reason='state "%s" not found?' % state_id)
181 |
182 | #
183 | # Exchange the temporary code for an access token:
184 | # http://developer.github.com/v3/oauth/#parameters-1
185 | #
186 | data = dict(client_id=github_client_id, code=code, client_secret=github_client_secret)
187 | resp = post('https://github.com/login/oauth/access_token', urlencode(data),
188 | headers={'Accept': 'application/json'})
189 | auth = resp.json()
190 |
191 | if 'error' in auth:
192 | return render_template('error-oauth.html', reason='Github said "%(error)s".' % auth)
193 |
194 | elif 'access_token' not in auth:
195 | return render_template('error-oauth.html', reason="missing `access_token`.")
196 |
197 | session['token'] = auth
198 |
199 | #
200 | # Figure out who's here.
201 | #
202 | url = 'https://api.github.com/user'
203 | id = OAuth2Session(github_client_id, token=session['token']).get(url).json()
204 | id = dict(login=id['login'], avatar_url=id['avatar_url'], html_url=id['html_url'])
205 | session['id'] = id
206 |
207 | other = redirect(state['redirect'], 302)
208 | other.headers['Cache-Control'] = 'no-store private'
209 | other.headers['Vary'] = 'Referer'
210 |
211 | return other
212 |
213 | @app.route('/logout', methods=['POST'])
214 | @errors_logged
215 | def logout():
216 | '''
217 | '''
218 | if 'id' in session:
219 | session.pop('id')
220 |
221 | if 'token' in session:
222 | session.pop('token')
223 |
224 | return redirect('/', 302)
225 |
226 | @app.route('//')
227 | @errors_logged
228 | def repo_only(account, repo):
229 | ''' Redirect to "master" on a hunch.
230 | '''
231 | if should_redirect():
232 | return make_redirect()
233 |
234 | return redirect('/%s/%s/master/' % (account, repo), 302)
235 |
236 | @app.route('///')
237 | @errors_logged
238 | def repo_only_slash(account, repo):
239 | ''' Redirect to "master" on a hunch.
240 | '''
241 | if should_redirect():
242 | return make_redirect()
243 |
244 | return redirect('/%s/%s/master/' % (account, repo), 302)
245 |
246 | @app.route('///[')
247 | @errors_logged
248 | def repo_ref(account, repo, ref):
249 | ''' Redirect to add trailing slash.
250 | '''
251 | if should_redirect():
252 | return make_redirect()
253 |
254 | return redirect('/%s/%s/%s/' % (account, repo, ref), 302)
255 |
256 | @app.route('/]//[/')
257 | @errors_logged
258 | def repo_ref_slash(account, repo, ref):
259 | ''' Show repository root directory listing.
260 | '''
261 | if should_redirect():
262 | return make_redirect()
263 |
264 | try:
265 | site_path = jekyll_build(prepare_git_checkout(account, repo, ref, token=get_token()))
266 | except MissingRepoException:
267 | return make_404_response('no-such-repo.html', dict(account=account, repo=repo))
268 | except MissingRefException:
269 | return make_404_response('no-such-ref.html', dict(account=account, repo=repo, ref=ref))
270 | except PrivateRepoException:
271 | return make_401_response()
272 | except RuntimeError, e:
273 | return make_500_response(e, format_exc())
274 |
275 | return get_directory_response(site_path)
276 |
277 | @app.route('/]//[/]')
278 | @errors_logged
279 | def repo_ref_path(account, repo, ref, path):
280 | ''' Show response for a path, whether a file or directory.
281 | '''
282 | if should_redirect():
283 | return make_redirect()
284 |
285 | try:
286 | site_path = jekyll_build(prepare_git_checkout(account, repo, ref, token=get_token()))
287 | except MissingRepoException:
288 | return make_404_response('no-such-repo.html', dict(account=account, repo=repo))
289 | except MissingRefException:
290 | return make_404_response('no-such-ref.html', dict(account=account, repo=repo, ref=ref))
291 | except PrivateRepoException:
292 | return make_401_response()
293 | except RuntimeError, e:
294 | return make_500_response(e, format_exc())
295 |
296 | local_path = join(site_path, path)
297 |
298 | if isfile(local_path) and not isdir(local_path):
299 | return get_file_response(local_path)
300 |
301 | if isdir(local_path) and not path.endswith('/'):
302 | other = redirect('/%s/%s/%s/%s/' % (account, repo, ref, path), 302)
303 | other.headers['Cache-Control'] = 'no-store private'
304 | return other
305 |
306 | if isdir(local_path):
307 | return get_directory_response(local_path)
308 |
309 | kwargs = dict(account=account, repo=repo, ref=ref, path=path)
310 | return make_404_response('error-404.html', kwargs)
311 |
312 | @app.route('/')
313 | @errors_logged
314 | def all_other_paths(path):
315 | '''
316 | '''
317 | if should_redirect():
318 | return make_redirect()
319 |
320 | if environ.get('app-logfile', None):
321 | handler = FileHandler(environ['app-logfile'])
322 | handler.setFormatter(Formatter('%(process)05s %(asctime)s %(levelname)06s: %(message)s'))
323 |
324 | else:
325 | handler = StreamHandler()
326 | handler.setFormatter(Formatter('%(process)05s %(levelname)06s: %(message)s'))
327 |
328 | getLogger('jekit').addHandler(handler)
329 |
330 | if __name__ == '__main__':
331 | app.run('0.0.0.0', debug=True)
332 |
--------------------------------------------------------------------------------