It looks like you are using the 'upload right away' pattern. Here is a complete example for future seekers:
app/views/static-pages/index.html:
<div ng-app='myApp'>
<h1>StaticPages#index</h1>
<p>Find me in: app/views/static_pages/index.html.erb</p>
<hr>
<div ng-controller="FileUploadCtrl">
<input type="file"
ng-file-select=""
ng-model='selectedFiles'
ng-file-change="myUpload(selectedFiles)"
ng-multiple="true">
</div>
</div>
app/assets/javascripts/main.js.coffee:
@app = angular.module 'myApp', ['angularFileUpload']
app/assets/javascripts/FileUploadCtrl.js.coffee:
@app.controller 'FileUploadCtrl', [
'$scope',
'$upload',
'$timeout',
($scope, $upload, $timeout) ->
$scope.myUpload = (files) ->
len = files.length
i = 0
fileReader = undefined
csrf_token = document.querySelector('meta[name="csrf-token"]').getAttribute('content')
for file in files
fileReader = new FileReader()
#-------
fileReader.onload = (e) ->
#Define function for timeout, e.g. $timeout(timeout_func, 5000)
timeout_func = ->
file.upload = $upload.http {
url: "/static_pages/upload",
method: 'POST',
headers: {
'Content-Type': file.type,
'X-CSRF-TOKEN': csrf_token
},
data: e.target.result #the file's contents as an ArrayBuffer
}
file.upload.then(
(success_resp) -> file.result = success_resp.data, #response from server (=upload.html)
(failure_resp) ->
if failure_resp.status > 0
$scope.errorMsg = "#{failure_resp.status}: #{response.data}"
)
file.upload.progress( (evt) ->
file.progress = Math.min 100, parseInt(100.0 * evt.loaded / evt.total)
)
#end of timeout_func
$timeout timeout_func, 5000
#end of FileReader.onload
fileReader.readAsArrayBuffer file
]
Note: In the code above, I had to add the csrf lines because in app/views/layouts/application.rb, I have this:
<%= csrf_meta_tags %>
which causes rails to add a csrf token to each web page. angular-file-upload was causing rails CSRF Errors, so I had to retrieve the csrf token and add it to the request headers.
app/assets/javascripts/application.js:
//I removed:
// require turbolinks
//for angular app
//
//= require jquery
//= require jquery_ujs
//
//The 'require_tree .' adds all the files in some random
//order, but for AngularJS the order is important:
//
//= require angular
//= require angular-file-upload-all
//
//And for some inexplicable reason, this is needed:
//= require main
//I would think 'require_tree .' would be enough for that file.
//
//= require_tree .
I didn't use gems for angular or angular-file-upload. I just copied the AngularJS code into a file named angular.js which I put inside app/assets/javascripts. Similarly, I copied the code in angular-file-upload-all into app/assets/javascripts/angular-file-upload-all.js
app/controllers/static_pages_controller.rb:
class StaticPagesController < ApplicationController
def index
end
def upload
puts "****PARAMS:"
p params
puts "****body of request: #{request.body.read.inspect}" #inspect => outputs "" for empty body rather than nothing
puts "****Content-Type: #{request.headers['Content-Type']}"
render nothing: true
end
end
config/routes.rb:
Test1::Application.routes.draw do
root "static_pages#index"
post "static_pages/upload"
As far as I can tell the data: key needs to be the contents of the file (as an ArrayBuffer). To get rails to insert additional data in the params hash, you could use the url, for example
url: "/media.json" + '?firstName=Kaspar
On the server side, the only way I could access the file was using request.body.read and the headers with request.headers['Content-Type']. What did you end up doing?
Also, I found two problems with file.type here:
headers: {
'Content-Type': file.type,
1) For some reason, neither FireFox nor Chrome can determine the file type of a .json file, so file.type ends up being a blank string: "". Rails then enters the file's contents as a key in the params hash. Huh?
If you tack .json onto the end of the url:
url: "/static_pages/upload.json",
...then Rails will parse the body of the request as JSON and enter the key/value pairs in the params hash. But adding .json to the url doesn't make the code very general because it prevents other file types from being processed correctly.
Here is a more general solution for uploading .json files:
for file in files
file_type = file.type
if file_type is '' #is => ===
[..., file_ext] = file.name.split '.'
if file_ext is 'json'
file_type = 'application/json'
...then later in the code:
headers: {
'Content-Type': file_type, #instead of file.type
2) However, there is still a closure problem in the original code, which needs to be corrected in order for multiple file selections to work correctly. If you select multiple files, then the file_type for all the files will end up being the file_type of the last file. For instance, if you select a .txt file and a .json file, then both files will have the type of the second file, i.e. application/json. That's problematic because rails will try to parse the body of the text file as JSON, which will produce the error ActionDispatch::ParamsParser::ParseError.
To correct the closure problem, one well known solution is to define a wrapper function around fileReader.onload(). Coffeescript has a syntax that makes adding a wrapper function especially pain free:
do (file_type) -> #wrapper function, which `do` immediately executes sending it the argument file_type
fileReader.onload = (e) ->
...
...
By adding one line, you can fix the shared variable problem. For details on what that does, go to the coffeescript home page and search the page for: do keyword.
app/assets/javascripts/FileUploadCtrl.js.coffee:
@app.controller 'FileUploadCtrl', [
'$scope',
'$upload',
'$timeout',
($scope, $upload, $timeout) ->
$scope.myUpload = (files) ->
len = files.length
i = 0
fileReader = undefined
csrf_token = document.querySelector('meta[name="csrf-token"]').getAttribute('content')
for file in files
#console.log file
file_type = file.type
#console.log file_type
if file_type is ''
[..., file_ext] = file.name.split '.'
#console.log file_ext
if file_ext is 'json'
file_type = 'application/json'
#console.log "Corrected file_type: " + file_type
fileReader = new FileReader()
#-------
do (file_type) ->
fileReader.onload = (e) ->
#Define function for timeout, e.g. $timeout(timeout_func, 5000)
timeout_func = ->
file.upload = $upload.http(
url: "/static_pages/upload"
method: 'POST'
headers:
'Content-Type': file_type
'X-CSRF-TOKEN': csrf_token
data: e.target.result, #file contents as ArrayBuffer
)
file.upload.then(
(success_resp) -> file.result = success_resp.data, #response from server
(failure_resp) ->
if failure_resp.status > 0
$scope.errorMsg = "#{failure_resp.status}: #{response.data}"
)
file.upload.progress (evt) ->
file.progress = Math.min 100, parseInt(100.0 * evt.loaded / evt.total)
#end of timeout_func
$timeout timeout_func, 5000
#end of FileReader.onload
fileReader.readAsArrayBuffer file
]
Finally, in this code,
data: e.target.result
...the entity returned by e.target.result is an ArrayBuffer, and I wasn't able to figure out how to modify that to add additional data.
data:key needs to be the contents of the file. To send additional params, you could use the url, for exampleurl: "/media.json" + '?firstName=KasparOn the server side, the only way I could access the file was usingrequest.body.readand the headers withrequest.headers['Content-Type']. What did you end up doing?