Serves the files of a disk over HTTP: the URLs Fil.Plugin.URL signs, or with public: true, every file.
Signed URLs make direct downloads and uploads work on every disk: local and memory disks, which can't sign URLs themselves, and S3 disks whose files should go through your application.
In a Phoenix app, put it in the endpoint before Plug.Parsers, at the path the plugin's :base_url points to:
plug Fil.Plug, at: "/storage/uploads", disk: &MyApp.Storage.uploads/0The :base_url of the disk's Fil.Plugin.URL is then "http://localhost:4000/storage/uploads", and every
URL Fil.signed_url/3 builds for it is served here. To run plugs of your own first, such as authentication, use
a router instead (see In a router).
What it answers:
GETandHEADon a URL signed for:getreturn the file, with its stored content type or one guessed from the extensionPUTon a URL signed for:putwrites the request body, with the request'scontent-type, the same as a presigned PUT on S3. Plugins attached to the disk run as for any other write- a request that doesn't match its signature, or comes after the URL expired, gets a
403 - an upload larger than
:max_body_sizegets a413, and one whose body something else already read (see In a router) a400. Neither writes anything - a missing file gets a
404, and so does a file the storage denies access to, so a client can't tell which files exist - a failed write gets a
409if the file already exists,507if the storage is full and503if it's unavailable. Any other error is a500with a generic body, and its message goes to theLogger
Requests pass through untouched when the disk doesn't sign URLs with Fil.Plugin.URL (and the plug isn't public).
An S3 disk without it signs URLs that go to S3 directly, so the plug can stay in the endpoint when production uses
S3.
Public disks
With public: true, the plug serves downloads without a signature, like Plug.Static for a disk:
plug Fil.Plug, at: "/avatars", disk: &MyApp.Storage.avatars/0, public: trueGET /avatars/1.png then returns 1.png from the disk, on every adapter. There are no directory listings, and a
path can't leave the disk root. With Fil.Plugin.URL and base_url: "http://localhost:4000/avatars" on the disk,
Fil.url/2 builds these URLs. Uploads still need a signed URL, and without a :secret for Fil.Plugin.URL on the
disk, a PUT gets a 403.
In a router
forward runs the plug behind a router pipeline, for example to let only signed-in users download from a public
disk:
pipeline :storage do
plug :require_authenticated_user
end
scope "/storage" do
pipe_through :storage
forward "/avatars", Fil.Plug, disk: &MyApp.Storage.avatars/0, public: true
endforward removes /storage/avatars from the path, so the plug needs no :at. The :base_url of
Fil.Plugin.URL is still the full URL, "http://localhost:4000/storage/avatars", because signatures cover the
whole request path.
Two things in a Phoenix app get in the way of uploads there:
Plug.Parsersin the endpoint reads the bodies it has a parser for before the router runs (in a new Phoenix app JSON, form and multipart bodies). APUTwith one of those content types reaches the plug without its body and gets a400(the plug compares what it can read with thecontent-length). Other content types, such asimage/pngorapplication/pdf, pass through unread. Use the endpoint for uploads in any format.- the
:browserpipeline'sprotect_from_forgeryrejects aPUTwithout a CSRF token. Use a pipeline of your own, as above.
Needs Plug, an optional dependency of Fil.
Options
:disk- Required. The disk to serve: aFil.Disk, or a function or{module, function, args}that returns one. Use a function when the disk comes from runtime config, because plug options are compiled.:at(String.t/0) - The path to serve under, e.g."/storage/uploads". Requests outside it pass through untouched, which is how the plug sits in an endpoint. Without it, the plug serves every request it gets, as withforward.:public(boolean/0) - ServesGETandHEADwithout a signature, so every file on the disk can be downloaded by anyone who knows its path. That's where the URLs ofFil.url/2point. Uploads still need a URL signed byFil.Plugin.URL. The default value isfalse.:max_body_size(pos_integer/0) - The largest upload in bytes, 100 MiB by default. A largerPUTgets a413, and nothing is written. Uploads are read into memory whole before they're written, so keep it at what your server can hold. The default value is104857600.