tessa 0.9.1 → 1.0.0.pre.rc2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.babelrc +8 -0
- data/.circleci/config.yml +42 -0
- data/.eslintrc +19 -0
- data/.gitignore +5 -0
- data/.ruby-version +1 -0
- data/.travis.yml +12 -4
- data/README.md +4 -4
- data/Rakefile +8 -0
- data/app/assets/javascripts/.keep +0 -0
- data/app/assets/javascripts/tessa.esm.js +672 -0
- data/app/assets/javascripts/tessa.js +662 -0
- data/app/javascript/activestorage/file_checksum.js +53 -0
- data/app/javascript/tessa/index.js.coffee +183 -0
- data/config/routes.rb +3 -0
- data/lib/tessa/active_storage/asset_wrapper.rb +24 -0
- data/lib/tessa/engine.rb +5 -0
- data/lib/tessa/model/dynamic_extensions.rb +151 -0
- data/lib/tessa/model.rb +40 -36
- data/lib/tessa/rack_upload_proxy.rb +18 -10
- data/lib/tessa/version.rb +1 -1
- data/lib/tessa/view_helpers.rb +4 -2
- data/lib/tessa.rb +30 -0
- data/package.json +43 -0
- data/rollup.config.js +44 -0
- data/spec/dummy/.gitignore +27 -0
- data/spec/dummy/README.md +24 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/config/manifest.js +2 -0
- data/spec/dummy/app/assets/images/.keep +0 -0
- data/spec/dummy/app/assets/stylesheets/application.css +15 -0
- data/spec/dummy/app/controllers/application_controller.rb +2 -0
- data/spec/dummy/app/controllers/concerns/.keep +0 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/jobs/application_job.rb +2 -0
- data/spec/dummy/app/models/concerns/.keep +0 -0
- data/spec/dummy/app/models/multiple_asset_model.rb +8 -0
- data/spec/dummy/app/models/single_asset_model.rb +5 -0
- data/spec/dummy/app/models/single_asset_model_form.rb +25 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/bin/setup +28 -0
- data/spec/dummy/bin/update +28 -0
- data/spec/dummy/bin/yarn +11 -0
- data/spec/dummy/config/application.rb +33 -0
- data/spec/dummy/config/boot.rb +3 -0
- data/spec/dummy/config/credentials.yml.enc +1 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +47 -0
- data/spec/dummy/config/environments/production.rb +76 -0
- data/spec/dummy/config/environments/test.rb +43 -0
- data/spec/dummy/config/initializers/application_controller_renderer.rb +8 -0
- data/spec/dummy/config/initializers/assets.rb +14 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/content_security_policy.rb +25 -0
- data/spec/dummy/config/initializers/cookies_serializer.rb +5 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +4 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +9 -0
- data/spec/dummy/config/locales/en.yml +33 -0
- data/spec/dummy/config/puma.rb +37 -0
- data/spec/dummy/config/routes.rb +3 -0
- data/spec/dummy/config/storage.yml +3 -0
- data/spec/dummy/config.ru +5 -0
- data/spec/dummy/db/migrate/20220606221557_create_active_storage_tables.active_storage.rb +27 -0
- data/spec/dummy/db/migrate/20220606221900_create_single_asset_models.rb +10 -0
- data/spec/dummy/db/migrate/20220607191519_create_multiple_asset_models.rb +10 -0
- data/spec/dummy/db/schema.rb +50 -0
- data/spec/dummy/lib/assets/.keep +0 -0
- data/spec/dummy/lib/tasks/.keep +0 -0
- data/spec/dummy/log/.keep +0 -0
- data/spec/dummy/package.json +5 -0
- data/spec/dummy/public/404.html +67 -0
- data/spec/dummy/public/422.html +67 -0
- data/spec/dummy/public/500.html +66 -0
- data/spec/dummy/public/apple-touch-icon-precomposed.png +0 -0
- data/spec/dummy/public/apple-touch-icon.png +0 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/public/robots.txt +1 -0
- data/spec/dummy/tmp/.keep +0 -0
- data/spec/dummy/vendor/.keep +0 -0
- data/spec/rails_helper.rb +14 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/tessa/model_spec.rb +195 -244
- data/spec/tessa/rack_upload_proxy_spec.rb +23 -81
- data/spec/tessa/upload/uploads_file_spec.rb +12 -8
- data/tessa.gemspec +10 -2
- data/yarn.lock +1661 -0
- metadata +196 -18
- 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,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
|
data/lib/tessa/engine.rb
ADDED
@@ -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,55 +35,59 @@ module Tessa
|
|
33
35
|
end
|
34
36
|
|
35
37
|
def fetch_tessa_remote_assets(ids)
|
36
|
-
|
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
|
|
41
|
+
private def reapplying_asset?(field, change_set)
|
42
|
+
additions = change_set.changes.select(&:add?)
|
43
|
+
|
44
|
+
return false if additions.none?
|
45
|
+
return false if change_set.changes.size > additions.size
|
46
|
+
|
47
|
+
additions.all? { |a| field.ids(on: self).include?(a.id) }
|
48
|
+
end
|
51
49
|
end
|
52
50
|
|
53
51
|
module ClassMethods
|
54
|
-
|
55
52
|
def asset(name, args={})
|
56
53
|
field = tessa_fields[name] = Field.new(args.merge(name: name))
|
54
|
+
|
55
|
+
multiple = args[:multiple]
|
57
56
|
|
58
|
-
dynamic_extensions = Module.new
|
59
57
|
|
60
|
-
|
61
|
-
if
|
62
|
-
|
58
|
+
if respond_to?(:has_one_attached)
|
59
|
+
if multiple
|
60
|
+
has_many_attached(name)
|
63
61
|
else
|
64
|
-
|
65
|
-
ivar,
|
66
|
-
fetch_tessa_remote_assets(field.id(on: self))
|
67
|
-
)
|
62
|
+
has_one_attached(name)
|
68
63
|
end
|
69
|
-
end
|
70
64
|
|
71
|
-
|
72
|
-
|
65
|
+
# We have to replace the after_destroy_commit callback added above
|
66
|
+
callbacks = get_callbacks(:commit)
|
67
|
+
callbacks.delete(callbacks.to_a.last)
|
68
|
+
after_destroy_commit { public_send("#{name}_attachment")&.purge_later }
|
69
|
+
end
|
73
70
|
|
74
|
-
|
75
|
-
|
76
|
-
|
71
|
+
dynamic_extensions =
|
72
|
+
if respond_to?(:has_one_attached)
|
73
|
+
if multiple
|
74
|
+
::Tessa::DynamicExtensions::MultipleRecord.new(field)
|
75
|
+
else
|
76
|
+
::Tessa::DynamicExtensions::SingleRecord.new(field)
|
77
|
+
end
|
78
|
+
else
|
79
|
+
if multiple
|
80
|
+
::Tessa::DynamicExtensions::MultipleFormObject.new(field)
|
81
|
+
else
|
82
|
+
::Tessa::DynamicExtensions::SingleFormObject.new(field)
|
83
|
+
end
|
77
84
|
end
|
85
|
+
include dynamic_extensions.build(Module.new)
|
78
86
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
include dynamic_extensions
|
87
|
+
# Undefine the activestorage default attribute method so it falls back
|
88
|
+
# to our dynamic module
|
89
|
+
remove_method "#{name}" rescue nil
|
90
|
+
remove_method "#{name}=" rescue nil
|
85
91
|
end
|
86
92
|
|
87
93
|
def tessa_fields
|
@@ -91,8 +97,6 @@ module Tessa
|
|
91
97
|
def inherited(subclass)
|
92
98
|
subclass.instance_variable_set(:@tessa_fields, @tessa_fields.dup)
|
93
99
|
end
|
94
|
-
|
95
100
|
end
|
96
|
-
|
97
101
|
end
|
98
102
|
end
|
@@ -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
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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] <<
|
19
|
+
env['rack.session'][:tessa_upload_asset_ids] << blob.signed_id
|
15
20
|
|
16
21
|
response = {
|
17
|
-
asset_id:
|
18
|
-
upload_url:
|
19
|
-
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
data/lib/tessa/view_helpers.rb
CHANGED
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
|
+
}
|