models.Attachment.java Source code

Java tutorial

Introduction

Here is the source code for models.Attachment.java

Source

/**
 * Yobi, Project Hosting SW
 *
 * Copyright 2012 NAVER Corp.
 * http://yobi.io
 *
 * @Author Yi EungJun
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package models;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Date;
import java.util.Formatter;
import java.util.List;
import java.util.concurrent.TimeUnit;

import javax.persistence.*;

import controllers.AttachmentApp;
import models.resource.GlobalResource;
import models.resource.Resource;
import models.resource.ResourceConvertible;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.tika.Tika;

import models.enumeration.ResourceType;

import org.apache.tika.metadata.Metadata;
import org.apache.tika.mime.MediaType;
import play.data.validation.*;

import play.db.ebean.Model;
import play.libs.Akka;
import scala.concurrent.duration.Duration;
import scalax.file.NotDirectoryException;
import utils.FileUtil;
import utils.JodaDateUtil;

@Entity
public class Attachment extends Model implements ResourceConvertible {
    private static final long serialVersionUID = 7856282252495067924L;
    public static final Finder<Long, Attachment> find = new Finder<>(Long.class, Attachment.class);
    public static final int NOTHING_TO_ATTACH = 0;
    private static String uploadDirectory = "uploads";
    @Id
    public Long id;

    @Constraints.Required
    public String name;

    @Constraints.Required
    public String hash;

    @Enumerated(EnumType.STRING)
    public ResourceType containerType;

    public String mimeType;
    public Long size;
    public String containerId;

    private Date createdDate;

    /**
     * Finds an attachment which matches the given one.
     *
     * Finds an attachment that matches {@link Attachment#name},
     * {@link Attachment#hash}, {@link Attachment#containerType} and
     * {@link Attachment#containerId} with the given one.
     *
     * @param attach
     * @return an attachment which matches up with the given one.
     */
    private static Attachment findBy(Attachment attach) {
        return find.where().eq("name", attach.name).eq("hash", attach.hash)
                .eq("containerType", attach.containerType).eq("containerId", attach.containerId).findUnique();
    }

    /**
     * @param hash
     * @return true if an attachment which has the given hash exists
     */
    public static boolean exists(String hash) {
        return find.where().eq("hash", hash).findRowCount() > 0;
    }

    /**
     * Gets all attachments from a container.
     *
     * @param containerType  the resource type of the container
     * @param containerId    the resource id of the container
     * @return attachments of the container
     */
    public static List<Attachment> findByContainer(ResourceType containerType, String containerId) {
        return find.where().eq("containerType", containerType).eq("containerId", containerId).findList();
    }

    /**
     * Gets all attachments from a container.
     *
     * @param container
     * @return attachments of the container
     */
    public static List<Attachment> findByContainer(Resource container) {
        return findByContainer(container.getType(), container.getId());
    }

    /**
     * @param container
     * @return the number of attachments in the container
     */
    public static int countByContainer(Resource container) {
        return find.where().eq("containerType", container.getType()).eq("containerId", container.getId())
                .findRowCount();
    }

    /**
     * Moves all attachments from a container to another container.
     *
     * This method is used when move attachments which were attached to an user
     * temporary to a specific resource(issue, posting, ...).
     *
     * @param from  a container in which the attachment is currently stored
     * @param to    a container to which the attachment moved
     * @return      the number of attachments which was moved to another
     *              container
     */
    public static int moveAll(Resource from, Resource to) {
        List<Attachment> attachments = Attachment.findByContainer(from);
        for (Attachment attachment : attachments) {
            attachment.moveTo(to);
        }
        return attachments.size();
    }

    /**
     * Moves specified attachments from a container to another one.
     *
     * This method is used when move attachments which were attached to an user
     * temporary to a specific resource(issue, posting, ...).
     *
     * @param from             a container to which it was attached
     * @param to               a container to which it will be attached
     * @param selectedFileIds  IDs of attachments to be moved
     * @return the number of attachments which was moved to another container
     */
    public static int moveOnlySelected(Resource from, Resource to, String[] selectedFileIds) {
        if (selectedFileIds.length == 0) {
            return NOTHING_TO_ATTACH;
        }
        List<Attachment> attachments = Attachment.find.where().idIn(Arrays.asList(selectedFileIds)).findList();
        for (Attachment attachment : attachments) {
            if (attachment.containerId.equals(from.getId()) && attachment.containerType == from.getType()) {
                attachment.moveTo(to);
            }
        }
        return attachments.size();
    }

    /**
     * Moves this attachment to another resource.
     *
     * @param to  the destination
     */
    public void moveTo(Resource to) {
        containerType = to.getType();
        containerId = to.getId();
        update();
    }

    /**
     * Moves a file to the Upload Directory.
     *
     * This method is used to move a file stored in temporary directory by
     * PlayFramework to the Upload Directory managed by Yobi.
     *
     * @param file
     * @return SHA1 hash of the file
     * @throws NoSuchAlgorithmException
     * @throws IOException
     */
    private static String moveFileIntoUploadDirectory(File file) throws NoSuchAlgorithmException, IOException {
        // Compute sha1 checksum.
        MessageDigest algorithm = MessageDigest.getInstance("SHA1");
        byte buf[] = new byte[10240];
        FileInputStream fis = new FileInputStream(file);
        for (int size = 0; size >= 0; size = fis.read(buf)) {
            algorithm.update(buf, 0, size);
        }
        Formatter formatter = new Formatter();
        for (byte b : algorithm.digest()) {
            formatter.format("%02x", b);
        }
        String hash = formatter.toString();
        formatter.close();
        fis.close();

        // Store the file.
        // Before do that, create upload directory if it doesn't exist.
        File uploads = new File(uploadDirectory);
        uploads.mkdirs();
        if (!uploads.isDirectory()) {
            throw new NotDirectoryException("'" + file.getAbsolutePath() + "' is not a directory.");
        }
        File attachedFile = new File(uploadDirectory, hash);
        boolean isMoved = file.renameTo(attachedFile);

        if (!isMoved) {
            FileUtils.copyFile(file, attachedFile);
            file.delete();
        }

        // Close all resources.

        return hash;
    }

    /**
     * Attaches an uploaded file to the given container with the given name.
     *
     * Moves an uploaded file to the Upload Directory and rename the file to
     * its SHA1 hash. And it stores the metadata of the file in this entity.
     *
     * If there is an entity that has the same values with this entity already,
     * it means the container has the same attachment. If that is the case,
     * this method will return {@code false} and do nothing; otherwise, return
     * {@code true}.
     *
     * This method is used when an uploaded file is attached to a user or
     * another resource directly.
     *
     * @param file       a file to be attached
     * @param name       the name of the file
     * @param container  the resource to which the file attached
     * @return {@code true} if the file is attached, {@code false} otherwise.
     * @throws IOException
     * @throws NoSuchAlgorithmException
     */
    @Transient
    public boolean store(File file, String name, Resource container) throws IOException, NoSuchAlgorithmException {
        // Store the file as its SHA1 hash in filesystem, and record its
        // metadata - containerType, containerId, size and hash - in Database.
        this.containerType = container.getType();
        this.containerId = container.getId();
        this.createdDate = JodaDateUtil.now();

        if (name == null) {
            this.name = file.getName();
        } else {
            this.name = name;
        }

        if (this.mimeType == null) {
            this.mimeType = FileUtil.detectMediaType(file, name).toString();
        }

        // the size must be set before it is moved.
        this.size = file.length();
        this.hash = Attachment.moveFileIntoUploadDirectory(file);

        // Add the attachment into the Database only if there is no same record.
        Attachment sameAttach = Attachment.findBy(this);
        if (sameAttach == null) {
            super.save();
            return true;
        } else {
            this.id = sameAttach.id;
            return false;
        }
    }

    /**
     * Gets a file which mathces the hash from the Upload Directory.
     *
     * This method is used when an user downloads a file
     *
     * @return the file
     */
    public File getFile() {
        return new File(uploadDirectory, this.hash);
    }

    /**
     * Sets the Upload Directory to store files that users uploaded.
     *
     * This method is used for unit tests.
     *
     * @param path  a path to the Upload Directory
     */
    public static void setUploadDirectory(String path) {
        uploadDirectory = path;
    }

    /**
     * Checks if there is a file that has the same hash in the Upload Directory.
     *
     * This method is used to check if the file exists in the system.
     *
     * @param hash
     * @return true if the file exists
     */
    public static boolean fileExists(String hash) {
        return new File(uploadDirectory, hash).isFile();
    }

    /**
     * Deletes this file.
     *
     * This method is used when an user delete an attachment or its container.
     */
    @Override
    public void delete() {
        super.delete();
        // FIXME: Rarely this may delete a file which is still referred by
        // attachment, if new attachment is added after checking nonexistence
        // of an attachment refers the file and before deleting the file.
        //
        // But synchronization with Attachment class may be a bad idea to solve
        // the problem. If you do that, blocking of project deletion causes
        // that all requests to attachments (even a user avatars you can see in
        // most of pages) are blocked.
        if (!exists(this.hash)) {
            try {
                Files.delete(Paths.get(uploadDirectory, this.hash));
            } catch (Exception e) {
                play.Logger.error("Failed to delete: " + this, e);
            }
        }
    }

    /**
     * Deletes every attachment attached to the given container.
     *
     * This method is used when a container, a resource may has attachments, is
     * deleted.
     *
     * @param container  the resource that has the attachments to be deleted
     */
    public static void deleteAll(Resource container) {
        List<Attachment> attachments = findByContainer(container);
        for (Attachment attachment : attachments) {
            attachment.delete();
        }
    }

    private String messageForLosingProject() {
        return "An attachment '" + this + "' lost the project it belongs to";
    }

    /**
     * Returns this as a resource.
     *
     * This method is used for access control.
     *
     * @return resource
     */
    @Override
    public Resource asResource() {
        boolean isContainerProject = containerType.equals(ResourceType.PROJECT);
        final Project project;
        final Resource container;

        if (isContainerProject) {
            project = Project.find.byId(Long.parseLong(containerId));
            if (project == null) {
                throw new RuntimeException(messageForLosingProject());
            }
            container = project.asResource();
        } else {
            container = Resource.get(containerType, containerId);
            if (!(container instanceof GlobalResource)) {
                project = container.getProject();
                if (project == null) {
                    throw new RuntimeException(messageForLosingProject());
                }
            } else {
                project = null;
            }
        }

        if (project != null) {
            return new Resource() {
                @Override
                public String getId() {
                    return id.toString();
                }

                @Override
                public Project getProject() {
                    return project;
                }

                @Override
                public ResourceType getType() {
                    return ResourceType.ATTACHMENT;
                }

                @Override
                public Resource getContainer() {
                    return container;
                }
            };
        } else {
            return new GlobalResource() {
                @Override
                public String getId() {
                    return id.toString();
                }

                @Override
                public ResourceType getType() {
                    return ResourceType.ATTACHMENT;
                }

                @Override
                public Resource getContainer() {
                    return container;
                }
            };
        }
    }

    /**
     * Remove all of temporary files uploaded by users
     */
    private static void cleanupTemporaryUploadFilesWithSchedule() {
        Akka.system().scheduler().schedule(Duration.create(0, TimeUnit.SECONDS),
                Duration.create(AttachmentApp.TEMPORARYFILES_KEEPUP_TIME_MILLIS, TimeUnit.MILLISECONDS),
                new Runnable() {
                    @Override
                    public void run() {
                        try {
                            String result = removeUserTemporaryFiles();
                            play.Logger.info("User uploaded temporary files are cleaned up..." + result);
                        } catch (Exception e) {
                            play.Logger.warn("Failed!! User uploaded temporary files clean-up action failed!", e);
                        }
                    }

                    private String removeUserTemporaryFiles() {
                        List<Attachment> attachmentList = Attachment.find.where()
                                .eq("containerType", ResourceType.USER)
                                .ge("createdDate",
                                        JodaDateUtil
                                                .beforeByMillis(AttachmentApp.TEMPORARYFILES_KEEPUP_TIME_MILLIS))
                                .findList();
                        int deletedFileCount = 0;
                        for (Attachment attachment : attachmentList) {
                            attachment.delete();
                            deletedFileCount++;
                        }
                        if (attachmentList.size() != deletedFileCount) {
                            play.Logger.error(String.format(
                                    "Failed to delete user temporary files.\nExpected: %d  Actual: %d",
                                    attachmentList.size(), deletedFileCount));
                        }
                        return String.format("(%d of %d)", attachmentList.size(), deletedFileCount);
                    }
                }, Akka.system().dispatcher());
    }

    public static void onStart() {
        cleanupTemporaryUploadFilesWithSchedule();
    }

    @Override
    public String toString() {
        return "Attachment{" + "id=" + id + ", name='" + name + '\'' + ", hash='" + hash + '\'' + ", containerType="
                + containerType + ", mimeType='" + mimeType + '\'' + ", size=" + size + ", containerId='"
                + containerId + '\'' + ", createdDate=" + createdDate + '}';
    }
}