# Typora

I'm a big fan of the [Typora](https://typora.io) markdown editor, as it has a nice WYSIWYG experience. They also support [custom image uploads](https://support.typora.io/Upload-Image/) to a storage backend of your choice.

This sample here is a small uploader CLI for Azure bblob storage. Essentially, you need to set a `TYPORA_IMAGE_UPLOAD_AZURE_CONNECTION` environment variable, and ensure you have a container named `typoraimages`.

```csharp
namespace TyporaUploaderAzure
{
    using System;
    using System.IO;
    using System.Linq;
    using System.Security.Cryptography;
    using System.Threading.Tasks;
    using Azure.Storage.Blobs.Models;
    using SimpleBase;

    class Program
    {
        // https://support.typora.io/Upload-Image/
        static async Task Main(string[] args)
        {
            var connectionString = Environment.GetEnvironmentVariable("TYPORA_IMAGE_UPLOAD_AZURE_CONNECTION");
            var serviceClient = new Azure.Storage.Blobs.BlobServiceClient(connectionString: connectionString);
            var containerClient = serviceClient.GetBlobContainerClient(blobContainerName: "typoraimages");
            // await containerClient.CreateIfNotExistsAsync(Azure.Storage.Blobs.Models.PublicAccessType.Blob);

            var prefix = DateTime.Now.ToString("yyyy/MM/dd/HH/mm");

            var tasks = args.Select(async filename =>
            {
                var fi = new FileInfo(filename);
                var bytes = await File.ReadAllBytesAsync(path: fi.FullName);
                using var hashAlgo = MD5.Create();
                using var reader = fi.OpenRead();

                var hash = hashAlgo.ComputeHash(bytes);
                var hashBase32 = Base32.Crockford.Encode(hash);
                var fileWithoutExtension = fi.Name.Substring(0, fi.Name.Length - fi.Extension.Length);
                var blobName = $"{prefix}/{fileWithoutExtension}----{hashBase32}{fi.Extension}";
                var blobClient = containerClient.GetBlobClient(blobName);

                string mimeType(string extension) => extension switch
                {
                    ".png" => "image/png",
                    ".jpeg" => "image/jpeg",
                    ".jpg" => "image/jpeg",
                    _ => "application/octet-stream",
                };

                if (!await blobClient.ExistsAsync())
                {
                    using var ms = new MemoryStream(bytes);
                    await blobClient.UploadAsync(ms);

                    var headers = new BlobHttpHeaders
                    {
                        ContentType = mimeType(fi.Extension),
                        ContentHash = hash,
                        CacheControl = "max-age=31536000",
                    };

                    await blobClient.SetHttpHeadersAsync(headers);
                }

                return blobClient.Uri.AbsoluteUri;
            });

            await Task.WhenAll(tasks);

            tasks
                .Select(t => t.Result)
                .ToList()
                .ForEach(Console.WriteLine);
        }
    }
}
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://cookbook.geuer-pollmann.de/productivity/typora-azure-uploads.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
