RESOStandards / transport

RESO Transport Workgroup - Specifications and Change Proposals
https://transport.reso.org
Other
18 stars 15 forks source link

[RCP-48] Add/Edit with Media #136

Open darnjo opened 5 months ago

darnjo commented 5 months ago

Discussed in https://github.com/RESOStandards/transport/discussions/116

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
darnjo commented 5 months ago

@grispin: Please make a PR in a branch called rcp-48-add-edit-with-media with a new file in proposals called add-edit-with-media.md.

grispin commented 5 months ago

Pull Request Ref: https://github.com/RESOStandards/transport/pull/137