Skip to content

Commit 2b7a9ef

Browse files
blachagadomski
andauthored
feat: convert relative paths to absolute (#108)
### Motivation Most of LINZ's STAC items and collections are relative links as to make it easier to move them around, we have a long term goal of converting them all to absolute links but that is going to take a way. In the mean time it would be great if this map tool supported relative links and assets. ### Modification I have added a symbol to all items that have been fetched via the `fetchStac` to store their source location onto the STAC document, so then any URL referenced from the document can converted with the source URL. I tried to avoid mutating the stac documents as I wasn't sure of the imapct, so I avoided these two other ideas - Converting the self link to be the actual source location then using the self link to create relative - Converting all links in the document to be absolute --------- Co-authored-by: Pete Gadomski <[email protected]>
1 parent f2ad23e commit 2b7a9ef

File tree

2 files changed

+138
-9
lines changed

2 files changed

+138
-9
lines changed

src/http.ts

Lines changed: 72 additions & 9 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) => makeStacHrefsAbsolute(json, href.toString()))
2020
.then((json) => maybeAddTypeField(json));
2121
} else {
2222
throw new Error(`${method} ${href}: ${response.statusText}`);
@@ -33,19 +33,82 @@ export async function fetchStacLink(link: StacLink, href?: string | undefined) {
3333
);
3434
}
3535

36-
// eslint-disable-next-line
37-
function maybeAddSelfLink(value: any, href: string) {
38-
if (!(value as StacValue)?.links?.find((link) => link.rel == "self")) {
39-
const link = { href, rel: "self" };
40-
if (Array.isArray(value.links)) {
41-
value.links.push(link);
42-
} else {
43-
value.links = [link];
36+
/**
37+
* Attempt to convert links and asset URLS to absolute URLs while ensuring a self link exists.
38+
*
39+
* @param value Source stac item, collection, or catalog
40+
* @param baseUrl base location of the STAC document
41+
*/
42+
export function makeStacHrefsAbsolute<T extends StacValue>(
43+
value: T,
44+
baseUrl: string,
45+
): T {
46+
const baseUrlObj = new URL(baseUrl);
47+
48+
if (value.links != null) {
49+
let hasSelf = false;
50+
for (const link of value.links) {
51+
if (link.rel === "self") hasSelf = true;
52+
if (link.href) {
53+
link.href = toAbsoluteUrl(link.href, baseUrlObj);
54+
}
55+
}
56+
if (hasSelf === false) {
57+
value.links.push({ href: baseUrl, rel: "self" });
58+
}
59+
} else {
60+
value.links = [{ href: baseUrl, rel: "self" }];
61+
}
62+
63+
if (value.assets != null) {
64+
for (const asset of Object.values(value.assets)) {
65+
if (asset.href) {
66+
asset.href = toAbsoluteUrl(asset.href, baseUrlObj);
67+
}
4468
}
4569
}
4670
return value;
4771
}
4872

73+
/**
74+
* Determine if the URL is absolute
75+
* @returns true if absolute, false otherwise
76+
*/
77+
function isAbsolute(url: string) {
78+
try {
79+
new URL(url);
80+
return true;
81+
} catch {
82+
return false;
83+
}
84+
}
85+
86+
/**
87+
* Attempt to convert a possibly relative URL to an absolute URL
88+
*
89+
* If the URL is already absolute, it is returned unchanged.
90+
*
91+
* **WARNING**: if the URL is http it will be returned as URL encoded
92+
*
93+
* @param href
94+
* @param baseUrl
95+
* @returns absolute URL
96+
*/
97+
export function toAbsoluteUrl(href: string, baseUrl: URL): string {
98+
if (isAbsolute(href)) return href;
99+
100+
const targetUrl = new URL(href, baseUrl);
101+
102+
if (targetUrl.protocol === "http:" || targetUrl.protocol === "https:") {
103+
return targetUrl.toString();
104+
}
105+
106+
// S3 links should not be encoded
107+
if (targetUrl.protocol === "s3:") return decodeURI(targetUrl.toString());
108+
109+
return targetUrl.toString();
110+
}
111+
49112
// eslint-disable-next-line
50113
function maybeAddTypeField(value: any) {
51114
if (!value.type) {

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 { makeStacHrefsAbsolute, 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(toAbsoluteUrl("a+🦄.tiff", new URL("s3://some-bucket/🌈/"))).equals(
17+
"s3://some-bucket/🌈/a+🦄.tiff",
18+
);
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+
makeStacHrefsAbsolute(
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+
makeStacHrefsAbsolute(
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)