Skip to content

Commit

Permalink
Use promises for URLs in IGV to have fresh signing each time they used (
Browse files Browse the repository at this point in the history
  • Loading branch information
fiskus authored May 24, 2024
1 parent 9ea699f commit 0563259
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 6 deletions.
9 changes: 8 additions & 1 deletion catalog/app/components/Preview/loaders/Igv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ interface PreviewResult {
}

export const Loader = function IgvLoader({ gated, handle, children }: IgvLoaderProps) {
const signUrls = useSignObjectUrls(handle, traverseUrls)
const signUrls = useSignObjectUrls(handle, traverseUrls, { asyncReady: true })

const { result, fetch } = utils.usePreview({
type: 'txt',
Expand All @@ -73,6 +73,13 @@ export const Loader = function IgvLoader({ gated, handle, children }: IgvLoaderP
const tail = data.tail.join('\n')
try {
const options = JSON.parse([head, tail].join('\n'))
if (!options.reference?.indexFile && options.reference?.fastaURL) {
// This is a copy of the IGV behaviour, that is a copy IGV desktop behaviour.
// But with a fix of a bug when after signing URLs `fastaURL` contains async thunk.
// `indexFile` will be discarded if reference has `indexURL`
// XXX: remove that after https://github.com/igvteam/igv.js/pull/1821 merged
options.reference.indexFile = `${options.reference.fastaURL}.fai`
}
const auxOptions = await signUrls(options)
return PreviewData.Igv({
options: auxOptions,
Expand Down
115 changes: 115 additions & 0 deletions catalog/app/components/Preview/loaders/useSignObjectUrls.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import * as R from 'ramda'

import type * as Model from 'model'
import type { JsonRecord } from 'utils/types'

import {
createObjectUrlsSigner,
createPathResolver,
createUrlProcessor,
} from './useSignObjectUrls'

describe('components/Preview/loaders/useSignObjectUrls', () => {
describe('createObjectUrlsSigner', () => {
const traverseUrls = (fn: (v: string) => string, obj: JsonRecord) =>
R.evolve(
{
foo: {
bar: fn,
foo: {
baz: fn,
},
},
},
obj,
)
const processUrl = async (path: string) => path.split('').reverse().join('')
let object = {
check: true,
foo: {
bar: 'FEDCBA',
foo: {
baz: '987654321',
},
},
}
it('should return strings', async () => {
let processor = createObjectUrlsSigner(traverseUrls, processUrl, false)
await expect(processor(object)).resolves.toEqual({
check: true,
foo: {
bar: 'ABCDEF',
foo: {
baz: '123456789',
},
},
})
})

it('should return async functions', async () => {
let processor = createObjectUrlsSigner(traverseUrls, processUrl, true)
const result = await processor(object)
let bar = (result.foo as any).bar
let baz = (result.foo as any).foo.baz
await expect(bar()).resolves.toBe('ABCDEF')
await expect(baz()).resolves.toBe('123456789')
})
})

describe('createUrlProcessor', () => {
let sign = ({ bucket, key, version }: Model.S3.S3ObjectLocation) =>
`https://${bucket}+${key}+${version}`
let resolvePath = async (path: string) => ({
bucket: 'resolved-bucket',
key: path,
})
let processUrl = createUrlProcessor(sign, resolvePath)
it('should return unsinged web', () => {
expect(processUrl('http://bucket/path')).resolves.toBe('http://bucket/path')
})
it('should return signed S3 URL', () => {
expect(processUrl('s3://bucket/path')).resolves.toBe(
'https://bucket+path+undefined',
)
})
it('should return signed S3 relative URL', () => {
expect(processUrl('s3://./relative/path')).resolves.toBe(
'https://resolved-bucket+./relative/path+undefined',
)
})
it('should return signed path', () => {
expect(processUrl('./relative/path')).resolves.toBe(
'https://resolved-bucket+./relative/path+undefined',
)
})
})

describe('createPathResolver', () => {
test('Join keys if no logical key', () => {
const resolveKey = (key: string) => ({
bucket: 'foo/bar',
key: `CCC/${key}`,
})
let resolve = createPathResolver(resolveKey, { bucket: 'foo/bar', key: 'AAA/' })
expect(resolve('BBB')).resolves.toEqual({
bucket: 'foo/bar',
key: 'AAA/BBB',
})
})
test('Resovle logical key', () => {
const resolveLogicalKey = (key: string) => ({
bucket: 'foo/bar',
key: `CCC/${key}`,
})
let resolve = createPathResolver(resolveLogicalKey, {
bucket: 'foo/bar',
key: 'AAA/',
logicalKey: 'AAA/',
})
expect(resolve('BBB')).resolves.toEqual({
bucket: 'foo/bar',
key: 'CCC/AAA/BBB',
})
})
})
})
20 changes: 15 additions & 5 deletions catalog/app/components/Preview/loaders/useSignObjectUrls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type { JsonRecord } from 'utils/types'

import { PreviewError } from '../types'

const createPathResolver = (
export const createPathResolver = (
resolveLogicalKey: LogicalKeyResolver.LogicalKeyResolver | null,
handle: LogicalKeyResolver.S3SummarizeHandle,
): ((path: string) => Promise<LogicalKeyResolver.S3SummarizeHandle>) =>
Expand All @@ -36,7 +36,7 @@ const createPathResolver = (
key: s3paths.resolveKey(handle.key, path),
})

const createUrlProcessor = (
export const createUrlProcessor = (
sign: (handle: Model.S3.S3ObjectLocation) => string,
resolvePath: (path: string) => Promise<Model.S3.S3ObjectLocation>,
) =>
Expand All @@ -50,12 +50,16 @@ const createUrlProcessor = (
}),
)

const createObjectUrlsSigner =
export const createObjectUrlsSigner =
(
traverseUrls: (fn: (v: any) => any, json: JsonRecord) => JsonRecord,
processUrl: (path: string) => Promise<string>,
asyncReady: boolean,
) =>
async (json: JsonRecord) => {
if (asyncReady) {
return traverseUrls((url: string) => () => processUrl(url), json)
}
const promises: Promise<string>[] = []
const jsonWithPlaceholders = traverseUrls((url: string): number => {
const len = promises.push(processUrl(url))
Expand All @@ -65,9 +69,15 @@ const createObjectUrlsSigner =
return traverseUrls((idx: number): string => results[idx], jsonWithPlaceholders)
}

/**
* Return function that can traverse properties in an object and process URLs.
* Use `R.evolve` to create `traverseUrls` argument and describe which properties contain URLs in question.
* Also, if consumer of that object is able to use async functions instead of strings, you can set `asyncReady` option
*/
export default function useSignObjectUrls(
handle: LogicalKeyResolver.S3SummarizeHandle,
traverseUrls: (fn: (v: any) => any, json: JsonRecord) => JsonRecord,
opts?: { asyncReady?: boolean },
) {
const resolveLogicalKey = LogicalKeyResolver.use()
// @ts-expect-error
Expand All @@ -81,7 +91,7 @@ export default function useSignObjectUrls(
[sign, resolvePath],
)
return React.useMemo(
() => createObjectUrlsSigner(traverseUrls, processUrl),
[traverseUrls, processUrl],
() => createObjectUrlsSigner(traverseUrls, processUrl, !!opts?.asyncReady),
[opts?.asyncReady, traverseUrls, processUrl],
)
}
8 changes: 8 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ Entries inside each section should be ordered by type:
## Catalog, Lambdas
!-->
# unreleased - YYYY-MM-DD
## Python API

## CLI

## Catalog, Lambdas
* [Changed] Use promises for URLs in IGV to have fresh signing each time they used ([#3979](https://github.com/quiltdata/quilt/pull/3979))

# 6.0.0a3 - 2024-04-25
## Python API
* [Added] `quilt3.search()` and `quilt3.Bucket.search()` now accepts custom Elasticsearch queries ([#3448](https://github.com/quiltdata/quilt/pull/3448))
Expand Down

0 comments on commit 0563259

Please sign in to comment.