6

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 "file=@t-bird.jpg" 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.

map7
  • 5,096
  • 6
  • 65
  • 128
  • 1
    Shrine's upload endpoint doesn't require any special headers, just a plain `multipart/form-data` file upload. The error message seems like it's coming from the `web-console` gem, though I'd be surprised if this was an actual exception and not just a warning. What is the response status of the `POST` request to Shrine's upload endpoint in the browser network tab? – Janko Aug 14 '19 at 05:50
  • At the moment all I have is "400 Bad Request" on the mobile phone. This is a NativeScript application so I don't have any form-data as the mobile doesn't have a web form. Could this be the problem? Can I send the data another way? – map7 Aug 14 '19 at 06:44
  • Ok, if "400 Bad Request" is coming from the upload endpoint, that probably means the file wasn't sent in the correct format. `multipart/form-data` is just a request body format that anyone can send, doesn't have to be a web form (e.g. `curl` can do it). The question is just whether the HTTP library you're using on the client side supports it, and it appears it does – using `multipartUpload` instead of `uploadFile` – https://github.com/NativeScript/nativescript-background-http#uploading-files – Janko Aug 14 '19 at 08:48
  • @janko-m Still get a 400 Bad request when using the multipartUpload. I added the code to the question – map7 Aug 15 '19 at 06:39
  • I would be great if you could find out exactly what HTTP request your NativeScript sent and what HTTP response the upload endpoint returns (at least reponse body). I’d love to help further, but at this point we need this information. – Janko Aug 15 '19 at 12:45
  • @map7 If you are able to upload using Angular blob, you should be fine with nativescript-http-formdata – Narendra Aug 20 '19 at 05:42
  • For android it uses android.graphics.Bitmap – Narendra Aug 20 '19 at 05:43
  • refer here https://github.com/dotnetdreamer/nativescript-http-formdata – Narendra Aug 20 '19 at 05:43
  • @Narendra I've just tried that and had problems please refer to my update. I might be missing something obvious as I've tried so much now. – map7 Aug 20 '19 at 06:40
  • https://github.com/dotnetdreamer/nativescript-http-formdata/blob/92cbc5a2b19e6e776adbf091e720baac555e3db0/src/TNSHttpFormData.android.ts#L29 – Narendra Aug 20 '19 at 06:56
  • May be you need to modify header, I am heading off for now meanwhile you can try as per this https://github.com/dotnetdreamer/nativescript-http-formdata/issues/3#issuecomment-420184134 – Narendra Aug 20 '19 at 06:57
  • @map7 , I just realized this plugin has dependency and you have add that in your gradle file : // https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp compile group: 'com.squareup.okhttp3', name: 'okhttp', version: '3.10.0' – Narendra Aug 21 '19 at 00:17
  • @janko-m Just tried Curl and got the same issue – map7 Aug 26 '19 at 06:39
  • 2
    @map7 Here is the proof that Shrine's upload endpoint works with curl: https://gist.github.com/janko/6b902b38eaea2d0bbb2a476a1781e8b9. You'll need to see what is the difference between my example and your app. – Janko Aug 27 '19 at 09:10
  • @janko-m thanks. I did get curl working with my app and updated the question. – map7 Aug 27 '19 at 23:42

2 Answers2

0

As you are successfully able to upload using blob in your angular project. You should use nativescript-http-formdata plugin. You can download this from npm tns plugin add nativescript-http-formdata or you can find the repo here.

P.S. This plugin has dependency on Okhttp, so you need to add the following in your app.gradle file

dependencies {
    compile "com.squareup.okhttp3:okhttp:3.10.0"
}
Narendra
  • 4,514
  • 2
  • 21
  • 38
  • I still get the same error. I've got the emulator working now and can but breakpoints in and debug. I found that nativescript-http-formdata has a 6.0 branch so I'm going to try that today. – map7 Aug 25 '19 at 23:28
0

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)

map7
  • 5,096
  • 6
  • 65
  • 128