Add file upload and download (zip for directories)

This commit is contained in:
sarah committed 2026-09-28 22:57:09 +02:00
1 parent 0d564982dc
commit da4e02472f
7 files changed
+383 -42

No files matched your search

+96 -27
View File
@@ -1,13 +1,15 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import {
CreateEntry,
DeleteEntry,
FetchLock,
FetchTree,
GetDownloadUrl,
MoveEntry,
RenameEntry,
SetLock as SetLockApi,
UploadFiles,
} from "../Api.js";
import { GetFileIcon, GetFolderIcon } from "../IconStyle.js";
import AccessModal from "./AccessModal.jsx";
@@ -31,6 +33,8 @@ export default function DirectoryPane({
const [LoadError, SetLoadError] = useState(null);
const [ModalConfig, SetModalConfig] = useState(null);
const [ErrorMessage, SetErrorMessage] = useState(null);
const [IsDraggingOver, SetIsDraggingOver] = useState(false);
const FileInputRef = useRef(null);
async function Reload() {
try {
@@ -154,16 +158,71 @@ export default function DirectoryPane({
}
}
async function HandleUpload(Files) {
if (!Files || Files.length === 0) return;
try {
const Result = await UploadFiles(RootName, DirectoryPath, Files);
if (Result.Errors && Result.Errors.length > 0) {
SetErrorMessage(Result.Errors.join("; "));
}
await Reload();
OnEntryCreated();
} catch (Error) {
SetErrorMessage(Error.message);
}
}
function HandleUploadButtonClick() {
FileInputRef.current?.click();
}
function HandleFileInputChange(Event) {
HandleUpload(Event.target.files);
Event.target.value = "";
}
function HandleDragOver(Event) {
if (IsGuest) return;
Event.preventDefault();
SetIsDraggingOver(true);
}
function HandleDragLeave(Event) {
Event.preventDefault();
SetIsDraggingOver(false);
}
function HandleDrop(Event) {
Event.preventDefault();
SetIsDraggingOver(false);
if (IsGuest) return;
HandleUpload(Event.dataTransfer.files);
}
return (
<div className="DirectoryPane">
{!IsGuest && (
<div className="PreviewToolbar">
<button onClick={OpenCreateFileModal}>New File</button>
<button onClick={OpenCreateDirectoryModal}>New Directory</button>
<button onClick={HandleUploadButtonClick}>Upload</button>
<input
ref={FileInputRef}
type="file"
multiple
style={{ display: "none" }}
onChange={HandleFileInputChange}
/>
</div>
)}
{LoadError && <div className="SidebarError">{LoadError}</div>}
<div className="DirectoryList">
<div
className={`DirectoryList${IsDraggingOver ? " DraggingOver" : ""}`}
onDragOver={HandleDragOver}
onDragLeave={HandleDragLeave}
onDrop={HandleDrop}
>
{Entries.map((Entry) => (
<div
className="DirectoryEntry"
@@ -178,31 +237,41 @@ export default function DirectoryPane({
: GetFileIcon(UseHyphenBullets)}
</span>
<span className="TreeLabel">{Entry.Name}</span>
{!IsGuest && (
<span className="TreeRowActions">
<button
className="TreeRowActionButton"
onClick={(Event) => HandleRequestLockEdit(Event, Entry)}
title="Access control"
>
🔒
</button>
<button
className="TreeRowActionButton"
onClick={(Event) => HandleRenameClick(Event, Entry)}
title="Rename / move"
>
✎
</button>
<button
className="TreeRowActionButton"
onClick={(Event) => HandleDeleteClick(Event, Entry)}
title="Delete"
>
✕
</button>
</span>
)}
<span className="TreeRowActions">
<a
className="TreeRowActionButton"
href={GetDownloadUrl(RootName, JoinPath(Entry.Name))}
onClick={(Event) => Event.stopPropagation()}
title={Entry.IsDirectory ? "Download as .zip" : "Download"}
>
⭳
</a>
{!IsGuest && (
<>
<button
className="TreeRowActionButton"
onClick={(Event) => HandleRequestLockEdit(Event, Entry)}
title="Access control"
>
🔒
</button>
<button
className="TreeRowActionButton"
onClick={(Event) => HandleRenameClick(Event, Entry)}
title="Rename / move"
>
✎
</button>
<button
className="TreeRowActionButton"
onClick={(Event) => HandleDeleteClick(Event, Entry)}
title="Delete"
>
✕
</button>
</>
)}
</span>
</div>
))}
{Entries.length === 0 && !LoadError && (
+23
View File
@@ -14,6 +14,7 @@ import {
FetchFile,
FetchResolvedFile,
GetAvatarUrl,
GetDownloadUrl,
GetFileUrl,
GetRawFileUrl,
SaveFile,
@@ -243,6 +244,11 @@ export default function PreviewPane({
if (FileType === "image") {
return (
<div className="PreviewMedia">
<div className={ToolbarClass}>
<a className="DownloadButton" href={GetDownloadUrl(RootName, FilePath)}>
Download
</a>
</div>
<img src={GetFileUrl(RootName, FilePath)} alt={FilePath} />
</div>
);
@@ -251,6 +257,11 @@ export default function PreviewPane({
if (FileType === "video") {
return (
<div className="PreviewMedia">
<div className={ToolbarClass}>
<a className="DownloadButton" href={GetDownloadUrl(RootName, FilePath)}>
Download
</a>
</div>
<video controls src={GetFileUrl(RootName, FilePath)} />
</div>
);
@@ -261,6 +272,9 @@ export default function PreviewPane({
<div className="PreviewFrame">
<div className={ToolbarClass}>
{!IsGuest && <button onClick={() => SetIsEditing(true)}>Edit</button>}
<a className="DownloadButton" href={GetDownloadUrl(RootName, FilePath)}>
Download
</a>
</div>
<iframe title={FilePath} sandbox="" src={GetRawFileUrl(RootName, FilePath)} />
</div>
@@ -272,6 +286,9 @@ export default function PreviewPane({
<div className="PreviewMarkdown">
<div className={ToolbarClass}>
{!IsGuest && <button onClick={() => SetIsEditing(true)}>Edit</button>}
<a className="DownloadButton" href={GetDownloadUrl(RootName, FilePath)}>
Download
</a>
</div>
<div
className="MarkdownBody"
@@ -294,6 +311,9 @@ export default function PreviewPane({
<div className="PreviewText">
<div className={ToolbarClass}>
{!IsGuest && <button onClick={() => SetIsEditing(true)}>Edit</button>}
<a className="DownloadButton" href={GetDownloadUrl(RootName, FilePath)}>
Download
</a>
</div>
<pre className="TextBody">{ResolvedContent}</pre>
</div>
@@ -312,6 +332,9 @@ export default function PreviewPane({
{IsSaving ? "Saving…" : "Save"}
</button>
)}
<a className="DownloadButton" href={GetDownloadUrl(RootName, FilePath)}>
Download
</a>
{SaveError && <span className="SaveErrorText">{SaveError}</span>}
</div>
<CodeMirror
+86 -15
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import {
CreateEntry,
@@ -6,9 +6,11 @@ import {
FetchLock,
FetchTree,
GetAvatarUrl,
GetDownloadUrl,
MoveEntry,
RenameEntry,
SetLock as SetLockApi,
UploadFiles,
} from "../Api.js";
import { GetFileIcon, GetFolderIcon } from "../IconStyle.js";
import AccessModal from "./AccessModal.jsx";
@@ -151,19 +153,29 @@ function TreeNode({
: GetFileIcon(UseHyphenBullets)}
</span>
<span className="TreeLabel">{Entry.Name}</span>
{!IsGuest && (
<span className="TreeRowActions">
<button className="TreeRowActionButton" onClick={HandleLockClick} title="Access control">
🔒
</button>
<button className="TreeRowActionButton" onClick={HandleRenameClick} title="Rename / move">
✎
</button>
<button className="TreeRowActionButton" onClick={HandleDeleteClick} title="Delete">
✕
</button>
</span>
)}
<span className="TreeRowActions">
<a
className="TreeRowActionButton"
href={GetDownloadUrl(RootName, FullPath)}
onClick={(Event) => Event.stopPropagation()}
title={Entry.IsDirectory ? "Download as .zip" : "Download"}
>
⭳
</a>
{!IsGuest && (
<>
<button className="TreeRowActionButton" onClick={HandleLockClick} title="Access control">
🔒
</button>
<button className="TreeRowActionButton" onClick={HandleRenameClick} title="Rename / move">
✎
</button>
<button className="TreeRowActionButton" onClick={HandleDeleteClick} title="Delete">
✕
</button>
</>
)}
</span>
</div>
{IsRenaming && (
<PromptModal
@@ -284,6 +296,8 @@ export default function Sidebar({
const [LoadError, SetLoadError] = useState(null);
const [ModalConfig, SetModalConfig] = useState(null);
const [ErrorMessage, SetErrorMessage] = useState(null);
const [IsDraggingOver, SetIsDraggingOver] = useState(false);
const FileInputRef = useRef(null);
useEffect(() => {
if (RootName) {
@@ -345,6 +359,47 @@ export default function Sidebar({
}
}
async function HandleUpload(Files) {
if (!Files || Files.length === 0) return;
try {
const Result = await UploadFiles(RootName, CurrentDirectoryPath, Files);
if (Result.Errors && Result.Errors.length > 0) {
SetErrorMessage(Result.Errors.join("; "));
}
OnEntryCreated();
} catch (Error) {
SetErrorMessage(Error.message);
}
}
function HandleUploadButtonClick() {
FileInputRef.current?.click();
}
function HandleFileInputChange(Event) {
HandleUpload(Event.target.files);
Event.target.value = "";
}
function HandleDragOver(Event) {
if (IsGuest) return;
Event.preventDefault();
SetIsDraggingOver(true);
}
function HandleDragLeave(Event) {
Event.preventDefault();
SetIsDraggingOver(false);
}
function HandleDrop(Event) {
Event.preventDefault();
SetIsDraggingOver(false);
if (IsGuest) return;
HandleUpload(Event.dataTransfer.files);
}
return (
<div className="Sidebar">
<div className="SidebarHeader">
@@ -364,10 +419,25 @@ export default function Sidebar({
<div className="SidebarNewEntry SidebarNewEntryTop">
<button onClick={OpenCreateFileModal}>New File</button>
<button onClick={OpenCreateDirectoryModal}>New Directory</button>
<button onClick={HandleUploadButtonClick}>Upload</button>
</div>
)}
{!IsGuest && (
<input
ref={FileInputRef}
type="file"
multiple
style={{ display: "none" }}
onChange={HandleFileInputChange}
/>
)}
{LoadError && <div className="SidebarError">{LoadError}</div>}
<div className="SidebarTree">
<div
className={`SidebarTree${IsDraggingOver ? " DraggingOver" : ""}`}
onDragOver={HandleDragOver}
onDragLeave={HandleDragLeave}
onDrop={HandleDrop}
>
{RootEntries.map((Entry) => (
<TreeNode
key={Entry.Name}
@@ -395,6 +465,7 @@ export default function Sidebar({
<div className="SidebarNewEntry">
<button onClick={OpenCreateFileModal}>New File</button>
<button onClick={OpenCreateDirectoryModal}>New Directory</button>
<button onClick={HandleUploadButtonClick}>Upload</button>
</div>
)}