Using File Storage

File Storage is an abstraction enabling different implementations of how and where the files are stored and providing a unified interface for accessing files and referring to them from the data model entities.

Jmix comes with two file storage implementations: Local and AWS. When you create a new project in Studio, it includes the local implementation.

You can store files of any size in the file storage, because transferring files to and from the file storage is performed by copying small chunks of data between input and output streams, so files are never fully loaded into memory.

Examples

Working with Files in UI

In this section, we will give an example of working with files in the file storage using the UI components.

Firstly, create an attribute of the FileRef type in your entity, for example:

@JmixEntity
@Entity
@Table(name = "ATTACHMENT")
public class Attachment {
    // ...

    @Column(name = "FILE_")
    private FileRef file;

    public FileRef getFile() {
        return file;
    }

    public void setFile(FileRef file) {
        this.file = file;
    }

When you run the application, Studio generates a database migration script for creating a corresponding column of the string type, because FileRef has a string representation in the URI format.

For uploading files from a screen of the user interface, use the FileStorageUploadField component bound to the entity attribute:

<data>
    <instance id="attachmentDc"
              class="files.ex1.entity.Attachment">
        <fetchPlan extends="_base"/>
        <loader/>
    </instance>
</data>
<layout spacing="true">
    <form dataContainer="attachmentDc">
        <column>
            <fileStorageUpload id="fileField" property="file"/>

To download attached files, add a custom column to the table in the browser screen:

<groupTable id="attachmentsTable"
            width="100%"
            dataContainer="attachmentsDc">
    <columns>
        <column id="fileName" caption="File"/>

And define the column generator:

@Autowired
private UiComponents uiComponents;
@Autowired
private Downloader downloader; (1)

@Install(to = "attachmentsTable.fileName", subject = "columnGenerator")
private Component attachmentsTableFileColumnGenerator(Attachment attachment) {
    if (attachment.getFile() != null) {
        LinkButton linkButton = uiComponents.create(LinkButton.class);
        linkButton.setAction(new BaseAction("download")
                .withCaption(attachment.getFile().getFileName())
                .withHandler(actionPerformedEvent ->
                        downloader.download(attachment.getFile()) (2)
                )
        );
        return linkButton;
    } else {
        return new Table.PlainTextCell("<empty>");
    }
}
1 Use the Downloader bean to download files.
2 The download() method accepts the FileRef value and takes the file from the file storage specified in the FileRef object. The name and type of the file are also encoded in FileRef, so the web browser correctly chooses whether to download or display the file.

Using FileStorage Interface

The following example shows how to work with the FileStorage interface directly.

The first method saves to file storage a file obtained from a web service. The second method loads a file from the file storage and saves it to the local file system.

The same Attachment entity is used as in the previous example.

@Autowired
private FileStorageLocator fileStorageLocator; (1)
@Autowired
private DataManager dataManager;

private void getAndSaveImage() {
    try {
        (2)
        URLConnection connection = new URL("https://picsum.photos/300").openConnection();
        try (InputStream responseStream = connection.getInputStream()) {
            (3)
            FileStorage fileStorage = fileStorageLocator.getDefault();
            FileRef fileRef = fileStorage.saveStream("photo.jpg", responseStream);
            (4)
            Attachment attachment = dataManager.create(Attachment.class);
            attachment.setFile(fileRef);
            dataManager.save(attachment);
        }
    } catch (IOException e) {
        throw new RuntimeException("Error getting image", e);
    }
}

private void saveToLocalFile(Attachment attachment, Path path) {
    FileStorage fileStorage = fileStorageLocator.getDefault();
    FileRef fileRef = attachment.getFile();
    (5)
    InputStream inputStream = fileStorage.openStream(fileRef);
    try {
        (6)
        Files.copy(inputStream, path.resolve(fileRef.getFileName()),
                StandardCopyOption.REPLACE_EXISTING);
    } catch (IOException e) {
        throw new RuntimeException("Error saving image", e);
    }
}
1 FileStorageLocator allows you to work with a particular file storage if you have defined multiple file storages in your project. If you have a single file storage (which is a default situation), you can inject the FileStorage interface directly.
2 Getting an input stream for the web resource. Instead of the URLConnection class, you may want to use HttpClient introduced in Java 11, or a third-party library like Apache HttpClient.
3 Saving the resource content to the file storage. The returned FileRef object is a reference to the file in the file storage.
4 Saving the reference to an entity attribute.
5 Getting an input stream to load the file from the file storage.
6 Saving the file to the local file system.

Local File Storage

The Local File Storage implementation allows you to store files on the local file system of the application server or on any network-attached storage (NAS).

To use Local File Storage in your application, make sure your build.gradle file contains the following line in the dependencies section:

implementation 'io.jmix.localfs:jmix-localfs-starter'

Files are stored in a special directory structure which is maintained by the file storage. By default, the root directory is ${user.dir}/.jmix/work/filestorage, where ${user.dir} is the user working directory (where JVM was started). You can change it by specifying a work directory in the jmix.core.work-dir application property or a complete path in the jmix.localfs.storage-dir application property, for example:

jmix.localfs.storage-dir = /opt/file-storage

AWS File Storage

The AWS File Storage implementation allows you to store files in Amazon S3.

To use AWS File Storage in your application, install the AWS File Storage add-on from the marketplace as described in the Add-ons section, or manually add the following line to the dependencies section of your build.gradle file:

implementation 'io.jmix.awsfs:jmix-awsfs-starter'

If you plan to use AWS File Storage only, remove the Local File Storage dependency from build.gradle. Otherwise, see the next section for how to configure multiple file storages.

Define application properties:

jmix.awsfs.access-key = <access key ID>
jmix.awsfs.secret-access-key = <secret access key>
jmix.awsfs.region = <AWS region, for example eu-north-1>
jmix.awsfs.bucket = <S3 bucket name>
jmix.awsfs.chunk-size = <S3 multipart upload chunk size in KB, default is 8192>
jmix.awsfs.endpoint-url = <optional endpoint URL for S3-compatible cloud storages>

Using Multiple File Storages

If you want to use more than one file storage in your application, specify a default one using its name in the application property:

jmix.core.default-file-storage = fs

The Local File Storage name is fs, the AWS File Storage name is s3.

If you want to use multiple storages of the same type, define additional storages with unique names. For example, to define an additional Local File Storage with the root directory at /var/tmp/myfs, add the following code to the main application class:

@Bean
FileStorage myFileStorage() {
    return new LocalFileStorage("myfs", "/var/tmp/myfs");
}

To work with different file storages programmatically, use the FileStorageLocator bean. It allows you to get a file storage by its name.

The FileStorageUploadField component has the fileStorage attribute to specify a file storage name. If it’s not set, the component uses the default file storage.