/**
* 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 controllers;
import static play.libs.Json.toJson;
import java.io.File;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import models.Attachment;
import models.User;
import models.enumeration.Operation;
import models.enumeration.ResourceType;
import org.codehaus.jackson.JsonNode;
import org.apache.commons.lang3.StringUtils;
import play.Configuration;
import play.Logger;
import play.mvc.Controller;
import play.mvc.Http.MultipartFormData.FilePart;
import play.mvc.Result;
import utils.AccessControl;
import utils.HttpUtil;
public class AttachmentApp extends Controller {
public static final String TAG_NAME_FOR_TEMPORARY_UPLOAD_FILES = "temporaryUploadFiles";
public static final long TEMPORARYFILES_KEEPUP_TIME_MILLIS = Configuration.root()
.getMilliseconds("application.temporaryfiles.keep-up.time", 24 * 60 * 60 * 1000L);
public static Result uploadFile() throws NoSuchAlgorithmException, IOException {
// Get the file from request.
FilePart filePart =
request().body().asMultipartFormData().getFile("filePath");
if (filePart == null) {
return badRequest();
}
File file = filePart.getFile();
User uploader = UserApp.currentUser();
// Anonymous cannot upload a file.
if (uploader.isAnonymous()) {
return forbidden();
}
// Attach the file to the user who upload it.
Attachment attach = new Attachment();
boolean isCreated = attach.store(file, filePart.getFilename(), uploader.asResource());
// The request has been fulfilled and resulted in a new resource being
// created. The newly created resource can be referenced by the URI(s)
// returned in the entity of the response, with the most specific URI
// for the resource given by a Location header field.
// -- RFC 2616, 10.2.2. 201 Created
String url = routes.AttachmentApp.getFile(attach.id).url();
response().setHeader("Location", url);
// The entity format is specified by the media type given in the
// Content-Type header field. -- RFC 2616, 10.2.2. 201 Created
// While upload a file using Internet Explorer, if the response is not in
// text/html, the browser will prompt the user to download it as a file.
// To avoid this, if application/json is not acceptable by client, the
// Content-Type field of response is set to "text/html". But, ACTUALLY
// IT WILL BE SEND IN JSON!
String contentType = HttpUtil.getPreferType(request(), "application/json", "text/html");
response().setHeader("Content-Type", contentType);
response().setHeader("Vary", "Accept");
// The response SHOULD include an entity containing a list of resource
// characteristics and location(s) from which the user or user agent can
// choose the one most appropriate. -- RFC 2616, 10.2.2. 201 Created
Map<String, String> fileInfo = new HashMap<>();
fileInfo.put("id", attach.id.toString());
fileInfo.put("mimeType", attach.mimeType);
fileInfo.put("name", attach.name);
fileInfo.put("url", url);
fileInfo.put("size", attach.size.toString());
JsonNode responseBody = toJson(fileInfo);
if (isCreated) {
// If an attachment has been created - it does NOT mean that
// a file is created in the filesystem - return 201 Created.
return created(responseBody);
} else {
// If the attachment already exists, return 200 OK.
// Why not 204? Because 204 doesn't allow response to have a body,
// so we cannot tell what is same with the file you try to add.
return ok(responseBody);
}
}
public static Result getFile(Long id) throws IOException {
Attachment attachment = Attachment.find.byId(id);
String action = HttpUtil.getFirstValueFromQuery(request().queryString(), "action");
String dispositionType = StringUtils.equals(action, "download") ? "attachment" : "inline";
if (attachment == null) {
return notFound("The file does not exist.");
}
String eTag = "\"" + attachment.hash + "-" + dispositionType + "\"";
if (!AccessControl.isAllowed(UserApp.currentUser(), attachment.asResource(), Operation.READ)) {
return forbidden("You have no permission to get the file.");
}
response().setHeader("Cache-Control", "private, max-age=3600");
String ifNoneMatchValue = request().getHeader("If-None-Match");
if(ifNoneMatchValue != null && ifNoneMatchValue.equals(eTag)) {
response().setHeader("ETag", eTag);
return status(NOT_MODIFIED);
}
File file = attachment.getFile();
if(file != null && !file.exists()){
Logger.error("Attachment ID:" + id + " (" + file.getAbsolutePath() + ") does not exist on storage");
return internalServerError("The file does not exist");
}
String filename = HttpUtil.encodeContentDisposition(attachment.name);
response().setHeader("Content-Type", attachment.mimeType);
response().setHeader("Content-Disposition", dispositionType + "; " + filename);
response().setHeader("ETag", eTag);
return ok(file);
}
public static Result deleteFile(Long id) {
// _method must be 'delete'
Map<String, String[]> data =
request().body().asMultipartFormData().asFormUrlEncoded();
if (!HttpUtil.getFirstValueFromQuery(data, "_method").toLowerCase()
.equals("delete")) {
return badRequest("_method must be 'delete'.");
}
// Remove the attachment.
Attachment attach = Attachment.find.byId(id);
if (attach == null) {
return notFound();
}
if (!AccessControl.isAllowed(UserApp.currentUser(), attach.asResource(), Operation.DELETE)) {
return forbidden();
}
attach.delete();
logIfOriginFileIsNotValid(attach.hash);
if (Attachment.fileExists(attach.hash)) {
return ok("The attachment is removed successfully, but its origin file still exists.");
} else {
return ok("Both the attachment and its origin file are removed successfully.");
}
}
private static void logIfOriginFileIsNotValid(String hash) {
if (!Attachment.fileExists(hash) && Attachment.exists(hash)) {
Logger.error("The origin file '" + hash + "' cannot be " +
"found even if the file is still referred by some" +
"attachments.");
}
if (Attachment.fileExists(hash) && !Attachment.exists(hash)) {
Logger.warn("The attachment is removed successfully, but its " +
"origin file '" + hash + "' still exists abnormally even if the file " +
"referred by nowhere.");
}
}
private static Map<String, String> extractFileMetaDataFromAttachementAsMap(Attachment attach) {
Map<String, String> metadata = new HashMap<>();
metadata.put("id", attach.id.toString());
metadata.put("mimeType", attach.mimeType);
metadata.put("name", attach.name);
metadata.put("url", routes.AttachmentApp.getFile(attach.id).url());
metadata.put("size", attach.size.toString());
return metadata;
}
public static Result getFileList() {
Map<String, List<Map<String, String>>> files =
new HashMap<>();
// Get attached files only if the user has permission to read it.
Map<String, String[]> query = request().queryString();
String containerType = HttpUtil.getFirstValueFromQuery(query, "containerType");
String containerId = HttpUtil.getFirstValueFromQuery(query, "containerId");
if (StringUtils.isNotEmpty(containerType) && StringUtils.isNotEmpty(containerId)) {
List<Map<String, String>> attachments = new ArrayList<>();
for (Attachment attach : Attachment.findByContainer(ResourceType.valueOf
(containerType), containerId)) {
if (!AccessControl.isAllowed(UserApp.currentUser(),
attach.asResource(), Operation.READ)) {
return forbidden();
}
attachments.add(extractFileMetaDataFromAttachementAsMap(attach));
}
files.put("attachments", attachments);
}
// Return the list of files as JSON.
response().setHeader("Content-Type", "application/json");
return ok(toJson(files));
}
}