22
33import { useState , useEffect } from "react" ;
44import toast from "react-hot-toast" ;
5- import {
6- getFile ,
7- overwriteFile ,
8- createContainerAt ,
9- getSolidDataset ,
10- getContainedResourceUrlAll ,
11- UrlString ,
12- } from "@inrupt/solid-client" ;
135import { useSearchParams , useRouter } from "next/navigation" ;
146import AuthWrapper from "./AuthWrapper" ;
157import Header from "./Header" ;
@@ -28,172 +20,10 @@ import { useSolidStorages, useBrowseStorage } from "../lib/hooks";
2820import {
2921 buildBreadcrumbItems ,
3022 getAuthenticatedSession ,
31- getDisplayNameFromMeta ,
32- updateMetaFile ,
23+ copyFileResource ,
24+ copyFolderResource ,
3325} from "../lib/helpers" ;
3426
35- const INVALID_NAME_CHARS = / [ < > : " / \\ | ? * ] / g;
36-
37- const sanitizeResourceName = ( name : string ) : string => {
38- const sanitized = name . replace ( INVALID_NAME_CHARS , "" ) . trim ( ) ;
39- return sanitized || "Untitled" ;
40- } ;
41-
42- const decodeResourceNameFromUrl = ( resourceUrl : string ) : string => {
43- try {
44- const urlObj = new URL ( resourceUrl ) ;
45- const segments = urlObj . pathname . split ( "/" ) . filter ( Boolean ) ;
46- if ( segments . length === 0 ) {
47- return urlObj . hostname ;
48- }
49- const lastSegment = resourceUrl . endsWith ( "/" ) ? segments [ segments . length - 1 ] : segments [ segments . length - 1 ] ;
50- return decodeURIComponent ( lastSegment ) ;
51- } catch {
52- return resourceUrl ;
53- }
54- } ;
55-
56- const ensureTrailingSlash = ( url : string ) : string => ( url . endsWith ( "/" ) ? url : `${ url } /` ) ;
57-
58- const getParentContainerUrl = ( resourceUrl : string ) : string => {
59- try {
60- const urlObj = new URL ( resourceUrl ) ;
61- const segments = urlObj . pathname . split ( "/" ) . filter ( Boolean ) ;
62- if ( segments . length === 0 ) {
63- return `${ urlObj . origin } /` ;
64- }
65- if ( ! resourceUrl . endsWith ( "/" ) ) {
66- segments . pop ( ) ;
67- } else if ( segments . length > 0 ) {
68- segments . pop ( ) ;
69- }
70- const parentPath = segments . length ? `/${ segments . join ( "/" ) } /` : "/" ;
71- return `${ urlObj . origin } ${ parentPath } ` ;
72- } catch {
73- return resourceUrl ;
74- }
75- } ;
76-
77- const shouldSkipResourceCopy = ( resourceUrl : string ) : boolean => {
78- return resourceUrl . endsWith ( ".meta" ) || resourceUrl . endsWith ( ".acl" ) ;
79- } ;
80-
81- const resourceExists = async ( url : string , fetchFn : typeof fetch ) : Promise < boolean > => {
82- try {
83- const response = await fetchFn ( url , { method : "HEAD" } ) ;
84- if ( response . status === 404 ) {
85- return false ;
86- }
87- if ( response . status >= 200 && response . status < 300 ) {
88- return true ;
89- }
90- // For other statuses (401, 403, 405, etc.) assume the resource exists to avoid collisions
91- return true ;
92- } catch {
93- return false ;
94- }
95- } ;
96-
97- const generateCopyTarget = async (
98- parentUrl : string ,
99- desiredName : string ,
100- isContainer : boolean ,
101- fetchFn : typeof fetch
102- ) : Promise < { targetUrl : string ; displayName : string } > => {
103- const parentWithSlash = ensureTrailingSlash ( parentUrl ) ;
104- let attempt = 0 ;
105-
106- while ( attempt < 100 ) {
107- const candidateDisplayName = attempt === 0 ? desiredName : `${ desiredName } (${ attempt } )` ;
108- const candidatePathName = sanitizeResourceName ( candidateDisplayName ) ;
109- const encodedName = encodeURIComponent ( candidatePathName ) ;
110- const candidateUrl = isContainer ? `${ parentWithSlash } ${ encodedName } /` : `${ parentWithSlash } ${ encodedName } ` ;
111- const exists = await resourceExists ( candidateUrl , fetchFn ) ;
112- if ( ! exists ) {
113- return { targetUrl : candidateUrl , displayName : candidateDisplayName } ;
114- }
115- attempt += 1 ;
116- }
117-
118- throw new Error ( "Unable to generate a unique name for the copy" ) ;
119- } ;
120-
121- const copyFileFromSource = async (
122- sourceUrl : string ,
123- targetUrl : string ,
124- displayName : string ,
125- fetchFn : typeof fetch ,
126- mimeTypeHint ?: string
127- ) : Promise < void > => {
128- const fileBlob = await getFile ( sourceUrl as UrlString , { fetch : fetchFn } ) ;
129- const contentType = fileBlob . type || mimeTypeHint || "application/octet-stream" ;
130- await overwriteFile ( targetUrl as UrlString , fileBlob , {
131- fetch : fetchFn ,
132- contentType,
133- } ) ;
134- await updateMetaFile ( targetUrl as UrlString , displayName , fetchFn ) ;
135- } ;
136-
137- const copyFolderContents = async (
138- sourceFolderUrl : string ,
139- destinationFolderUrl : string ,
140- fetchFn : typeof fetch
141- ) : Promise < void > => {
142- const dataset = await getSolidDataset ( sourceFolderUrl , { fetch : fetchFn } ) ;
143- const containedResources = getContainedResourceUrlAll ( dataset ) ;
144-
145- for ( const resourceUrl of containedResources ) {
146- if ( shouldSkipResourceCopy ( resourceUrl ) ) {
147- continue ;
148- }
149-
150- if ( resourceUrl . endsWith ( "/" ) ) {
151- const childName = decodeResourceNameFromUrl ( resourceUrl ) ;
152- const encodedChildName = encodeURIComponent ( childName ) ;
153- const childDestination = `${ ensureTrailingSlash ( destinationFolderUrl ) } ${ encodedChildName } /` ;
154-
155- await createContainerAt ( childDestination as UrlString , { fetch : fetchFn } ) ;
156- const childDisplayName =
157- ( await getDisplayNameFromMeta ( resourceUrl , fetchFn ) ) ?? childName ;
158- await updateMetaFile ( childDestination as UrlString , childDisplayName , fetchFn ) ;
159-
160- await copyFolderContents ( resourceUrl , childDestination , fetchFn ) ;
161- } else {
162- const childName = decodeResourceNameFromUrl ( resourceUrl ) ;
163- const encodedChildName = encodeURIComponent ( childName ) ;
164- const childDestination = `${ ensureTrailingSlash ( destinationFolderUrl ) } ${ encodedChildName } ` ;
165- const childDisplayName =
166- ( await getDisplayNameFromMeta ( resourceUrl , fetchFn ) ) ?? childName ;
167- await copyFileFromSource ( resourceUrl , childDestination , childDisplayName , fetchFn ) ;
168- }
169- }
170- } ;
171-
172- const copyFileResource = async ( file : FileItemData , fetchFn : typeof fetch ) : Promise < void > => {
173- const originalLabel =
174- ( await getDisplayNameFromMeta ( file . url , fetchFn ) ) ??
175- file . name ??
176- decodeResourceNameFromUrl ( file . url ) ;
177- const parentUrl = getParentContainerUrl ( file . url ) ;
178- const desiredName = `Copy of ${ originalLabel } ` ;
179- const { targetUrl, displayName } = await generateCopyTarget ( parentUrl , desiredName , false , fetchFn ) ;
180- await copyFileFromSource ( file . url , targetUrl , displayName , fetchFn , file . mimeType ) ;
181- } ;
182-
183- const copyFolderResource = async ( folder : FileItemData , fetchFn : typeof fetch ) : Promise < void > => {
184- const originalLabel =
185- ( await getDisplayNameFromMeta ( folder . url , fetchFn ) ) ??
186- folder . name ??
187- decodeResourceNameFromUrl ( folder . url ) ;
188- const parentUrl = getParentContainerUrl ( folder . url ) ;
189- const desiredName = `Copy of ${ originalLabel } ` ;
190- const { targetUrl, displayName } = await generateCopyTarget ( parentUrl , desiredName , true , fetchFn ) ;
191-
192- await createContainerAt ( targetUrl as UrlString , { fetch : fetchFn } ) ;
193- await updateMetaFile ( targetUrl as UrlString , displayName , fetchFn ) ;
194- await copyFolderContents ( folder . url , targetUrl , fetchFn ) ;
195- } ;
196-
19727
19828export default function FileManager ( ) {
19929 const searchParams = useSearchParams ( ) ;
0 commit comments