Refactor media upload
Use Upload Media logic from Pleroma Backend changes for picture upload Move AS <-> Model conversion to separate module Front changes Downgrade apollo-client: https://github.com/Akryum/vue-apollo/issues/577 Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
@@ -33,6 +33,7 @@ defmodule Mobilizon.Actors.Actor do
|
||||
alias Mobilizon.Users.User
|
||||
alias Mobilizon.Actors.{Actor, Follower, Member}
|
||||
alias Mobilizon.Events.{Event, FeedToken}
|
||||
alias Mobilizon.Media.File
|
||||
|
||||
alias MobilizonWeb.Router.Helpers, as: Routes
|
||||
alias MobilizonWeb.Endpoint
|
||||
@@ -62,8 +63,6 @@ defmodule Mobilizon.Actors.Actor do
|
||||
field(:openness, Mobilizon.Actors.ActorOpennessEnum, default: :moderated)
|
||||
field(:visibility, Mobilizon.Actors.ActorVisibilityEnum, default: :private)
|
||||
field(:suspended, :boolean, default: false)
|
||||
field(:avatar_url, :string)
|
||||
field(:banner_url, :string)
|
||||
# field(:openness, Mobilizon.Actors.ActorOpennessEnum, default: :moderated)
|
||||
has_many(:followers, Follower, foreign_key: :target_actor_id)
|
||||
has_many(:followings, Follower, foreign_key: :actor_id)
|
||||
@@ -71,6 +70,8 @@ defmodule Mobilizon.Actors.Actor do
|
||||
many_to_many(:memberships, Actor, join_through: Member)
|
||||
belongs_to(:user, User)
|
||||
has_many(:feed_tokens, FeedToken, foreign_key: :actor_id)
|
||||
embeds_one(:avatar, File)
|
||||
embeds_one(:banner, File)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
@@ -93,11 +94,11 @@ defmodule Mobilizon.Actors.Actor do
|
||||
:keys,
|
||||
:manually_approves_followers,
|
||||
:suspended,
|
||||
:avatar_url,
|
||||
:banner_url,
|
||||
:user_id
|
||||
])
|
||||
|> build_urls()
|
||||
|> cast_embed(:avatar)
|
||||
|> cast_embed(:banner)
|
||||
|> unique_username_validator()
|
||||
|> validate_required([:preferred_username, :keys, :suspended, :url])
|
||||
|> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index)
|
||||
@@ -119,10 +120,11 @@ defmodule Mobilizon.Actors.Actor do
|
||||
:suspended,
|
||||
:url,
|
||||
:type,
|
||||
:avatar_url,
|
||||
:user_id
|
||||
])
|
||||
|> build_urls()
|
||||
|> cast_embed(:avatar)
|
||||
|> cast_embed(:banner)
|
||||
# Needed because following constraint can't work for domain null values (local)
|
||||
|> unique_username_validator()
|
||||
|> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index)
|
||||
@@ -152,9 +154,7 @@ defmodule Mobilizon.Actors.Actor do
|
||||
:summary,
|
||||
:preferred_username,
|
||||
:keys,
|
||||
:manually_approves_followers,
|
||||
:avatar_url,
|
||||
:banner_url
|
||||
:manually_approves_followers
|
||||
])
|
||||
|> validate_required([
|
||||
:url,
|
||||
@@ -165,6 +165,8 @@ defmodule Mobilizon.Actors.Actor do
|
||||
:preferred_username,
|
||||
:keys
|
||||
])
|
||||
|> cast_embed(:avatar)
|
||||
|> cast_embed(:banner)
|
||||
# Needed because following constraint can't work for domain null values (local)
|
||||
|> unique_username_validator()
|
||||
|> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index)
|
||||
@@ -193,10 +195,10 @@ defmodule Mobilizon.Actors.Actor do
|
||||
:name,
|
||||
:domain,
|
||||
:summary,
|
||||
:preferred_username,
|
||||
:avatar_url,
|
||||
:banner_url
|
||||
:preferred_username
|
||||
])
|
||||
|> cast_embed(:avatar)
|
||||
|> cast_embed(:banner)
|
||||
|> build_urls(:Group)
|
||||
|> put_change(:domain, nil)
|
||||
|> put_change(:keys, Actors.create_keys())
|
||||
|
||||
@@ -11,7 +11,7 @@ defmodule Mobilizon.Actors do
|
||||
alias Mobilizon.Actors.{Actor, Bot, Member, Follower}
|
||||
|
||||
alias Mobilizon.Service.ActivityPub
|
||||
# import Exgravatar
|
||||
require Logger
|
||||
|
||||
@doc false
|
||||
def data() do
|
||||
@@ -57,9 +57,12 @@ defmodule Mobilizon.Actors do
|
||||
end
|
||||
|
||||
# Get actor by ID and preload organized events, followers and followings
|
||||
@spec get_actor_with_everything(integer()) :: Ecto.Query
|
||||
@spec get_actor_with_everything(integer()) :: Ecto.Query.t()
|
||||
defp do_get_actor_with_everything(id) do
|
||||
from(a in Actor, where: a.id == ^id, preload: [:organized_events, :followers, :followings])
|
||||
from(a in Actor,
|
||||
where: a.id == ^id,
|
||||
preload: [:organized_events, :followers, :followings]
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -239,24 +242,29 @@ defmodule Mobilizon.Actors do
|
||||
"""
|
||||
@spec insert_or_update_actor(map(), boolean()) :: {:ok, Actor.t()}
|
||||
def insert_or_update_actor(data, preload \\ false) do
|
||||
cs = Actor.remote_actor_creation(data)
|
||||
cs =
|
||||
data
|
||||
|> Actor.remote_actor_creation()
|
||||
|
||||
{:ok, actor} =
|
||||
Repo.insert(
|
||||
cs,
|
||||
on_conflict: [
|
||||
set: [
|
||||
keys: data.keys,
|
||||
avatar_url: data.avatar_url,
|
||||
banner_url: data.banner_url,
|
||||
name: data.name,
|
||||
summary: data.summary
|
||||
]
|
||||
],
|
||||
conflict_target: [:url]
|
||||
)
|
||||
|
||||
if preload, do: {:ok, Repo.preload(actor, [:followers])}, else: {:ok, actor}
|
||||
with {:ok, actor} <-
|
||||
Repo.insert(
|
||||
cs,
|
||||
on_conflict: [
|
||||
set: [
|
||||
keys: data.keys,
|
||||
name: data.name,
|
||||
summary: data.summary
|
||||
]
|
||||
],
|
||||
conflict_target: [:url]
|
||||
) do
|
||||
actor = if preload, do: Repo.preload(actor, [:followers]), else: actor
|
||||
{:ok, actor}
|
||||
else
|
||||
err ->
|
||||
Logger.error(inspect(err))
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
# def increase_event_count(%Actor{} = actor) do
|
||||
@@ -291,7 +299,8 @@ defmodule Mobilizon.Actors do
|
||||
{:error, :actor_not_found}
|
||||
|
||||
actor ->
|
||||
if preload, do: {:ok, Repo.preload(actor, [:followers])}, else: {:ok, actor}
|
||||
actor = if preload, do: Repo.preload(actor, [:followers]), else: actor
|
||||
{:ok, actor}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -371,7 +380,11 @@ defmodule Mobilizon.Actors do
|
||||
"""
|
||||
@spec get_local_actor_by_name(String.t()) :: Actor.t() | nil
|
||||
def get_local_actor_by_name(name) do
|
||||
Repo.one(from(a in Actor, where: a.preferred_username == ^name and is_nil(a.domain)))
|
||||
Repo.one(
|
||||
from(a in Actor,
|
||||
where: a.preferred_username == ^name and is_nil(a.domain)
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -435,6 +448,7 @@ defmodule Mobilizon.Actors do
|
||||
{:ok, actor}
|
||||
|
||||
_ ->
|
||||
Logger.error("Could not fetch by AP id")
|
||||
{:error, "Could not fetch by AP id"}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,6 +6,9 @@ defmodule Mobilizon.Application do
|
||||
import Cachex.Spec
|
||||
alias Mobilizon.Service.Export.{Feed, ICalendar}
|
||||
|
||||
@name Mix.Project.config()[:name]
|
||||
@version Mix.Project.config()[:version]
|
||||
|
||||
# See https://hexdocs.pm/elixir/Application.html
|
||||
# for more information on OTP Applications
|
||||
def start(_type, _args) do
|
||||
@@ -82,4 +85,13 @@ defmodule Mobilizon.Application do
|
||||
MobilizonWeb.Endpoint.config_change(changed, removed)
|
||||
:ok
|
||||
end
|
||||
|
||||
def named_version, do: @name <> " " <> @version
|
||||
|
||||
def user_agent do
|
||||
info =
|
||||
"#{MobilizonWeb.Endpoint.url()} <#{Mobilizon.CommonConfig.get([:instance, :email], "")}>"
|
||||
|
||||
named_version() <> "; " <> info
|
||||
end
|
||||
end
|
||||
|
||||
@@ -22,4 +22,45 @@ defmodule Mobilizon.CommonConfig do
|
||||
defp instance_config(), do: Application.get_env(:mobilizon, :instance)
|
||||
|
||||
defp to_bool(v), do: v == true or v == "true" or v == "True"
|
||||
|
||||
def get(key), do: get(key, nil)
|
||||
|
||||
def get([key], default), do: get(key, default)
|
||||
|
||||
def get([parent_key | keys], default) do
|
||||
case :mobilizon
|
||||
|> Application.get_env(parent_key)
|
||||
|> get_in(keys) do
|
||||
nil -> default
|
||||
any -> any
|
||||
end
|
||||
end
|
||||
|
||||
def get(key, default) do
|
||||
Application.get_env(:mobilizon, key, default)
|
||||
end
|
||||
|
||||
def get!(key) do
|
||||
value = get(key, nil)
|
||||
|
||||
if value == nil do
|
||||
raise("Missing configuration value: #{inspect(key)}")
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
def put([key], value), do: put(key, value)
|
||||
|
||||
def put([parent_key | keys], value) do
|
||||
parent =
|
||||
Application.get_env(:mobilizon, parent_key)
|
||||
|> put_in(keys, value)
|
||||
|
||||
Application.put_env(:mobilizon, parent_key, parent)
|
||||
end
|
||||
|
||||
def put(key, value) do
|
||||
Application.put_env(:mobilizon, key, value)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -35,6 +35,7 @@ defmodule Mobilizon.Events.Event do
|
||||
import Ecto.Changeset
|
||||
alias Mobilizon.Events.{Event, Participant, Tag, Session, Track}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Media.Picture
|
||||
alias Mobilizon.Addresses.Address
|
||||
|
||||
schema "events" do
|
||||
@@ -48,8 +49,6 @@ defmodule Mobilizon.Events.Event do
|
||||
field(:status, Mobilizon.Events.EventStatusEnum, default: :confirmed)
|
||||
field(:visibility, Mobilizon.Events.EventVisibilityEnum, default: :public)
|
||||
field(:join_options, Mobilizon.Events.JoinOptionsEnum, default: :free)
|
||||
field(:thumbnail, :string)
|
||||
field(:large_image, :string)
|
||||
field(:publish_at, :utc_datetime)
|
||||
field(:uuid, Ecto.UUID, default: Ecto.UUID.generate())
|
||||
field(:online_address, :string)
|
||||
@@ -62,6 +61,7 @@ defmodule Mobilizon.Events.Event do
|
||||
has_many(:tracks, Track)
|
||||
has_many(:sessions, Session)
|
||||
belongs_to(:physical_address, Address)
|
||||
belongs_to(:picture, Picture)
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
@@ -80,12 +80,11 @@ defmodule Mobilizon.Events.Event do
|
||||
:category,
|
||||
:status,
|
||||
:visibility,
|
||||
:thumbnail,
|
||||
:large_image,
|
||||
:publish_at,
|
||||
:online_address,
|
||||
:phone_address,
|
||||
:uuid
|
||||
:uuid,
|
||||
:picture_id
|
||||
])
|
||||
|> cast_assoc(:tags)
|
||||
|> cast_assoc(:physical_address)
|
||||
|
||||
@@ -178,7 +178,8 @@ defmodule Mobilizon.Events do
|
||||
:tracks,
|
||||
:tags,
|
||||
:participants,
|
||||
:physical_address
|
||||
:physical_address,
|
||||
:picture
|
||||
]
|
||||
)
|
||||
|> Repo.one()
|
||||
@@ -692,7 +693,7 @@ defmodule Mobilizon.Events do
|
||||
on: p.actor_id == a.id,
|
||||
on: p.event_id == e.id,
|
||||
where: a.id == ^id and p.role != ^:not_approved,
|
||||
preload: [:tags]
|
||||
preload: [:picture, :tags]
|
||||
)
|
||||
|> paginate(page, limit)
|
||||
)
|
||||
@@ -1239,11 +1240,7 @@ defmodule Mobilizon.Events do
|
||||
|
||||
"""
|
||||
def get_feed_token(token) do
|
||||
from(
|
||||
tk in FeedToken,
|
||||
where: tk.token == ^token,
|
||||
preload: [:actor, :user]
|
||||
)
|
||||
from(ftk in FeedToken, where: ftk.token == ^token, preload: [:actor, :user])
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
|
||||
115
lib/mobilizon/media.ex
Normal file
115
lib/mobilizon/media.ex
Normal file
@@ -0,0 +1,115 @@
|
||||
defmodule Mobilizon.Media do
|
||||
@moduledoc """
|
||||
The Media context.
|
||||
"""
|
||||
|
||||
import Ecto.Query, warn: false
|
||||
alias Mobilizon.Repo
|
||||
|
||||
alias Mobilizon.Media.Picture
|
||||
|
||||
@doc false
|
||||
def data() do
|
||||
Dataloader.Ecto.new(Mobilizon.Repo, query: &query/2)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def query(queryable, _params) do
|
||||
queryable
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single picture.
|
||||
|
||||
Raises `Ecto.NoResultsError` if the Picture does not exist.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_picture!(123)
|
||||
%Picture{}
|
||||
|
||||
iex> get_picture!(456)
|
||||
** (Ecto.NoResultsError)
|
||||
|
||||
"""
|
||||
def get_picture!(id), do: Repo.get!(Picture, id)
|
||||
|
||||
def get_picture(id), do: Repo.get(Picture, id)
|
||||
|
||||
@doc """
|
||||
Get a picture by it's URL
|
||||
"""
|
||||
@spec get_picture_by_url(String.t()) :: Picture.t() | nil
|
||||
def get_picture_by_url(url) do
|
||||
from(
|
||||
p in Picture,
|
||||
where: fragment("? @> ?", p.file, ~s|{"url": "#{url}"}|)
|
||||
)
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a picture.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> create_picture(%{field: value})
|
||||
{:ok, %Picture{}}
|
||||
|
||||
iex> create_picture(%{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def create_picture(attrs \\ %{}) do
|
||||
%Picture{}
|
||||
|> Picture.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a picture.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> update_picture(picture, %{field: new_value})
|
||||
{:ok, %Picture{}}
|
||||
|
||||
iex> update_picture(picture, %{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def update_picture(%Picture{} = picture, attrs) do
|
||||
picture
|
||||
|> Picture.changeset(attrs)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a Picture.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> delete_picture(picture)
|
||||
{:ok, %Picture{}}
|
||||
|
||||
iex> delete_picture(picture)
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def delete_picture(%Picture{} = picture) do
|
||||
Repo.delete(picture)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns an `%Ecto.Changeset{}` for tracking picture changes.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> change_picture(picture)
|
||||
%Ecto.Changeset{source: %Picture{}}
|
||||
|
||||
"""
|
||||
def change_picture(%Picture{} = picture) do
|
||||
Picture.changeset(picture, %{})
|
||||
end
|
||||
end
|
||||
22
lib/mobilizon/media/file.ex
Normal file
22
lib/mobilizon/media/file.ex
Normal file
@@ -0,0 +1,22 @@
|
||||
defmodule Mobilizon.Media.File do
|
||||
@moduledoc """
|
||||
Represents a file entity
|
||||
"""
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
embedded_schema do
|
||||
field(:name, :string)
|
||||
field(:url, :string)
|
||||
field(:content_type, :string)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(picture, attrs) do
|
||||
picture
|
||||
|> cast(attrs, [:name, :url, :content_type])
|
||||
|> validate_required([:name, :url])
|
||||
end
|
||||
end
|
||||
21
lib/mobilizon/media/picture.ex
Normal file
21
lib/mobilizon/media/picture.ex
Normal file
@@ -0,0 +1,21 @@
|
||||
defmodule Mobilizon.Media.Picture do
|
||||
@moduledoc """
|
||||
Represents a picture entity
|
||||
"""
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
alias Mobilizon.Media.File
|
||||
|
||||
schema "pictures" do
|
||||
embeds_one(:file, File, on_replace: :update)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(picture, attrs) do
|
||||
picture
|
||||
|> cast(attrs, [])
|
||||
|> cast_embed(:file)
|
||||
end
|
||||
end
|
||||
121
lib/mobilizon/mime.ex
Normal file
121
lib/mobilizon/mime.ex
Normal file
@@ -0,0 +1,121 @@
|
||||
# Portions of this file are derived from Pleroma:
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/mime.ex
|
||||
|
||||
defmodule Mobilizon.MIME do
|
||||
@moduledoc """
|
||||
Returns the mime-type of a binary and optionally a normalized file-name.
|
||||
"""
|
||||
@default "application/octet-stream"
|
||||
@read_bytes 35
|
||||
|
||||
@spec file_mime_type(String.t()) ::
|
||||
{:ok, content_type :: String.t(), filename :: String.t()} | {:error, any()} | :error
|
||||
def file_mime_type(path, filename) do
|
||||
with {:ok, content_type} <- file_mime_type(path),
|
||||
filename <- fix_extension(filename, content_type) do
|
||||
{:ok, content_type, filename}
|
||||
end
|
||||
end
|
||||
|
||||
@spec file_mime_type(String.t()) :: {:ok, String.t()} | {:error, any()} | :error
|
||||
def file_mime_type(filename) do
|
||||
File.open(filename, [:read], fn f ->
|
||||
check_mime_type(IO.binread(f, @read_bytes))
|
||||
end)
|
||||
end
|
||||
|
||||
def bin_mime_type(binary, filename) do
|
||||
with {:ok, content_type} <- bin_mime_type(binary),
|
||||
filename <- fix_extension(filename, content_type) do
|
||||
{:ok, content_type, filename}
|
||||
end
|
||||
end
|
||||
|
||||
@spec bin_mime_type(binary()) :: {:ok, String.t()} | :error
|
||||
def bin_mime_type(<<head::binary-size(@read_bytes), _::binary>>) do
|
||||
{:ok, check_mime_type(head)}
|
||||
end
|
||||
|
||||
def bin_mime_type(_), do: :error
|
||||
|
||||
def mime_type(<<_::binary>>), do: {:ok, @default}
|
||||
|
||||
defp fix_extension(filename, content_type) do
|
||||
parts = String.split(filename, ".")
|
||||
|
||||
new_filename =
|
||||
if length(parts) > 1 do
|
||||
Enum.drop(parts, -1) |> Enum.join(".")
|
||||
else
|
||||
Enum.join(parts)
|
||||
end
|
||||
|
||||
cond do
|
||||
content_type == "application/octet-stream" ->
|
||||
filename
|
||||
|
||||
ext = List.first(MIME.extensions(content_type)) ->
|
||||
new_filename <> "." <> ext
|
||||
|
||||
true ->
|
||||
Enum.join([new_filename, String.split(content_type, "/") |> List.last()], ".")
|
||||
end
|
||||
end
|
||||
|
||||
defp check_mime_type(<<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, _::binary>>) do
|
||||
"image/png"
|
||||
end
|
||||
|
||||
defp check_mime_type(<<0x47, 0x49, 0x46, 0x38, _, 0x61, _::binary>>) do
|
||||
"image/gif"
|
||||
end
|
||||
|
||||
defp check_mime_type(<<0xFF, 0xD8, 0xFF, _::binary>>) do
|
||||
"image/jpeg"
|
||||
end
|
||||
|
||||
defp check_mime_type(<<0x1A, 0x45, 0xDF, 0xA3, _::binary>>) do
|
||||
"video/webm"
|
||||
end
|
||||
|
||||
defp check_mime_type(<<0x00, 0x00, 0x00, _, 0x66, 0x74, 0x79, 0x70, _::binary>>) do
|
||||
"video/mp4"
|
||||
end
|
||||
|
||||
defp check_mime_type(<<0x49, 0x44, 0x33, _::binary>>) do
|
||||
"audio/mpeg"
|
||||
end
|
||||
|
||||
defp check_mime_type(<<255, 251, _, 68, 0, 0, 0, 0, _::binary>>) do
|
||||
"audio/mpeg"
|
||||
end
|
||||
|
||||
defp check_mime_type(
|
||||
<<0x4F, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00, _::size(160), 0x80, 0x74, 0x68, 0x65,
|
||||
0x6F, 0x72, 0x61, _::binary>>
|
||||
) do
|
||||
"video/ogg"
|
||||
end
|
||||
|
||||
defp check_mime_type(<<0x4F, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00, _::binary>>) do
|
||||
"audio/ogg"
|
||||
end
|
||||
|
||||
defp check_mime_type(<<"RIFF", _::binary-size(4), "WAVE", _::binary>>) do
|
||||
"audio/wav"
|
||||
end
|
||||
|
||||
defp check_mime_type(<<"RIFF", _::binary-size(4), "WEBP", _::binary>>) do
|
||||
"image/webp"
|
||||
end
|
||||
|
||||
defp check_mime_type(<<"RIFF", _::binary-size(4), "AVI.", _::binary>>) do
|
||||
"video/avi"
|
||||
end
|
||||
|
||||
defp check_mime_type(_) do
|
||||
@default
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user