Java tutorial
/** * 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 + '}'; } }