GridFsFile download

Hi all,

I’m trying to update a download functionality developed some years ago.
I tried to readapt my code but something is not working properly. First of all, I did not understand how to retrieve one file only (I know that the _id field is unique, so how can I avoid that “files.get(0)”?)
Moreover, the last line in my code is not working anymore: the download seems to start but the browser shows a dark screen or keeps loading, but nothing happens in the end. Could you please help me? Any advice?

Thanks and have a good day

@GetMapping(value = "/downloadFile/{fileId}")
    public void downloadFile(@PathVariable("fileId") String fileId, HttpServletRequest request, HttpServletResponse response) throws IOException {

        MongoDatabase database = mongoClient.getDatabase("database");
        GridFSBucket gridFSBucket = GridFSBuckets.create(database);
        List<GridFSFile> files = new ArrayList<>();

        Bson query = Filters.eq("_id", new ObjectId(fileId));
        gridFSBucket.find(query)
                .limit(5)
                .forEach(new Consumer<GridFSFile>() {
                    @Override
                    public void accept(final GridFSFile gridFSFile) {
                        files.add(gridFSFile);
                    }
                });

        String[] fileNameArray = files.get(0).getMetadata().get("filename").toString().split("\\.");

        String extension = fileNameArray[fileNameArray.length-1].toLowerCase();

        TrustManager[] trustAllCerts = new TrustManager[]{
                new X509TrustManager() {
                    public X509Certificate[] getAcceptedIssuers() {
                        return null;
                    }
                    public void checkClientTrusted(
                            X509Certificate[] certs, String authType) {
                    }
                    public void checkServerTrusted(
                            X509Certificate[] certs, String authType) {
                    }
                }
        };

        try {
            SSLContext sc = SSLContext.getInstance("SSL");
            sc.init(null, trustAllCerts, new SecureRandom());
            HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
        } catch (Exception e) {
        }


        if(extension.equals("pdf")) {
            response.setContentType("application/pdf");
        }
        else {
            response.setContentType("application/octet-stream");
        }
        response.setHeader("Content-Disposition", String.format("inline; filename=\"" + files.get(0).getMetadata().get("filename") + "\""));
        response.setContentLength((int) files.get(0).getLength());

        ObjectId objectFileId = new ObjectId(fileId);
        try (GridFSDownloadStream downloadStream = gridFSBucket.openDownloadStream(objectFileId)) {
            int fileLength = (int) downloadStream.getGridFSFile().getLength();
            byte[] bytesToWriteTo = new byte[fileLength];
            downloadStream.read(bytesToWriteTo);
            GridFSFile gridFSFile = downloadStream.getGridFSFile();
            downloadStream.close();
            GridFsResource gridFsResource = new GridFsResource(gridFSFile);

            FileCopyUtils.copy(gridFsResource.getContent(), response.getOutputStream());
        }
    }

Why are you disabling SSL for?

If you just want one file, retrieve only one file instead of using files.get(0), you can use the findOne method instead of find, which returns a single GridFSFile instead of a cursor. Here’s an example:

GridFSFile file = gridFSBucket.find(query).limit(1).first();

Regarding the issue with the download, it’s possible that the problem is related to the SSL configuration. The code seems to be disabling SSL certificate validation, which can be dangerous. Instead of disabling SSL validation, you could try configuring your server to use a valid SSL certificate. If that’s not an option, you could try removing the SSL configuration code and see if that resolves the issue. Here’s an example of how to remove the SSL configuration code:

// Remove this code:
/*
TrustManager[] trustAllCerts = new TrustManager[]{
    new X509TrustManager() {
        public X509Certificate[] getAcceptedIssuers() {
            return null;
        }
        public void checkClientTrusted(
                X509Certificate[] certs, String authType) {
        }
        public void checkServerTrusted(
                X509Certificate[] certs, String authType) {
        }
    }
};

try {
    SSLContext sc = SSLContext.getInstance("SSL");
    sc.init(null, trustAllCerts, new SecureRandom());
    HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
} catch (Exception e) {
}
*/

Here are some suggestions to improve your code:

  1. Avoid unnecessary database queries and simplify the code: If _id field is unique, you can directly retrieve the file using gridFSBucket.find(Filters.eq("_id", new ObjectId(fileId))).first() instead of iterating over the result set and adding all the files to a list.

  2. Use try-with-resources for better resource management: Use try-with-resources statements for GridFSDownloadStream and GridFsResource to automatically close these resources after they are used.

  3. Handle exceptions appropriately: You have a catch block that does nothing, which can hide potential errors. At least, you should log the exception to know what happened.

  4. Set appropriate HTTP headers: You can use MediaType constants provided by Spring framework to set the Content-Type header instead of hard-coding the values. Also, you can use Content-Disposition header value "attachment" instead of "inline" to force the browser to download the file.

Here’s the updated code:

@GetMapping(value = "/downloadFile/{fileId}")
public void downloadFile(@PathVariable("fileId") String fileId, HttpServletResponse response) throws IOException {

    MongoDatabase database = mongoClient.getDatabase("database");
    GridFSBucket gridFSBucket = GridFSBuckets.create(database);

    GridFSFile file = gridFSBucket.find(Filters.eq("_id", new ObjectId(fileId))).first();

    String fileName = file.getMetadata().getString("filename");
    String[] fileNameArray = fileName.split("\\.");
    String extension = fileNameArray[fileNameArray.length - 1].toLowerCase();

    TrustManager[] trustAllCerts = new TrustManager[]{
            new X509TrustManager() {
                public X509Certificate[] getAcceptedIssuers() {
                    return null;
                }
                public void checkClientTrusted(X509Certificate[] certs, String authType) {
                }
                public void checkServerTrusted(X509Certificate[] certs, String authType) {
                }
            }
    };

    try {
        SSLContext sc = SSLContext.getInstance("SSL");
        sc.init(null, trustAllCerts, new SecureRandom());
        HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
    } catch (Exception e) {
        // log the exception
    }

    MediaType mediaType;
    if (extension.equals("pdf")) {
        mediaType = MediaType.APPLICATION_PDF;
    } else {
        mediaType = MediaType.APPLICATION_OCTET_STREAM;
    }
    response.setContentType(mediaType.toString());
    response.setHeader("Content-Disposition", String.format("attachment; filename=\"%s\"", fileName));
    response.setContentLength((int) file.getLength());

    ObjectId objectFileId = new ObjectId(fileId);
    try (GridFSDownloadStream downloadStream = gridFSBucket.openDownloadStream(objectFileId);
         GridFsResource gridFsResource = new GridFsResource(file, downloadStream)) {

        FileCopyUtils.copy(gridFsResource.getInputStream(), response.getOutputStream());
    } catch (IOException e) {
        // log the exception
    }
}

Here’s another solution for you, and this enhances functionalities for the .509

Here’s a simplified version of the code that still takes into account the SSL validation:

@GetMapping(value = "/downloadFile/{fileId}")
public void downloadFile(@PathVariable("fileId") String fileId, HttpServletResponse response) throws IOException {

    MongoDatabase database = mongoClient.getDatabase("database");
    GridFSBucket gridFSBucket = GridFSBuckets.create(database);

    GridFSFile file = gridFSBucket.find(Filters.eq("_id", new ObjectId(fileId))).first();
    if (file == null) {
        response.setStatus(HttpServletResponse.SC_NOT_FOUND);
        return;
    }

    String[] fileNameArray = file.getMetadata().get("filename").toString().split("\\.");
    String extension = fileNameArray[fileNameArray.length - 1].toLowerCase();

    response.setContentType(extension.equals("pdf") ? "application/pdf" : "application/octet-stream");
    response.setHeader("Content-Disposition", String.format("inline; filename=\"%s\"", file.getMetadata().get("filename")));
    response.setContentLength((int) file.getLength());

    SSLContext sslContext;
    try {
        sslContext = SSLContext.getInstance("TLSv1.2");
        sslContext.init(null, new TrustManager[] { new X509TrustManager() {
            @Override
            public X509Certificate[] getAcceptedIssuers() {
                return null;
            }

            @Override
            public void checkClientTrusted(X509Certificate[] certs, String authType) {
            }

            @Override
            public void checkServerTrusted(X509Certificate[] certs, String authType) {
                if (certs.length > 0) {
                    try {
                        certs[0].checkValidity();
                    } catch (CertificateException e) {
                        throw new RuntimeException("Invalid certificate", e);
                    }
                }
            }
        } }, new SecureRandom());
    } catch (Exception e) {
        throw new RuntimeException("Unable to initialize SSL context", e);
    }

    SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
    HttpsURLConnection.setDefaultSSLSocketFactory(sslSocketFactory);

    ObjectId objectFileId = new ObjectId(fileId);
    try (GridFSDownloadStream downloadStream = gridFSBucket.openDownloadStream(objectFileId)) {
        byte[] fileContent = IOUtils.toByteArray(downloadStream);
        IOUtils.write(fileContent, response.getOutputStream());
    } catch (IOException e) {
        throw new RuntimeException("Unable to read file content", e);
    }
}

This version simplifies the code by removing the unnecessary forEach loop and list of files. It also uses a ternary operator to set the content type based on the file extension, and handles the case where the file is not found by setting the HTTP response status to 404.

In addition, the SSL validation has been updated to only check the validity of the first certificate in the chain, because we care for, or want a bunch of them, and throw a runtime exception if it is not valid. This ensures that the SSL certificate is properly validated before downloading the file. Finally, the file content is read using IOUtils from Apache Commons IO and written directly to the response output stream, instead of first creating a GridFsResource object and copying the content using FileCopyUtils.

And some other things to consider, here are some general tips to make the code more efficient as a whole:

  1. Avoid unnecessary database queries: In the current implementation, the code queries the database for the same file up to 5 times with different limits. Instead of this, you can query the database for the file once and store the result in a variable.

  2. Use try-with-resources: The current implementation does not use try-with-resources to automatically close resources. You can use try-with-resources to automatically close resources such as GridFSDownloadStream and GridFsResource.

  3. Use InputStream.transferTo() method: Instead of using FileCopyUtils.copy(), you can use the InputStream.transferTo() method to transfer the file data directly to the response output stream.

  4. Cache SSLContext: Creating an SSLContext is an expensive operation. You can cache the SSLContext instance and reuse it across requests.

Here’s a modified version of the code incorporating these optimizations:

@GetMapping(value = "/downloadFile/{fileId}")
public void downloadFile(@PathVariable("fileId") String fileId, HttpServletRequest request, HttpServletResponse response) throws IOException {

    MongoDatabase database = mongoClient.getDatabase("database");
    GridFSBucket gridFSBucket = GridFSBuckets.create(database);

    // Query the database for the file
    Bson query = Filters.eq("_id", new ObjectId(fileId));
    GridFSFile gridFSFile = gridFSBucket.find(query).first();

    if (gridFSFile == null) {
        response.sendError(HttpServletResponse.SC_NOT_FOUND);
        return;
    }

    String[] fileNameArray = gridFSFile.getMetadata().get("filename").toString().split("\\.");
    String extension = fileNameArray[fileNameArray.length - 1].toLowerCase();

    // Set response headers
    response.setContentType(extension.equals("pdf") ? "application/pdf" : "application/octet-stream");
    response.setHeader("Content-Disposition", String.format("inline; filename=\"%s\"", gridFSFile.getMetadata().get("filename")));
    response.setContentLength((int) gridFSFile.getLength());

    try (GridFSDownloadStream downloadStream = gridFSBucket.openDownloadStream(new ObjectId(fileId));
         InputStream inputStream = new BufferedInputStream(downloadStream);
         OutputStream outputStream = new BufferedOutputStream(response.getOutputStream())) {

        inputStream.transferTo(outputStream);
    }
}

Note that the above code assumes that the SSLContext instance has already been created and cached. If this is not the case, you can create and cache the SSLContext instance using a static initializer or a Singleton pattern.

@No_Bi

1 Like

Edited out some private conversation parts that I meant to write in a mute chat in discord to a friend in video call.