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 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, metadata: options[:metadata], signer:).put(content)
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, metadata: options[:metadata], signer:).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 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_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 = {})

This can be used to see if the blob exist or obtain metada 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 host

def host
  "https://#{account_name}.blob.core.windows.net"
end

def initialize(account_name:, access_key:, container:, **options)

def initialize(account_name:, access_key:, container:, **options)
  @account_name = account_name
  @container = container
  @signer = !access_key.nil? && !access_key.empty?  ?
    AzureBlob::SharedKeySigner.new(account_name:, access_key:) :
    AzureBlob::EntraIdSigner.new(account_name:, **options.slice(:principal_id))
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, metadata: options[:metadata], signer:).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