activestorage_saas 5.2.5.1 → 5.2.5.2

Sign up to get free protection for your applications and to get access to all the features.
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