activestorage_saas 5.2.5.1 → 5.2.5.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d1b5d632c8d8737928c64de3799d5871205fdc50b94306574a85323d47f22eda
4
- data.tar.gz: 8c8b86b0f37605aa95ea80ddf6cb88c44f5ab74c70e81a2e684d25aa3c692754
3
+ metadata.gz: 14d926492586f0ab3058a8ff4a48a7cec2824747366c677f9c91f5c46a658535
4
+ data.tar.gz: 1c57c47f19302d6edc4953e6dcf94cd7871f0e22dda6670955c99639f2e3fffb
5
5
  SHA512:
6
- metadata.gz: 819a58f6f078ca03aba8736559ac5e04379e54076f80264ad363b1f9007913033797aaff0896b2bbf418e246f13f91092232d13edb836471c25a634736001a33
7
- data.tar.gz: 9bf4a83504cc82ffe698f6f3725eb7c82b642a6d018ba3d14d1269b1f8d20408ea45c93ec3338e326280e491d125e8e739b856fc6c95fb4a513c828d788e719d
6
+ metadata.gz: b9101363a32b4d5fbe122d0c7e4953fd10c0a7b2dce02f3d83c4ba42e80745a88d3a50111a4ad100417b6b4e2f3a6244cf5c16f8f07539afbaedea2ea8adde67
7
+ data.tar.gz: b5cb1539cbc4129abcee5c5f7b1df6a3b2773a9a87cd4426c223be3bd6ca41ab82632d93deff93b9a59fd4d90a97394c3ae320afc397f4ef0ea16bfd75aa0738
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  activestorage_version = '5.2.5'
4
- gem_version = 1
4
+ gem_version = 2
5
5
 
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = "activestorage_saas"
@@ -1,4 +1,4 @@
1
- class ActiveStorageSaas::DirectUploadsController < ActiveStorageSaas::BaseController
1
+ class ActiveStorageSaas::DirectUploadsController < ActiveStorage::BaseController
2
2
  def create
3
3
  blob = ActiveStorage::Blob.create!(blob_args)
4
4
  render json: direct_upload_json(blob)
@@ -19,8 +19,10 @@ class ActiveStorageSaas::DirectUploadsController < ActiveStorageSaas::BaseContro
19
19
  blob.as_json(root: false, methods: :signed_id, only: :signed_id)
20
20
  .merge(direct_upload: {
21
21
  url: blob.service_url_for_direct_upload,
22
+ method: blob.service_http_method_for_direct_upload,
23
+ responseType: blob.service_http_response_type_for_direct_upload,
22
24
  headers: blob.service_headers_for_direct_upload,
23
- formData: blob.service_form_data_for_direct_upload
25
+ formData: blob.service_form_data_for_direct_upload.presence
24
26
  })
25
27
  end
26
28
  end
@@ -0,0 +1,73 @@
1
+ import { getMetaValue } from "./helpers"
2
+
3
+ export class BlobRecord {
4
+ constructor(file, checksum, url) {
5
+ this.file = file
6
+
7
+ this.attributes = {
8
+ filename: file.name,
9
+ content_type: file.type || "application/octet-stream",
10
+ byte_size: file.size,
11
+ checksum: checksum
12
+ }
13
+
14
+ this.xhr = new XMLHttpRequest
15
+ this.xhr.open("POST", url, true)
16
+ this.xhr.responseType = "json"
17
+ this.xhr.setRequestHeader("Content-Type", "application/json")
18
+ this.xhr.setRequestHeader("Accept", "application/json")
19
+ this.xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest")
20
+
21
+ const csrfToken = getMetaValue("csrf-token")
22
+ if (csrfToken != undefined) {
23
+ this.xhr.setRequestHeader("X-CSRF-Token", csrfToken)
24
+ }
25
+
26
+ this.xhr.addEventListener("load", event => this.requestDidLoad(event))
27
+ this.xhr.addEventListener("error", event => this.requestDidError(event))
28
+ }
29
+
30
+ get status() {
31
+ return this.xhr.status
32
+ }
33
+
34
+ get response() {
35
+ const { responseType, response } = this.xhr
36
+ if (responseType == "json") {
37
+ return response
38
+ } else {
39
+ // Shim for IE 11: https://connect.microsoft.com/IE/feedback/details/794808
40
+ return JSON.parse(response)
41
+ }
42
+ }
43
+
44
+ create(callback) {
45
+ this.callback = callback
46
+ this.xhr.send(JSON.stringify({ blob: this.attributes }))
47
+ }
48
+
49
+ requestDidLoad(event) {
50
+ if (this.status >= 200 && this.status < 300) {
51
+ const { response } = this
52
+ const { direct_upload } = response
53
+ delete response.direct_upload
54
+ this.attributes = response
55
+ this.directUploadData = direct_upload
56
+ this.callback(null, this.toJSON())
57
+ } else {
58
+ this.requestDidError(event)
59
+ }
60
+ }
61
+
62
+ requestDidError(event) {
63
+ this.callback(event, this)
64
+ }
65
+
66
+ toJSON() {
67
+ const result = {}
68
+ for (const key in this.attributes) {
69
+ result[key] = this.attributes[key]
70
+ }
71
+ return result
72
+ }
73
+ }
@@ -0,0 +1,45 @@
1
+ export class BlobUpload {
2
+ constructor(blob) {
3
+ this.blob = blob
4
+ this.file = blob.file
5
+
6
+ const { url, headers, method, responseType } = blob.directUploadData
7
+
8
+ this.xhr = new XMLHttpRequest
9
+ this.xhr.open(method || "PUT", url, true)
10
+ this.xhr.responseType = responseType || "text"
11
+ for (const key in headers) {
12
+ this.xhr.setRequestHeader(key, headers[key])
13
+ }
14
+ this.xhr.addEventListener("load", event => this.requestDidLoad(event))
15
+ this.xhr.addEventListener("error", event => this.requestDidError(event))
16
+ }
17
+
18
+ create(callback) {
19
+ this.callback = callback
20
+ if(this.blob.directUploadData.formData){
21
+ var formData
22
+ formData = new FormData()
23
+ for(const key in this.blob.directUploadData.formData){
24
+ formData.append(key, this.blob.directUploadData.formData[key])
25
+ }
26
+ formData.append('file', this.file)
27
+ this.xhr.send(formData)
28
+ }else{
29
+ this.xhr.send(this.file.slice())
30
+ }
31
+ }
32
+
33
+ requestDidLoad(event) {
34
+ const { status, response } = this.xhr
35
+ if (status >= 200 && status < 300) {
36
+ this.callback(null, response)
37
+ } else {
38
+ this.requestDidError(event)
39
+ }
40
+ }
41
+
42
+ requestDidError(event) {
43
+ this.callback(event, this)
44
+ }
45
+ }
@@ -0,0 +1,48 @@
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
+ }
@@ -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,51 @@
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
+ }
@@ -0,0 +1,78 @@
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
+ }
@@ -0,0 +1,5 @@
1
+ class TenantStorageService < ApplicationRecord
2
+ belongs_to :tenant, inverse_of: :storage_services
3
+
4
+ store :service_options, coder: ActiveRecord::Coders::JSON
5
+ end
@@ -39,6 +39,14 @@ module ActiveStorage
39
39
  service.respond_to?(method)
40
40
  end
41
41
 
42
+ def http_method_for_direct_upload
43
+ service.service_http_method_for_direct_upload if service.respond_to?(:http_method_for_direct_upload)
44
+ end
45
+
46
+ def http_response_type_for_direct_upload
47
+ service.http_response_type_for_direct_upload if service.respond_to?(:http_response_type_for_direct_upload)
48
+ end
49
+
42
50
  def form_data_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
43
51
  if service.respond_to?(:form_data_for_direct_upload)
44
52
  service.form_data_for_direct_upload key,
@@ -62,10 +70,10 @@ module ActiveStorage
62
70
 
63
71
  private
64
72
 
65
- def default_service
66
- @default_service ||= ActiveStorage::Service.configure @options[:default_service],
67
- Rails.configuration.active_storage.service_configurations
68
- end
73
+ def default_service
74
+ @default_service ||= ActiveStorage::Service.configure @options[:default_service],
75
+ Rails.configuration.active_storage.service_configurations
76
+ end
69
77
  end
70
78
  end
71
79
  end
@@ -1,3 +1,5 @@
1
+ require 'active_storage/service/saas_service'
2
+
1
3
  module ActiveStorageSaas
2
4
  module BlobPatch
3
5
  extend ActiveSupport::Concern
@@ -16,6 +18,14 @@ module ActiveStorageSaas
16
18
  end
17
19
  end
18
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
+
19
29
  # support sending data from form instead of headers
20
30
  def service_form_data_for_direct_upload(expires_in: service.url_expires_in)
21
31
  return {} unless service.respond_to?(:form_data_for_direct_upload)
@@ -3,9 +3,9 @@ module ActiveStorageSaas
3
3
  # rubocop: disable Layout/LineLength
4
4
  def draw_active_storage_saas_routes(
5
5
  prefix: '/rails/active_storage',
6
- blobs_controller: 'active_storage_saas/blobs',
7
- representations_controller: 'active_storage_saas/representations',
8
- disk_controller: 'active_storage_saas/disk',
6
+ blobs_controller: 'active_storage/blobs',
7
+ representations_controller: 'active_storage/representations',
8
+ disk_controller: 'active_storage/disk',
9
9
  direct_uploads_controller: 'active_storage_saas/direct_uploads',
10
10
  **option_overrides
11
11
  )
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activestorage_saas
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.2.5.1
4
+ version: 5.2.5.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - xiaohui
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-08-10 00:00:00.000000000 Z
11
+ date: 2022-08-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activestorage
@@ -41,17 +41,20 @@ files:
41
41
  - README.md
42
42
  - Rakefile
43
43
  - activestorage_saas.gemspec
44
- - app/controller/active_storage_saas/base_controller.rb
45
- - app/controller/active_storage_saas/blobs_controller.rb
46
44
  - app/controller/active_storage_saas/direct_uploads_controller.rb
47
- - app/controller/active_storage_saas/disk_controller.rb
48
- - app/controller/active_storage_saas/representations_controller.rb
45
+ - app/javascript/active_storage_saas/direct_upload_controller.js
46
+ - app/javascript/active_storage_saas/direct_upload_controller/blob_record.js
47
+ - app/javascript/active_storage_saas/direct_upload_controller/blob_upload.js
48
+ - app/javascript/active_storage_saas/direct_upload_controller/direct_upload.js
49
+ - app/javascript/active_storage_saas/direct_upload_controller/file_checksum.js
50
+ - app/javascript/active_storage_saas/direct_upload_controller/helpers.js
51
+ - app/models/tenant_storage_service.rb
49
52
  - db/migrate/001_activestorage_saas_tables.rb
50
53
  - lib/active_storage/service/saas_service.rb
51
54
  - lib/active_storage_saas.rb
52
- - lib/actives_torage_saas/blob_patch.rb
53
- - lib/actives_torage_saas/engine.rb
54
- - lib/actives_torage_saas/routes.rb
55
+ - lib/active_storage_saas/blob_patch.rb
56
+ - lib/active_storage_saas/engine.rb
57
+ - lib/active_storage_saas/routes.rb
55
58
  - lib/activestorage_saas.rb
56
59
  - sig/activestorage_saas.rbs
57
60
  homepage: https://github.com/xiaohui-zhangxh/activestorage_saas
@@ -76,7 +79,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
76
79
  - !ruby/object:Gem::Version
77
80
  version: '0'
78
81
  requirements: []
79
- rubygems_version: 3.2.3
82
+ rubygems_version: 3.3.7
80
83
  signing_key:
81
84
  specification_version: 4
82
85
  summary: Wraps multi-tenant storage services as ActiveStorage service
@@ -1,7 +0,0 @@
1
- class ActiveStorageSaas::BaseController < ActiveStorage::BaseController
2
- unless method_defined?(:current_tenant)
3
- def current_tenant
4
- raise NotImplementedError
5
- end
6
- end
7
- end
@@ -1,8 +0,0 @@
1
- class ActiveStorageSaas::BlobsController < ActiveStorageSaas::BaseController
2
- include ActiveStorage::SetBlob
3
-
4
- def show
5
- expires_in ActiveStorage::Blob.service.url_expires_in
6
- redirect_to @blob.service_url(disposition: params[:disposition])
7
- end
8
- end
@@ -1,63 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Serves files stored with the disk service in the same way that the cloud services do.
4
- # This means using expiring, signed URLs that are meant for immediate access, not permanent linking.
5
- # Always go through the BlobsController, or your own authenticated controller, rather than directly
6
- # to the service url.
7
- class ActiveStorageSaas::DiskController < ActiveStorage::BaseController
8
- skip_forgery_protection
9
-
10
- def show
11
- if key = decode_verified_key
12
- serve_file disk_service.path_for(key[:key]), content_type: key[:content_type], disposition: key[:disposition]
13
- else
14
- head :not_found
15
- end
16
- end
17
-
18
- def update
19
- if token = decode_verified_token
20
- if acceptable_content?(token)
21
- disk_service.upload token[:key], request.body, checksum: token[:checksum]
22
- head :no_content
23
- else
24
- head :unprocessable_entity
25
- end
26
- end
27
- rescue ActiveStorage::IntegrityError
28
- head :unprocessable_entity
29
- end
30
-
31
- private
32
- def disk_service
33
- ActiveStorage::Blob.service
34
- end
35
-
36
-
37
- def decode_verified_key
38
- ActiveStorage.verifier.verified(params[:encoded_key], purpose: :blob_key)
39
- end
40
-
41
- def serve_file(path, content_type:, disposition:)
42
- Rack::File.new(nil).serving(request, path).tap do |(status, headers, body)|
43
- self.status = status
44
- self.response_body = body
45
-
46
- headers.each do |name, value|
47
- response.headers[name] = value
48
- end
49
-
50
- response.headers["Content-Type"] = content_type || DEFAULT_SEND_FILE_TYPE
51
- response.headers["Content-Disposition"] = disposition || DEFAULT_SEND_FILE_DISPOSITION
52
- end
53
- end
54
-
55
-
56
- def decode_verified_token
57
- ActiveStorage.verifier.verified(params[:encoded_token], purpose: :blob_token)
58
- end
59
-
60
- def acceptable_content?(token)
61
- token[:content_type] == request.content_mime_type && token[:content_length] == request.content_length
62
- end
63
- end
@@ -1,14 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Take a signed permanent reference for a blob representation and turn it into an expiring service URL for download.
4
- # Note: These URLs are publicly accessible. If you need to enforce access protection beyond the
5
- # security-through-obscurity factor of the signed blob and variation reference, you'll need to implement your own
6
- # authenticated redirection controller.
7
- class ActiveStorageSaas::RepresentationsController < ActiveStorage::BaseController
8
- include ActiveStorage::SetBlob
9
-
10
- def show
11
- expires_in ActiveStorage::Blob.service.url_expires_in
12
- redirect_to @blob.representation(params[:variation_key]).processed.service_url(disposition: params[:disposition])
13
- end
14
- end