Skip to content

Commit 7a5ac65

Browse files
Merge pull request #19 from solid/feat/my-storages
Feat/my storages
2 parents b68a625 + acb4eb0 commit 7a5ac65

11 files changed

Lines changed: 123 additions & 223 deletions

File tree

app/components/AuthWrapper.tsx

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,8 @@ export default function AuthWrapper({ children }: AuthWrapperProps) {
2121
try {
2222
setError(null);
2323

24-
// The library uses this to restore session state from localStorage
25-
const redirectInfo = await handleIncomingRedirect({
26-
restorePreviousSession: true,
27-
});
24+
// First, handle any incoming OAuth redirect (processes code and state parameters)
25+
await handleIncomingRedirect({ restorePreviousSession: true });
2826

2927
// Get the session instance after handling redirect
3028
const session = getSession();
@@ -40,8 +38,6 @@ export default function AuthWrapper({ children }: AuthWrapperProps) {
4038
}
4139
}
4240

43-
44-
4541
setIsAuthenticated(isLoggedIn);
4642
} catch (err) {
4743
const errorMessage =
@@ -61,9 +57,6 @@ export default function AuthWrapper({ children }: AuthWrapperProps) {
6157
if (!isAuthenticated && !error) {
6258
const interval = setInterval(async () => {
6359
try {
64-
const redirectInfo = await handleIncomingRedirect({
65-
restorePreviousSession: true,
66-
});
6760
const session = getSession();
6861
if (session.info.isLoggedIn) {
6962
setIsAuthenticated(true);

app/components/FileManager.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,13 @@ export default function FileManager() {
228228
setShowRenameDialog(true);
229229
};
230230

231-
const handleRenamed = () => {
231+
const handleRenamed = (newUrl: string) => {
232+
233+
if (fileToRename && currentPath === fileToRename.url) {
234+
setCurrentPath(newUrl);
235+
updateUrl(newUrl);
236+
}
237+
// Trigger refresh to update file list immediately
232238
setRefreshKey((prev) => prev + 1);
233239
};
234240

app/components/LoginPage.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@ export default function LoginPage() {
1919
const handleLogin = async () => {
2020
setIsLoading(true);
2121
try {
22+
const baseUrl = window.location.origin + window.location.pathname;
2223
await login({
2324
oidcIssuer: selectedIssuer,
2425
clientName: "Solid File Manager",
25-
redirectUrl: window.location.href,
26+
redirectUrl: baseUrl,
2627
});
2728
} catch (error) {
2829
console.error("Login failed:", error);

app/components/MoveDialog.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { FileItemData } from "./FileItem";
99
import {
1010
moveFileResource,
1111
getAuthenticatedSession,
12-
getDisplayNameFromMeta,
1312
decodeResourceNameFromUrl,
1413
ensureTrailingSlash,
1514
} from "../lib/helpers";
@@ -58,11 +57,10 @@ export default function MoveDialog({
5857
if (resourceUrl.endsWith("/")) {
5958
// It's a folder
6059
const folderName = decodeResourceNameFromUrl(resourceUrl);
61-
const displayName = (await getDisplayNameFromMeta(resourceUrl, fetchFn)) ?? folderName;
6260

6361
folders.push({
6462
id: resourceUrl,
65-
name: displayName,
63+
name: folderName,
6664
type: "folder",
6765
url: resourceUrl,
6866
});

app/components/RenameDialog.tsx

Lines changed: 52 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@ import { useState, useEffect, useRef } from "react";
44
import Modal from "./shared/Modal";
55
import Button from "./shared/Button";
66
import Input from "./shared/Input";
7-
import { UrlString } from "@inrupt/solid-client";
7+
import { UrlString, getFile, overwriteFile, deleteFile, createContainerAt } from "@inrupt/solid-client";
88
import toast from "react-hot-toast";
99
import { FileItemData } from "./FileItem";
10-
import { updateMetaFile, getAuthenticatedSession, getDisplayNameFromMeta } from "../lib/helpers";
10+
import { getAuthenticatedSession, sanitizeResourceName, getParentContainerUrl, ensureTrailingSlash, copyFolderContents, deleteFolderResource } from "../lib/helpers";
1111

1212
interface RenameDialogProps {
1313
isOpen: boolean;
1414
onClose: () => void;
1515
file: FileItemData | null;
16-
onRenamed?: () => void;
16+
onRenamed?: (newUrl: string) => void;
1717
}
1818

1919
export default function RenameDialog({
@@ -29,32 +29,16 @@ export default function RenameDialog({
2929

3030
useEffect(() => {
3131
if (isOpen && file) {
32-
setIsLoadingName(true);
32+
setIsLoadingName(false);
3333
setIsRenaming(false);
34-
35-
// Fetch the .meta file to get the current display name
36-
const fetchDisplayName = async () => {
37-
try {
38-
const { fetch: fetchFn } = getAuthenticatedSession();
39-
const metaName = await getDisplayNameFromMeta(file.url, fetchFn);
40-
// Use .meta name if available, otherwise fall back to file.name
41-
setNewName(metaName || file.name);
42-
} catch (error) {
43-
// If .meta file doesn't exist or can't be read, use file.name
44-
setNewName(file.name);
45-
} finally {
46-
setIsLoadingName(false);
47-
// Focus and select the input text after loading
48-
setTimeout(() => {
49-
if (inputRef.current) {
50-
inputRef.current.focus();
51-
inputRef.current.select();
52-
}
53-
}, 100);
34+
setNewName(file.name);
35+
// Focus and select the input text
36+
setTimeout(() => {
37+
if (inputRef.current) {
38+
inputRef.current.focus();
39+
inputRef.current.select();
5440
}
55-
};
56-
57-
fetchDisplayName();
41+
}, 100);
5842
}
5943
}, [isOpen, file]);
6044

@@ -71,25 +55,56 @@ export default function RenameDialog({
7155

7256
setIsRenaming(true);
7357

74-
// Define variables outside try block so they're accessible in catch
75-
const sanitizedName = newName.trim();
76-
const resourceUrl = file.url.endsWith("/") ? file.url : file.url;
77-
const resourceUrlString = resourceUrl as UrlString;
78-
7958
try {
8059
const { fetch: fetchFn } = getAuthenticatedSession();
60+
const sanitizedName = sanitizeResourceName(newName.trim());
61+
const parentUrl = getParentContainerUrl(file.url);
62+
const parentWithSlash = ensureTrailingSlash(parentUrl);
63+
const encodedName = encodeURIComponent(sanitizedName);
64+
const isContainer = file.url.endsWith("/");
65+
const newUrl = isContainer ? `${parentWithSlash}${encodedName}/` : `${parentWithSlash}${encodedName}`;
66+
67+
// Check if target already exists
68+
try {
69+
const response = await fetchFn(newUrl, { method: "HEAD" });
70+
if (response.status !== 404) {
71+
toast.error(`A resource with the name "${sanitizedName}" already exists`);
72+
setIsRenaming(false);
73+
return;
74+
}
75+
} catch {
76+
// Continue if check fails
77+
}
8178

82-
// Update the .meta file for this resource
83-
// This is the standard Solid approach for storing metadata about resources
84-
await updateMetaFile(resourceUrlString, sanitizedName, fetchFn);
79+
if (isContainer) {
80+
// For folders: create new container, copy contents recursively, delete old
81+
await createContainerAt(newUrl as UrlString, { fetch: fetchFn });
82+
83+
await copyFolderContents(file.url, newUrl, fetchFn);
84+
85+
await deleteFolderResource(file.url, fetchFn);
86+
} else {
87+
// For files: fetch, create at new URL, delete old
88+
const fileBlob = await getFile(file.url as UrlString, { fetch: fetchFn });
89+
const contentType = fileBlob.type || "application/octet-stream";
90+
91+
await overwriteFile(newUrl as UrlString, fileBlob, {
92+
fetch: fetchFn,
93+
contentType,
94+
});
95+
96+
await deleteFile(file.url as UrlString, { fetch: fetchFn });
97+
}
8598

8699
toast.success(`Renamed to "${sanitizedName}"`);
87-
onClose();
88100

89101
// Notify parent to refresh
90102
if (onRenamed) {
91-
onRenamed();
103+
onRenamed(newUrl);
92104
}
105+
106+
// Close modal
107+
onClose();
93108
} catch (error) {
94109
console.error("Failed to rename:", error);
95110
toast.error(

app/components/shared/Button.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use client";
22

33
import { ButtonHTMLAttributes, ReactNode, forwardRef } from "react";
4+
import LoadingSpinner from "./LoadingSpinner";
45

56
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
67
variant?: "primary" | "secondary" | "ghost" | "icon";
@@ -46,10 +47,10 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button({
4647
{...props}
4748
>
4849
{isLoading ? (
49-
<>
50-
<span className="sr-only">Loading</span>
51-
<span aria-hidden="true">{children}</span>
52-
</>
50+
<span className="flex items-center justify-center gap-2">
51+
<LoadingSpinner size="sm" className="m-0" />
52+
<span>{children}</span>
53+
</span>
5354
) : (
5455
children
5556
)}

app/components/shared/Input.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(function Input({
3333
const paddingRight = rightIcon ? "pr-9" : "pr-3";
3434

3535
return (
36-
<div className="w-full">
36+
<section className="w-full">
3737
{label && (
3838
<label
3939
htmlFor={inputId}
@@ -80,7 +80,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(function Input({
8080
{helperText}
8181
</p>
8282
)}
83-
</div>
83+
</section>
8484
);
8585
});
8686

app/lib/helpers/copyUtils.ts

Lines changed: 14 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
deleteFile,
88
UrlString,
99
} from "@inrupt/solid-client";
10-
import { getDisplayNameFromMeta, updateMetaFile } from "./metaFileUtils";
1110

1211
const INVALID_NAME_CHARS = /[<>:"/\\|?*]/g;
1312

@@ -52,9 +51,10 @@ export const getParentContainerUrl = (resourceUrl: string): string => {
5251
};
5352

5453
const shouldSkipResourceCopy = (resourceUrl: string): boolean => {
55-
return resourceUrl.endsWith(".meta") || resourceUrl.endsWith(".acl");
54+
return resourceUrl.endsWith(".acl");
5655
};
5756

57+
5858
const resourceExists = async (url: string, fetchFn: typeof fetch): Promise<boolean> => {
5959
try {
6060
const response = await fetchFn(url, { method: "HEAD" });
@@ -64,7 +64,7 @@ const resourceExists = async (url: string, fetchFn: typeof fetch): Promise<boole
6464
if (response.status >= 200 && response.status < 300) {
6565
return true;
6666
}
67-
// For other statuses (401, 403, 405, etc.) assume the resource exists to avoid collisions
67+
6868
return true;
6969
} catch {
7070
return false;
@@ -108,10 +108,9 @@ const copyFileFromSource = async (
108108
fetch: fetchFn,
109109
contentType,
110110
});
111-
await updateMetaFile(targetUrl as UrlString, displayName, fetchFn);
112111
};
113112

114-
const copyFolderContents = async (
113+
export const copyFolderContents = async (
115114
sourceFolderUrl: string,
116115
destinationFolderUrl: string,
117116
fetchFn: typeof fetch
@@ -130,18 +129,13 @@ const copyFolderContents = async (
130129
const childDestination = `${ensureTrailingSlash(destinationFolderUrl)}${encodedChildName}/`;
131130

132131
await createContainerAt(childDestination as UrlString, { fetch: fetchFn });
133-
const childDisplayName =
134-
(await getDisplayNameFromMeta(resourceUrl, fetchFn)) ?? childName;
135-
await updateMetaFile(childDestination as UrlString, childDisplayName, fetchFn);
136-
137132
await copyFolderContents(resourceUrl, childDestination, fetchFn);
133+
138134
} else {
139135
const childName = decodeResourceNameFromUrl(resourceUrl);
140136
const encodedChildName = encodeURIComponent(childName);
141137
const childDestination = `${ensureTrailingSlash(destinationFolderUrl)}${encodedChildName}`;
142-
const childDisplayName =
143-
(await getDisplayNameFromMeta(resourceUrl, fetchFn)) ?? childName;
144-
await copyFileFromSource(resourceUrl, childDestination, childDisplayName, fetchFn);
138+
await copyFileFromSource(resourceUrl, childDestination, childName, fetchFn);
145139
}
146140
}
147141
};
@@ -150,31 +144,25 @@ export const copyFileResource = async (
150144
file: { url: string; name?: string; mimeType?: string },
151145
fetchFn: typeof fetch
152146
): Promise<void> => {
153-
const originalLabel =
154-
(await getDisplayNameFromMeta(file.url, fetchFn)) ??
155-
file.name ??
156-
decodeResourceNameFromUrl(file.url);
147+
const originalLabel = file.name ?? decodeResourceNameFromUrl(file.url);
157148
const parentUrl = getParentContainerUrl(file.url);
158149
const desiredName = `Copy of ${originalLabel}`;
159-
const { targetUrl, displayName } = await generateCopyTarget(parentUrl, desiredName, false, fetchFn);
160-
await copyFileFromSource(file.url, targetUrl, displayName, fetchFn, file.mimeType);
150+
const { targetUrl } = await generateCopyTarget(parentUrl, desiredName, false, fetchFn);
151+
await copyFileFromSource(file.url, targetUrl, desiredName, fetchFn, file.mimeType);
161152
};
162153

163154
export const copyFolderResource = async (
164155
folder: { url: string; name?: string },
165156
fetchFn: typeof fetch
166157
): Promise<void> => {
167-
const originalLabel =
168-
(await getDisplayNameFromMeta(folder.url, fetchFn)) ??
169-
folder.name ??
170-
decodeResourceNameFromUrl(folder.url);
158+
const originalLabel = folder.name ?? decodeResourceNameFromUrl(folder.url);
171159
const parentUrl = getParentContainerUrl(folder.url);
172160
const desiredName = `Copy of ${originalLabel}`;
173-
const { targetUrl, displayName } = await generateCopyTarget(parentUrl, desiredName, true, fetchFn);
161+
const { targetUrl } = await generateCopyTarget(parentUrl, desiredName, true, fetchFn);
174162

175163
await createContainerAt(targetUrl as UrlString, { fetch: fetchFn });
176-
await updateMetaFile(targetUrl as UrlString, displayName, fetchFn);
177164
await copyFolderContents(folder.url, targetUrl, fetchFn);
165+
178166
};
179167

180168
/**
@@ -187,10 +175,7 @@ export const moveFileResource = async (
187175
fetchFn: typeof fetch
188176
): Promise<void> => {
189177
// Get the original display name
190-
const originalLabel =
191-
(await getDisplayNameFromMeta(file.url, fetchFn)) ??
192-
file.name ??
193-
decodeResourceNameFromUrl(file.url);
178+
const originalLabel = file.name ?? decodeResourceNameFromUrl(file.url);
194179

195180
// Generate target URL in the destination folder
196181
const destinationWithSlash = ensureTrailingSlash(destinationFolderUrl);
@@ -214,18 +199,7 @@ export const moveFileResource = async (
214199
contentType,
215200
});
216201

217-
// Step 3: Update .meta file with display name
218-
await updateMetaFile(targetUrl as UrlString, originalLabel, fetchFn);
219-
220-
// Step 4: Delete the old file
202+
// Step 3: Delete the old file
221203
await deleteFile(file.url as UrlString, { fetch: fetchFn });
222-
223-
// Also delete the old .meta file if it exists
224-
try {
225-
const oldMetaUrl = `${file.url}.meta` as UrlString;
226-
await deleteFile(oldMetaUrl, { fetch: fetchFn });
227-
} catch (error) {
228-
// Ignore if .meta file doesn't exist
229-
}
230204
};
231205

0 commit comments

Comments
 (0)