diff --git a/app/controllers/admin/backups_controller.rb b/app/controllers/admin/backups_controller.rb
new file mode 100644
index 00000000000..dae617b0426
--- /dev/null
+++ b/app/controllers/admin/backups_controller.rb
@@ -0,0 +1,84 @@
+require_dependency "backup_restore"
+
+class Admin::BackupsController < Admin::AdminController
+
+  skip_before_filter :check_xhr, only: [:index, :show]
+
+  def index
+    respond_to do |format|
+      format.html do
+        store_preloaded("backups", MultiJson.dump(serialize_data(Backup.all, BackupSerializer)))
+        store_preloaded("operations_status", MultiJson.dump(BackupRestore.operations_status))
+        render "default/empty"
+      end
+      format.json do
+        render_serialized(Backup.all, BackupSerializer)
+      end
+    end
+  end
+
+  def status
+    render_json_dump(BackupRestore.operations_status)
+  end
+
+  def create
+    BackupRestore.backup!(current_user.id, true)
+  rescue BackupRestore::OperationRunningError
+    render json: failed_json.merge(message: I18n.t("backup.operation_already_running"))
+  else
+    render json: success_json
+  end
+
+  def cancel
+    BackupRestore.cancel!
+  rescue BackupRestore::OperationRunningError
+    render json: failed_json.merge(message: I18n.t("backup.operation_already_running"))
+  else
+    render json: success_json
+  end
+
+  # download
+  def show
+    filename = params.fetch(:id)
+    if backup = Backup[filename]
+      send_file backup.path
+    else
+      render nothing: true, status: 404
+    end
+  end
+
+  def destroy
+    filename = params.fetch(:id)
+    Backup.remove(filename)
+    render nothing: true
+  end
+
+  def logs
+    store_preloaded("operations_status", MultiJson.dump(BackupRestore.operations_status))
+    render "default/empty"
+  end
+
+  def restore
+    filename = params.fetch(:id)
+    BackupRestore.restore!(current_user.id, filename, true)
+  rescue BackupRestore::OperationRunningError
+    render json: failed_json.merge(message: I18n.t("backup.operation_already_running"))
+  else
+    render json: success_json
+  end
+
+  def rollback
+    BackupRestore.rollback!
+  rescue BackupRestore::OperationRunningError
+    render json: failed_json.merge(message: I18n.t("backup.operation_already_running"))
+  else
+    render json: success_json
+  end
+
+  def readonly
+    enable = params.fetch(:enable).to_s == "true"
+    enable ? Discourse.enable_readonly_mode : Discourse.disable_readonly_mode
+    render nothing: true
+  end
+
+end
diff --git a/spec/controllers/admin/backups_controller_spec.rb b/spec/controllers/admin/backups_controller_spec.rb
new file mode 100644
index 00000000000..936fb9756db
--- /dev/null
+++ b/spec/controllers/admin/backups_controller_spec.rb
@@ -0,0 +1,194 @@
+require "spec_helper"
+
+describe Admin::BackupsController do
+
+  it "is a subclass of AdminController" do
+    (Admin::BackupsController < Admin::AdminController).should be_true
+  end
+
+  let(:backup_filename) { "2014-02-10-065935.tar.gz" }
+
+  context "while logged in as an admin" do
+
+    before { @admin = log_in(:admin) }
+
+    describe ".index" do
+
+      context "html format" do
+
+        it "preloads both backups and operations_status" do
+          Backup.expects(:all).returns([])
+          subject.expects(:store_preloaded).with("backups", "[]")
+
+          BackupRestore.expects(:operations_status).returns({})
+          subject.expects(:store_preloaded).with("operations_status", "{}")
+
+          xhr :get, :index, format: :html
+
+          response.should be_success
+        end
+
+      end
+
+      context "json format" do
+
+        it "returns a list of all the backups" do
+          File.stubs(:size).returns(42)
+          Backup.expects(:all).returns([Backup.new("backup1"), Backup.new("backup2")])
+
+          xhr :get, :index, format: :json
+
+          response.should be_success
+
+          json = JSON.parse(response.body)
+          json[0]["filename"].should == "backup1"
+          json[1]["filename"].should == "backup2"
+        end
+
+      end
+
+    end
+
+    describe ".status" do
+
+      it "returns the current backups status" do
+        BackupRestore.expects(:operations_status)
+
+        xhr :get, :status
+
+        response.should be_success
+      end
+
+    end
+
+    describe ".create" do
+
+      it "starts a backup" do
+        BackupRestore.expects(:backup!).with(@admin.id, true)
+
+        xhr :post, :create
+
+        response.should be_success
+      end
+
+      # it "catches OperationRunningError exception" do
+      #   BackupRestore.expects(:is_operation_running?).returns(true)
+
+      #   xhr :post, :create
+
+      #   response.should be_success
+
+      #   json = JSON.parse(response.body)
+      #   json["message"].should_not be_nil
+      # end
+
+    end
+
+    describe ".cancel" do
+
+      it "cancels an export" do
+        BackupRestore.expects(:cancel!)
+
+        xhr :delete, :cancel
+
+        response.should be_success
+      end
+
+    end
+
+    describe ".show" do
+
+      it "uses send_file to transmit the backup" do
+        controller.stubs(:render) # we need this since we're stubing send_file
+
+        File.stubs(:size).returns(42)
+        backup = Backup.new("backup42")
+
+        Backup.expects(:[]).with(backup_filename).returns(backup)
+        subject.expects(:send_file).with(backup.path)
+
+        get :show, id: backup_filename
+      end
+
+      it "returns 404 when the backup does not exist" do
+        Backup.expects(:[]).returns(nil)
+
+        get :show, id: backup_filename
+
+        response.should be_not_found
+      end
+
+    end
+
+    describe ".destroy" do
+
+      it "removes the backup" do
+        Backup.expects(:remove).with(backup_filename)
+
+        xhr :delete, :destroy, id: backup_filename
+
+        response.should be_success
+      end
+
+    end
+
+    describe ".logs" do
+
+      it "preloads operations_status" do
+        BackupRestore.expects(:operations_status).returns({})
+        subject.expects(:store_preloaded).with("operations_status", "{}")
+
+        xhr :get, :logs, format: :html
+
+        response.should be_success
+      end
+
+    end
+
+    describe ".restore" do
+
+      it "starts a restore" do
+        BackupRestore.expects(:restore!).with(@admin.id, backup_filename, true)
+
+        xhr :post, :restore, id: backup_filename
+
+        response.should be_success
+      end
+
+    end
+
+    describe ".rollback" do
+
+      it "rolls back to previous working state" do
+        BackupRestore.expects(:rollback!)
+
+        xhr :get, :rollback
+
+        response.should be_success
+      end
+
+    end
+
+    describe ".readonly" do
+
+      it "enables readonly mode" do
+        Discourse.expects(:enable_readonly_mode)
+
+        xhr :put, :readonly, enable: true
+
+        response.should be_success
+      end
+
+      it "disables readonly mode" do
+        Discourse.expects(:disable_readonly_mode)
+
+        xhr :put, :readonly, enable: false
+
+        response.should be_success
+      end
+
+    end
+
+  end
+
+end