New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make ActiveStorage work for API only apps #32208

Open
hector opened this Issue Mar 8, 2018 · 19 comments

Comments

Projects
None yet
@hector

hector commented Mar 8, 2018

I have a Rails API serving a React SPA. It works perfect but I had to do a modification to use ActiveStorage's direct uploads.

The problem appears when trying to create a direct upload (i.e. a Blob). ActiveStorage::DirectUploadsController will fail with some errors which I believe are expected on a normal app but not on an API controllers. These are the errors:

  • HTTP Origin header (http://localhost:3001/) didn't match request.base_url (http://localhost:3000)
  • Can't verify CSRF token authenticity.

My solution has been to change this line:

class ActiveStorage::DirectUploadsController < ActionController::Base

and make:
class ActiveStorage::DirectUploadsController < ApplicationController

I think the problem is solved because my ApplicationController inherits from ActionController::API.

If my assumptions are correct, shouldn't ActiveStorage controllers inherit from ActionController::API or ActionController::Base depending on config.api_only = true?

pixeltrix added a commit that referenced this issue Mar 13, 2018

Make Active Storage work with API-only apps
Dynamically determine the base class for the Active Storage controllers
when they're loaded by the application - ActionController::Base for
ordinary applications and ActionController::API for API-only applications.

Fixes #32208.

@pixeltrix pixeltrix referenced a pull request that will close this issue Mar 13, 2018

Open

Make Active Storage work with API-only apps #32238

@kstratis

This comment has been minimized.

kstratis commented Mar 14, 2018

@hector Would you mind sharing how you used ActiveStorage outside of a Form field?

@hector

This comment has been minimized.

hector commented Mar 14, 2018

Sure, add the attribute data-direct-upload-url='/rails/active_storage/direct_uploads' to the file inputs that you want to act as direct uploads.

Remember you need to import the ActiveStorage javascript as suggested in the readme:
https://github.com/rails/rails/tree/master/activestorage#direct-upload-installation

For this importing step I had to do a variation since I am using React and ES6 imports, I needed more control over success/failure of the direct uploads and I am using React's controlled components. I extracted the code from ActiveStorage.start(). This is the general idea:

import React from 'react';
import {DirectUploadsController} from '../../lib/activestorage/direct_uploads_controller';

class NewProject extends React.Component {

  componentDidMount() {
    this.project = new Project(); // OptionaI: I store attributes in a Project model
    document.addEventListener('direct-upload:end', this.setDirectUploadAttr);
    // bind methods below here, I do it automatically with a decorator
  }

  setDirectUploadAttr(event) {
    const fileInput = event.target;
    const hiddenInput = document.querySelector(`input[type="hidden"][name="${fileInput.name}"`);
    // Here you have the value you have to assign to the has_one_attached attribute, 
    // do whatever you do assign it to. I have it in a project model
    this.project[fileInput.name] = hiddenInput.value;
  }

  // You may use promises instead of async/await
  async onFormSubmit(event) {
    event.preventDefault();

    try {
      // Upload attached files to external storage service
      const form = event.target;
      const controller = new DirectUploadsController(form);
      const {inputs} = controller;
      if (inputs.length) {
        await new Promise((resolve, reject) => {
          controller.start(error => error? reject(error) : resolve());
        });
      }

      // Run your submit function. In my case:
      await this.project.save();

    } catch (error) {
      // Do something with the error. For example:
      alert(error);
    }
  }

  render() {
    returns (
      <form onSubmit={this.onFormSubmit}>
        <input id="video" name="video" type="file" data-direct-upload-url="/rails/active_storage/direct_uploads" />
        <button type="submit">Submit</button>
      </form>
    );
  }

}

This would be the original Rails model:

class Project < ApplicationRecord
  has_one_attached :video
end
@cbothner

This comment has been minimized.

Contributor

cbothner commented Mar 14, 2018

I wrote this package to make it easier to use ActiveStorage in React components,
in case anyone finds it helpful. https://github.com/cbothner/react-activestorage-provider

@kstratis

This comment has been minimized.

kstratis commented Mar 15, 2018

Thank you @hector !

That's very useful!

@derigible

This comment has been minimized.

derigible commented Apr 20, 2018

A decent workaround for this issue that I have been using is in the config/initializers dir I have created a file called active_storage_api_modification.rb that has ActiveStorage::DirectUploadsController.instance_eval { skip_forgery_protection } to run.

@cdesch

This comment has been minimized.

cdesch commented Apr 21, 2018

@cbothner will the react-activestorage-provider work with ReactNative?

@cbothner

This comment has been minimized.

Contributor

cbothner commented Apr 21, 2018

@tommotaylor

This comment has been minimized.

tommotaylor commented Apr 23, 2018

Are there any recommendations on how to use ActiveStorage with a React Native app?

@Jarred-Sumner

This comment has been minimized.

Jarred-Sumner commented Apr 23, 2018

@tommotaylor I would like to use ActiveStorage with a React Native app as well.

Previously, I had a rather hacky setup combining react-native-fetch-blob and react-s3-uploader. This uploads to S3 directly via a presigned URL (you'd need to render the right response at /sign-s3):

/**
 * Taken, CommonJS-ified, and heavily modified from:
 * https://github.com/flyingsparx/NodeDirectUploader
 */
import RNFetchBlob from "react-native-fetch-blob";

var Blob = RNFetchBlob.polyfill.Blob;
const fs = RNFetchBlob.fs;

S3Upload.prototype.server = "";
S3Upload.prototype.signingUrl = "/sign-s3";
S3Upload.prototype.signingUrlMethod = "GET";
S3Upload.prototype.signingUrlSuccessResponses = [200, 201];
S3Upload.prototype.fileElement = null;
S3Upload.prototype.files = null;

S3Upload.prototype.onFinishS3Put = function(signResult, file) {
  return console.log("base.onFinishS3Put()", signResult.publicUrl);
};

S3Upload.prototype.preprocess = function(file, next) {
  console.log("base.preprocess()", file);
  return next(file);
};

S3Upload.prototype.onProgress = function(percent, status, file) {
  return console.log("base.onProgress()", percent, status);
};

S3Upload.prototype.onError = function(status, file) {
  return console.log("base.onError()", status);
};

S3Upload.prototype.scrubFilename = function(filename) {
  return filename.replace(/[^\w\d_\-\.]+/gi, "");
};

function S3Upload(options) {
  if (options == null) {
    options = {};
  }
  for (var option in options) {
    if (options.hasOwnProperty(option)) {
      this[option] = options[option];
    }
  }
  var files = this.fileElement ? this.fileElement.files : this.files || [];
  this.handleFileSelect(files);
}

S3Upload.prototype.handleFileSelect = function(files) {
  var result = [];
  for (var i = 0; i < files.length; i++) {
    var file = files[i];
    this.preprocess(
      file,
      function(processedFile) {
        this.onProgress(0, "Waiting", processedFile);
        result.push(this.uploadFile(processedFile));
        return result;
      }.bind(this)
    );
  }
};

S3Upload.prototype.createCORSRequest = function(method, url, opts) {
  var opts = opts || {};
  var xhr = new RNFetchBlob.polyfill.XMLHttpRequest();

  xhr.open(method, url, true);
  xhr.withCredentials = true;

  return xhr;
};

S3Upload.prototype.executeOnSignedUrl = function(file, callback) {
  var fileName = this.scrubFilename(file.name);
  var queryString =
    "?objectName=" + fileName + "&contentType=" + encodeURIComponent(file.type);
  if (this.s3path) {
    queryString += "&path=" + encodeURIComponent(this.s3path);
  }
  if (this.signingUrlQueryParams) {
    var signingUrlQueryParams =
      typeof this.signingUrlQueryParams === "function"
        ? this.signingUrlQueryParams()
        : this.signingUrlQueryParams;
    Object.keys(signingUrlQueryParams).forEach(function(key) {
      var val = signingUrlQueryParams[key];
      queryString += "&" + key + "=" + val;
    });
  }
  var xhr = this.createCORSRequest(
    this.signingUrlMethod,
    this.server + this.signingUrl + queryString,
    { withCredentials: this.signingUrlWithCredentials }
  );
  if (this.signingUrlHeaders) {
    var signingUrlHeaders =
      typeof this.signingUrlHeaders === "function"
        ? this.signingUrlHeaders()
        : this.signingUrlHeaders;
    Object.keys(signingUrlHeaders).forEach(function(key) {
      var val = signingUrlHeaders[key];
      xhr.setRequestHeader(key, val);
    });
  }
  xhr.onreadystatechange = function() {
    if (
      xhr.readyState === 4 &&
      this.signingUrlSuccessResponses.indexOf(xhr.status) >= 0
    ) {
      var result;
      try {
        result = JSON.parse(xhr.responseText);
      } catch (error) {
        this.onError("Invalid response from server", file);
        return false;
      }
      return callback(result);
    } else if (
      xhr.readyState === 4 &&
      this.signingUrlSuccessResponses.indexOf(xhr.status) < 0
    ) {
      return this.onError(
        "Could not contact request signing server. Status = " + xhr.status,
        file
      );
    }
  }.bind(this);
  return xhr.send();
};

S3Upload.prototype.uploadToS3 = function(file, signResult) {
  var xhr = this.createCORSRequest("PUT", signResult.signedUrl);
  if (!xhr) {
    this.onError("CORS not supported", file);
  } else {
    xhr.onload = function() {
      if (xhr.status === 200) {
        this.onProgress(100, "Upload completed", file);
        return this.onFinishS3Put(signResult, file);
      } else {
        return this.onError("Upload error: " + xhr.status, file);
      }
    }.bind(this);
    xhr.onerror = function(err) {
      return this.onError("XHR error", file);
    }.bind(this);
    xhr.upload.onprogress = function(e) {
      var percentLoaded;
      if (e.lengthComputable) {
        percentLoaded = Math.round(e.loaded / e.total * 100);
        return this.onProgress(
          percentLoaded,
          percentLoaded === 100 ? "Finalizing" : "Uploading",
          file
        );
      }
    }.bind(this);
  }
  xhr.setRequestHeader("Content-Type", file.type);
  if (this.contentDisposition) {
    var disposition = this.contentDisposition;
    if (disposition === "auto") {
      if (file.type.substr(0, 6) === "image/") {
        disposition = "inline";
      } else {
        disposition = "attachment";
      }
    }

    var fileName = this.scrubFilename(file.name);
    xhr.setRequestHeader(
      "Content-Disposition",
      disposition + '; filename="' + fileName + '"'
    );
  }
  if (signResult.headers) {
    var signResultHeaders = signResult.headers;
    Object.keys(signResultHeaders).forEach(function(key) {
      var val = signResultHeaders[key];
      xhr.setRequestHeader(key, val);
    });
  }
  if (this.uploadRequestHeaders) {
    var uploadRequestHeaders = this.uploadRequestHeaders;
    Object.keys(uploadRequestHeaders).forEach(function(key) {
      var val = uploadRequestHeaders[key];
      xhr.setRequestHeader(key, val);
    });
  } else {
    xhr.setRequestHeader("x-amz-acl", "public-read");
  }
  this.httprequest = xhr;
  return xhr.send(file);
};

S3Upload.prototype.uploadFile = function(file) {
  var uploadToS3Callback = this.uploadToS3.bind(this, file);

  if (this.getSignedUrl) return this.getSignedUrl(file, uploadToS3Callback);
  return this.executeOnSignedUrl(file, uploadToS3Callback);
};

S3Upload.prototype.abortUpload = function() {
  this.httprequest && this.httprequest.abort();
};

export default S3Upload;

This does not work at all with ActiveStorage...and using ActiveStorage would be much nicer.

@cbothner

This comment has been minimized.

Contributor

cbothner commented Apr 23, 2018

The npm package activestorage and anything that depends on it won’t work on React Native because FileReader.readAsArrayBuffer is not implemented. But that does not mean you can’t use ActiveStorage on the Rails side by uploading your files with the fetch API or as an HTML form would.

No Direct Upload

It is easiest if you expect only to receive small enough files or if you’re not using Heroku and you don’t need direct upload to S3. React Native supports the fetch API so you can use FormData to send the file as part of a standard POST request to your normal endpoint. After you get your image from one of the image picker components do

let data = new FormData()
data.append('user[image]', fileObject)
fetch(/* user_url */, {
  method: 'PUT',
  body: data
})

Direct Upload

It will be harder to do a direct upload, but these are the requests that the activestorage package performs.

Get the signed upload URL

fetch('/rails/active_storage/direct_uploads', {
  method: 'POST',
  body: {
    blob: {
      filename: "griffin.jpeg",
      content_type: "image/jpeg",
      byte_size: 1020753,
      checksum: /* base 64 of the MD5 hash of the file */
    }
})

(The activestorage package uses FileReader.readAsArrayBuffer to calculate the checksum incrementally. Maybe you could skip that optimization and avoid the incompatibility? https://github.com/rails/rails/blob/master/activestorage/app/javascript/activestorage/file_checksum.js)

That returns a JSON response with keys signed_id and direct_upload that you’ll need

Upload the file

PUT the file to response.direct_upload.url with the headers from response.direct_upload.headers. There’s no other body but the file as a blob.

fetch(response.direct_upload.url, {
  method: 'PUT',
  headers: response.direct_upload.headers,
  body: fileObject,
})

Update your Rails model with the signed_id of the blob you just made

fetch(/* user_url */, {
  method: 'PUT',
  body: {
    user: {
      image: response.signed_id
    }
  }
})

That should be it. Create a blob, which gives you a signed upload url. PUT the file to that signed URL. Then update the rails model to assign your signed url to the attachment relation.

@drale2k

This comment has been minimized.

drale2k commented Apr 30, 2018

I am having the same request and i am looking to send files from an iOS App to a Rails REST API which uses ActiveStorage. All the responses here seem to use a HTML form. I'd like some guidance on how to do it without a form but from any external service.

Would i need to replicate/mimic a form multipart? My setup uses local disk storage, not an external provider.

@cbothner

This comment has been minimized.

Contributor

cbothner commented Apr 30, 2018

@drale2k the Direct Upload section of my answer above should work for you using a URLRequest or some other abstraction in place of javascript’s fetch. You can still use the direct upload workflow with local disk storage—that’s what I’m using in development and it works the same.

You might even have an easier time with the MD5 in native iOS than others might have on React Native https://stackoverflow.com/questions/1684799/generate-hash-from-uiimage

@rodrigoHime

This comment has been minimized.

rodrigoHime commented May 18, 2018

@cbothner Thanks!! I could do activestorage direct_upload works with reactjs and redux-sagas with the help of your comments.

@krue-doug

This comment has been minimized.

krue-doug commented Jul 19, 2018

I have a promise-service with all the proper decorated headers for my requests including a bearer token, endpoint url, etc... I don't want to interpolate the strings into DirectUpload, does anyone know of a way to reuse the promise with DirectUpload?

@dagumak

This comment has been minimized.

dagumak commented Jul 22, 2018

Here is a write up for what I did to get this working. This is my contribution to Rails community for the rest of the year 😄.

My Project Setup

I want to provide some context first. I have my React app split away from Rails. In production, I will have my React app in S3 with Rails in Heroku. In summary, I am not using Rails to serve my single page application.

My rails server:

http://localhost:3001/

My react app:

http://localhost:3000/

High Level Overview

You will want to use DirectUpload provided by ActiveStorage to help you manage the upload with ActiveStorage.

Behind the scene, DirectUpload makes a POST to ActiveStorage and it will automatically follow up with a PUT request. In the response to your POST, it will give you the details of the file that was stored into ActiveStorage.

Here are examples of the request with their responses, but again, you don't have to worry about this since the library handles it for you:

Request URL: http://localhost:3001/rails/active_storage/direct_uploads
Request Method: POST
Status Code: 200 OK
Response:


{  
   "id":1,
   "key":"sxp2df7kkjEG8FWSzxonnYYp",
   "filename":"testname",
   "content_type":"image/png",
   "metadata":{  

   },
   "byte_size":6853,
   "checksum":"FAt9JP8ARkvkapFYB/IDLw==",
   "created_at":"2018-07-22T07:17:05.037Z",
   "signed_id":"eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBCZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--aa5d17cb4058350d7c3850c4bd644a59fe6e3676",
   "direct_upload":{  
      "url":"http://localhost:3001/rails/active_storage/disk/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDVG9JYTJWNVNTSWRjM2h3TW1SbU4ydHJha1ZIT0VaWFUzcDRiMjV1V1Zsd0Jqb0dSVlE2RVdOdmJuUmxiblJmZEhsd1pVa2lEbWx0WVdkbEwzQnVad1k3QmxRNkUyTnZiblJsYm5SZmJHVnVaM1JvYVFMRkdqb05ZMmhsWTJ0emRXMUpJaDFHUVhRNVNsQTRRVkpyZG10aGNFWlpRaTlKUkV4M1BUMEdPd1pVIiwiZXhwIjoiMjAxOC0wNy0yMlQwNzoyMjowNS4wODZaIiwicHVyIjoiYmxvYl90b2tlbiJ9fQ==--d6d1890294d1f650ba079208fca936cedd071e5f",
      "headers":{  
         "Content-Type":"image/png"
      }
   }
}



Request URL: http://localhost:3001/rails/active_storage/disk/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdDVG9JYTJWNVNTSWRjM2h3TW1SbU4ydHJha1ZIT0VaWFUzcDRiMjV1V1Zsd0Jqb0dSVlE2RVdOdmJuUmxiblJmZEhsd1pVa2lEbWx0WVdkbEwzQnVad1k3QmxRNkUyTnZiblJsYm5SZmJHVnVaM1JvYVFMRkdqb05ZMmhsWTJ0emRXMUpJaDFHUVhRNVNsQTRRVkpyZG10aGNFWlpRaTlKUkV4M1BUMEdPd1pVIiwiZXhwIjoiMjAxOC0wNy0yMlQwNzoyMjowNS4wODZaIiwicHVyIjoiYmxvYl90b2tlbiJ9fQ==--d6d1890294d1f650ba079208fca936cedd071e5f
Request Method: PUT
Status Code: 204 No Content

After getting this far, then you just take the signed_id from the POST response and use it for your custom endpoint. You can find an example implementation below.

The Implementation

Prepping Your Controllers

@derigible had the right idea to skip forgery, but you guys need to understand the consequences of skipping this and removing the authenticity token. These things help prevent random requests from replaying and storing random stuff into your ActiveRecord. Now that said, I have all my endpoints protected with an access token granted through OAuth. More specifically, I am using Doorkeeper and I will be decorating the controller to include it.

I didn't like @derigible method, because it was in an initializer. It doesn't follow convention well, and the next developer won't be able to predictably find where they would expect it.

Here's what I did instead:

routes.rb

...
post '/rails/active_storage/direct_uploads' => 'direct_uploads#create'
...
direct_uploads_controller.rb

class DirectUploadsController < ActiveStorage::DirectUploadsController
  protect_from_forgery with: :exception
  skip_before_action :verify_authenticity_token
  before_action :doorkeeper_authorize! # this is doorkeeper specific, but you can use any token authentication scheme here
end

Now after prepping my controllers, if I don't send an access/bearer token then this endpoint will respond with a 401.

Frontend

Now in your frontend, we have a few things to do. One, we need to decorate the requests made by DirectUpload with our access token since we protected the endpoint using Doorkeeper.

    const { accessToken } = this.state;    
    const railsActiveStorageDirectUploadsUrl = "http://localhost:3001/rails/active_storage/direct_uploads";
    const upload = new DirectUpload(file, railsActiveStorageDirectUploadsUrl, {
      directUploadWillCreateBlobWithXHR: (xhr) => {
        // This will decorate the requests with the access token header so you won't get a 401
        xhr.setRequestHeader("Authorization", `Bearer ${accessToken}`)
      }
    })

    upload.create((error, blob) => {
      if (error) {
        // Handle the error
        console.log(error)
      } else {
        console.log(blob)
        /* 
            By the time you reach here, DirectUpload has made the 2 requests that I mentioned above.
            When you're here, just use your services to talk to rails normally.

            When you reach this block of code, then you can expect a Blob to exist in your Rails backend now. You can run this in `rails c` to check:

                ActiveStorage::Blob.last        
        */
        
        userAvatar(blob.signed_id) // This makes a request to my custom endpoint (look below): "http://localhost:3001/users/profile/avatar.json"
      }
    })

Backend

routes.rb
...
post 'users/profile/avatar', to: 'users_profile#avatar'
....
class UsersProfileController < ApiController
  def avatar
    current_user.avatar.attach(avatar_params) # this is just the signed_id
    head :ok
  end

  private
  def avatar_params
    params.require(:avatar)
  end
end

If you made any changes to your image using HTML5 canvas like I did, then you'll need to add a name key to the blob before you pass it into DirectUpload.

Yielding the following:

    const { accessToken } = this.state;    
    const railsActiveStorageDirectUploadsUrl = "http://localhost:3001/rails/active_storage/direct_uploads";
    file.name = 'testname';
    const upload = new DirectUpload(file, railsActiveStorageDirectUploadsUrl, {
      directUploadWillCreateBlobWithXHR: (xhr) => {
        // This will decorate the requests with the access token header so you won't get a 401
        xhr.setRequestHeader("Authorization", `Bearer ${accessToken}`)
      }
    })

    upload.create((error, blob) => {
      if (error) {
        // Handle the error
        console.log(error)
      } else {
        console.log(blob)
        /* 
            By the time you reach here, DirectUpload has made the 2 requests that I mentioned above.
            When you're here, just use your services to talk to rails normally.

            When you reach this block of code, then you can expect a Blob to exist in your Rails backend now. You can run this in `rails c` to check:

                ActiveStorage::Blob.last        
        */
        
        userAvatar(blob.signed_id) // This makes a request to my custom endpoint (look below): "http://localhost:3001/users/profile/avatar.json"
      }
    })

Good luck guys! Let me know if I can help with anything.

If this helped, could you drop a 🍆 ?

And thanks @cbothner for your write up.

@el-rotny

This comment has been minimized.

el-rotny commented Aug 21, 2018

This is something we should definitely consider adding to the documentation for ActiveStorage. I took the above and cleaned up a little. Im using redux token auth and its a viable part of the conversation since using the JS package how its described in the docs wont work for token authenticated requests.

Thanks @dagumak !

A little bit on how it differs.

  1. I promisified the create method so it can be used in async/await
  2. Im grabbing my tokens from storage
  3. I had more headers I needed to add other than Authorization.
  4. I wanted a way to simply get my image blob so I could then dispatch the user update method through thunk
import { DirectUpload } from 'activestorage/src/direct_upload';
import getHeadersFromStorage from './apiHeaders';
import { HOST, authHeaderKeys } from '../constants';

const RASURL = `${HOST}/rails/active_storage/direct_uploads`;

/**
 * Promisify the create method provided by DirectUpload.
 * @param  {object} upload DirectUpload instance
 * @return {promise}       returns a promise to be used on async/await
 */
function createUpload(upload) {
  return new Promise((resolve, reject) => {
    upload.create((err, blob) => {
      if (err) reject(err);
      else resolve(blob);
    });
  });
}

/**
 * Upload to service using ActiveStorage DirectUpload module
 * @param  {Object} file Image buffer to be uploaded.
 * @return {Object}      blob object from server.
 * @see https://github.com/rails/rails/issues/32208
 */
async function activeStorageUpload(file) {
  let imageBlob;
  const headers = await getHeadersFromStorage();
  const upload = new DirectUpload(file, RASURL, {
    directUploadWillCreateBlobWithXHR: xhr => {
      authHeaderKeys.forEach(key => {
        xhr.setRequestHeader(key, headers[key]);
      });
    }
  });

  try {
    imageBlob = await createUpload(upload);
  } catch (err) {
    throw err;
  }
  return imageBlob;
}

export default activeStorageUpload;

These are my authHeaderKeys

const authHeaderKeys = [
  'access-token',
  'token-type',
  'client',
  'uid',
  'expiry'
];

Usage:

const imageBlob = await activeStorageUpload(imageData);

Edit

In terms of updating just the image record like in current_user.avatar.attach(avatar_params) # this is just the signed_id I recommend instead, to create method where you update your record and just send the params you want to update like updateUser({ image: image.signed_id });

For me, that hits my controller in the following way

  def update_profile
    if current_api_v1_user.update_attributes(user_params)
      user_serilized = UserSerializer.new(
        current_api_v1_user
      )
      render json: {
        data:         user_serilized,
        is_success:   true,
        status:       'success',
      }
    else
      render json: { error: "Failed to Update", is_success: false }, status: 422
    end
  end

With the above, you bypass the id needed in the default rails update route and capitalizes on using the current_user helper (in my case current_api_v1_user due to namespace from devise)

You could in fact do the same without creating a method or route using the default rails update method api/v1/users#update and pointing your update to /api/v1/users/:id

@robertsonsamuel

This comment has been minimized.

robertsonsamuel commented Oct 10, 2018

Want to also update folks who might have the same issue as I do to make this function correctly.

Referencing @dagumak 's post I needed to include a create method for this. Looks like that part was left out. My controller looks like:

class DirectUploadsController < ActiveStorage::DirectUploadsController
   # I'm using JWTSession for auth and have to include it
   # as this doesn't inherit due to `ActiveStorage::BaseController < ActionController::Base`
   # where we are using `ApplicationController < ActionController::API` in API Mode.

  include JWTSessions::RailsAuthorization
  rescue_from JWTSessions::Errors::Unauthorized, with: :not_authorized 

  protect_from_forgery with: :exception
  skip_before_action :verify_authenticity_token
  
  # Calling JWTSession to authorize
  before_action :authorize_access_request!

  def create
    blob = ActiveStorage::Blob.create_before_direct_upload!(blob_args)
    render json: direct_upload_json(blob)
  end

  private

  def blob_params
    params.require(:blob).permit(
    :filename,
    :content-type,
    # etc...
    )
  end

  # Rescue the Auth Error
  def not_authorized
    render json: { error: 'Not authorized' }, status: :unauthorized
  end
end

The create method comes right from the source code

That comes back with all the JSON DirectUpload expect when using direct uploads.

My route file then is just

Rails.application.routes.draw do
  post '/rails/active_storage/direct_uploads' => 'direct_uploads#create'
end

My JS also looks like (Using React)

const url = `${API_URL}/rails/active_storage/direct_uploads`;
const upload = new DirectUpload(file, url, {
      directUploadWillCreateBlobWithXHR: xhr => {
      // Put my JWT token in the auth header here
        xhr.setRequestHeader('Authorization', `Bearer ${this.state.accessToken}`);
       // Send progress upload updates
        xhr.upload.addEventListener('progress', event => this.directUploadProgress(event));
      }
    });

That should help others get started on 5.2 at least. Hope we can get something to work out here for API uploads to make this a little easier.

@dagumak

This comment has been minimized.

dagumak commented Oct 10, 2018

@robertsonsamuel You do not need to add the create method, because your DirectUploadsController is inheriting directly from ActiveStorage::DirectUploadsController. Rails will just use the create on ActiveStorage::DirectUploadsController; if you must declare a create method then I would suggest just doing:

def create 
   super
end

Edit: Actually @robertsonsamuel I noticed you are using include in your controller. This is most likely the issue.

@robertsonsamuel

This comment has been minimized.

robertsonsamuel commented Oct 10, 2018

@dagumak that was entirely correct. I was able to remove that create method as well. Cleaned it up quite nicely. Thanks for pointing that out. 🎉Includes didn't effect this however, thanks for the insight!
Still hopefully this helps others, even in SEO haha.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment