Building a Dating site with Neo4j – Part Twelve

It’s time to add “visions of love” to our dating site. So far our posts have been just text status updates and while it is possible to fall in love with someone’s words, it’s harder if they look like the troll that lives under the bridge. So what’s the plan here? Well… like most databases out there, it’s not a good idea to store images in Neo4j. What we are going to store instead is a link to where the image resides… but we also don’t want to deal with having images all over our file system and then having to worry about storage space and replicating them, geographically distributing them for faster access, etc. Hosting images is a problem solved by the use of Content Delivery Networks. So let’s leverage one and build our feature.

There are a ton of CDNs out there, some are cheap, some are expensive, we are going to go with the “ain’t got none of that sweet VC money” price point and use BunnyCDN. What I like about them, is that they are simple. Every time I see that AWS dashboard with a billion services and having to connect S3 to CloudFront to Route 53 feels like overkill.

BunnyCDN provides an API to send it files, so we are going to use Retrofit again and create a little interface:

 
public interface BunnyCDN {

    @PUT("{storageZoneName}/{path}/{fileName}")
    Call<ResponseBody> upload(@Path("storageZoneName") String storageZoneName,
                              @Path("path") String path,
                              @Path("fileName") String fileName,
                              @Body RequestBody body);

At BunnyCDN I created a Storage Zone called “fives”, we will use that for the first parameter. We will use the username property of our users for the path and we will standardize and clean up our filenames as we get them. More on that in a second. Before we can use our new interface, we need to create a client. According to the documentation we need to pass in an AccessKey in the header and since we are going to be sending binary files we set the content type to “octet-stream” while we are here.

 
          OkHttpClient.Builder builder2 = new OkHttpClient.Builder();
          builder2.addInterceptor(chain -> chain.proceed(
                  chain.request().newBuilder()
                          .addHeader("AccessKey", conf.getString("bunny.key"))
                          .addHeader("Content-Type", "application/octet-stream")
                          .build()));
          OkHttpClient client2 = builder2.build();

Now we can use this client in Retrofit and point it to the storage url.

 
          Retrofit retrofit2 = new Retrofit.Builder()
                  .client(client2)
                  .baseUrl("https://storage.bunnycdn.com/")
                  .build();

          bunny = retrofit2.create(BunnyCDN.class);

Ok, with that plumbing out of the way, we next need to edit our Post model to include a filename. We will go ahead and add a method to check if the post includes a file for convenience.

 
@Data
public class Post {
    ...
    private String filename;

    public boolean hasFile() {
        return filename != null;
    }

Next we’re going to add a little camera button to the right of the “new post”, so we can pass in the file:

 
            <label class="btn btn-primary">
                <span class="icon icon-camera">
                    <input name="file" type="file" style="display: none;">
                </span>
            </label>

It looks like this:

This will add the file to our Post form for us to handle. In our application we will edit the post method to grab the file using an “Upload” object and create a request body to send to BunnyCDN.

 
        post("/post", req -> {
            CommonProfile profile = require(CommonProfile.class);
            String username = profile.getUsername();

            Upload upload = req.file("file");
            String status = req.param("status").value();
            File file = upload.file();
            RequestBody body = RequestBody.create(MediaType.parse("application/octet"),
                    Files.readAllBytes(file.toPath()));

Next we’re going to do something a little weird. Let’s create a custom name for the file using the current time and the extension of the original filename. This is so we don’t have to deal with spaces or funny characters in our url path

 
String time = dateFormat.format(ZonedDateTime.now()) + getFileExtension(upload.name());

With that we are ready to send the file over to BunnyCDN, and assuming it was successful, we create a Post object, set the status and the filename (we are omitting the url, just keeping the bare minimum) and then creating the real Post in our database via the API.

 
            Response<ResponseBody> bunnyResponse =
                    App.bunny.upload("fives", username, time, body).execute();
            if (bunnyResponse.isSuccessful()) {
                Post post = new Post();
                post.setStatus(status);
                post.setFilename(username + "/" + time);
                Response<Post> response = App.api.createPost(username, post).execute();
                if (response.isSuccessful()) {
                    return Results.redirect("/user/" + username);
                }

In our backend, we just have to edit the createPost method and check for the existence of the filename in our input. Neo4j doesn’t store NULLs so if the value doesn’t exist we simply don’t store it. Alternatively, we could have created the time property in our front end, and passed that in to the post, then using the author of the post and the time we could have rebuilt our filename property. I didn’t feel that complexity was worth the cost savings, but something to keep in mind if you are building something like a photo sharing site where people upload thousands of photos.

 
        if (input.containsKey(FILENAME)) {
            post.setProperty(FILENAME, input.get(FILENAME));
        }

Back on our front-end, we need to edit the post partial. We can check to see if the post has a file and if it does, we add an image to our display using the server address of our linked BunnyCDN pull zone. This url can be changed to a custom one, by adding a CNAME record, but we’ll leave it for now.

 
            @if(post.hasFile()) {
                <p>
                    <img src="https://fives.b-cdn.net/@post.getFilename()" style="max-width: 100%" class="rounded">
                </p>
            }

When we add a couple of pictures and then check our posts we can now see:

If we check our Neo4j database with this query:

 
MATCH (n:Post) RETURN n.time, n.status, n.filename LIMIT 25

We can see some Posts have filenames and others return a null value when they don’t. Remember, every node in Neo4j can have different properties, even if they are the same “type” of node or share the same labels.

We’ve come pretty far, but there is still a ton to be added. We don’t have the “hide” effect the low fives should have on the posts, we haven’t built any kind of recommendations, we don’t have any messaging or email capabilities… I think we’ll keep going, but we may throw in some blog posts on different topics along the way. The source code is on github, add yourself as a “watcher” if you want to be notified of changes, or maybe just follow me on Twitter for further updates.

Tagged , , , , , , , , , ,

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: