Search code examples
ruby-on-railsangularmobilenativescriptshrine

Upload to Rails Shrine from NativeScript


I'm using Rails 5.2 with the Shrine gem for image upload. On the client side I'm using NativeScript 6.0 with Angular 8.0.

I've installed Shrine and it's working on the Rails side and direct upload via Uppy.

On the frontend (Android mobile) using NativeScript I can take a photo (using nativescript-camera) and send it using nativescript-background-http to the nativescript-background-http demo server (node based).

The problem I have is sending from NativeScript to Shrine.

On the backend I have these routes

Rails.application.routes.draw do
  resources :asset_items
  mount ImageUploader.upload_endpoint(:cache) => "/images/upload" # POST /images/upload
end

In my shrine setup I have

require "shrine"
require "shrine/storage/file_system"

Shrine.storages = {
  cache: Shrine::Storage::FileSystem.new("public", prefix: "uploads/cache"), 
  store: Shrine::Storage::FileSystem.new("public", prefix: "uploads"),           }

Shrine.plugin :logging, logger: Rails.logger
Shrine.plugin :upload_endpoint
Shrine.plugin :activerecord
Shrine.plugin :cached_attachment_data
Shrine.plugin :restore_cached_data

On the frontend

onTakePictureTap(args) {
    requestPermissions().then(
        () => {
            var imageModule = require("tns-core-modules/ui/image");

            takePicture({width: 150, height: 100, keepAspectRatio: true})
                .then((imageAsset: any) => {
                    this.cameraImage = imageAsset;
                    let image = new imageModule.Image();
                    image.src = imageAsset;
                    this._dataItem.picture_url = this.imageAssetURL(imageAsset);

                    // Send picture to backend
                    var file =  this._dataItem.picture_url;
                    var url = "https://192.168.1.4/images/upload";
                    var name = file.substr(file.lastIndexOf("/") + 1);

                    // upload configuration
                    var bghttp = require("nativescript-background-http");
                    var session = bghttp.session("image-upload");
                    var request = {
                        url: url,
                        method: "POST",
                        headers: {
                            "Content-Type": "application/octet-stream"
                        },
                        description: "Uploading " + name
                    };

                    var task = session.uploadFile(file, request);

                    task.on("error", this.errorHandler);
                    task.on("responded", this.respondedHandler);
                    task.on("complete", this.completeHandler);

                }, (error) => {
                    console.log("Error: " + error);
                });
        },
        () => alert('permissions rejected')
    );
} // onTakePictureTap

The handlers look like this

errorHandler(e) {
    alert("received " + e.responseCode + " code.");
    var serverResponse = e.response;
}

respondedHandler(e) {
    alert("received " + e.responseCode + " code. Server sent: " + e.data);
}

completeHandler(e) {
    alert("received")
}

When I take a picture it tries to send it to the backend and I get the following log on the server;

Started POST "/images/upload" for 103.232.216.30 at 2019-08-14 11:14:09 +1000
Cannot render console from 103.232.216.30! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255

I think the problem is in the headers I'm sending to Shrine, what is the correct way of sending the image from NativeScript to Shine?

Update: Try multipartUpload

I tried the multipartUpload and also got a 400 error code

                    // upload configuration
                    var bghttp = require("nativescript-background-http");
                    var session = bghttp.session("image-upload");
                    var request = {
                        url: url,
                        method: "POST",
                        headers: {
                            "Content-Type": "application/octet-stream"
                        },
                        description: "Uploading " + name
                    };

                    var params = [
                        {
                            name: "fileToUpload.jpg",
                            filename: file,
                            mimeType: "image/jpeg"
                        }
                    ];

                    var task = session.multipartUpload(params, request);

**Update: Try nativescript-background-http demo server

This works, I can successfully send a multipartUpload to the demo-server

Update: middleware to log request

Started to put together a middleware rack class to print out the response

class MyMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    status, headers, body = @app.call(env)
    puts "Middleware called. Status: #{status}, Headers: #{headers}"
    [status, headers, body]
  end
end

When I send a file, it sends the response

Middleware called. Status: 400, Headers: {"Content-Type"=>"text/plain", "Content-Length"=>"16"}

Having a look at headers,status and body in byebug when sending from NativeScript;

(byebug) headers
{"Content-Type"=>"text/plain", "Content-Length"=>"16", "Cache-Control"=>"no-cache", "X-Request-Id"=>"7a5d40e2-5c09-4fc7-88b5-83813cedf20e", "X-Runtime"=>"0.055892"}


(byebug) status
400


(byebug) body
#<Rack::BodyProxy:0x000056471192c580 @body=#<Rack::BodyProxy:0x000056471192c620 @body=#<Rack::BodyProxy:0x000056471192c878 @body=#<Rack::BodyProxy:0x000056471192c968 @body=#<Rack::BodyProxy:0x000056471192cdc8 @body=["Upload Not Found"], @block=#<Proc:0x000056471192cd28@/usr/local/rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/rack-2.0.7/lib/rack/tempfile_reaper.rb:16>, @closed=false>, @block=#<Proc:0x000056471192c918@/usr/local/rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/actionpack-5.2.3/lib/action_dispatch/middleware/executor.rb:15>, @closed=false>, @block=#<Proc:0x000056471192c850@/usr/local/rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/railties-5.2.3/lib/rails/rack/logger.rb:39>, @closed=false>, @block=#<Proc:0x000056471192c5d0@/usr/local/rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/activesupport-5.2.3/lib/active_support/cache/strategy/local_cache_middleware.rb:30>, @closed=false>, @block=#<Proc:0x000056471192c508@/usr/local/rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/actionpack-5.2.3/lib/action_dispatch/middleware/executor.rb:15>, @closed=false>

A successful send via Uppy on the Rails side shows this result

(byebug) headers
{"Content-Type"=>"application/json; charset=utf-8", "Content-Length"=>"149", "ETag"=>"W/\"29040a3f35783193f7ba450aac8906bd\"", "Cache-Control"=>"max-age=0, private, must-revalidate", "X-Request-Id"=>"53b380b8-e902-49d3-885f-634fc9ea82dc", "X-Runtime"=>"0.028117"}


(byebug) body
#<Rack::BodyProxy:0x00007f2c801f8868 @body=#<Rack::BodyProxy:0x00007f2c801f8980 @body=#<Rack::BodyProxy:0x00007f2c801f8de0 @body=#<Rack::BodyProxy:0x00007f2c801f90b0 @body=#<Rack::BodyProxy:0x00007f2c801f98f8 @body=["{\"id\":\"85bf685af3b7965c701227478e2189a2.jpg\",\"storage\":\"cache\",\"metadata\":{\"filename\":\"DSCF3107_edited.JPG\",\"size\":3998332,\"mime_type\":\"image/jpeg\"}}"], @block=#<Proc:0x00007f2c801f9858@/usr/local/rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/rack-2.0.7/lib/rack/etag.rb:30>, @closed=false>, @block=#<Proc:0x00007f2c801f8f98@/usr/local/rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/actionpack-5.2.3/lib/action_dispatch/middleware/executor.rb:15>, @closed=false>, @block=#<Proc:0x00007f2c801f8db8@/usr/local/rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/railties-5.2.3/lib/rails/rack/logger.rb:39>, @closed=false>, @block=#<Proc:0x00007f2c801f8890@/usr/local/rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/activesupport-5.2.3/lib/active_support/cache/strategy/local_cache_middleware.rb:30>, @closed=false>, @block=#<Proc:0x00007f2c801f8750@/usr/local/rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/actionpack-5.2.3/lib/action_dispatch/middleware/executor.rb:15>, @closed=false>

Info from Chrome Network on Successful upload via Uppy

      - General
        : Request URL: http://localhost:9000/images/upload
        : Request Method: POST
        : Status Code: 200 OK
        : Remote Address: [::1]:9000
        : Referrer Policy: strict-origin-when-cross-origin

      - Response                
        : Cache-Control: max-age=0, private, must-revalidate
        : Content-Length: 141
        : Content-Type: application/json; charset=utf-8
        : ETag: W/"8e3a470866888e1d724013e95d0a49b4"
        : X-Request-Id: 3e4222bd-e5bf-4270-bc31-1fc2c25696b1
        : X-Runtime: 0.010884

      - Request
        : Accept: */*
        : Accept-Encoding: gzip, deflate, br
        : Accept-Language: en-US,en;q=0.9
        : Cache-Control: no-cache
        : Connection: keep-alive
        : Content-Length: 110221
        : Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryBRJtv5UR0QTM2J2x
        : Cookie: _session_id=73b3a497c62bd745a789bc00b9f14361; org.cups.sid=c9eb7594a0515f4965b7a8e2f7900050; io=aArI7Q_64r2LWkc5AAAA; CSRF-Token-4MYJC=hLjA49c9bSsUhMUrYMfgSFSEnquQufo3; CSRF-Token-CAGDA=53tpJXxkvAstfeCoAKKbWgQDiQpU7xLj; CSRF-Token-TUFRR=kAWjSsQW4YCdEyGtaNKpfPT4gjToabYL; XSRF-TOKEN=HCjw%2B3WTJcSd1ddt45JGGGo8Uer43ggZZRrcsLc2NFgTdghJ852fqo0rWUx0%2FfBIOfv9YEMJ7mXw8TCix7d2cA%3D%3D; CSRF-Token-XDZDE=LyXXMXei6ci6FHrE3MfTxn3ARAKXYgMZ; _personal_property_rails_prototype_session=u65TkCvL9slUmGQQsP37lJH0BPcMw0E5%2FaDNw6frbuFw8NwqfM9gYPp%2F%2F830NFeZJqwxnYqc%2FCP%2FPIXhvPGFbD4waESKMKS1ChILCxTXZAPRFFULtu9m4Xl2G6AlF0ZamkzY7sdcE15vnpIBm8M%3D--98yhZGLNKsL5dnSX--Radl4qCShjACiTHc5UTH1A%3D%3D
        : Host: localhost:9000
        : Origin: http://localhost:9000
        : Pragma: no-cache
        : Referer: http://localhost:9000/asset_items/new
        : User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36

      - Form data
        : name: 2014-mlug.png
        : type: image/png
        : files[]: (binary)

Update: Can upload through Angular using a blob

I can upload from Angular 8.0 to Shrine using a blob.

sendImage(files: FileList){
    this.image = files.item(0);
    var directUrl = "http://localhost:9000/images/upload";

    // Create a formData object
    const formData: FormData = new FormData();
    formData.append('file', files.item(0), this.image.name);

    // Direct Upload
    this.http.post(directUrl, formData).subscribe(event => {
        console.log("Successfully uploaded: " + event);
        this.asset.image = JSON.stringify(event);
    });
}

Update: Try nativescript-http-formdata

Here is my implementation of sending the picture via nativescript-http-formdata;

async sendPicture(filepath, imageAsset) {
    var url = "http://localhost:9000/images/upload";
    var name = filepath.substr(filepath.lastIndexOf("/") + 1);

    // Get bitmap of file
    const imageAndroidBitmap = android.graphics.BitmapFactory.decodeFile(filepath);

    // Prepare the formdata
    let fd = new TNSHttpFormData();
    let param: TNSHttpFormDataParam = {
            data: imageAndroidBitmap,
            contentType: 'image/jpeg',
            fileName: 'test.jpg',
            parameterName: 'file1'
    };
    let params = [];
    params.push(param);
    try {
        const response: TNSHttpFormDataResponse = await fd.post(url, params, {
            headers: {}
        });
        console.log(response);
    } catch (e) {
        console.log(e);
    }

Error: After reviewing this error I think the okhttp3 is loaded but the syntax is wrong for NativeScript 6.0

LOG from device Galaxy S8: Error: java.lang.Exception: Failed resolving method create on class okhttp3.RequestBody

Update: Upload via Curl to Shrine = works

Tried the following and got the same Bad Request 400 error;

Send picture

  curl -X POST --form "[email protected]" http://localhost:9000/images/upload
   {"id":"4b4d42e77b4fa7ecddbd93cd07845cc2.jpg","storage":"cache","metadata":{"filename":"t-bird.jpg","size":1478512,"mime_type":"image/jpeg"}}
  *NOTE: when we send the picture we use 'file' instead of 'image'*

Send text form (optional)

  curl -X POST -d "asset_item[name]=curl" http://localhost:9000/asset_items.json

Convert output to JSON

  irb
  {id:"7276dc618cdd23bf3f5a9243d3c59399.jpg",storage:"cache",metadata:{filename:"t-bird.jpg",size:1478512,mime_type:"image/jpeg"}}.to_json

Result

"{\"id\":\"7276dc618cdd23bf3f5a9243d3c59399.jpg\",\"storage\":\"cache\",\"metadata\":{\"filename\":\"t-bird.jpg\",\"size\":1478512,\"mime_type\":\"image/jpeg\"}}"

POST text with the image data

  curl -X POST -d "asset_item[name]=curl" -d 'asset_item[image]="{\"id\":\"7276dc618cdd23bf3f5a9243d3c59399.jpg\",\"storage\":\"cache\",\"metadata\":{\"filename\":\"t-bird.jpg\",\"size\":1478512,\"mime_type\":\"image/jpeg\"}}"' http://localhost:9000/asset_items.json

Current throughts

I think my best chance at the moment is to use nativescript-background-http to send a multi-part post to shrine in the correct format. What ever that is.


Solution

  • I managed to send an image, the main problem I had all along was I had to set the 'name:' to "file" in my multipartUpload within the NativeScript-background-http plugin.

        var request = {
            url: url,
            method: "POST",
            headers: {
                "file-name": name,
                "Content-Type": "application/octet-stream"
            },
            description: "Uploading " + name
        };
    
        var params = [
            {
                name: "file",
                filename: filepath,
                mimeType: "image/jpeg"
            }
        ];
    
        var task = session.multipartUpload(params, request);
    
        task.on("responded", this.respondedHandler, this);
    

    Once I have got a successful response from Shrine I stringified the JSON and attached that result to my data model, which later gets sent as part of my form through JSON.

    respondedHandler(e) {
        alert("received " + e.responseCode + " code. Server sent: " + e.data);
        this._dataItem.image = JSON.stringify(e.data);
    }
    

    Thanks Narendra and Janko-m I didn't realise just how painful this turned out to be and as I got a greater understanding of my problem the question kept changing and I found out the real problem all along.

    NOTE: I've documented my findings on the Shrine WIKI

    From Curl to Shrine = https://github.com/shrinerb/shrine/wiki/Uploading-through-curl

    From Angular 8.0 to Shrine = https://github.com/shrinerb/shrine/wiki/Uploading-through-Angular

    From Nativescript to Shrine = https://github.com/shrinerb/shrine/wiki/Uploading-through-NativeScript-(Angular)