├── log └── .keep ├── .rspec ├── app ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── concerns │ │ └── .keep │ ├── app_build.rb │ └── ios_build.rb ├── assets │ ├── images │ │ └── .keep │ ├── stylesheets │ │ ├── apps.css.scss │ │ ├── ios_apps.css.scss │ │ ├── mobile.css │ │ └── application.css │ └── javascripts │ │ ├── apps.js.coffee │ │ ├── ios_apps.js.coffee │ │ ├── application.js │ │ └── mobile.js ├── controllers │ ├── concerns │ │ └── .keep │ ├── app_listing_module.rb │ ├── application_controller.rb │ ├── apps_controller.rb │ ├── android_apps_controller.rb │ └── ios_apps_controller.rb ├── helpers │ ├── apps_helper.rb │ ├── application_helper.rb │ └── ios_apps_controller.rb ├── views │ ├── shared │ │ ├── _header.html.haml │ │ └── _footer.html.haml │ ├── ios_apps │ │ ├── list_app_builds.html.haml │ │ ├── _release_table.html.haml │ │ ├── show_build.html.haml │ │ ├── show_build.mobile.haml │ │ ├── list_app_builds.mobile.haml │ │ ├── list_app_releases.mobile.haml │ │ └── list_app_releases.html.haml │ ├── layouts │ │ ├── application.mobile.haml │ │ └── application.html.haml │ ├── apps │ │ ├── _app_table.html.haml │ │ ├── index.html.haml │ │ └── index.mobile.haml │ └── android_apps │ │ ├── show_build.html.haml │ │ ├── show_build.mobile.haml │ │ ├── list_app_builds.html.haml │ │ ├── list_app_builds.mobile.haml │ │ ├── list_app_releases.html.haml │ │ └── list_app_releases.mobile.haml └── uploaders │ └── mobileprovision_uploader.rb ├── lib ├── assets │ └── .keep ├── tasks │ ├── .keep │ └── import_apps.rake ├── core_ext │ └── string.rb └── shipmate │ ├── ipa_parser.rb │ ├── apk_parser.rb │ ├── app_importer.rb │ ├── apk_import_strategy.rb │ └── ipa_import_strategy.rb ├── public ├── favicon.ico ├── robots.txt ├── 500.html ├── 422.html └── 404.html ├── vendor └── assets │ ├── javascripts │ ├── .keep │ └── bootstrap.min.js │ └── stylesheets │ ├── .keep │ ├── bootstrap-responsive.min.css │ └── bootstrap-responsive.css ├── config ├── initializers │ ├── core_ext.rb │ ├── session_store.rb │ ├── filter_parameter_logging.rb │ ├── mime_types.rb │ ├── backtrace_silencers.rb │ ├── wrap_parameters.rb │ ├── secret_token.rb │ └── inflections.rb ├── boot.rb ├── environment.rb ├── schedule.rb ├── database.yml ├── locales │ └── en.yml ├── routes.rb ├── environments │ ├── development.rb │ ├── test.rb │ └── production.rb └── application.rb ├── spec ├── fixtures │ ├── Tribes-101.ipa │ ├── ChristmasConspiracy.apk │ ├── Go Tomato iPad only.ipa │ └── Go-Tomato-Ad-Hoc-27.ipa ├── lib │ └── shipmate │ │ ├── apk_parser_spec.rb │ │ ├── ipa_parser_spec.rb │ │ ├── app_importer_spec.rb │ │ ├── ipa_import_strategy_spec.rb │ │ └── apk_import_strategy_spec.rb ├── models │ ├── app_build_spec.rb │ └── ios_build_spec.rb ├── controllers │ ├── application_controller_spec.rb │ ├── apps_controller_spec.rb │ ├── android_apps_controller_spec.rb │ └── ios_apps_controller_spec.rb └── spec_helper.rb ├── bin ├── rake ├── bundle ├── rails ├── rspec └── autospec ├── config.ru ├── .travis.yml ├── Rakefile ├── db └── seeds.rb ├── app.json ├── .gitignore ├── ssl └── instructions.txt ├── LICENSE ├── Gemfile ├── Guardfile ├── README.md └── Gemfile.lock /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /app/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/helpers/apps_helper.rb: -------------------------------------------------------------------------------- 1 | module AppsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/ios_apps_controller.rb: -------------------------------------------------------------------------------- 1 | module IosAppsControllerHelper 2 | end 3 | -------------------------------------------------------------------------------- /config/initializers/core_ext.rb: -------------------------------------------------------------------------------- 1 | require "#{Rails.root}/lib/core_ext/string.rb" -------------------------------------------------------------------------------- /spec/fixtures/Tribes-101.ipa: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmecklem/shipmate/HEAD/spec/fixtures/Tribes-101.ipa -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /spec/fixtures/ChristmasConspiracy.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmecklem/shipmate/HEAD/spec/fixtures/ChristmasConspiracy.apk -------------------------------------------------------------------------------- /spec/fixtures/Go Tomato iPad only.ipa: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmecklem/shipmate/HEAD/spec/fixtures/Go Tomato iPad only.ipa -------------------------------------------------------------------------------- /spec/fixtures/Go-Tomato-Ad-Hoc-27.ipa: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmecklem/shipmate/HEAD/spec/fixtures/Go-Tomato-Ad-Hoc-27.ipa -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../../config/application', __FILE__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /app/views/shared/_header.html.haml: -------------------------------------------------------------------------------- 1 | %div#header 2 | %h1 3 | = link_to 'Shipmate', root_path, :style => 'color:black;' 4 | Keeping your development app distribution ship-shape. -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Shipmate::Application.config.session_store :cookie_store, key: '_shipmate_session' 4 | -------------------------------------------------------------------------------- /app/views/shared/_footer.html.haml: -------------------------------------------------------------------------------- 1 | %div#footer 2 | .container-fluid 3 | .row-fluid 4 | .span1 5 | .span10 6 | %h4 7 | We need a better footer! 8 | .span1 -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | 4 | require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - "2.0.0" 4 | - "2.1.0" 5 | - "2.1.1" 6 | # uncomment this line if your project needs to run something other than `rake`: 7 | # script: bundle exec rspec spec 8 | -------------------------------------------------------------------------------- /app/assets/stylesheets/apps.css.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the apps controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/ios_apps.css.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the IosAppsController controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('../application', __FILE__) 3 | 4 | Mime::Type.register "text/plist", :plist 5 | 6 | # Initialize the Rails application. 7 | Shipmate::Application.initialize! 8 | 9 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /app/assets/javascripts/apps.js.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /app/assets/javascripts/ios_apps.js.coffee: -------------------------------------------------------------------------------- 1 | # Place all the behaviors and hooks related to the matching controller here. 2 | # All this logic will automatically be available in application.js. 3 | # You can use CoffeeScript in this file: http://coffeescript.org/ 4 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | 6 | Shipmate::Application.load_tasks 7 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | # Mime::Type.register_alias "text/html", :iphone 6 | 7 | Mime::Type.register_alias "text/html", :mobile 8 | -------------------------------------------------------------------------------- /app/views/ios_apps/list_app_builds.html.haml: -------------------------------------------------------------------------------- 1 | %h3 2 | = 'Apps : iOS : ' + @app_name + " : Releases : Builds" 3 | 4 | 5 | %table.table.table-striped.table-condensed 6 | %thead 7 | %th 8 | Builds 9 | %tbody 10 | - @app_builds.each do |build| 11 | %tr 12 | %td 13 | = link_to build.build_version, "#" -------------------------------------------------------------------------------- /lib/core_ext/string.rb: -------------------------------------------------------------------------------- 1 | class String 2 | def to_version_string 3 | if !self.nil? 4 | self.split('.').map do |version_part| 5 | if version_part.to_i.to_s == version_part 6 | version_part.to_i.to_s.rjust(10,'0') 7 | else 8 | version_part 9 | end 10 | end 11 | end 12 | end 13 | end -------------------------------------------------------------------------------- /app/views/layouts/application.mobile.haml: -------------------------------------------------------------------------------- 1 | !!! 2 | %html 3 | %head 4 | %title 5 | Shipmate - Apps Listing 6 | 7 | = stylesheet_link_tag 'mobile' 8 | = javascript_include_tag 'mobile' 9 | 10 | = csrf_meta_tags 11 | %meta{ :name => "viewport", :content => "width=device-width, initial-scale=1, maximum-scale=1"} 12 | 13 | %body 14 | = yield 15 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) 7 | # Mayor.create(name: 'Emanuel', city: cities.first) 8 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Shipmate", 3 | "description": "Shipmate is a tool to help deploy mobile applications for testing prior to release to a public app store", 4 | "keywords": [ 5 | "iOS", 6 | "Android", 7 | "apk", 8 | "ipa", 9 | "test", 10 | "ship" 11 | ], 12 | "repository": "https://github.com/tmecklem/shipmate", 13 | "addons": [], 14 | "env": {} 15 | } 16 | -------------------------------------------------------------------------------- /app/views/apps/_app_table.html.haml: -------------------------------------------------------------------------------- 1 | %table.table.table-striped.table-condensed 2 | %thead 3 | %th 4 | App Name 5 | %tbody 6 | - app_names.each do |app_name| 7 | %tr 8 | %td 9 | - if platform == 'ios' 10 | =link_to app_name, list_ios_app_releases_path(app_name) 11 | - elsif platform == 'android' 12 | =link_to app_name, list_android_app_releases_path(app_name) -------------------------------------------------------------------------------- /bin/rspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # This file was generated by Bundler. 4 | # 5 | # The application 'rspec' is installed as part of a gem, and 6 | # this file is here to facilitate running it. 7 | # 8 | 9 | require 'pathname' 10 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile", 11 | Pathname.new(__FILE__).realpath) 12 | 13 | require 'rubygems' 14 | require 'bundler/setup' 15 | 16 | load Gem.bin_path('rspec-core', 'rspec') 17 | -------------------------------------------------------------------------------- /bin/autospec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # This file was generated by Bundler. 4 | # 5 | # The application 'autospec' is installed as part of a gem, and 6 | # this file is here to facilitate running it. 7 | # 8 | 9 | require 'pathname' 10 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile", 11 | Pathname.new(__FILE__).realpath) 12 | 13 | require 'rubygems' 14 | require 'bundler/setup' 15 | 16 | load Gem.bin_path('rspec-core', 'autospec') 17 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /app/views/ios_apps/_release_table.html.haml: -------------------------------------------------------------------------------- 1 | %table.table.table-striped.table-condensed 2 | %thead 3 | %th 4 | Release Version 5 | %tbody 6 | - app_releases.each do |app_release| 7 | %tr 8 | %td 9 | = link_to app_release, list_ios_app_releases_path(app_release) 10 | -#- if platform == 'ios' 11 | -#- elsif platform == 'android' 12 | -# =link_to app_release, list_android_app_releases_path(app_release) -------------------------------------------------------------------------------- /app/views/apps/index.html.haml: -------------------------------------------------------------------------------- 1 | -#%div{:style => 'margin-top:10px;'} 2 | -# %ul.nav.nav-pills 3 | -# %li.active 4 | -# = link_to 'iOS', '#' 5 | -# %li 6 | -# = link_to 'Android','#' 7 | 8 | %h3 9 | Apps : iOS 10 | = render 'app_table', :app_names => @ios_app_names, :platform => 'ios' if !@ios_app_names.nil? 11 | 12 | %h3 13 | Apps : Android 14 | = render 'app_table', :app_names => @android_app_names, :platform => 'android' if !@android_app_names.nil? 15 | 16 | -------------------------------------------------------------------------------- /lib/shipmate/ipa_parser.rb: -------------------------------------------------------------------------------- 1 | module Shipmate 2 | 3 | class IpaParser 4 | 5 | attr_accessor :ipa_file 6 | 7 | def initialize(ipa_file) 8 | @ipa_file = ipa_file 9 | end 10 | 11 | def parse_plist 12 | ipa_info = nil 13 | begin 14 | IPA::IPAFile.open(@ipa_file) do |ipa| 15 | ipa_info = ipa.info 16 | end 17 | rescue Zip::ZipError => e 18 | puts e 19 | end 20 | ipa_info 21 | end 22 | 23 | end 24 | 25 | end -------------------------------------------------------------------------------- /app/views/ios_apps/show_build.html.haml: -------------------------------------------------------------------------------- 1 | %div{:"data-role" => 'page'} 2 | %div{:"data-role" => 'header', :"data-add-back-btn"=>'true'} 3 | %h1=@app_build.build_version 4 | %div{:"data-role" => 'content'} 5 | %p 6 | %a.ui-btn.ui-btn-icon-right.ui-icon-carat-r{:href => "itms-services://?action=download-manifest&url=#{CGI.escape(show_ios_build_manifest_url(@app_build.app_name, @app_build.build_version, :format => :plist))}"}=@app_build.build_version 7 | %div{:"data-role" => 'footer'} 8 | %h2=link_to('Home',root_path, :class=>"ui-btn") -------------------------------------------------------------------------------- /app/views/ios_apps/show_build.mobile.haml: -------------------------------------------------------------------------------- 1 | %div{:"data-role" => 'page'} 2 | %div{:"data-role" => 'header', :"data-add-back-btn"=>'true'} 3 | %h1=@app_build.build_version 4 | %div{:"data-role" => 'content'} 5 | %p 6 | %a.ui-btn.ui-btn-icon-right.ui-icon-carat-r{:href => "itms-services://?action=download-manifest&url=#{CGI.escape(show_ios_build_manifest_url(@app_build.app_name, @app_build.build_version, :format => :plist))}"}=@app_build.build_version 7 | %div{:"data-role" => 'footer'} 8 | %h2=link_to('Home',root_path, :class=>"ui-btn") -------------------------------------------------------------------------------- /app/views/android_apps/show_build.html.haml: -------------------------------------------------------------------------------- 1 | %div{:"data-role" => 'page'} 2 | %div{:"data-role" => 'header', :"data-add-back-btn"=>'true'} 3 | %h1=@app_build.build_version 4 | %div{:"data-role" => 'content'} 5 | %p 6 | %a.ui-btn.ui-btn-icon-right.ui-icon-carat-r{:href => "itms-services://?action=download-manifest&url=#{CGI.escape(show_android_build_manifest_url(@app_build.app_name, @app_build.build_version, :format => :plist))}"}=@app_build.build_version 7 | %div{:"data-role" => 'footer'} 8 | %h2=link_to('Home',root_path, :class=>"ui-btn") -------------------------------------------------------------------------------- /app/views/android_apps/show_build.mobile.haml: -------------------------------------------------------------------------------- 1 | %div{:"data-role" => 'page'} 2 | %div{:"data-role" => 'header', :"data-add-back-btn"=>'true'} 3 | %h1=@app_build.build_version 4 | %div{:"data-role" => 'content'} 5 | %p 6 | %a.ui-btn.ui-btn-icon-right.ui-icon-carat-r{:href => "itms-services://?action=download-manifest&url=#{CGI.escape(show_android_build_manifest_url(@app_build.app_name, @app_build.build_version, :format => :plist))}"}=@app_build.build_version 7 | %div{:"data-role" => 'footer'} 8 | %h2=link_to('Home',root_path, :class=>"ui-btn") -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters) 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /app/views/ios_apps/list_app_builds.mobile.haml: -------------------------------------------------------------------------------- 1 | %div{:"data-role" => 'page'} 2 | %div{:"data-role" => 'header', :"data-add-back-btn"=>'true'} 3 | %h1=@app_release 4 | %div{:"data-role" => 'content'} 5 | - @app_builds.each do |app_build| 6 | %p 7 | %a.ui-btn.ui-btn-icon-right.ui-icon-carat-r{:href => "itms-services://?action=download-manifest&url=#{CGI.escape(show_ios_build_manifest_url(@app_name, app_build.build_version, :format => :plist))}"}=app_build.build_version 8 | %div{:"data-role" => 'footer'} 9 | %h2=link_to('Home',root_path, :class=>"ui-btn") 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/assets/stylesheets/mobile.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the top of the 9 | * compiled file, but it's generally better to create a new file per style scope. 10 | * 11 | *= require_self 12 | *= require jquery.mobile 13 | */ 14 | -------------------------------------------------------------------------------- /app/views/android_apps/list_app_builds.html.haml: -------------------------------------------------------------------------------- 1 | %div{:"data-role" => 'page'} 2 | %div{:"data-role" => 'header', :"data-add-back-btn"=>'true'} 3 | %h1=@app_release 4 | %div{:"data-role" => 'content'} 5 | - @app_builds.each do |app_build| 6 | %p 7 | %a.ui-btn.ui-btn-icon-right.ui-icon-carat-r{:"data-ajax" => "false", :href => "#{@base_build_directory}/#{app_build.app_name}/#{app_build.build_version}/#{app_build.app_name}-#{app_build.build_version}.apk"}=app_build.build_version 8 | %div{:"data-role" => 'footer'} 9 | %h2=link_to('Home',root_path, :class=>"ui-btn") 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/views/android_apps/list_app_builds.mobile.haml: -------------------------------------------------------------------------------- 1 | %div{:"data-role" => 'page'} 2 | %div{:"data-role" => 'header', :"data-add-back-btn"=>'true'} 3 | %h1=@app_release 4 | %div{:"data-role" => 'content'} 5 | - @app_builds.each do |app_build| 6 | %p 7 | %a.ui-btn.ui-btn-icon-right.ui-icon-carat-r{:"data-ajax" => "false", :href => "#{@base_build_directory}/#{app_build.app_name}/#{app_build.build_version}/#{app_build.app_name}-#{app_build.build_version}.apk"}=app_build.build_version 8 | %div{:"data-role" => 'footer'} 9 | %h2=link_to('Home',root_path, :class=>"ui-btn") 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /lib/tasks/import_apps.rake: -------------------------------------------------------------------------------- 1 | require 'shipmate/app_importer' 2 | require 'shipmate/apk_import_strategy' 3 | require 'shipmate/ipa_import_strategy' 4 | 5 | namespace :cron do 6 | 7 | desc "Import any apps waiting in the rails import directory" 8 | task :import_apps do 9 | importer = Shipmate::AppImporter.new 10 | importer.add_strategy Shipmate::IpaImportStrategy.new(Shipmate::Application.config.import_dir, Shipmate::Application.config.ios_dir) 11 | importer.add_strategy Shipmate::ApkImportStrategy.new(Shipmate::Application.config.import_dir, Shipmate::Application.config.android_dir) 12 | importer.import_apps 13 | end 14 | 15 | end -------------------------------------------------------------------------------- /config/schedule.rb: -------------------------------------------------------------------------------- 1 | # Use this file to easily define all of your cron jobs. 2 | # 3 | # It's helpful, but not entirely necessary to understand cron before proceeding. 4 | # http://en.wikipedia.org/wiki/Cron 5 | 6 | #set :output, ("log","whenever_jobs.log") 7 | 8 | every 1.minute do 9 | rake 'cron:import_apps' 10 | end 11 | 12 | # Example: 13 | # 14 | # 15 | # every 2.hours do 16 | # command "/usr/bin/some_great_command" 17 | # runner "MyModel.some_method" 18 | # rake "some:great:rake:task" 19 | # end 20 | # 21 | # every 4.days do 22 | # runner "AnotherModel.prune_old_records" 23 | # end 24 | 25 | # Learn more: http://github.com/javan/whenever 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore the default SQLite database. 11 | /db/*.sqlite3 12 | /db/*.sqlite3-journal 13 | 14 | # Ignore all logfiles and tempfiles. 15 | /log/*.log 16 | /tmp 17 | public/apps 18 | ssl/server.crt 19 | ssl/server.csr 20 | ssl/server.key 21 | server.crt 22 | public/import 23 | .elasticbeanstalk/ 24 | -------------------------------------------------------------------------------- /app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require bootstrap -------------------------------------------------------------------------------- /lib/shipmate/apk_parser.rb: -------------------------------------------------------------------------------- 1 | require 'apktools/apkxml' 2 | require 'nokogiri' 3 | 4 | module Shipmate 5 | 6 | class ApkParser 7 | 8 | attr_accessor :apk_name 9 | 10 | def initialize apk_name 11 | @apk_name = apk_name 12 | end 13 | 14 | def parse_manifest 15 | xml = ApkXml.new(@apk_name) 16 | manifest_xml = xml.parse_xml("AndroidManifest.xml", true, true) 17 | xml_doc = Nokogiri::XML(manifest_xml) 18 | 19 | app_name = xml_doc.at_xpath("/manifest/application")["android:label"] 20 | app_version = xml_doc.first_element_child.attributes["versionName"].value 21 | 22 | {"app_name" => app_name, "app_version" => app_version} 23 | end 24 | end 25 | 26 | end -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | development: 7 | adapter: sqlite3 8 | database: db/development.sqlite3 9 | pool: 5 10 | timeout: 5000 11 | 12 | # Warning: The database defined as "test" will be erased and 13 | # re-generated from your development database when you run "rake". 14 | # Do not set this db to the same as development or production. 15 | test: 16 | adapter: sqlite3 17 | database: db/test.sqlite3 18 | pool: 5 19 | timeout: 5000 20 | 21 | production: 22 | adapter: sqlite3 23 | database: db/production.sqlite3 24 | pool: 5 25 | timeout: 5000 26 | -------------------------------------------------------------------------------- /config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure your secret_key_base is kept private 11 | # if you're sharing your code publicly. 12 | Shipmate::Application.config.secret_key_base = 'fcad2ebcbc5e91dea0b87ca5758228c3836a20ac8139acb17cd2db76c1359c16fdfe930410b9a777f3b0f4202089713e6bc6e20576853c7822515db237daa860' 13 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /app/assets/javascripts/mobile.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require jquery 14 | //= require jquery_ujs 15 | //= require jquery.mobile 16 | //= require turbolinks -------------------------------------------------------------------------------- /app/controllers/app_listing_module.rb: -------------------------------------------------------------------------------- 1 | require 'ios_build' 2 | 3 | module AppListingModule 4 | 5 | IOS_APP_TYPE = 1 6 | ANDROID_APP_TYPE = 2 7 | 8 | def app_builds(app_name,app_dir,app_type) 9 | app_builds = subdirectories(app_dir.join(app_name)).map do |build_version| 10 | if app_type == IOS_APP_TYPE 11 | IosBuild.new(app_dir, app_name, build_version) 12 | elsif app_type == ANDROID_APP_TYPE 13 | AppBuild.new(app_dir, app_name, build_version) 14 | end 15 | 16 | end 17 | app_builds.sort.reverse 18 | end 19 | 20 | def subdirectories(dir) 21 | Dir.entries(dir).select do |entry| 22 | File.directory?(File.join(dir, entry)) and not entry.eql?('.') and not entry.eql?('..') 23 | end 24 | end 25 | 26 | end -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | # Prevent CSRF attacks by raising an exception. 3 | # For APIs, you may want to use :null_session instead. 4 | protect_from_forgery with: :exception 5 | 6 | before_filter :prepare_for_mobile_views 7 | 8 | before_action do 9 | @device_type = :iphone if browser.iphone? or browser.ipod? 10 | @device_type = :ipad if browser.ipad? 11 | @device_type = :android if browser.android? 12 | @device_type ||= :desktop 13 | end 14 | 15 | private 16 | 17 | def prepare_for_mobile_views 18 | if browser.iphone? or browser.ipod? or browser.ipad? or browser.android? 19 | request.format = :mobile unless request.format == :plist 20 | end 21 | end 22 | 23 | end 24 | -------------------------------------------------------------------------------- /lib/shipmate/app_importer.rb: -------------------------------------------------------------------------------- 1 | module Shipmate 2 | class AppImporter 3 | 4 | attr_reader :import_strategies 5 | 6 | def initialize 7 | super 8 | @import_strategies = [] 9 | end 10 | 11 | def add_strategy(strategy) 12 | @import_strategies << strategy 13 | end 14 | 15 | def import_apps 16 | @import_strategies.each do |import_strategy| 17 | app_files = Dir.glob("#{import_strategy.import_dir}/**/*").reject { |entry| !entry.upcase.end_with?(import_strategy.app_extension.upcase) } 18 | app_files.each do |app_file| 19 | begin 20 | import_strategy.import_app app_file 21 | rescue StandardError => e 22 | puts "Unable to import #{app_file}: #{e}" 23 | end 24 | end 25 | end 26 | end 27 | 28 | end 29 | end -------------------------------------------------------------------------------- /app/models/app_build.rb: -------------------------------------------------------------------------------- 1 | class AppBuild 2 | 3 | include Comparable 4 | 5 | attr_accessor :apps_dir, :app_name, :build_version 6 | 7 | def initialize(apps_dir, app_name, build_version) 8 | @apps_dir = apps_dir 9 | @app_name = app_name 10 | @build_version = build_version 11 | end 12 | 13 | def build_file_root_path 14 | @apps_dir.join(app_name, build_version) 15 | end 16 | 17 | def release 18 | @build_version.split('.')[0...-1].join('.') 19 | end 20 | 21 | def supports_device?(device) 22 | device_families.include? device 23 | end 24 | 25 | def <=>(other) 26 | self.build_version.to_version_string <=> other.build_version.to_version_string 27 | end 28 | 29 | def ==(other) 30 | self.apps_dir==other.apps_dir && self.app_name==other.app_name && self.build_version==other.build_version 31 | end 32 | end -------------------------------------------------------------------------------- /app/views/apps/index.mobile.haml: -------------------------------------------------------------------------------- 1 | %div{:"data-role" => 'page'} 2 | %div{:"data-role" => 'content'} 3 | - if !@ios_app_names.nil? && @ios_app_names.size > 0 4 | %div{:"data-role"=>"collapsible", :"data-collapsed"=>"false", :"data-theme"=>"b", :"data-inset"=>"false"} 5 | %h2="iOS Apps" 6 | - @ios_app_names.each do |app_name| 7 | %p=link_to(app_name, list_ios_app_releases_path(app_name), :class => "ui-btn ui-btn-icon-right ui-icon-carat-r") 8 | - if !@android_app_names.nil? && @android_app_names.size > 0 9 | %div{:"data-role"=>"collapsible", :"data-collapsed"=>"false", :"data-theme"=>"b", :"data-inset"=>"false"} 10 | %h2="Android Apps" 11 | - @android_app_names.each do |app_name| 12 | %p=link_to(app_name, list_android_app_releases_path(app_name), :class => "ui-btn ui-btn-icon-right ui-icon-carat-r") 13 | -------------------------------------------------------------------------------- /app/views/android_apps/list_app_releases.html.haml: -------------------------------------------------------------------------------- 1 | %div{:"data-role" => 'page'} 2 | %div{:"data-role" => 'header', :"data-add-back-btn"=>'true'} 3 | %h1=@app_name 4 | %div{:"data-role" => 'content'} 5 | - @app_releases.each do |app_release| 6 | %div{:"data-role"=>"collapsible"} 7 | %h2="Release #{app_release}" 8 | %a.ui-btn.ui-btn-b.ui-btn-icon-right.ui-icon-carat-r{:"data-ajax" => "false", :href => "#{@base_build_directory}/#{@app_name}/#{@most_recent_build_hash[app_release].build_version}/#{@app_name}-#{@most_recent_build_hash[app_release].build_version}.apk"}="Install #{@most_recent_build_hash[app_release].build_version}" 9 | =link_to("More Builds", list_android_app_builds_path(@app_name, app_release), :class => "ui-btn ui-mini ui-btn-icon-right ui-icon-carat-r") 10 | %p 11 | %div{:"data-role" => 'footer'} 12 | %h2=link_to('Home',root_path, :class=>"ui-btn") -------------------------------------------------------------------------------- /spec/lib/shipmate/apk_parser_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'shipmate/apk_parser' 3 | 4 | describe Shipmate::ApkParser do 5 | 6 | let(:apk_file) { Rails.root.join('spec','fixtures','ChristmasConspiracy.apk') } 7 | let(:apk_parser) { Shipmate::ApkParser.new(apk_file) } 8 | 9 | describe "#initialize" do 10 | it 'stores a property of the apk file' do 11 | expect(apk_parser.apk_name.to_s).to include("ChristmasConspiracy.apk") 12 | end 13 | end 14 | 15 | describe "#parse_manifest" do 16 | 17 | it 'returns a hash' do 18 | expect(apk_parser.parse_manifest).to_not eq nil 19 | end 20 | 21 | it 'returns a Hash containing the correct app name' do 22 | expect(apk_parser.parse_manifest["app_name"]).to eq "Christmas Conspiracy" 23 | end 24 | 25 | it 'returns a Hash containing the correct app version' do 26 | expect(apk_parser.parse_manifest["app_version"]).to eq "1.0" 27 | end 28 | end 29 | 30 | end -------------------------------------------------------------------------------- /spec/models/app_build_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe AppBuild do 4 | 5 | let(:apps_dir) { Pathname.new(Dir.mktmpdir) } 6 | let(:app_name) { "Go Tomato" } 7 | let(:build_version) { "1.0.27" } 8 | let(:app_build) {AppBuild.new(apps_dir, app_name, build_version)} 9 | let(:expected_build_file_root_path) { Pathname.new("#{apps_dir}/Go Tomato/1.0.27") } 10 | 11 | describe '#initialize' do 12 | 13 | it 'stores properties for the root apps_dir, app_name, and build_version that identify ' do 14 | expect(app_build.apps_dir).to eq(apps_dir) 15 | expect(app_build.app_name).to eq "Go Tomato" 16 | expect(app_build.build_version).to eq "1.0.27" 17 | end 18 | 19 | end 20 | 21 | describe '#build_file_root_path' do 22 | it 'returns the location of where the ipa file should be' do 23 | expect(app_build.build_file_root_path).to eq(expected_build_file_root_path) 24 | end 25 | end 26 | end -------------------------------------------------------------------------------- /app/views/layouts/application.html.haml: -------------------------------------------------------------------------------- 1 | !!! XML 2 | !!! 3 | %html 4 | %head 5 | %title 6 | Shipmate - Apps Listing 7 | 8 | -#%meta{ :name => "viewport", :content => "width=device-width, initial-scale=1.0, width=device-width, initial-scale=1.0" } 9 | %link{ :rel => "stylesheet", :type => "text/css", :href => "https://fonts.googleapis.com/css?family=Open+Sans" } 10 | 11 | = stylesheet_link_tag 'application' 12 | = javascript_include_tag 'application' 13 | 14 | = csrf_meta_tags 15 | 16 | %body 17 | #wrap 18 | .container-fluid 19 | .row-fluid 20 | .span1 21 | .span10 22 | = render 'shared/header' 23 | .span1 24 | 25 | .container-fluid#content 26 | .row-fluid 27 | .span1 28 | .span10 29 | = yield 30 | .span1 31 | 32 | #push 33 | 34 | = render 'shared/footer' 35 | 36 | -------------------------------------------------------------------------------- /app/views/android_apps/list_app_releases.mobile.haml: -------------------------------------------------------------------------------- 1 | %div{:"data-role" => 'page'} 2 | %div{:"data-role" => 'header', :"data-add-back-btn"=>'true'} 3 | %h1=@app_name 4 | %div{:"data-role" => 'content'} 5 | - @app_releases.each_with_index do |app_release,index| 6 | %div{:"data-role"=>"collapsible", :"data-collapsed"=>(index!=0).to_s} 7 | %h2="Release #{app_release}" 8 | %a.ui-btn.ui-btn-b.ui-btn-icon-right.ui-icon-carat-r{:"data-ajax" => "false", :href => "#{@base_build_directory}/#{@app_name}/#{@most_recent_build_hash[app_release].build_version}/#{@app_name}-#{@most_recent_build_hash[app_release].build_version}.apk"}="Install #{@most_recent_build_hash[app_release].build_version}" 9 | =link_to("More Builds", list_android_app_builds_path(@app_name, app_release), :class => "ui-btn ui-mini ui-btn-icon-right ui-icon-carat-r") 10 | %p 11 | %div{:"data-role" => 'footer'} 12 | %h2=link_to('Home',root_path, :class=>"ui-btn") -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Shipmate::Application.routes.draw do 2 | 3 | get "apps" => 'apps#index' 4 | 5 | get "ios_apps/:app_name" => 'ios_apps#list_app_releases', as: :list_ios_app_releases 6 | get "ios_apps/:app_name/:app_release" => 'ios_apps#list_app_builds', :constraints => { :app_release => /[^\/]+/ }, as: :list_ios_app_builds 7 | get "ios_apps/:app_name/:build_version/manifest" => 'ios_apps#show_build_manifest', :constraints => { :build_version => /[^\/]+/ }, as: :show_ios_build_manifest 8 | get "ios_apps/:app_name/:build_version/show" => 'ios_apps#show_build', :constraints => { :build_version => /[^\/]+/ }, as: :show_ios_build 9 | 10 | put 'ios_apps/:app_name/upload_mobileprovision', :to => 'ios_apps#upload_mobileprovision', as: :upload_mobileprovision 11 | 12 | get "android_apps/:app_name" => 'android_apps#list_app_releases', as: :list_android_app_releases 13 | get "android_apps/:app_name/:app_release" => 'android_apps#list_app_builds', :constraints => { :app_release => /[^\/]+/ }, as: :list_android_app_builds 14 | 15 | root 'apps#index' 16 | end 17 | -------------------------------------------------------------------------------- /ssl/instructions.txt: -------------------------------------------------------------------------------- 1 | If you want to test this with iOS 7.1+ apps, you need to front the rails server with an ssl proxy. 2 | 3 | Here's a sample nginx server config section 4 | 5 | server { 6 | listen 443; 7 | ssl on; 8 | server_name localhost; 9 | 10 | #path to your certificate 11 | ssl_certificate /usr/local/etc/nginx/ssl/server.crt; 12 | # path to your ssl key 13 | ssl_certificate_key /usr/local/etc/nginx/ssl/server.key; 14 | 15 | # put the rest of your server configuration here. 16 | 17 | root /dev/null; 18 | 19 | location / { 20 | # set X-FORWARDED_PROTO so ssl_requirement plugin works 21 | proxy_set_header X-FORWARDED_PROTO https; 22 | proxy_set_header X-Real-IP $remote_addr; 23 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 24 | proxy_set_header Host $http_host; 25 | proxy_redirect off; 26 | proxy_pass http://localhost:3000; 27 | } 28 | } -------------------------------------------------------------------------------- /spec/lib/shipmate/ipa_parser_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'shipmate/ipa_parser' 3 | 4 | describe Shipmate::IpaParser do 5 | 6 | let(:ipa_file) { Rails.root.join('spec','fixtures','Go-Tomato-Ad-Hoc-27.ipa') } 7 | let(:ipa_parser) { Shipmate::IpaParser.new(ipa_file) } 8 | let(:tmp_dir) { Dir.mktmpdir } 9 | 10 | before(:each) do 11 | FileUtils.mkdir_p(tmp_dir) 12 | end 13 | 14 | after (:each) do 15 | FileUtils.remove_entry_secure tmp_dir 16 | end 17 | 18 | describe '#initialize' do 19 | 20 | it 'stores a property of the ipa file' do 21 | expect(ipa_parser.ipa_file).to eq ipa_file 22 | end 23 | 24 | end 25 | 26 | describe '#parse_plist' do 27 | 28 | it 'returns a hash of important information from the ipa\'s info.plist file' do 29 | plist = ipa_parser.parse_plist 30 | expect(plist["CFBundleDisplayName"]).to eq("Go Tomato") 31 | expect(plist["CFBundleName"]).to eq("Go Tomato") 32 | expect(plist["CFBundleIdentifier"]).to eq("com.mecklem.Go-Tomato") 33 | expect(plist["CFBundleVersion"]).to eq("1.0.27") 34 | 35 | end 36 | 37 | end 38 | 39 | end -------------------------------------------------------------------------------- /lib/shipmate/apk_import_strategy.rb: -------------------------------------------------------------------------------- 1 | require 'shipmate/apk_parser' 2 | 3 | module Shipmate 4 | 5 | class ApkImportStrategy 6 | 7 | attr_reader :import_dir, :apps_dir 8 | 9 | def app_extension 10 | "apk" 11 | end 12 | 13 | def initialize import_dir, apps_dir 14 | @import_dir = import_dir 15 | @apps_dir = apps_dir 16 | 17 | FileUtils.mkdir_p(@import_dir) 18 | FileUtils.mkdir_p(@apps_dir) 19 | end 20 | 21 | def create_app_directory app_name, app_version 22 | FileUtils.mkdir_p(@apps_dir.join(app_name, app_version)) 23 | end 24 | 25 | def move_apk_file apk_file, app_name, app_version 26 | FileUtils.mv(apk_file, @apps_dir.join(app_name, app_version, "#{app_name}-#{app_version}.apk")) 27 | end 28 | 29 | def import_app apk_file 30 | apk_parser = Shipmate::ApkParser.new(apk_file) 31 | app_name = apk_parser.parse_manifest["app_name"] 32 | app_version = apk_parser.parse_manifest["app_version"] 33 | 34 | create_app_directory app_name, app_version 35 | move_apk_file apk_file, app_name, app_version 36 | end 37 | 38 | end 39 | 40 | end -------------------------------------------------------------------------------- /app/views/ios_apps/list_app_releases.mobile.haml: -------------------------------------------------------------------------------- 1 | %div{:"data-role" => 'page'} 2 | %div{:"data-role" => 'header', :"data-add-back-btn"=>'true'} 3 | %h1=@app_name 4 | %div{:"data-role" => 'content'} 5 | - @app_releases.each_with_index do |app_release, index| 6 | %div{:"data-role"=>"collapsible", :"data-collapsed"=>(index!=0).to_s} 7 | %h2="Release #{app_release}" 8 | %a.ui-btn.ui-btn-b.ui-btn-icon-right.ui-icon-carat-r{:href => "itms-services://?action=download-manifest&url=#{CGI.escape(show_ios_build_manifest_url(@app_name, @most_recent_build_hash[app_release].build_version, :format => :plist))}"}="Install #{@most_recent_build_hash[app_release].build_version}" 9 | =link_to("More Builds", list_ios_app_builds_path(@app_name, app_release), :class => "ui-btn ui-mini ui-btn-icon-right ui-icon-carat-r") 10 | %p 11 | - if not @mobileprovision.nil? 12 | %a{:href => @mobileprovision, :"data-ajax" => "false", :class => "ui-btn ui-mini ui-icon-carat-r ui-btn-icon-right"}="Install mobileprovision" 13 | %div{:"data-role" => 'footer'} 14 | %h2=link_to('Home',root_path, :class=>"ui-btn") 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Timothy Mecklem 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /app/views/ios_apps/list_app_releases.html.haml: -------------------------------------------------------------------------------- 1 | %h3 2 | = 'Apps : iOS : ' + @app_name + " : Releases" 3 | 4 | .container-fluid 5 | .row-fluid 6 | .span8{:style => 'border-right: 1px solid silver; padding:0px 10px 0px 0px;'} 7 | %table.table.table-striped.table-condensed 8 | %thead 9 | %th 10 | Release Version 11 | %tbody 12 | - @app_releases.each do |app_release| 13 | %tr 14 | %td 15 | = link_to app_release, list_ios_app_builds_path(params['app_name'], app_release) 16 | 17 | .span4 18 | %div{:style => 'border:0px solid silver; padding:5px;'} 19 | %h4 Mobile Provisioning File 20 | 21 | - unless @mobileprovision.nil? 22 | = link_to @mobileprovision_file_name, @mobileprovision 23 | 24 | %div{:style => 'margin-top:15px; padding-top:10px; border-top:1px solid silver;'} 25 | = form_tag upload_mobileprovision_path, :method => :put, :app_name => params['app_name'], :multipart => true do 26 | = file_field_tag 'upload_mobileprovision[file]' 27 | = submit_tag 'Upload', :class => 'btn btn-mini btn-primary' -------------------------------------------------------------------------------- /app/controllers/apps_controller.rb: -------------------------------------------------------------------------------- 1 | require 'browser' 2 | 3 | class AppsController < ApplicationController 4 | 5 | include AppListingModule 6 | 7 | attr_accessor :ios_dir 8 | attr_accessor :android_dir 9 | 10 | def initialize 11 | @ios_dir,@android_dir = app_dirs = [Shipmate::Application.config.ios_dir,Shipmate::Application.config.android_dir] 12 | app_dirs.each do |app_dir| 13 | FileUtils.mkdir_p(app_dir) 14 | end 15 | super 16 | end 17 | 18 | def index 19 | @ios_app_names = app_names(@ios_dir) if [:iphone,:ipad,:desktop].include?(@device_type) 20 | @android_app_names = app_names(@android_dir) if [:android,:desktop].include?(@device_type) 21 | end 22 | 23 | def app_names(app_dir) 24 | app_type = app_dir == @ios_dir ? IOS_APP_TYPE : ANDROID_APP_TYPE 25 | subdirectories(app_dir).sort.select do |app_name| 26 | app_builds = self.app_builds(app_name,app_dir,app_type) 27 | 28 | if app_type == IOS_APP_TYPE # Only iOS has different apps for phone and tablets 29 | (app_builds.first && app_builds.first.supports_device?(@device_type)) || @device_type == :desktop 30 | end 31 | 32 | app_builds 33 | end 34 | end 35 | 36 | end 37 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rails', '4.0.3' 4 | gem 'sass-rails', '~> 4.0.0' 5 | gem 'uglifier', '>= 1.3.0' 6 | gem 'coffee-rails', '~> 4.0.0' 7 | gem 'jquery-rails', '~> 3.1.0' 8 | gem 'jquery_mobile_rails', '~> 1.4.1' #mobile html5 framework 9 | 10 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder 11 | gem 'jbuilder', '~> 1.2' 12 | 13 | group :doc do 14 | # bundle exec rake doc:rails generates the API under doc/api. 15 | gem 'sdoc', require: false 16 | end 17 | 18 | gem 'haml-rails', '~> 0.5.3' 19 | gem 'ipa', :git => 'git://github.com/sjmulder/ipa' #ipa and plist parsing support 20 | 21 | group :development, :test do 22 | gem 'rspec-rails', '~> 2.14.2' 23 | gem 'guard-rspec', '~> 4.2.8' 24 | gem 'guard-rails', '~> 0.5.0' 25 | gem 'terminal-notifier-guard', '~> 1.5.3', require: false 26 | gem 'pry' 27 | gem 'pry-byebug' 28 | end 29 | 30 | group :development do 31 | gem "better_errors" 32 | gem "binding_of_caller" 33 | end 34 | 35 | gem 'whenever', '~> 0.9.0' #cron 36 | gem 'browser', '~> 0.4.0' #device family detection 37 | 38 | # Required to parse apk files 39 | gem "apktools", '~> 0.6.0' 40 | gem "nokogiri", '~> 1.6.1' 41 | 42 | # For uploading files 43 | gem 'carrierwave', '0.10.0' 44 | -------------------------------------------------------------------------------- /app/controllers/android_apps_controller.rb: -------------------------------------------------------------------------------- 1 | require 'shipmate/apk_parser' 2 | 3 | class AndroidAppsController < AppsController 4 | 5 | def list_app_builds 6 | @app_name = params[:app_name] 7 | @app_release = params[:app_release] 8 | @base_build_directory = public_url_for_build_directory 9 | 10 | @app_builds = self.app_builds(@app_name, @android_dir, ANDROID_APP_TYPE).select do |app_build| 11 | app_build.release.eql?(@app_release) 12 | end 13 | 14 | end 15 | 16 | def list_app_releases 17 | @app_name = params[:app_name] 18 | app_builds = self.app_builds(@app_name, @android_dir, ANDROID_APP_TYPE) 19 | 20 | @most_recent_build_hash = most_recent_build_by_release(app_builds) 21 | @base_build_directory = public_url_for_build_directory 22 | @app_releases = @most_recent_build_hash.keys.sort{|x,y| y.to_version_string<=>x.to_version_string } 23 | end 24 | 25 | def public_url_for_build_directory 26 | "#{request.base_url}/apps/android" 27 | end 28 | 29 | def most_recent_build_by_release(app_builds) 30 | most_recent_builds_hash = {} 31 | app_builds.each do |app_build| 32 | most_recent_builds_hash[app_build.release] ||= app_build 33 | end 34 | most_recent_builds_hash 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Shipmate::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports and disable caching. 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send. 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger. 20 | config.active_support.deprecation = :log 21 | 22 | # Raise an error on page load if there are pending migrations 23 | # config.active_record.migration_error = :page_load 24 | 25 | # Debug mode disables concatenation and preprocessing of assets. 26 | # This option may cause significant delays in view rendering with a large 27 | # number of complex assets. 28 | config.assets.debug = true 29 | end 30 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # A sample Guardfile 2 | # More info at https://github.com/guard/guard#readme 3 | 4 | guard 'rails' do 5 | watch('Gemfile.lock') 6 | watch(%r{^(config|lib)/.*}) 7 | end 8 | 9 | 10 | guard :rspec do 11 | watch(%r{^spec/.+_spec\.rb$}) 12 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } 13 | watch('spec/spec_helper.rb') { "spec" } 14 | 15 | # Rails example 16 | watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 17 | watch(%r{^app/(.*)(\.erb|\.haml|\.slim)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" } 18 | watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] } 19 | watch(%r{^spec/support/(.+)\.rb$}) { "spec" } 20 | watch('config/routes.rb') { "spec/routing" } 21 | watch('app/controllers/application_controller.rb') { "spec/controllers" } 22 | 23 | # Capybara features specs 24 | watch(%r{^app/views/(.+)/.*\.(erb|haml|slim)$}) { |m| "spec/features/#{m[1]}_spec.rb" } 25 | 26 | # Turnip features and steps 27 | watch(%r{^spec/acceptance/(.+)\.feature$}) 28 | watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance' } 29 | 30 | #notification :growl_notify 31 | end 32 | 33 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | #require 'rails/all' 4 | require "action_controller/railtie" 5 | require "action_mailer/railtie" 6 | require "sprockets/railtie" 7 | #require "rails/test_unit/railtie" 8 | 9 | # Require the gems listed in Gemfile, including any gems 10 | # you've limited to :test, :development, or :production. 11 | Bundler.require(:default, Rails.env) 12 | 13 | 14 | module Shipmate 15 | class Application < Rails::Application 16 | # Settings in config/environments/* take precedence over those specified here. 17 | # Application configuration should go into files in config/initializers 18 | # -- all .rb files in that directory are automatically loaded. 19 | 20 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 21 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 22 | # config.time_zone = 'Central Time (US & Canada)' 23 | 24 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 25 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 26 | # config.i18n.default_locale = :de 27 | 28 | config.apps_dir = Rails.root.join('public','apps') 29 | config.import_dir = Rails.root.join('public','import') 30 | config.ios_dir = config.apps_dir.join('ios') 31 | config.android_dir = config.apps_dir.join('android') 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the top of the 9 | * compiled file, but it's generally better to create a new file per style scope. 10 | * 11 | *= require bootstrap 12 | *= require_self 13 | */ 14 | 15 | html, body { 16 | height: 100%; 17 | /* The html and body elements cannot have any padding or margin. */ 18 | } 19 | 20 | /* Wrapper for page content to push down footer */ 21 | #wrap { 22 | min-height: 100%; 23 | height: auto !important; 24 | height: 100%; 25 | /* Negative indent footer by it's height */ 26 | margin: 0 auto -80px; 27 | } 28 | 29 | /* Set the fixed height of the footer here */ 30 | #push, #footer { 31 | height: 80px; 32 | } 33 | 34 | #header { 35 | padding-bottom:4px; 36 | border-bottom:1px solid silver; 37 | } 38 | 39 | #footer { 40 | background-color: #f5f5f5; 41 | } 42 | 43 | /* Lastly, apply responsive CSS fixes as necessary */ 44 | @media (max-width: 767px) { 45 | #footer { 46 | margin-left: -20px; 47 | margin-right: -20px; 48 | padding-left: 20px; 49 | padding-right: 20px; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |If you are the application owner check the logs for more information.
56 | 57 | 58 | -------------------------------------------------------------------------------- /spec/controllers/application_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe ApplicationController do 4 | 5 | controller do 6 | def index 7 | render :text => 'good' 8 | end 9 | end 10 | 11 | describe 'before_action_detect_browser' do 12 | 13 | it 'assigns device_type to iPhone' do 14 | request.env['HTTP_USER_AGENT'] = "Apple-iPhone5C1/1001.525" 15 | get :index 16 | expect(assigns[:device_type]).to eq :iphone 17 | end 18 | 19 | it 'assigns device_type to iPhone for iPods' do 20 | request.env['HTTP_USER_AGENT'] = "Apple-iPod4C1/902.176" 21 | get :index 22 | expect(assigns[:device_type]).to eq :iphone 23 | end 24 | 25 | it 'assigns device_type to iPad' do 26 | request.env['HTTP_USER_AGENT'] = "Apple-iPad3C2/1001.523" 27 | get :index 28 | expect(assigns[:device_type]).to eq :ipad 29 | end 30 | 31 | it 'assigns device_type to Desktop' do 32 | request.env['HTTP_USER_AGENT'] = "Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1667.0 Safari/537.36" 33 | get :index 34 | expect(assigns[:device_type]).to eq :desktop 35 | end 36 | 37 | it 'assigns device_type to Android' do 38 | request.env['HTTP_USER_AGENT'] = "Mozilla/5.0 (Linux; Android 4.2.1; en-us; Nexus 4 Build/JOP40D) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Mobile Safari/535.19" 39 | get :index 40 | expect(assigns[:device_type]).to eq :android 41 | end 42 | 43 | end 44 | 45 | end -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |Maybe you tried to change something you didn't have access to.
55 |If you are the application owner check the logs for more information.
57 | 58 | 59 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |You may have mistyped the address or the page may have moved.
55 |If you are the application owner check the logs for more information.
57 | 58 | 59 | -------------------------------------------------------------------------------- /lib/shipmate/ipa_import_strategy.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | require 'ipa' 3 | require 'pathname' 4 | require 'digest' 5 | require 'shipmate/ipa_parser' 6 | 7 | module Shipmate 8 | 9 | class IpaImportStrategy 10 | 11 | attr_reader :import_dir, :apps_dir 12 | 13 | def app_extension 14 | "ipa" 15 | end 16 | 17 | def initialize(import_dir, apps_dir) 18 | @import_dir = Pathname.new(import_dir) 19 | @apps_dir = Pathname.new(apps_dir) 20 | 21 | FileUtils.mkdir_p(@import_dir) 22 | FileUtils.mkdir_p(@apps_dir) 23 | end 24 | 25 | def import_app(ipa_file) 26 | ipa_parser = Shipmate::IpaParser.new(ipa_file) 27 | plist_hash = ipa_parser.parse_plist 28 | app_name = plist_hash["CFBundleDisplayName"] 29 | app_version = plist_hash["CFBundleVersion"] 30 | create_app_directory app_name, app_version 31 | # touch_digest_file(calculate_digest(ipa_file), app_name, app_version) 32 | move_ipa_file ipa_file, app_name, app_version 33 | end 34 | 35 | def create_app_directory(app_name, app_version) 36 | FileUtils.mkdir_p(@apps_dir.join(app_name,app_version)) 37 | end 38 | 39 | def move_ipa_file(ipa_file, app_name, app_version) 40 | FileUtils.mv(ipa_file, @apps_dir.join(app_name,app_version,"#{app_name}-#{app_version}.ipa")) 41 | end 42 | 43 | # def touch_digest_file(digest, app_name, app_version) 44 | # FileUtils.touch(@apps_dir.join(app_name,app_version,"#{digest}.sha1")) 45 | # end 46 | 47 | # def calculate_digest(ipa_file) 48 | # Digest::SHA1.hexdigest( File.read(ipa_file) ) 49 | # end 50 | 51 | end 52 | 53 | end -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Shipmate::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure static asset server for tests with Cache-Control for performance. 16 | config.serve_static_assets = true 17 | config.static_cache_control = "public, max-age=3600" 18 | 19 | # Show full error reports and disable caching. 20 | config.consider_all_requests_local = true 21 | config.action_controller.perform_caching = false 22 | 23 | # Raise exceptions instead of rendering exception templates. 24 | config.action_dispatch.show_exceptions = false 25 | 26 | # Disable request forgery protection in test environment. 27 | config.action_controller.allow_forgery_protection = false 28 | 29 | # Tell Action Mailer not to deliver emails to the real world. 30 | # The :test delivery method accumulates sent emails in the 31 | # ActionMailer::Base.deliveries array. 32 | config.action_mailer.delivery_method = :test 33 | 34 | # Print deprecation notices to the stderr. 35 | config.active_support.deprecation = :stderr 36 | end 37 | -------------------------------------------------------------------------------- /app/uploaders/mobileprovision_uploader.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | class MobileprovisionUploader < CarrierWave::Uploader::Base 4 | 5 | # Include RMagick or MiniMagick support: 6 | # include CarrierWave::RMagick 7 | # include CarrierWave::MiniMagick 8 | 9 | # Choose what kind of storage to use for this uploader: 10 | storage :file 11 | # storage :fog 12 | 13 | # Override the directory where uploaded files will be stored. 14 | # This is a sensible default for uploaders that are meant to be mounted: 15 | def store_dir 16 | # "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" 17 | "public/apps/ios/#{model.app_name}" 18 | end 19 | 20 | # Provide a default URL as a default if there hasn't been a file uploaded: 21 | # def default_url 22 | # # For Rails 3.1+ asset pipeline compatibility: 23 | # # ActionController::Base.helpers.asset_path("fallback/" + [version_name, "default.png"].compact.join('_')) 24 | # 25 | # "/images/fallback/" + [version_name, "default.png"].compact.join('_') 26 | # end 27 | 28 | # Process files as they are uploaded: 29 | # process :scale => [200, 300] 30 | # 31 | # def scale(width, height) 32 | # # do something 33 | # end 34 | 35 | # Create different versions of your uploaded files: 36 | # version :thumb do 37 | # process :resize_to_fit => [50, 50] 38 | # end 39 | 40 | # Add a white list of extensions which are allowed to be uploaded. 41 | # For images you might use something like this: 42 | # def extension_white_list 43 | # %w(jpg jpeg gif png) 44 | # end 45 | 46 | # Override the filename of the uploaded files: 47 | # Avoid using model.id or version_name here, see uploader/store.rb for details. 48 | # def filename 49 | # "something.jpg" if original_filename 50 | # end 51 | 52 | end 53 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file is copied to spec/ when you run 'rails generate rspec:install' 2 | ENV["RAILS_ENV"] ||= 'test' 3 | require File.expand_path("../../config/environment", __FILE__) 4 | require 'rspec/rails' 5 | 6 | # Requires supporting ruby files with custom matchers and macros, etc, in 7 | # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are 8 | # run as spec files by default. This means that files in spec/support that end 9 | # in _spec.rb will both be required and run as specs, causing the specs to be 10 | # run twice. It is recommended that you do not name files matching this glob to 11 | # end with _spec.rb. You can configure this pattern with with the --pattern 12 | # option on the command line or in ~/.rspec, .rspec or `.rspec-local`. 13 | Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } 14 | 15 | # Checks for pending migrations before tests are run. 16 | # If you are not using ActiveRecord, you can remove this line. 17 | ActiveRecord::Migration.check_pending! if defined?(ActiveRecord::Migration) 18 | 19 | RSpec.configure do |config| 20 | # ## Mock Framework 21 | # 22 | # If you prefer to use mocha, flexmock or RR, uncomment the appropriate line: 23 | # 24 | # config.mock_with :mocha 25 | # config.mock_with :flexmock 26 | # config.mock_with :rr 27 | 28 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures 29 | #config.fixture_path = "#{::Rails.root}/spec/fixtures" 30 | 31 | # If you're not using ActiveRecord, or you'd prefer not to run each of your 32 | # examples within a transaction, remove the following line or assign false 33 | # instead of true. 34 | #config.use_transactional_fixtures = true 35 | 36 | # Run specs in random order to surface order dependencies. If you find an 37 | # order dependency and want to debug it, you can fix the order by providing 38 | # the seed, which is printed after each run. 39 | # --seed 1234 40 | config.order = "random" 41 | end 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [](https://codeclimate.com/github/tmecklem/shipmate) [](https://travis-ci.org/tmecklem/shipmate) 2 | 3 | Shipmate is a tool to provide dedicated developers and QA professionals access to over the air installation for iOS and Android alpha and beta apps. It is similar to TestFlight, but with less group and access ceremony, and similar to a HockeyKit standalone installation with the exception that it's built on Rails and aims to provide a bit more functionality. 4 | 5 | Differences from TestFlight: 6 | 7 | * It requires no teams or permissions structure. 8 | * It has no manual step to release builds to teams. Once a build is imported, it's available. 9 | * It is privately deployable. 10 | * It can import App Store signed builds for end-to-end testing of the same binary deliverable. 11 | 12 | Differences from HockeyKit 13 | 14 | * It provides more structured mobile device navigation based on app->release->build. 15 | * It provides the means to install older build versions of the same release with minimal little hassle. 16 | * It's not PHP. 17 | 18 | Getting Started 19 | =============== 20 | 21 | One click heroku deploy 22 | ----------------------- 23 | [](https://heroku.com/deploy) 24 | 25 | Manual setup 26 | ------------ 27 | 28 | * Check out the code from github 29 | * run bundle install 30 | * set up the import cron job: bundle exec whenever -w 31 | * run rails s 32 | * add your apk or ipa files to public/import/ 33 | 34 | How does Shipmate work? 35 | ======================= 36 | 37 | Shipmate analyzes iOS ipa and Android apk files dropped into public/import by reading the application structure within the files. It uses the internal structure to build up a corresponding filesystem. When you load the shipmate webapp in a mobile browser, it identifies your platform and gives sideload access to imported apps. 38 | -------------------------------------------------------------------------------- /spec/lib/shipmate/app_importer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'shipmate/app_importer' 3 | require 'shipmate/apk_import_strategy' 4 | require 'shipmate/ipa_import_strategy' 5 | 6 | describe Shipmate::AppImporter do 7 | 8 | let(:tmp_root) { Pathname.new(Dir.mktmpdir) } 9 | let(:import_dir) { tmp_root.join('public','import') } 10 | let(:apps_dir) { tmp_root.join('public','apps') } 11 | let(:importer) { Shipmate::AppImporter.new } 12 | 13 | before(:each) do 14 | FileUtils.remove_entry_secure tmp_root 15 | FileUtils.mkdir_p import_dir 16 | end 17 | 18 | after(:each) do 19 | FileUtils.remove_entry_secure tmp_root 20 | end 21 | 22 | describe 'Android import' do 23 | 24 | let(:apk_file_fixture) { Rails.root.join('spec','fixtures','ChristmasConspiracy.apk') } 25 | let(:import_apk_file) { import_dir.join("ChristmasConspiracy.apk") } 26 | 27 | before(:each) do 28 | FileUtils.cp(apk_file_fixture, import_apk_file) 29 | end 30 | 31 | it 'searches the import directory and imports android apps found there' do 32 | importer.add_strategy(Shipmate::ApkImportStrategy.new(import_dir, apps_dir)) 33 | importer.import_apps 34 | 35 | expect(File.directory?(apps_dir.join("Christmas Conspiracy", "1.0"))).to be true 36 | expect(File.file?(apps_dir.join("Christmas Conspiracy", "1.0", "Christmas Conspiracy-1.0.apk"))).to be true 37 | end 38 | end 39 | 40 | describe 'iOS import' do 41 | 42 | let(:ipa_file_fixture) { Rails.root.join('spec','fixtures','Go-Tomato-Ad-Hoc-27.ipa') } 43 | let(:import_ipa_file) { import_dir.join("Go-Tomato-Ad-Hoc-27.ipa") } 44 | 45 | before(:each) do 46 | FileUtils.cp(ipa_file_fixture, import_ipa_file) 47 | end 48 | 49 | it 'searches the import directory and imports iOS apps found there' do 50 | importer.add_strategy(Shipmate::IpaImportStrategy.new(import_dir, apps_dir)) 51 | importer.import_apps 52 | 53 | expect(File.directory?(apps_dir.join("Go Tomato","1.0.27"))).to be true 54 | expect(File.file?(apps_dir.join("Go Tomato","1.0.27", "Go Tomato-1.0.27.ipa"))).to be true 55 | end 56 | 57 | end 58 | 59 | end -------------------------------------------------------------------------------- /spec/controllers/apps_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe AppsController do 4 | 5 | let(:ios_dir) { Rails.root.join('public','apps','ios') } 6 | let(:android_dir) {Rails.root.join('public', 'apps', 'android')} 7 | 8 | describe '#initialize' do 9 | 10 | it 'sets the ios_dir property' do 11 | apps_controller = AppsController.new 12 | expect(apps_controller.ios_dir).to_not eq nil 13 | end 14 | 15 | it 'sets the android_dir property' do 16 | apps_controller = AppsController.new 17 | expect(apps_controller.android_dir).to_not eq nil 18 | end 19 | 20 | end 21 | 22 | describe 'GET #index' do 23 | 24 | before (:each) do 25 | FileUtils.mkdir_p(ios_dir.join('Chocolate')) 26 | FileUtils.mkdir_p(ios_dir.join('Monkeybread')) 27 | FileUtils.mkdir_p(android_dir.join('Chocolate')) 28 | FileUtils.mkdir_p(android_dir.join('Monkeybread')) 29 | end 30 | 31 | after(:each) do 32 | FileUtils.rm_r(ios_dir.join('Chocolate')) 33 | FileUtils.rm_r(ios_dir.join('Monkeybread')) 34 | FileUtils.rm_r(android_dir.join('Chocolate')) 35 | FileUtils.rm_r(android_dir.join('Monkeybread')) 36 | end 37 | 38 | it 'returns a 200' do 39 | get :index 40 | expect(response).to be_success 41 | expect(response.status).to eq(200) 42 | end 43 | 44 | it "renders the index template" do 45 | get :index 46 | expect(response).to render_template("index") 47 | end 48 | 49 | it 'assigns a list of the ios apps' do 50 | get :index 51 | expect(assigns[:ios_app_names]).to include('Chocolate') 52 | expect(assigns[:ios_app_names]).to include('Monkeybread') 53 | end 54 | 55 | it 'assigns a list of the android apps' do 56 | get :index 57 | expect(assigns[:android_app_names]).to include('Chocolate') 58 | expect(assigns[:android_app_names]).to include('Monkeybread') 59 | end 60 | 61 | it 'does not include . or .. in ios app names listing' do 62 | get :index 63 | expect(assigns[:ios_app_names]).to_not include('.') 64 | expect(assigns[:ios_app_names]).to_not include('..') 65 | end 66 | 67 | it 'does not include . or .. in android app names listing' do 68 | get :index 69 | expect(assigns[:android_app_names]).to_not include('.') 70 | expect(assigns[:android_app_names]).to_not include('..') 71 | end 72 | 73 | end 74 | 75 | end 76 | -------------------------------------------------------------------------------- /spec/lib/shipmate/ipa_import_strategy_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'shipmate/ipa_import_strategy' 3 | 4 | describe Shipmate::IpaImportStrategy do 5 | 6 | let(:tmp_root) { Pathname.new(Dir.mktmpdir) } 7 | let(:import_dir) { tmp_root.join('public','import') } 8 | let(:apps_dir) { tmp_root.join('public','apps') } 9 | let(:importer) { Shipmate::IpaImportStrategy.new(import_dir.to_s, apps_dir.to_s) } 10 | 11 | after(:each) do 12 | FileUtils.remove_entry_secure tmp_root 13 | end 14 | 15 | describe '#initialize' do 16 | 17 | it 'stores the import folder as a property' do 18 | expect(importer.import_dir).to eq import_dir 19 | end 20 | 21 | it 'makes sure the import directory exists' do 22 | expect(File.directory?(importer.import_dir)).to eq true 23 | end 24 | 25 | it 'stores the apps folder as a property' do 26 | expect(importer.apps_dir).to eq apps_dir 27 | end 28 | 29 | it 'makes sure the apps directory exists' do 30 | expect(File.directory?(importer.apps_dir)).to eq true 31 | end 32 | 33 | end 34 | 35 | describe 'import methods' do 36 | 37 | let(:ipa_file_fixture) { Rails.root.join('spec','fixtures','Go-Tomato-Ad-Hoc-27.ipa') } 38 | let(:import_ipa_file) { import_dir.join("Go-Tomato-Ad-Hoc-27.ipa") } 39 | 40 | before(:each) do 41 | FileUtils.mkdir_p import_dir 42 | FileUtils.rm_rf(apps_dir.join("Go Tomato")) 43 | FileUtils.cp(ipa_file_fixture, import_ipa_file) 44 | end 45 | 46 | describe '#import_app' do 47 | 48 | let(:import_ipa_file) { import_dir.join("Go-Tomato-Ad-Hoc-27.ipa") } 49 | 50 | it 'takes the location of an ipa file and... does the import' do 51 | importer.import_app(import_ipa_file) 52 | 53 | expect(File.directory?(apps_dir.join("Go Tomato","1.0.27"))).to be true 54 | expect(File.file?(apps_dir.join("Go Tomato","1.0.27", "Go Tomato-1.0.27.ipa"))).to be true 55 | end 56 | 57 | end 58 | 59 | describe '#create_app_directory' do 60 | 61 | it 'creates a folder in the apps_dir with the given app_name and app_version' do 62 | importer.create_app_directory("Go Tomato","1.0.27") 63 | expect(File.directory?(apps_dir.join("Go Tomato","1.0.27"))).to be true 64 | end 65 | 66 | end 67 | 68 | describe '#move_ipa_file' do 69 | 70 | it 'moves an ipa file to the apps directory' do 71 | FileUtils.mkdir_p(apps_dir.join("Go Tomato","1.0.27")) 72 | importer.move_ipa_file(import_ipa_file, "Go Tomato","1.0.27") 73 | expect(File.file?(apps_dir.join("Go Tomato","1.0.27", "Go Tomato-1.0.27.ipa"))).to be true 74 | end 75 | 76 | end 77 | 78 | end 79 | 80 | end -------------------------------------------------------------------------------- /app/models/ios_build.rb: -------------------------------------------------------------------------------- 1 | require 'ipa' 2 | require 'app_build' 3 | 4 | class IosBuild < AppBuild 5 | 6 | extend CarrierWave::Mount 7 | attr_accessor :mobileprovision 8 | mount_uploader :mobileprovision, MobileprovisionUploader 9 | 10 | def ipa_file_path 11 | build_file_root_path.join("#{app_name}-#{build_version}.ipa") 12 | end 13 | 14 | def ipa_file? 15 | File.file? ipa_file_path 16 | end 17 | 18 | def icon_file_path 19 | path = build_file_root_path.join("Icon.png") 20 | if !File.file?(path) 21 | extract_icon_to_file(ipa_file_path, path) 22 | end 23 | if File.file?(path) then path else nil end 24 | end 25 | 26 | def icon_file? 27 | icon_file_path = self.icon_file_path 28 | !icon_file_path.nil? and File.file?(icon_file_path) 29 | end 30 | 31 | def ipa_checksum 32 | Digest::SHA1.hexdigest( File.read(ipa_file_path) ) 33 | end 34 | 35 | def info_plist_hash 36 | ipa_info = nil 37 | begin 38 | IPA::IPAFile.open(ipa_file_path) do |ipa| 39 | ipa_info = ipa.info 40 | end 41 | rescue Zip::ZipError => e 42 | puts e 43 | end 44 | ipa_info 45 | end 46 | 47 | def manifest_plist_hash 48 | info_plist_hash = self.info_plist_hash 49 | manifest = {} 50 | assets = [{'kind'=>'software-package', 'url'=>'__URL__'},{'kind'=>'display-image','needs-shine'=>false,'url'=>'__URL__'}] 51 | metadata = {} 52 | metadata["bundle-identifier"] = info_plist_hash["CFBundleIdentifier"] 53 | metadata["bundle-version"] = info_plist_hash["CFBundleVersion"] 54 | metadata["kind"] = "software" 55 | metadata["title"] = "#{info_plist_hash['CFBundleDisplayName']} #{info_plist_hash['CFBundleVersion']}" 56 | metadata["subtitle"] = "#{info_plist_hash['CFBundleDisplayName']} #{info_plist_hash['CFBundleVersion']}" 57 | 58 | items = [{"assets"=>assets, "metadata"=>metadata}] 59 | manifest["items"] = items 60 | 61 | manifest 62 | 63 | end 64 | 65 | def extract_icon_to_file(ipa_file, icon_file) 66 | icon_destination = Pathname.new(icon_file) 67 | begin 68 | IPA::IPAFile.open(ipa_file) do |ipa| 69 | proc_that_returns_icon_data = ipa.icons["Icon.png"] || ipa.icons["Icon@2x.png"] 70 | File.open(icon_destination, 'wb') do 71 | |f| f.write proc_that_returns_icon_data.call() 72 | end 73 | end 74 | rescue Zip::ZipError 75 | 76 | end 77 | end 78 | 79 | def device_families 80 | families = info_plist_hash["UIDeviceFamily"].map do |family_id| 81 | if family_id == 1 82 | :iphone 83 | elsif family_id == 2 84 | :ipad 85 | else 86 | family_id 87 | end 88 | end 89 | 90 | families << :ipad unless families.include?(:ipad) 91 | families 92 | end 93 | 94 | end -------------------------------------------------------------------------------- /spec/lib/shipmate/apk_import_strategy_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'shipmate/apk_import_strategy' 3 | 4 | describe Shipmate::ApkImportStrategy do 5 | 6 | let(:tmp_root) { Pathname.new(Dir.mktmpdir) } 7 | let(:import_dir) { tmp_root.join('public','import') } 8 | let(:apps_dir) { tmp_root.join('public','apps') } 9 | let(:apk_importer) { Shipmate::ApkImportStrategy.new(import_dir, apps_dir) } 10 | 11 | after(:each) do 12 | FileUtils.remove_entry_secure tmp_root 13 | end 14 | 15 | describe "#initialize" do 16 | 17 | it 'stores the import folder as a property' do 18 | expect(apk_importer.import_dir).to eq import_dir 19 | end 20 | 21 | it 'makes sure the import directory exists' do 22 | expect(File.directory?(apk_importer.import_dir)).to be true 23 | end 24 | 25 | it 'stores the app folder as a property' do 26 | expect(apk_importer.apps_dir).to eq apps_dir 27 | end 28 | 29 | it 'makes sure the apps directory exists' do 30 | expect(File.directory?(apk_importer.apps_dir)).to be true 31 | end 32 | 33 | describe "import methods" do 34 | 35 | let(:apk_file_fixture) { Rails.root.join('spec','fixtures','ChristmasConspiracy.apk') } 36 | let(:import_apk_file) { import_dir.join("ChristmasConspiracy.apk") } 37 | 38 | before(:each) do 39 | FileUtils.mkdir_p import_dir 40 | FileUtils.rm_rf(apps_dir.join("Christmas Conspiracy")) 41 | FileUtils.cp(apk_file_fixture, import_apk_file) 42 | end 43 | 44 | after(:each) do 45 | FileUtils.rm(import_apk_file) if File.file?(import_apk_file) 46 | FileUtils.rm_rf(apps_dir.join("Christmas Conspiracy")) 47 | end 48 | 49 | describe '#create_app_directory' do 50 | it 'creates a folder in the apps_dir with the given app_name and app_version' do 51 | apk_importer.create_app_directory("Christmas Conspiracy", "4.2.55") 52 | expect(File.directory?(apps_dir.join("Christmas Conspiracy", "4.2.55"))).to be true 53 | end 54 | end 55 | 56 | describe '#move_apk_file' do 57 | it 'moves an apk file to the apps directory' do 58 | FileUtils.mkdir_p(apps_dir.join("Christmas Conspiracy", "4.2.55")) 59 | apk_importer.move_apk_file(import_apk_file, "Christmas Conspiracy", "4.2.55") 60 | expect(File.file?(apps_dir.join("Christmas Conspiracy", "4.2.55", "Christmas Conspiracy-4.2.55.apk"))).to be true 61 | end 62 | end 63 | 64 | describe '#import_app' do 65 | it 'takes the location of an apk file and... does the import' do 66 | apk_importer.import_app(import_apk_file) 67 | 68 | expect(File.directory?(apps_dir.join("Christmas Conspiracy", "1.0"))).to be true 69 | expect(File.file?(apps_dir.join("Christmas Conspiracy", "1.0", "Christmas Conspiracy-1.0.apk"))).to be true 70 | end 71 | 72 | end 73 | 74 | end 75 | 76 | end 77 | end -------------------------------------------------------------------------------- /spec/controllers/android_apps_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe AndroidAppsController do 4 | 5 | let(:android_dir) { Rails.root.join('public','apps','android') } 6 | 7 | describe '#initialize' do 8 | it 'sets the android_dir property' do 9 | apps_controller = AndroidAppsController.new 10 | expect(apps_controller.android_dir).to_not eq nil 11 | end 12 | end 13 | 14 | describe 'GET #list_app_releases' do 15 | before (:each) do 16 | FileUtils.mkdir_p(android_dir.join('Vanilla','1.2.4.0')) 17 | FileUtils.mkdir_p(android_dir.join('Vanilla','1.2.4.10')) 18 | FileUtils.mkdir_p(android_dir.join('Vanilla','1.2.6.0')) 19 | FileUtils.mkdir_p(android_dir.join('Vanilla','1.2.2.0')) 20 | end 21 | 22 | after(:each) do 23 | FileUtils.rm_r(android_dir.join('Vanilla')) 24 | end 25 | 26 | it 'returns a 200' do 27 | get :list_app_releases, :app_name => 'Vanilla' 28 | expect(response).to be_success 29 | expect(response.status).to eq(200) 30 | end 31 | 32 | it 'creates a list of reverse release sorted by app releases' do 33 | get :list_app_releases, :app_name => 'Vanilla' 34 | expect(assigns[:app_releases]).to eq ['1.2.6', '1.2.4', '1.2.2'] 35 | end 36 | 37 | it 'creates a hash of the keys and the most recent builds as values' do 38 | app_name = 'Vanilla' 39 | get :list_app_releases, :app_name => app_name 40 | expect(assigns[:most_recent_build_hash]).to eq({'1.2.6'=>AppBuild.new(android_dir,app_name,'1.2.6.0'), '1.2.4'=>AppBuild.new(android_dir,app_name,'1.2.4.10'), '1.2.2'=>AppBuild.new(android_dir,app_name,'1.2.2.0')}) 41 | end 42 | end 43 | 44 | describe 'GET #list_app_builds' do 45 | before (:each) do 46 | FileUtils.mkdir_p(android_dir.join('Vanilla','1.2.0.14')) 47 | FileUtils.mkdir_p(android_dir.join('Vanilla','1.2.0.12')) 48 | FileUtils.mkdir_p(android_dir.join('Vanilla','1.2.0.1')) 49 | FileUtils.mkdir_p(android_dir.join('Vanilla','1.2.3.goofy')) 50 | FileUtils.mkdir_p(android_dir.join('Vanilla','1.2.0.2')) 51 | end 52 | 53 | after(:each) do 54 | FileUtils.rm_r(android_dir.join('Vanilla')) 55 | end 56 | 57 | it 'returns a 200' do 58 | get :list_app_builds, :app_name => 'Vanilla', :app_release => '1.2.0' 59 | expect(response).to be_success 60 | expect(response.status).to eq(200) 61 | end 62 | 63 | it 'assembles a reverse version sorted list of builds within a release' do 64 | app_name = 'Vanilla' 65 | get :list_app_builds, :app_name => app_name, :app_release => '1.2.0' 66 | expect(assigns[:app_builds]).to eq [AppBuild.new(android_dir,app_name,'1.2.0.14'),AppBuild.new(android_dir,app_name,'1.2.0.12'),AppBuild.new(android_dir,app_name,'1.2.0.2'),AppBuild.new(android_dir,app_name,'1.2.0.1')] 67 | end 68 | 69 | it 'assigns the app name' do 70 | get :list_app_builds, :app_name => 'Vanilla', :app_release => '1.2.0' 71 | expect(assigns[:app_name]).to eq 'Vanilla' 72 | end 73 | end 74 | 75 | end -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Shipmate::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both thread web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application 18 | # Add `rack-cache` to your Gemfile before enabling this. 19 | # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid. 20 | # config.action_dispatch.rack_cache = true 21 | 22 | # Disable Rails's static asset server (Apache or nginx will already do this). 23 | config.serve_static_assets = true 24 | 25 | # Compress JavaScripts and CSS. 26 | config.assets.js_compressor = :uglifier 27 | # config.assets.css_compressor = :sass 28 | 29 | # Do not fallback to assets pipeline if a precompiled asset is missed. 30 | config.assets.compile = false 31 | 32 | # Generate digests for assets URLs. 33 | config.assets.digest = true 34 | 35 | # Version of your assets, change this if you want to expire all your assets. 36 | config.assets.version = '1.0' 37 | 38 | # Specifies the header that your server uses for sending files. 39 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache 40 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx 41 | 42 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 43 | # config.force_ssl = true 44 | 45 | # Set to :debug to see everything in the log. 46 | config.log_level = :info 47 | 48 | # Prepend all log lines with the following tags. 49 | # config.log_tags = [ :subdomain, :uuid ] 50 | 51 | # Use a different logger for distributed setups. 52 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 53 | 54 | # Use a different cache store in production. 55 | # config.cache_store = :mem_cache_store 56 | 57 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 58 | # config.action_controller.asset_host = "http://assets.example.com" 59 | 60 | # Precompile additional assets. 61 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 62 | # config.assets.precompile += %w( search.js ) 63 | 64 | # Ignore bad email addresses and do not raise email delivery errors. 65 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 66 | # config.action_mailer.raise_delivery_errors = false 67 | 68 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 69 | # the I18n.default_locale when a translation can not be found). 70 | config.i18n.fallbacks = true 71 | 72 | # Send deprecation notices to registered listeners. 73 | config.active_support.deprecation = :notify 74 | 75 | # Disable automatic flushing of the log to improve performance. 76 | # config.autoflush_log = false 77 | 78 | # Use default logging formatter so that PID and timestamp are not suppressed. 79 | config.log_formatter = ::Logger::Formatter.new 80 | end 81 | -------------------------------------------------------------------------------- /app/controllers/ios_apps_controller.rb: -------------------------------------------------------------------------------- 1 | require 'shipmate/ipa_parser' 2 | 3 | class IosAppsController < AppsController 4 | 5 | include AppListingModule 6 | 7 | APP_ASSET_INDEX = 0 8 | ICON_ASSET_INDEX = 1 9 | 10 | attr_accessor :ios_dir 11 | 12 | def initialize 13 | @ios_dir = Shipmate::Application.config.ios_dir 14 | FileUtils.mkdir_p(@ios_dir) 15 | super 16 | end 17 | 18 | def list_app_releases 19 | @app_name = params[:app_name] 20 | app_builds = self.app_builds(@app_name, @ios_dir, IOS_APP_TYPE) 21 | 22 | @most_recent_build_hash = most_recent_build_by_release(app_builds) 23 | @app_releases = @most_recent_build_hash.keys.sort{|x,y| y.to_version_string<=>x.to_version_string } 24 | @mobileprovision = mobileprovision_file_url(@app_name) 25 | @mobileprovision_file_name = mobileprovision_file_name(@app_name) 26 | 27 | end 28 | 29 | def mobileprovision_file_url(app_name) 30 | mobileprovision_file = Dir.glob(@ios_dir.join(app_name,"*.mobileprovision")).first 31 | if mobileprovision_file 32 | "#{request.base_url}/apps/ios/#{app_name}/#{mobileprovision_file.split('/').last}" 33 | else 34 | nil 35 | end 36 | end 37 | 38 | def mobileprovision_file_name(app_name) 39 | mobileprovision_file = Dir.glob(@ios_dir.join(app_name,"*.mobileprovision")).first 40 | if mobileprovision_file 41 | "#{mobileprovision_file.split('/').last}" 42 | else 43 | nil 44 | end 45 | end 46 | 47 | def list_app_builds 48 | @app_name = params[:app_name] 49 | @app_release = params[:app_release] 50 | 51 | @app_builds = self.app_builds(@app_name, @ios_dir, IOS_APP_TYPE).select do |app_build| 52 | app_build.release.eql?(@app_release) 53 | end 54 | end 55 | 56 | def most_recent_build_by_release(app_builds) 57 | most_recent_builds_hash = {} 58 | app_builds.each do |app_build| 59 | most_recent_builds_hash[app_build.release] ||= app_build 60 | end 61 | most_recent_builds_hash 62 | end 63 | 64 | def show_build 65 | expires_now 66 | @app_build = IosBuild.new(@ios_dir, params[:app_name], params[:build_version]) 67 | end 68 | 69 | def show_build_manifest 70 | expires_now 71 | @app_name = params[:app_name] 72 | build_version = params[:build_version] 73 | 74 | respond_to do |format| 75 | format.plist { render :text => gen_plist_hash(@app_name, build_version).to_plist(plist_format: CFPropertyList::List::FORMAT_XML) } 76 | end 77 | end 78 | 79 | def gen_plist_hash(app_name, build_version) 80 | app_build = IosBuild.new(@ios_dir, app_name, build_version) 81 | plist_hash = app_build.manifest_plist_hash 82 | replace_url_in_plist_hash APP_ASSET_INDEX, "#{public_url_for_build_directory(@app_name, build_version)}/#{@app_name}-#{build_version}.ipa", plist_hash 83 | replace_url_in_plist_hash ICON_ASSET_INDEX, "#{public_url_for_build_directory(@app_name, build_version)}/Icon.png", plist_hash 84 | plist_hash 85 | end 86 | 87 | def public_url_for_build_directory(app_name, build_version) 88 | "#{request.base_url}/apps/ios/#{app_name}/#{build_version}" 89 | end 90 | 91 | def replace_url_in_plist_hash(asset_type, url, plist_hash) 92 | plist_hash["items"][0]["assets"][asset_type]['url'] = URI.escape(url) 93 | end 94 | 95 | def upload_mobileprovision 96 | @mobile_provisioning_file = params[:upload_mobileprovision][:file] 97 | 98 | delete_mobileprovision_files_for_app(params[:app_name]) 99 | 100 | File.open("public/apps/ios/#{params[:app_name]}/#{@mobile_provisioning_file.original_filename}", 'wb') do |f| 101 | if f.write @mobile_provisioning_file.read 102 | flash[:notice] = "Successfully saved the mobileprovision file for #{params[:app_name]}" 103 | else 104 | flash[:error] = "There was a problem saving the mobileprovision file for #{params[:app_name]}" 105 | end 106 | end 107 | 108 | # TODO why isn't this working? 109 | redirect_to list_ios_app_releases_path 110 | end 111 | 112 | private 113 | 114 | def delete_mobileprovision_files_for_app(app_name) 115 | mobileprovision_files = Dir.glob(@ios_dir.join(app_name,"*.mobileprovision")) 116 | mobileprovision_files.each do |file| 117 | File.delete(file) 118 | end 119 | end 120 | 121 | end 122 | -------------------------------------------------------------------------------- /spec/controllers/ios_apps_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe IosAppsController do 4 | 5 | let(:ios_dir) { Rails.root.join('public','apps','ios') } 6 | 7 | describe '#initialize' do 8 | 9 | it 'sets the ios_dir property' do 10 | apps_controller = IosAppsController.new 11 | expect(apps_controller.ios_dir).to_not eq nil 12 | end 13 | 14 | end 15 | 16 | describe 'GET #list_app_releases' do 17 | before (:each) do 18 | FileUtils.mkdir_p(ios_dir.join('Chocolate','1.2.4.0')) 19 | FileUtils.mkdir_p(ios_dir.join('Chocolate','1.2.4.10')) 20 | FileUtils.mkdir_p(ios_dir.join('Chocolate','1.2.6.0')) 21 | FileUtils.mkdir_p(ios_dir.join('Chocolate','1.2.2.0')) 22 | FileUtils.touch(ios_dir.join('Chocolate','something.mobileprovision')) 23 | end 24 | 25 | after(:each) do 26 | FileUtils.rm_r(ios_dir.join('Chocolate')) 27 | end 28 | 29 | it 'returns a 200' do 30 | get :list_app_releases, :app_name => 'Chocolate' 31 | expect(response).to be_success 32 | expect(response.status).to eq(200) 33 | end 34 | 35 | it 'creates a list of reverse release sorted app releases' do 36 | get :list_app_releases, :app_name => 'Chocolate' 37 | expect(assigns[:app_releases]).to eq ['1.2.6', '1.2.4', '1.2.2'] 38 | end 39 | 40 | it 'creates a hash of releases as keys and the most recent builds as values' do 41 | app_name = 'Chocolate' 42 | get :list_app_releases, :app_name => app_name 43 | expect(assigns[:most_recent_build_hash]).to eq({'1.2.6'=>IosBuild.new(ios_dir,app_name,'1.2.6.0'), '1.2.4'=>IosBuild.new(ios_dir,app_name,'1.2.4.10'), '1.2.2'=>IosBuild.new(ios_dir,app_name,'1.2.2.0')}) 44 | end 45 | 46 | it 'assigns @mobileprovision as a url if a mobileprovision file exists in the app root directory' do 47 | app_name = 'Chocolate' 48 | get :list_app_releases, :app_name => app_name 49 | expect(assigns[:mobileprovision]).to include("/something.mobileprovision") 50 | end 51 | 52 | end 53 | 54 | describe 'GET #list_app_builds' do 55 | before (:each) do 56 | FileUtils.mkdir_p(ios_dir.join('Chocolate','1.2.0.14')) 57 | FileUtils.mkdir_p(ios_dir.join('Chocolate','1.2.0.12')) 58 | FileUtils.mkdir_p(ios_dir.join('Chocolate','1.2.0.1')) 59 | FileUtils.mkdir_p(ios_dir.join('Chocolate','1.2.3.goofy')) 60 | FileUtils.mkdir_p(ios_dir.join('Chocolate','1.2.0.2')) 61 | end 62 | 63 | after(:each) do 64 | FileUtils.rm_r(ios_dir.join('Chocolate')) 65 | end 66 | 67 | it 'returns a 200' do 68 | get :list_app_builds, :app_name => 'Chocolate', :app_release => '1.2.0' 69 | expect(response).to be_success 70 | expect(response.status).to eq(200) 71 | end 72 | 73 | it 'assembles a reverse version sorted list of builds within a release' do 74 | app_name = 'Chocolate' 75 | get :list_app_builds, :app_name => app_name, :app_release => '1.2.0' 76 | expect(assigns[:app_builds]).to eq [IosBuild.new(ios_dir,app_name,'1.2.0.14'),IosBuild.new(ios_dir,app_name,'1.2.0.12'),IosBuild.new(ios_dir,app_name,'1.2.0.2'),IosBuild.new(ios_dir,app_name,'1.2.0.1')] 77 | end 78 | 79 | it 'assigns the app_name' do 80 | get :list_app_builds, :app_name => 'Chocolate', :app_release => '1.2.0' 81 | expect(assigns[:app_name]).to eq 'Chocolate' 82 | end 83 | 84 | end 85 | 86 | describe 'single build methods' do 87 | 88 | before(:each) do 89 | FileUtils.mkdir_p(ios_dir.join('Go Tomato','1.0.27')) 90 | FileUtils.cp(Rails.root.join('spec','fixtures','Go-Tomato-Ad-Hoc-27.ipa'), ios_dir.join('Go Tomato','1.0.27','Go Tomato-1.0.27.ipa')) 91 | end 92 | 93 | after(:each) do 94 | FileUtils.rm_rf(ios_dir.join('Go Tomato')) 95 | end 96 | 97 | describe 'GET #show_build' do 98 | 99 | it 'returns a 200' do 100 | get :show_build, :app_name => 'Go Tomato', :build_version => '1.0.27' 101 | expect(response).to be_success 102 | expect(response.status).to eq(200) 103 | end 104 | 105 | it 'sets @app_build' do 106 | app_name = 'Go Tomato' 107 | build_version = '1.0.27' 108 | get :show_build, :app_name => app_name, :build_version => build_version 109 | expect(assigns[:app_build]).to eq IosBuild.new(ios_dir, app_name, build_version) 110 | end 111 | 112 | end 113 | 114 | describe 'GET #show_build_manifest' do 115 | 116 | before(:each) do 117 | request.env["HTTP_ACCEPT"] = 'text/plist' 118 | end 119 | 120 | it 'returns a 200' do 121 | get :show_build_manifest, :app_name => 'Go Tomato', :build_version => '1.0.27' 122 | expect(response).to be_success 123 | expect(response.status).to eq(200) 124 | end 125 | 126 | it 'set @app_name' do 127 | get :show_build_manifest, :app_name => 'Go Tomato', :build_version => '1.0.27' 128 | expect(assigns[:app_name]).to eq 'Go Tomato' 129 | end 130 | 131 | it 'returns a plist file' do 132 | get :show_build_manifest, :app_name => 'Go Tomato', :build_version => '1.0.27' 133 | expect(response.body).to include("Go Tomato") 134 | expect(response.body).to include("1.0.27") 135 | expect(response.body).to include("software") 136 | expect(response.body).to include("url") 137 | end 138 | 139 | end 140 | end 141 | 142 | end 143 | -------------------------------------------------------------------------------- /spec/models/ios_build_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe IosBuild do 4 | 5 | let(:apps_dir) { Pathname.new(Dir.mktmpdir) } 6 | let(:app_name) { "Go Tomato" } 7 | let(:build_version) { "1.0.27" } 8 | let(:app_build) {IosBuild.new(apps_dir, app_name, build_version)} 9 | let(:expected_build_file_root_path) { Pathname.new("#{apps_dir}/Go Tomato/1.0.27") } 10 | let(:ipa_file_fixture) { Rails.root.join('spec','fixtures','Go-Tomato-Ad-Hoc-27.ipa') } 11 | let(:expected_ipa_file_location) { Pathname.new("#{apps_dir}/Go Tomato/1.0.27/Go Tomato-1.0.27.ipa") } 12 | 13 | before(:each) do 14 | FileUtils.mkdir_p(expected_build_file_root_path) 15 | FileUtils.cp(ipa_file_fixture, expected_ipa_file_location) 16 | end 17 | 18 | after(:each) do 19 | FileUtils.rm_rf apps_dir 20 | end 21 | 22 | describe '#ipa_file_path' do 23 | it 'returns the location of the ipa file' do 24 | expect(app_build.ipa_file_path).to eq(expected_ipa_file_location) 25 | end 26 | end 27 | 28 | describe '#ipa_file?' do 29 | it 'returns true if the ipa file exists' do 30 | expect(app_build.ipa_file?).to be true 31 | end 32 | it 'returns false if the ipa file does not exist' do 33 | FileUtils.rm_rf(apps_dir.to_s) 34 | expect(app_build.ipa_file?).to be false 35 | end 36 | end 37 | 38 | describe '#icon_file_path' do 39 | it 'returns the location of a representative icon for the app' do 40 | FileUtils.touch(expected_build_file_root_path.join("Icon.png")) 41 | expect(app_build.icon_file_path).to eq(expected_build_file_root_path.join("Icon.png")) 42 | end 43 | it 'extracts a representative icon for the app if it exists in the ipa but not in the app directory' do 44 | expect(app_build.icon_file_path).to eq(expected_build_file_root_path.join("Icon.png")) 45 | expect(File.file?(expected_build_file_root_path.join("Icon.png"))).to be true 46 | end 47 | end 48 | 49 | describe '#icon_file?' do 50 | 51 | let(:ipa_file_fixture_without_icon) { Rails.root.join('spec','fixtures','Tribes-101.ipa') } 52 | let(:app_name_without_icon) { "Tribes" } 53 | let(:build_version_without_icon) { "2.0.101" } 54 | 55 | it 'returns true if the icon file exists' do 56 | FileUtils.touch(expected_build_file_root_path.join("Icon.png")) 57 | expect(app_build.icon_file?).to be true 58 | end 59 | 60 | it 'returns false if the icon file does not exist and cannot be lazily extracted from the ipa' do 61 | FileUtils.mkdir_p(apps_dir.join(app_name_without_icon, build_version_without_icon)) 62 | FileUtils.cp(ipa_file_fixture_without_icon, apps_dir.join(app_name_without_icon, build_version_without_icon, "#{app_name_without_icon}-#{build_version_without_icon}.ipa")) 63 | app_build = IosBuild.new(ipa_file_fixture_without_icon, app_name_without_icon, build_version_without_icon) 64 | expect(app_build.icon_file?).to be false 65 | FileUtils.rm_rf(apps_dir.to_s) 66 | end 67 | end 68 | 69 | describe '#ipa_checksum' do 70 | it 'returns a sha1 hash of the ipa file' do 71 | expect(app_build.ipa_checksum).to eq "45a5a4862ebcc0b80a3f5e1a60649734eebca18a" 72 | end 73 | end 74 | 75 | describe '#manifest_plist_hash' do 76 | it 'returns a Hash containing the proper structure and values for an iOS OTA manifest' do 77 | expected_manifest_plist_hash = {'items' => [ {'assets' => [ {'kind' => 'software-package', 'url' => '__URL__'}, {'kind'=>'display-image', 'needs-shine'=>false, 'url'=>'__URL__'} ], 'metadata' => {'bundle-identifier'=>'com.mecklem.Go-Tomato', 'bundle-version'=>'1.0.27', 'kind'=>'software', 'title'=>'Go Tomato 1.0.27', 'subtitle'=>'Go Tomato 1.0.27'} } ]} 78 | expect(app_build.manifest_plist_hash).to eq expected_manifest_plist_hash 79 | end 80 | end 81 | 82 | describe '#info_plist_hash' do 83 | it 'returns a hash of the ipa\'s info.plist file' do 84 | plist = app_build.info_plist_hash 85 | expect(plist["CFBundleDisplayName"]).to eq("Go Tomato") 86 | expect(plist["CFBundleName"]).to eq("Go Tomato") 87 | expect(plist["CFBundleIdentifier"]).to eq("com.mecklem.Go-Tomato") 88 | expect(plist["CFBundleVersion"]).to eq("1.0.27") 89 | end 90 | end 91 | 92 | describe '#extract_icon_to_file' do 93 | it 'extracts a representative app icon from the ipa' do 94 | icon_path = expected_build_file_root_path.join('Icon.png') 95 | app_build.extract_icon_to_file(expected_ipa_file_location,icon_path.to_s) 96 | expect(Digest::SHA1.hexdigest( File.read(icon_path) )).to eq "ae9535eb6575d2745b984df8447b976ffce9cc6a" 97 | end 98 | end 99 | 100 | describe '#supports_device?' do 101 | 102 | let(:ipa_file_fixture_ipad_only) { Rails.root.join('spec','fixtures','Go Tomato iPad only.ipa') } 103 | let(:build_version_ipad_only) { "2014.0.46" } 104 | 105 | it 'returns true if the iphone device family is supported by the ipa' do 106 | expect(app_build.supports_device?(:iphone)).to be true 107 | end 108 | it 'returns false if the iphone device family is not supported by the ipa' do 109 | FileUtils.mkdir_p(apps_dir.join(app_name, build_version_ipad_only)) 110 | FileUtils.cp(ipa_file_fixture_ipad_only, apps_dir.join(app_name, build_version_ipad_only, "#{app_name}-#{build_version_ipad_only}.ipa")) 111 | app_build = IosBuild.new(apps_dir, app_name, build_version_ipad_only) 112 | expect(app_build.supports_device?(:iphone)).to be false 113 | FileUtils.rm_rf(apps_dir.to_s) 114 | end 115 | 116 | it 'returns true' do 117 | expect(app_build.supports_device?(:ipad)).to be true 118 | end 119 | end 120 | 121 | end -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: git://github.com/sjmulder/ipa 3 | revision: 432657362a4198f62d4e7b69f5323c6ac2813898 4 | specs: 5 | ipa (0.1.3) 6 | CFPropertyList (~> 2.2.0) 7 | rubyzip (~> 0.9.4) 8 | 9 | GEM 10 | remote: https://rubygems.org/ 11 | specs: 12 | CFPropertyList (2.2.7) 13 | actionmailer (4.0.3) 14 | actionpack (= 4.0.3) 15 | mail (~> 2.5.4) 16 | actionpack (4.0.3) 17 | activesupport (= 4.0.3) 18 | builder (~> 3.1.0) 19 | erubis (~> 2.7.0) 20 | rack (~> 1.5.2) 21 | rack-test (~> 0.6.2) 22 | activemodel (4.0.3) 23 | activesupport (= 4.0.3) 24 | builder (~> 3.1.0) 25 | activerecord (4.0.3) 26 | activemodel (= 4.0.3) 27 | activerecord-deprecated_finders (~> 1.0.2) 28 | activesupport (= 4.0.3) 29 | arel (~> 4.0.0) 30 | activerecord-deprecated_finders (1.0.3) 31 | activesupport (4.0.3) 32 | i18n (~> 0.6, >= 0.6.4) 33 | minitest (~> 4.2) 34 | multi_json (~> 1.3) 35 | thread_safe (~> 0.1) 36 | tzinfo (~> 0.3.37) 37 | apktools (0.6.0) 38 | rubyzip 39 | arel (4.0.2) 40 | atomic (1.1.16) 41 | better_errors (1.1.0) 42 | coderay (>= 1.0.0) 43 | erubis (>= 2.6.6) 44 | binding_of_caller (0.7.2) 45 | debug_inspector (>= 0.0.1) 46 | browser (0.4.1) 47 | builder (3.1.4) 48 | byebug (2.7.0) 49 | columnize (~> 0.3) 50 | debugger-linecache (~> 1.2) 51 | carrierwave (0.10.0) 52 | activemodel (>= 3.2.0) 53 | activesupport (>= 3.2.0) 54 | json (>= 1.7) 55 | mime-types (>= 1.16) 56 | celluloid (0.15.2) 57 | timers (~> 1.1.0) 58 | celluloid-io (0.15.0) 59 | celluloid (>= 0.15.0) 60 | nio4r (>= 0.5.0) 61 | chronic (0.10.2) 62 | coderay (1.1.0) 63 | coffee-rails (4.0.1) 64 | coffee-script (>= 2.2.0) 65 | railties (>= 4.0.0, < 5.0) 66 | coffee-script (2.2.0) 67 | coffee-script-source 68 | execjs 69 | coffee-script-source (1.7.0) 70 | columnize (0.9.0) 71 | debug_inspector (0.0.2) 72 | debugger-linecache (1.2.0) 73 | diff-lcs (1.2.5) 74 | erubis (2.7.0) 75 | execjs (2.0.2) 76 | ffi (1.9.3) 77 | formatador (0.2.4) 78 | guard (2.6.0) 79 | formatador (>= 0.2.4) 80 | listen (~> 2.7) 81 | lumberjack (~> 1.0) 82 | pry (>= 0.9.12) 83 | thor (>= 0.18.1) 84 | guard-rails (0.5.0) 85 | guard (>= 2.0.0) 86 | guard-rspec (4.2.8) 87 | guard (~> 2.1) 88 | rspec (>= 2.14, < 4.0) 89 | haml (4.0.5) 90 | tilt 91 | haml-rails (0.5.3) 92 | actionpack (>= 4.0.1) 93 | activesupport (>= 4.0.1) 94 | haml (>= 3.1, < 5.0) 95 | railties (>= 4.0.1) 96 | hike (1.2.3) 97 | i18n (0.6.9) 98 | jbuilder (1.5.3) 99 | activesupport (>= 3.0.0) 100 | multi_json (>= 1.2.0) 101 | jquery-rails (3.1.0) 102 | railties (>= 3.0, < 5.0) 103 | thor (>= 0.14, < 2.0) 104 | jquery_mobile_rails (1.4.1) 105 | railties (>= 3.1.0) 106 | json (1.8.1) 107 | listen (2.7.1) 108 | celluloid (>= 0.15.2) 109 | celluloid-io (>= 0.15.0) 110 | rb-fsevent (>= 0.9.3) 111 | rb-inotify (>= 0.9) 112 | lumberjack (1.0.5) 113 | mail (2.5.4) 114 | mime-types (~> 1.16) 115 | treetop (~> 1.4.8) 116 | method_source (0.8.2) 117 | mime-types (1.25.1) 118 | mini_portile (0.5.3) 119 | minitest (4.7.5) 120 | multi_json (1.9.2) 121 | nio4r (1.0.0) 122 | nokogiri (1.6.1) 123 | mini_portile (~> 0.5.0) 124 | polyglot (0.3.4) 125 | pry (0.9.12.6) 126 | coderay (~> 1.0) 127 | method_source (~> 0.8) 128 | slop (~> 3.4) 129 | pry-byebug (1.3.2) 130 | byebug (~> 2.7) 131 | pry (~> 0.9.12) 132 | rack (1.5.2) 133 | rack-test (0.6.2) 134 | rack (>= 1.0) 135 | rails (4.0.3) 136 | actionmailer (= 4.0.3) 137 | actionpack (= 4.0.3) 138 | activerecord (= 4.0.3) 139 | activesupport (= 4.0.3) 140 | bundler (>= 1.3.0, < 2.0) 141 | railties (= 4.0.3) 142 | sprockets-rails (~> 2.0.0) 143 | railties (4.0.3) 144 | actionpack (= 4.0.3) 145 | activesupport (= 4.0.3) 146 | rake (>= 0.8.7) 147 | thor (>= 0.18.1, < 2.0) 148 | rake (10.2.1) 149 | rb-fsevent (0.9.4) 150 | rb-inotify (0.9.3) 151 | ffi (>= 0.5.0) 152 | rdoc (4.1.1) 153 | json (~> 1.4) 154 | rspec (2.14.1) 155 | rspec-core (~> 2.14.0) 156 | rspec-expectations (~> 2.14.0) 157 | rspec-mocks (~> 2.14.0) 158 | rspec-core (2.14.8) 159 | rspec-expectations (2.14.5) 160 | diff-lcs (>= 1.1.3, < 2.0) 161 | rspec-mocks (2.14.6) 162 | rspec-rails (2.14.2) 163 | actionpack (>= 3.0) 164 | activemodel (>= 3.0) 165 | activesupport (>= 3.0) 166 | railties (>= 3.0) 167 | rspec-core (~> 2.14.0) 168 | rspec-expectations (~> 2.14.0) 169 | rspec-mocks (~> 2.14.0) 170 | rubyzip (0.9.9) 171 | sass (3.2.18) 172 | sass-rails (4.0.2) 173 | railties (>= 4.0.0, < 5.0) 174 | sass (~> 3.2.0) 175 | sprockets (~> 2.8, <= 2.11.0) 176 | sprockets-rails (~> 2.0.0) 177 | sdoc (0.4.0) 178 | json (~> 1.8) 179 | rdoc (~> 4.0, < 5.0) 180 | slop (3.5.0) 181 | sprockets (2.11.0) 182 | hike (~> 1.2) 183 | multi_json (~> 1.0) 184 | rack (~> 1.0) 185 | tilt (~> 1.1, != 1.3.0) 186 | sprockets-rails (2.0.1) 187 | actionpack (>= 3.0) 188 | activesupport (>= 3.0) 189 | sprockets (~> 2.8) 190 | terminal-notifier-guard (1.5.3) 191 | thor (0.19.1) 192 | thread_safe (0.3.1) 193 | atomic (>= 1.1.7, < 2) 194 | tilt (1.4.1) 195 | timers (1.1.0) 196 | treetop (1.4.15) 197 | polyglot 198 | polyglot (>= 0.3.1) 199 | tzinfo (0.3.39) 200 | uglifier (2.5.0) 201 | execjs (>= 0.3.0) 202 | json (>= 1.8.0) 203 | whenever (0.9.2) 204 | activesupport (>= 2.3.4) 205 | chronic (>= 0.6.3) 206 | 207 | PLATFORMS 208 | ruby 209 | 210 | DEPENDENCIES 211 | apktools (~> 0.6.0) 212 | better_errors 213 | binding_of_caller 214 | browser (~> 0.4.0) 215 | carrierwave (= 0.10.0) 216 | coffee-rails (~> 4.0.0) 217 | guard-rails (~> 0.5.0) 218 | guard-rspec (~> 4.2.8) 219 | haml-rails (~> 0.5.3) 220 | ipa! 221 | jbuilder (~> 1.2) 222 | jquery-rails (~> 3.1.0) 223 | jquery_mobile_rails (~> 1.4.1) 224 | nokogiri (~> 1.6.1) 225 | pry 226 | pry-byebug 227 | rails (= 4.0.3) 228 | rspec-rails (~> 2.14.2) 229 | sass-rails (~> 4.0.0) 230 | sdoc 231 | terminal-notifier-guard (~> 1.5.3) 232 | uglifier (>= 1.3.0) 233 | whenever (~> 0.9.0) 234 | -------------------------------------------------------------------------------- /vendor/assets/stylesheets/bootstrap-responsive.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Responsive v2.3.2 3 | * 4 | * Copyright 2013 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world by @mdo and @fat. 9 | */.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}@-ms-viewport{width:device-width}.hidden{display:none;visibility:hidden}.visible-phone{display:none!important}.visible-tablet{display:none!important}.hidden-desktop{display:none!important}.visible-desktop{display:inherit!important}@media(min-width:768px) and (max-width:979px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-tablet{display:inherit!important}.hidden-tablet{display:none!important}}@media(max-width:767px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-phone{display:inherit!important}.hidden-phone{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:inherit!important}.hidden-print{display:none!important}}@media(min-width:1200px){.row{margin-left:-30px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:30px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px}.span12{width:1170px}.span11{width:1070px}.span10{width:970px}.span9{width:870px}.span8{width:770px}.span7{width:670px}.span6{width:570px}.span5{width:470px}.span4{width:370px}.span3{width:270px}.span2{width:170px}.span1{width:70px}.offset12{margin-left:1230px}.offset11{margin-left:1130px}.offset10{margin-left:1030px}.offset9{margin-left:930px}.offset8{margin-left:830px}.offset7{margin-left:730px}.offset6{margin-left:630px}.offset5{margin-left:530px}.offset4{margin-left:430px}.offset3{margin-left:330px}.offset2{margin-left:230px}.offset1{margin-left:130px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.564102564102564%;*margin-left:2.5109110747408616%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.564102564102564%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.45299145299145%;*width:91.39979996362975%}.row-fluid .span10{width:82.90598290598291%;*width:82.8527914166212%}.row-fluid .span9{width:74.35897435897436%;*width:74.30578286961266%}.row-fluid .span8{width:65.81196581196582%;*width:65.75877432260411%}.row-fluid .span7{width:57.26495726495726%;*width:57.21176577559556%}.row-fluid .span6{width:48.717948717948715%;*width:48.664757228587014%}.row-fluid .span5{width:40.17094017094017%;*width:40.11774868157847%}.row-fluid .span4{width:31.623931623931625%;*width:31.570740134569924%}.row-fluid .span3{width:23.076923076923077%;*width:23.023731587561375%}.row-fluid .span2{width:14.52991452991453%;*width:14.476723040552828%}.row-fluid .span1{width:5.982905982905983%;*width:5.929714493544281%}.row-fluid .offset12{margin-left:105.12820512820512%;*margin-left:105.02182214948171%}.row-fluid .offset12:first-child{margin-left:102.56410256410257%;*margin-left:102.45771958537915%}.row-fluid .offset11{margin-left:96.58119658119658%;*margin-left:96.47481360247316%}.row-fluid .offset11:first-child{margin-left:94.01709401709402%;*margin-left:93.91071103837061%}.row-fluid .offset10{margin-left:88.03418803418803%;*margin-left:87.92780505546462%}.row-fluid .offset10:first-child{margin-left:85.47008547008548%;*margin-left:85.36370249136206%}.row-fluid .offset9{margin-left:79.48717948717949%;*margin-left:79.38079650845607%}.row-fluid .offset9:first-child{margin-left:76.92307692307693%;*margin-left:76.81669394435352%}.row-fluid .offset8{margin-left:70.94017094017094%;*margin-left:70.83378796144753%}.row-fluid .offset8:first-child{margin-left:68.37606837606839%;*margin-left:68.26968539734497%}.row-fluid .offset7{margin-left:62.393162393162385%;*margin-left:62.28677941443899%}.row-fluid .offset7:first-child{margin-left:59.82905982905982%;*margin-left:59.72267685033642%}.row-fluid .offset6{margin-left:53.84615384615384%;*margin-left:53.739770867430444%}.row-fluid .offset6:first-child{margin-left:51.28205128205128%;*margin-left:51.175668303327875%}.row-fluid .offset5{margin-left:45.299145299145295%;*margin-left:45.1927623204219%}.row-fluid .offset5:first-child{margin-left:42.73504273504273%;*margin-left:42.62865975631933%}.row-fluid .offset4{margin-left:36.75213675213675%;*margin-left:36.645753773413354%}.row-fluid .offset4:first-child{margin-left:34.18803418803419%;*margin-left:34.081651209310785%}.row-fluid .offset3{margin-left:28.205128205128204%;*margin-left:28.0987452264048%}.row-fluid .offset3:first-child{margin-left:25.641025641025642%;*margin-left:25.53464266230224%}.row-fluid .offset2{margin-left:19.65811965811966%;*margin-left:19.551736679396257%}.row-fluid .offset2:first-child{margin-left:17.094017094017094%;*margin-left:16.98763411529369%}.row-fluid .offset1{margin-left:11.11111111111111%;*margin-left:11.004728132387708%}.row-fluid .offset1:first-child{margin-left:8.547008547008547%;*margin-left:8.440625568285142%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:30px}input.span12,textarea.span12,.uneditable-input.span12{width:1156px}input.span11,textarea.span11,.uneditable-input.span11{width:1056px}input.span10,textarea.span10,.uneditable-input.span10{width:956px}input.span9,textarea.span9,.uneditable-input.span9{width:856px}input.span8,textarea.span8,.uneditable-input.span8{width:756px}input.span7,textarea.span7,.uneditable-input.span7{width:656px}input.span6,textarea.span6,.uneditable-input.span6{width:556px}input.span5,textarea.span5,.uneditable-input.span5{width:456px}input.span4,textarea.span4,.uneditable-input.span4{width:356px}input.span3,textarea.span3,.uneditable-input.span3{width:256px}input.span2,textarea.span2,.uneditable-input.span2{width:156px}input.span1,textarea.span1,.uneditable-input.span1{width:56px}.thumbnails{margin-left:-30px}.thumbnails>li{margin-left:30px}.row-fluid .thumbnails{margin-left:0}}@media(min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px}.span12{width:724px}.span11{width:662px}.span10{width:600px}.span9{width:538px}.span8{width:476px}.span7{width:414px}.span6{width:352px}.span5{width:290px}.span4{width:228px}.span3{width:166px}.span2{width:104px}.span1{width:42px}.offset12{margin-left:764px}.offset11{margin-left:702px}.offset10{margin-left:640px}.offset9{margin-left:578px}.offset8{margin-left:516px}.offset7{margin-left:454px}.offset6{margin-left:392px}.offset5{margin-left:330px}.offset4{margin-left:268px}.offset3{margin-left:206px}.offset2{margin-left:144px}.offset1{margin-left:82px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.7624309392265194%;*margin-left:2.709239449864817%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.7624309392265194%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.43646408839778%;*width:91.38327259903608%}.row-fluid .span10{width:82.87292817679558%;*width:82.81973668743387%}.row-fluid .span9{width:74.30939226519337%;*width:74.25620077583166%}.row-fluid .span8{width:65.74585635359117%;*width:65.69266486422946%}.row-fluid .span7{width:57.18232044198895%;*width:57.12912895262725%}.row-fluid .span6{width:48.61878453038674%;*width:48.56559304102504%}.row-fluid .span5{width:40.05524861878453%;*width:40.00205712942283%}.row-fluid .span4{width:31.491712707182323%;*width:31.43852121782062%}.row-fluid .span3{width:22.92817679558011%;*width:22.87498530621841%}.row-fluid .span2{width:14.3646408839779%;*width:14.311449394616199%}.row-fluid .span1{width:5.801104972375691%;*width:5.747913483013988%}.row-fluid .offset12{margin-left:105.52486187845304%;*margin-left:105.41847889972962%}.row-fluid .offset12:first-child{margin-left:102.76243093922652%;*margin-left:102.6560479605031%}.row-fluid .offset11{margin-left:96.96132596685082%;*margin-left:96.8549429881274%}.row-fluid .offset11:first-child{margin-left:94.1988950276243%;*margin-left:94.09251204890089%}.row-fluid .offset10{margin-left:88.39779005524862%;*margin-left:88.2914070765252%}.row-fluid .offset10:first-child{margin-left:85.6353591160221%;*margin-left:85.52897613729868%}.row-fluid .offset9{margin-left:79.8342541436464%;*margin-left:79.72787116492299%}.row-fluid .offset9:first-child{margin-left:77.07182320441989%;*margin-left:76.96544022569647%}.row-fluid .offset8{margin-left:71.2707182320442%;*margin-left:71.16433525332079%}.row-fluid .offset8:first-child{margin-left:68.50828729281768%;*margin-left:68.40190431409427%}.row-fluid .offset7{margin-left:62.70718232044199%;*margin-left:62.600799341718584%}.row-fluid .offset7:first-child{margin-left:59.94475138121547%;*margin-left:59.838368402492065%}.row-fluid .offset6{margin-left:54.14364640883978%;*margin-left:54.037263430116376%}.row-fluid .offset6:first-child{margin-left:51.38121546961326%;*margin-left:51.27483249088986%}.row-fluid .offset5{margin-left:45.58011049723757%;*margin-left:45.47372751851417%}.row-fluid .offset5:first-child{margin-left:42.81767955801105%;*margin-left:42.71129657928765%}.row-fluid .offset4{margin-left:37.01657458563536%;*margin-left:36.91019160691196%}.row-fluid .offset4:first-child{margin-left:34.25414364640884%;*margin-left:34.14776066768544%}.row-fluid .offset3{margin-left:28.45303867403315%;*margin-left:28.346655695309746%}.row-fluid .offset3:first-child{margin-left:25.69060773480663%;*margin-left:25.584224756083227%}.row-fluid .offset2{margin-left:19.88950276243094%;*margin-left:19.783119783707537%}.row-fluid .offset2:first-child{margin-left:17.12707182320442%;*margin-left:17.02068884448102%}.row-fluid .offset1{margin-left:11.32596685082873%;*margin-left:11.219583872105325%}.row-fluid .offset1:first-child{margin-left:8.56353591160221%;*margin-left:8.457152932878806%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span12,textarea.span12,.uneditable-input.span12{width:710px}input.span11,textarea.span11,.uneditable-input.span11{width:648px}input.span10,textarea.span10,.uneditable-input.span10{width:586px}input.span9,textarea.span9,.uneditable-input.span9{width:524px}input.span8,textarea.span8,.uneditable-input.span8{width:462px}input.span7,textarea.span7,.uneditable-input.span7{width:400px}input.span6,textarea.span6,.uneditable-input.span6{width:338px}input.span5,textarea.span5,.uneditable-input.span5{width:276px}input.span4,textarea.span4,.uneditable-input.span4{width:214px}input.span3,textarea.span3,.uneditable-input.span3{width:152px}input.span2,textarea.span2,.uneditable-input.span2{width:90px}input.span1,textarea.span1,.uneditable-input.span1{width:28px}}@media(max-width:767px){body{padding-right:20px;padding-left:20px}.navbar-fixed-top,.navbar-fixed-bottom,.navbar-static-top{margin-right:-20px;margin-left:-20px}.container-fluid{padding:0}.dl-horizontal dt{float:none;width:auto;clear:none;text-align:left}.dl-horizontal dd{margin-left:0}.container{width:auto}.row-fluid{width:100%}.row,.thumbnails{margin-left:0}.thumbnails>li{float:none;margin-left:0}[class*="span"],.uneditable-input[class*="span"],.row-fluid [class*="span"]{display:block;float:none;width:100%;margin-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.span12,.row-fluid .span12{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="offset"]:first-child{margin-left:0}.input-large,.input-xlarge,.input-xxlarge,input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.input-prepend input,.input-append input,.input-prepend input[class*="span"],.input-append input[class*="span"]{display:inline-block;width:auto}.controls-row [class*="span"]+[class*="span"]{margin-left:0}.modal{position:fixed;top:20px;right:20px;left:20px;width:auto;margin:0}.modal.fade{top:-100px}.modal.fade.in{top:20px}}@media(max-width:480px){.nav-collapse{-webkit-transform:translate3d(0,0,0)}.page-header h1 small{display:block;line-height:20px}input[type="checkbox"],input[type="radio"]{border:1px solid #ccc}.form-horizontal .control-label{float:none;width:auto;padding-top:0;text-align:left}.form-horizontal .controls{margin-left:0}.form-horizontal .control-list{padding-top:0}.form-horizontal .form-actions{padding-right:10px;padding-left:10px}.media .pull-left,.media .pull-right{display:block;float:none;margin-bottom:10px}.media-object{margin-right:0;margin-left:0}.modal{top:10px;right:10px;left:10px}.modal-header .close{padding:10px;margin:-10px}.carousel-caption{position:static}}@media(max-width:979px){body{padding-top:0}.navbar-fixed-top,.navbar-fixed-bottom{position:static}.navbar-fixed-top{margin-bottom:20px}.navbar-fixed-bottom{margin-top:20px}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding:5px}.navbar .container{width:auto;padding:0}.navbar .brand{padding-right:10px;padding-left:10px;margin:0 0 0 -5px}.nav-collapse{clear:both}.nav-collapse .nav{float:none;margin:0 0 10px}.nav-collapse .nav>li{float:none}.nav-collapse .nav>li>a{margin-bottom:2px}.nav-collapse .nav>.divider-vertical{display:none}.nav-collapse .nav .nav-header{color:#777;text-shadow:none}.nav-collapse .nav>li>a,.nav-collapse .dropdown-menu a{padding:9px 15px;font-weight:bold;color:#777;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.nav-collapse .btn{padding:4px 10px 4px;font-weight:normal;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.nav-collapse .dropdown-menu li+li a{margin-bottom:2px}.nav-collapse .nav>li>a:hover,.nav-collapse .nav>li>a:focus,.nav-collapse .dropdown-menu a:hover,.nav-collapse .dropdown-menu a:focus{background-color:#f2f2f2}.navbar-inverse .nav-collapse .nav>li>a,.navbar-inverse .nav-collapse .dropdown-menu a{color:#999}.navbar-inverse .nav-collapse .nav>li>a:hover,.navbar-inverse .nav-collapse .nav>li>a:focus,.navbar-inverse .nav-collapse .dropdown-menu a:hover,.navbar-inverse .nav-collapse .dropdown-menu a:focus{background-color:#111}.nav-collapse.in .btn-group{padding:0;margin-top:5px}.nav-collapse .dropdown-menu{position:static;top:auto;left:auto;display:none;float:none;max-width:none;padding:0;margin:0 15px;background-color:transparent;border:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.nav-collapse .open>.dropdown-menu{display:block}.nav-collapse .dropdown-menu:before,.nav-collapse .dropdown-menu:after{display:none}.nav-collapse .dropdown-menu .divider{display:none}.nav-collapse .nav>li>.dropdown-menu:before,.nav-collapse .nav>li>.dropdown-menu:after{display:none}.nav-collapse .navbar-form,.nav-collapse .navbar-search{float:none;padding:10px 15px;margin:10px 0;border-top:1px solid #f2f2f2;border-bottom:1px solid #f2f2f2;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1)}.navbar-inverse .nav-collapse .navbar-form,.navbar-inverse .nav-collapse .navbar-search{border-top-color:#111;border-bottom-color:#111}.navbar .nav-collapse .nav.pull-right{float:none;margin-left:0}.nav-collapse,.nav-collapse.collapse{height:0;overflow:hidden}.navbar .btn-navbar{display:block}.navbar-static .navbar-inner{padding-right:10px;padding-left:10px}}@media(min-width:980px){.nav-collapse.collapse{height:auto!important;overflow:visible!important}} 10 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap.js by @fat & @mdo 3 | * Copyright 2013 Twitter, Inc. 4 | * http://www.apache.org/licenses/LICENSE-2.0.txt 5 | */ 6 | !function(e){"use strict";e(function(){e.support.transition=function(){var e=function(){var e=document.createElement("bootstrap"),t={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"},n;for(n in t)if(e.style[n]!==undefined)return t[n]}();return e&&{end:e}}()})}(window.jQuery),!function(e){"use strict";var t='[data-dismiss="alert"]',n=function(n){e(n).on("click",t,this.close)};n.prototype.close=function(t){function s(){i.trigger("closed").remove()}var n=e(this),r=n.attr("data-target"),i;r||(r=n.attr("href"),r=r&&r.replace(/.*(?=#[^\s]*$)/,"")),i=e(r),t&&t.preventDefault(),i.length||(i=n.hasClass("alert")?n:n.parent()),i.trigger(t=e.Event("close"));if(t.isDefaultPrevented())return;i.removeClass("in"),e.support.transition&&i.hasClass("fade")?i.on(e.support.transition.end,s):s()};var r=e.fn.alert;e.fn.alert=function(t){return this.each(function(){var r=e(this),i=r.data("alert");i||r.data("alert",i=new n(this)),typeof t=="string"&&i[t].call(r)})},e.fn.alert.Constructor=n,e.fn.alert.noConflict=function(){return e.fn.alert=r,this},e(document).on("click.alert.data-api",t,n.prototype.close)}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.button.defaults,n)};t.prototype.setState=function(e){var t="disabled",n=this.$element,r=n.data(),i=n.is("input")?"val":"html";e+="Text",r.resetText||n.data("resetText",n[i]()),n[i](r[e]||this.options[e]),setTimeout(function(){e=="loadingText"?n.addClass(t).attr(t,t):n.removeClass(t).removeAttr(t)},0)},t.prototype.toggle=function(){var e=this.$element.closest('[data-toggle="buttons-radio"]');e&&e.find(".active").removeClass("active"),this.$element.toggleClass("active")};var n=e.fn.button;e.fn.button=function(n){return this.each(function(){var r=e(this),i=r.data("button"),s=typeof n=="object"&&n;i||r.data("button",i=new t(this,s)),n=="toggle"?i.toggle():n&&i.setState(n)})},e.fn.button.defaults={loadingText:"loading..."},e.fn.button.Constructor=t,e.fn.button.noConflict=function(){return e.fn.button=n,this},e(document).on("click.button.data-api","[data-toggle^=button]",function(t){var n=e(t.target);n.hasClass("btn")||(n=n.closest(".btn")),n.button("toggle")})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.$indicators=this.$element.find(".carousel-indicators"),this.options=n,this.options.pause=="hover"&&this.$element.on("mouseenter",e.proxy(this.pause,this)).on("mouseleave",e.proxy(this.cycle,this))};t.prototype={cycle:function(t){return t||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(e.proxy(this.next,this),this.options.interval)),this},getActiveIndex:function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},to:function(t){var n=this.getActiveIndex(),r=this;if(t>this.$items.length-1||t<0)return;return this.sliding?this.$element.one("slid",function(){r.to(t)}):n==t?this.pause().cycle():this.slide(t>n?"next":"prev",e(this.$items[t]))},pause:function(t){return t||(this.paused=!0),this.$element.find(".next, .prev").length&&e.support.transition.end&&(this.$element.trigger(e.support.transition.end),this.cycle(!0)),clearInterval(this.interval),this.interval=null,this},next:function(){if(this.sliding)return;return this.slide("next")},prev:function(){if(this.sliding)return;return this.slide("prev")},slide:function(t,n){var r=this.$element.find(".item.active"),i=n||r[t](),s=this.interval,o=t=="next"?"left":"right",u=t=="next"?"first":"last",a=this,f;this.sliding=!0,s&&this.pause(),i=i.length?i:this.$element.find(".item")[u](),f=e.Event("slide",{relatedTarget:i[0],direction:o});if(i.hasClass("active"))return;this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid",function(){var t=e(a.$indicators.children()[a.getActiveIndex()]);t&&t.addClass("active")}));if(e.support.transition&&this.$element.hasClass("slide")){this.$element.trigger(f);if(f.isDefaultPrevented())return;i.addClass(t),i[0].offsetWidth,r.addClass(o),i.addClass(o),this.$element.one(e.support.transition.end,function(){i.removeClass([t,o].join(" ")).addClass("active"),r.removeClass(["active",o].join(" ")),a.sliding=!1,setTimeout(function(){a.$element.trigger("slid")},0)})}else{this.$element.trigger(f);if(f.isDefaultPrevented())return;r.removeClass("active"),i.addClass("active"),this.sliding=!1,this.$element.trigger("slid")}return s&&this.cycle(),this}};var n=e.fn.carousel;e.fn.carousel=function(n){return this.each(function(){var r=e(this),i=r.data("carousel"),s=e.extend({},e.fn.carousel.defaults,typeof n=="object"&&n),o=typeof n=="string"?n:s.slide;i||r.data("carousel",i=new t(this,s)),typeof n=="number"?i.to(n):o?i[o]():s.interval&&i.pause().cycle()})},e.fn.carousel.defaults={interval:5e3,pause:"hover"},e.fn.carousel.Constructor=t,e.fn.carousel.noConflict=function(){return e.fn.carousel=n,this},e(document).on("click.carousel.data-api","[data-slide], [data-slide-to]",function(t){var n=e(this),r,i=e(n.attr("data-target")||(r=n.attr("href"))&&r.replace(/.*(?=#[^\s]+$)/,"")),s=e.extend({},i.data(),n.data()),o;i.carousel(s),(o=n.attr("data-slide-to"))&&i.data("carousel").pause().to(o).cycle(),t.preventDefault()})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.collapse.defaults,n),this.options.parent&&(this.$parent=e(this.options.parent)),this.options.toggle&&this.toggle()};t.prototype={constructor:t,dimension:function(){var e=this.$element.hasClass("width");return e?"width":"height"},show:function(){var t,n,r,i;if(this.transitioning||this.$element.hasClass("in"))return;t=this.dimension(),n=e.camelCase(["scroll",t].join("-")),r=this.$parent&&this.$parent.find("> .accordion-group > .in");if(r&&r.length){i=r.data("collapse");if(i&&i.transitioning)return;r.collapse("hide"),i||r.data("collapse",null)}this.$element[t](0),this.transition("addClass",e.Event("show"),"shown"),e.support.transition&&this.$element[t](this.$element[0][n])},hide:function(){var t;if(this.transitioning||!this.$element.hasClass("in"))return;t=this.dimension(),this.reset(this.$element[t]()),this.transition("removeClass",e.Event("hide"),"hidden"),this.$element[t](0)},reset:function(e){var t=this.dimension();return this.$element.removeClass("collapse")[t](e||"auto")[0].offsetWidth,this.$element[e!==null?"addClass":"removeClass"]("collapse"),this},transition:function(t,n,r){var i=this,s=function(){n.type=="show"&&i.reset(),i.transitioning=0,i.$element.trigger(r)};this.$element.trigger(n);if(n.isDefaultPrevented())return;this.transitioning=1,this.$element[t]("in"),e.support.transition&&this.$element.hasClass("collapse")?this.$element.one(e.support.transition.end,s):s()},toggle:function(){this[this.$element.hasClass("in")?"hide":"show"]()}};var n=e.fn.collapse;e.fn.collapse=function(n){return this.each(function(){var r=e(this),i=r.data("collapse"),s=e.extend({},e.fn.collapse.defaults,r.data(),typeof n=="object"&&n);i||r.data("collapse",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.collapse.defaults={toggle:!0},e.fn.collapse.Constructor=t,e.fn.collapse.noConflict=function(){return e.fn.collapse=n,this},e(document).on("click.collapse.data-api","[data-toggle=collapse]",function(t){var n=e(this),r,i=n.attr("data-target")||t.preventDefault()||(r=n.attr("href"))&&r.replace(/.*(?=#[^\s]+$)/,""),s=e(i).data("collapse")?"toggle":n.data();n[e(i).hasClass("in")?"addClass":"removeClass"]("collapsed"),e(i).collapse(s)})}(window.jQuery),!function(e){"use strict";function r(){e(".dropdown-backdrop").remove(),e(t).each(function(){i(e(this)).removeClass("open")})}function i(t){var n=t.attr("data-target"),r;n||(n=t.attr("href"),n=n&&/#/.test(n)&&n.replace(/.*(?=#[^\s]*$)/,"")),r=n&&e(n);if(!r||!r.length)r=t.parent();return r}var t="[data-toggle=dropdown]",n=function(t){var n=e(t).on("click.dropdown.data-api",this.toggle);e("html").on("click.dropdown.data-api",function(){n.parent().removeClass("open")})};n.prototype={constructor:n,toggle:function(t){var n=e(this),s,o;if(n.is(".disabled, :disabled"))return;return s=i(n),o=s.hasClass("open"),r(),o||("ontouchstart"in document.documentElement&&e('').insertBefore(e(this)).on("click",r),s.toggleClass("open")),n.focus(),!1},keydown:function(n){var r,s,o,u,a,f;if(!/(38|40|27)/.test(n.keyCode))return;r=e(this),n.preventDefault(),n.stopPropagation();if(r.is(".disabled, :disabled"))return;u=i(r),a=u.hasClass("open");if(!a||a&&n.keyCode==27)return n.which==27&&u.find(t).focus(),r.click();s=e("[role=menu] li:not(.divider):visible a",u);if(!s.length)return;f=s.index(s.filter(":focus")),n.keyCode==38&&f>0&&f--,n.keyCode==40&&f