activestorage 6.0.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of activestorage might be problematic. Click here for more details.
- checksums.yaml +7 -0
- data/CHANGELOG.md +198 -0
- data/MIT-LICENSE +20 -0
- data/README.md +162 -0
- data/app/assets/javascripts/activestorage.js +942 -0
- data/app/controllers/active_storage/base_controller.rb +8 -0
- data/app/controllers/active_storage/blobs_controller.rb +14 -0
- data/app/controllers/active_storage/direct_uploads_controller.rb +23 -0
- data/app/controllers/active_storage/disk_controller.rb +66 -0
- data/app/controllers/active_storage/representations_controller.rb +14 -0
- data/app/controllers/concerns/active_storage/set_blob.rb +16 -0
- data/app/controllers/concerns/active_storage/set_current.rb +15 -0
- data/app/javascript/activestorage/blob_record.js +73 -0
- data/app/javascript/activestorage/blob_upload.js +35 -0
- data/app/javascript/activestorage/direct_upload.js +48 -0
- data/app/javascript/activestorage/direct_upload_controller.js +67 -0
- data/app/javascript/activestorage/direct_uploads_controller.js +50 -0
- data/app/javascript/activestorage/file_checksum.js +53 -0
- data/app/javascript/activestorage/helpers.js +51 -0
- data/app/javascript/activestorage/index.js +11 -0
- data/app/javascript/activestorage/ujs.js +86 -0
- data/app/jobs/active_storage/analyze_job.rb +12 -0
- data/app/jobs/active_storage/base_job.rb +4 -0
- data/app/jobs/active_storage/purge_job.rb +13 -0
- data/app/models/active_storage/attachment.rb +50 -0
- data/app/models/active_storage/blob.rb +278 -0
- data/app/models/active_storage/blob/analyzable.rb +57 -0
- data/app/models/active_storage/blob/identifiable.rb +31 -0
- data/app/models/active_storage/blob/representable.rb +93 -0
- data/app/models/active_storage/current.rb +5 -0
- data/app/models/active_storage/filename.rb +77 -0
- data/app/models/active_storage/preview.rb +89 -0
- data/app/models/active_storage/variant.rb +131 -0
- data/app/models/active_storage/variation.rb +80 -0
- data/config/routes.rb +32 -0
- data/db/migrate/20170806125915_create_active_storage_tables.rb +26 -0
- data/db/update_migrate/20180723000244_add_foreign_key_constraint_to_active_storage_attachments_for_blob_id.rb +9 -0
- data/lib/active_storage.rb +73 -0
- data/lib/active_storage/analyzer.rb +38 -0
- data/lib/active_storage/analyzer/image_analyzer.rb +52 -0
- data/lib/active_storage/analyzer/null_analyzer.rb +13 -0
- data/lib/active_storage/analyzer/video_analyzer.rb +118 -0
- data/lib/active_storage/attached.rb +25 -0
- data/lib/active_storage/attached/changes.rb +16 -0
- data/lib/active_storage/attached/changes/create_many.rb +46 -0
- data/lib/active_storage/attached/changes/create_one.rb +69 -0
- data/lib/active_storage/attached/changes/create_one_of_many.rb +10 -0
- data/lib/active_storage/attached/changes/delete_many.rb +27 -0
- data/lib/active_storage/attached/changes/delete_one.rb +19 -0
- data/lib/active_storage/attached/many.rb +65 -0
- data/lib/active_storage/attached/model.rb +147 -0
- data/lib/active_storage/attached/one.rb +79 -0
- data/lib/active_storage/downloader.rb +43 -0
- data/lib/active_storage/downloading.rb +47 -0
- data/lib/active_storage/engine.rb +149 -0
- data/lib/active_storage/errors.rb +26 -0
- data/lib/active_storage/gem_version.rb +17 -0
- data/lib/active_storage/log_subscriber.rb +58 -0
- data/lib/active_storage/previewer.rb +84 -0
- data/lib/active_storage/previewer/mupdf_previewer.rb +36 -0
- data/lib/active_storage/previewer/poppler_pdf_previewer.rb +35 -0
- data/lib/active_storage/previewer/video_previewer.rb +26 -0
- data/lib/active_storage/reflection.rb +64 -0
- data/lib/active_storage/service.rb +141 -0
- data/lib/active_storage/service/azure_storage_service.rb +165 -0
- data/lib/active_storage/service/configurator.rb +34 -0
- data/lib/active_storage/service/disk_service.rb +166 -0
- data/lib/active_storage/service/gcs_service.rb +141 -0
- data/lib/active_storage/service/mirror_service.rb +55 -0
- data/lib/active_storage/service/s3_service.rb +116 -0
- data/lib/active_storage/transformers/image_processing_transformer.rb +39 -0
- data/lib/active_storage/transformers/mini_magick_transformer.rb +38 -0
- data/lib/active_storage/transformers/transformer.rb +42 -0
- data/lib/active_storage/version.rb +10 -0
- data/lib/tasks/activestorage.rake +22 -0
- metadata +174 -0
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Take a signed permanent reference for a blob 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 references, you'll need to implement your own
|
6
|
+
# authenticated redirection controller.
|
7
|
+
class ActiveStorage::BlobsController < ActiveStorage::BaseController
|
8
|
+
include ActiveStorage::SetBlob
|
9
|
+
|
10
|
+
def show
|
11
|
+
expires_in ActiveStorage.service_urls_expire_in
|
12
|
+
redirect_to @blob.service_url(disposition: params[:disposition])
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Creates a new blob on the server side in anticipation of a direct-to-service upload from the client side.
|
4
|
+
# When the client-side upload is completed, the signed_blob_id can be submitted as part of the form to reference
|
5
|
+
# the blob that was created up front.
|
6
|
+
class ActiveStorage::DirectUploadsController < ActiveStorage::BaseController
|
7
|
+
def create
|
8
|
+
blob = ActiveStorage::Blob.create_before_direct_upload!(blob_args)
|
9
|
+
render json: direct_upload_json(blob)
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
def blob_args
|
14
|
+
params.require(:blob).permit(:filename, :byte_size, :checksum, :content_type, :metadata).to_h.symbolize_keys
|
15
|
+
end
|
16
|
+
|
17
|
+
def direct_upload_json(blob)
|
18
|
+
blob.as_json(root: false, methods: :signed_id).merge(direct_upload: {
|
19
|
+
url: blob.service_url_for_direct_upload,
|
20
|
+
headers: blob.service_headers_for_direct_upload
|
21
|
+
})
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,66 @@
|
|
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 ActiveStorage::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
|
+
rescue Errno::ENOENT
|
17
|
+
head :not_found
|
18
|
+
end
|
19
|
+
|
20
|
+
def update
|
21
|
+
if token = decode_verified_token
|
22
|
+
if acceptable_content?(token)
|
23
|
+
disk_service.upload token[:key], request.body, checksum: token[:checksum]
|
24
|
+
else
|
25
|
+
head :unprocessable_entity
|
26
|
+
end
|
27
|
+
else
|
28
|
+
head :not_found
|
29
|
+
end
|
30
|
+
rescue ActiveStorage::IntegrityError
|
31
|
+
head :unprocessable_entity
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
def disk_service
|
36
|
+
ActiveStorage::Blob.service
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
def decode_verified_key
|
41
|
+
ActiveStorage.verifier.verified(params[:encoded_key], purpose: :blob_key)
|
42
|
+
end
|
43
|
+
|
44
|
+
def serve_file(path, content_type:, disposition:)
|
45
|
+
Rack::File.new(nil).serving(request, path).tap do |(status, headers, body)|
|
46
|
+
self.status = status
|
47
|
+
self.response_body = body
|
48
|
+
|
49
|
+
headers.each do |name, value|
|
50
|
+
response.headers[name] = value
|
51
|
+
end
|
52
|
+
|
53
|
+
response.headers["Content-Type"] = content_type || DEFAULT_SEND_FILE_TYPE
|
54
|
+
response.headers["Content-Disposition"] = disposition || DEFAULT_SEND_FILE_DISPOSITION
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
def decode_verified_token
|
60
|
+
ActiveStorage.verifier.verified(params[:encoded_token], purpose: :blob_token)
|
61
|
+
end
|
62
|
+
|
63
|
+
def acceptable_content?(token)
|
64
|
+
token[:content_type] == request.content_mime_type && token[:content_length] == request.content_length
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,14 @@
|
|
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 ActiveStorage::RepresentationsController < ActiveStorage::BaseController
|
8
|
+
include ActiveStorage::SetBlob
|
9
|
+
|
10
|
+
def show
|
11
|
+
expires_in ActiveStorage.service_urls_expire_in
|
12
|
+
redirect_to @blob.representation(params[:variation_key]).processed.service_url(disposition: params[:disposition])
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveStorage::SetBlob #:nodoc:
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
before_action :set_blob
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
def set_blob
|
12
|
+
@blob = ActiveStorage::Blob.find_signed(params[:signed_blob_id] || params[:signed_id])
|
13
|
+
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
14
|
+
head :not_found
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Sets the <tt>ActiveStorage::Current.host</tt> attribute, which the disk service uses to generate URLs.
|
4
|
+
# Include this concern in custom controllers that call ActiveStorage::Blob#service_url,
|
5
|
+
# ActiveStorage::Variant#service_url, or ActiveStorage::Preview#service_url so the disk service can
|
6
|
+
# generate URLs using the same host, protocol, and base path as the current request.
|
7
|
+
module ActiveStorage::SetCurrent
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
included do
|
11
|
+
before_action do
|
12
|
+
ActiveStorage::Current.host = request.base_url
|
13
|
+
end
|
14
|
+
end
|
15
|
+
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,
|
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(`Error creating Blob for "${this.file.name}". Status: ${this.status}`)
|
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,35 @@
|
|
1
|
+
export class BlobUpload {
|
2
|
+
constructor(blob) {
|
3
|
+
this.blob = blob
|
4
|
+
this.file = blob.file
|
5
|
+
|
6
|
+
const { url, headers } = blob.directUploadData
|
7
|
+
|
8
|
+
this.xhr = new XMLHttpRequest
|
9
|
+
this.xhr.open("PUT", url, true)
|
10
|
+
this.xhr.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
|
+
this.xhr.send(this.file.slice())
|
21
|
+
}
|
22
|
+
|
23
|
+
requestDidLoad(event) {
|
24
|
+
const { status, response } = this.xhr
|
25
|
+
if (status >= 200 && status < 300) {
|
26
|
+
this.callback(null, response)
|
27
|
+
} else {
|
28
|
+
this.requestDidError(event)
|
29
|
+
}
|
30
|
+
}
|
31
|
+
|
32
|
+
requestDidError(event) {
|
33
|
+
this.callback(`Error storing "${this.file.name}". Status: ${this.xhr.status}`)
|
34
|
+
}
|
35
|
+
}
|
@@ -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,67 @@
|
|
1
|
+
import { DirectUpload } from "./direct_upload"
|
2
|
+
import { dispatchEvent } from "./helpers"
|
3
|
+
|
4
|
+
export class DirectUploadController {
|
5
|
+
constructor(input, file) {
|
6
|
+
this.input = input
|
7
|
+
this.file = file
|
8
|
+
this.directUpload = new DirectUpload(this.file, this.url, this)
|
9
|
+
this.dispatch("initialize")
|
10
|
+
}
|
11
|
+
|
12
|
+
start(callback) {
|
13
|
+
const hiddenInput = document.createElement("input")
|
14
|
+
hiddenInput.type = "hidden"
|
15
|
+
hiddenInput.name = this.input.name
|
16
|
+
this.input.insertAdjacentElement("beforebegin", hiddenInput)
|
17
|
+
|
18
|
+
this.dispatch("start")
|
19
|
+
|
20
|
+
this.directUpload.create((error, attributes) => {
|
21
|
+
if (error) {
|
22
|
+
hiddenInput.parentNode.removeChild(hiddenInput)
|
23
|
+
this.dispatchError(error)
|
24
|
+
} else {
|
25
|
+
hiddenInput.value = attributes.signed_id
|
26
|
+
}
|
27
|
+
|
28
|
+
this.dispatch("end")
|
29
|
+
callback(error)
|
30
|
+
})
|
31
|
+
}
|
32
|
+
|
33
|
+
uploadRequestDidProgress(event) {
|
34
|
+
const progress = event.loaded / event.total * 100
|
35
|
+
if (progress) {
|
36
|
+
this.dispatch("progress", { progress })
|
37
|
+
}
|
38
|
+
}
|
39
|
+
|
40
|
+
get url() {
|
41
|
+
return this.input.getAttribute("data-direct-upload-url")
|
42
|
+
}
|
43
|
+
|
44
|
+
dispatch(name, detail = {}) {
|
45
|
+
detail.file = this.file
|
46
|
+
detail.id = this.directUpload.id
|
47
|
+
return dispatchEvent(this.input, `direct-upload:${name}`, { detail })
|
48
|
+
}
|
49
|
+
|
50
|
+
dispatchError(error) {
|
51
|
+
const event = this.dispatch("error", { error })
|
52
|
+
if (!event.defaultPrevented) {
|
53
|
+
alert(error)
|
54
|
+
}
|
55
|
+
}
|
56
|
+
|
57
|
+
// DirectUpload delegate
|
58
|
+
|
59
|
+
directUploadWillCreateBlobWithXHR(xhr) {
|
60
|
+
this.dispatch("before-blob-request", { xhr })
|
61
|
+
}
|
62
|
+
|
63
|
+
directUploadWillStoreFileWithXHR(xhr) {
|
64
|
+
this.dispatch("before-storage-request", { xhr })
|
65
|
+
xhr.upload.addEventListener("progress", event => this.uploadRequestDidProgress(event))
|
66
|
+
}
|
67
|
+
}
|
@@ -0,0 +1,50 @@
|
|
1
|
+
import { DirectUploadController } from "./direct_upload_controller"
|
2
|
+
import { findElements, dispatchEvent, toArray } from "./helpers"
|
3
|
+
|
4
|
+
const inputSelector = "input[type=file][data-direct-upload-url]:not([disabled])"
|
5
|
+
|
6
|
+
export class DirectUploadsController {
|
7
|
+
constructor(form) {
|
8
|
+
this.form = form
|
9
|
+
this.inputs = findElements(form, inputSelector).filter(input => input.files.length)
|
10
|
+
}
|
11
|
+
|
12
|
+
start(callback) {
|
13
|
+
const controllers = this.createDirectUploadControllers()
|
14
|
+
|
15
|
+
const startNextController = () => {
|
16
|
+
const controller = controllers.shift()
|
17
|
+
if (controller) {
|
18
|
+
controller.start(error => {
|
19
|
+
if (error) {
|
20
|
+
callback(error)
|
21
|
+
this.dispatch("end")
|
22
|
+
} else {
|
23
|
+
startNextController()
|
24
|
+
}
|
25
|
+
})
|
26
|
+
} else {
|
27
|
+
callback()
|
28
|
+
this.dispatch("end")
|
29
|
+
}
|
30
|
+
}
|
31
|
+
|
32
|
+
this.dispatch("start")
|
33
|
+
startNextController()
|
34
|
+
}
|
35
|
+
|
36
|
+
createDirectUploadControllers() {
|
37
|
+
const controllers = []
|
38
|
+
this.inputs.forEach(input => {
|
39
|
+
toArray(input.files).forEach(file => {
|
40
|
+
const controller = new DirectUploadController(input, file)
|
41
|
+
controllers.push(controller)
|
42
|
+
})
|
43
|
+
})
|
44
|
+
return controllers
|
45
|
+
}
|
46
|
+
|
47
|
+
dispatch(name, detail = {}) {
|
48
|
+
return dispatchEvent(this.form, `direct-uploads:${name}`, { detail })
|
49
|
+
}
|
50
|
+
}
|
@@ -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
|
+
}
|