Пример #1
0
  /**
   * 파일의 목록을 가져온다.
   *
   * <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));
  }
Пример #2
0
  /**
   * 사용자 첨부파일로 업로드한다
   *
   * <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);
    }
  }
Пример #3
0
  /**
   * {@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.");
    }
  }
Пример #4
0
  /**
   * 업로드된 {@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;
    }
  }
Пример #5
0
 /**
  * {@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();
 }
Пример #6
0
 /**
  * {@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();
 }
Пример #7
0
  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;
  }
Пример #8
0
  /**
   * {@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);
  }
Пример #9
0
  /**
   * 사용자 정보 수정
   *
   * @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));
  }
Пример #10
0
  @Override
  public void onStart(Application app) {
    isSecretInvalid = equalsDefaultSecret();
    insertInitialData();

    PullRequest.onStart();
    NotificationMail.onStart();
    NotificationEvent.onStart();
    Attachment.onStart();
  }
Пример #11
0
  /**
   * 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.");
    }
  }
Пример #12
0
  /**
   * {@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);
  }
Пример #13
0
  /**
   * 새 댓글 저장 핸들러
   *
   * <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);
  }
Пример #14
0
 /**
  * 주어진 {@code container}에 첨부된 모든 첨부 파일을 삭제한다.
  *
  * <p>when: 첨부 파일을 가질 수 있는 어떤 리소스가 삭제되었을 때
  *
  * @param container 첨부 파일을 삭제할 리소스
  */
 public static void deleteAll(Resource container) {
   List<Attachment> attachments = findByContainer(container);
   for (Attachment attachment : attachments) {
     attachment.delete();
   }
 }