在 JHipster 中实现文件附件

Implementing file attachments in JHipster

我有一个单体 JHipster (v5.6.1) 项目,我需要实现一个实体的文件附件。

我不知道从哪里开始。 DTO 被发送到 REST 控制器以创建或更新它们,我应该如何发送文件?

我可以将 Base64 附加到我的 DTO 并以这种方式发送,但我不确定如何(是否有任何 angular 插件来完成此操作?)。这将需要对服务器进行额外的工作,并且我认为需要额外的文件大小。

另一种选择是分别发送实体 (DTO) 和文件,同样我不太确定如何。

我认为没有理由不回答这个问题,因为我现在已经在我的几个项目中成功实施了附件。

如果您愿意,可以跳过此解释并查看我在 vicpermir/jhipster-ng-attachments 创建的 github 存储库。这是一个可以启动和运行的工作项目。

总体思路是创建一个包含所有必填字段(文件大小、名称、内容类型等)的 Attachment 实体,并设置与任何实体的 many-to-many 关系想要实现文件附件。

JDL是这样的:

// Test entity, just to showcase attachments, this should
// be the entity you want to add attachments to
entity Report {
    name String required
}

entity Attachment {
    filename String required            // Generated unique filename on the server
    originalFilename String required    // Original filename on the users computer
    extension String required
    sizeInBytes Integer required
    sha256 String required              // Can be useful for duplication and integrity checks
    contentType String required
    uploadDate Instant required
}

// ManyToMany instead of OneToMany because it will be easier and cleaner
// to integrate attachments into other entities in case we need to do it
relationship ManyToMany {
    Report{attachments} to Attachment{reports}
}

我同时拥有 filenameoriginalFilename,因为我的要求之一是保留用户上传时使用的任何文件名。我在服务器端使用的生成的唯一名称对用户是透明的。

一旦您使用这样的 JDL 生成项目,您将必须将文件负载添加到您的 DTO(如果您不使用 DTO,则为实体)以便服务器可以在 base64 并存储它。

我的 AttachmentDTO:

...
    private Instant uploadDate;

    // File payload (transient)
    private byte[] file;

    public Long getId() {
        return id;
    }
...

然后,您只需在服务器端处理这些字节数组,存储它们并将对该位置的引用保存到数据库中。

AttachmentService.java

    /**
     * Process file attachments
     */
    public Set<AttachmentDTO> processAttachments(Set<AttachmentDTO> attachments) {
        Set<AttachmentDTO> result = new HashSet<>();
        if (attachments != null && attachments.size() > 0) {
            for (AttachmentDTO a : attachments) {
                if (a.getId() == null) {
                    Optional<AttachmentDTO> existingAttachment = this.findBySha256(a.getSha256());
                    if(existingAttachment.isPresent()) {
                        a.setId(existingAttachment.get().getId());
                    } else {
                        String fileExtension = FilenameUtils.getExtension(a.getOriginalFilename());
                        String fileName = UUID.randomUUID() + "." + fileExtension;
                        if (StringUtils.isBlank(a.getContentType())) {
                            a.setContentType("application/octet-stream");
                        }
                        Boolean saved = this.createBase64File(fileName, a.getFile());
                        if (saved) {
                            a.setFilename(fileName);
                        }
                    }
                }
                result.add(a);
            }
        }
        return result;
    }

我在这里所做的是检查附件是否已经存在(使用 SHA256 哈希)。如果是,我使用那个,否则我存储新文件并保留新的附件数据。

现在剩下的就是在客户端管理附件了。我为此创建了两个组件,因此向新实体添加附件非常容易。

attachment-download.component.ts

...
@Component({
  selector: 'jhi-attachment-download',
  template: , // Removed to reduce verbosity
  providers: [JhiDataUtils]
})
export class JhiAttachmentDownloadComponent {
  @Input()
  attachments: IAttachment[] = [];
}

这只是调用一个获取附件 ID 的映射,在服务器上查找关联的文件,然后 returns 该文件供浏览器下载。在您的实体详细信息视图中使用此组件:

<jhi-attachment-download [attachments]="[your_entity].attachments"></jhi-attachment-download>

attachment-upload.component.ts

...
@Component({
  selector: 'jhi-attachment-upload',
  template: , // Removed to reduce verbosity
  providers: [JhiDataUtils]
})
export class JhiAttachmentUploadComponent {
  @Input()
  attachments: IAttachment[] = [];
  loadingFiles: number;

  constructor(private dataUtils: JhiDataUtils) {
    this.loadingFiles = 0;
  }

  addAttachment(e: any): void {
    this.loadingFiles = 0;
    if (e && e.target.files) {
      this.loadingFiles = e.target.files.length;
      for (let i = 0; i < this.loadingFiles; i++) {
        const file = e.target.files[i];
        const fileName = file.name;
        const attachment: IAttachment = {
          originalFilename: fileName,
          contentType: file.type,
          sizeInBytes: file.size,
          extension: this.getExtension(fileName),
          processing: true
        };
        this.attachments.push(attachment);
        this.dataUtils.toBase64(file, (base64Data: any) => {
          attachment.file = base64Data;
          attachment.sha256 = hash
            .sha256()
            .update(base64Data)
            .digest('hex');
          attachment.processing = false;
          this.loadingFiles--;
        });
      }
    }
    e.target.value = '';
  }

  getExtension(fileName: string): string {
    return fileName.substring(fileName.lastIndexOf('.'));
  }
}

在您的实体更新视图中使用此组件:

<jhi-attachment-upload [attachments]="editForm.get('attachments')!.value"></jhi-attachment-upload>

一旦发送到服务器,文件将存储在您在 application-*.yml 中配置的文件夹中,按年和月分隔在子目录中。这是为了避免在同一个文件夹中存储太多文件,这会让人很头疼。

我确信很多事情可以做得更好,但这对我有用。