FileManager.java

/*
 * The coLAB project
 * Copyright (C) 2021-2023 AlbaSim, MEI, HEIG-VD, HES-SO
 *
 * Licensed under the MIT License
 */
package ch.colabproject.colab.api.controller.document;

import ch.colabproject.colab.api.controller.project.ProjectManager;
import ch.colabproject.colab.api.model.document.DocumentFile;
import ch.colabproject.colab.api.model.project.Project;
import ch.colabproject.colab.api.persistence.jcr.JcrManager;
import ch.colabproject.colab.api.setup.ColabConfiguration;
import ch.colabproject.colab.generator.model.exceptions.HttpErrorMessage;
import java.io.BufferedInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import javax.ejb.LocalBean;
import javax.ejb.Stateless;
import javax.inject.Inject;
import javax.jcr.RepositoryException;
import javax.ws.rs.core.MediaType;
import org.apache.commons.lang3.tuple.ImmutableTriple;
import org.apache.hc.core5.net.URIBuilder;
import org.glassfish.jersey.media.multipart.FormDataBodyPart;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Handles DocumentFiles instances both DB and JCR persistence
 *
 * @author xaviergood
 */
@LocalBean
@Stateless
public class FileManager {

    /**
     * Logger
     */
    private static final Logger logger = LoggerFactory.getLogger(FileManager.class);

    /**
     * File persistence management
     */
    @Inject
    private JcrManager jcrManager;

    /**
     * Document specific logic management
     */
    @Inject
    private DocumentManager documentManager;

    /**
     * Project specific logic management
     */
    @Inject
    private ProjectManager projectManager;

    /**
     * Update an existing document's file content
     *
     * @param docId    document id
     * @param fileSize size of the file in bytes
     * @param file     file contents
     * @param body     body of the request, containing all meta
     *
     * @throws RepositoryException in case of a JCR issue
     */
    public void updateOrCreateFile(
        Long docId,
        Long fileSize,
        InputStream file,
        FormDataBodyPart body)
        throws RepositoryException {
        FormDataContentDisposition details = body.getFormDataContentDisposition();

        // char-set black magic
        var fileNameBytes = details.getFileName().getBytes(StandardCharsets.ISO_8859_1);
        var fileName = new String(fileNameBytes, StandardCharsets.UTF_8);

        FileManager.logger.debug("Updating file {} with id {}", fileName, docId);

        DocumentFile colabDocFile = null;
        try {
            colabDocFile = documentManager.assertAndGetDocumentFile(docId);
        } catch (HttpErrorMessage hem) {
            throw HttpErrorMessage.notFound();
        }

        // Check file size limit
        if (fileSize > ColabConfiguration.getJcrRepositoryFileSizeLimit()) {
            FileManager.logger.debug("File exceeds authorized size ({} bytes)"
                + ", size limit is {} bytes",
                fileSize, ColabConfiguration.getJcrRepositoryFileSizeLimit());

            throw HttpErrorMessage.fileSizeLimitExceededError();
        }

        // Check quota limit
        Project project = colabDocFile.getProject();
        if (project != null) {
            var usedQuota = getUsage(project.getId());
            if (usedQuota + fileSize > getQuota()) {
                FileManager.logger.debug("Quota exceeded. Used : {}, Authorized : {}",
                    usedQuota + fileSize, ColabConfiguration.getJcrRepositoryProjectQuota());

                throw HttpErrorMessage.projectQuotaExceededError();
            }
        }

        colabDocFile.setFileName(fileName);
        colabDocFile.setFileSize(fileSize);
        colabDocFile.setMimeType(body.getMediaType().toString());

        this.jcrManager.updateOrCreateFile(project, docId, file);
    }

    /**
     * Update or create the file for the given document
     *
     * @param documentId  document id
     * @param fileContent file content
     *
     * @throws RepositoryException in case of JCR problem
     */
    public void updateOrCreateFile(Long documentId, InputStream fileContent)
        throws RepositoryException {
        DocumentFile colabDocFile = null;
        try {
            colabDocFile = documentManager.assertAndGetDocumentFile(documentId);
        } catch (HttpErrorMessage hem) {
            throw HttpErrorMessage.notFound();
        }

        Project project = colabDocFile.getProject();

        this.jcrManager.updateOrCreateFile(project, documentId, fileContent);
    }

    /**
     * Delete the file contents, and resets size and mime type
     *
     * @param docId id of document
     *
     * @throws RepositoryException in case of a JCR issue
     */
    public void deleteFile(Long docId) throws RepositoryException {
        DocumentFile colabDocFile = null;
        try {
            colabDocFile = documentManager.assertAndGetDocumentFile(docId);
        } catch (HttpErrorMessage hem) {
            throw HttpErrorMessage.notFound();
        }

        Project project = colabDocFile.getProject();
        FileManager.logger.debug("Deleting file '{}' with id {}", colabDocFile.getFileName(),
            colabDocFile.getId());

        colabDocFile.setFileName(null);
        colabDocFile.setFileSize(0L);
        colabDocFile.setMimeType(MediaType.APPLICATION_OCTET_STREAM);

        this.jcrManager.deleteFile(project, docId);

    }

    /**
     * Tells if a file exists for the given identifier
     *
     * @param documentId id of the requested document
     *
     * @return true if there is a corresponding file
     *
     * @throws RepositoryException in case of a JCR issue
     */
    public boolean hasFile(Long documentId) throws RepositoryException {
        DocumentFile colabDocFile = null;
        try {
            colabDocFile = documentManager.assertAndGetDocumentFile(documentId);
        } catch (HttpErrorMessage hem) {
            throw HttpErrorMessage.notFound();
        }

        Project project = colabDocFile.getProject();

        return jcrManager.nodeExists(project, documentId);

    }

    /**
     * Retrieves the file content.
     *
     * @param documentId id of the requested document
     *
     * @return a stream to the file contents
     *
     * @throws RepositoryException in case of a JCR issue
     */
    public InputStream getFileStream(Long documentId) throws RepositoryException {
        DocumentFile colabDocFile = null;
        try {
            colabDocFile = documentManager.assertAndGetDocumentFile(documentId);
        } catch (HttpErrorMessage hem) {
            throw HttpErrorMessage.notFound();
        }

        Project project = colabDocFile.getProject();

        return new BufferedInputStream(this.jcrManager.getFileStream(project, documentId));
    }

    /**
     * Encode path as URI component
     *
     * @param path path to encode
     *
     * @return URI encoded path
     */
    public String encodePath(final String path) {
        if (path == null || path.length() == 0) {
            return "";
        } else {
            return new URIBuilder().setPath(path).toString();
        }
    }

    /**
     * Builds a well formatted response with a stream to the content and correct content headers
     *
     * @param documentId document id
     *
     * @return a triplet containing in order : a stream to the file (empty stream if no file), the
     *         file name in UTF-8 or an empty string if no file has been uploaded, the media type of
     *         the file or MediaType.APPLICATION_OCTET_STREAM if no file present
     *
     * @throws RepositoryException in case of a JCR issue
     */
    public ImmutableTriple<BufferedInputStream, String, MediaType> getDownloadFileInfo(
        Long documentId) throws RepositoryException {
        DocumentFile colabDocFile = null;
        try {
            colabDocFile = documentManager.assertAndGetDocumentFile(documentId);
        } catch (HttpErrorMessage hem) {
            throw HttpErrorMessage.notFound();
        }

        Project project = colabDocFile.getProject();

        var stream = new BufferedInputStream(this.jcrManager.getFileStream(project, documentId));

        var fileName = colabDocFile.getFileName();
        String safeFileName = "";
        if (fileName != null) {
            safeFileName = this.encodePath(fileName);
        }

        MediaType mediaType = MediaType.valueOf(colabDocFile.getMimeType());

        return new ImmutableTriple<>(stream, safeFileName, mediaType);
    }

    /**
     * Gets projects quota
     *
     * @return the quota of disk space usage for files per project in bytes
     */
    public static Long getQuota() {
        return ColabConfiguration.getJcrRepositoryProjectQuota();
    }

    /**
     * Computes the current disk space usage of a given project
     *
     * @param projectId project id
     *
     * @return used space in bytes
     *
     * @throws RepositoryException in case of a JCR issue
     */
    public Long getUsage(Long projectId) throws RepositoryException {
        Project project = projectManager.assertAndGetProject(projectId);
        return jcrManager.computeMemoryUsage(project);
    }
}