Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions app/components/FileItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ interface FileItemProps {
onPreview?: (file: FileItemData) => void;
onCopy?: (file: FileItemData) => void;
onMove?: (file: FileItemData) => void;
onDownload?: (file: FileItemData) => void;
isSelected?: boolean;
}

Expand All @@ -37,6 +38,7 @@ export default function FileItem({
onPreview,
onCopy,
onMove,
onDownload,
isSelected = false,
}: FileItemProps) {
const [isHovered, setIsHovered] = useState(false);
Expand Down Expand Up @@ -131,7 +133,7 @@ export default function FileItem({
position="top-right"
onRename={onRename}
onPreview={onPreview}
onDownload={(f) => console.log("Download:", f.name)}
onDownload={onDownload}
onCopy={onCopy}
onMove={onMove}
onDelete={(f) => console.log("Delete:", f.name)}
Expand Down Expand Up @@ -179,7 +181,7 @@ export default function FileItem({
position="right"
onRename={onRename}
onPreview={onPreview}
onDownload={(f) => console.log("Download:", f.name)}
onDownload={onDownload}
onCopy={onCopy}
onMove={onMove}
onDelete={(f) => console.log("Delete:", f.name)}
Expand Down
4 changes: 4 additions & 0 deletions app/components/FileList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface FileListProps {
onFilePreview?: (file: FileItemData) => void;
onFileCopy?: (file: FileItemData) => void;
onFileMove?: (file: FileItemData) => void;
onFileDownload?: (file: FileItemData) => void;
selectedFileIds: string[];
}

Expand All @@ -28,6 +29,7 @@ export default function FileList({
onFilePreview,
onFileCopy,
onFileMove,
onFileDownload,
selectedFileIds,
}: FileListProps) {
const [view, setView] = useState<"grid" | "list">(() => {
Expand Down Expand Up @@ -65,6 +67,7 @@ export default function FileList({
onPreview={onFilePreview}
onCopy={onFileCopy}
onMove={onFileMove}
onDownload={onFileDownload}
isSelected={selectedFileIds.includes(file.id)}
/>
))}
Expand All @@ -82,6 +85,7 @@ export default function FileList({
onPreview={onFilePreview}
onCopy={onFileCopy}
onMove={onFileMove}
onDownload={onFileDownload}
isSelected={selectedFileIds.includes(file.id)}
/>
))}
Expand Down
33 changes: 33 additions & 0 deletions app/components/FileManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import {
getAuthenticatedSession,
copyFileResource,
copyFolderResource,
downloadFile,
downloadFolderAsZip,
} from "../lib/helpers";


Expand Down Expand Up @@ -262,6 +264,36 @@ export default function FileManager() {
setRefreshKey((prev) => prev + 1);
};

const handleDownload = async (file: FileItemData) => {
if (!file) {
return;
}

const toastId = toast.loading(
file.type === "folder" ? `Preparing "${file.name}" for download...` : `Downloading "${file.name}"...`
);

try {
const { fetch: fetchFn } = getAuthenticatedSession();

if (file.type === "folder") {
await downloadFolderAsZip(file.url, file.name, fetchFn);
toast.success(`Downloaded "${file.name}.zip"`, { id: toastId });
} else {
await downloadFile(file.url, file.name, fetchFn);
toast.success(`Downloaded "${file.name}"`, { id: toastId });
}
} catch (error) {
console.error("Failed to download resource:", error);
toast.error(
error instanceof Error
? `Failed to download: ${error.message}`
: "Failed to download resource",
{ id: toastId }
);
}
};

const storageFiles: FileItemData[] = storages.map((storage) => ({
id: storage.id,
name: storage.name,
Expand Down Expand Up @@ -467,6 +499,7 @@ export default function FileManager() {
onFilePreview={handlePreview}
onFileCopy={handleCopy}
onFileMove={handleMove}
onFileDownload={handleDownload}
selectedFileIds={selectedFileIds}
/>
</div>
Expand Down
139 changes: 139 additions & 0 deletions app/lib/helpers/downloadUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import JSZip from "jszip";
import { getSolidDataset, getContainedResourceUrlAll, UrlString } from "@inrupt/solid-client";
import { decodeResourceNameFromUrl, ensureTrailingSlash } from "./copyUtils";

/**
* Downloads a single file
*/
export async function downloadFile(
fileUrl: string,
fileName: string,
fetchFn: typeof fetch
): Promise<void> {
try {
console.log("downloadFile: Starting fetch for", fileUrl);
const response = await fetchFn(fileUrl);

if (!response.ok) {
const errorText = await response.text().catch(() => response.statusText);
throw new Error(`Failed to fetch file: ${response.status} ${errorText}`);
}

const blob = await response.blob();

// Create a download link
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = fileName;
link.style.display = "none";
document.body.appendChild(link);

// Trigger download immediately
link.click();

// Clean up after a delay to ensure download starts
setTimeout(() => {
document.body.removeChild(link);
URL.revokeObjectURL(url);
}, 100);
} catch (error) {
console.error("Failed to download file:", error);
throw new Error(`Failed to download file: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}

/**
* Recursively collects all files in a folder
*/
async function collectFolderFiles(
folderUrl: string,
fetchFn: typeof fetch,
zip: JSZip,
basePath: string = "",
visited: Set<string> = new Set()
): Promise<void> {
const normalizedUrl = ensureTrailingSlash(folderUrl);

// Prevent infinite loops
if (visited.has(normalizedUrl)) {
return;
}
visited.add(normalizedUrl);

try {
const dataset = await getSolidDataset(normalizedUrl as UrlString, { fetch: fetchFn });
const containedResources = getContainedResourceUrlAll(dataset);

for (const resourceUrl of containedResources) {
if (resourceUrl.endsWith("/")) {
// It's a folder - recurse
const folderName = decodeResourceNameFromUrl(resourceUrl);
const folderPath = basePath ? `${basePath}/${folderName}` : folderName;
await collectFolderFiles(resourceUrl, fetchFn, zip, folderPath, visited);
} else {
// It's a file - add to zip
try {
const response = await fetchFn(resourceUrl);

if (!response.ok) {
console.warn(`Failed to fetch file ${resourceUrl}: ${response.status} ${response.statusText}`);
continue;
}

const blob = await response.blob();
const fileName = decodeResourceNameFromUrl(resourceUrl);
const filePath = basePath ? `${basePath}/${fileName}` : fileName;

zip.file(filePath, blob);
} catch (error) {
console.warn(`Failed to add file ${resourceUrl} to zip:`, error);

}
}
}
} catch (error) {
console.error(`Failed to access folder ${folderUrl}:`, error);
throw error;
}
}

/**
* Downloads a folder as a ZIP file
*/
export async function downloadFolderAsZip(
folderUrl: string,
folderName: string,
fetchFn: typeof fetch
): Promise<void> {
try {
const zip = new JSZip();

// Collect all files recursively
await collectFolderFiles(folderUrl, fetchFn, zip);

// Generate the ZIP file
const zipBlob = await zip.generateAsync({ type: "blob" });

// Create a download link
const url = URL.createObjectURL(zipBlob);
const link = document.createElement("a");
link.href = url;
link.download = `${folderName}.zip`;
link.style.display = "none";
document.body.appendChild(link);

// Trigger download immediately
link.click();

// Clean up after a delay to ensure download starts
setTimeout(() => {
document.body.removeChild(link);
URL.revokeObjectURL(url);
}, 100);
} catch (error) {
console.error("Failed to download folder as zip:", error);
throw new Error(`Failed to download folder: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}

1 change: 1 addition & 0 deletions app/lib/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ export * from "./profileUtils";
export * from "./sessionUtils";
export * from "./metaFileUtils";
export * from "./copyUtils";
export * from "./downloadUtils";

Loading