@@ -4,9 +4,11 @@ import { useEffect, useState } from "react";
44import { getDefaultSession } from "@inrupt/solid-client-authn-browser" ;
55import { Parser , Store , NamedNode , Literal } from "n3" ;
66
7- // Storage predicates
7+ // Storage predicates and types
88const PIM_STORAGE = "http://www.w3.org/ns/pim/space#storage" ;
99const 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" ;
1012const FOAF_NAME = "http://xmlns.com/foaf/0.1/name" ;
1113const 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 */
29152export 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