Problem/Motivation

In a headless site, one almost certainly needs to provide downloadable links for files and/or embed styled images. Right now, file entities only provide a filename and a relatively useless URI. If the public/private path is not known in advance, it is impossible to construct a direct URL to the file in question. It also impossible to generate image src URLs for styled images because one would not have the necessary itok.

Proposed resolution

Research the best method of including these direct download links. Possible solutions are computed fields, extra attributes specific to file entities, or information in the meta section.

The document might look something like this:

{
  "meta": {
    "download_urls": {
      "canonical": "http://example.com/sites/default/files/example.png",
      "image_styles": {
        "large": "http://example.com/sites/default/files/styles/large/public/example.png?itok=Ba6brbPN",
        "medium": "http://example.com/sites/default/files/styles/medium/public/example.png?itok=s1_JGK_m",
        "thumbnail": "http://example.com/sites/default/files/styles/thumbnail/public/example.png?itok=FQ44e_kN"
      }
    }
  }
}

Remaining tasks

Come up with an implementation strategy and make it happen.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

gabesullice created an issue. See original summary.

e0ipso’s picture

I don't think meta is quite the perfect way to put this information. It could perfectly be a computed field in the image attributes (in the vein of #2775205: [PP-2] [FEATURE] Discus how to make JSON API use text input formats for long text fields).

As for the image styles I'm more on the fence. That is not something pertaining to the data model (which is what we're exposing). There is a lot of value on getting the processed images, but this is something very hard to decouple since image styles imply presentation logic.

Maybe image styles and responsive images deserve their own jsonapi-contrib?

e0ipso’s picture

Generating the download URL for files (and images in particular) is something that we may want to add at the core level (like the processed text did).

Generating a list of the available image styles given an image could also be done in core in an RPC endpoint.

I'll try to get get @dawehner and @WimLeers involved to see what they think about it.

gabesullice’s picture

;) I had a feeling that the "meta" proposal wouldn't go over well. I even mentioned as much to @pixelwhip when we were thinking about this. It was worth a shot though.

I spent a few hours last night thinking about how best to do this. It's a difficult problem because the file entity abstraction means there's just no good place to put direct links. The most pure implementation might actually be a relationship on the file entity to a file object, perhaps with the URI as a key.

A couple other thoughts (I'm just spit-balling here):

1. One could add a computed field the entity referencing the file/image entity with a usable URL.
2. One could put a computed field on the file entity, but this means yet another round-trip for the server.

Both these solutions could work, but when you put image styles into the mix, then you have to start thinking about something like view modes for our resources, which I don't feel is really acceptable at this point. It muddles up the separation between the backend and the presentation layer (as you mentioned)

In order to keep that separation clear, I think the best way to include styled images is to not have a site administrator choose which style is represented by the computed field, but instead present all available styles and associated URLs for an image. This allows the presentation layer to choose the most appropriate URL. This is what's in the example document in the issue summary.

An RPC endpoint might make sense for actually triggering the process of generating a styled image, but it's not going to be ideal for 80-90% of use cases. My best guess is that a frontend developer really just wants the image URL at hand or a choice of URLs at hand to choose from.

@e0ipso How were you imagining another contrib solution might work?

Edit: grammar and punctuation

e0ipso’s picture

Both these solutions could work, but when you put image styles into the mix

I agree, it all goes sour with the image styles, because they hold (almost) no semantics. They're purely presentational, and we can't care about that because the API needs to be presentation agnostic.

That being said, maybe the way to go is to compute the file URL and make use of the Picture module. Or a custom entity that has computed fields for all the image styles that are needed (that can be linked from the original Image entity).

That being said, my goal would be to have a jsonapi extension that allows consumers to specify the image transformations to be made to an image. Something like:

/api/blah/foo?
imageDerivatives[field_image1][focalPoint][x]=12&
imageDerivatives[field_image1][focalPoint][y]=34&
imageDerivatives[field_image1][resize][width]=600&
imageDerivatives[field_image1][resize][width]=600&
imageDerivatives[field_image1][resize][width]=800&
…

Then the server returns the closest image URL that it can deliver based on the existing image styles. Alternatively we could have GD or ImageMagick deliver the exact requirements. Of course, this could be done using any other tech besides Drupal via a proxy that generates those images and URLs. Another alternative is that based on those parameters Drupal delivers the image URL with special parameters that http://cloudinary.com/ or https://www.akamai.com/us/en/solutions/products/web-performance/cloudlet... can interpret to do the job transparently.

Of course this is all brainstorming at this phase. In any case this is why I think that a different contrib can handle the myriad of solutions that are out there.

e0ipso’s picture

Wim Leers’s picture

This is also a problem in core's REST. For e.g. image fields. It has not been solved there yet, nor is there even an issue. The closest we have is the one you mentioned. @dawehner added it to #2794263: REST: top priorities for Drupal 8.3.x (and I agree).

gabesullice’s picture

@e0ipso maybe we're both narrowing in on a way this might be done, see:

The most pure implementation might actually be a relationship on the file entity to a file object, perhaps with the URI as a key.

Or a custom entity that has computed fields for all the image styles that are needed (that can be linked from the original Image entity).

e0ipso’s picture

Initial patch. This still needs testing, but it shapes the implementation.

GET /api/node/article/737905c9-dc72-45fd-b10d-2205b925a7c6?_format=api_json&include=type,field_image&fields[file--file]=url,uri&fields[node--article]=field_image,type

{
    "data": {
        "type": "node--article",
        "id": "737905c9-dc72-45fd-b10d-2205b925a7c6",
        "relationships": {
            "type": {
                "data": {
                    "type": "node_type--node_type",
                    "id": "4fae95d3-2c67-4897-a95d-f0a0608b8ce3"
                },
                "links": {
                    "self": "http://d8dev.local/api/node/article/737905c9-dc72-45fd-b10d-2205b925a7c6/relationships/type?_format=api_json",
                    "related": "http://d8dev.local/api/node/article/737905c9-dc72-45fd-b10d-2205b925a7c6/type?_format=api_json"
                }
            },
            "field_image": {
                "data": {
                    "type": "file--file",
                    "id": "786d2571-f52f-4db2-bf78-a50aa98bb3fa"
                },
                "links": {
                    "self": "http://d8dev.local/api/node/article/737905c9-dc72-45fd-b10d-2205b925a7c6/relationships/field_image?_format=api_json",
                    "related": "http://d8dev.local/api/node/article/737905c9-dc72-45fd-b10d-2205b925a7c6/field_image?_format=api_json"
                }
            }
        },
        "links": {
            "self": "http://d8dev.local/api/node/article/737905c9-dc72-45fd-b10d-2205b925a7c6?_format=api_json"
        }
    },
    "links": {
        "self": "http://d8dev.local/api/node/article/737905c9-dc72-45fd-b10d-2205b925a7c6?_format=api_json&include=type%2Cfield_image&fields[file--file]=url%2Curi&fields[node--article]=field_image%2Ctype"
    },
    "included": [
        {
            "data": {
                "type": "node_type--node_type",
                "id": "4fae95d3-2c67-4897-a95d-f0a0608b8ce3",
                "attributes": {
                    "uuid": "4fae95d3-2c67-4897-a95d-f0a0608b8ce3",
                    "langcode": "en",
                    "status": true,
                    "dependencies": [],
                    "_core": "AeW1SEDgb1OTQACAWGhzvMknMYAJlcZu0jljfeU3oso",
                    "name": "Article",
                    "type": "article",
                    "description": "Use <em>articles</em> for time-sensitive content like news, press releases or blog posts.",
                    "help": "",
                    "new_revision": true,
                    "preview_mode": 1,
                    "display_submitted": true
                },
                "links": {
                    "self": "http://d8dev.local/api/node_type/node_type/4fae95d3-2c67-4897-a95d-f0a0608b8ce3?_format=api_json"
                }
            },
            "links": {
                "self": "http://d8dev.local/api/node/article/737905c9-dc72-45fd-b10d-2205b925a7c6?_format=api_json&include=type%2Cfield_image&fields[file--file]=url%2Curi&fields[node--article]=field_image%2Ctype"
            }
        },
        {
            "data": {
                "type": "file--file",
                "id": "786d2571-f52f-4db2-bf78-a50aa98bb3fa",
                "attributes": {
                    "uri": "public://2016-09/1. node (node) 2016-08-31 16-55-17_3.png",
                    "url": "http://d8dev.local/sites/default/files/2016-09/1.%20node%20%28node%29%202016-08-31%2016-55-17_3.png"
                },
                "links": {
                    "self": "http://d8dev.local/api/file/file/786d2571-f52f-4db2-bf78-a50aa98bb3fa?_format=api_json"
                }
            },
            "links": {
                "self": "http://d8dev.local/api/node/article/737905c9-dc72-45fd-b10d-2205b925a7c6?_format=api_json&include=type%2Cfield_image&fields[file--file]=url%2Curi&fields[node--article]=field_image%2Ctype"
            }
        }
    ]
}

Notice that we still need to be able to bubble up the cacheability metatada for the generated link. But that can happen after the testing.

e0ipso’s picture

Status: Active » Needs review

I think this code as is (with all the required improvements) could be leveraged by REST core. Maybe @WimLeers has an opinion on this.

gabesullice’s picture

+++ b/src/Plugin/FileDownloadUrl.php
@@ -0,0 +1,22 @@
+      return ['value' => file_create_url($uri['value'])];

I think we should be able to do this in a testable way. I'll find some code that I wrote the other day for this.

Edit: Code I could find was for image styles, not straight file URLs.

Wim Leers’s picture

  1. --- /dev/null
    +++ b/jsonapi.module
    

    So sad you have to create a .module file for this :D

  2. +++ b/jsonapi.module
    @@ -0,0 +1,28 @@
    +    $fields['url'] = BaseFieldDefinition::create('uri')
    

    url instead of uri makes perfect sense.

  3. +++ b/jsonapi.module
    @@ -0,0 +1,28 @@
    +      ->setDisplayOptions('view', array(
    +        'label' => 'above',
    +        'weight' => -5,
    

    Because you did not do setDisplayConfigurable('url', TRUE), this is not exposed in Field UI, right?

    I think this is a solution worth considering for sure. We'd need input from Field API and Entity API experts though.

    I think a great person to do a first round of review of this is Berdir.

  4. +++ b/src/Plugin/FileDownloadUrl.php
    @@ -0,0 +1,22 @@
    +    $values = array_map(function ($uri) {
    +      return ['value' => file_create_url($uri['value'])];
    +    }, $this->getEntity()->get('uri')->getValue());
    +    $this->setValue($values);
    

    This code looks great :) Very, very elegant.

Wim Leers’s picture

Notice that we still need to be able to bubble up the cacheability metatada for the generated link.

We don't have cacheability metadata for file URLs. They don't get path processors etc applied. So that's a non-issue :)

That being said, this should probably use file_url_transform_relative(file_create_url(…)), so you end up with root-relative URLs that are not tied to a particular hostname in a multisite installation.

e0ipso’s picture

Thanks for the feedback @gabesullice and @Wim Leers. Here's the next iteration of this.

The tests are not passing yet, but I could not find what is wrong with them :-@

The mocked ->getUris() returns NULL instead of the specified value when called in the constructor. I may need more PHPUnit foo. I'm more a Prophecy guy, but I don't think you can write this test with that.

Help?

e0ipso’s picture

The last submitted patch, 14: 2793809--image-download-url--14.patch, failed testing.

The last submitted patch, 14: 2793809--image-download-url--14.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 15: 2793809--image-download-url--15.patch, failed testing.

The last submitted patch, 15: 2793809--image-download-url--15.patch, failed testing.

e0ipso’s picture

Fixed tests! Merging if green.

  • e0ipso committed c490936 on 8.x-1.x
    Issue #2793809 by e0ipso, gabesullice, Wim Leers: [FEATURE] Provide...
Wim Leers’s picture

Nit:

+++ b/src/Plugin/FileDownloadUrl.php
@@ -46,8 +43,21 @@ class FileDownloadUrl extends FieldItemList {
     return $this->getEntity()->get('uri')->getValue();

This should use \Drupal\file\Entity\File::getFileUri().

You committed this already, which is fine for the JSON API module of course. :) But I think we may want to bring something like this to Drupal core. You should add a @todo to either point to the core issue (#2517030: Add a URL formatter for the image field), or to state that this is a temporary measure that may fail to work if another contrib module does the same thing, or both.

e0ipso’s picture

Yup. You are right @Wim Leers, I have the tendency to merge very quick and iterate over it. I am allergic to analysis paralysis and many times I overcompensate. I'll make sure to add those comments.

This should use \Drupal\file\Entity\File::getFileUri().

:+1:

I created a follow up: #2797823: [CLEANUP] Indicate that the file URL is a temporary solution

Wim Leers’s picture

I am allergic to analysis paralysis and many times I overcompensate

Hah :D

e0ipso’s picture

Status: Needs review » Fixed

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

justinlevi’s picture

Adding this comment here as it took me a minute to figure out the correct request to get to the image URL for a image media entity on a content type

http://[HOST]/jsonapi/node/landing_page/[UUID]?include=field_image,field_image.image,field_image.image.file--file&fields[field_image]=image&fields[file--file]=uri,url

e0ipso’s picture

@justinlevi would you mind adding a note somewhere in https://www.drupal.org/docs/8/modules/json-api as well?

Many thanks!

justinlevi’s picture

e0ipso’s picture

@justinlevi thanks!

arefen’s picture

HI. sample query on comment #28 not work for me on core jsonapi in drupal 8.7.
How can i write a query for that

Hygglo’s picture

Same issue here, the solution provided in #28 does not work in 8.7