Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Pushing export to a background process, with export progress bar upda…

…tes via ajax :-D
  • Loading branch information...
commit a5d4835baa7e68f4005be2471cac46de9d116ef8 1 parent 853302f
Kieran Pilkington authored
Showing with 381 additions and 42 deletions.
  1. +2 −0  Gemfile
  2. +1 −0  README.md
  3. +6 −0 Rakefile
  4. +0 −1  Todo.md
  5. +43 −0 app/controllers/export_controller.rb
  6. +1 −24 app/controllers/vm_controller.rb
  7. +2 −0  app/helpers/export_helper.rb
  8. +68 −0 app/models/export.rb
  9. +53 −0 app/stylesheets/application.less
  10. +3 −5 app/views/{vm/export.html.erb → export/_form.html.erb}
  11. +9 −0 app/views/export/_progress.html.erb
  12. +26 −0 app/views/export/index.html.erb
  13. +2 −0  app/views/export/new.html.erb
  14. +1 −0  app/views/export/progress.html.erb
  15. +7 −0 app/views/export/show.html.erb
  16. +1 −1  app/views/hd/show.html.erb
  17. +2 −2 app/views/vm/_controls.html.erb
  18. +1 −1  app/views/vm/destroy.html.erb
  19. +1 −1  app/views/vm/settings.html.erb
  20. +2 −0  app/views/vm/show.html.erb
  21. +1 −1  config/application.rb
  22. +22 −0 config/database.yml
  23. +10 −6 config/routes.rb
  24. +21 −0 db/migrate/20100417010923_create_delayed_jobs.rb
  25. +16 −0 db/migrate/20100417030507_create_exports.rb
  26. +38 −0 db/schema.rb
  27. +7 −0 db/seeds.rb
  28. +10 −0 public/javascripts/vboxweb.js
  29. +5 −0 script/delayed_job
  30. +8 −0 test/functional/export_controller_test.rb
  31. +8 −0 test/unit/export_test.rb
  32. +4 −0 test/unit/helpers/export_helper_test.rb
View
2  Gemfile
@@ -4,3 +4,5 @@ gem 'rails', '3.0.0.beta3'
gem 'mongrel'
gem 'less'
gem 'virtualbox', '>= 0.6.0'
+gem 'delayed_job'
+gem 'sqlite3-ruby', :require => 'sqlite3'
View
1  README.md
@@ -14,6 +14,7 @@ Then run the following in a shell console:
$ git clone git://github.com/KieranP/vboxweb_rb.git
$ cd vboxweb_rb
$ bundle install
+ $ script/delayed_job start
Then adjust config/config.yml, changing the username and password.
View
6 Rakefile
@@ -7,4 +7,10 @@ require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'
+begin
+ require 'delayed/tasks'
+rescue LoadError
+ STDERR.puts "Run `bundle install` to install delayed_job"
+end
+
Rails::Application.load_tasks
View
1  Todo.md
@@ -3,7 +3,6 @@
## Virtual Machines
* Readd settings form fields removed after update to work with virtualbox gem v0.6.0
* Export a VM with additional information (feature removed in virtualbox gem v0.6.0)
-* Push exporting to a background process, and use a progress screen via AJAX
* Add links for where the user can download the newly exported VM
* Create a VM
* Import a VM
View
43 app/controllers/export_controller.rb
@@ -0,0 +1,43 @@
+class ExportController < ApplicationController
+ before_filter :find_virtual_machine_from_uuid, :except => [:index]
+ before_filter :redirect_unless_vm_powered_off, :only => [:new, :create]
+
+ def index
+ @vm = VirtualBox::VM.find(params[:uuid])
+ end
+
+ def show
+ @export = Export.find(params[:id])
+ end
+
+ def new
+ if request.post?
+ export = Export.new(:machine_id => @vm.uuid, :export_data => params[:export])
+ if export.save
+ export.export!
+ flash[:notice] = "#{@vm.name} is now exporting. You can see the progress of that import below."
+ redirect_to vm_export_path(:id => export.id)
+ else
+ flash[:error] = export.errors.full_messages.join(', ')
+ end
+ end
+ end
+
+ def progress
+ @export = Export.find(params[:id])
+ render :layout => false
+ end
+
+ private
+
+ def find_virtual_machine_from_uuid
+ @vm = VirtualBox::VM.find(params[:uuid])
+ end
+
+ def redirect_unless_vm_powered_off
+ unless @vm.powered_off?
+ flash[:error] = "Cannot export a virtual machine unless it is powered off."
+ redirect_to vm_path
+ end
+ end
+end
View
25 app/controllers/vm_controller.rb
@@ -33,29 +33,6 @@ def settings
end
end
- def export
- @vm = VirtualBox::VM.find(params[:uuid])
-
- unless @vm.powered_off?
- flash[:error] = "Cannot export a virtual machine unless it is powered off."
- redirect_to vm_path
- end
-
- if request.post?
- filename = params[:export].delete(:filename).parameterize.to_s
- filepath = Rails.root.join('exports', filename, filename+".ovf").to_s
- if File.exist?(filepath)
- flash[:error] = "Export of this name already exists. Please choose another."
- else
- # TODO: Pass in extra params
- # @vm.export(filepath, params[:export])
- @vm.export(filepath)
- flash[:notice] = "#{@vm.name} has been exported to #{filepath}."
- redirect_to vm_path
- end
- end
- end
-
def destroy
@vm = VirtualBox::VM.find(params[:uuid])
@@ -97,7 +74,7 @@ def control
@vm.discard_state if @vm.saved?
flash[:notice] = "#{@vm.name} saved state has been discarded."
else
- flash[:error] = "Unsupported Virtual Machine Operation '#{params[:action]}'"
+ flash[:error] = "Unsupported Virtual Machine Operation '#{params[:command]}'"
end
redirect_to vm_path
View
2  app/helpers/export_helper.rb
@@ -0,0 +1,2 @@
+module ExportHelper
+end
View
68 app/models/export.rb
@@ -0,0 +1,68 @@
+class Export < ActiveRecord::Base
+
+ validates_presence_of :machine_id, :export_data
+
+ serialize :export_data
+
+ before_create :set_status_and_percent_exported_if_blank
+
+ STATUSES = {
+ :starting => 'Starting...',
+ :failed => 'Failed... (if the vm running?)',
+ :running => 'Running...',
+ :finalizing => 'Finalizing...',
+ :completed => "Completed"
+ }
+
+ def machine
+ @machine ||= VirtualBox::VM.find(machine_id)
+ end
+
+ def filename
+ @filename ||= export_data[:filename].to_s.parameterize
+ end
+
+ def filepath
+ @filepath ||= Rails.root.join('exports', filename, filename+".ovf").to_s
+ end
+
+ def export!
+ unless machine.powered_off?
+ update_attribute(:status, 'failed')
+ return false
+ end
+ update_attribute(:status, 'running')
+ machine.export(filepath) do |percent|
+ update_attribute(:percent_exported, percent)
+ update_attribute(:status, 'finalizing') if percent.to_i == 100
+ end
+ update_attribute(:status, 'completed')
+ end
+ handle_asynchronously :export!
+
+ Export::STATUSES.keys.each do |status_value|
+ define_method "#{status_value}?" do
+ status.to_s.downcase == status_value.to_s
+ end
+ end
+
+ private
+
+ def set_status_and_percent_exported_if_blank
+ self.status = 'starting' if self.status.blank?
+ self.percent_exported = 0 if self.percent_exported.blank?
+ end
+
+ def validate
+ if filename.blank?
+ errors.add_to_base("You must specify a filename to export to.")
+ false
+ elsif File.exist?(filepath)
+ errors.add_to_base("Export of this name already exists. Please choose another.")
+ false
+ else
+ true
+ end
+ end
+
+end
View
53 app/stylesheets/application.less
@@ -240,6 +240,46 @@ body {
}
}
}
+
+ .previous_exports_table {
+ tr {
+ th, td {
+ padding: 3px 20px;
+ text-align: center;
+ }
+ }
+ }
+
+ #export_status {
+ text-align: center;
+ font-size: 1.3em;
+
+ .percent_exported {
+ position: relative;
+ width: 200px;
+ height: 25px;
+ margin: 5px auto;
+ border: 1px solid #000;
+
+ .bar {
+ position: absolute;
+ z-index: 1;
+ height: 25px;
+ background-color: green;
+ }
+
+ .text {
+ position: relative;
+ z-index: 2;
+ margin-top: 5px;
+ }
+ }
+ }
+
+ #return_to_exports {
+ text-align: center;
+ margin-top: 25px;
+ }
}
#footer {
@@ -249,6 +289,19 @@ body {
border: 1px solid #000;
}
+h1 {
+ font-size: 1.4em;
+}
+
+h2 {
+ font-size: 1.2em;
+}
+
+h3 {
+ font-size: 1.0em;
+ margin: 10px 0;
+}
+
p {
margin: 10px 0;
}
View
8 app/views/vm/export.html.erb → app/views/export/_form.html.erb
@@ -1,11 +1,9 @@
-<h3 class="heading">Exporting <%= @vm.name %></h3>
-
<p>
Here you are able to export this Virtual Machine into
- a format other VirtualBox users are able to import
+ a format other VirtualBox users are able to import.
</p>
-<%= form_tag(vm_export_path, :method => :post) do %>
+<%= form_tag(vm_new_export_path, :method => :post) do %>
<fieldset class="data_box">
<legend class="heading">Export Settings (required)</legend>
@@ -50,6 +48,6 @@
</div>
</fieldset> -->
- <%= submit_tag 'Export Virtual Machine' %> (may take several minutes)
+ <%= submit_tag 'Export Virtual Machine' %>
<% end %>
View
9 app/views/export/_progress.html.erb
@@ -0,0 +1,9 @@
+<div class="status"><%= Export::STATUSES[@export.status.to_sym] %></div>
+<div class="percent_exported">
+ <div class="bar" style="width: <%= @export.percent_exported %>%;"></div>
+ <div class="text"><%= @export.percent_exported %>% </div>
+</div>
+
+<div class="reload"><%= link_to('Reload', '') unless request.xhr? %></div>
+
+<%= javascript_tag("stop_current_export_progress();") if @export.completed? %>
View
26 app/views/export/index.html.erb
@@ -0,0 +1,26 @@
+<h2 class="heading"><%= @vm.name %> Exports</h2>
+
+<h3>Create Export</h3>
+<%= render 'form' %>
+
+<% previous_exports = Export.find_all_by_machine_id(@vm.uuid) %>
+<% if previous_exports.size > 0 %>
+ <h3>Previous Exports</h3>
+ <table class="previous_exports_table">
+ <tr class="export_headings">
+ <th class="date">Exported</th>
+ <th class="filename">Filename</th>
+ <th class="status">Status</th>
+ </tr>
+ <% previous_exports.each do |export| %>
+ <tr class="export">
+ <td class="date"><%= export.created_at.to_date %></td>
+ <td class="filename"><%= export.filename %></td>
+ <td class="status"><%= export.status %> <%= "(#{export.percent_exported}%)" if export.running? %></td>
+ <% if export.running? %>
+ <td><%= link_to '(view export progress)', vm_export_path(:id => export.id) %></td>
+ <% end %>
+ </tr>
+ <% end %>
+ </table>
+<% end %>
View
2  app/views/export/new.html.erb
@@ -0,0 +1,2 @@
+<h2 class="heading">Export <%= @vm.name %></h2>
+<%= render 'form' %>
View
1  app/views/export/progress.html.erb
@@ -0,0 +1 @@
+<%= render 'progress' %>
View
7 app/views/export/show.html.erb
@@ -0,0 +1,7 @@
+<h2 class="heading">Exporting <%= @export.machine.name %>...</h2>
+
+<div id="export_status"><%= render 'progress' %></div>
+
+<%= javascript_tag("update_export_progress('export_status', '#{vm_export_progress_path}');") unless @export.completed? %>
+
+<div id="return_to_exports"><%= link_to 'Return to Exports', vm_exports_path %></div>
View
2  app/views/hd/show.html.erb
@@ -1,4 +1,4 @@
-<h3 class="heading">Displaying <%= @hd.filename %></h3>
+<h2 class="heading">Displaying <%= @hd.filename %></h2>
<div id="general" class="data_box">
<% unless @hd.description.blank? %>
View
4 app/views/vm/_controls.html.erb
@@ -13,8 +13,8 @@
</div>
<div class="action">
- <div class="icon"><%= link_to(vbicon('controls/export_16px', 'Export'), vm_export_path) %></div>
- <div class="label"><%= link_to('Export', vm_export_path) %></div>
+ <div class="icon"><%= link_to(vbicon('controls/export_16px', 'Exports'), vm_exports_path) %></div>
+ <div class="label"><%= link_to('Exports', vm_exports_path) %></div>
</div>
<div class="action">
View
2  app/views/vm/destroy.html.erb
@@ -1,4 +1,4 @@
-<h3>Are you sure you want to do this?</h3>
+<h2 class="heading">Are you sure you want to do this?</h2>
<p>
Deleting a Virtual Machine is not undoable. Once executed, the
View
2  app/views/vm/settings.html.erb
@@ -1,4 +1,4 @@
-<h3 class="heading">Updating <%= @vm.name %></h3>
+<h2 class="heading">Updating <%= @vm.name %></h2>
<div id="vm_properties">
<%= form_tag(vm_settings_path, :method => :put) do %>
View
2  app/views/vm/show.html.erb
@@ -1,3 +1,5 @@
+<h2 class="heading">Viewing <%= @vm.name %></h2>
+
<%= render 'controls' %>
<div id="categories">
View
2  config/application.rb
@@ -1,7 +1,7 @@
require File.expand_path('../boot', __FILE__)
# Pick the frameworks you want:
-# require "active_record/railtie"
+require "active_record/railtie"
require "action_controller/railtie"
# require "action_mailer/railtie"
# require "active_resource/railtie"
View
22 config/database.yml
@@ -0,0 +1,22 @@
+# SQLite version 3.x
+# gem install sqlite3-ruby (not necessary on OS X Leopard)
+development:
+ adapter: sqlite3
+ database: db/development.sqlite3
+ pool: 5
+ timeout: 5000
+
+# Warning: The database defined as "test" will be erased and
+# re-generated from your development database when you run "rake".
+# Do not set this db to the same as development or production.
+test:
+ adapter: sqlite3
+ database: db/test.sqlite3
+ pool: 5
+ timeout: 5000
+
+production:
+ adapter: sqlite3
+ database: db/production.sqlite3
+ pool: 5
+ timeout: 5000
View
16 config/routes.rb
@@ -1,11 +1,15 @@
VboxwebRb::Application.routes.draw do |map|
- match 'vm/:uuid/settings' => 'vm#settings', :as => 'vm_settings'
- match 'vm/:uuid/export' => 'vm#export', :as => 'vm_export'
- match 'vm/:uuid/destroy' => 'vm#destroy', :as => 'vm_destroy'
- match 'vm/:uuid/:command' => 'vm#control', :as => 'vm_control'
- match 'vm/:uuid' => 'vm#show', :as => 'vm'
+ match 'vm/:uuid/exports' => 'export#index', :as => 'vm_exports'
+ match 'vm/:uuid/exports/new' => 'export#new', :as => 'vm_new_export'
+ match 'vm/:uuid/exports/:id/progress' => 'export#progress', :as => 'vm_export_progress'
+ match 'vm/:uuid/exports/:id' => 'export#show', :as => 'vm_export'
- match 'hd/:uuid' => 'hd#show', :as => 'hd'
+ match 'vm/:uuid/settings' => 'vm#settings', :as => 'vm_settings'
+ match 'vm/:uuid/destroy' => 'vm#destroy', :as => 'vm_destroy'
+ match 'vm/:uuid/:command' => 'vm#control', :as => 'vm_control'
+ match 'vm/:uuid' => 'vm#show', :as => 'vm'
+
+ match 'hd/:uuid' => 'hd#show', :as => 'hd'
root :to => 'homepage#index'
end
View
21 db/migrate/20100417010923_create_delayed_jobs.rb
@@ -0,0 +1,21 @@
+class CreateDelayedJobs < ActiveRecord::Migration
+ def self.up
+ create_table :delayed_jobs, :force => true do |table|
+ table.integer :priority, :default => 0 # Allows some jobs to jump to the front of the queue
+ table.integer :attempts, :default => 0 # Provides for retries, but still fail eventually.
+ table.text :handler # YAML-encoded string of the object that will do work
+ table.text :last_error # reason for last failure (See Note below)
+ table.datetime :run_at # When to run. Could be Time.zone.now for immediately, or sometime in the future.
+ table.datetime :locked_at # Set when a client is working on this object
+ table.datetime :failed_at # Set when all retries have failed (actually, by default, the record is deleted instead)
+ table.string :locked_by # Who is working on this object (if locked)
+ table.timestamps
+ end
+
+ add_index :delayed_jobs, [:priority, :run_at], :name => 'delayed_jobs_priority'
+ end
+
+ def self.down
+ drop_table :delayed_jobs
+ end
+end
View
16 db/migrate/20100417030507_create_exports.rb
@@ -0,0 +1,16 @@
+class CreateExports < ActiveRecord::Migration
+ def self.up
+ create_table :exports do |t|
+ t.string :machine_id
+ t.string :export_data
+ t.string :status
+ t.integer :percent_exported
+
+ t.timestamps
+ end
+ end
+
+ def self.down
+ drop_table :exports
+ end
+end
View
38 db/schema.rb
@@ -0,0 +1,38 @@
+# This file is auto-generated from the current state of the database. Instead of editing this file,
+# please use the migrations feature of Active Record to incrementally modify your database, and
+# then regenerate this schema definition.
+#
+# Note that this schema.rb definition is the authoritative source for your database schema. If you need
+# to create the application database on another system, you should be using db:schema:load, not running
+# all the migrations from scratch. The latter is a flawed and unsustainable approach (the more migrations
+# you'll amass, the slower it'll run and the greater likelihood for issues).
+#
+# It's strongly recommended to check this file into your version control system.
+
+ActiveRecord::Schema.define(:version => 20100417030507) do
+
+ create_table "delayed_jobs", :force => true do |t|
+ t.integer "priority", :default => 0
+ t.integer "attempts", :default => 0
+ t.text "handler"
+ t.text "last_error"
+ t.datetime "run_at"
+ t.datetime "locked_at"
+ t.datetime "failed_at"
+ t.string "locked_by"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ add_index "delayed_jobs", ["priority", "run_at"], :name => "delayed_jobs_priority"
+
+ create_table "exports", :force => true do |t|
+ t.string "machine_id"
+ t.string "export_data"
+ t.string "status"
+ t.integer "percent_exported"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+end
View
7 db/seeds.rb
@@ -0,0 +1,7 @@
+# This file should contain all the record creation needed to seed the database with its default values.
+# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
+#
+# Examples:
+#
+# cities = City.create([{ :name => 'Chicago' }, { :name => 'Copenhagen' }])
+# Mayor.create(:name => 'Daley', :city => cities.first)
View
10 public/javascripts/vboxweb.js
@@ -1,3 +1,13 @@
+var export_progress_updater;
+
+function update_export_progress(element_id, url) {
+ export_progress_updater = new Ajax.PeriodicalUpdater(element_id, url, { evalScripts: true });
+}
+
+function stop_current_export_progress() {
+ if(export_progress_updater) { export_progress_updater.stop(); }
+}
+
document.observe('dom:loaded', function() {
$$('.record, .action').each(function(record) {
View
5 script/delayed_job
@@ -0,0 +1,5 @@
+#!/usr/bin/env ruby
+
+require File.expand_path(File.join(File.dirname(__FILE__), '..', 'config', 'environment'))
+require 'delayed/command'
+Delayed::Command.new(ARGV).daemonize
View
8 test/functional/export_controller_test.rb
@@ -0,0 +1,8 @@
+require 'test_helper'
+
+class ExportControllerTest < ActionController::TestCase
+ # Replace this with your real tests.
+ test "the truth" do
+ assert true
+ end
+end
View
8 test/unit/export_test.rb
@@ -0,0 +1,8 @@
+require 'test_helper'
+
+class ExportTest < ActiveSupport::TestCase
+ # Replace this with your real tests.
+ test "the truth" do
+ assert true
+ end
+end
View
4 test/unit/helpers/export_helper_test.rb
@@ -0,0 +1,4 @@
+require 'test_helper'
+
+class ExportHelperTest < ActionView::TestCase
+end
Please sign in to comment.
Something went wrong with that request. Please try again.