Upload to Rails Shrine from NativeScript
Asked Answered
V

2

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 "[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.

Vole answered 14/8, 2019 at 3:14 Comment(15)
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?Babbittry
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?Vole
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 uploadFilegithub.com/NativeScript/…Babbittry
@janko-m Still get a 400 Bad request when using the multipartUpload. I added the code to the questionVole
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.Babbittry
@Vole If you are able to upload using Angular blob, you should be fine with nativescript-http-formdataTham
For android it uses android.graphics.BitmapTham
refer here github.com/dotnetdreamer/nativescript-http-formdataTham
@Tham 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.Vole
github.com/dotnetdreamer/nativescript-http-formdata/blob/…Tham
May be you need to modify header, I am heading off for now meanwhile you can try as per this github.com/dotnetdreamer/nativescript-http-formdata/issues/…Tham
@Vole , I just realized this plugin has dependency and you have add that in your gradle file : // mvnrepository.com/artifact/com.squareup.okhttp3/okhttp compile group: 'com.squareup.okhttp3', name: 'okhttp', version: '3.10.0'Tham
@janko-m Just tried Curl and got the same issueVole
@Vole Here is the proof that Shrine's upload endpoint works with curl: gist.github.com/janko/6b902b38eaea2d0bbb2a476a1781e8b9. You'll need to see what is the difference between my example and your app.Babbittry
@janko-m thanks. I did get curl working with my app and updated the question.Vole
V
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)

Vole answered 28/8, 2019 at 4:20 Comment(0)
T
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"
}
Tham answered 21/8, 2019 at 0:20 Comment(1)
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.Vole
V
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)

Vole answered 28/8, 2019 at 4:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.