defmodule ActiveStorage.Blob do use Ecto.Schema import Ecto.Changeset import ActiveStorage.Blob.Representable import ActiveStorage.Blob.Identifiable import ActiveStorage.Blob.Analyzable # @foreign_key_type :binary_id schema "active_storage_blobs" do field :byte_size, :integer field :checksum, :string field :content_type, :string field :filename, :string # field :key, :string # I wasn't able to get the JSON to render. Error: # cannot load `"{\"identified\":true,\"analyzed\":true}"` as type :map for field :metadata in %ActiveStorage.Blob{... # field :metadata, :map field :service_name, :string has_many :variant_records, ActiveStorage.VariantRecord # before_destroy { variant_records.destroy_all if ActiveStorage.track_variants } # has_one_attached :preview_image timestamps(inserted_at: :created_at, updated_at: false) end @doc false def changeset(storage_blob, attrs) do storage_blob |> cast(attrs, [ :filename, :content_type, :metadata, :byte_size, :checksum, :service_name ]) |> validate_required([ :filename, :content_type, # :metadata, # :byte_size, # :checksum, :service_name ]) end def creation_changeset(storage_blob, attrs) do storage_blob |> cast(attrs, [ :filename, :content_type, :metadata, :byte_size, :checksum, :service_name ]) |> validate_required([ :filename, :content_type, :metadata, :byte_size, :checksum, :service_name ]) end # active storage ported methods def build_after_unfurling(blob, %{ # key: k, io: io, filename: filename, content_type: content_type, metadata: metadata, service_name: service_name, identify: identify # record: record }) do n = blob |> ActiveStorage.Blob.changeset(%{ # byte_size: byte_size, # checksum: checksum, content_type: content_type, filename: filename, # :key: key, metadata: metadata, service_name: service_name }) case n do %Ecto.Changeset{valid?: true} -> n |> unfurl(io, %{identify: identify}) %Ecto.Changeset{valid?: false} -> n end # new(key: key, filename: filename, content_type: content_type, metadata: metadata, service_name: service_name).tap do |blob| # blob.unfurl(io, identify: identify) # end end def create_after_unfurling!(blob, %{ # key: k, io: io, filename: filename, content_type: content_type, metadata: metadata, service_name: service_name, identify: identify # record: record }) do build_after_unfurling(blob, %{ # key: k, io: io, filename: filename, content_type: content_type, metadata: metadata, service_name: service_name, identify: identify }) |> Chaskiq.Repo.insert!() # changeset.tap(&:save!) end # Creates a new blob instance and then uploads the contents of # the given io to the service. The blob instance is going to # be saved before the upload begins to prevent the upload clobbering another due to key collisions. # When providing a content type, pass identify: false to bypass # automatic content type inference. def create_and_upload!(blob, %{ # key: k, io: io, filename: filename, content_type: content_type, metadata: metadata, service_name: service_name, identify: identify # record: record }) do blob = create_after_unfurling!(blob, %{ io: io, filename: filename, content_type: content_type, metadata: metadata, service_name: service_name, identify: identify }) blob |> upload_without_unfurling(io) # create_after_unfurling!(%{key: key, io: io, filename: filename, content_type: content_type, metadata: metadata, service_name: service_name, identify: identify}) .tap do |blob| # blob.upload_without_unfurling(io) # end end # Returns a saved blob _without_ uploading a file to the service. This blob will point to a key where there is # no file yet. It's intended to be used together with a client-side upload, which will first create the blob # in order to produce the signed URL for uploading. This signed URL points to the key generated by the blob. # Once the form using the direct upload is submitted, the blob can be associated with the right record using # the signed ID. def create_before_direct_upload!(blob, %{ key: _k, io: _io, filename: _filename, content_type: _content_type, metadata: _metadata, service_name: _service_name, identify: _identify, record: _record }) do # create! key: key, filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type, metadata: metadata, service_name: service_name end # Uploads the +io+ to the service on the +key+ for this blob. Blobs are intended to be immutable, so you shouldn't be # using this method after a file has already been uploaded to fit with a blob. If you want to create a derivative blob, # you should instead simply create a new blob based on the old one. # # Prior to uploading, we compute the checksum, which is sent to the service for transit integrity validation. If the # checksum does not match what the service receives, an exception will be raised. We also measure the size of the +io+ # and store that in +byte_size+ on the blob record. The content type is automatically extracted from the +io+ unless # you specify a +content_type+ and pass +identify+ as false. # # Normally, you do not have to call this method directly at all. Use the +create_and_upload!+ class method instead. # If you do use this method directly, make sure you are using it on a persisted Blob as otherwise another blob's # data might get overwritten on the service. def upload(blob, io, %{identify: true} = identify) do blob |> unfurl(io, identify) |> upload_without_unfurling(io) end # Deletes the files on the service associated with the blob. This should only be done if the blob is going to be # deleted as well or you will essentially have a dead reference. It's recommended to use #purge and #purge_later # methods in most circumstances. def delete(_blob) do # service.delete(key) # service.delete_prefixed("variants/#{key}/") if image? end # Destroys the blob record and then deletes the file on the service. This is the recommended way to dispose of unwanted # blobs. Note, though, that deleting the file off the service will initiate an HTTP connection to the service, which may # be slow or prevented, so you should not use this method inside a transaction or in callbacks. Use #purge_later instead. def purge(_blob) do # destroy # delete if previously_persisted? # rescue ActiveRecord::InvalidForeignKey end # Enqueues an ActiveStorage::PurgeJob to call #purge. This is the recommended way to purge blobs from a transaction, # an Active Record callback, or in any other real-time scenario. def purge_later do # ActiveStorage::PurgeJob.perform_later(self) end def service(blob) do # Application.get_env(:active_storage, :storage) # |> Keyword.get(blob.service_name |> String.to_existing_atom()) ActiveStorage.Service.Registry.fetch(blob.service_name) # services.fetch(blob.service_name) end # :nodoc: def unfurl(blob, io, _identify) do checksum = compute_checksum_in_chunks(blob, io) # ExImageInfo.seems?(io) <> = io bite_size = head data = %{ byte_size: bite_size, metadata: %{identified: true}, checksum: checksum } data = if blob.changes |> Map.has_key?(:content_type) != true do {mime, _w, _h, _} = ExImageInfo.info(io) content_type = mime data |> Map.merge(%{content_type: content_type}) else data end blob |> Ecto.Changeset.change(data) # content_type = extract_content_type(io) if content_type.nil? || identify # byte_size = io.size # identified = true end def compose(_blob, _keys) do # self.composed = true # service.compose(keys, key, **service_metadata) end def upload_without_unfurling(blob, io) do srv = blob |> service case srv.upload(blob, io) do {:ok, _response} -> blob {:error, err} -> require IEx IEx.pry() nil end # srv.upload(blob, "./README.md") # service.upload key, io, checksum: checksum, **service_metadata end def compute_checksum_in_chunks(_blob, io) do :crypto.hash(:md5, io) |> Base.encode64() # OpenSSL::Digest::MD5.new.tap do |checksum| # while chunk = io.read(5.megabytes) # checksum << chunk # end # io.rewind # end.base64digest end # Returns an ActiveStorage::Filename instance of the filename that can be # queried for basename, extension, and a sanitized version of the filename # that's safe to use in URLs. # def filename # ActiveStorage::Filename.new(self[:filename]) # end # Downloads the blob to a tempfile on disk. Yields the tempfile. # # The tempfile's name is prefixed with +ActiveStorage-+ and the blob's ID. Its extension matches that of the blob. # # By default, the tempfile is created in Dir.tmpdir. Pass +tmpdir:+ to create it in a different directory: # # blob.open(tmpdir: "/path/to/tmp") do |file| # # ... # end # # The tempfile is automatically closed and unlinked after the given block is executed. # # Raises ActiveStorage::IntegrityError if the downloaded data does not match the blob's checksum. # , tmpdir: nil, fn) do def open(blob, block) do [ext] = MIME.extensions(MIME.from_path(blob.filename)) name = ["ActiveStorage-#{blob.id}-", ".#{ext}"] service(blob).open(blob.id, %{checksum: blob.checksum, name: name}, block) # service.open blob.key, checksum: blob.checksum, # name: [ "ActiveStorage-#{id}-", blob.filename.extension_with_delimiter ], tmpdir: tmpdir, &block end def extract_content_type(_io) do # Marcel::MimeType.for io, name: filename.to_s, declared_type: content_type end def forcibly_serve_as_binary?(blob) do ActiveStorage.content_types_to_serve_as_binary() |> Enum.member?(blob.content_type) end def allowed_inline?(blob) do ActiveStorage.content_types_allowed_inline() |> Enum.member?(blob.content_type) end def web_image?(blob) do ActiveStorage.web_image_content_types() |> Enum.member?(blob.content_type) end # Returns true if the content_type of this blob is in the image range, like image/png. def image?(blob) do blob.content_type |> String.starts_with?("image") end # Returns true if the content_type of this blob is in the audio range, like audio/mpeg. def audio?(blob) do blob.content_type |> String.starts_with?("audio") end # Returns true if the content_type of this blob is in the video range, like video/mp4. def video?(blob) do blob.content_type |> String.starts_with?("video") end # Returns true if the content_type of this blob is in the text range, like text/plain. def text?(blob) do blob.content_type |> String.starts_with?("text") end end