jhipster angular primeNG file upload storage

For any application, you have several different options to store files uploaded by the user: database, file system, cloud services, etc.

In our case we choose to store them in the filesystem instead of the database following our DBA’s recommendation to ease database maintenance and backups.

The service implementation is as follows.

First, we need an entity to store metadata about our files. You can create this with the entity subgenerator of jhipster or make your own. We need at least a title, path, mimetype and filesize (the url could be calculated from path or service but we can also store this information)

Archivo.java

@Entity
@Table(name = "archivo")
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
public class Archivo implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
    @SequenceGenerator(name = "sequenceGenerator")
    private Long id;

    @NotNull
    @Size(min = 4)
    @Column(name = "title", nullable = false)
    private String title;

    @NotNull
    @Column(name = "url", nullable = false)
    private String url;

    @NotNull
    @Column(name = "path", nullable = false)
    private String path;

    @NotNull
    @Column(name = "mimetype", nullable = false)
    private String mimetype;

    @NotNull
    @Column(name = "filesize", nullable = false)
    private Long filesize;
...

then our Resource Controller would be

    @PostMapping("/cursos/archivos")
    @Timed
    public ResponseEntity createCurso(@Valid @RequestPart(value = "curso") Curso curso, @RequestPart("archivos[]")MultipartFile[] archivos) throws URISyntaxException {
        log.debug("peticion REST para guardar el Curso {} y sus {} archivos", curso, archivos.length);
        if (curso.getId() != null) {
            throw new BadRequestAlertException("A new curso cannot already have an ID", ENTITY_NAME, "idexists");
        }
        Curso result = this.cursoService.save(curso);
        Arrays.stream(archivos).forEach(archivoMultipart -> {
            Archivo archivo = new Archivo();
            archivo.setTitulo(archivoMultipart.getOriginalFilename());
            archivo.setDescripcion("");
            archivo.setMimetype(archivoMultipart.getContentType());
            archivo.setFilesize(archivoMultipart.getSize());
            archivo.setPath("");
            archivo.setUrl("");
            archivo = this.archivoRepository.save(archivo);
            File archivoFS = saveToFile(archivoMultipart, archivo);
            archivo.setPath(archivoFS.getAbsolutePath());
            archivo.setUrl(this.servletContext.getContextPath() + "api/archivos/" + archivo.getId());
            archivo.setCurso(result);
            archivo = this.archivoRepository.save(archivo);
        });
        return ResponseEntity.created(new URI("/api/cursos/" + result.getId()))
            .headers(HeaderUtil.createEntityCreationAlert(ENTITY_NAME, result.getId().toString()))
            .body(result);
    }

    private File saveToFile(MultipartFile archivoMultipart, Archivo archivo){
        File archivoFS = null;
        try {
            archivoFS = new File("/storage/cursos/" + archivo.getId() + MimeTypes.getDefaultMimeTypes().forName(archivo.getMimetype()).getExtension());
            archivoFS.getParentFile().mkdirs();
            Files.copy(archivoMultipart.getInputStream(), archivoFS.toPath(), StandardCopyOption.REPLACE_EXISTING);
        } catch (MimeTypeException mimeTypeException){
            log.error("Error detecting file extension for mimetype", mimeTypeException);
            throw new RuntimeException(mimeTypeException);
        } catch (IOException e) {
            log.error("Error while saving file", e);
            throw new RuntimeException(e);
        }
        return archivoFS;
    }

first, we send our Entity alongside files using FormData in a multipart form request using the PrimeNG file upload component.
Then we retrieve the metadata from our MultipartFile. We need to store the entity first in order to get an ID which is autogenerated, which we’ll use it to construct the Path where we’ll store our file.
We use Tika to determine the file extension correspondent to the MimeType.
We ensure the path directories exist by calling mkdirs()
Finally we copy the InputStream from MultipartFile to the File using the Files.copy method.
Notice that we handle checked exceptions in java streams by re throwing them as RuntimeExceptions.

Now that we store our files to the folder /storage (outside our app folder, in the root file system), we need to configure this on our server and give proper permissions. In our case we use dokku for deployment, so we need to configure persistent storage as explained here.

This example demonstrates how to mount the recommended directory to /storage inside an application called node-js-app:

# we use a subdirectory inside of the host directory to scope it to just the app
dokku storage:mount node-js-app /var/lib/dokku/data/storage/node-js-app:/storage
Dokku will then mount the shared contents of /var/lib/dokku/data/storage to /storage inside the container.

Once you have mounted persistent storage, you will also need to restart the application. See the process scaling documentation for more information.

dokku ps:restart app-name

sources:
https://stackoverflow.com/questions/2833853/create-whole-path-automatically-when-writing-to-a-new-file
View story at Medium.com
https://www.baeldung.com/convert-input-stream-to-a-file
https://stackoverflow.com/questions/5541694/how-to-get-file-extension-from-content-type
http://dokku.viewdocs.io/dokku/advanced-usage/persistent-storage/
https://www.oreilly.com/ideas/handling-checked-exceptions-in-java-streams

Anuncios

jhipster angular primeNG file upload with entity as application/json

I was uploading some files along an entity in a jhipster generated app with the primeNG file upload component, but I was appending each field value in a new form data request parameter.

CursoResource.java

    @PostMapping("/cursos/archivos")
    @Timed
    public ResponseEntity<Curso> createCurso(@RequestPart(value = "titulo") String titulo, 
                                             @RequestPart(value = "descripcion") String descripcion,
                                             @RequestPart("archivos[]")MultipartFile[] archivos
    ) {
...
Curso curso = new Curso();
curso.setTitulo(titulo);
curso.setDescripcion(descripcion);
curso = this.cursoService.save(curso);
...
}

and in the client

curso-update.component.html

<p-fileUpload id="field_archivos" name="archivos[]" url="api/cursos/archivos" multiple="multiple"
                                  accept=".pdf,.doc,.docx,.xls,.xlsx"
                                  [maxFileSize]="3*1024*1024"                                  
                    (onBeforeSend)="onBeforeSend($event)">
                    </p-fileUpload>

curso-update.component.ts

    private onBeforeSend(event) {
        const token = this.localStorage.retrieve('authenticationToken') || this.sessionStorage.retrieve('authenticationToken');
        if (!!token) {
            event.xhr.setRequestHeader('Authorization', `Bearer  ${token}`);
        }
        event.formData.append('titulo', this.curso.titulo);
        event.formData.append('descripcion', this.curso.descripcion);
    }

but we can send the Entity serialized as JSON in a Blob inside our FormData using the correct Content-Type 'application/json'. With this we can include nested objects, add fields to our Entity -and we won't have to change the Resource Controller method signature with every new field-

curso-update.component.ts

    private onBeforeSend(event) {
        const token = this.localStorage.retrieve('authenticationToken') || this.sessionStorage.retrieve('authenticationToken');
        if (!!token) {
            event.xhr.setRequestHeader('Authorization', `Bearer  ${token}`);
        }
        event.formData.append('curso', new Blob([JSON.stringify(this.curso)], { type: 'application/json' }));
    }

change the Resource Controller accordingly

CursoResource.java

    @PostMapping("/cursos/archivos")
    @Timed
    public ResponseEntity createCurso(@Valid @RequestPart(value = "curso") Curso curso, 
                                             @RequestPart("archivos[]")MultipartFile[] archivos) {
...
Curso result = this.cursoService.save(curso);
...
}

primeng file upload angular i18n internationalization

primeNG doesn’t support i18n out of the box. But its components, like file upload, provide properties or templates to fit your needs.

In the case of file upload it has Label properties which we can use to translate

chooseLabel Label of the choose button.
uploadLabel Label of the upload button.
cancelLabel Label of the cancel button.

We have an app generated by jhipster which support internationalization through language files and directives. First we have to add the label translations in their respective files, in example

i18n/es/curso.json

{
    "cursosApp": {
        "curso" : {
            ...
            "upload": {
                "chooseLabel": "Seleccionar archivo"
            }
        }
    }
}

i18n/en/curso.json

{
    "cursosApp": {
        "curso" : {
            ...
            "upload": {
                "chooseLabel": "Choose file"
            }
        }
    }
}

and in your html

curso-update-component.html

<p-fileUpload id=...
[chooseLabel]="'cursosApp.curso.upload.chooseLabel' | translate">
</p-fileUpload>

notice the single quotes around labels

That’s it, now you can enable i18n in your primeNG components.

If you want to translate messages from your components, you could use the TranslateService as explained in this answer from stackoverflow.

sources:
https://forum.primefaces.org/viewtopic.php?t=51332
View story at Medium.com
https://github.com/jhipster/ng-jhipster/blob/master/src/language/jhi-translate.directive.ts
https://stackoverflow.com/questions/49513167/primeng-jhipster-insert-jhitranslation-in-growl-messages

angular primeng file upload restrict multiple mime types

primeNG version: 6.1.4

PrimeNG file upload docs don’t specify how to restrict the file types allowed for multiple or different mime types (except for images)

Name Type Default Description
accept string false Pattern to restrict the allowed file types such as “image/*”.

I’ve found 2 answers which suggests to add file types separated by ‘commas’ without spaces between them like so

Image filter
accept=”image/*”[accepts all image file types]
accept=”image/*.jpeg”[accepts only JPEG image file types]

File filter
accept=”application/msexcel” [accepts only xls file types]
accept=”.xlsm,application/msexcel” [accepts only xls & xlsm file types]
accept=”application/msmsword” [accepts only word file types]

but it didn’t work with mime types. In the end I had to specify different file types using only its extension (without spaces between commas)

accept=”.pdf,.doc,.docx,.xls,.xlsx”

add primeNG library to jhipster generated app

jhipster version: v5.4.2
node version: v8.12.0
npm version: 6.4.1
primeng version: 6.1.4
java version: 1.8.0_91

You can follow the Steps to integrate PrimeNG with JHipster from stackoverflow.

The only thing that caught me down guard was that I was adding the required Modules on the app.module.ts instead of the Entity module -where I wanted to use a file upload control- or better yet, the shared-libs.module.ts.

First, let’s follow the steps from the answer above

install primeng, primeicons and angular animations (if you haven’t got back to npm, please do or just keep using yarn instead of npm commands)

npm install –save primeng primeicons @angular/animations

then import the required css files in the file vendor.[scss|css]

@import ‘~primeicons/primeicons.css’;
@import ‘~primeng/resources/themes/nova-light/theme.css’;
@import ‘~primeng/resources/primeng.min.css’;

now in your shared-libs-module.ts (this way we can use the controls in whichever module that imports this module, all jhipster generated modules by default, i.e. from the command line)

import { BrowserAnimationsModule } from ‘@angular/platform-browser/animations’;
import { FileUploadModule } from ‘primeng/primeng’;

@NgModule({
imports: [

BrowserAnimationsModule,
FileUploadModule
],
exports: [

BrowserAnimationsModule,
FileUploadModule
]
})

that’s it, now add your control in your template file

<p-fileUpload id="field_archivos" name="archivos[]" url="api/cursos/archivos" multiple="multiple"
                                  accept=".pdf,.doc,.docx,.xls,.xlsx"
                                  [maxFileSize]="3*1024*1024"
                    (onBeforeSend)="onBeforeSend($event)">
                    </p-fileUpload>

ubuntu 16.04 – switch from yarn back to npm again

Recently I’ve upgraded jhipster using yarn, but it caused me some issues running a newly created application. I don’t think the upgrade caused the problem but yarn and the way I installed node.

Time ago I installed node, building it from source to have the latest LTS version. Afterwards, I installed yarn following their instructions and adding their repo. In between creating and upgrading projects, something must have gone wrong and I ended up with different incompatible versions of npm installed. Which led to the issue described above.

if i did a

node -v
v8.12.0

but

npm -v
2.5.2

I checked where did it came from

which npm
~/.config/yarn/global/node_modules/npm/bin/npm

but i never installed (so i remember) npm through yarn.

I tried removing it with

yarn global remove npm
error This module isn’t specified in a package.json file.

I decided to clean up all the mess and start over again.
First I had to uninstall node (remember I built it from source)

cd ~/Projects/opensource/node
sudo make uninstall

Then remove yarn

sudo apt remove yarn

optionally you could delete all the cache and configuration files

rm -rf ~/.config/yarn

Now we’re ready to install again, but this time it’s easier to add their repos and install and update from them.

install node (LTS)

curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash –
sudo apt-get install -y nodejs
node -v
v8.12.0
npm -v
6.4.1
which npm
/usr/bin/npm

Since jhipster now recommends npm as default, that’s it.

There’s a lot of discussion about npm or yarn, like this on reddit or this article

optionally install yarn

curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add –
echo “deb https://dl.yarnpkg.com/debian/ stable main” | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt-get update && sudo apt-get install yarn

yarn global upgrade jhipster doesn’t update

I was trying to upgrade jhipster to its latest version in order to update an application that was generated by a previous version of the generator.

Since I use yarn for package management (although it seems i will be getting back to npm), I just tried

yarn global upgrade generator-jhipster

which let me with the same version I had before

jhipster -v
4.10.2

I found the answer here

Keep in mind that upgrade respects semver range specified in package.json. I believe the default location on OSX is ~/.config/yarn/global/package.json

So yarn global add some-package would add a carret-range, something like ^1.0.0 so if a 2.0.0 comes out, yarn global upgrade would not upgrade to that because it doesn’t fit the range. You would specify the –latest/-L flag to ignore the semver range and get the latest as tagged in the registry.

so, in order to update jhipster run the following command

yarn global upgrade generator-jhipster –latest