tessa 0.9.2 → 1.0.0.pre.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (90) hide show
  1. checksums.yaml +5 -5
  2. data/.babelrc +8 -0
  3. data/.circleci/config.yml +42 -0
  4. data/.eslintrc +19 -0
  5. data/.gitignore +5 -0
  6. data/.ruby-version +1 -1
  7. data/Rakefile +8 -0
  8. data/app/assets/javascripts/.keep +0 -0
  9. data/app/assets/javascripts/tessa.esm.js +672 -0
  10. data/app/assets/javascripts/tessa.js +662 -0
  11. data/app/javascript/activestorage/file_checksum.js +53 -0
  12. data/app/javascript/tessa/index.js.coffee +183 -0
  13. data/config/routes.rb +3 -0
  14. data/lib/tessa/active_storage/asset_wrapper.rb +24 -0
  15. data/lib/tessa/engine.rb +5 -0
  16. data/lib/tessa/model/dynamic_extensions.rb +151 -0
  17. data/lib/tessa/model.rb +27 -36
  18. data/lib/tessa/rack_upload_proxy.rb +18 -10
  19. data/lib/tessa/version.rb +1 -1
  20. data/lib/tessa/view_helpers.rb +4 -2
  21. data/lib/tessa.rb +30 -0
  22. data/package.json +43 -0
  23. data/rollup.config.js +44 -0
  24. data/spec/dummy/.gitignore +27 -0
  25. data/spec/dummy/README.md +24 -0
  26. data/spec/dummy/Rakefile +6 -0
  27. data/spec/dummy/app/assets/config/manifest.js +2 -0
  28. data/spec/dummy/app/assets/images/.keep +0 -0
  29. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  30. data/spec/dummy/app/controllers/application_controller.rb +2 -0
  31. data/spec/dummy/app/controllers/concerns/.keep +0 -0
  32. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  33. data/spec/dummy/app/jobs/application_job.rb +2 -0
  34. data/spec/dummy/app/models/concerns/.keep +0 -0
  35. data/spec/dummy/app/models/multiple_asset_model.rb +8 -0
  36. data/spec/dummy/app/models/single_asset_model.rb +5 -0
  37. data/spec/dummy/app/models/single_asset_model_form.rb +25 -0
  38. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  39. data/spec/dummy/bin/bundle +3 -0
  40. data/spec/dummy/bin/rails +4 -0
  41. data/spec/dummy/bin/rake +4 -0
  42. data/spec/dummy/bin/setup +28 -0
  43. data/spec/dummy/bin/update +28 -0
  44. data/spec/dummy/bin/yarn +11 -0
  45. data/spec/dummy/config/application.rb +33 -0
  46. data/spec/dummy/config/boot.rb +3 -0
  47. data/spec/dummy/config/credentials.yml.enc +1 -0
  48. data/spec/dummy/config/database.yml +25 -0
  49. data/spec/dummy/config/environment.rb +5 -0
  50. data/spec/dummy/config/environments/development.rb +47 -0
  51. data/spec/dummy/config/environments/production.rb +76 -0
  52. data/spec/dummy/config/environments/test.rb +43 -0
  53. data/spec/dummy/config/initializers/application_controller_renderer.rb +8 -0
  54. data/spec/dummy/config/initializers/assets.rb +14 -0
  55. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  56. data/spec/dummy/config/initializers/content_security_policy.rb +25 -0
  57. data/spec/dummy/config/initializers/cookies_serializer.rb +5 -0
  58. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  59. data/spec/dummy/config/initializers/inflections.rb +16 -0
  60. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  61. data/spec/dummy/config/initializers/wrap_parameters.rb +9 -0
  62. data/spec/dummy/config/locales/en.yml +33 -0
  63. data/spec/dummy/config/puma.rb +37 -0
  64. data/spec/dummy/config/routes.rb +3 -0
  65. data/spec/dummy/config/storage.yml +3 -0
  66. data/spec/dummy/config.ru +5 -0
  67. data/spec/dummy/db/migrate/20220606221557_create_active_storage_tables.active_storage.rb +27 -0
  68. data/spec/dummy/db/migrate/20220606221900_create_single_asset_models.rb +10 -0
  69. data/spec/dummy/db/migrate/20220607191519_create_multiple_asset_models.rb +10 -0
  70. data/spec/dummy/db/schema.rb +50 -0
  71. data/spec/dummy/lib/assets/.keep +0 -0
  72. data/spec/dummy/lib/tasks/.keep +0 -0
  73. data/spec/dummy/log/.keep +0 -0
  74. data/spec/dummy/package.json +5 -0
  75. data/spec/dummy/public/404.html +67 -0
  76. data/spec/dummy/public/422.html +67 -0
  77. data/spec/dummy/public/500.html +66 -0
  78. data/spec/dummy/public/apple-touch-icon-precomposed.png +0 -0
  79. data/spec/dummy/public/apple-touch-icon.png +0 -0
  80. data/spec/dummy/public/favicon.ico +0 -0
  81. data/spec/dummy/public/robots.txt +1 -0
  82. data/spec/dummy/tmp/.keep +0 -0
  83. data/spec/dummy/vendor/.keep +0 -0
  84. data/spec/rails_helper.rb +14 -0
  85. data/spec/tessa/model_spec.rb +195 -262
  86. data/spec/tessa/rack_upload_proxy_spec.rb +23 -81
  87. data/tessa.gemspec +7 -2
  88. data/yarn.lock +1661 -0
  89. metadata +179 -16
  90. data/circle.yml +0 -3
@@ -0,0 +1,53 @@
1
+ import SparkMD5 from "spark-md5"
2
+
3
+ const fileSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice
4
+
5
+ export class FileChecksum {
6
+ static create(file, callback) {
7
+ const instance = new FileChecksum(file)
8
+ instance.create(callback)
9
+ }
10
+
11
+ constructor(file) {
12
+ this.file = file
13
+ this.chunkSize = 2097152 // 2MB
14
+ this.chunkCount = Math.ceil(this.file.size / this.chunkSize)
15
+ this.chunkIndex = 0
16
+ }
17
+
18
+ create(callback) {
19
+ this.callback = callback
20
+ this.md5Buffer = new SparkMD5.ArrayBuffer
21
+ this.fileReader = new FileReader
22
+ this.fileReader.addEventListener("load", event => this.fileReaderDidLoad(event))
23
+ this.fileReader.addEventListener("error", event => this.fileReaderDidError(event))
24
+ this.readNextChunk()
25
+ }
26
+
27
+ fileReaderDidLoad(event) {
28
+ this.md5Buffer.append(event.target.result)
29
+
30
+ if (!this.readNextChunk()) {
31
+ const binaryDigest = this.md5Buffer.end(true)
32
+ const base64digest = btoa(binaryDigest)
33
+ this.callback(null, base64digest)
34
+ }
35
+ }
36
+
37
+ fileReaderDidError(event) {
38
+ this.callback(`Error reading ${this.file.name}`)
39
+ }
40
+
41
+ readNextChunk() {
42
+ if (this.chunkIndex < this.chunkCount || (this.chunkIndex == 0 && this.chunkCount == 0)) {
43
+ const start = this.chunkIndex * this.chunkSize
44
+ const end = Math.min(start + this.chunkSize, this.file.size)
45
+ const bytes = fileSlice.call(this.file, start, end)
46
+ this.fileReader.readAsArrayBuffer(bytes)
47
+ this.chunkIndex++
48
+ return true
49
+ } else {
50
+ return false
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,183 @@
1
+ import {FileChecksum} from '../activestorage/file_checksum'
2
+
3
+ window.WCC ||= {}
4
+ $ = window.jQuery
5
+
6
+ Dropzone.autoDiscover = off
7
+
8
+ class window.WCC.Dropzone extends window.Dropzone
9
+
10
+ uploadFile: (file) ->
11
+ xhr = new XMLHttpRequest
12
+ file.xhr = xhr
13
+
14
+ # Set in our custom accept method
15
+ xhr.open file.uploadMethod, file.uploadURL, true
16
+ xhr.setRequestHeader headerName, headerValue for headerName, headerValue of file.uploadHeaders
17
+
18
+ response = null
19
+
20
+ handleError = =>
21
+ @_errorProcessing [file], response || @options.dictResponseError.replace("{{statusCode}}", xhr.status), xhr
22
+
23
+ updateProgress = (e) =>
24
+ if e?
25
+ progress = 100 * e.loaded / e.total
26
+
27
+ file.upload =
28
+ progress: progress
29
+ total: e.total
30
+ bytesSent: e.loaded
31
+ else
32
+ # Called when the file finished uploading
33
+
34
+ allFilesFinished = yes
35
+
36
+ progress = 100
37
+
38
+ allFilesFinished = no unless file.upload.progress == 100 and file.upload.bytesSent == file.upload.total
39
+ file.upload.progress = progress
40
+ file.upload.bytesSent = file.upload.total
41
+
42
+ # Nothing to do, all files already at 100%
43
+ return if allFilesFinished
44
+
45
+ @emit "uploadprogress", file, progress, file.upload.bytesSent
46
+
47
+ xhr.onload = (e) =>
48
+ return if file.status == WCC.Dropzone.CANCELED
49
+ return unless xhr.readyState is 4
50
+
51
+ response = xhr.responseText
52
+
53
+ if xhr.getResponseHeader("content-type") and ~xhr.getResponseHeader("content-type").indexOf "application/json"
54
+ try
55
+ response = JSON.parse response
56
+ catch e
57
+ response = "Invalid JSON response from server."
58
+
59
+ updateProgress()
60
+
61
+ unless 200 <= xhr.status < 300
62
+ handleError()
63
+ else
64
+ @_finished [file], response, e
65
+
66
+ xhr.onerror = =>
67
+ return if file.status == WCC.Dropzone.CANCELED
68
+ handleError()
69
+
70
+ # Some browsers do not have the .upload property
71
+ progressObj = xhr.upload ? xhr
72
+ progressObj.onprogress = updateProgress
73
+
74
+ headers =
75
+ "Accept": "application/json",
76
+ "Cache-Control": "no-cache",
77
+ "X-Requested-With": "XMLHttpRequest",
78
+
79
+ extend headers, @options.headers if @options.headers
80
+
81
+ xhr.setRequestHeader headerName, headerValue for headerName, headerValue of headers
82
+
83
+ @emit "sending", file, xhr
84
+
85
+ xhr.send file
86
+
87
+ uploadFiles: (files) ->
88
+ @uploadFile(file) for file in files
89
+
90
+ WCC.Dropzone.uploadPendingWarning =
91
+ "File uploads have not yet completed. If you submit the form now they will
92
+ not be saved. Are you sure you want to continue?"
93
+
94
+ WCC.Dropzone.prototype.defaultOptions.url = "UNUSED"
95
+
96
+ WCC.Dropzone.prototype.defaultOptions.dictDefaultMessage = "Drop files or click to upload."
97
+
98
+ WCC.Dropzone.prototype.defaultOptions.accept = (file, done) ->
99
+ dz = $(file._removeLink).closest('.tessa-upload').first()
100
+ tessaParams = dz.data('tessa-params') or {}
101
+
102
+ postData =
103
+ name: file.name
104
+ size: file.size
105
+ mime_type: file.type
106
+
107
+ postData = $.extend postData, tessaParams
108
+
109
+ FileChecksum.create file, (error, checksum) ->
110
+ return done(error) if error
111
+
112
+ postData['checksum'] = checksum
113
+
114
+ $.ajax '/tessa/uploads',
115
+ type: 'POST',
116
+ data: postData,
117
+ success: (response) ->
118
+ file.uploadURL = response.upload_url
119
+ file.uploadMethod = response.upload_method
120
+ file.uploadHeaders = response.upload_headers
121
+ file.assetID = response.asset_id
122
+ done()
123
+ error: (response) ->
124
+ done("Failed to initiate the upload process!")
125
+
126
+ window.WCC.tessaInit = (sel) ->
127
+ sel = sel || 'form:has(.tessa-upload)'
128
+ $(sel).each (i, form) ->
129
+ $form = $(form)
130
+ $form.bind 'submit', (event) ->
131
+ safeToSubmit = true
132
+ $form.find('.tessa-upload').each (j, dropzoneElement) ->
133
+ $(dropzoneElement.dropzone.files).each (k, file) ->
134
+ if file.status? and (file.status != WCC.Dropzone.SUCCESS)
135
+ safeToSubmit = false
136
+ if not safeToSubmit and not confirm(WCC.Dropzone.uploadPendingWarning)
137
+ return false
138
+
139
+ $('.tessa-upload', sel).each (i, item) ->
140
+ $item = $(item)
141
+ args =
142
+ maxFiles: 1
143
+ addRemoveLinks: true
144
+
145
+ $.extend args, $item.data("dropzone-options")
146
+ args.maxFiles = null if $item.hasClass("multiple")
147
+ inputPrefix = $item.data("asset-field-prefix")
148
+
149
+ dropzone = new WCC.Dropzone item, args
150
+
151
+ $item.find('input[type="hidden"]').each (j, input) ->
152
+ $input = $(input)
153
+ mockFile = $input.data("meta")
154
+ mockFile.accepted = true
155
+ dropzone.options.addedfile.call(dropzone, mockFile)
156
+ dropzone.options.thumbnail.call(dropzone, mockFile, mockFile.url)
157
+ dropzone.emit("complete", mockFile)
158
+ dropzone.files.push mockFile
159
+
160
+ updateAction = (file) ->
161
+ return unless file.assetID?
162
+ inputID = "#tessa_asset_action_#{file.assetID}"
163
+ actionInput = $(inputID)
164
+ unless actionInput.length
165
+ actionInput = $('<input type="hidden">')
166
+ .attr
167
+ id: inputID
168
+ name: "#{inputPrefix}[#{file.assetID}][action]"
169
+ .appendTo item
170
+
171
+ actionInput.val file.action
172
+
173
+ dropzone.on 'success', (file) ->
174
+ file.action = "add"
175
+ updateAction(file)
176
+
177
+ dropzone.on 'removedfile', (file) ->
178
+ file.action = "remove"
179
+ updateAction(file)
180
+
181
+
182
+ $ ->
183
+ window.WCC.tessaInit()
data/config/routes.rb ADDED
@@ -0,0 +1,3 @@
1
+ Tessa::Engine.routes.draw do
2
+ post '/tessa/uploads', to: Tessa::RackUploadProxy
3
+ end
@@ -0,0 +1,24 @@
1
+ module Tessa::ActiveStorage
2
+ class AssetWrapper < SimpleDelegator
3
+ def public_url
4
+ Rails.application.routes.url_helpers.
5
+ rails_blob_url(__getobj__, disposition: :inline)
6
+ end
7
+
8
+ def private_url(expires_in: 1.day)
9
+ service_url(disposition: :inline, expires_in: expires_in)
10
+ end
11
+
12
+ def private_download_url(expires_in: 1.day)
13
+ service_url(disposition: 'attachment', expires_in: expires_in)
14
+ end
15
+
16
+ def meta
17
+ {}
18
+ end
19
+
20
+ def failure?
21
+ false
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,5 @@
1
+ module Tessa
2
+ class Engine < Rails::Engine
3
+ isolate_namespace Tessa
4
+ end
5
+ end
@@ -0,0 +1,151 @@
1
+ require 'forwardable'
2
+
3
+ class Tessa::DynamicExtensions
4
+ extend Forwardable
5
+
6
+ attr_reader :field
7
+
8
+ def name
9
+ field.name
10
+ end
11
+
12
+ def initialize(field)
13
+ @field = field
14
+ end
15
+
16
+ class SingleRecord < ::Tessa::DynamicExtensions
17
+ def build(mod)
18
+ mod.class_eval <<~CODE, __FILE__, __LINE__ + 1
19
+ def #{name}
20
+ # has_one_attached defines the getter using class_eval so we can't call
21
+ # super() here.
22
+ if #{name}_attachment.present?
23
+ return Tessa::ActiveStorage::AssetWrapper.new(#{name}_attachment)
24
+ end
25
+
26
+ # fall back to old Tessa fetch if not present
27
+ if field = self.class.tessa_fields["#{name}".to_sym]
28
+ @#{name} ||= fetch_tessa_remote_assets(field.id(on: self))
29
+ end
30
+ end
31
+
32
+ def #{field.id_field}
33
+ # Use the attachment's key
34
+ return #{name}_attachment.key if #{name}_attachment.present?
35
+
36
+ # fallback to Tessa's database column
37
+ super
38
+ end
39
+
40
+ def #{name}=(attachable)
41
+ # Every new upload is going to ActiveStorage
42
+ a = @active_storage_attached_#{name} ||=
43
+ ::ActiveStorage::Attached::One.new("#{name}", self, dependent: :purge_later)
44
+
45
+ case attachable
46
+ when Tessa::AssetChangeSet
47
+ attachable.changes.each do |change|
48
+ a.attach(change.id) if change.add?
49
+ a.detach if change.remove?
50
+ end
51
+ when nil
52
+ a.detach
53
+ else
54
+ a.attach(attachable)
55
+ end
56
+
57
+ # overwrite the tessa ID in the database
58
+ self.#{field.id_field} = nil
59
+ end
60
+
61
+ def attributes
62
+ super.merge({
63
+ '#{field.id_field}' => #{field.id_field}
64
+ })
65
+ end
66
+ CODE
67
+ mod
68
+ end
69
+ end
70
+
71
+ class MultipleRecord < ::Tessa::DynamicExtensions
72
+ def build(mod)
73
+ mod.class_eval <<~CODE, __FILE__, __LINE__ + 1
74
+ def #{name}
75
+ if #{name}_attachments.present?
76
+ return #{name}_attachments.map do |a|
77
+ Tessa::ActiveStorage::AssetWrapper.new(a)
78
+ end
79
+ end
80
+
81
+ # fall back to old Tessa fetch if not present
82
+ if field = self.class.tessa_fields["#{name}".to_sym]
83
+ @#{name} ||= fetch_tessa_remote_assets(field.id(on: self))
84
+ end
85
+ end
86
+
87
+ def #{field.id_field}
88
+ # Use the attachment's key
89
+ return #{name}_attachments.map(&:key) if #{name}_attachments.present?
90
+
91
+ # fallback to Tessa's database column
92
+ super
93
+ end
94
+
95
+ def #{name}=(attachables)
96
+ # Every new upload is going to ActiveStorage
97
+ a = @active_storage_attached_#{name} ||=
98
+ ::ActiveStorage::Attached::Many.new("#{name}", self, dependent: :purge_later)
99
+
100
+ case attachables
101
+ when Tessa::AssetChangeSet
102
+ attachables.changes.each do |change|
103
+ a.attach(change.id) if change.add?
104
+ raise 'TODO' if change.remove?
105
+ end
106
+ when nil
107
+ a.detach
108
+ else
109
+ a.attach(*attachables)
110
+ end
111
+
112
+ # overwrite the tessa ID in the database
113
+ self.#{field.id_field} = nil
114
+ end
115
+
116
+ def attributes
117
+ super.merge({
118
+ '#{field.id_field}' => #{field.id_field}
119
+ })
120
+ end
121
+ CODE
122
+ mod
123
+ end
124
+ end
125
+
126
+ class SingleFormObject < ::Tessa::DynamicExtensions
127
+ def build(mod)
128
+ mod.class_eval <<~CODE, __FILE__, __LINE__ + 1
129
+ attr_accessor :#{name}_id
130
+ attr_writer :#{name}
131
+ def #{name}
132
+ @#{name} ||= fetch_tessa_remote_assets(#{name}_id)
133
+ end
134
+ CODE
135
+ mod
136
+ end
137
+ end
138
+
139
+ class MultipleFormObject < ::Tessa::DynamicExtensions
140
+ def build(mod)
141
+ mod.class_eval <<~CODE, __FILE__, __LINE__ + 1
142
+ attr_accessor :#{name}_ids
143
+ attr_writer :#{name}
144
+ def #{name}
145
+ @#{name} ||= fetch_tessa_remote_assets(#{name}_ids)
146
+ end
147
+ CODE
148
+ mod
149
+ end
150
+ end
151
+ end
data/lib/tessa/model.rb CHANGED
@@ -1,4 +1,6 @@
1
1
  require 'tessa/model/field'
2
+ require 'tessa/model/dynamic_extensions'
3
+ require 'tessa/active_storage/asset_wrapper'
2
4
 
3
5
  module Tessa
4
6
  module Model
@@ -33,19 +35,7 @@ module Tessa
33
35
  end
34
36
 
35
37
  def fetch_tessa_remote_assets(ids)
36
- if [*ids].empty?
37
- [] if ids.is_a?(Array)
38
- else
39
- Tessa::Asset.find(ids)
40
- end
41
- rescue Tessa::RequestFailed => err
42
- if ids.is_a?(Array)
43
- ids.map do |id|
44
- Tessa::Asset::Failure.factory(id: id, response: err.response)
45
- end
46
- else
47
- Tessa::Asset::Failure.factory(id: ids, response: err.response)
48
- end
38
+ Tessa.find_assets(ids)
49
39
  end
50
40
 
51
41
  private def reapplying_asset?(field, change_set)
@@ -61,37 +51,38 @@ module Tessa
61
51
  module ClassMethods
62
52
  def asset(name, args={})
63
53
  field = tessa_fields[name] = Field.new(args.merge(name: name))
54
+
55
+ multiple = args[:multiple]
64
56
 
65
- dynamic_extensions = Module.new
66
57
 
67
- dynamic_extensions.send(:define_method, name) do
68
- if instance_variable_defined?(ivar = "@#{name}")
69
- instance_variable_get(ivar)
58
+ if respond_to?(:has_one_attached)
59
+ if multiple
60
+ has_many_attached(name)
70
61
  else
71
- instance_variable_set(
72
- ivar,
73
- fetch_tessa_remote_assets(field.id(on: self))
74
- )
62
+ has_one_attached(name)
75
63
  end
76
64
  end
77
65
 
78
- dynamic_extensions.send(:define_method, "#{name}=") do |value|
79
- change_set = field.change_set_for(value)
80
-
81
- # should effectively cause a no-op
82
- return if reapplying_asset?(field, change_set)
83
-
84
- if !(field.multiple? && value.is_a?(AssetChangeSet))
85
- new_ids = change_set.scoped_changes.select(&:add?).map(&:id)
86
- change_set += field.difference_change_set(new_ids, on: self)
66
+ dynamic_extensions =
67
+ if respond_to?(:has_one_attached)
68
+ if multiple
69
+ ::Tessa::DynamicExtensions::MultipleRecord.new(field)
70
+ else
71
+ ::Tessa::DynamicExtensions::SingleRecord.new(field)
72
+ end
73
+ else
74
+ if multiple
75
+ ::Tessa::DynamicExtensions::MultipleFormObject.new(field)
76
+ else
77
+ ::Tessa::DynamicExtensions::SingleFormObject.new(field)
78
+ end
87
79
  end
80
+ include dynamic_extensions.build(Module.new)
88
81
 
89
- pending_tessa_change_sets[name] += change_set
90
-
91
- field.apply(change_set, on: self)
92
- end
93
-
94
- include dynamic_extensions
82
+ # Undefine the activestorage default attribute method so it falls back
83
+ # to our dynamic module
84
+ remove_method "#{name}" rescue nil
85
+ remove_method "#{name}=" rescue nil
95
86
  end
96
87
 
97
88
  def tessa_fields
@@ -2,26 +2,34 @@ module Tessa
2
2
  class RackUploadProxy
3
3
 
4
4
  def call(env)
5
+ request = Rack::Request.new(env)
6
+ ::ActiveStorage::Current.host ||= request.base_url
7
+
8
+ # Call in to ActiveStorage to create a DirectUpload blob
5
9
  params = env['rack.request.form_hash']
6
- upload = Tessa::Upload.create({
7
- name: params["name"],
8
- size: params["size"],
9
- date: params["date"],
10
- mime_type: params["mime_type"],
11
- }.reject { |k, v| v.nil? })
10
+
11
+ blob = ::ActiveStorage::Blob.create_before_direct_upload!({
12
+ filename: params["name"],
13
+ byte_size: params["size"],
14
+ content_type: params["mime_type"],
15
+ checksum: params["checksum"]
16
+ })
12
17
 
13
18
  env['rack.session'][:tessa_upload_asset_ids] ||= []
14
- env['rack.session'][:tessa_upload_asset_ids] << upload.asset_id
19
+ env['rack.session'][:tessa_upload_asset_ids] << blob.signed_id
15
20
 
16
21
  response = {
17
- asset_id: upload.asset_id,
18
- upload_url: upload.upload_url,
19
- upload_method: upload.upload_method,
22
+ asset_id: blob.signed_id,
23
+ upload_url: blob.service_url_for_direct_upload,
24
+ upload_method: 'PUT', # ActiveStorage is always PUT
25
+ upload_headers: blob.service_headers_for_direct_upload
20
26
  }
21
27
 
22
28
  [200, {"Content-Type" => "application/json"}, [response.to_json]]
23
29
  rescue Tessa::RequestFailed
24
30
  [500, {"Content-Type" => "application/json"}, [{ "error" => "Failed to retreive upload URL" }.to_json]]
31
+ rescue ActiveRecord::NotNullViolation => e
32
+ [400, {"Content-Type" => "application/json"}, [{ "error" => e.message }.to_json]]
25
33
  end
26
34
 
27
35
  def self.call(*args)
data/lib/tessa/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Tessa
2
- VERSION = "0.9.2"
2
+ VERSION = "1.0.0-rc1"
3
3
  end
@@ -1,8 +1,10 @@
1
1
  module Tessa
2
2
  module ViewHelpers
3
- def tessa_image_tag(asset)
3
+ def tessa_image_tag(asset, private: false)
4
4
  handle_asset_failure(asset) do
5
- image_tag(asset.private_url)
5
+ image_tag(
6
+ private ? asset.private_url : asset.public_url
7
+ )
6
8
  end
7
9
  end
8
10
 
data/lib/tessa.rb CHANGED
@@ -24,6 +24,32 @@ module Tessa
24
24
  yield config
25
25
  end
26
26
 
27
+ def self.find_assets(ids)
28
+ if [*ids].empty?
29
+ if ids.is_a?(Array)
30
+ []
31
+ else
32
+ nil
33
+ end
34
+ elsif (blobs = ::ActiveStorage::Blob.where(key: ids).to_a).present?
35
+ if ids.is_a?(Array)
36
+ blobs.map { |a| Tessa::ActiveStorage::AssetWrapper.new(a) }
37
+ else
38
+ Tessa::ActiveStorage::AssetWrapper.new(blobs.first)
39
+ end
40
+ else
41
+ Tessa::Asset.find(ids)
42
+ end
43
+ rescue Tessa::RequestFailed => err
44
+ if ids.is_a?(Array)
45
+ ids.map do |id|
46
+ Tessa::Asset::Failure.factory(id: id, response: err.response)
47
+ end
48
+ else
49
+ Tessa::Asset::Failure.factory(id: ids, response: err.response)
50
+ end
51
+ end
52
+
27
53
  class RequestFailed < StandardError
28
54
  attr_reader :response
29
55
 
@@ -34,3 +60,7 @@ module Tessa
34
60
  end
35
61
 
36
62
  end
63
+
64
+ if defined?(Rails::Railtie)
65
+ require "tessa/engine"
66
+ end
data/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@watermarkchurch/tessa",
3
+ "version": "2.0.0",
4
+ "description": "Attach cloud and local files in Rails applications",
5
+ "module": "app/assets/javascripts/tessa.esm.js",
6
+ "main": "app/assets/javascripts/tessa.js",
7
+ "files": [
8
+ "app/assets/javascripts/*.js",
9
+ "src/*.js"
10
+ ],
11
+ "homepage": "https://www.watermark.org/",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/watermarkchurch/tessa-client.git"
15
+ },
16
+ "bugs": {
17
+ "url": "https://github.com/watermarkchurch/tessa-client/issues"
18
+ },
19
+ "author": "Watermark Dev <dev@watermark.org>",
20
+ "license": "MIT",
21
+ "dependencies": {
22
+ "spark-md5": "^3.0.1"
23
+ },
24
+ "peerDependencies": {
25
+ "dropzone": ">= 4"
26
+ },
27
+ "devDependencies": {
28
+ "@rollup/plugin-commonjs": "^19.0.1",
29
+ "@rollup/plugin-node-resolve": "^11.0.1",
30
+ "coffeescript": "^2.7.0",
31
+ "eslint": "^4.3.0",
32
+ "eslint-plugin-import": "^2.23.4",
33
+ "rollup": "^2.35.1",
34
+ "rollup-plugin-coffee-script": "^2.0.0",
35
+ "rollup-plugin-terser": "^7.0.2"
36
+ },
37
+ "scripts": {
38
+ "prebuild": "yarn lint",
39
+ "build": "rollup --config rollup.config.js",
40
+ "lint": "eslint app/javascript",
41
+ "prepublishOnly": "rm -rf src && cp -R app/javascript/activestorage src"
42
+ }
43
+ }