Skip to content

Commit d1819c6

Browse files
Merge pull request #4 from solid/feat/my-storages
Added hierarchical traversal function to useSolidStorages hook
2 parents 1e98887 + 76632be commit d1819c6

1 file changed

Lines changed: 145 additions & 4 deletions

File tree

app/lib/hooks/useSolidStorages.ts

Lines changed: 145 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import { useEffect, useState } from "react";
44
import { getDefaultSession } from "@inrupt/solid-client-authn-browser";
55
import { Parser, Store, NamedNode, Literal } from "n3";
66

7-
// Storage predicates
7+
// Storage predicates and types
88
const PIM_STORAGE = "http://www.w3.org/ns/pim/space#storage";
99
const SOLID_STORAGE = "http://www.w3.org/ns/solid/terms#storage";
10+
const PIM_STORAGE_TYPE = "http://www.w3.org/ns/pim/space#Storage";
11+
const RDF_TYPE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
1012
const FOAF_NAME = "http://xmlns.com/foaf/0.1/name";
1113
const VCARD_FN = "http://www.w3.org/2006/vcard/ns#fn";
1214

@@ -22,9 +24,130 @@ interface UseSolidStoragesResult {
2224
error: Error | null;
2325
}
2426

27+
/**
28+
* Discovers storage by traversing up the folder hierarchy from the WebID
29+
* Based on: https://github.com/SolidLabResearch/Bashlib/blob/80de25cbb4b3ed057f95e25bc057f1be9b00cef3/src/utils/util.ts#L73-L104
30+
* @param {string} webId - The WebID to start traversal from
31+
* @param {typeof fetch} fetchFn - The fetch function to use (should be authenticated)
32+
* @returns {Promise<string[]>} - Array of storage URLs found via traversal
33+
*/
34+
async function discoverStorageViaTraversal(
35+
webId: string,
36+
fetchFn: typeof fetch
37+
): Promise<string[]> {
38+
const storageUrls: string[] = [];
39+
40+
try {
41+
// Extract the base URL from the WebID (remove fragment)
42+
const url = new URL(webId);
43+
const baseUrl = `${url.origin}${url.pathname}`;
44+
45+
// Start from the parent directory of the WebID
46+
let currentUrl = baseUrl.substring(0, baseUrl.lastIndexOf('/') + 1);
47+
48+
// Traverse up the hierarchy
49+
const maxLevels = 10; // Prevent infinite loops
50+
let level = 0;
51+
52+
while (currentUrl && level < maxLevels) {
53+
try {
54+
console.log(`[Traversal] Checking container: ${currentUrl}`);
55+
56+
// Fetch the container with content negotiation
57+
const response = await fetchFn(currentUrl, {
58+
method: 'GET',
59+
headers: {
60+
'Accept': 'text/turtle, application/ld+json, */*;q=0.1',
61+
},
62+
});
63+
64+
if (!response.ok) {
65+
console.log(`[Traversal] Container not accessible: ${response.status} ${response.statusText}`);
66+
// Move up one level and continue
67+
const parentUrl = currentUrl.substring(0, currentUrl.lastIndexOf('/', currentUrl.length - 2) + 1);
68+
if (parentUrl === currentUrl || parentUrl === `${url.origin}/`) {
69+
break; // Reached root
70+
}
71+
currentUrl = parentUrl;
72+
level++;
73+
continue;
74+
}
75+
76+
const contentType = response.headers.get('Content-Type') || '';
77+
const content = await response.text();
78+
79+
// Parse the RDF content
80+
const store = new Store();
81+
if (contentType.includes('text/turtle') || contentType.includes('application/turtle') ||
82+
contentType.includes('text/n3') || contentType.includes('application/n3')) {
83+
const parser = new Parser({ baseIRI: currentUrl });
84+
const quads = parser.parse(content);
85+
store.addQuads(quads);
86+
} else {
87+
// Try parsing as Turtle anyway (some servers don't set content-type correctly)
88+
try {
89+
const parser = new Parser({ baseIRI: currentUrl });
90+
const quads = parser.parse(content);
91+
store.addQuads(quads);
92+
} catch (e) {
93+
console.warn(`[Traversal] Failed to parse content from ${currentUrl}:`, e);
94+
// Move up one level and continue
95+
const parentUrl = currentUrl.substring(0, currentUrl.lastIndexOf('/', currentUrl.length - 2) + 1);
96+
if (parentUrl === currentUrl || parentUrl === `${url.origin}/`) {
97+
break;
98+
}
99+
currentUrl = parentUrl;
100+
level++;
101+
continue;
102+
}
103+
}
104+
105+
const containerNode = new NamedNode(currentUrl);
106+
const rdfType = new NamedNode(RDF_TYPE);
107+
const pimStorageType = new NamedNode(PIM_STORAGE_TYPE);
108+
109+
// Check if this container is of type pim:Storage
110+
const typeQuads = store.getQuads(containerNode, rdfType, pimStorageType, null);
111+
const isStorage = typeQuads.length > 0;
112+
113+
if (isStorage) {
114+
// Ensure URL ends with /
115+
const storageUrl = currentUrl.endsWith('/') ? currentUrl : currentUrl + '/';
116+
storageUrls.push(storageUrl);
117+
console.log(`[Traversal] Found pim:Storage at: ${storageUrl}`);
118+
break; // Found storage, no need to continue
119+
}
120+
121+
// Move up one level
122+
const parentUrl = currentUrl.substring(0, currentUrl.lastIndexOf('/', currentUrl.length - 2) + 1);
123+
if (parentUrl === currentUrl || parentUrl === `${url.origin}/`) {
124+
break; // Reached root
125+
}
126+
currentUrl = parentUrl;
127+
level++;
128+
} catch (error) {
129+
// If we can't fetch a container, try the parent
130+
console.debug(`[Traversal] Could not fetch ${currentUrl}, trying parent:`, error);
131+
const parentUrl = currentUrl.substring(0, currentUrl.lastIndexOf('/', currentUrl.length - 2) + 1);
132+
if (parentUrl === currentUrl || parentUrl === `${url.origin}/`) {
133+
break;
134+
}
135+
currentUrl = parentUrl;
136+
level++;
137+
}
138+
}
139+
} catch (error) {
140+
console.error("[Traversal] Error discovering storage via traversal:", error);
141+
}
142+
143+
return storageUrls;
144+
}
145+
25146
/**
26147
* Hook to fetch Solid storage roots from the user's WebID profile.
27-
* Uses direct RDF parsing to discover storage locations via pim:storage and solid:storage predicates.
148+
* Uses two methods:
149+
* 1. Direct RDF parsing to discover storage locations via pim:storage and solid:storage predicates
150+
* 2. Hierarchical traversal to find pim:Storage containers by walking up the directory tree
28151
*/
29152
export function useSolidStorages(): UseSolidStoragesResult {
30153
const [storages, setStorages] = useState<SolidStorage[]>([]);
@@ -383,9 +506,27 @@ export function useSolidStorages(): UseSolidStoragesResult {
383506
});
384507
console.log("=========================");
385508

386-
// If no storage found via predicates, try to infer from WebID
509+
// Method 2: Hierarchical traversal (if no storage found via predicates)
510+
// Based on: https://github.com/SolidLabResearch/Bashlib/blob/80de25cbb4b3ed057f95e25bc057f1be9b00cef3/src/utils/util.ts#L73-L104
511+
if (storageUrls.length === 0) {
512+
console.log("No storage found via predicates, attempting hierarchical traversal...");
513+
514+
try {
515+
const traversalStorages = await discoverStorageViaTraversal(webId, session.fetch || fetch);
516+
traversalStorages.forEach(url => {
517+
if (!storageUrls.includes(url)) {
518+
storageUrls.push(url);
519+
console.log("Found storage via hierarchical traversal:", url);
520+
}
521+
});
522+
} catch (err) {
523+
console.warn("Hierarchical traversal failed:", err);
524+
}
525+
}
526+
527+
// If still no storage found, try to infer from WebID
387528
if (storageUrls.length === 0) {
388-
console.log("No storage found via predicates, attempting to infer from WebID...");
529+
console.log("No storage found via predicates or traversal, attempting to infer from WebID...");
389530

390531
// Extract base URL from WebID
391532
const webIdUrl = new URL(webId);

0 commit comments

Comments
 (0)