atamanroman
atamanroman

Reputation: 11818

How to get a binary stream by GridFS ObjectId with Spring Data MongoDB

I can't figure out how to stream a binary file from GridFS with spring-data-mongodb and its GridFSTemplate when I already have the right ObjectId.

GridFSTemplate returns either GridFSResource (getResource()) or GridFSFile (findX()).

I can get the GridFSFile by ID:

// no way to get the InputStream?
GridFSFile file = gridFsTemplate.findOne(Query.query(Criteria.where("_id").is(id)))

but there is no obvious way how to get an InputStream for that GridFSFile.

Only GridFSResource allows me to get hold of the corresonding InputStream with InputStreamResource#getInputstream. But the only way to get a GridFSResource is by its filename.

// no way to get GridFSResource by ID?
GridFSResource resource = gridFsTemplate.getResource("test.jpeg");
return resource.getInputStream();

Somehow the GridFsTemplate API implies that filenames are unique - which they are not. The GridFsTemplate implementation just returns the first element.

Now I'm using the native MongoDB API and everything makes sense again:

GridFS gridFs = new GridFs(mongo);
GridFSDBFile nativeFile = gridFs.find(blobId);
return nativeFile.getInputStream();

It looks like I'm misunderstanding the fundamental concepts behind the Spring Data Mongo GridFS abstraction. I'd expect (at least) one of the following things to be possible/true:

Am I wrong or is there something odd with this particular piece of the Spring Data MongoDB API?

Upvotes: 17

Views: 14467

Answers (10)

M. Justin
M. Justin

Reputation: 21239

Spring Data 2.1.0 added an overload of getResource() to GridFsTemplate that returns the GridFsResource for a given GridFsFile. GridFsResource has a method to get the InputStream. Therefore, if you're on at least this version of Spring Data, you can get the InputStream by making two calls to the GridFsTemplate:

GridFSFile gridFsFile =
        gridFsTemplate.findOne(Query.query(Criteria.where("_id").is(id)));

// In real code, perform necessary null checks in case the file doesn't exist

GridFsResource resource = gridFsTemplate.getResource(gridFsFile);
InputStream inputStream = resource.getInputStream();

Upvotes: 2

Paul Warren
Paul Warren

Reputation: 2479

Have you considered using Spring Content for Mongo for the content storage piece on your solution?

Assuming you are using Spring Boot as well as Spring Data Mongo then it might look something like the following:

pom.xml

<dependency>
    <groupId>com.github.paulcwarren</groupId>
    <artifactId>spring-content-mongo-boot-starter</artifactId>
    <version>0.0.10</version>
</dependency>
<dependency>
    <groupId>com.github.paulcwarren</groupId>
    <artifactId>spring-content-rest-boot-starter</artifactId>
    <version>0.0.10</version>
</dependency>

Update your Spring Data Mongo entity, with the following attributes:

@ContentId
private String contentId;

@ContentLength 
private long contentLength = 0L;

@MimeType
private String mimeType;

Add a store interface:

@StoreRestResource(path="content")
public interface MongoContentStore extends ContentStore<YourEntity, String> {
}

That's all that you need. When you application starts Spring Content will see the dependencies on the Spring Content Mongo/REST modules and it will inject an implementation of the MongonContenStore store for GridFs as well as an implementation of a controller that supports full CRUD functionality and maps those operations down onto the underlying store interface. The REST endpoint will be available under /content.

i.e.

curl -X PUT /content/{entityId} will create or update an entity's image

curl -X GET /content/{entityId} will fetch the entity's image

curl -X DELETE /content/{entityId} will delete the entity's image

There are a couple of getting started guides here. They use Spring Content for the filesystem but the modules are interchangeable. The Mongo reference guide is here. And there is a tutorial video here.

HTH

Upvotes: 2

Chris
Chris

Reputation: 3657

Old question I know, but trying to do this in 2019 using WebFlux, I had to do the following

  public Mono<GridFsResource> getImageFromDatabase(final String id) {

    return Mono.fromCallable(
        () ->
            this.gridFsTemplate.getResource(
                Objects.requireNonNull(
                        this.gridFsTemplate.findOne(new Query(Criteria.where("_id").is(id))))
                    .getFilename()));
  }

Which will give you a Mono which can be returned in a controller. I'm sure there is a nicer solution however.

Upvotes: 0

Rohit Basu
Rohit Basu

Reputation: 218

@RequestMapping(value = "/api ")
public class AttachmentController {

private final GridFsOperations gridFsOperations;

@Autowired
public AttachmentController(GridFsOperations gridFsOperations) {
    this.gridFsOperations = gridFsOperations;
}

@GetMapping("/file/{fileId}")
public ResponseEntity<Resource> getFile(@PathVariable String fileId) {
GridFSFile file = 
gridFsOperations.findOne(Query.query(Criteria.where("_id").is(fileId)));

    return ResponseEntity.ok()
            .contentLength(file.getLength())
            .body(gridFsOperations.getResource(file));
}

Upvotes: 0

Rohit Basu
Rohit Basu

Reputation: 218

getResource(com.mongodb.client.gridfs.model.GridFSFile file) function of GridFsTemplate returns the GridFsResource for a GridFSFile.

GridFSFile gridfsFile= gridFsTemplate.findOne(new 
Query(Criteria.where("filename").is(fileName)));
GridFsResource gridFSResource= gridFsTemplate.getResource(gridfsFile);
InputStream inputStream= gridFSResource.getInputStream();

If the above one is not working in some higher version of Spring boot, use the bellow:

GridFSFile gridfsFile= gridFsTemplate.findOne(new 
Query(Criteria.where("filename").is(fileName)));
//or
GridFSFile  gridfsFile = 
gridFsOperations.findOne(Query.query(Criteria.where("filename").is(fileName)));
 return ResponseEntity.ok()
                .contentLength(gridFsdbFile.getLength())
                .contentType(MediaType.valueOf("image/png"))
                .body(gridFsOperations.getResource(gridFsdbFile));

Upvotes: 0

Bodil
Bodil

Reputation: 161

Wrap the GridFSFile in a GridFsResource or use this

GridFSFile file = gridFsTemplate.findOne(Query.query(Criteria.where("_id").is(fileId)));
GridFsResource resource = gridFsTemplate.getResource(file);
return resource.getInputStream();

Upvotes: 0

Niklas
Niklas

Reputation: 55

i discovered the solution to this problem!

Just wrap the GridFSFile in a GridFsResource! This is designed to be instantiated with a GridFSFile.

public GridFsResource getUploadedFileResource(String id) {
    var file = this.gridFsTemplate.findOne(new Query(Criteria.where("_id").is(id)));
    return new GridFsResource(file);
}

@GetMapping("/{userId}/files/{id}")
public ResponseEntity<InputStreamResource> getUploadedFile(
    @PathVariable Long userId,
    @PathVariable String id
){
    var user = userService
        .getCurrentUser()
        .orElseThrow(EntityNotFoundException::new);

    var resource = userService.getUploadedFileResource(id);

    try {
        return ResponseEntity
            .ok()
            .contentType(MediaType.parseMediaType(resource.getContentType()))
            .contentLength(resource.contentLength())
            .body(resource);
    } catch (IOException e) {
        return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
    }


}

The great advantage of this is, that you can directly pass the GridFsResource to a ResponseEntity due to the fact, that the GridFsResource extends a InputStreamResource.

Hope this helps!

Greetings Niklas

Upvotes: 1

Steffen Jacobs
Steffen Jacobs

Reputation: 126

I stumbled upon this, too. And I am actually pretty shocked that the GridFsTemplate has been designed like this... Anyway, my ugly "solution" to this so far:

public GridFsResource download(String fileId) {
    GridFSFile file = gridFsTemplate.findOne(Query.query(Criteria.where("_id").is(fileId)));

    return new GridFsResource(file, getGridFs().openDownloadStream(file.getObjectId()));
}

private GridFSBucket getGridFs() {

    MongoDatabase db = mongoDbFactory.getDb();
    return GridFSBuckets.create(db);
}

Note: You have to inject the MongoDbFactory for this to work...

Upvotes: 11

deii
deii

Reputation: 113

There is a bit mess in these types:

From Spring GridFsTemplate source:

public getResource(String location) {

    GridFSFile file = findOne(query(whereFilename().is(location)));
    return file != null ? new GridFsResource(file, getGridFs().openDownloadStream(location)) : null;
}

There is an ugly solution:

@Autowired
private GridFsTemplate template;

@Autowired
private GridFsOperations operations;

public InputStream loadResource(ObjectId id) throws IOException {
    GridFSFile file = template.findOne(query(where("_id").is(id)));
    GridFsResource resource = template.getResource(file.getFilename());

    GridFSFile file = operations.findOne(query(where("_id").is(id)));
    GridFsResource resource = operations.getResource(file.getFilename());
    return resource.getInputStream();
}

Upvotes: 6

Steve H
Steve H

Reputation: 11

GridFSDBFile file = ... 
ByteArrayOutputStream baos = new ByteArrayOutputStream();
file.writeTo(baos);
byte[] ba = baos.toByteArray()

Upvotes: -3

Related Questions