Rails 4 Angularjs Paperclip как загрузить файл

Я начинаю манипулировать угловыми с Rails 4, которые предоставляют только api. Я пытаюсь создать простую службу angular для загрузки файла. Но я использую Paperclip для управления файлом, и у меня есть некоторые проблемы.

Во-первых, я не понимаю, как правильно собрать файл ввода. Я вижу много директив плагина или жира, чтобы сделать это. Но я хочу, чтобы просто простая директива, которая собирала мой файл и помещала в мою ng-модель.

И, наконец, я хочу знать, эффективнее ли кодировать мой файл в Base64?

Контроллер My Rails

 
class Api::EmployeesController < Api::BaseController
  def create
    employee = Employee.create(employee_params)
    if employee.save
      render json: employee
    else
     render :json => { :errors => employee.errors.full_messages }, :status => 406
     end
  end

  def employee_params
    params.require(:employee).permit(:first_name,:mobile_phone,:file)
  end
end

Моя служба Angularjs

 
angular.module('test').factory 'Employee', ($resource, $http) ->
 class Employee
  constructor: (errorHandler) ->
  @service = $resource('/api/employees/:id',
  {id: '@id'},
  {update: {method: 'PATCH'}})
  @errorHandler = errorHandler

  create: (attrs, $scope) ->
    new @service(employee: attrs).$save ((employee) ->
      $scope.employees.push(employee)
      $scope.success = true
      $timeout (->
        $scope.success = false
      ), 3000
    ), @errorHandler

My Angularjs Controller

 
angular.module('test').controller "EmployeesController", ($scope, $timeout,  $routeParams, $location, Employee) ->

$scope.init = ->
 @employeeService = new Employee(serverErrorHandler)
 $scope.employees = @employeeService.all($scope)

$scope.createEmployee = (employee) ->
  if $scope.employeeFirstName
    @employeeService.create (
      first_name: $scope.employeeFirstName
      last_name:     $scope.employeeLastName
      promotion: $scope.employeePromotion
      mobile_phone: $scope.employeeMobilePhone
      nationality: $scope.employeeNationality
      social_number: $scope.employeeSocialNumber
      born_place: $scope.employeeBornPlace
      employee_convention: $scope.employeeConvention
      employee_type: $scope.employeeType
  ), $scope
  else
    $scope.error = "fields missing"

Ответ 1

После нескольких дней устранения неполадок и выяснения того, как работают обе технологии (я новичок в обоих.), мне удалось что-то сделать. Я не знаю, лучший ли это, но он работает. Если у кого есть какие-то улучшения, я буду рад их услышать.

В общем, я сделал следующее:

  • Создать директиву в AngularJS для обработки загрузки файла
    • Закодировал файл как строку base64 и привязал его к объекту JSON.
  • Контроллер Rails декодировал base64 String с помощью StringIO и снова привязал файл к параметрам
    • Затем я обновил или создал модель с новыми обновленными параметрами.

Это было действительно круто, поэтому, если есть другой способ сделать это, я хотел бы знать!

Я использую Rails 4 и самую последнюю стабильную версию AngularJS, Paperclip и Restangular.

Вот связанный код:

Директива Angularjs

var baseUrl = 'http localhost:port'; // fill in as needed

angular.module('uploadFile', ['Restangular']) // using restangular is optional

.directive('uploadImage', function () {
return {
 restrict: 'A',
 link: function (scope, elem, attrs) {
  var reader = new FileReader();
  reader.onload = function (e) {
    // retrieves the image data from the reader.readAsBinaryString method and stores as data
    // calls the uploadImage method, which does a post or put request to server
    scope.user.imageData = btoa(e.target.result);
    scope.uploadImage(scope.user.imagePath);
    // updates scope
    scope.$apply();
  };

  // listens on change event
  elem.on('change', function() {
    console.log('entered change function');
    var file = elem[0].files[0];
    // gathers file data (filename and type) to send in json
    scope.user.imageContent = file.type;
    scope.user.imagePath = file.name;
    // updates scope; not sure if this is needed here, I can not remember with the testing I did...and I do not quite understand the apply method that well, as I have read limited documentation on it.
    scope.$apply();
    // converts file to binary string
    reader.readAsBinaryString(file);
  });
 },
 // not sure where the restangular dependency is needed. This is in my code from troubleshooting scope issues before, it may not be needed in all locations. will have to reevaluate when I have time to clean up code.
 // Restangular is a nice module for handling REST transactions in angular. It is certainly optional, but it was used in my project.
 controller: ['$scope', 'Restangular', function($scope, Restangular){
  $scope.uploadImage = function (path) {
   // if updating user
    if ($scope.user.id) {
      // do put request
      $scope.user.put().then( function (result) {
        // create image link (rails returns the url location of the file; depending on your application config, you may not need baseurl)
        $scope.userImageLink = baseUrl + result.image_url;
      }, function (error) {
        console.log('errors', JSON.stringify(errors));
      });
    } else {
      // if user does not exist, create user with image
      Restangular.all('users')
      .post({user: $scope.user})
      .then(function (response) { 
        console.log('Success!!!');
      }, function(error) {
        console.log('errors', JSON.stringify(errors));
      });
    }
   };
 }]
};
});

Angular Файл с директивой

<input type="file" id="fileUpload" ng-show="false" upload-image />
<img ng-src="{{userImageLink}}" ng-click="openFileWindow()" ng-class="{ hidden: !userImageLink}" >
<div class="drop-box" ng-click="openFileWindow()" ng-class=" {hidden: userImageLink}">
    Click to add an image.
</div>

Это создает скрытый ввод файла. userImageLink задается в контроллере, как и метод openFileWindow(). Если изображение пользователя существует, оно отображается, в противном случае он отображает пустой div, указывающий пользователю щелкнуть, чтобы загрузить изображение.

В контроллере, который отвечает за код html выше, у меня есть следующий метод:

// triggers click event for input file, causing the file selection window to open
$scope.openFileWindow = function () {
  angular.element( document.querySelector( '#fileUpload' ) ).trigger('click');
  console.log('triggering click');
};

Сторона рельсов

В контроллере модели пользователя у меня есть следующие методы:

# set user params 
before_action :user_params, only: [:show, :create, :update, :destroy]

def create
  # if there is an image, process image before save
  if params[:imageData]
    decode_image
  end

  @user = User.new(@up)

  if @user.save
    render json: @user
  else
    render json: @user.errors, status: :unprocessable_entity
    Rails.logger.info @user.errors
  end
end

def update
  # if there is an image, process image before save
  if params[:imageData]
    decode_image
  end

  if @user.update(@up)
    render json: @user
  else
    render json: @user.errors, status: :unprocessable_entity
  end
end

private 

  def user_params
    @up = params.permit(:userIcon, :whateverElseIsPermittedForYourModel)
  end

  def decode_image
    # decode base64 string
    Rails.logger.info 'decoding now'
    decoded_data = Base64.decode64(params[:imageData]) # json parameter set in directive scope
    # create 'file' understandable by Paperclip
    data = StringIO.new(decoded_data)
    data.class_eval do
      attr_accessor :content_type, :original_filename
    end

    # set file properties
    data.content_type = params[:imageContent] # json parameter set in directive scope
    data.original_filename = params[:imagePath] # json parameter set in directive scope

    # update hash, I had to set @up to persist the hash so I can pass it for saving
    # since set_params returns a new hash everytime it is called (and must be used to explicitly list which params are allowed otherwise it throws an exception)
    @up[:userIcon] = data # user Icon is the model attribute that i defined as an attachment using paperclip generator
  end

Файл user.rb будет иметь следующее:

### image validation functions
has_attached_file :userIcon, styles: {thumb: "100x100#"}
#validates :userIcon, :attachment_presence => true
validates_attachment :userIcon, :content_type => { :content_type => ["image/jpg", "image/gif", "image/png"] }
validates_attachment_file_name :userIcon, :matches => [/png\Z/, /jpe?g\Z/]

Я думаю, что это все, что имеет значение. Надеюсь это поможет. Я, вероятно, опубликую это где-то еще немного более четко, когда у меня будет время.

Ответ 2

Но я хочу, чтобы просто простая директива, которая собирала мой файл и вставляла моя ng-модель

ng-file-upload просто делает это, и это легкое, простое в использовании, кросс-браузерное решение, которое поддерживает прогресс/прерывание, drag &amp, кадр и предварительный просмотр.

<div ng-controller="MyCtrl">
   <input type="file" ngf-select ng-model="files" multiple>
</div>

$scope.$watch('files', function(files) {
  for (var i = 0; i < $files.length; i++) {
      var file = $files[i];
      $scope.upload = $upload.upload({
          url: 'server/upload/url', 
          file: file,
      }).progress(function(evt) {
         console.log('percent: ' + parseInt(100.0 * evt.loaded / evt.total));
      }).success(function(data, status, headers, config) {
         console.log(data);
      });
   }
});