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
Original file line number Diff line number Diff line change
Expand Up @@ -2182,4 +2182,49 @@ class DatasetResource extends LazyLogging {
Response.temporaryRedirect(new URI(presignedUrl)).build()
}
}

/**
* Get a presigned S3 URL for the dataset cover image as JSON.
* JWT-aware variant of GET /{did}/cover; required for private datasets
* since `<img src>` cannot attach the Authorization header.
*/
@GET
@Path("/{did}/cover-url")
@Produces(Array(MediaType.APPLICATION_JSON))
def getDatasetCoverUrl(
@PathParam("did") did: Integer,
@Auth sessionUser: Optional[SessionUser]
): Response = {
withTransaction(context) { ctx =>
val dataset = getDatasetByID(ctx, did)

val requesterUid = if (sessionUser.isPresent) Some(sessionUser.get().getUid) else None

if (requesterUid.isEmpty && !dataset.getIsPublic) {
throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)
} else if (requesterUid.exists(uid => !userHasReadAccess(ctx, did, uid))) {
throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)
}

Option(dataset.getCoverImage) match {
case None =>
Response.ok(Map("url" -> null)).build()
case Some(coverImage) =>
val owner = getOwner(ctx, did)
val fullPath = s"${owner.getEmail}/${dataset.getName}/$coverImage"

val document = DocumentFactory
.openReadonlyDocument(FileResolver.resolve(fullPath))
.asInstanceOf[OnDataset]

val presignedUrl = LakeFSStorageClient.getFilePresignedUrl(
document.getRepositoryName(),
document.getVersionHash(),
document.getFileRelativePath()
)

Response.ok(Map("url" -> presignedUrl)).build()
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2531,6 +2531,50 @@ class DatasetResourceSpec
response.getHeaderString("Location") should not be null
}

"getDatasetCoverUrl" should "return presigned url for owner of private dataset" in {
testDatasetVersion

val dataset = datasetDao.fetchOneByDid(baseDataset.getDid)
dataset.setIsPublic(false)
dataset.setCoverImage(testCoverImagePath)
datasetDao.update(dataset)

val response = datasetResource.getDatasetCoverUrl(
baseDataset.getDid,
Optional.of(sessionUser)
)

response.getStatus shouldEqual 200
Option(entityAsScalaMap(response)("url")) shouldBe defined
}

it should "reject private dataset cover for users without access" in {
val dataset = datasetDao.fetchOneByDid(baseDataset.getDid)
dataset.setOwnerUid(ownerUser.getUid)
dataset.setIsPublic(false)
dataset.setCoverImage("v1/cover.jpg")
datasetDao.update(dataset)

assertThrows[ForbiddenException] {
datasetResource.getDatasetCoverUrl(baseDataset.getDid, Optional.of(sessionUser2))
}
}

it should "return null url when no cover image is set" in {
val dataset = datasetDao.fetchOneByDid(baseDataset.getDid)
dataset.setCoverImage(null)
dataset.setIsPublic(true)
datasetDao.update(dataset)

val response = datasetResource.getDatasetCoverUrl(
baseDataset.getDid,
Optional.of(sessionUser)
)

response.getStatus shouldEqual 200
Option(entityAsScalaMap(response)("url")) shouldBe empty
}

"LakeFS error handling" should "return 500 when ETag is invalid, with the message included in the error response body" in {
val filePath = uniqueFilePath("error-body")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,10 @@ <h2>Dataset: {{datasetName}}</h2>
</div>
</div>
<img
*ngIf="coverImageUrl; else coverPlaceholder"
*ngIf="coverImageUrl"
class="dataset-cover-image"
[src]="coverImageUrl"
alt="Dataset cover" />
<ng-template #coverPlaceholder>
<div class="dataset-cover-image"></div>
</ng-template>
</div>

<div class="description-section">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,10 @@ import { FilesUploaderComponent } from "../../files-uploader/files-uploader.comp
import { NzProgressComponent } from "ng-zorro-antd/progress";
import { UserDatasetStagedObjectsListComponent } from "./user-dataset-staged-objects-list/user-dataset-staged-objects-list.component";
import { NzInputDirective } from "ng-zorro-antd/input";
import { AppSettings } from "../../../../../common/app-setting";

export const THROTTLE_TIME_MS = 1000;
export const ABORT_RETRY_MAX_ATTEMPTS = 10;
export const ABORT_RETRY_BACKOFF_BASE_MS = 100;
const DEFAULT_COVER_IMAGE = "assets/card_background.jpg";

@UntilDestroy()
@Component({
Expand Down Expand Up @@ -121,7 +119,7 @@ export class DatasetDetailComponent implements OnInit {
public datasetCreationTime: string = "";
public datasetCreationTimeTooltip: string = "";
public datasetIsPublic: boolean = false;
public coverImageUrl: string = "";
public coverImageUrl: string | null = null;
public datasetIsDownloadable: boolean = true;
public userDatasetAccessLevel: "READ" | "WRITE" | "NONE" = "NONE";
public ownerEmail: string = "";
Expand Down Expand Up @@ -332,8 +330,9 @@ export class DatasetDetailComponent implements OnInit {

retrieveDatasetInfo() {
if (this.did) {
const did = this.did;
this.datasetService
.getDataset(this.did, this.isLogin)
.getDataset(did, this.isLogin)
.pipe(untilDestroyed(this))
.subscribe(dashboardDataset => {
const dataset = dashboardDataset.dataset;
Expand All @@ -344,9 +343,17 @@ export class DatasetDetailComponent implements OnInit {
this.datasetIsDownloadable = dataset.isDownloadable;
this.ownerEmail = dashboardDataset.ownerEmail;
this.isOwner = dashboardDataset.isOwner;
this.coverImageUrl = dataset.coverImage
? `${AppSettings.getApiEndpoint()}/dataset/${this.did}/cover?v=${encodeURIComponent(dataset.coverImage)}`
: DEFAULT_COVER_IMAGE;
if (dataset.coverImage) {
this.datasetService
.getDatasetCoverUrl(did)
.pipe(untilDestroyed(this))
.subscribe({
next: ({ url }) => (this.coverImageUrl = url),
error: () => (this.coverImageUrl = null),
});
} else {
this.coverImageUrl = null;
}
if (typeof dataset.creationTime === "number") {
const date = new Date(dataset.creationTime);
this.datasetCreationTime = format(date, "MM/dd/yyyy HH:mm:ss");
Expand Down Expand Up @@ -776,14 +783,21 @@ export class DatasetDetailComponent implements OnInit {
if (!this.did || !this.selectedVersion) {
return;
}
const did = this.did;

const newCoverPath = `${this.selectedVersion.name}/${filePath}`;
this.datasetService
.updateDatasetCoverImage(this.did, newCoverPath)
.updateDatasetCoverImage(did, newCoverPath)
.pipe(untilDestroyed(this))
.subscribe({
next: () => {
this.coverImageUrl = `${AppSettings.getApiEndpoint()}/dataset/${this.did}/cover?v=${encodeURIComponent(newCoverPath)}`;
this.datasetService
.getDatasetCoverUrl(did)
.pipe(untilDestroyed(this))
.subscribe({
next: ({ url }) => (this.coverImageUrl = url),
error: () => (this.coverImageUrl = null),
});
this.notificationService.success("Cover image updated.");
},
error: (err: unknown) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -557,4 +557,8 @@ export class DatasetService {
coverImage: coverImage,
});
}

public getDatasetCoverUrl(did: number): Observable<{ url: string | null }> {
return this.http.get<{ url: string | null }>(`${AppSettings.getApiEndpoint()}/dataset/${did}/cover-url`);
}
}
Loading