Compare commits

...

58 Commits

Author SHA1 Message Date
Marcin Kulik cc5aaabb33 No need to ignore these files anymore 7 years ago
Marcin Kulik bb9fd9c644 Close tmp in-memory file after usage 7 years ago
Marcin Kulik 18b31747ee Remove old unused code 7 years ago
Marcin Kulik 9966a27800 /cache is a volume 7 years ago
Marcin Kulik a311b91370 Use fork of plugsnag that ignores Phoenix.NotAcceptableError 7 years ago
Marcin Kulik 20baf9e9eb Capture stderr when invoking a2png 7 years ago
Marcin Kulik 48ae3e353d Properly handle porcelain result 7 years ago
Marcin Kulik 07fdf2b7f1 Route png requests to Elixir app and cache it good 7 years ago
Marcin Kulik 1cac231aaa Cache png for 7 days 7 years ago
Marcin Kulik 3b538b1821 Generate PNG with proper theme and scale 7 years ago
Marcin Kulik ce962910b1 Mount /tmp on host 7 years ago
Marcin Kulik a754c0fdc2 503 + Retry-After 7 years ago
Marcin Kulik e0511053a4 Basic PNG generation in Elixir 7 years ago
Marcin Kulik 135720df52 More fine grained COPY in Dockerfile 7 years ago
Marcin Kulik 867509a328 Serve asciicast files from Phoenix 7 years ago
Marcin Kulik 51d6559b27 Don't report Phoenix.NotAcceptableError to Bugsnag 7 years ago
Marcin Kulik 609dc7dbf3 Upgrade plugsnag 7 years ago
Marcin Kulik f74b3edfed Sort deps and applications in mix.exs 7 years ago
Marcin Kulik b1aebceda2 Show asciicast2gif instructions when request for .gif 7 years ago
Marcin Kulik 928a378b64 Fix formatting of faq doc 7 years ago
Marcin Kulik 4cb2ff6750 Remove old doc pages 7 years ago
Marcin Kulik 403871f089 Make running Phoenix in dev mode via docker-compose easier 7 years ago
Marcin Kulik 3c28348b04 Fix indentation 7 years ago
Marcin Kulik 041fb81c7e Use default Phoenix port (4000) 7 years ago
Marcin Kulik e6cbd117bc Port latest installation doc changes to eex 7 years ago
Marcin Kulik 045a26da1f Upgrade phoenix_markdown 7 years ago
Marcin Kulik 0eb704e31d Ignore .nrepl-history 7 years ago
Marcin Kulik faaf28b913 Route docs requests to Phoenix 7 years ago
Marcin Kulik 1e130fb159 Use Ecto compatible DATABASE_URL 7 years ago
Marcin Kulik ce5f5ae778 Don't require ssl connection for postgres 7 years ago
Marcin Kulik fe4d85b48a Update mix.lock after upgrading hex 7 years ago
Marcin Kulik 943154421e Remove default Phoenix page view and template 7 years ago
Marcin Kulik 5b7c33e24f Bigger top margin for h2 7 years ago
Marcin Kulik 8f4a58fb79 Port navbar changes to eex layout 7 years ago
Marcin Kulik 3d63620cc6 Use SECRET_KEY_BASE from env, only default to fixed string in dev env 7 years ago
Marcin Kulik 4d5baf3a49 Upgrade plugsnag 7 years ago
Marcin Kulik acdbf453d5 Use Rails' name for inserted_at column for all models 7 years ago
Marcin Kulik 8c68876d7d Add timestamp fields to Ecto User schema 7 years ago
Marcin Kulik ea869eb59b Fix OpenBSD installation instructions 7 years ago
Marcin Kulik 65d1272279 Fix example of setting record command in cfg file in FAQ 7 years ago
Marcin Kulik 0a55e6d6c5 Add OpenBSD installation instructions 7 years ago
Marcin Kulik 0d9cfb4509 Update ArchLinux installation instructions 7 years ago
Marcin Kulik 447839d0a4 Fix test 7 years ago
Marcin Kulik 5e3b8cc4c9 Send static files gzipped 7 years ago
Marcin Kulik c76502a707 Add template for 404 7 years ago
Marcin Kulik e37a9f3702 Report errors to Bugsnag via Plugsnag 7 years ago
Marcin Kulik b3657e6058 `secure` is not used by PlugRailsCookieSessionStore 7 years ago
Marcin Kulik fed8fc8c8c Default Rails salts are safe with long enough secret_key_base 7 years ago
Marcin Kulik 821cef7bf0 Don't enforce SSL on Phoenix level 7 years ago
Marcin Kulik 1c7bb5e164 Configure secret_key_base and database_url via env vars 7 years ago
Marcin Kulik f640817108 Share session between Rails and Phoenix app 7 years ago
Marcin Kulik acf44345eb Port session info in navbar 7 years ago
Marcin Kulik 017a3d683d Port docs 7 years ago
Marcin Kulik f0b7b296c0 Add markdown template engine 7 years ago
Marcin Kulik 57c878136b Use vendored bootstrap 7 years ago
Marcin Kulik 648d927ad7 Ported layout 7 years ago
Marcin Kulik 5eeb278b90 Add Ecto user model 7 years ago
Marcin Kulik 74384b1c39 mix phoenix.new 7 years ago

@ -7,6 +7,7 @@ tmp/*
log/*
target
uploads/*
public/uploads/*
coverage
volumes
Dockerfile

@ -12,7 +12,7 @@ BASE_URL=http://localhost:3000
SECRET_KEY_BASE=
## PostgreSQL connection URL.
## Default: postgresql://postgres/postgres?user=postgres
## Default: postgresql://postgres@postgres/postgres
# DATABASE_URL=
## Redis connection URL.

77
.gitignore vendored

@ -1,42 +1,47 @@
*.rbc
*.sassc
.sass-cache
capybara-*.html
.rspec
## Rails app
/.bundle
/vendor/bundle
/log/*
/.vagrant
/.rspec
/.env
/tmp/*
/db/*.sqlite3
/public/system/*
/coverage/
/spec/tmp/*
**.orig
config/database.yml
rerun.txt
pickle-email-*.html
.jhw-cache/
config/asciinema.yml
public/uploads/*
/log/*
/bin/*
/config/database.yml
/config/asciinema.yml
/db/*.sqlite3
/coverage
.rbx
.env
.env.production
/target
/classes
/checkouts
pom.xml
pom.xml.asc
*.jar
*.class
/.lein-*
/.nrepl-port
/.dir-locals.el
/profiles.clj
/dev/resources/local.edn
/dev/src/local.clj
/public/uploads/*
/uploads/*
## Elixir app
# App artifacts
/_build
/db
/deps
/*.ez
# Generated on crash by the VM
erl_crash.dump
# Static artifacts
/node_modules
# Since we are building assets from web/static,
# we ignore priv/static. You may want to comment
# this depending on your deployment strategy.
/priv/static/
# The config/prod.secret.exs file by default contains sensitive
# data and you should not commit it into version control.
#
# Alternatively, you may comment the line below and commit the
# secrets file as long as you replace its contents by environment
# variables.
/config/prod.secret.exs
## Docker
/volumes
/.m2
.m2
/.env.production

@ -10,11 +10,15 @@ RUN apt-get update && \
apt-get install -y wget software-properties-common apt-transport-https && \
add-apt-repository ppa:brightbox/ruby-ng && \
echo "deb https://deb.nodesource.com/$NODE_VERSION $DISTRO main" >/etc/apt/sources.list.d/nodesource.list && \
echo "deb https://packages.erlang-solutions.com/ubuntu $DISTRO contrib" >/etc/apt/sources.list.d/esl.list && \
wget --quiet -O - https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - && \
wget --quiet -O - https://packages.erlang-solutions.com/ubuntu/erlang_solutions.asc | apt-key add - && \
apt-get update && \
apt-get install -y \
autoconf \
build-essential \
elixir \
esl-erlang \
git-core \
libfontconfig1 \
libpq-dev \
@ -35,9 +39,15 @@ RUN apt-get update && \
# libfontconfig1 for PhantomJS
# ttf-bitstream-vera for a2png
# install Bundler
# install Bundler and SASS
RUN gem install bundler
RUN gem install bundler sass
# install Hex and Rebar
ENV LANG=C.UTF-8
RUN mix local.hex --force && mix local.rebar --force
# install PhantomJS
@ -77,10 +87,13 @@ ARG LEIN_ROOT=yes
# install asciinema
ENV RAILS_ENV "production"
ENV MIX_ENV "prod"
RUN mkdir -p /app/tmp /app/log
WORKDIR /app
# install gems
COPY Gemfile* /app/
RUN bundle install --deployment --without development test --jobs 10 --retry 5
@ -95,30 +108,68 @@ RUN cd a2png && npm install
COPY a2png /app/a2png
RUN cd a2png && lein cljsbuild once main && lein cljsbuild once page
# build uberjar
COPY project.clj /app/
RUN lein deps
# service URLs
COPY src /app/src
COPY resources /app/resources
RUN lein uberjar
# copy the rest of the source code
COPY . /app
ENV DATABASE_URL "postgresql://postgres/postgres?user=postgres"
ENV DATABASE_URL "postgresql://postgres@postgres/postgres"
ENV REDIS_URL "redis://redis:6379"
# compile terminal.c
RUN mkdir -p /app/bin
COPY src/Makefile /app/src/
COPY src/terminal.c /app/src/
RUN cd src && make
# compile assets
# add Ruby source files
COPY config/*.rb /app/config/
COPY config/*.yml /app/config/
COPY config/environments /app/config/environments
COPY config/initializers /app/config/initializers
COPY config/locales /app/config/locales
COPY db /app/db
COPY lib/*.rb /app/lib/
COPY lib/authentication /app/lib/authentication
COPY lib/tasks /app/lib/tasks
COPY public /app/public
COPY vendor /app/vendor
COPY config.ru /app/
COPY Rakefile /app/
COPY app /app/app
# compile assets with assets pipeline
RUN bundle exec rake assets:precompile
# install hex packages
COPY mix.* /app/
RUN mix deps.get --only prod
# install brunch & co
COPY package.json /app/
RUN npm install
# compile assets with brunch and generate digest file
COPY brunch-config.js /app/
COPY web/static /app/web/static
RUN node_modules/brunch/bin/brunch build --production && mix phoenix.digest
# add Elixir source files
COPY config/*.exs /app/config/
COPY lib/*.ex /app/lib
COPY lib/asciinema /app/lib/asciinema
COPY priv/gettext /app/priv/gettext
COPY priv/repo /app/priv/repo
COPY web /app/web
# compile Elixir app
RUN mix compile
# install smtp configuration
COPY docker/asciinema.yml /app/config/asciinema.yml
@ -135,14 +186,10 @@ COPY docker/supervisor/asciinema.conf /etc/supervisor/conf.d/asciinema.conf
# add start script for Clojure app
ENV A2PNG_BIN_PATH "/app/a2png/a2png.sh"
COPY docker/start.sh /app/start.sh
RUN chmod a+x /app/start.sh
VOLUME ["/app/log", "/app/uploads"]
VOLUME ["/app/log", "/app/uploads", "/cache"]
CMD ["/usr/bin/supervisord"]
# bundle exec rake db:setup
# bundle exec sidekiq
EXPOSE 80
EXPOSE 3000

@ -1,10 +0,0 @@
class DocsController < ApplicationController
layout 'docs'
rescue_from ActionView::MissingTemplate, with: :handle_not_found
def show
@current_category = params[:page].to_sym
render params[:page]
end
end

@ -1,2 +0,0 @@
module DocsHelper
end

@ -46,4 +46,4 @@
p Paste the above script tag where you want the player to be displayed on your page.
hr
p See #{link_to 'Sharing & embedding', docs_path(:embedding)} for additional options.
p See #{link_to 'Sharing & embedding', '/docs/embedding'} for additional options.

@ -1,10 +0,0 @@
h2 Docs
= category_links current_category do |categories|
= categories.link_to 'How it works', docs_path('how-it-works'), :'how-it-works'
= categories.link_to 'Getting started', docs_path('getting-started'), :'getting-started'
= categories.link_to 'Installation', docs_path(:installation), :installation
= categories.link_to 'Usage', docs_path(:usage), :usage
= categories.link_to 'Configuration file', docs_path(:config), :config
= categories.link_to 'Sharing & embedding', docs_path(:embedding), :embedding
= categories.link_to 'FAQ', docs_path(:faq), :faq

@ -2,4 +2,4 @@ Install asciinema with:
<%= render partial: "docs/install_homebrew" %>
See other <%= link_to('installation options', docs_path(:installation)) %>.
See other <%= link_to('installation options', '/docs/installation') %>.

@ -1,31 +0,0 @@
<% content_for(:title, 'Getting started') %>
# Getting started
## 1. Install the recorder
<%= render partial: "docs/quick_install" %>
## 2. Record
To start recording run the following command:
asciinema rec
This spawns a new shell instance and records all terminal output.
When you're ready to finish simply exit the shell either by typing `exit` or
hitting <kbd>Ctrl-D</kdb>.
See [usage instructions](<%= docs_path(:usage) %>) to learn about all commands and options.
## 3. Manage your recordings (optional)
If you want to manage your recordings on asciinema.org (set title/description,
delete etc) you need to authenticate. Run the following command and open
displayed URL in your web browser:
asciinema auth
If you skip this step now, you can run the above command later and all
previously recorded asciicasts will automatically get assigned to your
profile.

@ -30,7 +30,7 @@
' [as-kee-nuh-muh]
' is a free and open source solution for recording
terminal sessions and sharing them on the web. Read about
a href=docs_path('how-it-works') how it works
a href="/docs/how-it-works" how it works
' .
section.odd
@ -42,7 +42,7 @@
' Record right where you work - in a terminal.
To start just run <code>asciinema rec</code>, to finish hit <kbd>Ctrl-D</kbd> or type <code>exit</code>.
p
a href=docs_path('getting-started') Get started &raquo;
a href="/docs/getting-started" Get started &raquo;
.col-md-4
h1 Copy & paste
@ -56,7 +56,7 @@
p
' Easily embed an asciicast player in your blog post, project documentation page or in your conference talk slides.
p
a href=docs_path(:embedding) See the embedding docs &raquo;
a href="/docs/embedding" See the embedding docs &raquo;
section.even
.container

@ -13,7 +13,7 @@ header.navbar.navbar-default[role="navigation"]
li
= link_to "Browse", category_path(:featured)
li
= link_to "Docs", docs_index_path
= link_to "Docs", "/docs"
li
= link_to "Blog", 'http://blog.asciinema.org'

@ -1,10 +0,0 @@
- content_for :content do
- cache do
.container
.row
.col-md-3
= render 'docs_topics', current_category: @current_category
.col-md-9
= yield
= render :template => 'layouts/application'

@ -16,7 +16,7 @@ asciinema project is built of several sub-projects:
* javascript player ([source code](https://github.com/asciinema/asciinema-player))
If technical details interest you then read more about
[how it works](<%= docs_path('how-it-works') %>).
[how it works](/docs/how-it-works).
## Contributing

@ -1,5 +1,5 @@
markdown:
Make sure you have asciinema recorder [installed](#{docs_path(:installation)}).
Make sure you have asciinema recorder [installed](/docs/installation).
To start recording run the following command in your terminal:

@ -0,0 +1,83 @@
exports.config = {
// See http://brunch.io/#documentation for docs.
files: {
javascripts: {
joinTo: "js/app.js",
// To use a separate vendor.js bundle, specify two files path
// http://brunch.io/docs/config#-files-
// joinTo: {
// "js/app.js": /^(web\/static\/js)/,
// "js/vendor.js": /^(web\/static\/vendor)|(deps)/
// }
//
// To change the order of concatenation of files, explicitly mention here
order: {
before: [
"web/static/vendor/js/jquery-2.2.4.min.js",
"web/static/vendor/js/bootstrap.js"
]
}
},
stylesheets: {
joinTo: "css/app.css",
order: {
before: [
"web/static/vendor/css/bootstrap.css",
"web/static/css/source-sans-pro.css",
"web/static/css/base.sass",
"web/static/css/header.sass",
"web/static/css/flash.sass",
"web/static/css/footer.sass",
"web/static/css/home.sass",
"web/static/css/asciicasts.sass",
"web/static/css/users.sass",
"web/static/css/preview.sass",
"web/static/css/player.sass",
"web/static/css/contributing.sass",
"web/static/css/simple-layout.sass",
]
}
},
templates: {
joinTo: "js/app.js"
}
},
conventions: {
// This option sets where we should place non-css and non-js assets in.
// By default, we set this to "/web/static/assets". Files in this directory
// will be copied to `paths.public`, which is "priv/static" by default.
assets: /^(web\/static\/assets)/
},
// Phoenix paths configuration
paths: {
// Dependencies and current project directories to watch
watched: [
"web/static",
"test/static"
],
// Where to compile files to
public: "priv/static"
},
// Configure your plugins
plugins: {
babel: {
// Do not use ES6 compiler in vendor code
ignore: [/web\/static\/vendor/]
}
},
modules: {
autoRequire: {
"js/app.js": ["web/static/js/app"]
}
},
npm: {
enabled: true
}
};

@ -14,6 +14,8 @@ module Asciinema
attribute :google_analytics_id, String
attribute :home_asciicast_id, String
attribute :secret_key_base, String
attribute :session_encryption_salt, String, default: 'encrypted cookie'
attribute :session_signing_salt, String, default: 'signed encrypted cookie'
attribute :admin_ids, Array[Integer]
attribute :smtp_settings, Hash
attribute :smtp_from_address, String

@ -0,0 +1,54 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
#
# This configuration file is loaded before any dependency and
# is restricted to this project.
use Mix.Config
# General application configuration
config :asciinema,
ecto_repos: [Asciinema.Repo]
# Configures the endpoint
config :asciinema, Asciinema.Endpoint,
url: [host: "localhost"],
secret_key_base: System.get_env("SECRET_KEY_BASE"),
render_errors: [view: Asciinema.ErrorView, accepts: ~w(html json)],
pubsub: [name: Asciinema.PubSub,
adapter: Phoenix.PubSub.PG2]
# Configures Elixir's Logger
config :logger, :console,
format: "$time $metadata[$level] $message\n",
metadata: [:request_id]
config :phoenix, :template_engines,
md: PhoenixMarkdown.Engine
config :bugsnag, api_key: System.get_env("BUGSNAG_API_KEY")
config :bugsnag, release_stage: Mix.env
if System.get_env("S3_BUCKET") do
config :asciinema, :file_store, Asciinema.FileStore.S3
config :asciinema, Asciinema.FileStore.S3,
region: System.get_env("S3_REGION"),
bucket: System.get_env("S3_BUCKET"),
path: "uploads/"
config :ex_aws,
access_key_id: [{:system, "AWS_ACCESS_KEY_ID"}, :instance_role],
secret_access_key: [{:system, "AWS_SECRET_ACCESS_KEY"}, :instance_role]
else
config :asciinema, :file_store, Asciinema.FileStore.Local
config :asciinema, Asciinema.FileStore.Local, path: "uploads/"
end
config :asciinema, :png_generator, Asciinema.PngGenerator.A2png
config :asciinema, Asciinema.PngGenerator.A2png,
bin_path: System.get_env("A2PNG_BIN_PATH") || "./a2png/a2png.sh",
pool_size: String.to_integer(System.get_env("A2PNG_POOL_SIZE") || "2")
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env}.exs"

@ -0,0 +1,45 @@
use Mix.Config
# For development, we disable any cache and enable
# debugging and code reloading.
#
# The watchers configuration can be used to run external
# watchers to your application. For example, we use it
# with brunch.io to recompile .js and .css sources.
config :asciinema, Asciinema.Endpoint,
http: [port: 4000],
debug_errors: true,
code_reloader: true,
check_origin: false,
secret_key_base: "60BnXnzGGwwiZj91YA9XYKF9BCiM7lQ/1um8VXcWWLSdUp9OcPZV6YnQv7eFTYSY",
watchers: [node: ["node_modules/brunch/bin/brunch", "watch", "--stdin",
cd: Path.expand("../", __DIR__)]]
# Watch static and templates for browser reloading.
config :asciinema, Asciinema.Endpoint,
live_reload: [
patterns: [
~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
~r{priv/gettext/.*(po)$},
~r{web/views/.*(ex)$},
~r{web/templates/.*(eex|md)$}
]
]
# Do not include metadata nor timestamps in development logs
config :logger, :console, format: "[$level] $message\n"
# Set a higher stacktrace during development. Avoid configuring such
# in production as building large stacktraces may be expensive.
config :phoenix, :stacktrace_depth, 20
# Configure your database
config :asciinema, Asciinema.Repo,
adapter: Ecto.Adapters.Postgres,
url: System.get_env("DATABASE_URL"),
username: "postgres",
password: "postgres",
database: "asciinema_development",
hostname: "localhost",
pool_size: 10

@ -1,3 +1,5 @@
# Be sure to restart your server when you modify this file.
Rails.application.config.session_store :cookie_store, key: '_asciinema_session', secure: CFG.ssl?
Rails.application.config.action_dispatch.encrypted_cookie_salt = CFG.session_encryption_salt
Rails.application.config.action_dispatch.encrypted_signed_cookie_salt = CFG.session_signing_salt

@ -0,0 +1,63 @@
use Mix.Config
# For production, we configure the host to read the PORT
# from the system environment. Therefore, you will need
# to set PORT=80 before running your server.
#
# You should also configure the url host to something
# meaningful, we use this information when generating URLs.
#
# Finally, we also include the path to a manifest
# containing the digested version of static files. This
# manifest is generated by the mix phoenix.digest task
# which you typically run after static files are built.
config :asciinema, Asciinema.Endpoint,
http: [port: {:system, "PORT"}],
url: [scheme: "https", host: "asciinema.org", port: 443],
cache_static_manifest: "priv/static/manifest.json"
# Do not print debug messages in production
config :logger, level: :info
# ## SSL Support
#
# To get SSL working, you will need to add the `https` key
# to the previous section and set your `:url` port to 443:
#
# config :asciinema, Asciinema.Endpoint,
# ...
# url: [host: "example.com", port: 443],
# https: [port: 443,
# keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
# certfile: System.get_env("SOME_APP_SSL_CERT_PATH")]
#
# Where those two env variables return an absolute path to
# the key and cert in disk or a relative path inside priv,
# for example "priv/ssl/server.key".
#
# We also recommend setting `force_ssl`, ensuring no data is
# ever sent via http, always redirecting to https:
#
# config :asciinema, Asciinema.Endpoint,
# force_ssl: [hsts: true]
#
# Check `Plug.SSL` for all available options in `force_ssl`.
# ## Using releases
#
# If you are doing OTP releases, you need to instruct Phoenix
# to start the server for all endpoints:
#
# config :phoenix, :serve_endpoints, true
#
# Alternatively, you can configure exactly which server to
# start per endpoint:
#
# config :asciinema, Asciinema.Endpoint, server: true
#
config :asciinema, Asciinema.Repo,
adapter: Ecto.Adapters.Postgres,
url: System.get_env("DATABASE_URL"),
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "20"),
ssl: false

@ -21,9 +21,6 @@ Rails.application.routes.draw do
resources :asciicasts
end
get "/docs" => "docs#show", :page => 'getting-started', :as => :docs_index
get "/docs/:page" => "docs#show", :as => :docs
resource :login, only: [:new, :create] do
get :sent
end

@ -0,0 +1,22 @@
use Mix.Config
# We don't run a server during test. If one is required,
# you can enable the server option below.
config :asciinema, Asciinema.Endpoint,
http: [port: 4001],
server: false
# Print only warnings and errors during test
config :logger, level: :warn
# Configure your database
config :asciinema, Asciinema.Repo,
adapter: Ecto.Adapters.Postgres,
username: "postgres",
password: "postgres",
database: "asciinema_test",
hostname: "localhost",
pool: Ecto.Adapters.SQL.Sandbox
config :asciinema, :file_store, Asciinema.FileStore.Local
config :asciinema, Asciinema.FileStore.Local, path: "uploads/test/"

@ -1,5 +1,5 @@
RAILS_ENV=development
MIX_ENV=dev
BASE_URL=http://localhost:3000
SECRET_KEY_BASE=19c70247f4034dd5ce4f3d6bd3b2b592624b63439d518367de9add564fdee9e6b8513f6cec24c2a933a84ea639136786813bb70d3dc4e84a365205a52e5bf1fa
LEIN_ROOT=yes
BUNDLE_PATH=/bundle

@ -3,6 +3,8 @@ version: '3'
volumes:
bundle:
m2:
node_modules:
build:
services:
postgres:
@ -30,9 +32,12 @@ services:
ports:
- 3000:3000
- 4000:4000
- 44444:44444
volumes:
- ../uploads:/app/uploads
- ../uploads:/app/uploads:cached
- ../deps:/app/deps:cached
- bundle:/bundle:cached
- node_modules:/app/node_modules:cached
- build:/app/_build:cached
- m2:/root/.m2:cached
- ../:/app:cached
- /tmp/asciinema-web:/tmp

@ -1,6 +0,0 @@
{:components
{:mem-expiring-set #var asciinema.component.mem-expiring-set/mem-expiring-set}
:config
{:a2png {:bin-path "a2png/a2png.sh"}}
:dependencies
{:asciicasts {:exp-set :mem-expiring-set}}}

@ -1,33 +0,0 @@
(ns dev
(:refer-clojure :exclude [test])
(:require [clojure.repl :refer :all]
[clojure.pprint :refer [pprint]]
[clojure.tools.namespace.repl :refer [refresh]]
[clojure.java.io :as io]
[com.stuartsierra.component :as component]
[duct.generate :as gen]
[duct.util.repl :refer [setup test cljs-repl migrate rollback]]
[duct.util.system :refer [load-system]]
[environ.core :refer [env]]
[reloaded.repl :refer [system init start stop go reset]]
[asciinema.boundary.file-store :as file-store]
[asciinema.boundary.asciicast-database :as asciicast-database]
[asciinema.component.local-file-store :refer [->LocalFileStore]]
[asciinema.component.s3-file-store :refer [->S3FileStore]]))
(def default-db-uri "jdbc:postgresql://localhost/asciinema_development?user=asciinema")
(defn new-system []
(let [bindings {'http-port (Integer/parseInt (:port env "4000"))
'db-uri (:database-url env default-db-uri)
's3-bucket (:s3-bucket env)
's3-access-key (:s3-access-key env)
's3-secret-key (:s3-secret-key env)}]
(load-system (keep io/resource ["asciinema/system.edn" "dev.edn" "local.edn"]) bindings)))
(when (io/resource "local.clj")
(load "local"))
(gen/set-ns-prefix 'asciinema)
(reloaded.repl/set-init! new-system)

@ -1,8 +0,0 @@
(ns user)
(defn dev
"Load and switch to the 'dev' namespace."
[]
(require 'dev)
(in-ns 'dev)
:loaded)

@ -33,6 +33,7 @@ services:
volumes:
- ./uploads:/app/uploads
- ./log:/app/log
- /tmp/asciinema-web:/tmp
smtp:
image: namshi/smtp
@ -59,3 +60,5 @@ services:
volumes:
- ./uploads:/app/uploads
- ./log:/app/log
- /tmp/asciinema-web:/tmp
- ./volumes/cache:/cache

@ -2,10 +2,13 @@ upstream rails-server {
server 127.0.0.1:3000 fail_timeout=0;
}
upstream clj-server {
upstream phoenix-server {
server 127.0.0.1:4000 fail_timeout=0;
}
proxy_cache_path /cache levels=1:2 keys_zone=png_cache:10m max_size=10g
inactive=14d use_temp_path=off;
server {
listen 80 default_server;
listen [::]:80 default_server;
@ -16,8 +19,28 @@ server {
client_max_body_size 16m;
location ~ ^/a/[^.]+\.(json|png)$ {
try_files $uri $uri/index.html $uri.html @clj;
location ~ ^/(phoenix/|css/|js/|images/|fonts/|docs/?) {
try_files /maintenance.html $uri/index.html $uri.html $uri @phoenix;
}
location ~ ^/a/[^.]+\.png$ {
proxy_cache png_cache;
proxy_cache_revalidate on;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_lock on;
proxy_cache_key $uri;
proxy_ignore_headers Set-Cookie;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_pass http://phoenix-server;
proxy_redirect off;
}
location ~ ^/a/[^.]+\.(json|gif)$ {
try_files $uri $uri/index.html $uri.html @phoenix;
}
location / {
@ -42,12 +65,12 @@ server {
error_page 500 502 504 /500.html; # Rails error pages
}
location @clj {
location @phoenix {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_pass http://clj-server;
proxy_pass http://phoenix-server;
proxy_redirect off;
}
}

@ -1,6 +0,0 @@
#!/usr/bin/env bash
export S3_ACCESS_KEY=${AWS_ACCESS_KEY_ID}
export S3_SECRET_KEY=${AWS_SECRET_ACCESS_KEY}
exec java -server -jar /app/target/uberjar/asciinema-0.1.0-SNAPSHOT-standalone.jar

@ -16,9 +16,10 @@ stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:clj]
command=/app/start.sh
[program:phoenix]
command=mix phoenix.server
directory=/app
environment=PORT=4000
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr

@ -0,0 +1,32 @@
defmodule Asciinema do
use Application
# See http://elixir-lang.org/docs/stable/elixir/Application.html
# for more information on OTP Applications
def start(_type, _args) do
import Supervisor.Spec
# Define workers and child supervisors to be supervised
children = [
# Start the Ecto repository
supervisor(Asciinema.Repo, []),
# Start the endpoint when the application starts
supervisor(Asciinema.Endpoint, []),
# Start your own worker by calling: Asciinema.Worker.start_link(arg1, arg2, arg3)
# worker(Asciinema.Worker, [arg1, arg2, arg3]),
:poolboy.child_spec(:worker, Asciinema.PngGenerator.A2png.poolboy_config(), []),
]
# See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: Asciinema.Supervisor]
Supervisor.start_link(children, opts)
end
# Tell Phoenix to update the endpoint configuration
# whenever the application is updated.
def config_change(changed, _new, removed) do
Asciinema.Endpoint.config_change(changed, removed)
:ok
end
end

@ -0,0 +1,14 @@
defmodule Asciinema.Auth do
alias Asciinema.{Repo, User}
alias Plug.Conn
def init(opts) do
opts
end
def call(conn, _opts) do
user_id = Conn.get_session(conn, "warden.user.user.key")
user = user_id && Repo.get(User, user_id)
Conn.assign(conn, :current_user, user)
end
end

@ -0,0 +1,5 @@
defmodule Crypto do
def md5(data) do
Base.encode16(:erlang.md5(data), case: :lower)
end
end

@ -0,0 +1,47 @@
defmodule Asciinema.Endpoint do
use Phoenix.Endpoint, otp_app: :asciinema
socket "/socket", Asciinema.UserSocket
# Serve at "/" the static files from "priv/static" directory.
#
# You should set gzip to true if you are running phoenix.digest
# when deploying your static files in production.
plug Plug.Static,
at: "/", from: :asciinema, gzip: true,
only: ~w(css fonts images js favicon.ico robots.txt)
# Code reloading can be explicitly enabled under the
# :code_reloader configuration of your endpoint.
if code_reloading? do
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
plug Phoenix.LiveReloader
plug Phoenix.CodeReloader
end
plug Plug.RequestId
plug Plug.Logger
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Poison
plug Plug.MethodOverride
plug Plug.Head
plug Plug.Session,
store: PlugRailsCookieSessionStore,
key: "_asciinema_session",
signing_salt: "signed encrypted cookie",
encrypt: true,
encryption_salt: "encrypted cookie",
key_iterations: 1000,
key_length: 64,
key_digest: :sha,
serializer: Poison
plug Asciinema.TrailingFormat
plug Asciinema.Router
end

@ -0,0 +1,10 @@
defmodule Asciinema.FileStore do
@doc "Serves file at given path in store"
@callback serve_file(conn :: %Plug.Conn{}, path :: String.t, filename :: String.t) :: %Plug.Conn{}
@doc "Opens the given path in store"
@callback open(path :: String.t) :: {:ok, File.io_device} | {:error, File.posix}
@doc "Opens the given path in store, executes given fn and closes the file"
@callback open(path :: String.t, function :: (File.io_device -> res)) :: {:ok, res} | {:error, File.posix} when res: var
end

@ -0,0 +1,36 @@
defmodule Asciinema.FileStore.Local do
@behaviour Asciinema.FileStore
import Plug.Conn
alias Plug.MIME
def serve_file(conn, path, nil) do
do_serve_file(conn, path)
end
def serve_file(conn, path, filename) do
conn
|> put_resp_header("content-disposition", "attachment; filename=#{filename}")
|> do_serve_file(path)
end
defp do_serve_file(conn, path) do
conn
|> put_resp_header("content-type", MIME.path(path))
|> send_file(200, base_path() <> path)
|> halt
end
def open(path) do
File.open(base_path() <> path, [:binary, :read])
end
def open(path, function) do
File.open(base_path() <> path, [:binary, :read], function)
end
defp config do
Application.get_env(:asciinema, Asciinema.FileStore.Local)
end
defp base_path do
Keyword.get(config(), :path)
end
end

@ -0,0 +1,52 @@
defmodule Asciinema.FileStore.S3 do
@behaviour Asciinema.FileStore
import Phoenix.Controller, only: [redirect: 2]
alias ExAws.S3
def serve_file(conn, path, nil) do
do_serve_file(conn, path)
end
def serve_file(conn, path, filename) do
do_serve_file(conn, path, ["response-content-disposition": "attachment; filename=#{filename}"])
end
defp do_serve_file(conn, path, query_params \\ []) do
{:ok, url} =
ExAws.Config.new(:s3, region: region())
|> ExAws.S3.presigned_url(:get, bucket(), base_path() <> path, query_params: query_params)
conn
|> redirect(external: url)
end
def open(path, function \\ nil) do
response = S3.get_object(bucket(), base_path() <> path) |> ExAws.request(region: region())
case response do
{:ok, %{body: body}} ->
if function do
File.open(body, [:ram, :binary, :read], function)
else
File.open(body, [:ram, :binary, :read])
end
{:error, reason} ->
{:error, reason}
end
end
defp config do
Application.get_env(:asciinema, Asciinema.FileStore.S3)
end
defp region do
Keyword.get(config(), :region)
end
defp bucket do
Keyword.get(config(), :bucket)
end
defp base_path do
Keyword.get(config(), :path)
end
end

@ -0,0 +1,14 @@
defmodule Asciinema.Gravatar do
def gravatar_url(email, opts \\ []) do
hash = email
|> String.downcase
|> Crypto.md5
size = Keyword.get(opts, :size, 128)
default = Keyword.get(opts, :default, "retro")
"//gravatar.com/avatar/#{hash}?s=#{size}&d=#{default}"
end
end

@ -0,0 +1,11 @@
defmodule Asciinema.PngGenerator do
alias Asciinema.Asciicast
alias Asciinema.PngGenerator.PngParams
@doc "Generates PNG image from asciicast and returns path to it"
@callback generate(asciicast :: %Asciicast{}, png_params :: %PngParams{}) :: {:ok, String.t} | {:error, term}
def generate(asciicast, png_params) do
Application.get_env(:asciinema, :png_generator).generate(asciicast, png_params)
end
end

@ -0,0 +1,92 @@
defmodule Asciinema.PngGenerator.A2png do
@behaviour Asciinema.PngGenerator
use GenServer
alias Asciinema.Asciicast
alias Asciinema.PngGenerator.PngParams
@pool_name :worker
@acquire_timeout 5000
@a2png_timeout 30000
@result_timeout 35000
def generate(%Asciicast{} = asciicast, %PngParams{} = png_params) do
{:ok, tmp_dir_path} = Briefly.create(directory: true)
try do
:poolboy.transaction(
@pool_name,
(fn pid ->
try do
GenServer.call(pid, {:generate, asciicast, png_params, tmp_dir_path}, @result_timeout)
catch
:exit, {:timeout, _} ->
{:error, :timeout}
end
end),
@acquire_timeout
)
catch
:exit, {:timeout, _} ->
{:error, :busy}
end
end
# GenServer API
def start_link(_) do
GenServer.start_link(__MODULE__, nil, [])
end
def init(_) do
{:ok, nil}
end
def handle_call({:generate, asciicast, png_params, tmp_dir_path}, _from, state) do
{:reply, do_generate(asciicast, png_params, tmp_dir_path), state}
end
def poolboy_config do
[{:name, {:local, @pool_name}},
{:worker_module, __MODULE__},
{:size, pool_size()},
{:max_overflow, 0}]
end
defp do_generate(asciicast, png_params, tmp_dir_path) do
path = Asciicast.json_store_path(asciicast)
json_path = Path.join(tmp_dir_path, "tmp.json")
png_path = Path.join(tmp_dir_path, "tmp.png")
args = [
json_path,
png_path,
Float.to_string(png_params.snapshot_at),
png_params.theme,
Integer.to_string(png_params.scale)
]
{:ok, {:ok, _}} = file_store().open(path, &(:file.copy(&1, json_path)))
process = Porcelain.spawn(bin_path(), args, err: :string)
case Porcelain.Process.await(process, @a2png_timeout) do
{:ok, %{status: 0}} ->
{:ok, png_path}
{:ok, %Porcelain.Result{} = result} ->
{:error, result}
otherwise ->
otherwise
end
end
defp bin_path do
Keyword.get(Application.get_env(:asciinema, __MODULE__), :bin_path)
end
defp pool_size do
Keyword.get(Application.get_env(:asciinema, __MODULE__), :pool_size)
end
defp file_store do
Application.get_env(:asciinema, :file_store)
end
end

@ -0,0 +1,3 @@
defmodule Asciinema.PngGenerator.PngParams do
defstruct [:snapshot_at, :theme, :scale]
end

@ -0,0 +1,3 @@
defmodule Asciinema.Repo do
use Ecto.Repo, otp_app: :asciinema
end

@ -0,0 +1,28 @@
defmodule Asciinema.TrailingFormat do
@known_extensions ["json", "png", "gif"]
def init(opts), do: opts
def call(conn, _opts) do
case conn.path_info do
[] ->
conn
path_info ->
%{conn | path_info: rewrite_path_info(path_info)}
end
end
defp rewrite_path_info(path_info) do
path_info
|> List.last
|> String.split(".")
|> Enum.reverse
|> case do
[format | fragments] when format in @known_extensions ->
id = fragments |> Enum.reverse |> Enum.join(".")
path_info |> List.replace_at(-1, id) |> List.insert_at(-1, format)
_ ->
path_info
end
end
end

@ -0,0 +1,78 @@
defmodule Asciinema.Mixfile do
use Mix.Project
def project do
[app: :asciinema,
version: "0.0.1",
elixir: "~> 1.2",
elixirc_paths: elixirc_paths(Mix.env),
compilers: [:phoenix, :gettext] ++ Mix.compilers,
build_embedded: Mix.env == :prod,
start_permanent: Mix.env == :prod,
aliases: aliases(),
deps: deps()]
end
# Configuration for the OTP application.
#
# Type `mix help compile.app` for more information.
def application do
[mod: {Asciinema, []},
applications: [
:briefly,
:bugsnag,
:cowboy,
:ex_aws,
:gettext,
:logger,
:phoenix,
:phoenix_ecto,
:phoenix_html,
:phoenix_pubsub,
:plug_rails_cookie_session_store,
:poolboy,
:porcelain,
:postgrex,
]]
end
# Specifies which paths to compile per environment.
defp elixirc_paths(:test), do: ["lib", "web", "test/support"]
defp elixirc_paths(_), do: ["lib", "web"]
# Specifies your project dependencies.
#
# Type `mix help deps` for examples and options.
defp deps do
[
{:briefly, "~> 0.3"},
{:cowboy, "~> 1.0"},
{:ex_aws, "~> 1.0"},
{:gettext, "~> 0.11"},
{:phoenix, "~> 1.2.1"},
{:phoenix_ecto, "~> 3.0"},
{:phoenix_html, "~> 2.6"},
{:phoenix_live_reload, "~> 1.0", only: :dev},
{:phoenix_markdown, "~> 0.1"},
{:phoenix_pubsub, "~> 1.0"},
{:plug_rails_cookie_session_store, "~> 0.1"},
{:plugsnag, "~> 1.3.0", github: "sickill/plugsnag"},
{:poison, "~> 2.2"},
{:poolboy, "~> 1.5"},
{:porcelain, "~> 2.0"},
{:postgrex, ">= 0.0.0"},
]
end
# Aliases are shortcuts or tasks specific to the current project.
# For example, to create, migrate and run the seeds file at once:
#
# $ mix ecto.setup
#
# See the documentation for `Mix` for more info on aliases.
defp aliases do
["ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
"ecto.reset": ["ecto.drop", "ecto.setup"],
"test": ["ecto.create --quiet", "ecto.migrate", "test"]]
end
end

@ -0,0 +1,34 @@
%{"briefly": {:hex, :briefly, "0.3.0", "16e6b76d2070ebc9cbd025fa85cf5dbaf52368c4bd896fb482b5a6b95a540c2f", [:mix], [], "hexpm"},
"bugsnag": {:hex, :bugsnag, "1.4.0", "fda8c3f550c93568b6e9ac615b1a9be0c1c4e06c7eb0ffb04a133dfaf1e01327", [:mix], [{:httpoison, "~> 0.9", [hex: :httpoison, repo: "hexpm", optional: false]}, {:poison, "~> 1.5 or ~> 2.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
"certifi": {:hex, :certifi, "1.0.0", "1c787a85b1855ba354f0b8920392c19aa1d06b0ee1362f9141279620a5be2039", [:rebar3], [], "hexpm"},
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"},
"cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"},
"cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], [], "hexpm"},
"db_connection": {:hex, :db_connection, "1.0.0-rc.5", "1d9ab6e01387bdf2de7a16c56866971f7c2f75aea7c69cae2a0346e4b537ae0d", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0.0-beta.3", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"},
"decimal": {:hex, :decimal, "1.1.2", "79a769d4657b2d537b51ef3c02d29ab7141d2b486b516c109642d453ee08e00c", [:mix], [], "hexpm"},
"earmark": {:hex, :earmark, "1.2.2", "f718159d6b65068e8daeef709ccddae5f7fdc770707d82e7d126f584cd925b74", [:mix], [], "hexpm"},
"ecto": {:hex, :ecto, "2.0.4", "03fd3b9aa508b1383eb38c00ac389953ed22af53811aa2e504975a3e814a8d97", [:mix], [{:db_connection, "~> 1.0-rc.2", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.7.7", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 1.5 or ~> 2.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.11.2", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0-beta", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"},
"ex_aws": {:hex, :ex_aws, "1.1.2", "b78416d0a84efe92c22e5df8ba7ca028d63b2b4228f95871a1ecf10324b6493b", [:mix], [{:configparser_ex, "~> 0.2.1", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "1.6.3 or 1.6.5 or 1.7.1", [hex: :hackney, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8", [hex: :jsx, repo: "hexpm", optional: true]}, {:poison, ">= 1.2.0", [hex: :poison, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:xml_builder, "~> 0.0.6", [hex: :xml_builder, repo: "hexpm", optional: true]}], "hexpm"},
"fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [:rebar], [], "hexpm"},
"gettext": {:hex, :gettext, "0.11.0", "80c1dd42d270482418fa158ec5ba073d2980e3718bacad86f3d4ad71d5667679", [:mix], [], "hexpm"},
"hackney": {:hex, :hackney, "1.7.1", "e238c52c5df3c3b16ce613d3a51c7220a784d734879b1e231c9babd433ac1cb4", [:rebar3], [{:certifi, "1.0.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "4.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
"httpoison": {:hex, :httpoison, "0.11.1", "d06c571274c0e77b6cc50e548db3fd7779f611fbed6681fd60a331f66c143a0b", [:mix], [{:hackney, "~> 1.7.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
"idna": {:hex, :idna, "4.0.0", "10aaa9f79d0b12cf0def53038547855b91144f1bfcc0ec73494f38bb7b9c4961", [:rebar3], [], "hexpm"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"},
"mime": {:hex, :mime, "1.1.0", "01c1d6f4083d8aa5c7b8c246ade95139620ef8effb009edde934e0ec3b28090a", [:mix], [], "hexpm"},
"mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"},
"phoenix": {:hex, :phoenix, "1.2.3", "b68dd6a7e6ff3eef38ad59771007d2f3f344988ea6e658e9b2c6ffb2ef494810", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.4 or ~> 1.3.3 or ~> 1.2.4 or ~> 1.1.8 or ~> 1.0.5", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 1.5 or ~> 2.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
"phoenix_ecto": {:hex, :phoenix_ecto, "3.0.1", "42eb486ef732cf209d0a353e791806721f33ff40beab0a86f02070a5649ed00a", [:mix], [{:ecto, "~> 2.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.6", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"phoenix_html": {:hex, :phoenix_html, "2.9.3", "1b5a2122cbf743aa242f54dced8a4f1cc778b8bd304f4b4c0043a6250c58e258", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.0.5", "829218c4152ba1e9848e2bf8e161fcde6b4ec679a516259442561d21fde68d0b", [:mix], [{:fs, "~> 0.9.1", [hex: :fs, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.0 or ~> 1.2-rc", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm"},
"phoenix_markdown": {:hex, :phoenix_markdown, "0.1.4", "246727d998997c6b90374a678bbdca5b300dcbbfecb29dbcca8a7891751f6cd5", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.1", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}], "hexpm"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.1", "c10ddf6237007c804bf2b8f3c4d5b99009b42eca3a0dfac04ea2d8001186056a", [:mix], [], "hexpm"},
"plug": {:hex, :plug, "1.3.5", "7503bfcd7091df2a9761ef8cecea666d1f2cc454cbbaf0afa0b6e259203b7031", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"},
"plug_rails_cookie_session_store": {:hex, :plug_rails_cookie_session_store, "0.1.0", "8d87967eb2d4d25837e1b5778265aebf8ac797291d6ff65dbd828c4ffa7f0955", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:plug, ">= 0.9.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"plugsnag": {:git, "https://github.com/sickill/plugsnag.git", "a5d8dcc370f52cec9fcf21acb42e2b293b003b37", []},
"poison": {:hex, :poison, "2.2.0", "4763b69a8a77bd77d26f477d196428b741261a761257ff1cf92753a0d4d24a63", [:mix], [], "hexpm"},
"poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], [], "hexpm"},
"porcelain": {:hex, :porcelain, "2.0.3", "2d77b17d1f21fed875b8c5ecba72a01533db2013bd2e5e62c6d286c029150fdc", [:mix], [], "hexpm"},
"postgrex": {:hex, :postgrex, "0.11.2", "139755c1359d3c5c6d6e8b1ea72556d39e2746f61c6ddfb442813c91f53487e8", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.0-rc", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"},
"ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"}}

@ -0,0 +1,21 @@
{
"repository": {},
"license": "MIT",
"scripts": {
"deploy": "brunch build --production",
"watch": "brunch watch --stdin"
},
"dependencies": {
"phoenix": "file:deps/phoenix",
"phoenix_html": "file:deps/phoenix_html"
},
"devDependencies": {
"babel-brunch": "~6.0.0",
"brunch": "2.7.4",
"clean-css-brunch": "~2.0.0",
"css-brunch": "~2.0.0",
"sass-brunch": "^2.6.3",
"javascript-brunch": "~2.0.0",
"uglify-js-brunch": "~2.0.1"
}
}

@ -0,0 +1,93 @@
## `msgid`s in this file come from POT (.pot) files.
##
## Do not add, change, or remove `msgid`s manually here as
## they're tied to the ones in the corresponding POT file
## (with the same domain).
##
## Use `mix gettext.extract --merge` or `mix gettext.merge`
## to merge POT files into PO files.
msgid ""
msgstr ""
"Language: en\n"
## From Ecto.Changeset.cast/4
msgid "can't be blank"
msgstr ""
## From Ecto.Changeset.unique_constraint/3
msgid "has already been taken"
msgstr ""
## From Ecto.Changeset.put_change/3
msgid "is invalid"
msgstr ""
## From Ecto.Changeset.validate_format/3
msgid "has invalid format"
msgstr ""
## From Ecto.Changeset.validate_subset/3
msgid "has an invalid entry"
msgstr ""
## From Ecto.Changeset.validate_exclusion/3
msgid "is reserved"
msgstr ""
## From Ecto.Changeset.validate_confirmation/3
msgid "does not match confirmation"
msgstr ""
## From Ecto.Changeset.no_assoc_constraint/3
msgid "is still associated to this entry"
msgstr ""
msgid "are still associated to this entry"
msgstr ""
## From Ecto.Changeset.validate_length/3
msgid "should be %{count} character(s)"
msgid_plural "should be %{count} character(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should have %{count} item(s)"
msgid_plural "should have %{count} item(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should be at least %{count} character(s)"
msgid_plural "should be at least %{count} character(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should have at least %{count} item(s)"
msgid_plural "should have at least %{count} item(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should be at most %{count} character(s)"
msgid_plural "should be at most %{count} character(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should have at most %{count} item(s)"
msgid_plural "should have at most %{count} item(s)"
msgstr[0] ""
msgstr[1] ""
## From Ecto.Changeset.validate_number/3
msgid "must be less than %{number}"
msgstr ""
msgid "must be greater than %{number}"
msgstr ""
msgid "must be less than or equal to %{number}"
msgstr ""
msgid "must be greater than or equal to %{number}"
msgstr ""
msgid "must be equal to %{number}"
msgstr ""

@ -0,0 +1,91 @@
## This file is a PO Template file.
##
## `msgid`s here are often extracted from source code.
## Add new translations manually only if they're dynamic
## translations that can't be statically extracted.
##
## Run `mix gettext.extract` to bring this file up to
## date. Leave `msgstr`s empty as changing them here as no
## effect: edit them in PO (`.po`) files instead.
## From Ecto.Changeset.cast/4
msgid "can't be blank"
msgstr ""
## From Ecto.Changeset.unique_constraint/3
msgid "has already been taken"
msgstr ""
## From Ecto.Changeset.put_change/3
msgid "is invalid"
msgstr ""
## From Ecto.Changeset.validate_format/3
msgid "has invalid format"
msgstr ""
## From Ecto.Changeset.validate_subset/3
msgid "has an invalid entry"
msgstr ""
## From Ecto.Changeset.validate_exclusion/3
msgid "is reserved"
msgstr ""
## From Ecto.Changeset.validate_confirmation/3
msgid "does not match confirmation"
msgstr ""
## From Ecto.Changeset.no_assoc_constraint/3
msgid "is still associated to this entry"
msgstr ""
msgid "are still associated to this entry"
msgstr ""
## From Ecto.Changeset.validate_length/3
msgid "should be %{count} character(s)"
msgid_plural "should be %{count} character(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should have %{count} item(s)"
msgid_plural "should have %{count} item(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should be at least %{count} character(s)"
msgid_plural "should be at least %{count} character(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should have at least %{count} item(s)"
msgid_plural "should have at least %{count} item(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should be at most %{count} character(s)"
msgid_plural "should be at most %{count} character(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should have at most %{count} item(s)"
msgid_plural "should have at most %{count} item(s)"
msgstr[0] ""
msgstr[1] ""
## From Ecto.Changeset.validate_number/3
msgid "must be less than %{number}"
msgstr ""
msgid "must be greater than %{number}"
msgstr ""
msgid "must be less than or equal to %{number}"
msgstr ""
msgid "must be greater than or equal to %{number}"
msgstr ""
msgid "must be equal to %{number}"
msgstr ""

@ -0,0 +1,11 @@
# Script for populating the database. You can run it as:
#
# mix run priv/repo/seeds.exs
#
# Inside the script, you can read and write to any of your
# repositories directly:
#
# Asciinema.Repo.insert!(%Asciinema.SomeModel{})
#
# We recommend using the bang functions (`insert!`, `update!`
# and so on) as they will fail if something goes wrong.

@ -1,48 +0,0 @@
(defproject asciinema "0.1.0-SNAPSHOT"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:min-lein-version "2.0.0"
:dependencies [[org.clojure/clojure "1.8.0"]
[com.stuartsierra/component "0.3.1"]
[clj-time "0.13.0"]
[duct "0.8.2"]
[yada "1.2.0"]
[aleph "0.4.1"]
[bidi "2.0.16"]
[prismatic/schema "1.1.3"]
[environ "1.1.0"]
[ring "1.5.0"]
[clj-bugsnag "0.2.9"]
[clj-aws-s3 "0.3.10" :exclusions [joda-time]]
[cheshire "5.7.0"]
[pandect "0.6.1"]
[com.taoensso/timbre "4.8.0"]
[com.taoensso/carmine "2.15.1"]
[org.slf4j/slf4j-nop "1.7.21"]
[duct/hikaricp-component "0.1.0"]
[org.postgresql/postgresql "9.4.1211"]
[duct/ragtime-component "0.1.4"]
[me.raynes/conch "0.8.0"]]
:plugins [[lein-environ "1.0.3"]]
:main ^:skip-aot asciinema.main
:target-path "target/%s/"
:aliases {"setup" ["run" "-m" "duct.util.repl/setup"]}
:profiles
{:dev [:project/dev :profiles/dev]
:test [:project/test :profiles/test]
:uberjar {:aot :all}
:repl {:repl-options {:host "0.0.0.0"
:port 44444}}
:profiles/dev {}
:profiles/test {}
:project/dev {:dependencies [[duct/generate "0.8.2"]
[reloaded.repl "0.2.3"]
[org.clojure/tools.namespace "0.2.11"]
[org.clojure/tools.nrepl "0.2.12"]
[eftest "0.1.1"]
[com.gearswithingears/shrubbery "0.4.1"]
[kerodon "0.8.0"]]
:source-paths ["dev/src"]
:resource-paths ["dev/resources"]
:repl-options {:init-ns user}}
:project/test {}})

@ -1,11 +0,0 @@
<!DOCTYPE html>
<html lang="en" class="example">
<head>
<title>Example Endpoint</title>
<link rel="stylesheet" href="/assets/normalize.css/normalize.css">
<link rel="stylesheet" href="/css/site.css">
</head>
<body>
<h1>This is an example endpoint</h1>
</body>
</html>

@ -1,12 +0,0 @@
<!DOCTYPE html>
<html lang="en" class="error-page">
<head>
<title>Server Error</title>
<link rel="stylesheet" href="/assets/normalize.css/normalize.css">
<link rel="stylesheet" href="/css/site.css">
</head>
<body>
<h1>Resource Not Found</h1>
<h2>The requested page does not exist.</h2>
</body>
</html>

@ -1,12 +0,0 @@
<!DOCTYPE html>
<html lang="en" class="error-page">
<head>
<title>Server Error</title>
<link rel="stylesheet" href="/assets/normalize.css/normalize.css">
<link rel="stylesheet" href="/css/site.css">
</head>
<body>
<h1>Internal Server Error</h1>
<h2>Sorry, something went wrong.</h2>
</body>
</html>

@ -1,103 +0,0 @@
.error-page body {
background: #eee;
}
.error-page h1 {
margin: 15% 0 0 0;
text-align: center;
font-size: 42px;
color: #900;
}
.error-page h2 {
text-align: center;
font-size: 32px;
font-weight: normal;
color: #333;
}
.welcome body {
background: #eee;
color: #333;
font-family: Helvetica, Arial, sans-serif;
max-width: 700px;
padding: 15px;
margin: auto;
}
.welcome p {
line-height: 1.4em;
}
.welcome code {
font-family: Menlo, DejaVu Sans Mono, Lucida Console, monospace;
font-size: 12px;
background: #ddd;
color: #111;
}
.welcome h1 {
text-align: center;
font-size: 36px;
font-weight: lighter;
margin: 40px 0 30px 0;
}
.welcome h1 .outer {
border: solid 4px #555;
padding: 3px;
display: inline-block;
}
.welcome h1 .inner {
border: solid 2px #555;
padding: 0 3px;
display: inline-block;
font-weight: normal;
color: #444;
}
.welcome .project-name {
font-weight: bold;
}
.welcome .profiles {
margin-top: 30px;
}
.welcome .profiles code {
font-size: 11px;
}
.welcome .profiles h2 {
font-weight: normal;
font-size: 23px;
margin-bottom: 0;
color: #333;
}
.welcome .profiles dl {
margin: 0 10px;
}
.welcome .profiles dt {
font-weight: normal;
font-size: 19px;
margin: 18px 0 5px 0;
}
.welcome .profiles dd {
font-size: 14px;
margin: 8px 0 8px 0;
}
.example body {
background: #eee;
}
.example h1 {
margin: 15% 0 0 0;
text-align: center;
font-size: 36px;
font-weight: normal;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

@ -1,36 +0,0 @@
<!DOCTYPE html>
<html lang="en" class="welcome">
<head>
<title>Welcome to Duct</title>
<link rel="stylesheet" href="/assets/normalize.css/normalize.css">
<link rel="stylesheet" href="/css/site.css">
</head>
<body>
<h1>Welcome to <span class="outer"><span class="inner">Duct</span></span></h1>
<div class="intro">
<p>Congratulations! Your project <span class="project-name">asciinema</span> is
ready and running.</p>
<p>This is a static welcome page located at <code>resources/asciinema/public/index.html</code>
in the project directory. Remove or replace it when you start developing.
If you remove the index page entirely, be sure to change the
<code>:route-aliases</code> map in <code>resources/asciinema/system.edn</code>.
</div>
<div class="profiles">
<h2>Template profiles used:</h2>
<dl>
<dt>+example</dt>
<dd>Adds an example endpoint at <a href="/example">/example</a>.</dd>
<dt>+postgres</dt>
<dd>Adds a PostgreSQL dependency and database component. The database used for
development defaults to <code>postgres</code> on <code>localhost</code>.</dd>
<dt>+ragtime</dt>
<dd>Adds Ragtime migrations. Use <code>(migrate)</code> and <code>(rollback)</code>
in the REPL. Migrations are stored in <code>resources/asciinema/migrations</code>.
</dd>
<dt>+site</dt>
<dd>Adds middleware and configuration suited for a user-facing website.</dd>
</dl>
</div>
</body>
</html>

@ -1,2 +0,0 @@
User-agent: *
Disallow:

@ -1,37 +0,0 @@
{:components
{:http #var asciinema.component.yada-listener/yada-listener
:db #var asciinema.component.db/hikaricp
:ragtime #var duct.component.ragtime/ragtime
:auto-file-store #var asciinema.component.auto-file-store/auto-file-store
:redis-client #var asciinema.component.redis-client/redis-client
:a2png #var asciinema.component.a2png/a2png
:fixed-thread-executor #var asciinema.component.fixed-thread-executor/fixed-thread-executor}
:endpoints
{:asciicasts #var asciinema.endpoint.asciicasts/asciicasts-endpoint}
:dependencies
{:http {:app :asciicasts}
:ragtime [:db]
:asciicasts {:db :db
:file-store :auto-file-store
:exp-set :redis-client
:executor :fixed-thread-executor
:png-gen :a2png}}
:config
{:http
{:port http-port}
:db
{:uri db-uri}
:ragtime
{:resource-path "asciinema/migrations"}
:auto-file-store
{:path "uploads/"
:s3-bucket s3-bucket
:s3-cred {:access-key s3-access-key
:secret-key s3-secret-key}}
:redis-client
{:uri redis-url}
:a2png
{:bin-path a2png-bin-path}
:fixed-thread-executor
{:threads 2
:queue-length 16}}}

@ -1,5 +0,0 @@
require 'rails_helper'
describe DocsController do
end

@ -1,65 +0,0 @@
require 'rails_helper'
feature "Docs" do
scenario 'Visiting "how it works" page' do
visit docs_path('how-it-works')
within('.main') do
expect(page).to have_content('How it works')
end
expect_doc_links
end
scenario 'Visiting "getting started" page' do
visit docs_path('getting-started')
within('.main') do
expect(page).to have_content('Getting started')
end
expect_doc_links
end
scenario 'Visiting installation page' do
visit docs_path(:installation)
within('.main') do
expect(page).to have_content('Installation')
end
expect_doc_links
end
scenario 'Visiting usage page' do
visit docs_path(:usage)
within('.main') do
expect(page).to have_content('Usage')
end
expect_doc_links
end
scenario 'Visiting embedding page' do
visit docs_path(:embedding)
within('.main') do
expect(page).to have_content('Embedding')
end
expect_doc_links
end
scenario 'Visiting FAQ page' do
visit docs_path(:faq)
within('.main') do
expect(page).to have_content('Frequently Asked Questions')
end
expect_doc_links
end
end

@ -1,5 +0,0 @@
(ns asciinema.boundary.asciicast-database)
(defprotocol AsciicastDatabase
(get-asciicast-by-id [this id])
(get-asciicast-by-token [this token]))

@ -1,4 +0,0 @@
(ns asciinema.boundary.executor)
(defprotocol Executor
(execute [this f]))

@ -1,6 +0,0 @@
(ns asciinema.boundary.expiring-set
(:refer-clojure :exclude [conj! contains?]))
(defprotocol ExpiringSet
(conj! [this value expires-at])
(contains? [this value]))

@ -1,8 +0,0 @@
(ns asciinema.boundary.file-store)
(defprotocol FileStore
(put-file [this file path] [this file path size])
(input-stream [this path])
(move-file [this old-path new-path])
(delete-file [this path])
(serve-file [this ctx path opts]))

@ -1,4 +0,0 @@
(ns asciinema.boundary.png-generator)
(defprotocol PngGenerator
(generate [this json-is png-params]))

@ -1,4 +0,0 @@
(ns asciinema.boundary.user-database)
(defprotocol UserDatabase
(get-user-by-id [this id]))

@ -1,26 +0,0 @@
(ns asciinema.component.a2png
(:require [asciinema.boundary.png-generator :as png-generator]
[asciinema.util.io :refer [cleanup-input-stream create-tmp-dir]]
[clojure.java.io :as io]
[clojure.java
[io :as io]
[shell :as shell]]
[me.raynes.conch :as conch]))
(defn- exec-a2png [bin-path in-url out-path {:keys [snapshot-at theme scale]}]
(conch/let-programs [a2png bin-path]
(a2png in-url out-path (str snapshot-at) theme (str scale) {:timeout 30000})))
(defrecord A2png [bin-path]
png-generator/PngGenerator
(generate [this json-is png-params]
(let [dir (create-tmp-dir "a2png-")
cleanup #(shell/sh "rm" "-rf" (.getPath dir))
json-local-path (str dir "/asciicast.json")
png-local-path (str dir "/asciicast.png")]
(io/copy json-is (io/file json-local-path))
(exec-a2png bin-path json-local-path png-local-path png-params)
(cleanup-input-stream (io/input-stream png-local-path) cleanup))))
(defn a2png [{:keys [bin-path]}]
(->A2png bin-path))

@ -1,8 +0,0 @@
(ns asciinema.component.auto-file-store
(:require [asciinema.component.local-file-store :refer [local-file-store]]
[asciinema.component.s3-file-store :refer [s3-file-store]]))
(defn auto-file-store [config]
(if (:s3-bucket config)
(s3-file-store config)
(local-file-store config)))

@ -1,59 +0,0 @@
(ns asciinema.component.db
(:require [asciinema.boundary.asciicast-database :refer :all]
[asciinema.boundary.user-database :refer :all]
[clojure.java.jdbc :as jdbc]
[clj-time.coerce :as timec]
[duct.component.hikaricp :as hikaricp]
[clojure.string :as str]))
(extend-protocol jdbc/ISQLValue
org.joda.time.DateTime
(sql-value [val]
(timec/to-sql-time val)))
(extend-protocol jdbc/IResultSetReadColumn
java.sql.Timestamp
(result-set-read-column [x _ _]
(timec/from-sql-time x)))
;; AsciicastDatabase
(def q-get-asciicast-by-id "SELECT * FROM asciicasts WHERE id=?")
(def q-get-asciicast-by-secret-token "SELECT * FROM asciicasts WHERE secret_token=?")
(def q-get-public-asciicast-by-id "SELECT * FROM asciicasts WHERE id=? AND private=FALSE")
(extend-protocol AsciicastDatabase
duct.component.hikaricp.HikariCP
(get-asciicast-by-id [{db :spec} id]
(first (jdbc/query db [q-get-asciicast-by-id id])))
(get-asciicast-by-token [{db :spec} token]
(when-let [query (cond
(re-matches #"\d+" token)
[q-get-public-asciicast-by-id (Long/parseLong token)]
(= (count token) 25)
[q-get-asciicast-by-secret-token token])]
(first (jdbc/query db query)))))
;; UserDatabase
(def q-get-user-by-id "SELECT * FROM users WHERE id=?")
(extend-protocol UserDatabase
duct.component.hikaricp.HikariCP
(get-user-by-id [{db :spec} id]
(first (jdbc/query db [q-get-user-by-id id]))))
;; constructor
(defn- fix-uri [uri]
(when uri
(if (str/starts-with? uri "jdbc:")
uri
(str "jdbc:" uri))))
(defn hikaricp [opts]
(let [opts (update opts :uri fix-uri)]
(hikaricp/hikaricp opts)))

@ -1,39 +0,0 @@
(ns asciinema.component.fixed-thread-executor
(:require [aleph.flow :as flow]
[asciinema.boundary.executor :as executor]
[com.stuartsierra.component :as component]
[manifold.deferred :as d])
(:import [java.util.concurrent
ExecutorService
RejectedExecutionException
TimeUnit]))
(defrecord FixedThreadExecutor [threads queue-length]
executor/Executor
(execute [{:keys [^ExecutorService executor]} f]
(try
(let [result (d/deferred)
f (fn []
(try
(d/success! result (f))
(catch Exception e
(d/error! result e))))]
(.execute executor f)
result)
(catch RejectedExecutionException _
nil)))
component/Lifecycle
(start [{:keys [threads queue-length] :as component}]
(let [executor (flow/fixed-thread-executor threads {:onto? false
:initial-thread-count threads
:queue-length queue-length})]
(assoc component :executor executor)))
(stop [{:keys [^ExecutorService executor] :as component}]
(.shutdown executor)
(when-not (.awaitTermination executor 1000 TimeUnit/MILLISECONDS)
(.shutdownNow executor))
(assoc component :executor nil)))
(defn fixed-thread-executor [{:keys [threads queue-length]}]
(->FixedThreadExecutor threads queue-length))

@ -1,38 +0,0 @@
(ns asciinema.component.local-file-store
(:require [asciinema.boundary.file-store :as file-store]
[clojure.java.io :as io]
[ring.util.http-response :as response]))
(defrecord LocalFileStore [base-path]
file-store/FileStore
(put-file [this file path]
(let [path (str base-path path)]
(io/make-parents path)
(io/copy file (io/file path))))
(put-file [this file path size]
(file-store/put-file this file path))
(input-stream [this path]
(let [path (str base-path path)]
(io/input-stream path)))
(move-file [this old-path new-path]
(let [old-path (str base-path old-path)
new-path (str base-path new-path)]
(.renameTo (io/file old-path) (io/file new-path))))
(delete-file [this path]
(let [path (str base-path path)]
(io/delete-file path)))
(serve-file [this ctx path {:keys [filename]}]
(let [path (str base-path path)
response (assoc (:response ctx) :body (io/file path))]
(if filename
(update response :headers assoc "content-disposition" (str "attachment; filename=" filename))
response))))
(defn local-file-store [{:keys [path]}]
(->LocalFileStore path))

@ -1,14 +0,0 @@
(ns asciinema.component.mem-expiring-set
(:require [asciinema.boundary.expiring-set :as exp-set]))
(defrecord MemExpiringSet [store]
exp-set/ExpiringSet
(conj! [this value _expires-at]
(swap! store conj value))
(contains? [this value]
(contains? @store value)))
(defn mem-expiring-set [{:keys [store]}]
(->MemExpiringSet (or store (atom #{}))))

@ -1,28 +0,0 @@
(ns asciinema.component.redis-client
(:require [asciinema.boundary.expiring-set :as exp-set]
[clj-time.core :as t]
[clj-time.local :as tl]
[com.stuartsierra.component :as component]
[taoensso.carmine :as car]))
(defrecord RedisClient [uri]
component/Lifecycle
(start [component]
(if (:listener component)
component
(let [conn {:pool {} :spec {:uri uri}}]
(assoc component :conn conn))))
(stop [component]
(if (:conn component)
(dissoc component :conn)
component))
exp-set/ExpiringSet
(conj! [this value expires-at]
(let [seconds (t/in-seconds (t/interval (tl/local-now) expires-at))]
(car/wcar (:conn this) (car/setex value seconds true))))
(contains? [this value]
(car/as-bool (car/wcar (:conn this) (car/exists value)))))
(defn redis-client [{:keys [uri]}]
(->RedisClient uri))

@ -1,66 +0,0 @@
(ns asciinema.component.s3-file-store
(:require [asciinema.boundary.file-store :as file-store]
[aws.sdk.s3 :as s3]
[clj-time
[coerce :as timec]
[core :as time]]
[ring.util.http-response :as response]
[ring.util.mime-type :as mime-type])
(:import com.amazonaws.auth.BasicAWSCredentials
com.amazonaws.services.s3.AmazonS3Client
[com.amazonaws.services.s3.model GeneratePresignedUrlRequest ResponseHeaderOverrides]))
(defn- s3-client* [cred]
(let [credentials (BasicAWSCredentials. (:access-key cred) (:secret-key cred))]
(AmazonS3Client. credentials)))
(def ^:private s3-client (memoize s3-client*))
(defn- generate-presigned-url [cred bucket path {:keys [expires filename]
:or {expires (-> 1 time/days time/from-now)}}]
(let [client (s3-client cred)
request (GeneratePresignedUrlRequest. bucket path)]
(.setExpiration request (timec/to-date expires))
(when filename
(let [header-overrides (doto (ResponseHeaderOverrides.)
(.setContentDisposition (str "attachment; filename=" filename)))]
(.setResponseHeaders request header-overrides)))
(.toString (.generatePresignedUrl client request))))
(defrecord S3FileStore [cred bucket path-prefix]
file-store/FileStore
(put-file [this file path]
(file-store/put-file this file path nil))
(put-file [this file path size]
(let [path (str path-prefix path)
content-type (mime-type/ext-mime-type path)]
(s3/put-object cred bucket path file {:content-length size
:content-type content-type})))
(input-stream [this path]
(let [path (str path-prefix path)]
(:content (s3/get-object cred bucket path))))
(move-file [this old-path new-path]
(let [old-path (str path-prefix old-path)
new-path (str path-prefix new-path)]
(s3/copy-object cred bucket old-path new-path)
(s3/delete-object cred bucket old-path)))
(delete-file [this path]
(let [path (str path-prefix path)]
(s3/delete-object cred bucket path)))
(serve-file [this ctx path opts]
(let [path (str path-prefix path)
url (generate-presigned-url cred bucket path opts)]
(-> (:response ctx)
(assoc :status 302)
(update :headers assoc "location" url)))))
(defn s3-file-store
[{:keys [s3-cred s3-bucket path]}]
{:pre [(some? s3-cred) (some? s3-bucket) (some? path)]}
(->S3FileStore s3-cred s3-bucket path))

@ -1,22 +0,0 @@
(ns asciinema.component.yada-listener
(:require [bidi.vhosts :refer [vhosts-model]]
[com.stuartsierra.component :as component]
[yada.yada :as yada]))
(defrecord YadaListener [port server app]
component/Lifecycle
(start [component]
(if server
component
(let [handler (vhosts-model [:* (:routes app)]) ; wrap in * vhost to make path-for work
server (yada/listener handler {:port port})]
(assoc component :server server))))
(stop [component]
(if server
(do
((:close server))
(assoc component :server nil))
component)))
(defn yada-listener [{:keys [port app]}]
(map->YadaListener {:port port :app app}))

@ -1,90 +0,0 @@
(ns asciinema.endpoint.asciicasts
(:require [asciinema.boundary
[asciicast-database :as adb]
[executor :as executor]
[expiring-set :as exp-set]
[file-store :as fstore]
[png-generator :as png]
[user-database :as udb]]
[asciinema.model.asciicast :as asciicast]
[asciinema.yada :refer [not-found-model resource]]
[clj-time.core :as t]
[schema.core :as s]
[yada.yada :as yada]))
(def Theme (apply s/enum asciicast/themes))
(defn- service-unavailable-response [ctx]
(-> (:response ctx)
(assoc :status 503)
(update :headers assoc "retry-after" "5")))
(defn- async-response [ctx executor f]
(or (executor/execute executor f)
(service-unavailable-response ctx)))
(defn asciicast-file-resource [db file-store]
(resource
{:produces "application/json"
:parameters {:path {:token String}
:query {(s/optional-key :dl) s/Bool}}
:properties (fn [ctx]
(if-let [asciicast (adb/get-asciicast-by-token db (-> ctx :parameters :path :token))]
{::asciicast asciicast}
{:exists? false}))
:response (fn [ctx]
(let [asciicast (-> ctx :properties ::asciicast)
dl (-> ctx :parameters :query :dl)
path (asciicast/json-store-path asciicast)
filename (str "asciicast-" (:id asciicast) ".json")]
(fstore/serve-file file-store ctx path (when dl {:filename filename}))))}))
(def png-ttl-days 7)
(defn asciicast-image-resource [db file-store exp-set executor png-gen]
(resource
{:produces
"image/png"
:parameters
{:path {:token String}
:query {(s/optional-key :time) s/Num
(s/optional-key :theme) Theme
(s/optional-key :scale) (s/enum "1" "2")}}
:properties
(fn [ctx]
(if-let [asciicast (adb/get-asciicast-by-token db (-> ctx :parameters :path :token))]
(let [user (udb/get-user-by-id db (:user_id asciicast))
{:keys [time theme scale]} (-> ctx :parameters :query)
png-params (cond-> (asciicast/png-params asciicast user)
time (assoc :snapshot-at time)
theme (assoc :theme theme)
scale (assoc :scale (Integer/parseInt scale)))]
{:version (asciicast/png-version asciicast png-params)
::asciicast asciicast
::png-params png-params})
{:exists? false}))
:response
(fn [ctx]
(let [asciicast (-> ctx :properties ::asciicast)
png-params (-> ctx :properties ::png-params)
png-store-path (asciicast/png-store-path asciicast png-params)
expires (-> png-ttl-days t/days t/from-now)]
(if (exp-set/contains? exp-set png-store-path)
(fstore/serve-file file-store ctx png-store-path {})
(async-response ctx
executor
(fn []
(let [json-store-path (asciicast/json-store-path asciicast)]
(with-open [json-is (fstore/input-stream file-store json-store-path)
png-is (png/generate png-gen json-is png-params)]
(fstore/put-file file-store png-is png-store-path)))
(exp-set/conj! exp-set png-store-path expires)
(fstore/serve-file file-store ctx png-store-path {}))))))}))
(defn asciicasts-endpoint [{:keys [db file-store exp-set executor png-gen]}]
["" [["/a/" [[[:token ".json"] (asciicast-file-resource db file-store)]
[[:token ".png"] (asciicast-image-resource db file-store exp-set executor png-gen)]]]
[true (yada/resource not-found-model)]]])

@ -1,41 +0,0 @@
(ns asciinema.main
(:gen-class)
(:require [asciinema.yada :as y]
[clj-bugsnag.core :as bugsnag]
[com.stuartsierra.component :as component]
[duct.util.runtime :refer [add-shutdown-hook]]
[duct.util.system :refer [load-system]]
[environ.core :refer [env]]
[clojure.java.io :as io]))
(defn- request-context [req]
(str (-> req (get :request-method :unknown) name .toUpperCase)
" "
(:uri req)))
(defn- create-exception-notifier []
(when-let [key (:bugsnag-key env)]
(let [environment (:env-name env "production")
version (:git-sha env)]
(fn [ex req]
(bugsnag/notify ex {:api-key key
:environment environment
:project-ns "asciinema"
:version version
:context (request-context req)
:meta {:request (dissoc req :body)}})))))
(defn -main [& args]
(binding [y/*exception-notifier* (create-exception-notifier)]
(let [bindings {'http-port (Integer/parseInt (:port env "4000"))
'db-uri (:database-url env)
's3-bucket (:s3-bucket env)
's3-access-key (:s3-access-key env)
's3-secret-key (:s3-secret-key env)
'redis-url (:redis-url env "redis://localhost")
'a2png-bin-path (:a2png-bin-path env "a2png/a2png.sh")}
system (->> (load-system [(io/resource "asciinema/system.edn")] bindings)
(component/start))]
(add-shutdown-hook ::stop-system #(component/stop system))
(println "Started HTTP server on port" (-> system :http :port))))
@(promise))

@ -1,38 +0,0 @@
(ns asciinema.model.asciicast
(:require [pandect.algo.sha1 :as sha1]
[clojure.string :as str]))
(defn json-store-path [{:keys [id file stdout_frames]}]
(cond
file (str "asciicast/file/" id "/" file)
stdout_frames (str "asciicast/stdout_frames/" id "/" stdout_frames)))
(def themes #{"asciinema" "tango" "solarized-dark" "solarized-light" "monokai"})
(def default-theme "asciinema")
(defn theme-name [asciicast user]
(or (:theme_name asciicast)
(:theme_name user)
default-theme))
(defn snapshot-at [{:keys [snapshot_at duration]}]
(or snapshot_at (/ duration 2.0)))
(def default-png-scale 2)
(defn png-params [asciicast user]
{:snapshot-at (snapshot-at asciicast)
:theme (theme-name asciicast user)
:scale default-png-scale})
(defn png-version [asciicast params]
(let [attrs (assoc params :id (:id asciicast))]
(->> attrs
(map (fn [[k v]] (str (name k) "=" v)))
(str/join "/")
(sha1/sha1))))
(defn png-store-path [asciicast params]
(let [ver (png-version asciicast params)
png-filename (str ver ".png")]
(str "png/" (:id asciicast) "/" png-filename)))

@ -1,22 +0,0 @@
(ns asciinema.util.io
(:require [clojure.java.shell :as shell])
(:import java.io.FilterInputStream
java.nio.file.attribute.FileAttribute
java.nio.file.Files))
(defn create-tmp-dir [prefix]
(let [dir (Files/createTempDirectory prefix (into-array FileAttribute []))]
(.toFile dir)))
(defmacro with-tmp-dir [[sym prefix] & body]
`(let [~sym (create-tmp-dir ~prefix)]
(try
~@body
(finally
(shell/sh "rm" "-rf" (.getPath ~sym))))))
(defn cleanup-input-stream [is cleanup]
(proxy [FilterInputStream] [is]
(close []
(proxy-super close)
(cleanup))))

@ -1,46 +0,0 @@
(ns asciinema.yada
(:require [clojure.java.io :as io]
[taoensso.timbre :as log]
[yada.status :as status]
[yada.yada :as yada]))
(def ^:dynamic *exception-notifier* nil)
(def not-found-model
{:produces
#{"text/html" "text/plain"}
:response
(fn [ctx]
(assoc (:response ctx)
:status 404
:body (case (yada/content-type ctx)
"text/html" (io/input-stream (io/resource "asciinema/errors/404.html"))
"Not found")))})
(defn error-response [ctx]
(let [status (-> ctx :response :status)
status-name (get-in status/status [status :name])]
(case (yada/content-type ctx)
"text/html" (str "<html><body><h1>" status-name "</h1></body></html>")
status-name)))
(defn create-logger []
(let [notifier *exception-notifier*]
(fn [ctx]
(when-let [error (:error ctx)]
(let [status (-> ctx :response :status)]
(when (not= status 404)
(log/error error))
(when (and (= status 500) notifier)
(let [ex (or (-> error ex-data :error) error)]
(notifier ex (:request ctx))))))
ctx)))
(defn resource [model]
(let [error-statuses (set (concat (range 400 404) (range 405 600) ))]
(-> model
(assoc :logger (create-logger))
(update-in [:responses 404] #(or % not-found-model))
(update-in [:responses error-statuses] #(or % {:produces #{"text/html" "text/plain"}
:response error-response}))
yada/resource)))

@ -1,7 +0,0 @@
(ns asciinema.boundary.file-store-test
(:require [clojure.test :refer :all]
[asciinema.boundary.file-store :as file-store]))
(deftest a-test
(testing "FIXME, I fail."
(is (= 0 1))))

@ -1,54 +0,0 @@
(ns asciinema.component.db-test
(:require [clojure.test :refer :all]
[clojure.java.jdbc :as jdbc]
[clj-time.local :as timel]
[com.stuartsierra.component :as component]
[asciinema.component.db :as db]
[asciinema.boundary.asciicast-database :as adb]))
(defmacro with-db-component [component-var & body]
`(let [component# (-> (db/hikaricp {:uri "jdbc:postgresql://localhost:15432/asciinema_test?user=vagrant"})
component/start)]
(try
(jdbc/with-db-transaction [db# (:spec component#)]
(let [~component-var (assoc component# :spec db#)]
(jdbc/db-set-rollback-only! db#)
~@body))
(finally
(component/stop component#)))))
(defn insert-asciicast
([db] (insert-asciicast db {}))
([db attrs]
(first (jdbc/insert! db :asciicasts (merge {:duration 10.0
:terminal_columns 80
:terminal_lines 24
:created_at (timel/local-now)
:updated_at (timel/local-now)
:version 1
:secret_token "abcdeabcdeabcdeabcdeabcde"}
attrs)))))
(deftest get-asciicast-by-id-test
(testing "for existing asciicast"
(with-db-component db
(let [asciicast (insert-asciicast (:spec db))]
(is (map? (adb/get-asciicast-by-id db (:id asciicast)))))))
(testing "for non-existing asciicast"
(with-db-component db
(is (nil? (adb/get-asciicast-by-id db 1))))))
(deftest get-asciicast-by-token-test
(testing "for existing public asciicast"
(with-db-component db
(let [asciicast (insert-asciicast (:spec db) {:private false})]
(is (map? (adb/get-asciicast-by-token db (:secret_token asciicast))))
(is (map? (adb/get-asciicast-by-token db (-> asciicast :id str)))))))
(testing "for existing private asciicast"
(with-db-component db
(let [asciicast (insert-asciicast (:spec db) {:private true})]
(is (map? (adb/get-asciicast-by-token db (:secret_token asciicast))))
(is (nil? (adb/get-asciicast-by-token db (-> asciicast :id str)))))))
(testing "for non-existing asciicast"
(with-db-component db
(is (nil? (adb/get-asciicast-by-token db "1"))))))

@ -0,0 +1,22 @@
defmodule Asciinema.DocControllerTest do
use Asciinema.ConnCase
test "GET /docs", %{conn: conn} do
conn = get conn, "/docs"
assert redirected_to(conn, 302) == "/docs/getting-started"
end
test "GET /docs/*", %{conn: conn} do
Enum.each(["/docs/how-it-works",
"/docs/getting-started",
"/docs/installation",
"/docs/usage",
"/docs/config",
"/docs/embedding",
"/docs/faq"], fn(path) ->
conn = get conn, path
assert html_response(conn, 200) =~ "<h2>Docs</h2>"
end)
end
end

@ -0,0 +1,12 @@
defmodule Asciinema.GravatarTest do
use ExUnit.Case, async: true
alias Asciinema.Gravatar
test "default options" do
assert Gravatar.gravatar_url("test@example.com") == "//gravatar.com/avatar/55502f40dc8b7c769880b10874abc9d0?s=128&d=retro"
end
test "custom options" do
assert Gravatar.gravatar_url("test@example.com", size: 256, default: "mm") == "//gravatar.com/avatar/55502f40dc8b7c769880b10874abc9d0?s=256&d=mm"
end
end

@ -0,0 +1,18 @@
defmodule Asciinema.UserTest do
use Asciinema.ModelCase
alias Asciinema.User
@valid_attrs %{asciicasts_private_by_default: true, auth_token: "some content", email: "some content", name: "some content", temporary_username: "some content", theme_name: "some content", username: "some content"}
@invalid_attrs %{}
test "changeset with valid attributes" do
changeset = User.changeset(%User{}, @valid_attrs)
assert changeset.valid?
end
test "changeset with invalid attributes" do
changeset = User.changeset(%User{}, @invalid_attrs)
refute changeset.valid?
end
end

@ -0,0 +1,43 @@
defmodule Asciinema.ChannelCase do
@moduledoc """
This module defines the test case to be used by
channel tests.
Such tests rely on `Phoenix.ChannelTest` and also
import other functionality to make it easier
to build and query models.
Finally, if the test case interacts with the database,
it cannot be async. For this reason, every test runs
inside a transaction which is reset at the beginning
of the test unless the test case is marked as async.
"""
use ExUnit.CaseTemplate
using do
quote do
# Import conveniences for testing with channels
use Phoenix.ChannelTest
alias Asciinema.Repo
import Ecto
import Ecto.Changeset
import Ecto.Query
# The default endpoint for testing
@endpoint Asciinema.Endpoint
end
end
setup tags do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Asciinema.Repo)
unless tags[:async] do
Ecto.Adapters.SQL.Sandbox.mode(Asciinema.Repo, {:shared, self()})
end
:ok
end
end

@ -0,0 +1,44 @@
defmodule Asciinema.ConnCase do
@moduledoc """
This module defines the test case to be used by
tests that require setting up a connection.
Such tests rely on `Phoenix.ConnTest` and also
import other functionality to make it easier
to build and query models.
Finally, if the test case interacts with the database,
it cannot be async. For this reason, every test runs
inside a transaction which is reset at the beginning
of the test unless the test case is marked as async.
"""
use ExUnit.CaseTemplate
using do
quote do
# Import conveniences for testing with connections
use Phoenix.ConnTest
alias Asciinema.Repo
import Ecto
import Ecto.Changeset
import Ecto.Query
import Asciinema.Router.Helpers
# The default endpoint for testing
@endpoint Asciinema.Endpoint
end
end
setup tags do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Asciinema.Repo)
unless tags[:async] do
Ecto.Adapters.SQL.Sandbox.mode(Asciinema.Repo, {:shared, self()})
end
{:ok, conn: Phoenix.ConnTest.build_conn()}
end
end

@ -0,0 +1,65 @@
defmodule Asciinema.ModelCase do
@moduledoc """
This module defines the test case to be used by
model tests.
You may define functions here to be used as helpers in
your model tests. See `errors_on/2`'s definition as reference.
Finally, if the test case interacts with the database,
it cannot be async. For this reason, every test runs
inside a transaction which is reset at the beginning
of the test unless the test case is marked as async.
"""
use ExUnit.CaseTemplate
using do
quote do
alias Asciinema.Repo
import Ecto
import Ecto.Changeset
import Ecto.Query
import Asciinema.ModelCase
end
end
setup tags do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Asciinema.Repo)
unless tags[:async] do
Ecto.Adapters.SQL.Sandbox.mode(Asciinema.Repo, {:shared, self()})
end
:ok
end
@doc """
Helper for returning list of errors in a struct when given certain data.
## Examples
Given a User schema that lists `:name` as a required field and validates
`:password` to be safe, it would return:
iex> errors_on(%User{}, %{password: "password"})
[password: "is unsafe", name: "is blank"]
You could then write your assertion like:
assert {:password, "is unsafe"} in errors_on(%User{}, %{password: "password"})
You can also create the changeset manually and retrieve the errors
field directly:
iex> changeset = User.changeset(%User{}, password: "password")
iex> {:password, "is unsafe"} in changeset.errors
true
"""
def errors_on(struct, data) do
struct.__struct__.changeset(struct, data)
|> Ecto.Changeset.traverse_errors(&Asciinema.ErrorHelpers.translate_error/1)
|> Enum.flat_map(fn {key, errors} -> for msg <- errors, do: {key, msg} end)
end
end

@ -0,0 +1,4 @@
ExUnit.start
Ecto.Adapters.SQL.Sandbox.mode(Asciinema.Repo, :manual)

@ -0,0 +1,21 @@
defmodule Asciinema.ErrorViewTest do
use Asciinema.ConnCase, async: true
# Bring render/3 and render_to_string/3 for testing custom views
import Phoenix.View
test "renders 404.html" do
assert render_to_string(Asciinema.ErrorView, "404.html", []) =~
"not found"
end
test "render 500.html" do
assert render_to_string(Asciinema.ErrorView, "500.html", []) ==
"Internal server error"
end
test "render any other" do
assert render_to_string(Asciinema.ErrorView, "505.html", []) ==
"Internal server error"
end
end

@ -0,0 +1,3 @@
defmodule Asciinema.LayoutViewTest do
use Asciinema.ConnCase, async: true
end

@ -0,0 +1,3 @@
defmodule Asciinema.PageViewTest do
use Asciinema.ConnCase, async: true
end

@ -0,0 +1,37 @@
defmodule Asciinema.UserSocket do
use Phoenix.Socket
## Channels
# channel "room:*", Asciinema.RoomChannel
## Transports
transport :websocket, Phoenix.Transports.WebSocket
# transport :longpoll, Phoenix.Transports.LongPoll
# Socket params are passed from the client and can
# be used to verify and authenticate a user. After
# verification, you can put default assigns into
# the socket that will be set for all channels, ie
#
# {:ok, assign(socket, :user_id, verified_user_id)}
#
# To deny connection, return `:error`.
#
# See `Phoenix.Token` documentation for examples in
# performing token verification on connect.
def connect(_params, socket) do
{:ok, socket}
end
# Socket id's are topics that allow you to identify all sockets for a given user:
#
# def id(socket), do: "users_socket:#{socket.assigns.user_id}"
#
# Would allow you to broadcast a "disconnect" event and terminate
# all active sockets and channels for a given user:
#
# Asciinema.Endpoint.broadcast("users_socket:#{user.id}", "disconnect", %{})
#
# Returning `nil` makes this socket anonymous.
def id(_socket), do: nil
end

@ -0,0 +1,12 @@
defmodule Asciinema.AsciicastAnimationController do
use Asciinema.Web, :controller
alias Asciinema.{Repo, Asciicast}
def show(conn, %{"id" => id}) do
asciicast = Repo.one!(Asciicast.by_id_or_secret_token(id))
conn
|> put_layout("simple.html")
|> render("show.html", asciicast: asciicast)
end
end

@ -0,0 +1,23 @@
defmodule Asciinema.AsciicastFileController do
use Asciinema.Web, :controller
alias Asciinema.{Repo, Asciicast}
def show(conn, %{"id" => id} = params) do
asciicast = Repo.one!(Asciicast.by_id_or_secret_token(id))
path = Asciicast.json_store_path(asciicast)
filename = download_filename(asciicast, params)
file_store().serve_file(conn, path, filename)
end
defp download_filename(%Asciicast{id: id}, %{"dl" => _}) do
"asciicast-#{id}.json"
end
defp download_filename(_asciicast, _params) do
nil
end
defp file_store do
Application.get_env(:asciinema, :file_store)
end
end

@ -0,0 +1,26 @@
defmodule Asciinema.AsciicastImageController do
use Asciinema.Web, :controller
alias Asciinema.{Repo, Asciicast, PngGenerator}
alias Plug.MIME
@max_age 604800 # 7 days
def show(conn, %{"id" => id} = _params) do
asciicast = Repo.one!(Asciicast.by_id_or_secret_token(id))
user = Repo.preload(asciicast, :user).user
png_params = Asciicast.png_params(asciicast, user)
case PngGenerator.generate(asciicast, png_params) do
{:ok, png_path} ->
conn
|> put_resp_header("content-type", MIME.path(png_path))
|> put_resp_header("cache-control", "public, max-age=#{@max_age}")
|> send_file(200, png_path)
|> halt
{:error, :busy} ->
conn
|> put_resp_header("retry-after", "5")
|> send_resp(503, "")
end
end
end

@ -0,0 +1,27 @@
defmodule Asciinema.DocController do
use Asciinema.Web, :controller
alias Asciinema.{DocView, ErrorView}
@topics ["how-it-works", "getting-started", "installation", "usage", "config", "embedding", "faq"]
def index(conn, _params) do
redirect conn, to: doc_path(conn, :show, :"getting-started")
end
def show(conn, %{"topic" => topic}) when topic in @topics do
topic = String.to_atom(topic)
conn
|> assign(:topic, topic)
|> assign(:page_title, DocView.title_for(topic))
|> put_layout(:docs)
|> render("#{topic}.html")
end
def show(conn, _params) do
conn
|> put_status(404)
|> render(ErrorView, "404.html")
end
end

@ -0,0 +1,24 @@
defmodule Asciinema.Gettext do
@moduledoc """
A module providing Internationalization with a gettext-based API.
By using [Gettext](https://hexdocs.pm/gettext),
your module gains a set of macros for translations, for example:
import Asciinema.Gettext
# Simple translation
gettext "Here is the string to translate"
# Plural translation
ngettext "Here is the string to translate",
"Here are the strings to translate",
3
# Domain-based translation
dgettext "errors", "Here is the error message to translate"
See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
"""
use Gettext, otp_app: :asciinema
end

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save