activestorage_saas 5.2.5.2 → 7.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/AGENTS.md +260 -0
- data/CHANGELOG.md +12 -0
- data/COVERAGE.md +64 -0
- data/Gemfile +8 -4
- data/README.md +206 -17
- data/lib/active_storage/service/saas_service.rb +15 -10
- data/lib/active_storage_saas/blob_model_mixin.rb +48 -0
- data/lib/active_storage_saas/direct_uploads_controller_mixin.rb +31 -0
- data/lib/active_storage_saas/engine.rb +26 -5
- data/lib/active_storage_saas/routes.rb +3 -3
- data/lib/active_storage_saas/storage_service_configuration_model_mixin.rb +48 -0
- data/lib/active_storage_saas.rb +4 -2
- data/lib/generators/active_storage_saas/install/USAGE +5 -0
- data/lib/generators/active_storage_saas/install/install_generator.rb +19 -0
- data/lib/generators/active_storage_saas/install/templates/config/initializers/active_storage_saas.rb +6 -0
- metadata +13 -21
- data/.rspec +0 -3
- data/.rubocop.yml +0 -17
- data/Gemfile.lock +0 -112
- data/Rakefile +0 -12
- data/activestorage_saas.gemspec +0 -33
- data/app/controller/active_storage_saas/direct_uploads_controller.rb +0 -28
- data/app/javascript/active_storage_saas/direct_upload_controller/blob_record.js +0 -73
- data/app/javascript/active_storage_saas/direct_upload_controller/blob_upload.js +0 -45
- data/app/javascript/active_storage_saas/direct_upload_controller/direct_upload.js +0 -48
- data/app/javascript/active_storage_saas/direct_upload_controller/file_checksum.js +0 -53
- data/app/javascript/active_storage_saas/direct_upload_controller/helpers.js +0 -51
- data/app/javascript/active_storage_saas/direct_upload_controller.js +0 -78
- data/app/models/tenant_storage_service.rb +0 -5
- data/db/migrate/001_activestorage_saas_tables.rb +0 -17
- data/lib/active_storage_saas/blob_patch.rb +0 -51
- data/sig/activestorage_saas.rbs +0 -4
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { FileChecksum } from "./file_checksum"
|
|
2
|
-
import { BlobRecord } from "./blob_record"
|
|
3
|
-
import { BlobUpload } from "./blob_upload"
|
|
4
|
-
|
|
5
|
-
let id = 0
|
|
6
|
-
|
|
7
|
-
export class DirectUpload {
|
|
8
|
-
constructor(file, url, delegate) {
|
|
9
|
-
this.id = ++id
|
|
10
|
-
this.file = file
|
|
11
|
-
this.url = url
|
|
12
|
-
this.delegate = delegate
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
create(callback) {
|
|
16
|
-
FileChecksum.create(this.file, (error, checksum) => {
|
|
17
|
-
if (error) {
|
|
18
|
-
callback(error)
|
|
19
|
-
return
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const blob = new BlobRecord(this.file, checksum, this.url)
|
|
23
|
-
notify(this.delegate, "directUploadWillCreateBlobWithXHR", blob.xhr)
|
|
24
|
-
|
|
25
|
-
blob.create(error => {
|
|
26
|
-
if (error) {
|
|
27
|
-
callback(error)
|
|
28
|
-
} else {
|
|
29
|
-
const upload = new BlobUpload(blob)
|
|
30
|
-
notify(this.delegate, "directUploadWillStoreFileWithXHR", upload.xhr)
|
|
31
|
-
upload.create(error => {
|
|
32
|
-
if (error) {
|
|
33
|
-
callback(error)
|
|
34
|
-
} else {
|
|
35
|
-
callback(null, blob.toJSON())
|
|
36
|
-
}
|
|
37
|
-
})
|
|
38
|
-
}
|
|
39
|
-
})
|
|
40
|
-
})
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function notify(object, methodName, ...messages) {
|
|
45
|
-
if (object && typeof object[methodName] == "function") {
|
|
46
|
-
return object[methodName](...messages)
|
|
47
|
-
}
|
|
48
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
export function getMetaValue(name) {
|
|
2
|
-
const element = findElement(document.head, `meta[name="${name}"]`)
|
|
3
|
-
if (element) {
|
|
4
|
-
return element.getAttribute("content")
|
|
5
|
-
}
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function findElements(root, selector) {
|
|
9
|
-
if (typeof root == "string") {
|
|
10
|
-
selector = root
|
|
11
|
-
root = document
|
|
12
|
-
}
|
|
13
|
-
const elements = root.querySelectorAll(selector)
|
|
14
|
-
return toArray(elements)
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function findElement(root, selector) {
|
|
18
|
-
if (typeof root == "string") {
|
|
19
|
-
selector = root
|
|
20
|
-
root = document
|
|
21
|
-
}
|
|
22
|
-
return root.querySelector(selector)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function dispatchEvent(element, type, eventInit = {}) {
|
|
26
|
-
const { disabled } = element
|
|
27
|
-
const { bubbles, cancelable, detail } = eventInit
|
|
28
|
-
const event = document.createEvent("Event")
|
|
29
|
-
|
|
30
|
-
event.initEvent(type, bubbles || true, cancelable || true)
|
|
31
|
-
event.detail = detail || {}
|
|
32
|
-
|
|
33
|
-
try {
|
|
34
|
-
element.disabled = false
|
|
35
|
-
element.dispatchEvent(event)
|
|
36
|
-
} finally {
|
|
37
|
-
element.disabled = disabled
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return event
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function toArray(value) {
|
|
44
|
-
if (Array.isArray(value)) {
|
|
45
|
-
return value
|
|
46
|
-
} else if (Array.from) {
|
|
47
|
-
return Array.from(value)
|
|
48
|
-
} else {
|
|
49
|
-
return [].slice.call(value)
|
|
50
|
-
}
|
|
51
|
-
}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { Controller } from "@hotwired/stimulus";
|
|
2
|
-
import { DirectUpload } from "./direct_upload_controller/direct_upload";
|
|
3
|
-
|
|
4
|
-
const URL = window.URL || window.webkitURL
|
|
5
|
-
|
|
6
|
-
function setupFilePreview(target, file){
|
|
7
|
-
const onLoaded = function(){
|
|
8
|
-
URL.revokeObjectURL(target.src)
|
|
9
|
-
target.removeEventListener('load', onLoaded)
|
|
10
|
-
}
|
|
11
|
-
target.addEventListener('load', onLoaded)
|
|
12
|
-
target.src = URL.createObjectURL(file)
|
|
13
|
-
}
|
|
14
|
-
export default class extends Controller {
|
|
15
|
-
static targets = ['file', 'preview'];
|
|
16
|
-
static values = {
|
|
17
|
-
url: String,
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
initialize(){
|
|
21
|
-
this.onFileChange = this.onFileChange.bind(this)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
fileTargetConnected(target){
|
|
25
|
-
if(!target.hiddenInput){
|
|
26
|
-
const hiddenInput = document.createElement("input")
|
|
27
|
-
hiddenInput.type = "hidden"
|
|
28
|
-
hiddenInput.name = target.name
|
|
29
|
-
target.insertAdjacentElement("beforebegin", hiddenInput)
|
|
30
|
-
target.removeAttribute('name')
|
|
31
|
-
target.hiddenInput = hiddenInput
|
|
32
|
-
}
|
|
33
|
-
target.addEventListener('change', this.onFileChange)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
fileTargetDisconnected(target){
|
|
37
|
-
target.removeEventListener('change', this.onFileChange)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
onFileChange(event){
|
|
41
|
-
const { target } = event
|
|
42
|
-
const { hiddenInput, files } = target
|
|
43
|
-
const file = files[0]
|
|
44
|
-
if(!file) return
|
|
45
|
-
|
|
46
|
-
const directUpload = new DirectUpload(file, this.urlValue, this)
|
|
47
|
-
|
|
48
|
-
if(this.hasPreviewTarget){
|
|
49
|
-
setupFilePreview(this.previewTarget, file)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
directUpload.create((error, attributes) => {
|
|
53
|
-
if(error){
|
|
54
|
-
hiddenInput.removeAttribute('value')
|
|
55
|
-
}else{
|
|
56
|
-
hiddenInput.setAttribute('value', attributes.signed_id)
|
|
57
|
-
}
|
|
58
|
-
})
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
directUploadWillCreateBlobWithXHR(xhr) {
|
|
62
|
-
this.dispatch("before-blob-request", { detail: xhr })
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
directUploadWillStoreFileWithXHR(xhr) {
|
|
66
|
-
this.dispatch('started', { detail: xhr })
|
|
67
|
-
this.dispatch('progress', { detail: { percent: 0 } })
|
|
68
|
-
xhr.upload.addEventListener("progress", event => this.uploadRequestDidProgress(event))
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
uploadRequestDidProgress(event){
|
|
72
|
-
const percent = event.loaded / event.total
|
|
73
|
-
this.dispatch('progress', { detail: { percent } })
|
|
74
|
-
if(percent == 1){
|
|
75
|
-
this.dispatch('uploaded')
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
class ActivestorageSaasTables < ActiveRecord::Migration[5.2]
|
|
2
|
-
def change
|
|
3
|
-
tenant_class = ActiveStorageSaas.tenant_class_name.constantize
|
|
4
|
-
tenant_primary_key = tenant_class.columns.find{|col| col.name == tenant_class.primary_key }
|
|
5
|
-
create_table :tenant_storage_services do |t|
|
|
6
|
-
t.references :tenant, type: tenant_primary_key.type, foreign_key: { to_table: tenant_class.table_name }
|
|
7
|
-
t.string :service_name
|
|
8
|
-
t.json :service_options
|
|
9
|
-
|
|
10
|
-
t.timestamps
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
add_reference tenant_class.table_name.to_sym, :tenant_storage_service, foreign_key: true
|
|
14
|
-
add_reference :active_storage_blobs, :tenant, type: tenant_primary_key.type, foreign_key: true
|
|
15
|
-
add_reference :active_storage_blobs, :tenant_storage_service, foreign_key: true
|
|
16
|
-
end
|
|
17
|
-
end
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
require 'active_storage/service/saas_service'
|
|
2
|
-
|
|
3
|
-
module ActiveStorageSaas
|
|
4
|
-
module BlobPatch
|
|
5
|
-
extend ActiveSupport::Concern
|
|
6
|
-
|
|
7
|
-
included do
|
|
8
|
-
belongs_to :tenant, class_name: ActiveStorageSaas.tenant_class_name # rubocop: disable Rails/ReflectionClassName
|
|
9
|
-
belongs_to :tenant_storage_service, optional: true
|
|
10
|
-
|
|
11
|
-
redefine_method :service do
|
|
12
|
-
class_service = self.class.service
|
|
13
|
-
if class_service.is_a?(ActiveStorage::Service::SaasService)
|
|
14
|
-
@service ||= ActiveStorage::Service::SaasService.new(class_service.options.merge(blob: self))
|
|
15
|
-
else
|
|
16
|
-
class_service
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def service_http_method_for_direct_upload
|
|
22
|
-
service.respond_to?(:http_method_for_direct_upload) ? service.http_method_for_direct_upload : nil
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def service_http_response_type_for_direct_upload
|
|
26
|
-
service.respond_to?(:http_response_type_for_direct_upload) ? service.http_response_type_for_direct_upload : nil
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# support sending data from form instead of headers
|
|
30
|
-
def service_form_data_for_direct_upload(expires_in: service.url_expires_in)
|
|
31
|
-
return {} unless service.respond_to?(:form_data_for_direct_upload)
|
|
32
|
-
|
|
33
|
-
service.form_data_for_direct_upload(key,
|
|
34
|
-
expires_in: expires_in,
|
|
35
|
-
content_type: content_type,
|
|
36
|
-
content_length: byte_size,
|
|
37
|
-
checksum: checksum)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
private
|
|
41
|
-
|
|
42
|
-
def saas_service?
|
|
43
|
-
service.is_a?(ActiveStorage::Service::SaasService)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def analyzer_class
|
|
47
|
-
analyzers = saas_service? ? service.analyzers : ActiveStorage.analyzers
|
|
48
|
-
analyzers.detect { |klass| klass.accept?(self) } || ActiveStorage::Analyzer::NullAnalyzer
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|
data/sig/activestorage_saas.rbs
DELETED