Skip to main content
Scholarly utilizes a novel, 3-step process for uploading files into a user’s “My Documents” section which requires a few requests in sequence to successfully upload a file. Scholarly requires this to ensure that file uploads can complete, regardless of connection speed. Scholarly has customers send files directly to S3 using a secure method. This method is as follows:
  1. API clients create a file object and get credentials for the upload. The response contains authentication information for the next step.
  2. API clients then upload the file directly to S3.
  3. API clients then confirm the upload with a final request.
By uploading the file directly to S3 in Step 2, this ensures the smooth operation of the Scholarly API servers. Here are some code snippets in different languages for handling this 3-step process. If you have trouble with this, please reach out to your Scholarly contact and we can pair program a solution in the language you’re using.
require "net/http"
require "json"
require "uri"
require "digest"
require "base64"

class ScholarlyFileUploader
  BASE_URL = "https://api.scholarlysoftware.com"

  def initialize(api_token)
    @api_token = api_token
  end

  # Step 1: Initiate the direct upload
  def initiate_upload(folder_id:, file_path:, content_type: "application/octet-stream")
    file_content = File.binread(file_path)
    file_name = File.basename(file_path)
    file_size = file_content.bytesize
    checksum = Base64.strict_encode64(Digest::MD5.digest(file_content))

    uri = URI("#{BASE_URL}/api/v1/folders/#{folder_id}/file_upload_requests")

    request_body = {
      data: {
        type: "file",
        attributes: {
          name: file_name,
          size: file_size,
          checksum: checksum,
          content_type: content_type
        }
      }
    }

    response = make_request(:post, uri, request_body)

    {
      file_id: response.dig("data", "id"),
      upload_url: response.dig("meta", "direct_upload", "url"),
      upload_headers: response.dig("meta", "direct_upload", "headers"),
      blob_signed_id: response.dig("meta", "direct_upload", "blob_signed_id"),
      file_content: file_content
    }
  end

  # Step 2: Upload file directly to S3
  def upload_to_s3(upload_url:, upload_headers:, file_content:)
    uri = URI(upload_url)

    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true

    request = Net::HTTP::Put.new(uri)
    upload_headers.each { |key, value| request[key] = value }
    request["Content-Length"] = file_content.bytesize.to_s
    request.body = file_content

    response = http.request(request)

    unless response.is_a?(Net::HTTPSuccess)
      raise "S3 upload failed: #{response.code} - #{response.body}"
    end

    true
  end

  # Step 3: Confirm the upload
  def confirm_upload(file_id:, blob_signed_id:)
    uri = URI("#{BASE_URL}/api/v1/file_upload_requests/#{file_id}/confirm")

    request_body = {blob_signed_id: blob_signed_id}

    make_request(:post, uri, request_body)
  end

  # Convenience method to upload a file in one call
  def upload_file(folder_id:, file_path:, content_type: "application/octet-stream")
    # Step 1: Initiate
    upload_info = initiate_upload(
      folder_id: folder_id,
      file_path: file_path,
      content_type: content_type
    )

    puts "Initiated upload for file ID: #{upload_info[:file_id]}"

    # Step 2: Upload to S3
    upload_to_s3(
      upload_url: upload_info[:upload_url],
      upload_headers: upload_info[:upload_headers],
      file_content: upload_info[:file_content]
    )

    puts "Uploaded to S3 successfully"

    # Step 3: Confirm
    result = confirm_upload(
      file_id: upload_info[:file_id],
      blob_signed_id: upload_info[:blob_signed_id]
    )

    puts "Upload confirmed. File state: #{result.dig("data", "attributes", "state")}"

    result
  end

  private

  def make_request(method, uri, body = nil)
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true

    request = case method
    when :post then Net::HTTP::Post.new(uri)
    when :get then Net::HTTP::Get.new(uri)
    when :put then Net::HTTP::Put.new(uri)
    end

    request["Authorization"] = "Bearer #{@api_token}"
    request["Content-Type"] = "application/vnd.api+json"
    request["Accept"] = "application/vnd.api+json"

    request.body = body.to_json if body

    response = http.request(request)

    unless response.is_a?(Net::HTTPSuccess)
      raise "API request failed: #{response.code} - #{response.body}"
    end

    JSON.parse(response.body)
  end
end

# Usage example:
if __FILE__ == $0
  api_token = ENV.fetch("SCHOLARLY_API_TOKEN")
  folder_id = "uuid-of-the-folder"
  file_path = "/path/to/your/file.md"

  uploader = ScholarlyFileUploader.new(api_token)

  begin
    result = uploader.upload_file(
      folder_id: folder_id,
      file_path: file_path,
      content_type: "text/plain"
    )

    puts "\nUpload complete!"
    puts "File ID: #{result.dig("data", "id")}"
    puts "File URL: #{result.dig("data", "attributes", "url")}"
    puts "Download link: #{result.dig("data", "links", "download")}"
  rescue => e
    puts "Error: #{e.message}"
    exit 1
  end
end