Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions common-lib/src/main/scala/com/gu/mediaservice/GridClient.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import com.gu.mediaservice.model.leases.LeasesByMedia
import com.gu.mediaservice.model.usage.Usage
import com.typesafe.scalalogging.LazyLogging
import play.api.http.HeaderNames
import play.api.libs.json.{JsArray, JsObject, JsValue, Json, Reads}
import play.api.libs.json.{JsArray, JsObject, JsTrue, JsValue, Json, Reads}

import scala.concurrent.duration.{Duration, DurationInt}
import scala.concurrent.{ExecutionContext, Future}
Expand Down Expand Up @@ -38,8 +38,8 @@ object ClientResponse {
case class ClientErrorMessages(errorMessage: String, downstreamErrorMessage: String)

object GridClient extends LazyLogging {
def apply(services: Services)(implicit wsClient: WSClient): GridClient =
new GridClient(services)
def apply(services: Services, originUri: String)(implicit wsClient: WSClient): GridClient =
new GridClient(services, originUri)

sealed trait Response {
def status: Int
Expand Down Expand Up @@ -96,7 +96,7 @@ object GridClient extends LazyLogging {

}

class GridClient(services: Services)(implicit wsClient: WSClient) extends LazyLogging {
class GridClient(services: Services, originDomain: String)(implicit wsClient: WSClient) extends LazyLogging {

/*
* `requestTimeout` will set the max duration of the request before timing out. You may also want to increase the
Expand Down Expand Up @@ -260,6 +260,11 @@ class GridClient(services: Services)(implicit wsClient: WSClient) extends LazyLo
authorisedRequest.post(Json.obj("data" -> data)).map { response => validateResponse(response, url)}
}

def putArchived(mediaId: String, authFn: WSRequest => WSRequest)(implicit ec: ExecutionContext) = {
val url = new URL(s"${services.metadataBaseUri}/metadata/$mediaId/archived")
val request = authFn(wsClient.url(url.toString))
request.put(Json.obj("data" -> JsTrue)).map { response => validateResponse(response, url)}
}
}

class DownstreamApiInBadStateException(message: String, downstreamMessage: String) extends IllegalStateException(message) {
Expand Down
2 changes: 1 addition & 1 deletion image-loader/app/ImageLoaderComponents.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class ImageLoaderComponents(context: Context) extends GridComponents(context, ne
logger.info(s" $index -> ${processor.description}")
}

private val gridClient = GridClient(config.services)(wsClient)
private val gridClient = GridClient(config.services, config.services.loaderBaseUri)(wsClient)

val store = new ImageLoaderStore(config)
val maybeIngestQueue = config.maybeIngestSqsQueueUrl.map(queueUrl => new SimpleSqsMessageConsumer(queueUrl, config))
Expand Down
27 changes: 23 additions & 4 deletions kahuna/public/js/components/gr-archiver/gr-archiver.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,16 @@ module.controller('grArchiverCtrl', [
'humanJoin',
'mediaApi',
'getDeletedState',
'apiPoll',
function ($scope,
$window,
archiveService,
imageAccessor,
imageLogic,
humanJoin,
mediaApi,
getDeletedState) {
getDeletedState,
apiPoll) {

const ctrl = this;

Expand Down Expand Up @@ -115,14 +117,31 @@ module.controller('grArchiverCtrl', [
reduce((all, items) => all.union(items)).
toArray();
}

function pollUndeleted(imageId) {
const findImage = () => mediaApi.find(imageId).then(
(i) => i.data?.softDeletedMetadata === undefined && i.data.userMetadata?.data?.archived
? Promise.resolve()
: Promise.reject()
);
return apiPoll(findImage);
}
function undelete() {
ctrl.undeleting = true;
const imageId = ctrl.image.data.id;
mediaApi.undelete(imageId)
.then(
ctrl.canUndelete = ctrl.isDeleted = false
).catch(() => {
.then(() => pollUndeleted(imageId))
.then(() => {
ctrl.canUndelete = false;
ctrl.isDeleted = false;
ctrl.image.softDeletedMetadata = undefined;
if (ctrl.image.userMetadata?.data !== undefined) {
ctrl.image.userMetadata.data.archived = true;
}
$scope.$emit('images-updated', [ctrl.image]);
}).catch((e) => {
$window.alert('Failed to undelete image!, please try again.');
console.error('Image undeletion failed', e);
}).finally(() => {
ctrl.undeleting = false;
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,35 @@ export const undeleteImage = angular.module('gr.undeleteImage', [
]);

undeleteImage.controller('grUnDeleteImageCtrl', [
'$rootScope', '$q', '$timeout', 'mediaApi', 'apiPoll',
function ($rootScope, $q, $timeout, mediaApi, apiPoll) {
'$rootScope', '$q', 'mediaApi', 'apiPoll',
function ($rootScope, $q, mediaApi, apiPoll) {
var ctrl = this;

ctrl.$onInit = () => {

function pollDeleted (image) {
function pollUndeleted (image) {
const findImage = () => mediaApi.find(image.data.id).then(
() => $q.reject(),
// resolve when image cannot be found, i.e. image has been deleted.
() => $q.resolve()
(i) => i.data.softDeletedMetadata === undefined && i.data.userMetadata?.data?.archived
? $q.resolve()
: $q.reject()
);

apiPoll(findImage);
return apiPoll(findImage);
}

ctrl.unDeleteImage = function (image) {
return mediaApi.undelete(image.data.id)
.then(() => pollDeleted(image))
.then(() => pollUndeleted(image))
.catch((err) => {
$rootScope.$emit('image-delete-failure', err, image);
$rootScope.$emit('image-undelete-failure', err, image);
});
};

ctrl.unDeleteSelected = function () {
// HACK to wait for thrall to process the message so that when we
// poll the api, it will be up to date.
return $q.all(Array.from(ctrl.images.values()).map(image => ctrl.unDeleteImage(image)))
.then(() => $rootScope.$emit('images-deleted', ctrl.images));
.then(() => $rootScope.$emit('images-undeleted', ctrl.images));
};
};
}
Expand Down
7 changes: 7 additions & 0 deletions kahuna/public/js/image/controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,13 @@ image.controller('ImageCtrl', [

ctrl.image = image;
if (ctrl.image && ctrl.image.data.softDeletedMetadata !== undefined) { ctrl.isDeleted = true; }

$scope.$watch('ctrl.image.data.softDeletedMetadata', () => {
if (ctrl.image) {
ctrl.isDeleted = ctrl.image?.data.softDeletedMetadata !== undefined;
}
});

ctrl.optimisedImageUri = optimisedImageUri;
ctrl.lowResImageUri = lowResImageUri;

Expand Down
32 changes: 18 additions & 14 deletions kahuna/public/js/search/results.js
Original file line number Diff line number Diff line change
Expand Up @@ -827,24 +827,27 @@ results.controller('SearchResultsCtrl', [
});
};

const freeImageDeleteListener = $rootScope.$on('images-deleted', (e, images) => {
images.forEach(image => {
// TODO: should not be needed here, the selection and
// results should listen to these events and update
// itself outside of any controller
ctrl.deselect(image);
const imageDeleteHandler = (_, images) => {
images.forEach(image => {
// TODO: should not be needed here, the selection and
// results should listen to these events and update
// itself outside of any controller
ctrl.deselect(image);

const indexAll = ctrl.imagesAll.findIndex(i => image.data.id === i.data.id);
results.removeAt(indexAll);
const indexAll = ctrl.imagesAll.findIndex(i => image.data.id === i.data.id);
results.removeAt(indexAll);

updateImageArray(ctrl.images, image);
updateImageArray(ctrl.imagesAll, image);
updateImageArray(ctrl.images, image);
updateImageArray(ctrl.imagesAll, image);

updatePositions(image);
updatePositions(image);

ctrl.totalResults--;
});
});
ctrl.totalResults--;
});
};

const freeImageDeleteListener = $rootScope.$on('images-deleted', imageDeleteHandler);
const freeImageUndeleteListener = $rootScope.$on('images-undeleted', imageDeleteHandler);

// Safer than clearing the timeout in case of race conditions
// FIXME: nicer (reactive?) way to do this?
Expand Down Expand Up @@ -913,6 +916,7 @@ results.controller('SearchResultsCtrl', [
}
freeUpdatesListener();
freeImageDeleteListener();
freeImageUndeleteListener();
scopeGone = true;
});
}
Expand Down
22 changes: 13 additions & 9 deletions media-api/app/controllers/MediaApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class MediaApi(
)(implicit val ec: ExecutionContext) extends BaseController with MessageSubjects with ArgoHelpers with ContentDisposition {

val services: Services = new Services(config.domainRoot, config.serviceHosts, Set.empty)
val gridClient: GridClient = GridClient(services)(ws)
val gridClient: GridClient = GridClient(services, services.apiBaseUri)(ws)

// Process-local cache keyed on normalised query text. Stores the Bedrock Future so that
// concurrent requests for the same query share a single in-flight Bedrock call, and
Expand Down Expand Up @@ -404,15 +404,19 @@ class MediaApi(
&& ImageExtras.userMayUndeleteImage(request.user, image, authorisation) =>
logger.info(logMarker, s"undeleting image $id")

softDeletedMetadataTable.updateStatus(id, isDeleted = false)
.map { _ =>
messageSender.publish(
UpdateMessage(
subject = UnSoftDeleteImage,
id = Some(id)
)
for {
// Take an "undeletion" as a marker that the file should be kept and not reaped.
// Mark as archived before undeleting, to make sure the reaper doesn't immediately reap it.
_ <- gridClient.putArchived(id, auth.getOnBehalfOfPrincipal(request.user))
_ <- softDeletedMetadataTable.updateStatus(id, isDeleted = false)
_ = messageSender.publish(
UpdateMessage(
subject = UnSoftDeleteImage,
id = Some(id)
)
}.map { _ => Accepted }
)
} yield Accepted

case Some(image) if isVisibleToAccessor(request.user, image) =>
logger.info(logMarker, s"user ${request.user.accessor.identity} was not permitted to undelete image $id")
Future.successful(ImageDeleteForbidden)
Expand Down
2 changes: 1 addition & 1 deletion metadata-editor/app/controllers/EditsController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ class EditsController(
import com.gu.mediaservice.lib.metadata.UsageRightsMetadataMapper.usageRightsToMetadata

val services: Services = new Services(config.domainRoot, config.serviceHosts, Set.empty)
val gridClient: GridClient = GridClient(services)(ws)
val gridClient: GridClient = GridClient(services, services.metadataBaseUri)(ws)

val metadataBaseUri = config.services.metadataBaseUri
private val AuthenticatedAndAuthorised = auth andThen authorisation.CommonActionFilters.authorisedForArchive
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,26 +112,36 @@ class PandaAuthenticationProvider(
override def flushToken: Option[(RequestHeader, Result) => Result] = Some((rh, _) => processLogout(rh))

val PandaCookieKey: TypedKey[Cookie] = TypedKey[Cookie]("PandaCookie")
val OriginKey: TypedKey[String] = TypedKey[String]("Origin")

private def withPersistedOrigin(request: Principal): WSRequest => WSRequest = { wsRequest =>
request.attributes.get(OriginKey) match {
case Some(origin) => wsRequest.withHttpHeaders(("Origin", origin))
case None => wsRequest
}
}

private def withPersistedCookie(request: Principal): Either[String, WSRequest => WSRequest] = {
val cookieName = panDomainSettings.settings.cookieSettings.cookieName
request.attributes.get(PandaCookieKey) match {
case Some(cookie) => Right(wsRequest => wsRequest.addCookies(DefaultWSCookie(cookieName, cookie.value)))
case None => Left(s"Pan domain cookie $cookieName is missing in principal.")
}
}

/**
* A function that allows downstream API calls to be made using the credentials of the inflight request
*
* @param request The request header of the inflight call
* @return A function that adds appropriate data to a WSRequest
*/
override def onBehalfOf(request: Principal): Either[String, WSRequest => WSRequest] = {
val cookieName = panDomainSettings.settings.cookieSettings.cookieName
request.attributes.get(PandaCookieKey) match {
case Some(cookie) => Right { wsRequest: WSRequest =>
wsRequest.addCookies(DefaultWSCookie(cookieName, cookie.value))
}
case None => Left(s"Pan domain cookie $cookieName is missing in principal.")
}
}
override def onBehalfOf(request: Principal): Either[String, WSRequest => WSRequest] =
withPersistedCookie(request).map(_.andThen(withPersistedOrigin(request)))

private def gridUserFrom(pandaUser: User, request: RequestHeader): UserPrincipal = {
val maybePandaCookie: Option[TypedEntry[Cookie]] = request.cookies.get(panDomainSettings.settings.cookieSettings.cookieName).map(TypedEntry[Cookie](PandaCookieKey, _))
val attributes = TypedMap.empty + (maybePandaCookie.toSeq:_*)
val maybeOrigin = request.headers.get("Origin").map(TypedEntry[String](OriginKey, _))
val attributes = TypedMap(Seq(maybePandaCookie, maybeOrigin).flatten:_*)
UserPrincipal(
firstName = pandaUser.firstName,
lastName = pandaUser.lastName,
Expand Down
2 changes: 1 addition & 1 deletion thrall/app/ThrallComponents.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class ThrallComponents(context: Context) extends GridComponents(context, new Thr
es.ensureIndexExistsAndAliasAssigned()

val services: Services = new Services(config.domainRoot, config.serviceHosts, Set.empty)
val gridClient: GridClient = GridClient(services)(wsClient)
val gridClient: GridClient = GridClient(services, services.thrallBaseUri)(wsClient)

// before firing up anything to consume streams or say we are OK let's do the critical good to go check
private val goodToGoCheckResult = Await.ready(GoodToGoCheck.run(es), 30 seconds)
Expand Down
Loading