Skip to content

Commit 4f94b29

Browse files
committed
Create a Rails Engine version of the uppy-s3_multipart gem's Roda app
1 parent a8eb4df commit 4f94b29

File tree

5 files changed

+207
-4
lines changed

5 files changed

+207
-4
lines changed

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ source "https://rubygems.org"
1010
gemspec
1111

1212
gem "rubocop"
13+
gem "uppy-s3_multipart"
1314

1415
group :test do
1516
gem "byebug"

Gemfile.lock

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,28 @@ GEM
7979
minitest (>= 5.1)
8080
tzinfo (~> 2.0)
8181
ast (2.4.2)
82+
aws-eventstream (1.2.0)
83+
aws-partitions (1.781.0)
84+
aws-sdk-core (3.175.0)
85+
aws-eventstream (~> 1, >= 1.0.2)
86+
aws-partitions (~> 1, >= 1.651.0)
87+
aws-sigv4 (~> 1.5)
88+
jmespath (~> 1, >= 1.6.1)
89+
aws-sdk-kms (1.67.0)
90+
aws-sdk-core (~> 3, >= 3.174.0)
91+
aws-sigv4 (~> 1.1)
92+
aws-sdk-s3 (1.126.0)
93+
aws-sdk-core (~> 3, >= 3.174.0)
94+
aws-sdk-kms (~> 1)
95+
aws-sigv4 (~> 1.4)
96+
aws-sigv4 (1.5.2)
97+
aws-eventstream (~> 1, >= 1.0.2)
8298
builder (3.2.4)
8399
byebug (11.1.3)
84100
case_transform (0.2)
85101
activesupport
86102
concurrent-ruby (1.1.10)
103+
content_disposition (1.0.0)
87104
crass (1.0.6)
88105
diff-lcs (1.5.0)
89106
digest (3.1.0)
@@ -97,6 +114,7 @@ GEM
97114
activesupport (>= 5.0)
98115
i18n (1.10.0)
99116
concurrent-ruby (~> 1.0)
117+
jmespath (1.6.2)
100118
json (2.6.2)
101119
jsonapi-renderer (0.2.2)
102120
loofah (2.18.0)
@@ -164,6 +182,8 @@ GEM
164182
rake (13.0.6)
165183
regexp_parser (2.5.0)
166184
rexml (3.2.5)
185+
roda (3.69.0)
186+
rack
167187
rspec-core (3.11.0)
168188
rspec-support (~> 3.11.0)
169189
rspec-expectations (3.11.0)
@@ -201,6 +221,10 @@ GEM
201221
tzinfo (2.0.4)
202222
concurrent-ruby (~> 1.0)
203223
unicode-display_width (2.2.0)
224+
uppy-s3_multipart (1.2.1)
225+
aws-sdk-s3 (~> 1.0)
226+
content_disposition (~> 1.0)
227+
roda (>= 2.27, < 4)
204228
websocket-driver (0.7.5)
205229
websocket-extensions (>= 0.1.0)
206230
websocket-extensions (0.1.5)
@@ -216,6 +240,7 @@ DEPENDENCIES
216240
rubocop
217241
scalient-rails!
218242
sqlite3
243+
uppy-s3_multipart
219244

220245
BUNDLED WITH
221-
2.2.33
246+
2.4.13
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
# frozen_string_literal: true
2+
3+
# Adapted from `https://github.com/janko/uppy-s3_multipart/blob/master/lib/uppy/s3_multipart/app.rb`.
4+
5+
require "uppy/s3_multipart"
6+
7+
module Scalient
8+
module Rails
9+
class UppyS3MultipartsController < ::Rails.application.config.uppy_s3_multipart.base_controller.safe_constantize
10+
# `POST /`
11+
def create
12+
type = params["type"]
13+
filename = params["filename"]
14+
15+
key = SecureRandom.hex + File.extname(filename.to_s)
16+
key = [*prefix, key].join("/")
17+
18+
options = {}
19+
20+
if type
21+
options[:content_type] = type
22+
end
23+
24+
if filename
25+
options[:content_disposition] = ContentDisposition.inline(filename)
26+
end
27+
28+
if is_public
29+
options[:acl] = "public-read"
30+
end
31+
32+
result = client_call(:create_multipart_upload, key: key, **options)
33+
34+
render json: {uploadId: result.fetch(:upload_id), key: result.fetch(:key)}
35+
end
36+
37+
# `GET /:upload_id`
38+
def show
39+
upload_id = params.require(:upload_id)
40+
key = params.require(:key)
41+
42+
result = client_call(:list_parts, upload_id: upload_id, key: key)
43+
44+
render json: result.map do |part|
45+
{PartNumber: part.fetch(:part_number), Size: part.fetch(:size), ETag: part.fetch(:etag)}
46+
end
47+
end
48+
49+
# `GET /:upload_id/batch`
50+
def batch
51+
upload_id = params.require(:upload_id)
52+
key = params.require(:key)
53+
part_numbers = params.require(:partNumbers).split(",")
54+
55+
batch = part_numbers.to_h do |part_number|
56+
result = client_call(:prepare_upload_part, upload_id: upload_id, key: key, part_number: part_number)
57+
[part_number, result.fetch(:url)]
58+
end
59+
60+
render json: {presignedUrls: batch}
61+
end
62+
63+
# `GET /:upload_id/:part_number`
64+
def part_number
65+
upload_id = params.require(:upload_id)
66+
key = params.require(:key)
67+
part_number = params.require(:part_number)
68+
69+
result = client_call(:prepare_upload_part, upload_id: upload_id, key: key, part_number: part_number)
70+
71+
render json: {url: result.fetch(:url)}
72+
end
73+
74+
# `POST /:upload_id/complete`
75+
def complete
76+
upload_id = params.require(:upload_id)
77+
key = params.require(:key)
78+
parts = params.require(:parts)
79+
80+
parts = parts.map do |part|
81+
begin
82+
{part_number: part.fetch("PartNumber"), etag: part.fetch("ETag")}
83+
rescue KeyError
84+
render json: error_json("At least one part is missing \"PartNumber\" or \"ETag\" field"), status: 400
85+
86+
return
87+
end
88+
end
89+
90+
client_call(:complete_multipart_upload, upload_id: upload_id, key: key, parts: parts)
91+
92+
object_url = client_call(:object_url, key: key, public: is_public)
93+
94+
render json: {location: object_url}
95+
end
96+
97+
# `DELETE /:upload_id`
98+
def destroy
99+
upload_id = params.require(:upload_id)
100+
key = params.require(:key)
101+
102+
begin
103+
client_call(:abort_multipart_upload, upload_id: upload_id, key: key)
104+
rescue Aws::S3::Errors::NoSuchUpload
105+
render json: error_json("Upload doesn't exist for \"key\" parameter"), status: 404
106+
107+
return
108+
end
109+
110+
render :head
111+
end
112+
113+
private
114+
115+
def is_public
116+
::Rails.application.config.uppy_s3_multipart.public
117+
end
118+
119+
def prefix
120+
@prefix ||= ::Rails.application.config.uppy_s3_multipart.prefix
121+
end
122+
123+
def client
124+
@client ||= Uppy::S3Multipart::Client.new(bucket: ::Rails.application.config.uppy_s3_multipart.bucket)
125+
end
126+
127+
def app_options
128+
@options ||= ::Rails.application.config.uppy_s3_multipart.options
129+
end
130+
131+
def client_call(operation, **options)
132+
overrides = app_options[operation] || {}
133+
overrides = overrides.call(request) if overrides.respond_to?(:call)
134+
135+
options = options.merge(overrides)
136+
137+
client.send(operation, **options)
138+
end
139+
140+
def error_json(message)
141+
{message: message}
142+
end
143+
end
144+
end
145+
end

config/routes.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# frozen_string_literal: true
2+
3+
# Copyright 2023 Scalient LLC
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
6+
# use this file except in compliance with the License. You may obtain a copy of
7+
# the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14+
# License for the specific language governing permissions and limitations under
15+
# the License.
16+
17+
Scalient::Rails::Engine.routes.draw do
18+
id_any_pattern = Regexp.new("[^\\/]+")
19+
20+
constraints(upload_id: id_any_pattern) do
21+
# Draw the Uppy Companion routes (see `https://uppy.io/docs/companion/`).
22+
post "/s3/multipart", to: "uppy_s3_multiparts#create"
23+
get "/s3/multipart/:upload_id", to: "uppy_s3_multiparts#show"
24+
get "/s3/multipart/:upload_id/batch", to: "uppy_s3_multiparts#show", as: "batch"
25+
get "/s3/multipart/:upload_id/:part_number", to: "uppy_s3_multiparts#part_number", as: "part_number"
26+
post "/s3/multipart/:upload_id/complete", to: "uppy_s3_multiparts#complete", as: "complete"
27+
delete "/s3/multipart/:upload_id", to: "uppy_s3_multiparts#destroy"
28+
end
29+
end

lib/scalient/rails/engine.rb

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# frozen_string_literal: true
22

3-
#
4-
# Copyright 2015 Scalient LLC
3+
# Copyright 2015-2023 Scalient LLC
54
# All rights reserved.
65

76
require "scalient/rails/helpers/application_helper"
@@ -11,7 +10,11 @@ module Rails
1110
class Engine < ::Rails::Engine
1211
isolate_namespace ::Scalient::Rails
1312

14-
config.autoload_paths.push(File.expand_path("../../../../app/policies", __FILE__))
13+
config.uppy_s3_multipart = ActiveSupport::OrderedOptions.new
14+
config.uppy_s3_multipart.base_controller = "ActionController::Base"
15+
config.uppy_s3_multipart.prefix = nil
16+
config.uppy_s3_multipart.public = false
17+
config.uppy_s3_multipart.options = {}
1518
end
1619

1720
::Rails::Application.include ApplicationHelper

0 commit comments

Comments
 (0)