Originally posted by **grispin** January 5, 2024
Here a the latest proposal for a Media Upload specification using the OData stream specification
---------------------------------------------------------------------
# Media Upload
This is a proposed solution to RESO media upload using the OData streams specification [Section 4.5.11](https://docs.oasis-open.org/odata/odata-json-format/v4.0/os/odata-json-format-v4.0-os.html).
## Data Representation
### Data Structure
These are the required fields to represent the upload process for media
```xml
```
## Status of the Media
The status of the media will be tracked on the Media resource. The MediaStatus field will show the state of the media.
* `Incomplete` - the Media record that does not have a complete record (typically missing byte array)
* `Processing` - the Media record is currently being processed by the backend
* `Complete` - the Media record is complete and can be used with it's related resource
* `Deleted` - the Media is no longer published with its related resources
* `Rejected` - the Media has failed post processing for some reason. Details of the problem will be presented in the MediaStatusDescription field.
Many validation tests for media cannot be performed until the complete image is uploaded and this can take time to process. These processes are often asynchronous.
Some examples of `Rejected` reasons could be:
* Media stream does not match MediaType (Sent a video for a photo Media record)
* The internal image checksum failed
* Image failed copyright ID check
## Media Upload Process - Happy Path
The creation of the media resource will be done using as a standard OData update process. (Defined in RCP-10). The post should include all known metadata about the media with the exception of only the media byte stream.
There will be multiple responses shown for the requests as we will be illustrating both when the media is handled directly by the WebApi server and when the WebApi server is using external references.
### Create Media Resource
Create the initial media record using the OData standard. This could be an expanded record on another resource as a secondary approach but the example will be with Media as a tier 1 Resource/Model.
**REQUEST**
```http
POST https://api.my-webapi.io/Media
OData-Version: 4.01
Content-Type: application/json
Accept: application/json
Prefer: return=representation
```
```json
{
"Caption": "Ipsum Lorum",
"Order": 1,
"ImageType": "image/jpeg",
}
```
**RESPONSE - External Media storage**
```http
HTTP/2 201 Created
OData-Version: 4.01
EntityId: "12345"
Location: https://api.my-webapi.io/Media('12345')
Content-Length: 200
Content-Type: application/json
Preference-Applied: return=representation
```
```json
{
"@odata.context": "https://api.my-webapi.io/$metadata#Media/$entity",
"@odata.id": "Media('12345')",
"@odata.editLink": "https://api.my-webapi.io/Media('12345')",
"@odata.mediaReadLink": "https://api-my-webapi.io/image/not_available.jpg",
"@odata.mediaEditLink": "https://storage.my-webapi.io/media/12345.jpg?authentication_token=my-one-time-use-auth-token-12345-zyxwut",
"@odata.etag": "W/\"aBcDeFgHiJkLmNoPqRsTuVwXyz\"",
"MediaObjectID": "12345",
"Caption": "Ipsum Lorum",
"Order": 1,
"ImageType": "image/jpeg",
"MediaStatus": "Incomplete",
"MediaStatusDescription": "Awaiting Byte Stream",
}
```
**RESPONSE - Media through WebApi**
When the `MediaEditLink` annotation is not provided, there is an implicit URL for the editMediaLink `https://api.my-webapi.io/Media('12345')/$value` provided by the Odata specification
```http
HTTP/2 201 Created
OData-Version: 4.01
EntityId: "12345"
Location: https://api.my-webapi.io/Media('12345')
Content-Length: 200
Content-Type: application/json
Preference-Applied: return=representation
```
```json
{
"@odata.context":"https://api.my-webapi.io/$metadata#Media/$entity",
"@odata.id":"Media('12345')",
"@odata.editLink":"https://api.my-webapi.io/Media('12345')",
"@odata.mediaReadLink": "https://api-my-webapi.io/image/not_available.jpg",
"@odata.etag": "W/\"aBcDeFgHiJkLmNoPqRsTuVwXyz\"",
"MediaObjectID": "12345",
"Caption": "Ipsum Lorum",
"Order": 1,
"ImageType": "image/jpeg",
"MediaStatus": "Incomplete",
"MediaStatusDescription": "Awaiting Byte Stream",
}
```
The return from the POST will return the Media record with the object in an `Incomplete` status. The next step in the process is to provide the byte stream for the media object to the server at the provided endpoint. The client MUST use the `odata.mediaEditLink` if provided and use the implicit URL only if one is not provided as per the OData specification.
## Upload the Media byte array
The byte stream upload is a simple HTTP POST transaction to the provided to the endpoint. If it is an request outside the WebApi domain, then all required data (authentication, etc.) must be provided in the `mediaEditLink` URL. If `mediaEditLink` is not in WebApi domain, then the established authentication (cookies, authentication headers, etc.) must continue to be provided.
**REQUEST - External Target**
```http
POST https://storage.my-webapi.io/media/12345.jpg?authentication_token=my-one-time-use-auth-token=12345-zyxwut
Content-Type: image/jpeg
<>
```
**RESPONSE**
```http
HTTP/2 200 OK
```
**REQUEST - WebApi Target**
```http
POST https://api.my-webapi.io/Media('12345')/$value
Content-Type: image/jpeg
<>
```
**RESPONSE**
```http
HTTP/2 200 OK
```
## Media post processing
The original media record will transition to Processing state while the server prepares the media for distribution downstream. The implementation will do all the work required for distribution before changing the state to `Complete`
If any processing fails, the media record will be have a status of `Rejected` and the reasoning for the Media submitter will be populated in the `MediaStatusDescription` field for actions to be taken.
Rules may be written against the `MediaStatus` field to handle the order of operations required to publish a listing. Eg. Media must be complete before it can be attached to a listing or Media attached to a listing must all be Complete before a listing can be on-market.
## Successful Media Upload
The media record can be queried to confirm the media has successfully been processed.
**REQUEST**
```http
GET https://api.my-webapi.io/Media('12345')
OData-Version: 4.01
Content-Type: application/json
Accept: application/json
Prefer: return=representation
```
**RESPONSE - Through External Resource**
```http
HTTP/2 200
OData-Version: 4.01
EntityId: "12345"
Location: https://api.my-webapi.io/Media('12345')
Content-Length: 200
Content-Type: application/json
Preference-Applied: return=representation
```
```json
{
"@odata.context": "https://api.my-webapi.io/$metadata#Media/$entity",
"@odata.id": "Media('12345')",
"@odata.editLink": "https://api.webapi.io/Media('12345')",
"@odata.mediaReadLink": "https://storage.my-webapi.io/media/12345.jpg",
"@odata.mediaEditLink": "https://storage.my-webapi.io/media/12345.jpg?authentication_token=my-one-time-use-auth-token-12345-zyxwut",
"@odata.etag": "W/\"aBcDeFgHiJkLmNoPqRsTuVwXyz\"",
"MediaObjectID": "12345",
"Caption": "Ipsum Lorum",
"Order": 1,
"ImageType": "image/jpeg",
"MediaStatus": "Complete",
"MediaStatusDescription": "Processing Complete",
}
```
**RESPONSE - Through WebApi**
```http
HTTP/2 200
OData-Version: 4.01
EntityId: "12345"
Location: https://api.my-webapi.io/Media('12345')
Content-Length: 200
Content-Type: application/json
Preference-Applied: return=representation
```
```json
{
"@odata.context": "https://api.my-webapi.io/$metadata#Media/$entity",
"@odata.id": "Media('12345')",
"@odata.editLink": "https://api.webapi.io/Media('12345')",
"@odata.mediaReadLink": "https://api.webapi.io/Media('12345')/$value",
"@odata.etag": "W/\"aBcDeFgHiJkLmNoPqRsTuVwXyz\"",
"MediaObjectID": "12345",
"Caption": "Ipsum Lorum",
"Order": 1,
"ImageType": "image/jpeg",
"MediaStatus": "Complete",
"MediaStatusDescription": "Processing Complete",
}
```
## Media Upload Process error states
These are the cases where the media record will not reach the `Complete` state without additional actions.
### Media Record created but no byte stream provided
This will leave the media record in a Incomplete state and the vendor can do culling/clean up as required. The client can requery the Media record for the URLs to attempt to re-use the Media record. Rules can be used to prevent the transition of the listing until valid media is provided if required.
**REQUEST**
```http
GET https://api.my-webapi.io/Media('12345')
OData-Version: 4.01
Content-Type: application/json
Accept: application/json
Prefer: return=representation
```
**RESPONSE**
```http
HTTP/2 200
OData-Version: 4.01
EntityId: "12345"
Location: https://api.my-webapi.io/Media('12345')
Content-Length: 200
Content-Type: application/json
Preference-Applied: return=representation
```
```json
{
"@odata.context": "https://api.my-webapi.io/$metadata#Media/$entity",
"@odata.id": "Media('12345')",
"@odata.editLink": "https://api.webapi.io/Media('12345')",
"@odata.mediaReadLink": "https://api-my-webapi.io/image/not_available.jpg",
"@odata.mediaEditLink": "https://storage.my-webapi.io/media/12345.jpg?authentication_token=my-one-time-use-auth-token-12345-zyxwut",
"@odata.etag": "W/\"aBcDeFgHiJkLmNoPqRsTuVwXyz\"",
"MediaObjectID": "12345",
"Caption": "Ipsum Lorum",
"Order": 1,
"ImageType": "image/jpeg",
"MediaStatus": "Incomplete",
"MediaStatusDescription": "Awaiting Byte Stream",
}
```
**REQUEST**
```http
POST https://storage.my-webapi.io/media/12345.jpg?authentication_token=my-one-time-use-auth-token=12345-zyxwut
Content-Type: image/jpeg
<>
```
**RESPONSE**
```http
HTTP/2 200 OK
```
## Error Uploading Media
If a client gets a HTTP 4xx error (except for 409 see Write-Once behaviour below) uploading media they validate their `mediaEditLink` URL and re-attempt the upload. After a couple of retries, they should consider the upload a failure and contact the server provider.
This can happen in normal operation if the `mediaEditLink` is a pre-signed URL for a storage provider and the authentication credentials have timed out. Requiring the Media record would get a updated URL with a fresh set of credentials.
If the server has timed out the Media record due to lack of completion (a byte stream was not uploaded within 30 days for example), the server will transition the `MediaStatus` to the `Rejected` state.
### Refreshed Case
In this case, the client will get a new `mediaEditLink` URL as the authentication token was updated.
**REQUEST**
```http
POST https://api.my-webapi.io/Media('12345')
OData-Version: 4.01
Content-Type: application/json
Accept: application/json
Prefer: return=representation
```
**RESPONSE**
```http
HTTP/2 200
OData-Version: 4.01
EntityId: "12345"
Location: https://api.my-webapi.io/Media('12345')
Content-Length: 200
Content-Type: application/json
Preference-Applied: return=representation
```
```json
{
"@odata.context": "https://api.my-webapi.io/$metadata#Media/$entity",
"@odata.id": "Media('12345')",
"@odata.editLink": "https://api.webapi.io/Media('12345')",
"@odata.mediaReadLink": "https://api-my-webapi.io/image/not_available.jpg",
"@odata.mediaEditLink": "https://storage.my-webapi.io/media/12345.jpg?authentication_token=my-one-time-use-auth-token-12345-take2",
"@odata.etag": "W/\"aBcDeFgHiJkLmNoPqRsTuVwXyz\"",
"MediaObjectID": "12345",
"Caption": "Ipsum Lorum",
"Order": 1,
"ImageType": "image/jpeg",
"MediaStatus": "Incomplete",
"MediaStatusDescription": "Awaiting Byte Stream",
}
```
**REQUEST**
```http
POST https://storage.my-webapi.io/media/12345.jpg?authentication_token=my-one-time-use-auth-token=12345-take2
Content-Type: image/jpeg
<>
```
**RESPONSE**
```http
HTTP/2 200 OK
```
### Rejected Case
In the case of a `Rejected` media record, the client should replace the media record by adding a new record and deleting the old one.
**REQUEST**
```http
GET https://api.my-webapi.io/Media('12345')
OData-Version: 4.01
Content-Type: application/json
Accept: application/json
Prefer: return=representation
```
**RESPONSE**
```http
HTTP/2 200
OData-Version: 4.01
EntityId: "12345"
Location: https://api.my-webapi.io/Media('12345')
Content-Length: 200
Content-Type: application/json
Preference-Applied: return=representation
```
```json
{
"@odata.context": "https://api.my-webapi.io/$metadata#Media/$entity",
"@odata.id": "Media('12345')",
"@odata.editLink": "https://api.webapi.io/Media('12345')",
"@odata.etag": "W/\"aBcDeFgHiJkLmNoPqRsTuVwXyz\"",
"MediaObjectID": "12345",
"Caption": "Ipsum Lorum",
"Order": 1,
"ImageType": "image/jpeg",
"MediaStatus": "Rejected",
"MediaStatusDescription": "Media record has timed out",
}
```
```http
POST https://api.my-webapi.io/Media('12345')/$value
Content-Type: image/jpeg
<>
```
**RESPONSE**
```http
HTTP/2 403 Forbidden
```
## Media uploaded but byte stream rejected
The media provided is not of the correct type or failed some other validation process after upload but before distribution occurred.
### Resubmit Case
This is the case where the server allows the client to replace the byte stream of a media record.
REQUEST
```http
GET https://api.my-webapi.io/Media('12345')
OData-Version: 4.01
Content-Type: application/json
Accept: application/json
Prefer: return=representation
```
**RESPONSE**
```http
HTTP/2 200
OData-Version: 4.01
EntityId: "12345"
Location: https://api.my-webapi.io/Media('12345')
Content-Length: 200
Content-Type: application/json
Preference-Applied: return=representation
```
```json
{
"@odata.context": "https://api.my-webapi.io/$metadata#Media/$entity",
"@odata.id": "Media('12345')",
"@odata.editLink": "https://api.webapi.io/Media('12345')",
"@odata.mediaReadLink": "https://api-my-webapi.io/image/not_available.jpg",
"@odata.mediaEditLink": "https://storage.my-webapi.io/media/12345.jpg?authentication_token=my-one-time-use-auth-token-12345-take2",
"@odata.etag": "W/\"aBcDeFgHiJkLmNoPqRsTuVwXyz\"",
"MediaObjectID": "12345",
"Caption": "Ipsum Lorum",
"Order": 1,
"ImageType": "image/jpeg",
"MediaStatus": "Rejected",
"MediaStatusDescription": "Byte stream is not of type image/jpeg",
}
```
** REQUEST **
```http
POST https://storage.my-webapi.io/media/12345.jpg?authentication_token=my-one-time-use-auth-token=12345-take2
Content-Type: image/jpeg
<>
```
**RESPONSE**
```http
HTTP/2 200 OK
```
## Write Once Behaviour for Servers
Some implementations have a write-once behaviour for media where the client gets a single upload of the media byte stream. After the stream is received the byte stream becomes read-only. Replacement Media must be done by adding a new Media record and deleting the old one.
In this case, the server will not provide the `mediaEditLink` annotation and client will get a HTTP 409 Conflict response from the server.
### Write-Once Case
The client will try the resubmit behaviour above but will receive the HTTP 409 response when attempting to send the byte stream at which point the client switch to the write-once behaviour, creating a new media record with byte stream and then deleting the old record. This approach is because the client does not know ahead of time if the server is write-once or not.
**REQUEST**
```http
GET https://api.my-webapi.io/Media('12345')
OData-Version: 4.01
Content-Type: application/json
Accept: application/json
Prefer: return=representation
```
**RESPONSE**
```http
HTTP/2 200
OData-Version: 4.01
EntityId: "12345"
Location: https://api.my-webapi.io/Media('12345')
Content-Length: 200
Content-Type: application/json
Preference-Applied: return=representation
```
```json
{
"@odata.context":"https://api.my-webapi.io/$metadata#Media/$entity",
"@odata.id":"Media('12345')",
"@odata.editLink":"https://api.webapi.io/Media('12345')",
"@odata.etag": "W/\"aBcDeFgHiJkLmNoPqRsTuVwXyz\"",
"MediaObjectID": "12345",
"Caption": "Ipsum Lorum",
"Order": 1,
"ImageType": "image/jpeg",
"MediaStatus": "Rejected",
"MediaStatusDescription": "Byte stream is not of type image/jpeg",
}
```
**REQUEST**
```http
POST https://api.my-webapi.io/Media('12345')/$value
Content-Type: image/jpeg
<>
```
**RESPONSE**
```http
HTTP/2 409 Conflict
```
* Creation new record is done using the above examples
* Deletion of the old record uses the standard Add/Edit behaviour on the media record
Discussed in https://github.com/RESOStandards/transport/discussions/116