struct Amber::Router::File

Overview

A file uploaded in a multipart request, held in a tempfile that Amber owns until the request finishes.

During the action, read the upload through #file, which opens the tempfile on first use, or through #open_tempfile. When the request finishes, Amber closes that handle and deletes the tempfile.

To keep an upload beyond the request, transfer it during the action:

# Move it to permanent storage; Amber no longer deletes it.
stored_path = params.files["avatar"].move_to("storage/avatars/#{user.id}.png")

# Or keep the tempfile where it is and take over deleting it, for
# example before handing the path to a background job.
ProcessUpload.new(params.files["report"].retain!).enqueue

Each method transfers ownership once: after #move_to or #retain!, Amber's cleanup leaves the path alone.

Defined in:

amber/router/file.cr

Constant Summary

Log = ::Log.for(self)
UPLOAD_DIRECTORY = ::File.join(Dir.tempdir, upload_directory_name)

The preferred upload directory: one per user, which every worker and every restart of the app reuses, so directories never pile up.

Constructors

Class Method Summary

Instance Method Summary

Constructor Detail

def self.new(upload, max_bytes : Int64 | Nil = nil) #

Copies the upload into a newly created tempfile. With max_bytes, a larger upload raises Amber::Exceptions::ParameterLimitExceeded; on any failure the tempfile is removed. #size is the number of bytes copied; the client's declared size is ignored.


[View source]

Class Method Detail

def self.retry_leftover_deletions : Nil #

Deletes the tempfiles discard could not, when their paths still name the files Amber created; Amber calls it when a request finishes. Files that still cannot be deleted are kept for the next call.


[View source]
def self.safe_extension(filename : String | Nil) : String #

Returns the extension of filename when it is a dot followed by one to sixteen ASCII letters or digits, and an empty string otherwise.


[View source]
def self.upload_directory : String #

Returns a directory only this user can enter, so no other local user can create or replace files in it: a real directory, owned by this user, with mode 0700.

It is UPLOAD_DIRECTORY, created when missing. If another user has claimed that path in a shared temporary directory, this process uses a new directory with an unpredictable name instead, and keeps using it.


[View source]

Instance Method Detail

def creation_time : Time | Nil #

[View source]
def delete_tempfile : Nil #

Amber's cleanup, run when the request finishes or its parse fails.

Closes the handle #file opened. When Amber still owns the tempfile, it deletes the path if, when checked, it still names the file Amber created (same device and inode, symlinks not followed), then gives up ownership. A failed delete raises and leaves ownership in place, so a later call can retry it. The check and the unlink are two system calls, so code running as this user that swaps the path between them can still lose the file it put there; the private upload directory keeps every other user out. To keep an upload, transfer it with #move_to or #retain!, which stop this cleanup entirely.

A handle that fails to close does not stop the deletion.


[View source]
def file : ::File #

A read handle on the upload, opened on first use and positioned at its start. Amber closes it when the request finishes. Raises IO::Error after #move_to, or when the path no longer names the tempfile.


[View source]
def filename : String | Nil #

[View source]
def headers : HTTP::Headers #

[View source]
def modification_time : Time | Nil #

[View source]
def move_to(destination : String | Path) : String #

Moves the upload to destination, replacing any file there, and returns that path. From then on the file belongs to the caller: Amber's cleanup leaves it alone, and #file raises.

Across file systems the upload is copied to a new file beside destination and renamed into place only after the copy succeeds; the tempfile is deleted only after that rename. If the copy fails, the partial copy is removed, destination is left as it was, and Amber still owns the tempfile.


[View source]
def open_tempfile(& : ::File -> T) : T forall T #

Opens the tempfile for reading after checking that its path still names the file Amber created, yields the handle, and closes it.


[View source]
def owned_by_amber? : Bool #

Whether Amber still owns the tempfile and will delete it when the request finishes.


[View source]
def path : String #

The path of the tempfile Amber created.


[View source]
def read_time : Time | Nil #

[View source]
def retain! : String #

Keeps the tempfile at #path after the request and returns that path. From then on the caller must delete it; Amber's cleanup only closes the handle that #file opened.


[View source]
def size : UInt64 | Nil #

[View source]