/** * 파일의 목록을 가져온다. * * <p>이슈, 게시물, 댓글을 볼 때, 첨부된 파일들의 목록을 보여주기 위해 이슈, 게시물, 댓글을 편집할 때, 첨부된 파일들의 목록 및 사용자 파일들의 목록을 보여주기 * 위해 * * <p>로그인한 사용자의 파일들의 목록을 {@code tempFiles} 프로퍼티로 넘겨준다. 첨부 파일들의 목록을 {@code attachments} 프로퍼티로 넘겨준다. * 첨부 파일들 중 로그인한 사용자가 읽기 권한을 갖지 못한 것이 하나라도 있다면 403 Forbidden 으로 응답한다. * * @return json 포맷으로 된 파일 목록을 본문으로 하는 응답. 다음고 같은 형식이다. {@code {tempFiles: 사용자 파일 목록, attachments: * 첨부 파일 목록 }} */ public static Result getFileList() { Map<String, List<Map<String, String>>> files = new HashMap<String, List<Map<String, String>>>(); // Get files from the user's area. List<Map<String, String>> userFiles = new ArrayList<Map<String, String>>(); for (Attachment attach : Attachment.findByContainer(UserApp.currentUser().asResource())) { userFiles.add(extractFileMetaDataFromAttachementAsMap(attach)); } files.put("tempFiles", userFiles); // 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 (containerType != null && containerId != null) { List<Map<String, String>> attachments = new ArrayList<Map<String, String>>(); for (Attachment attach : Attachment.findByContainer( ResourceType.valueOf(containerType), Long.parseLong(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)); }
/** * 사용자 첨부파일로 업로드한다 * * <p>when 이슈나 글, 코멘트등에서 파일을 첨부하기 전에 먼저 업로드 * * <p>멀티파트 폼데이터로 파일 업로드 요청을 받아서 서버에 파일 저장을 시도하고 만약 이미 같은 파일이 서버내에 globally 존재한다면 200OK로 응답 존재하지 않는 * 파일이라면 201 created로 응답 * * <p>요청에 첨부파일이 없는 것으로 보일때는 400 Bad Request로 응답 업로더가 익명 사용자일 경우에는 403 Forbidden 으로 응답 * * <p>업로드된 파일은 그 파일을 업로드한 사용자에게 첨부된 상태가 된다. 이후 {@link Attachment#moveAll(models.resource.Resource, * models.resource.Resource)} 등의 메소드를 사용해서 사용자의 첨부를 이슈 등의 다른 리소스로 옮길 수 있다. * * @return 생성된 파일의 메타데이터를 JSON 타입으로 반환하는 응답 * @throws NoSuchAlgorithmException * @throws IOException */ 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! List<MediaRange> accepts = request().acceptedTypes(); String contentType = request().accepts("application/json") ? "application/json" : "text/html"; response().setHeader("Content-Type", contentType); // 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<String, String>(); 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 that response has body, // so we cannot tell what is same with the file you try to add. return ok(responseBody); } }
/** * {@code id}에 해당하는 첨부파일을 지운다. * * <p>게시물, 이슈, 댓글들의 첨부파일을 지울때 사용한다. * * <p>폼의 필드에 {@code _method}가 존재하고 값이 delete로 지정되어 있지 않으면 Bad Request로 응답한다. 파일을 못 찾으면 Not Found * 삭제 권한이 없으면 Forbidden * * <p>첨부내용을 삭제한 후 해당 첨부의 origin 파일 유효검증 * * @param id 첨부파일 id * @return attachment 삭제 결과 (하지만 해당 메시지를 쓰고 있지는 않다. 아까운 네크워크 자원..) * @throws NoSuchAlgorithmException * @throws IOException */ public static Result deleteFile(Long id) throws NoSuchAlgorithmException, IOException { // _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."); } }
/** * 업로드된 {@code file}을 주어진 {@code name}으로 {@code container}에 첨부한다. * * <p>when: 업로드된 파일이 사용자에게 첨부될 때. 혹은 사용자를 거치지 않고 바로 다른 리소스로 첨부될 때. * * <p>업로드된 파일을 업로드 디렉토리로 옮긴다. 이 때 파일이름을 그 파일의 해시값으로 변경한다. 그 후 이 파일에 대한 메타정보 및 첨부될 대상에 대한 정보를 이 * 엔터티에 담는다. 만약 이 엔터티와 같은 내용을 갖고 있는 엔터티가 이미 존재한다면, 이미 {@code container}에 같은 첨부가 존재하고 있으므로 첨부하지 않고 * {@code false}를 반환한다. 그렇지 않다면 첨부 후 {@code true}를 반환한다. * * @param file 첨부할 파일 * @param name 파일 이름 * @param container 파일이 첨부될 리소스 * @return 파일이 새로 첨부되었다면 {@code true}, 이미 같은 첨부가 존재하여 첨부되지 않았다면 {@code false} * @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); } // 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; } }
/** * {@code from}에 첨부된 모든 첨부 파일을 {@code to}로 옮긴다. * * <p>when: 업로드 직후 일시적으로 사용자에게 첨부되었던 첨부 파일들을, 특정 리소스(이슈, 게시물 등)으로 옮기려 할 때 * * @param from 첨부 파일이 원래 있었던 리소스 * @param to 첨부 파일이 새로 옮겨질 리소스 * @return */ public static int moveAll(Resource from, Resource to) { List<Attachment> attachments = Attachment.findByContainer(from); for (Attachment attachment : attachments) { attachment.moveTo(to); } return attachments.size(); }
/** * {@code from}에 첨부된 파일중 파일 아이디가{@code selectedFileIds}에 해당하는 첨부 파일을 {@code to}로 옮긴다. * * <p>when: 업로드 직후 일시적으로 사용자에게 첨부되었던 첨부 파일들을, 특정 리소스(이슈, 게시물 등)으로 옮기려 할 때 * * @param from 첨부 파일이 원래 있었던 리소스 * @param to 첨부 파일이 새로 옮겨질 리소스 * @return */ 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(); }
private static Attachment saveAttachment(Part partToAttach, Resource container) throws MessagingException, IOException, NoSuchAlgorithmException { Attachment attach = new Attachment(); String fileName = MimeUtility.decodeText(partToAttach.getFileName()); attach.store(partToAttach.getInputStream(), fileName, container); if (!attach.mimeType.equalsIgnoreCase(partToAttach.getContentType())) { Logger.info( "The email says the content type is '" + partToAttach.getContentType() + "' but Yobi determines it is '" + attach.mimeType + "'"); } return attach; }
/** * {@code posting}에 {@code original} 정보를 채우고 갱신한다. * * <p>when: 게시물이나 이슈를 수정할 떄 사용한다. * * @param original * @param posting * @param postingForm * @param redirectTo * @param updatePosting * @return */ protected static Result editPosting( AbstractPosting original, AbstractPosting posting, Form<? extends AbstractPosting> postingForm, Call redirectTo, Callback updatePosting) { if (postingForm.hasErrors()) { return badRequest(postingForm.errors().toString()); } if (!AccessControl.isAllowed(UserApp.currentUser(), original.asResource(), Operation.UPDATE)) { return forbidden(views.html.error.forbidden.render(original.project)); } posting.id = original.id; posting.createdDate = original.createdDate; posting.authorId = original.authorId; posting.authorLoginId = original.authorLoginId; posting.authorName = original.authorName; posting.project = original.project; updatePosting.run(); posting.update(); // Attach the files in the current user's temporary storage. Attachment.moveAll(UserApp.currentUser().asResource(), original.asResource()); return redirect(redirectTo); }
/** * 사용자 정보 수정 * * @return */ @With(AnonymousCheckAction.class) @Transactional public static Result editUserInfo() { Form<User> userForm = new Form<>(User.class).bindFromRequest("name", "email"); String newEmail = userForm.data().get("email"); String newName = userForm.data().get("name"); User user = UserApp.currentUser(); if (StringUtils.isEmpty(newEmail)) { userForm.reject("email", "user.wrongEmail.alert"); } else { if (!StringUtils.equals(user.email, newEmail) && User.isEmailExist(newEmail)) { userForm.reject("email", "user.email.duplicate"); } } if (userForm.error("email") != null) { flash(Constants.WARNING, userForm.error("email").message()); return badRequest(edit.render(userForm, user)); } user.email = newEmail; user.name = newName; try { Long avatarId = Long.valueOf(userForm.data().get("avatarId")); if (avatarId != null) { Attachment attachment = Attachment.find.byId(avatarId); String primary = attachment.mimeType.split("/")[0].toLowerCase(); if (attachment.size > AVATAR_FILE_LIMIT_SIZE) { userForm.reject("avatarId", "user.avatar.fileSizeAlert"); } if (primary.equals("image")) { Attachment.deleteAll(currentUser().avatarAsResource()); attachment.moveTo(currentUser().avatarAsResource()); } } } catch (NumberFormatException ignored) { } Email.deleteOtherInvalidEmails(user.email); user.update(); return redirect( routes.UserApp.userInfo(user.loginId, DEFAULT_GROUP, DAYS_AGO, DEFAULT_SELECTED_TAB)); }
@Override public void onStart(Application app) { isSecretInvalid = equalsDefaultSecret(); insertInitialData(); PullRequest.onStart(); NotificationMail.onStart(); NotificationEvent.onStart(); Attachment.onStart(); }
/** * origin file의 유효성을 검증하고, 유효하지 않다면 로그를 남긴다. * * <p>origin file이 존재하지 않지만 그 파일을 참조하는 첨부가 존재하는 경우엔 에러 로그를 남긴다. origin file이 존재하지만 그 파일을 참조하는 첨부가 * 존재하지 않는 경우엔 경고 로그를 남긴다. * * @param hash origin file의 hash */ 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."); } }
/** * {@code id}로 파일을 찾아서 첨부파일로 돌려준다. * * <p>when: 첨부파일을 다운로드 받을 때 * * <p>주의사항: 파일명이 깨지지 않도록 {@link utils.HttpUtil#encodeContentDisposition)}로 인코딩한다. * * @param id 첨부파일 id * @return 파일이 첨부된 응답 * @throws NoSuchAlgorithmException * @throws IOException */ public static Result getFile(Long id) throws NoSuchAlgorithmException, IOException { Attachment attachment = Attachment.find.byId(id); if (attachment == null) { return notFound(); } if (!AccessControl.isAllowed(UserApp.currentUser(), attachment.asResource(), Operation.READ)) { return forbidden(); } File file = attachment.getFile(); String filename = HttpUtil.encodeContentDisposition(attachment.name); response().setHeader("Content-Type", attachment.mimeType); response().setHeader("Content-Disposition", "attachment; " + filename); return ok(file); }
/** * 새 댓글 저장 핸들러 * * <p>{@code commentForm}에서 입력값을 꺼내 현재 사용자를 작성자로 설정하고 댓글을 저장한다. 현재 사용자 임시 저장소에 있는 첨부파일을 댓글의 첨부파일로 * 옮긴다. * * @param comment * @param commentForm * @param redirectTo * @param containerUpdater * @return * @throws IOException */ public static Result newComment( Comment comment, Form<? extends Comment> commentForm, Call redirectTo, Callback containerUpdater) throws IOException { if (commentForm.hasErrors()) { flash(Constants.WARNING, "board.comment.empty"); return redirect(redirectTo); } comment.setAuthor(UserApp.currentUser()); containerUpdater.run(); // this updates comment.issue or comment.posting; comment.save(); // Attach all of the files in the current user's temporary storage. Attachment.moveAll(UserApp.currentUser().asResource(), comment.asResource()); return redirect(redirectTo); }
/** * 주어진 {@code container}에 첨부된 모든 첨부 파일을 삭제한다. * * <p>when: 첨부 파일을 가질 수 있는 어떤 리소스가 삭제되었을 때 * * @param container 첨부 파일을 삭제할 리소스 */ public static void deleteAll(Resource container) { List<Attachment> attachments = findByContainer(container); for (Attachment attachment : attachments) { attachment.delete(); } }