Skip to content

Commit eaef7d6

Browse files
committed
refactor: convert all links and assets to absolute when loaded
1 parent 920eff9 commit eaef7d6

File tree

2 files changed

+137
-4
lines changed

2 files changed

+137
-4
lines changed

src/http.ts

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export async function fetchStac(
1616
if (response.ok) {
1717
return response
1818
.json()
19-
.then((json) => maybeAddSelfLink(json, href.toString()));
19+
.then((json) => makeStacUrlAbsolute(json, href.toString()));
2020
} else {
2121
throw new Error(`${method} ${href}: ${response.statusText}`);
2222
}
@@ -32,9 +32,8 @@ export async function fetchStacLink(link: StacLink, href?: string | undefined) {
3232
);
3333
}
3434

35-
// eslint-disable-next-line
36-
function maybeAddSelfLink(value: any, href: string) {
37-
if (!(value as StacValue)?.links?.find((link) => link.rel == "self")) {
35+
function maybeAddSelfLink(value: StacValue, href: string) {
36+
if (!value?.links?.find((link) => link.rel == "self")) {
3837
const link = { href, rel: "self" };
3938
if (Array.isArray(value.links)) {
4039
value.links.push(link);
@@ -44,3 +43,71 @@ function maybeAddSelfLink(value: any, href: string) {
4443
}
4544
return value;
4645
}
46+
47+
/**
48+
* Attempt to convert links and asset URLS to absolute URLs while ensuring a self link exists.
49+
*
50+
* @param value Source stac item, collection, or catalog
51+
* @param baseUrl base location of the STAC document
52+
*/
53+
export function makeStacUrlAbsolute<T extends StacValue>(
54+
value: T,
55+
baseUrl: string,
56+
): T {
57+
maybeAddSelfLink(value, baseUrl);
58+
const baseUrlObj = new URL(baseUrl);
59+
60+
if (value.links != null) {
61+
for (const link of value.links) {
62+
if (link.href == null) continue;
63+
link.href = toAbsoluteUrl(link.href, baseUrlObj);
64+
}
65+
}
66+
67+
if (value.assets != null) {
68+
for (const asset of Object.values(value.assets)) {
69+
if (asset.href == null) continue;
70+
asset.href = toAbsoluteUrl(asset.href, baseUrlObj);
71+
}
72+
}
73+
return value;
74+
}
75+
76+
/**
77+
* Determine if the URL is absolute
78+
* @returns true if absolute, false otherwise
79+
*/
80+
function isAbsolute(url: string) {
81+
try {
82+
new URL(url);
83+
return true;
84+
} catch {
85+
return false;
86+
}
87+
}
88+
89+
/**
90+
* Attempt to convert a possibly relative URL to an absolute URL
91+
*
92+
* If the URL is already absolute, it is returned unchanged.
93+
*
94+
* **WARNING**: if the URL is http it will be returned as URL encoded
95+
*
96+
* @param href
97+
* @param baseUrl
98+
* @returns absolute URL
99+
*/
100+
export function toAbsoluteUrl(href: string, baseUrl: URL): string {
101+
if (isAbsolute(href)) return href;
102+
103+
const targetUrl = new URL(href, baseUrl);
104+
105+
if (targetUrl.protocol === "http:" || targetUrl.protocol === "https:") {
106+
return targetUrl.toString();
107+
}
108+
109+
// S3 links should not be encoded
110+
if (targetUrl.protocol === "s3:") return decodeURI(targetUrl.toString());
111+
112+
return targetUrl.toString();
113+
}

tests/http.spec.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { expect, test } from "vitest";
2+
import { makeStacUrlAbsolute, toAbsoluteUrl } from "../src/http";
3+
import { StacItem } from "stac-ts";
4+
5+
test("should preserve UTF8 characters while making URLS absolute", async () => {
6+
expect(toAbsoluteUrl("🦄.tiff", new URL("s3://some-bucket"))).equals(
7+
"s3://some-bucket/🦄.tiff",
8+
);
9+
expect(
10+
toAbsoluteUrl("https://foo/bar/🦄.tiff", new URL("s3://some-bucket")),
11+
).equals("https://foo/bar/🦄.tiff");
12+
expect(
13+
toAbsoluteUrl("../../../🦄.tiff", new URL("s3://some-bucket/🌈/path/a/b/")),
14+
).equals("s3://some-bucket/🌈/🦄.tiff");
15+
16+
expect(
17+
toAbsoluteUrl('a+🦄.tiff', new URL("s3://some-bucket/🌈/")),
18+
).equals("s3://some-bucket/🌈/a+🦄.tiff");
19+
20+
expect(
21+
toAbsoluteUrl("../../../🦄.tiff", new URL("https://some-url/🌈/path/a/b/")),
22+
).equals("https://some-url/%F0%9F%8C%88/%F0%9F%A6%84.tiff");
23+
expect(
24+
toAbsoluteUrl(
25+
"foo/🦄.tiff?width=1024",
26+
new URL("https://user@[2601:195:c381:3560::f42a]:1234/test"),
27+
),
28+
).equals(
29+
"https://user@[2601:195:c381:3560::f42a]:1234/foo/%F0%9F%A6%84.tiff?width=1024",
30+
);
31+
});
32+
33+
test("should convert relative links to absolute", () => {
34+
expect(
35+
makeStacUrlAbsolute(
36+
{
37+
links: [
38+
{ href: "a/b/c", rel: "child" },
39+
{ href: "/d/e/f", rel: "child" },
40+
],
41+
} as unknown as StacItem,
42+
"https://example.com/root/item.json",
43+
).links,
44+
).deep.equals([
45+
{ href: "https://example.com/root/a/b/c", rel: "child" },
46+
{ href: "https://example.com/d/e/f", rel: "child" },
47+
{ href: "https://example.com/root/item.json", rel: "self" },
48+
]);
49+
});
50+
51+
test("should convert relative assets to absolute", () => {
52+
expect(
53+
makeStacUrlAbsolute(
54+
{
55+
assets: {
56+
tiff: { href: "./foo.tiff" },
57+
thumbnail: { href: "../thumbnails/foo.png" },
58+
},
59+
} as unknown as StacItem,
60+
"https://example.com/root/item.json",
61+
).assets,
62+
).deep.equals({
63+
tiff: { href: "https://example.com/root/foo.tiff" },
64+
thumbnail: { href: "https://example.com/thumbnails/foo.png" },
65+
});
66+
});

0 commit comments

Comments
 (0)