class AzureBlob::Client

through an instance of this class.
AzureBlob Client class. You interact with the Azure Blob api

def append_blob_block(key, content, options = {})

The checksum must be the checksum of the block not the blob.
Will ensure integrity of the upload. The checksum must be a base64 digest. Can be produced with +OpenSSL::Digest::MD5.base64digest+.
[+:content_md5+]

Options:

Calls to {Append Block}[https://learn.microsoft.com/en-us/rest/api/storageservices/append-block]

Append a block to an Append Blob
def append_blob_block(key, content, options = {})
  uri = generate_uri("#{container}/#{key}")
  uri.query = URI.encode_www_form(comp: "appendblock")
  headers = {
    "Content-Length": content.size,
    "Content-Type": options[:content_type],
    "Content-MD5": options[:content_md5],
  }
  Http.new(uri, headers, signer:).put(content)
end

def blob_exist?(key, options = {})

Calls to {Get Blob Properties}[https://learn.microsoft.com/en-us/rest/api/storageservices/get-blob-properties]

Returns a boolean indicating if the blob exists.
def blob_exist?(key, options = {})
  get_blob_properties(key, options).present?
rescue AzureBlob::Http::FileNotFoundError
  false
end

def commit_blob_blocks(key, block_ids, options = {})

Add a checksum for each block if you want Azure to validate integrity.
This is the checksum for the whole blob. The checksum is saved on the blob, but it is not validated!
[+:content_md5+]

Options:

Takes a key (path) and an array of block ids

Calls to {Put Block List}[https://learn.microsoft.com/en-us/rest/api/storageservices/put-block-list]

Commits the list of blocks to a blob.
def commit_blob_blocks(key, block_ids, options = {})
  block_list = BlockList.new(block_ids)
  content = block_list.to_s
  uri = generate_uri("#{container}/#{key}")
  uri.query = URI.encode_www_form(comp: "blocklist")
  headers = {
    "Content-Length": content.size,
    "Content-Type": options[:content_type],
    "x-ms-blob-content-md5": options[:content_md5],
    "x-ms-blob-content-disposition": options[:content_disposition],
  }
  Http.new(uri, headers, signer:, **options.slice(:metadata, :tags)).put(content)
end

def container_exist?(options = {})

Calls to {Get Container Properties}[https://learn.microsoft.com/en-us/rest/api/storageservices/get-container-properties]

Returns a boolean indicating if the container exists.
def container_exist?(options = {})
  get_container_properties(options = {}).present?
end

def create_append_blob(key, options = {})

Will be saved on the blob in Azure.
[+:content_disposition+]
Will be saved on the blob in Azure.
[+:content_type+]

Options:
You are expected to append blocks to the blob with append_blob_block after creating the blob.

Calls to {Put Blob}[https://learn.microsoft.com/en-us/rest/api/storageservices/put-blob]

Creates a Blob of type append.
def create_append_blob(key, options = {})
  uri = generate_uri("#{container}/#{key}")
  headers = {
    "x-ms-blob-type": "AppendBlob",
    "Content-Length": 0,
    "Content-Type": options[:content_type],
    "Content-MD5": options[:content_md5],
    "x-ms-blob-content-disposition": options[:content_disposition],
  }
  Http.new(uri, headers, signer:, **options.slice(:metadata, :tags)).put(nil)
end

def create_block_blob(key, content, options = {})

Block size in bytes, can be used to force the method to split the upload in smaller chunk. Defaults to +AzureBlob::DEFAULT_BLOCK_SIZE+ and cannot be bigger than +AzureBlob::MAX_UPLOAD_SIZE+
[+:block_size+]
a checksum for each block, then commit the blocks with commit_blob_blocks.
The checksum is only checked on a single upload! To verify checksum when uploading multiple blocks, call directly put_blob_block with
Will ensure integrity of the upload. The checksum must be a base64 digest. Can be produced with +OpenSSL::Digest::MD5.base64digest+.
[+:content_md5+]
Will be saved on the blob in Azure.
[+:content_disposition+]
Will be saved on the blob in Azure.
[+:content_type+]

Options:

Takes a key (path), the content (String or IO object), and options.

followed by a {Put Block List}[https://learn.microsoft.com/en-us/rest/api/storageservices/put-block-list] to commit the block list.
If the blob is too big, the blob is split in blocks sent through a series of {Put Block}[https://learn.microsoft.com/en-us/rest/api/storageservices/put-block] requests

When the blob is small enough this method will send the blob through {Put Blob}[https://learn.microsoft.com/en-us/rest/api/storageservices/put-blob]

Create a blob of type block. Will automatically split the the blob in multiple block and send the blob in pieces (blocks) if the blob is too big.
def create_block_blob(key, content, options = {})
  if content.size > (options[:block_size] || DEFAULT_BLOCK_SIZE)
    put_blob_multiple(key, content, **options)
  else
    put_blob_single(key, content, **options)
  end
end

def create_container(options = {})

Calls to {Create Container}[https://learn.microsoft.com/en-us/rest/api/storageservices/create-container]

Create the container
def create_container(options = {})
  uri = generate_uri(container)
  headers = {}
  headers[:"x-ms-blob-public-access"] = "blob" if options[:public_access]
  headers[:"x-ms-blob-public-access"] = options[:public_access] if ["container","blob"].include?(options[:public_access])
  uri.query = URI.encode_www_form(restype: "container")
  response = Http.new(uri, headers, signer:).put
end

def delete_blob(key, options = {})

Sets the value of the x-ms-delete-snapshots header. Default to +include+
[+:delete_snapshots+]
Options:

Takes a key (path) and options.

Calls to {Delete Blob}[https://learn.microsoft.com/en-us/rest/api/storageservices/delete-blob]

Delete a blob
def delete_blob(key, options = {})
  uri = generate_uri("#{container}/#{key}")
  headers = {
    "x-ms-delete-snapshots": options[:delete_snapshots] || "include",
  }
  Http.new(uri, headers, signer:).delete
end

def delete_container(options = {})

Calls to {Delete Container}[https://learn.microsoft.com/en-us/rest/api/storageservices/delete-container]

Delete the container
def delete_container(options = {})
  uri = generate_uri(container)
  uri.query = URI.encode_www_form(restype: "container")
  response = Http.new(uri, signer:).delete
end

def delete_prefix(prefix, options = {})

Look delete_blob for the list of options.

Takes a prefix and options

followed to a series of calls to {Delete Blob}[https://learn.microsoft.com/en-us/rest/api/storageservices/delete-blob]
Calls to {List blobs}[https://learn.microsoft.com/en-us/rest/api/storageservices/list-blobs]

Delete all blobs prefixed by the given prefix.
def delete_prefix(prefix, options = {})
  results = list_blobs(prefix:)
  results.each { |key| delete_blob(key) }
end

def generate_block_id(index)

def generate_block_id(index)
  Base64.urlsafe_encode64(index.to_s.rjust(6, "0"))
end

def generate_uri(path)

Example: +generate_uri("#{container}/#{key}")+

Return a URI object to a resource in the container. Takes a path.
def generate_uri(path)
  URI.parse(URI::DEFAULT_PARSER.escape(File.join(host, path)))
end

def get_blob(key, options = {})

Ending point in bytes
[+:end+]
Starting point in bytes
[+:start+]

Options:

Takes a key (path) and options.

Calls to the {Get Blob}[https://learn.microsoft.com/en-us/rest/api/storageservices/get-blob] endpoint.

Returns the full or partial content of the blob
def get_blob(key, options = {})
  uri = generate_uri("#{container}/#{key}")
  headers = {
    "x-ms-range": options[:start] && "bytes=#{options[:start]}-#{options[:end]}",
  }
  Http.new(uri, headers, signer:).get
end

def get_blob_properties(key, options = {})

To check for blob presence, look for `blob_exist?` as `get_blob_properties` raises on missing blob.
This can be used to obtain metadata such as content type, disposition, checksum or Azure custom metadata.

Calls to {Get Blob Properties}[https://learn.microsoft.com/en-us/rest/api/storageservices/get-blob-properties]

Returns a Blob object without the content.
def get_blob_properties(key, options = {})
  uri = generate_uri("#{container}/#{key}")
  response = Http.new(uri, signer:).head
  Blob.new(response)
end

def get_blob_tags(key)

Returns a hash of the blob's tags.

Takes a key (path) of the blob.

Calls to the {Get Blob Tags}[https://learn.microsoft.com/en-us/rest/api/storageservices/get-blob-tags] endpoint.

Returns the tags associated with a blob
def get_blob_tags(key)
  uri = generate_uri("#{container}/#{key}?comp=tags")
  response = Http.new(uri, signer:).get
  Tags.from_response(response).to_h
end

def get_container_properties(options = {})

This can be used to see if the container exist or obtain metadata.

Calls to {Get Container Properties}[https://learn.microsoft.com/en-us/rest/api/storageservices/get-container-properties]

Returns a Container object.
def get_container_properties(options = {})
  uri = generate_uri(container)
  uri.query = URI.encode_www_form(restype: "container")
  response = Http.new(uri, signer:, raise_on_error: false).head
  Container.new(response)
end

def host

def host
  @host ||= "https://#{account_name}.blob.#{CLOUD_REGIONS_SUFFIX[cloud_regions]}"
end

def initialize(account_name:, access_key: nil, principal_id: nil, container:, host: nil, **options)

def initialize(account_name:, access_key: nil, principal_id: nil, container:, host: nil, **options)
  @account_name = account_name
  @container = container
  @host = host
  @cloud_regions = options[:cloud_regions]&.to_sym || :global
  @access_key = access_key
  @principal_id = principal_id
  @use_managed_identities = options[:use_managed_identities]
  signer unless options[:lazy]
end

def list_blobs(options = {})

Maximum number of results to return per page.
[:+max_results+]
Prefix of the blobs to be listed. Defaults to listing everything in the container.
[+:prefix+]
Options:

Calls to {List blobs}[https://learn.microsoft.com/en-us/rest/api/storageservices/list-blobs]

Returns a BlobList containing a list of keys (paths)
def list_blobs(options = {})
  uri = generate_uri(container)
  query = {
    comp: "list",
    restype: "container",
    prefix: options[:prefix].to_s.gsub(/\\/, "/"),
  }
  query[:maxresults] = options[:max_results] if options[:max_results]
  uri.query = URI.encode_www_form(**query)
  fetcher = ->(marker) do
    query[:marker] = marker
    query.reject! { |key, value| value.to_s.empty? }
    uri.query = URI.encode_www_form(**query)
    response = Http.new(uri, signer:).get
  end
  BlobList.new(fetcher)
end

def put_blob_block(key, index, content, options = {})

Must be the checksum for the block not the blob. The checksum must be a base64 digest. Can be produced with +OpenSSL::Digest::MD5.base64digest+.
[+:content_md5+]

Options:

Returns the id of the block. Required to commit the list of blocks to a blob.

Calls to {Put Block}[https://learn.microsoft.com/en-us/rest/api/storageservices/put-block]

Uploads a block to a blob.
def put_blob_block(key, index, content, options = {})
  block_id = generate_block_id(index)
  uri = generate_uri("#{container}/#{key}")
  uri.query = URI.encode_www_form(comp: "block", blockid: block_id)
  headers = {
    "Content-Length": content.size,
    "Content-Type": options[:content_type],
    "Content-MD5": options[:content_md5],
  }
  Http.new(uri, headers, signer:).put(content)
  block_id
end

def put_blob_multiple(key, content, options = {})

def put_blob_multiple(key, content, options = {})
  content = StringIO.new(content) if content.is_a? String
  block_size = options[:block_size] || DEFAULT_BLOCK_SIZE
  block_count = (content.size.to_f / block_size).ceil
  block_ids = block_count.times.map do |i|
    put_blob_block(key, i, content.read(block_size))
  end
  commit_blob_blocks(key, block_ids, options)
end

def put_blob_single(key, content, options = {})

def put_blob_single(key, content, options = {})
  content = StringIO.new(content) if content.is_a? String
  uri = generate_uri("#{container}/#{key}")
  headers = {
    "x-ms-blob-type": "BlockBlob",
    "Content-Length": content.size,
    "Content-Type": options[:content_type],
    "x-ms-blob-content-md5": options[:content_md5],
    "x-ms-blob-content-disposition": options[:content_disposition],
  }
  Http.new(uri, headers, signer:, **options.slice(:metadata, :tags)).put(content.read)
end

def signed_uri(key, permissions:, expiry:, **options)

- options
- expiry as a UTC iso8601 time string
- A permission string (+"r"+, +"rw"+)
- key (path)
Takes a

Returns an SAS signed URI
def signed_uri(key, permissions:, expiry:, **options)
  uri = generate_uri("#{container}/#{key}")
  uri.query = signer.sas_token(uri, permissions:, expiry:, **options)
  uri
end

def signer

def signer
  @signer ||=
    begin
      no_access_key = access_key.nil? || access_key&.empty?
      using_managed_identities = no_access_key && !principal_id.nil? || use_managed_identities
      if !using_managed_identities && no_access_key
        raise AzureBlob::Error.new(
          "`access_key` cannot be empty. To use managed identities instead, pass a `principal_id` or set `use_managed_identities` to true."
        )
      end
      using_managed_identities ?
        AzureBlob::EntraIdSigner.new(account_name:, host:, principal_id:) :
        AzureBlob::SharedKeySigner.new(account_name:, access_key:, host:)
    end
end