How to create the content hash for a Dropbox file upload in Elixir
Dropbox has a specific way they want you to calculate the content hash for a file you are uploading. You can find the instructions here but to summarize:
- Split the file into blocks of 4 MB (4,194,304 or 4 * 1024 * 1024 bytes). The last block (if any) may be smaller than 4 MB.
- Compute the hash of each block using SHA-256.
- Concatenate the hash of all blocks in the binary format to form a single binary string.
- Compute the hash of the concatenated string using SHA-256. Output the resulting hash in hexadecimal format.
We can convert each of these steps into parts of a pipeline!
First we need to start reading in a file, split into chunks of the specified size. File.stream! is a good way of achieving this.
chunk_size = 4 * 1024 * 1024
File.stream!("my_file.txt", [], chunk_size)
Next, we need to create a new hash from each chunk. Iterating over the stream chunks with Enum.map and passing those chunks into the Erlang module :crypto.hash will get us our chunk hashes.
chunk_size = 4 * 1024 * 1024
File.stream!("my_file.txt", [], chunk_size)
|> Enum.map(&:crypto.hash(:sha256, &1))
After that, we'll combine all individual hashes into one big chunky string of hashes. Enum.join will let us that list of strings and slap them together
chunk_size = 4 * 1024 * 1024
File.stream!("my_file.txt", [], chunk_size)
|> Enum.map(&:crypto.hash(:sha256, &1))
|> Enum.join()
Getting warmer, time to take that big boy and get one final hash using :crypto.hash again. We'll have to wrap it inside an anonymous function since we need to pass the string in as the second argument.
chunk_size = 4 * 1024 * 1024
File.stream!("my_file.txt", [], chunk_size)
|> Enum.map(&:crypto.hash(:sha256, &1))
|> Enum.join()
|> (&:crypto.hash(:sha256, &1)).()
Home stretch! Lastily, base 16 encode the final hash using Base.encode16. Don't forget to lowercase it by passing in case: :lower
.
chunk_size = 4 * 1024 * 1024
File.stream!("my_file.txt", [], chunk_size)
|> Enum.map(&:crypto.hash(:sha256, &1))
|> Enum.join()
|> (&:crypto.hash(:sha256, &1)).()
|> Base.encode16(case: :lower)
There ya go! You should now be able to create the content hash for a file that you intend to upload to Dropbox.