Michael Coxon
Michael Coxon

Reputation: 3535

How to go from spring mvc multipartfile into zipinputstream

I have a Spring MVC controller that accepts a MultipartFile, which will be a zip file. The problem is I can't seem to go from that to a ZipInputStream or ZipFile, so that I can go through the entries. It either closes the stream prematurely, produces an empty file, or as in the case below, zipInputStream.getNextEntry() returning null.

This is my MVC controller:

@RequestMapping(value = "/content/general-import", method = RequestMethod.POST)
public ModelAndView handleGeneralUpload(
                                  @RequestParam("file") MultipartFile file) throws IOException {

    // hard code the signature for the moment
    String signature = "RETAILER_GROUP:*|CHANNEL:*|LOCALE:de-AT|INDUSTRY:5499";

    LOG.info("Processing file archive: {} with signature: {}.", file.getName(), signature);

    ModelAndView mav = new ModelAndView();
    mav.setViewName("contentUpload");

    LOG.debug("File={} is empty={}.", file.getName(), file.isEmpty());
    if (!file.isEmpty()) {

        processFileZipEntry(file, signature);


        mav.addObject("form", UploadViewModel.make("/content/general-import", "Updated content with file"));

        return mav;
    } else {

        mav.addObject("form", UploadViewModel.make("/content/general-import", "Could not update content with file"));

        return mav;
    }
}

It delegates to the following method for processing:

protected void processFileZipEntry(MultipartFile file, String signature) throws IOException {
    byte[] bytes = file.getBytes();
    LOG.debug("Processing archive with bytes={}.", file.getBytes().length);

    ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(bytes));

    LOG.debug("Processing archive with size={}.", file.getSize());
    ZipEntry entry = null;
    while ((entry = zis.getNextEntry()) != null) {

        LOG.debug("Processing file={} is directory?={}.", entry.getName(), entry.isDirectory());
        // process each file, based on what it is and whether its a directory etc.
        if (!entry.isDirectory()) {
            // if the entry is a file, extract it
            LOG.debug("Processing entry: {}",entry.getName());

            int length = (int) entry.getSize();

            Content contentToSave = null;
            if(entry.getName().contains("gif")) {
                contentToSave = Content.makeImage(entry.getName(), Content.GIF, signature, getBytesFrom(zis, "gif"));
            } else if (entry.getName().contains("png")) {
                contentToSave = Content.makeImage(entry.getName(), Content.PNG, signature, getBytesFrom(zis, "png"));
            } else if (entry.getName().contains("jpeg")) {
                contentToSave = Content.makeImage(entry.getName(), Content.JPEG, signature, getBytesFrom(zis, "jpeg"));
            } else if (entry.getName().contains("json")) {
                contentToSave = Content.makeFile(entry.getName(), Content.JSON, signature, getStringFrom(zis, length));
            } else if (entry.getName().contains("js")) {
                contentToSave = Content.makeFile(entry.getName(), Content.JS, signature, getStringFrom(zis, length));
            } else if (entry.getName().contains("css")) {
                contentToSave = Content.makeFile(entry.getName(), Content.CSS, signature, getStringFrom(zis, length));
            }

            Content contentAleadyThere = contentService.fetch(entry.getName());
            if(contentAleadyThere != null) {
                LOG.warn("Replacing file: {} with uploaded version.", contentToSave.getName());
            }

            contentService.put(contentToSave);
            LOG.info("Persisted file: {} from uploaded version.", contentToSave.getName());
        }

    }
}

Basically, in this permutation, the file bytes are there, but there are no entries (zis.getNextEntry() does not exist. I can see that the zip file contains files, and the byte[] has about 3MB worth of stuff, so something must be going wrong with the streaming. Does anyone have a recipe for going from MultipartFile to ZipFile or ZipInputStream?

EDIT

To give you more information, I have a test harnass around this code, by using a MockMvc

@Test
public void testProcessingGeneralUpload() throws Exception {

    Resource template = wac.getResource("classpath:lc_content/content.zip");

    System.out.println("template content length: " + template.contentLength());
    System.out.println("template path: " + template.getFile().getPath());
    System.out.println("template filename: " + template.getFilename());



    MockMultipartFile firstFile = new MockMultipartFile(
            "file", "content.zip", MediaType.APPLICATION_OCTET_STREAM_VALUE, extractFile(template.getFile()));

    MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.fileUpload("/content/general-import")
            .file(firstFile))
            .andExpect(status().isOk())
            .andExpect(view().name("contentUpload"))
            .andExpect(model().attributeExists("form")).andReturn();

    // processing assertions
    ModelMap modelMap = mvcResult.getModelAndView().getModelMap();
    Object object = modelMap.get("form");
        assertThat(object, is(not(nullValue())));
        assertThat(object, is(instanceOf(UploadViewModel.class)));
    UploadViewModel addModel = (UploadViewModel) object;
        assertThat(addModel.getMessage(), is(notNullValue()));
    assertThat(addModel.getPostUrl(), is(notNullValue()));
    assertThat(addModel.getPostUrl(), is("/content/general-import"));
    assertThat(addModel.getMessage(), is("Updated content with file"));

    // persistence assertions

    assertThat(contentDao.findByName("/content/control/basket-manager.js"), is(notNullValue()) );

}

The extractFile method is as follows:

private byte[] extractFile(File zipFile) throws IOException {

    ZipInputStream zipIn = new ZipInputStream(new FileInputStream(zipFile));
    System.out.println("length of file: " + zipFile.length());

    byte[] output = null;

    try {
        byte[] data = new byte[(int)zipFile.length()];
        zipIn.read(data);
        zipIn.close();

        output = data;
    } catch (IOException e) {
        e.printStackTrace();
    }
    return output;

}

The length of bytes it produces is 3617817, which is the size I expect, and this is fed into the controller method at the top of this question.

I have continued working the problem. The size of the file is correct, it is a zipped file (it unpacks via the OS perfectly), and yet no ZipEntry enumeration.

Upvotes: 2

Views: 11478

Answers (1)

M. Deinum
M. Deinum

Reputation: 125009

I would for starters rewrite some of the code instead of doing things in memory with additional byte[].

You are using Spring's Resource class so why not simply use the getInputStream() method to construct the MockMultipartFile as you want to upload that file.

Resource template = wac.getResource("classpath:lc_content/content.zip");
MockMultipartFile firstFile = new MockMultipartFile(
        "file", "content.zip", MediaType.APPLICATION_OCTET_STREAM_VALUE, template.getInputStream());

The same for your upload processing code the ZipInputStream can also be constructed on another InputStream which is also provided by the MultipartFile interface.

protected void processFileZipEntry(MultipartFile file, String signature) throws IOException {

    LOG.debug("Processing archive with size={}.", file.getSize());
    ZipInputStream zis = new ZipInputStream(file.getInputStream());

Wouldn't be the first time that jugling around with byte[] gives a problem. I also vaguely recall some issues with ZipInputStream which lead us to use ZipFile but for this you will first have to store the file in a temp directoy using the transferTo method on MultipartFile.

File tempFile = File.createTempFile("upload", null);
file.transferTo(tempFile);
ZipFile zipFile = new ZipFle(tempFile);
// Proces Zip
tempFile.delete();

Upvotes: 3

Related Questions