<turbo-stream action="append" target="posts_list"><template>    <div class="postbit" id="295478" data-post-id="295478">
  <section>
    <div class="post-wrap">


					<div class="post-header">
		        <div class="user-avatar">
		          <img alt="emadalam" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/emadalam/120/29439_2.png" width="120" height="120" />
		        </div>
					
						<div class="user-details">
		          <div class="user-name">
		            <h3>
                  emadalam
                  </h3>
		          </div>
						
						</div>
					
					</div>

	        <div class="thread-main">
	            <div class="post-body" data-turbo="false">
								<p>I’ve been tinkering with this myself for a couple of days. I was able to create the presigned url that allowed me to upload to Backblaze directly, however for some reason the uploaded file was corrupted all the time when I tried accessing the file even when the file size and everything else seemed exactly the same.</p>
<ul>
<li>Use b2 command line to update the cors rule.</li>
</ul>
<pre data-code-wrap="elixir"><code class="lang-elixir">b2 update-bucket --corsRules '[          
  {                                        
      "corsRuleName": "downloadFromAnyOriginWithUpload", 
      "allowedOrigins": [
          "*"                                                          
      ],
      "allowedHeaders": [
          "*"
      ],
      "allowedOperations": [
          "s3_put", "s3_post", "s3_head", "s3_get"   
      ],
      "maxAgeSeconds": 3600
  }
]' your_bucket_name allPrivate
</code></pre>
<ul>
<li>Use <a href="https://hex.pm/packages/aws_signature" rel="nofollow">aws_signature</a> package for signing</li>
</ul>
<pre data-code-wrap="elixir"><code class="lang-elixir">  def presigned_upload(opts) do
    key = Keyword.fetch!(opts, :key)
    max_file_size = opts[:max_file_size] || 10_000_000
    expires_in = opts[:expires_in] || 7200
    content_type = MIME.from_path(key)

    uri = "https://s3.eu-central-003.backblazeb2.com/your_bucket_name/#{URI.encode(key)}"

    url = :aws_signature.sign_v4_query_params(
      b2_access_key_id,
      b2_application_key,
      "eu-central-003",
      "S3",
      :calendar.universal_time(),
      "PUT",
      uri,
      ttl: expires_in,
      uri_encode_path: false,
      body_digest: "UNSIGNED-PAYLOAD"
    )
    {:ok, url}
  end
</code></pre>
<ul>
<li>Use <code>PUT</code> in the js xhr upload</li>
</ul>
<pre data-code-wrap="js"><code class="lang-js">    const formData = new FormData()
    const {url} = entry.meta

    formData.append("file", entry.file)

    const xhr = new XMLHttpRequest()
    onViewError(() =&gt; xhr.abort())
    xhr.onload = () =&gt; {
      xhr.status &gt;= 200 &amp;&amp; xhr.status &lt; 300 ? entry.progress(100) : entry.error()
    }
    xhr.onerror = () =&gt; entry.error()
    xhr.upload.addEventListener("progress", (event) =&gt; {
      console.log(event)
      if(event.lengthComputable){
        let percent = Math.round((event.loaded / event.total) * 100)
        if(percent &lt; 100){ entry.progress(percent) }
      }
    })
    xhr.open("PUT", url, true)
    xhr.send(formData)
</code></pre>
<p>Trying the above did work for uploading from the liveview but as I said, the file itself in the storage bucket was corrupted. I gave up on this method after trying other permutations and combinations via different signing libraries like <a href="https://hexdocs.pm/ex_aws/ExAws.html" rel="noopener nofollow ugc">ex_aws</a>, which all worked for uploads with the same file corruption issue in the bucket. Ultimately I rolled my own signing solution and use the b2 native apis with Cloudflare Workers (which I anyways needed to take advantage of the unmetered download bandwidth for the CDN alliance between Backblaze and Cloudflare). This is what I’m doing currently.</p>
<ul>
<li>Have a shared secret key (on phoenix app and cloudflare worker) used for signing.</li>
<li>Use custom json as message for adding the details that needs to be verified for uploading</li>
<li>Sign using the hmac algorithm</li>
</ul>
<pre data-code-wrap="ex"><code class="lang-ex">  def presigned_upload_url(opts) do
    key = Keyword.fetch!(opts, :key)
    secret = "shared_secret"
    expires_at = DateTime.add(DateTime.utc_now(), 2, :hour) |&gt; DateTime.to_iso8601()

    message =
      Jason.encode!(%{
        "uid" =&gt; uid,
        "file" =&gt; key,
        "exp" =&gt; expires_at
        # add more details like content size etc which you can verify during upload
      })

    signature = :crypto.mac(:hmac, :sha256, secret, message) |&gt; Base.encode64()
    path = "#{signature}|#{message}" |&gt; Base.encode64()

    {:ok, "https://your_cloudflare_workers_location/file/#{path}"}
  end
</code></pre>
<ul>
<li>On cloudflare worker side
<ul>
<li>Receive the request and verify the signature using the same secret key</li>
<li>If the signature is verified proceed with upload/download using b2 native api</li>
<li>Save the response for b2 native api authorization in the Cloudflare KV for a day to save cost</li>
</ul>
</li>
</ul>
<pre data-code-wrap="js"><code class="lang-js">// signingKey.ts
export default async function signingKey(signingSecret: string) {
  return await crypto.subtle.importKey(
    "raw",
    new TextEncoder().encode(signingSecret),
    { name: "HMAC", hash: "SHA-256" },
    false,
    ["sign", "verify"]
  );
}
// ------------------

// verifySignature.ts
export default async function verifySignature(
  signingKey: CryptoKey,
  signature: string,
  message: string
) {
  const sigBuf = Uint8Array.from(atob(signature), (c) =&gt; c.charCodeAt(0));

  return crypto.subtle.verify(
    "HMAC",
    signingKey,
    sigBuf,
    new TextEncoder().encode(message)
  );
}
// ------------------

// formatPayload.ts
import { mapValues } from "lodash";

export type FormattedData = {
  uid: string | null;
  exp: Date | null;
  file: string;
  cdn: boolean;
};

export type FormattedPayloadResponse = {
  signature: string;
  message: string;
  data: FormattedData;
};

export default function formatPayload(
  payloadBase64: string
): FormattedPayloadResponse {
  const decodedPayload = atob(payloadBase64);
  const [signature, message] = decodedPayload.split(DELIMITER, 2);

  const data = formatData(JSON.parse(message));

  return { signature, message, data };
}

const DELIMITER = "|";

const DATA_FORMATTERS = {
  uid: (val: string) =&gt; val ?? null,
  exp: (val: string) =&gt; (val ? new Date(val) : null),
  file: (val: string) =&gt; val ?? null,
  cdn: (val: string | number) =&gt; (val === "0" || val === 0 ? false : !!val),
};

function formatData(data: Record&lt;string, string&gt;) {
  return mapValues&lt;typeof DATA_FORMATTERS, any&gt;(DATA_FORMATTERS, (fn, key) =&gt;
    fn(data[key])
  );
}
// ------------------

// isValidPayload.ts
import { get } from "lodash";
import { FormattedData, FormattedPayloadResponse } from "./formatPayload";

export default function isValidPayload(
  formattedPayload: FormattedPayloadResponse
) {
  const signature = get(formattedPayload, "signature");
  const message = get(formattedPayload, "message");
  const data = get(formattedPayload, "data");

  return !!signature &amp;&amp; !!message &amp;&amp; isValidData(data);
}

function isValidData(data: FormattedData) {
  const filePath = get(data, "file");
  const expiresAt = get(data, "exp");
  const isCdn = get(data, "cdn");

  const filePathParts = filePath.split("/");
  const filePathValidForCdn = isCdn ? filePathParts[1] === "public" : true;

  return (
    !!filePath &amp;&amp;
    filePathValidForCdn &amp;&amp;
    (!isCdn ? isExpiryDateValid(expiresAt) : true)
  );
}

function isExpiryDateValid(expiresAt: Date | null) {
  return !!expiresAt &amp;&amp; !isNaN(+expiresAt) &amp;&amp; expiresAt.valueOf() &gt;= Date.now();
}

// ------------------

// use something like Hono https://hono.dev/ to run a lightweight web server on the Cloudflare workers
// middleware/verifySignedRequest.ts
import { MiddlewareHandler } from "hono";
import { HTTPException } from "hono/http-exception";
import { Env } from "../env";

import { formatPayload, isValidPayload, signingKey, verifySignature } from "../utils";

export default function verifySignedRequest(
  pathParamName = "payload"
): MiddlewareHandler&lt;Env&gt; {
  return async (ctx, next) =&gt; {
    const payload = ctx.req.param(pathParamName);

    if (!payload) {
      throw new HTTPException(400, { message: "INVALID_REQUEST" });
    }

    try {
      const formattedPayload = formatPayload(payload);

      if (!isValidPayload(formattedPayload)) {
        throw new HTTPException(400, {
          message: "INVALID_REQUEST",
        });
      }

      const { signature, message, data } = formattedPayload;
      const key = await signingKey(ctx.env.SIGNING_SECRET);

      if (!verifySignature(key, signature, message)) {
        throw new HTTPException(401, {
          message: "INVALID_SIGNATURE",
        });
      }

      ctx.set("signedRequestData", data); // use this later for downloads or uploads as per the need
    } catch (error) {
      throw new HTTPException(400, { message: "INVALID_REQUEST" });
    }

    // request is valid, proceed and use for uploads
    await next();
  };
}

// for uploading
app.put("/file/:payload", async (c) =&gt; {
  const formData = await c.req.formData();
  const file = formData.get("file") as unknown as Blob;
  const signedRequestData = c.get("signedRequestData");

  const data = await b2.api.uploadFile({
    KV: c.env.KV,
    baseUrl: c.env.B2_API_URL,
    keyId: c.env.B2_KEY_ID,
    applicationKey: c.env.B2_APPLICATION_KEY,
    bucketId: c.env.B2_BUCKET_ID,
    file: file,
    fileName: signedRequestData.file,
  });

  return c.json(data); // or some formatted subset of data you want to expose
});
</code></pre>
<p>I have deployed it on Cloudflare workers and testing it for signed upload/download use cases from phoenix liveview and it’s been working perfectly. The best part is I’m able to use Cloudflare’s builtin caching for downloads via fetch api to completely bypass the backblaze’s server in most cases for a file access request. All of this is still WIP at my end but let me know if anyone wants to have a peek into my private repo, I can give you access for a while to see the complete setup for Cloudflare workers.</p> 
	            </div>

	            <div class="base-line">
	                <div class="thread-counters">
	                    <span class="thread-count count-likes js-likers-trigger" title="Likes" data-post-id="295478" data-batch-url="/posts/batch_likers">
                        3
                      </span>
                      <!-- <span class="thread-count js-solved-indicator" title="Marked as solution"></span> -->
	                </div>
	                <div class="go-to-post">
	                  <a title="Go to post" alt="Go to post" href="https://forum.elixirforum.com/t/backblaze-and-phoenix-liveview-uploads/57153/12">Post #11</a>
	                </div>
	            </div>
              <div id="likers-container-295478" 
                   class="likers-container"
                   data-first-post="false"
                   data-batch-url="/posts/batch_likers">
                   <div class="likers-placeholder" 
                     data-likers-post-id="295478"
                     data-batch-url="/posts/batch_likers">
                  <div class="post-likers"></div>
                </div>
              </div>
	        </div>
			

    </div>

    <div class="triangle-top-right type-standard-post cat-standard-post" title="Post #11"></div>
  </section>
</div>
    <div class="postbit" id="295492" data-post-id="295492">
  <section>
    <div class="post-wrap">


					<div class="post-header">
		        <div class="user-avatar">
		          <img alt="metadaddy" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/metadaddy/120/31282_2.png" width="120" height="120" />
		        </div>
					
						<div class="user-details">
		          <div class="user-name">
		            <h3>
                  metadaddy
                  </h3>
		          </div>
						
						</div>
					
					</div>

	        <div class="thread-main">
	            <div class="post-body" data-turbo="false">
								<p>Nice work with Cloudflare Workers!</p>
<p>BTW - when you use XHR to <code>PUT</code> a file at a presigned URL, you should send the raw file content, rather than wrapping it in a form. If you dispense with the form stuff and just do something like</p>
<pre data-code-wrap="js"><code class="lang-js">xhr.send(entry.file)
</code></pre>
<p>that approach should work.</p> 
	            </div>

	            <div class="base-line">
	                <div class="thread-counters">
	                    <span class="thread-count count-likes js-likers-trigger" title="Likes" data-post-id="295492" data-batch-url="/posts/batch_likers">
                        2
                      </span>
                      <!-- <span class="thread-count js-solved-indicator" title="Marked as solution"></span> -->
	                </div>
	                <div class="go-to-post">
	                  <a title="Go to post" alt="Go to post" href="https://forum.elixirforum.com/t/backblaze-and-phoenix-liveview-uploads/57153/13">Post #12</a>
	                </div>
	            </div>
              <div id="likers-container-295492" 
                   class="likers-container"
                   data-first-post="false"
                   data-batch-url="/posts/batch_likers">
                   <div class="likers-placeholder" 
                     data-likers-post-id="295492"
                     data-batch-url="/posts/batch_likers">
                  <div class="post-likers"></div>
                </div>
              </div>
	        </div>
			

    </div>

    <div class="triangle-top-right type-standard-post cat-standard-post" title="Post #12"></div>
  </section>
</div>
    <div class="postbit" id="295563" data-post-id="295563">
  <section>
    <div class="post-wrap">


					<div class="post-header">
		        <div class="user-avatar">
		          <img alt="emadalam" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/emadalam/120/29439_2.png" width="120" height="120" />
		        </div>
					
						<div class="user-details">
		          <div class="user-name">
		            <h3>
                  emadalam
                  </h3>
		          </div>
						
						</div>
					
					</div>

	        <div class="thread-main">
	            <div class="post-body" data-turbo="false">
								<p><a class="mention" href="/u/metadaddy" rel="nofollow">@metadaddy</a> This indeed was the missing piece, I tried to send the file directly and it worked like a charm, I confirm this <img src="https://forum.elixirforum.com/images/emoji/apple/+1.png?v=15" title=":+1:" class="emoji" alt=":+1:" loading="lazy" width="20" height="20"> I can now have this as a backup option if I need to support upload file size greater than 100MB which is the limit for Cloudflare Workers request body.</p>
<p><a class="mention" href="/u/maz" rel="nofollow">@maz</a> Can you try the steps that I posted for presigned url and just changing the xhr upload as <a class="mention" href="/u/metadaddy" rel="nofollow">@metadaddy</a> suggested, it worked for me.</p>
<p><a class="mention" href="/u/metadaddy" rel="nofollow">@metadaddy</a> since we have you as the SME on this, can you confirm a few things regarding the Backblaze S3 compatible presigned urls.</p>
<ol>
<li>Does it support verified SHA-1 digest if I provide it via the signed url?</li>
<li>Does it support enforcing policies like content type, content range etc which AWS S3’s presigned post url does? This is crucial to forbid any malicious users from using the presigned url to upload any arbitrary files that we do not want on our systems. Otherwise someone can easily abuse the presigned url to upload huge files with random file types in the bucket.</li>
</ol>
<p>For me, 1. is nice to have but not so important. However 2. is absolute essential to ensure the integrity of the system. If 2. is not supported, I’d need to stick to my custom implementation using Cloudflare Workers and use chunked upload if I need to support uploading files larger than 100MB size limit.</p> 
	            </div>

	            <div class="base-line">
	                <div class="thread-counters">
	                    <span class="thread-count count-likes js-likers-trigger" title="Likes" data-post-id="295563" data-batch-url="/posts/batch_likers">
                        2
                      </span>
                      <!-- <span class="thread-count js-solved-indicator" title="Marked as solution"></span> -->
	                </div>
	                <div class="go-to-post">
	                  <a title="Go to post" alt="Go to post" href="https://forum.elixirforum.com/t/backblaze-and-phoenix-liveview-uploads/57153/14">Post #13</a>
	                </div>
	            </div>
              <div id="likers-container-295563" 
                   class="likers-container"
                   data-first-post="false"
                   data-batch-url="/posts/batch_likers">
                   <div class="likers-placeholder" 
                     data-likers-post-id="295563"
                     data-batch-url="/posts/batch_likers">
                  <div class="post-likers"></div>
                </div>
              </div>
	        </div>
			

    </div>

    <div class="triangle-top-right type-standard-post cat-standard-post" title="Post #13"></div>
  </section>
</div>
    <div class="postbit" id="295577" data-post-id="295577">
  <section>
    <div class="post-wrap">


					<div class="post-header">
		        <div class="user-avatar">
		          <img alt="maz" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/maz/120/6075_2.png" width="120" height="120" />
		        </div>
					
						<div class="user-details">
		          <div class="user-name">
		            <h3>
                  maz
                    <span class="op-star" title="Thread Starter">
                      <img alt="OP" class="op-star-icon" src="/assets/thread-icons/thread-icon-thread-starter-df91e872.png" />
                    </span>
                  </h3>
		          </div>
						
						</div>
					
					</div>

	        <div class="thread-main">
	            <div class="post-body" data-turbo="false">
								<p>Is it possible to generate the presigned url like you did without the use of cloudflare workers?</p>
<p>We have cloudflare but I would like implement the generation of a presigned url(which I’ve yet to be successful with in conjunction with LiveView uploads. currently I am seeing a 403 Invalid Signature error using the signature generation code found at  <a href="https://gist.github.com/denvaar/66721b7a2f54f90592a509d29f57f831" class="inline-onebox" rel="noopener nofollow ugc">Dependency free presigned S3 links · GitHub</a>) that is decoupled from cloudflare workers.</p> 
	            </div>

	            <div class="base-line">
	                <div class="thread-counters">
	                    <span class="thread-count count-likes js-likers-trigger" title="Likes" data-post-id="295577" data-batch-url="/posts/batch_likers">
                        0
                      </span>
                      <!-- <span class="thread-count js-solved-indicator" title="Marked as solution"></span> -->
	                </div>
	                <div class="go-to-post">
	                  <a title="Go to post" alt="Go to post" href="https://forum.elixirforum.com/t/backblaze-and-phoenix-liveview-uploads/57153/15">Post #14</a>
	                </div>
	            </div>
              <div id="likers-container-295577" 
                   class="likers-container"
                   data-first-post="false"
                   data-batch-url="/posts/batch_likers">
                   <div class="likers-placeholder" 
                     data-likers-post-id="295577"
                     data-batch-url="/posts/batch_likers">
                  <div class="post-likers"></div>
                </div>
              </div>
	        </div>
			

    </div>

    <div class="triangle-top-right type-standard-post cat-standard-post" title="Post #14"></div>
  </section>
</div>
    <div class="postbit" id="295578" data-post-id="295578">
  <section>
    <div class="post-wrap">


					<div class="post-header">
		        <div class="user-avatar">
		          <img alt="emadalam" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/emadalam/120/29439_2.png" width="120" height="120" />
		        </div>
					
						<div class="user-details">
		          <div class="user-name">
		            <h3>
                  emadalam
                  </h3>
		          </div>
						
						</div>
					
					</div>

	        <div class="thread-main">
	            <div class="post-body" data-turbo="false">
								<p>Yes. Just use the steps I described in my original post for generating the presigned url and use the <code>PUT</code> for xhr and send the whole file entry instead of using FormData as <a class="mention" href="/u/metadaddy" rel="nofollow">@metadaddy</a> recommended. I was able to successfully upload and use the file in the bucket.</p> 
	            </div>

	            <div class="base-line">
	                <div class="thread-counters">
	                    <span class="thread-count count-likes js-likers-trigger" title="Likes" data-post-id="295578" data-batch-url="/posts/batch_likers">
                        0
                      </span>
                      <!-- <span class="thread-count js-solved-indicator" title="Marked as solution"></span> -->
	                </div>
	                <div class="go-to-post">
	                  <a title="Go to post" alt="Go to post" href="https://forum.elixirforum.com/t/backblaze-and-phoenix-liveview-uploads/57153/16">Post #15</a>
	                </div>
	            </div>
              <div id="likers-container-295578" 
                   class="likers-container"
                   data-first-post="false"
                   data-batch-url="/posts/batch_likers">
                   <div class="likers-placeholder" 
                     data-likers-post-id="295578"
                     data-batch-url="/posts/batch_likers">
                  <div class="post-likers"></div>
                </div>
              </div>
	        </div>
			

    </div>

    <div class="triangle-top-right type-standard-post cat-standard-post" title="Post #15"></div>
  </section>
</div>
    <div class="postbit" id="295761" data-post-id="295761">
  <section>
    <div class="post-wrap">


					<div class="post-header">
		        <div class="user-avatar">
		          <img alt="maz" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/maz/120/6075_2.png" width="120" height="120" />
		        </div>
					
						<div class="user-details">
		          <div class="user-name">
		            <h3>
                  maz
                    <span class="op-star" title="Thread Starter">
                      <img alt="OP" class="op-star-icon" src="/assets/thread-icons/thread-icon-thread-starter-df91e872.png" />
                    </span>
                  </h3>
		          </div>
						
						</div>
					
					</div>

	        <div class="thread-main">
	            <div class="post-body" data-turbo="false">
								<p>How do you refer to your external function in your LiveView? I’m getting a compile-time error with:</p>
<p><code>external: &amp;@upload_provider.emadalam_presigned_upload/1,</code></p>
<pre data-code-wrap="elixir"><code class="lang-elixir">@upload_provider Word.FileUploads.S3Backblaze

  def mount(_params, _session, socket) do
    socket =
      socket
      |&gt; assign(%{
        page_title: "Settings",
        changeset: User.profile_changeset(socket.assigns.current_user),
        uploaded_files: []
      })
      |&gt; allow_upload(:avatar,
        external: &amp;@upload_provider.emadalam_presigned_upload/1,
        accept: ~w(.jpg .jpeg .png .gif .svg .webp),
        max_entries: 1
      )

    {:ok, socket}
  end
</code></pre>
<p>from s3_backblaze.ex:</p>
<pre data-code-wrap="elixir"><code class="lang-elixir">  def emadalam_presigned_upload(opts) do
    key = Keyword.fetch!(opts, :key)
    max_file_size = opts[:max_file_size] || 10_000_000
    expires_in = opts[:expires_in] || 7200
    content_type = MIME.from_path(key)

    uri = "https://s3.us-east-005.backblazeb2.com/your_bucket_name/#{URI.encode(key)}"

    url =
      :aws_signature.sign_v4_query_params(
        "my_b2_access_key_id",
        "my_b2_application_key",
        "us-east-005",
        "S3",
        :calendar.universal_time(),
        "PUT",
        uri,
        ttl: expires_in,
        uri_encode_path: false,
        body_digest: "UNSIGNED-PAYLOAD"
      )

    {:ok, url}
  end
</code></pre>
<p>error:</p>
<pre data-code-wrap="elixir"><code class="lang-elixir">invalid :external value provided to allow_upload.

Only an anymous function receiving the socket as an argument is supported. Got:

&amp;Word.FileUploads.S3Backblaze.emadalam_presigned_upload/1```</code></pre> 
	            </div>

	            <div class="base-line">
	                <div class="thread-counters">
	                    <span class="thread-count count-likes js-likers-trigger" title="Likes" data-post-id="295761" data-batch-url="/posts/batch_likers">
                        0
                      </span>
                      <!-- <span class="thread-count js-solved-indicator" title="Marked as solution"></span> -->
	                </div>
	                <div class="go-to-post">
	                  <a title="Go to post" alt="Go to post" href="https://forum.elixirforum.com/t/backblaze-and-phoenix-liveview-uploads/57153/17">Post #16</a>
	                </div>
	            </div>
              <div id="likers-container-295761" 
                   class="likers-container"
                   data-first-post="false"
                   data-batch-url="/posts/batch_likers">
                   <div class="likers-placeholder" 
                     data-likers-post-id="295761"
                     data-batch-url="/posts/batch_likers">
                  <div class="post-likers"></div>
                </div>
              </div>
	        </div>
			

    </div>

    <div class="triangle-top-right type-standard-post cat-standard-post" title="Post #16"></div>
  </section>
</div>
    <div class="postbit" id="295765" data-post-id="295765">
  <section>
    <div class="post-wrap">


					<div class="post-header">
		        <div class="user-avatar">
		          <img alt="maz" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/maz/120/6075_2.png" width="120" height="120" />
		        </div>
					
						<div class="user-details">
		          <div class="user-name">
		            <h3>
                  maz
                    <span class="op-star" title="Thread Starter">
                      <img alt="OP" class="op-star-icon" src="/assets/thread-icons/thread-icon-thread-starter-df91e872.png" />
                    </span>
                  </h3>
		          </div>
						
						</div>
					
					</div>

	        <div class="thread-main">
	            <div class="post-body" data-turbo="false">
								<p>As a follow-up to the question I just asked(I fixed the compile time error by passing <code>socket</code> to the emadalam_presigned_upload() function. I would delete the post but I cannot seem to be able to.)</p>
<p>How are you populating the <code>opts</code> with the value for <code>:key</code>?</p>
<p>I am currently getting a runtime crash at:<br>
<code>key = Keyword.fetch!(opts, :key)</code></p>
<p><code>** (FunctionClauseError) no function clause matching in Keyword.fetch!/2     (elixir 1.15.2) lib/keyword.ex:592: Keyword.fetch!(%Phoenix.LiveView.UploadEntry{progress: 0, preflighted?: true, upload_config: :avatar, upload_ref: "phx-F3U8Eb4f02p5eQ3h", ref: "0", uuid: "21190387-7145-48de-9b95-3198cb091861", valid?: true, done?: false, cancelled?: false, client_name: "image.jpg", client_relative_path: "", client_size: 660164, client_type: "image/jpeg", client_last_modified: 1675744474060}, :key) </code></p>
<pre data-code-wrap="elixir"><code class="lang-elixir">  def emadalam_presigned_upload(opts, socket) do
    key = Keyword.fetch!(opts, :key)
    max_file_size = opts[:max_file_size] || 10_000_000
    expires_in = opts[:expires_in] || 7200
    content_type = MIME.from_path(key)

    uri = "https://s3.us-east-005.backblazeb2.com/bucket-name/#{URI.encode(key)}"

    url =
      :aws_signature.sign_v4_query_params(
        "secret",
        "secret2",
        "us-east-005",
        "S3",
        :calendar.universal_time(),
        "PUT",
        uri,
        ttl: expires_in,
        uri_encode_path: false,
        body_digest: "UNSIGNED-PAYLOAD"
      )

    {:ok, url}
  end
</code></pre> 
	            </div>

	            <div class="base-line">
	                <div class="thread-counters">
	                    <span class="thread-count count-likes js-likers-trigger" title="Likes" data-post-id="295765" data-batch-url="/posts/batch_likers">
                        0
                      </span>
                      <!-- <span class="thread-count js-solved-indicator" title="Marked as solution"></span> -->
	                </div>
	                <div class="go-to-post">
	                  <a title="Go to post" alt="Go to post" href="https://forum.elixirforum.com/t/backblaze-and-phoenix-liveview-uploads/57153/18">Post #17</a>
	                </div>
	            </div>
              <div id="likers-container-295765" 
                   class="likers-container"
                   data-first-post="false"
                   data-batch-url="/posts/batch_likers">
                   <div class="likers-placeholder" 
                     data-likers-post-id="295765"
                     data-batch-url="/posts/batch_likers">
                  <div class="post-likers"></div>
                </div>
              </div>
	        </div>
			

    </div>

    <div class="triangle-top-right type-standard-post cat-standard-post" title="Post #17"></div>
  </section>
</div>
    <div class="postbit" id="295772" data-post-id="295772">
  <section>
    <div class="post-wrap">


					<div class="post-header">
		        <div class="user-avatar">
		          <img alt="thomas.fortes" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/thomas.fortes/120/19404_2.png" width="120" height="120" />
		        </div>
					
						<div class="user-details">
		          <div class="user-name">
		            <h3>
                  thomas.fortes
                  </h3>
		          </div>
						
						</div>
					
					</div>

	        <div class="thread-main">
	            <div class="post-body" data-turbo="false">
								<aside class="quote no-group" data-username="maz" data-post="15" data-topic="57153">
<div class="title">
<div class="quote-controls"></div>
<img alt="" width="24" height="24" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/maz/48/6075_2.png" class="avatar"> maz:</div>
<blockquote>
<p>I would like implement the generation of a presigned url</p>
</blockquote>
</aside>
<p>Not dependency free, you need <code>ex_aws</code>, but should work.</p>
<p>In your <code>config.exs</code></p>
<pre data-code-wrap="elixir"><code class="lang-elixir">config :ex_aws,
  access_key_id: "your_access_key_id",
  secret_access_key: "your_access_key_secret",
  s3: [
    scheme: "https://",
    host: "your_host"
  ]
</code></pre>
<p>Elsewhere you could get the presigned url like this:</p>
<pre data-code-wrap="elixir"><code class="lang-elixir">    s3_config = ExAws.Config.new(:s3)
    {:ok, url} = ExAws.S3.presigned_url(s3_config, :put, bucket, key,
                      expires_in: 3600,
                      query_params: ["Content-Type": entry.client_type])
</code></pre> 
	            </div>

	            <div class="base-line">
	                <div class="thread-counters">
	                    <span class="thread-count count-likes js-likers-trigger" title="Likes" data-post-id="295772" data-batch-url="/posts/batch_likers">
                        0
                      </span>
                      <!-- <span class="thread-count js-solved-indicator" title="Marked as solution"></span> -->
	                </div>
	                <div class="go-to-post">
	                  <a title="Go to post" alt="Go to post" href="https://forum.elixirforum.com/t/backblaze-and-phoenix-liveview-uploads/57153/19">Post #18</a>
	                </div>
	            </div>
              <div id="likers-container-295772" 
                   class="likers-container"
                   data-first-post="false"
                   data-batch-url="/posts/batch_likers">
                   <div class="likers-placeholder" 
                     data-likers-post-id="295772"
                     data-batch-url="/posts/batch_likers">
                  <div class="post-likers"></div>
                </div>
              </div>
	        </div>
			

    </div>

    <div class="triangle-top-right type-standard-post cat-standard-post" title="Post #18"></div>
  </section>
</div>
    <div class="postbit" id="295786" data-post-id="295786">
  <section>
    <div class="post-wrap">


					<div class="post-header">
		        <div class="user-avatar">
		          <img alt="emadalam" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/emadalam/120/29439_2.png" width="120" height="120" />
		        </div>
					
						<div class="user-details">
		          <div class="user-name">
		            <h3>
                  emadalam
                  </h3>
		          </div>
						
						</div>
					
					</div>

	        <div class="thread-main">
	            <div class="post-body" data-turbo="false">
								<p>Here’s a minimal but complete working example for you.</p>
<pre data-code-wrap="elixir"><code class="lang-elixir"># Utility module to deal with S3 operations
defmodule MyAppWeb.S3 do
  def presigned_put(opts) do
    key = Keyword.fetch!(opts, :key)
    max_file_size = opts[:max_file_size] || 10_000_000
    expires_in = opts[:expires_in] || 7200

    uri = "https://s3.us-east-005.backblazeb2.com/bucket-name/#{key}"

    url =
      :aws_signature.sign_v4_query_params(
        "secret",
        "secret2",
        "us-east-005",
        "S3",
        :calendar.universal_time(),
        "PUT",
        uri,
        ttl: expires_in,
        uri_encode_path: false,
        body_digest: "UNSIGNED-PAYLOAD"
      )

    {:ok, url}
  end
end
</code></pre>
<pre data-code-wrap="elixir"><code class="lang-elixir"># Phoenix live upload
defmodule MyAppWeb.UploadLive do
  use MyAppWeb, :live_view

  @impl Phoenix.LiveView
  def mount(_params, _session, socket) do
    {:ok,
     socket
     |&gt; assign(:uploaded_files, [])
     |&gt; allow_upload(:avatar,
       max_file_size: 50_000_000,
       accept: ~w(.jpg .jpeg .png .gif .svg .webp),
       max_entries: 1,
       external: &amp;presign_upload/2
     )}
  end

  defp presign_upload(entry, socket) do
    uploads = socket.assigns.uploads
    key = "public/#{URI.encode(entry.client_name)}"

    {:ok, presigned_url} = MyAppWeb.S3.presigned_put(key: key)

    meta = %{uploader: "S3", key: key, url: presigned_url}
    {:ok, meta, socket}
  end

end
</code></pre>
<pre data-code-wrap="js"><code class="lang-js">// assets/js/app.js
...
...

const Uploaders = {}

Uploaders.S3 = function(entries, onViewError){
  entries.forEach(entry =&gt; {
    let {url} = entry.meta
    let xhr = new XMLHttpRequest()

    onViewError(() =&gt; xhr.abort())
    xhr.onload = () =&gt; {
      xhr.status &gt;= 200 &amp;&amp; xhr.status &lt; 300 ? entry.progress(100) : entry.error()
    }
    xhr.onerror = () =&gt; entry.error()
    xhr.upload.addEventListener("progress", (event) =&gt; {
      if(event.lengthComputable){
        let percent = Math.round((event.loaded / event.total) * 100)
        if(percent &lt; 100){ entry.progress(percent) }
      }
    })

    xhr.open("PUT", url, true)
    xhr.send(entry.file)
  })
}
...
...
let liveSocket = new LiveSocket("/live", Socket, {
  uploaders: Uploaders,
  params: {_csrf_token: csrfToken}
})
...
</code></pre> 
	            </div>

	            <div class="base-line">
	                <div class="thread-counters">
	                    <span class="thread-count count-likes js-likers-trigger" title="Likes" data-post-id="295786" data-batch-url="/posts/batch_likers">
                        4
                      </span>
                      <!-- <span class="thread-count js-solved-indicator" title="Marked as solution"></span> -->
	                </div>
	                <div class="go-to-post">
	                  <a title="Go to post" alt="Go to post" href="https://forum.elixirforum.com/t/backblaze-and-phoenix-liveview-uploads/57153/20">Post #19</a>
	                </div>
	            </div>
              <div id="likers-container-295786" 
                   class="likers-container"
                   data-first-post="false"
                   data-batch-url="/posts/batch_likers">
                   <div class="likers-placeholder" 
                     data-likers-post-id="295786"
                     data-batch-url="/posts/batch_likers">
                  <div class="post-likers"></div>
                </div>
              </div>
	        </div>
			

    </div>

    <div class="triangle-top-right type-solved cat-solved" title="Marked as solution"></div>
  </section>
</div>
    <div class="postbit" id="295965" data-post-id="295965">
  <section>
    <div class="post-wrap">


					<div class="post-header">
		        <div class="user-avatar">
		          <img alt="maz" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/maz/120/6075_2.png" width="120" height="120" />
		        </div>
					
						<div class="user-details">
		          <div class="user-name">
		            <h3>
                  maz
                    <span class="op-star" title="Thread Starter">
                      <img alt="OP" class="op-star-icon" src="/assets/thread-icons/thread-icon-thread-starter-df91e872.png" />
                    </span>
                  </h3>
		          </div>
						
						</div>
					
					</div>

	        <div class="thread-main">
	            <div class="post-body" data-turbo="false">
								<p>Upload to Backblaze via LiveView Uploads FTW. Thanks for all your help!</p>
<p>to help clarify for others, <code>my_b2_access_key_id</code> goes first, <code>my_b2_application_key</code> goes second</p>
<pre data-code-wrap="elixir"><code class="lang-elixir">      :aws_signature.sign_v4_query_params(
        "my_b2_access_key_id",
        "my_b2_application_key",
        "us-east-005",
</code></pre> 
	            </div>

	            <div class="base-line">
	                <div class="thread-counters">
	                    <span class="thread-count count-likes js-likers-trigger" title="Likes" data-post-id="295965" data-batch-url="/posts/batch_likers">
                        0
                      </span>
                      <!-- <span class="thread-count js-solved-indicator" title="Marked as solution"></span> -->
	                </div>
	                <div class="go-to-post">
	                  <a title="Go to post" alt="Go to post" href="https://forum.elixirforum.com/t/backblaze-and-phoenix-liveview-uploads/57153/22">Post #21</a>
	                </div>
	            </div>
              <div id="likers-container-295965" 
                   class="likers-container"
                   data-first-post="false"
                   data-batch-url="/posts/batch_likers">
                   <div class="likers-placeholder" 
                     data-likers-post-id="295965"
                     data-batch-url="/posts/batch_likers">
                  <div class="post-likers"></div>
                </div>
              </div>
	        </div>
			

    </div>

    <div class="triangle-top-right type-standard-post cat-standard-post" title="Post #21"></div>
  </section>
</div>
</template></turbo-stream><turbo-stream action="replace" target="load-more-container"><template><div id="load-more-container" class="load-more-container">
    <a class="load-more-button" data-turbo-stream="true" href="/topics/57153/load_more?page=3">Load more posts (1 remaining)</a>
</div></template></turbo-stream>