Magpie.Metadata (Magpie v0.5.1)

Copy Markdown View Source

Decodes the metadata objects of the files endpoints into structs.

Dropbox describes every entry in a user's Dropbox with one of three objects, told apart by a ".tag" key: "file", "folder" or "deleted". Magpie turns them into Magpie.FileMetadata, Magpie.FolderMetadata and Magpie.DeletedMetadata, with DateTime timestamps and a first-class content_hash, so callers pattern match on the struct instead of inspecting string keys:

client
|> Magpie.Files.ListFolder.stream("/Photos")
|> Enum.map(fn
  %Magpie.FileMetadata{name: name, size: size} -> {name, size}
  %Magpie.FolderMetadata{name: name} -> {name, :folder}
  %Magpie.DeletedMetadata{name: name} -> {name, :deleted}
end)

Every function in Magpie.Files, Magpie.Files.ListFolder and Magpie.Files.UploadSession that returns metadata decodes it before handing it back, so decode/1 is only needed when you call an endpoint yourself through Magpie.post/3:

{:ok, raw} = Magpie.post(client, "/files/get_metadata", %{"path" => "/a.txt"})
%Magpie.FileMetadata{} = Magpie.Metadata.decode(raw)

Forward compatibility

Decoding is lenient by design. A map whose ".tag" Magpie does not know (should Dropbox add a fourth kind of entry) is returned untouched, and so is anything that is not a map — a stubbed test response, or an error payload that reached the decoder. Fields Dropbox adds later that the structs do not carry are dropped; the raw payload is always available through Magpie.post/3.

Summary

Functions

Computes the Dropbox content_hash of data — a binary, or an enumerable of binaries such as File.stream!/2 — so a transfer can be verified against Magpie.FileMetadata.content_hash without another request.

Decodes one metadata object.

Decodes the "metadata" of every search match on a /files/search_v2 page. Dropbox wraps each match's entry in a one-variant union (%{".tag" => "metadata", "metadata" => ...}); Magpie flattens it so the match holds the struct directly.

Decodes every item under "entries" of a paginated page, leaving the rest of the page — "cursor", "has_more" — as it is, so the page still drives Magpie.Pager. kind is passed on to decode/2.

Decodes a response that wraps the metadata under a "metadata" key — /files/create_folder_v2, /files/delete_v2, /files/copy_v2 and /files/move_v2 all answer that way — returning just the struct.

Types

Functions

content_hash(data)

@spec content_hash(binary() | Enumerable.t()) :: String.t()

Computes the Dropbox content_hash of data — a binary, or an enumerable of binaries such as File.stream!/2 — so a transfer can be verified against Magpie.FileMetadata.content_hash without another request.

Dropbox hashes the content in 4 MiB blocks with SHA-256, concatenates the block digests and hashes the result once more (see the Content hash reference).

iex> Magpie.Metadata.content_hash("")
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"

{:ok, %{body: body}} = Magpie.Files.download(client, "/report.pdf")
{:ok, %Magpie.FileMetadata{content_hash: hash}} = Magpie.Files.get_metadata(client, "/report.pdf")
Magpie.Metadata.content_hash(body) == hash
# => true

decode(value, kind \\ :infer)

@spec decode(term(), :infer | :file | :folder) :: t() | term()

Decodes one metadata object.

The ".tag" picks the struct. Dropbox omits the tag when the endpoint's return type is known up front — /files/upload and /files/list_revisions always answer with files, /files/create_folder_v2 with a folder — and kind says what to expect then: :file, :folder, or :infer (the default), which treats a map with a "rev" as a file (every file has one, no folder does) and any other map with a "name" as a folder. Unknown tags and non-map values are returned as they are.

iex> Magpie.Metadata.decode(%{".tag" => "folder", "name" => "Photos", "id" => "id:1"})
%Magpie.FolderMetadata{name: "Photos", id: "id:1"}

iex> Magpie.Metadata.decode(%{"name" => "a.txt", "rev" => "015", "size" => 3})
%Magpie.FileMetadata{name: "a.txt", rev: "015", size: 3}

iex> Magpie.Metadata.decode(%{"name" => "a.txt"}, :file)
%Magpie.FileMetadata{name: "a.txt"}

iex> Magpie.Metadata.decode(%{".tag" => "hologram", "name" => "x"})
%{".tag" => "hologram", "name" => "x"}

decode_matches(page)

@spec decode_matches(term()) :: term()

Decodes the "metadata" of every search match on a /files/search_v2 page. Dropbox wraps each match's entry in a one-variant union (%{".tag" => "metadata", "metadata" => ...}); Magpie flattens it so the match holds the struct directly.

iex> page = %{"matches" => [%{"metadata" => %{".tag" => "metadata", "metadata" => %{".tag" => "file", "name" => "a", "rev" => "1"}}}]}
iex> Magpie.Metadata.decode_matches(page)
%{"matches" => [%{"metadata" => %Magpie.FileMetadata{name: "a", rev: "1"}}]}

decode_page(page, kind \\ :infer)

@spec decode_page(term(), :infer | :file | :folder) :: term()

Decodes every item under "entries" of a paginated page, leaving the rest of the page — "cursor", "has_more" — as it is, so the page still drives Magpie.Pager. kind is passed on to decode/2.

iex> Magpie.Metadata.decode_page(%{"entries" => [%{".tag" => "deleted", "name" => "x"}], "has_more" => false})
%{"entries" => [%Magpie.DeletedMetadata{name: "x"}], "has_more" => false}

unwrap(value, kind \\ :infer)

@spec unwrap(term(), :infer | :file | :folder) :: t() | term()

Decodes a response that wraps the metadata under a "metadata" key — /files/create_folder_v2, /files/delete_v2, /files/copy_v2 and /files/move_v2 all answer that way — returning just the struct.

kind is passed on to decode/2. Responses of any other shape are returned untouched.

iex> Magpie.Metadata.unwrap(%{"metadata" => %{".tag" => "file", "name" => "a", "rev" => "1"}})
%Magpie.FileMetadata{name: "a", rev: "1"}