Upload Directories via Web
This implements the option to choose directories when uploading files via the Web UI (The most important part of #52). When you choose a directory, its file and sub-directory structure will be maintained. Tested with the latest Safari, Firefox, and Chrome. Additionally, the Deno version was updated, which required some accessibility improvements as well.
This commit is contained in:
@@ -5,3 +5,6 @@
|
|||||||
docker-compose*
|
docker-compose*
|
||||||
Dockerfile
|
Dockerfile
|
||||||
render.yaml
|
render.yaml
|
||||||
|
LICENSE
|
||||||
|
README.md
|
||||||
|
.env.sample
|
||||||
|
|||||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -7,7 +7,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: denoland/setup-deno@v1
|
- uses: denoland/setup-deno@v2
|
||||||
with:
|
with:
|
||||||
deno-version-file: .dvmrc
|
deno-version-file: .dvmrc
|
||||||
- run: |
|
- run: |
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM denoland/deno:ubuntu-2.1.9
|
FROM denoland/deno:ubuntu-2.3.1
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ export default function BudgetModal(
|
|||||||
<button
|
<button
|
||||||
class='px-5 py-2 bg-red-600 text-white cursor-pointer rounded-md mr-2 opacity-30 hover:opacity-100'
|
class='px-5 py-2 bg-red-600 text-white cursor-pointer rounded-md mr-2 opacity-30 hover:opacity-100'
|
||||||
onClick={() => onClickDelete()}
|
onClick={() => onClickDelete()}
|
||||||
|
type='button'
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
@@ -112,6 +113,7 @@ export default function BudgetModal(
|
|||||||
<button
|
<button
|
||||||
class='px-5 py-2 bg-slate-600 hover:bg-slate-500 text-white cursor-pointer rounded-md mr-2'
|
class='px-5 py-2 bg-slate-600 hover:bg-slate-500 text-white cursor-pointer rounded-md mr-2'
|
||||||
onClick={() => onClose()}
|
onClick={() => onClose()}
|
||||||
|
type='button'
|
||||||
>
|
>
|
||||||
{budget ? 'Cancel' : 'Close'}
|
{budget ? 'Cancel' : 'Close'}
|
||||||
</button>
|
</button>
|
||||||
@@ -123,6 +125,7 @@ export default function BudgetModal(
|
|||||||
newBudgetMonth.value.substring(0, 7),
|
newBudgetMonth.value.substring(0, 7),
|
||||||
formatInputToNumber(newBudgetValue.value),
|
formatInputToNumber(newBudgetValue.value),
|
||||||
)}
|
)}
|
||||||
|
type='button'
|
||||||
>
|
>
|
||||||
{budget ? 'Update' : 'Create'}
|
{budget ? 'Update' : 'Create'}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -237,6 +237,7 @@ export default function ExpenseModal(
|
|||||||
<button
|
<button
|
||||||
class='px-5 py-2 bg-red-600 text-white cursor-pointer rounded-md mr-2 opacity-30 hover:opacity-100'
|
class='px-5 py-2 bg-red-600 text-white cursor-pointer rounded-md mr-2 opacity-30 hover:opacity-100'
|
||||||
onClick={() => onClickDelete()}
|
onClick={() => onClickDelete()}
|
||||||
|
type='button'
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
@@ -245,6 +246,7 @@ export default function ExpenseModal(
|
|||||||
<button
|
<button
|
||||||
class='px-5 py-2 bg-slate-600 hover:bg-slate-500 text-white cursor-pointer rounded-md mr-2'
|
class='px-5 py-2 bg-slate-600 hover:bg-slate-500 text-white cursor-pointer rounded-md mr-2'
|
||||||
onClick={() => onClose()}
|
onClick={() => onClose()}
|
||||||
|
type='button'
|
||||||
>
|
>
|
||||||
{expense ? 'Cancel' : 'Close'}
|
{expense ? 'Cancel' : 'Close'}
|
||||||
</button>
|
</button>
|
||||||
@@ -259,6 +261,7 @@ export default function ExpenseModal(
|
|||||||
newExpenseIsRecurring.value,
|
newExpenseIsRecurring.value,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
type='button'
|
||||||
>
|
>
|
||||||
{expense ? 'Update' : 'Create'}
|
{expense ? 'Update' : 'Create'}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -729,12 +729,14 @@ export default function MainExpenses({ initialBudgets, initialExpenses, initialM
|
|||||||
<button
|
<button
|
||||||
class={`text-white block px-4 py-2 text-sm w-full text-left hover:bg-slate-600`}
|
class={`text-white block px-4 py-2 text-sm w-full text-left hover:bg-slate-600`}
|
||||||
onClick={() => onClickCreateExpense()}
|
onClick={() => onClickCreateExpense()}
|
||||||
|
type='button'
|
||||||
>
|
>
|
||||||
New Expense
|
New Expense
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class={`text-white block px-4 py-2 text-sm w-full text-left hover:bg-slate-600`}
|
class={`text-white block px-4 py-2 text-sm w-full text-left hover:bg-slate-600`}
|
||||||
onClick={() => onClickCreateBudget()}
|
onClick={() => onClickCreateBudget()}
|
||||||
|
type='button'
|
||||||
>
|
>
|
||||||
New Budget
|
New Budget
|
||||||
</button>
|
</button>
|
||||||
@@ -746,12 +748,14 @@ export default function MainExpenses({ initialBudgets, initialExpenses, initialM
|
|||||||
<button
|
<button
|
||||||
class={`text-white block px-4 py-2 text-sm w-full text-left hover:bg-slate-600`}
|
class={`text-white block px-4 py-2 text-sm w-full text-left hover:bg-slate-600`}
|
||||||
onClick={() => onClickImportFile()}
|
onClick={() => onClickImportFile()}
|
||||||
|
type='button'
|
||||||
>
|
>
|
||||||
Import
|
Import
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class={`text-white block px-4 py-2 text-sm w-full text-left hover:bg-slate-600`}
|
class={`text-white block px-4 py-2 text-sm w-full text-left hover:bg-slate-600`}
|
||||||
onClick={() => onClickExportFile()}
|
onClick={() => onClickExportFile()}
|
||||||
|
type='button'
|
||||||
>
|
>
|
||||||
Export
|
Export
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -44,12 +44,14 @@ export default function CreateDirectoryModal(
|
|||||||
<button
|
<button
|
||||||
class='px-5 py-2 bg-slate-600 hover:bg-slate-500 text-white cursor-pointer rounded-md'
|
class='px-5 py-2 bg-slate-600 hover:bg-slate-500 text-white cursor-pointer rounded-md'
|
||||||
onClick={() => onClickSave(newDirectoryName.value)}
|
onClick={() => onClickSave(newDirectoryName.value)}
|
||||||
|
type='button'
|
||||||
>
|
>
|
||||||
Create
|
Create
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class='px-5 py-2 bg-slate-600 hover:bg-slate-500 text-white cursor-pointer rounded-md'
|
class='px-5 py-2 bg-slate-600 hover:bg-slate-500 text-white cursor-pointer rounded-md'
|
||||||
onClick={() => onClose()}
|
onClick={() => onClose()}
|
||||||
|
type='button'
|
||||||
>
|
>
|
||||||
Close
|
Close
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -61,6 +61,11 @@ export default function MainFiles({ initialDirectories, initialFiles, initialPat
|
|||||||
const fileInput = document.createElement('input');
|
const fileInput = document.createElement('input');
|
||||||
fileInput.type = 'file';
|
fileInput.type = 'file';
|
||||||
fileInput.multiple = true;
|
fileInput.multiple = true;
|
||||||
|
fileInput.webkitdirectory = true;
|
||||||
|
// @ts-expect-error - mozdirectory is not typed
|
||||||
|
fileInput.mozdirectory = true;
|
||||||
|
// @ts-expect-error - directory is not typed
|
||||||
|
fileInput.directory = true;
|
||||||
fileInput.click();
|
fileInput.click();
|
||||||
|
|
||||||
fileInput.onchange = async (event) => {
|
fileInput.onchange = async (event) => {
|
||||||
@@ -78,10 +83,19 @@ export default function MainFiles({ initialDirectories, initialFiles, initialPat
|
|||||||
areNewOptionsOpen.value = false;
|
areNewOptionsOpen.value = false;
|
||||||
|
|
||||||
const requestBody = new FormData();
|
const requestBody = new FormData();
|
||||||
|
requestBody.set('path_in_view', path.value);
|
||||||
requestBody.set('parent_path', path.value);
|
requestBody.set('parent_path', path.value);
|
||||||
requestBody.set('name', chosenFile.name);
|
requestBody.set('name', chosenFile.name);
|
||||||
requestBody.set('contents', chosenFile);
|
requestBody.set('contents', chosenFile);
|
||||||
|
|
||||||
|
// Keep directory structure if the file comes from a chosen directory
|
||||||
|
if (chosenFile.webkitRelativePath) {
|
||||||
|
const directoryPath = chosenFile.webkitRelativePath.replace(chosenFile.name, '');
|
||||||
|
|
||||||
|
// We don't need to worry about path joining here, the API will handle it (and make sure it's secure)
|
||||||
|
requestBody.set('parent_path', `${path.value}${directoryPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/files/upload`, {
|
const response = await fetch(`/api/files/upload`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -94,6 +108,7 @@ export default function MainFiles({ initialDirectories, initialFiles, initialPat
|
|||||||
}
|
}
|
||||||
|
|
||||||
files.value = [...result.newFiles];
|
files.value = [...result.newFiles];
|
||||||
|
directories.value = [...result.newDirectories];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
@@ -509,6 +524,7 @@ export default function MainFiles({ initialDirectories, initialFiles, initialPat
|
|||||||
<button
|
<button
|
||||||
class={`text-white block px-4 py-2 text-sm w-full text-left hover:bg-slate-600`}
|
class={`text-white block px-4 py-2 text-sm w-full text-left hover:bg-slate-600`}
|
||||||
onClick={() => onClickBulkDelete()}
|
onClick={() => onClickBulkDelete()}
|
||||||
|
type='button'
|
||||||
>
|
>
|
||||||
Delete {bulkItemsCount} item{bulkItemsCount === 1 ? '' : 's'}
|
Delete {bulkItemsCount} item{bulkItemsCount === 1 ? '' : 's'}
|
||||||
</button>
|
</button>
|
||||||
@@ -557,12 +573,14 @@ export default function MainFiles({ initialDirectories, initialFiles, initialPat
|
|||||||
<button
|
<button
|
||||||
class={`text-white block px-4 py-2 text-sm w-full text-left hover:bg-slate-600`}
|
class={`text-white block px-4 py-2 text-sm w-full text-left hover:bg-slate-600`}
|
||||||
onClick={() => onClickUploadFile()}
|
onClick={() => onClickUploadFile()}
|
||||||
|
type='button'
|
||||||
>
|
>
|
||||||
Upload File
|
Upload Files
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class={`text-white block px-4 py-2 text-sm w-full text-left hover:bg-slate-600`}
|
class={`text-white block px-4 py-2 text-sm w-full text-left hover:bg-slate-600`}
|
||||||
onClick={() => onClickCreateDirectory()}
|
onClick={() => onClickCreateDirectory()}
|
||||||
|
type='button'
|
||||||
>
|
>
|
||||||
New Directory
|
New Directory
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -129,12 +129,14 @@ export default function MoveDirectoryOrFileModal(
|
|||||||
<button
|
<button
|
||||||
class='px-5 py-2 bg-slate-600 hover:bg-slate-500 text-white cursor-pointer rounded-md'
|
class='px-5 py-2 bg-slate-600 hover:bg-slate-500 text-white cursor-pointer rounded-md'
|
||||||
onClick={() => onClickSave(newPath.value)}
|
onClick={() => onClickSave(newPath.value)}
|
||||||
|
type='button'
|
||||||
>
|
>
|
||||||
Move {isDirectory ? 'directory' : 'file'} here
|
Move {isDirectory ? 'directory' : 'file'} here
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class='px-5 py-2 bg-slate-600 hover:bg-slate-500 text-white cursor-pointer rounded-md'
|
class='px-5 py-2 bg-slate-600 hover:bg-slate-500 text-white cursor-pointer rounded-md'
|
||||||
onClick={() => onClose()}
|
onClick={() => onClose()}
|
||||||
|
type='button'
|
||||||
>
|
>
|
||||||
Close
|
Close
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -51,12 +51,14 @@ export default function RenameDirectoryOrFileModal(
|
|||||||
<button
|
<button
|
||||||
class='px-5 py-2 bg-slate-600 hover:bg-slate-500 text-white cursor-pointer rounded-md'
|
class='px-5 py-2 bg-slate-600 hover:bg-slate-500 text-white cursor-pointer rounded-md'
|
||||||
onClick={() => onClickSave(newName.value)}
|
onClick={() => onClickSave(newName.value)}
|
||||||
|
type='button'
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class='px-5 py-2 bg-slate-600 hover:bg-slate-500 text-white cursor-pointer rounded-md'
|
class='px-5 py-2 bg-slate-600 hover:bg-slate-500 text-white cursor-pointer rounded-md'
|
||||||
onClick={() => onClose()}
|
onClick={() => onClose()}
|
||||||
|
type='button'
|
||||||
>
|
>
|
||||||
Close
|
Close
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -44,12 +44,14 @@ export default function CreateNoteModal(
|
|||||||
<button
|
<button
|
||||||
class='px-5 py-2 bg-slate-600 hover:bg-slate-500 text-white cursor-pointer rounded-md'
|
class='px-5 py-2 bg-slate-600 hover:bg-slate-500 text-white cursor-pointer rounded-md'
|
||||||
onClick={() => onClickSave(newNoteName.value)}
|
onClick={() => onClickSave(newNoteName.value)}
|
||||||
|
type='button'
|
||||||
>
|
>
|
||||||
Create
|
Create
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class='px-5 py-2 bg-slate-600 hover:bg-slate-500 text-white cursor-pointer rounded-md'
|
class='px-5 py-2 bg-slate-600 hover:bg-slate-500 text-white cursor-pointer rounded-md'
|
||||||
onClick={() => onClose()}
|
onClick={() => onClose()}
|
||||||
|
type='button'
|
||||||
>
|
>
|
||||||
Close
|
Close
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ export default function MainNotes({ initialDirectories, initialFiles, initialPat
|
|||||||
<>
|
<>
|
||||||
<section class='flex flex-row items-center justify-between mb-4'>
|
<section class='flex flex-row items-center justify-between mb-4'>
|
||||||
<section class='flex items-center justify-end w-full'>
|
<section class='flex items-center justify-end w-full'>
|
||||||
<FilesBreadcrumb path={path.value} isShowingNotes={true} />
|
<FilesBreadcrumb path={path.value} isShowingNotes />
|
||||||
|
|
||||||
<section class='relative inline-block text-left ml-2'>
|
<section class='relative inline-block text-left ml-2'>
|
||||||
<div>
|
<div>
|
||||||
@@ -241,12 +241,14 @@ export default function MainNotes({ initialDirectories, initialFiles, initialPat
|
|||||||
<button
|
<button
|
||||||
class={`text-white block px-4 py-2 text-sm w-full text-left hover:bg-slate-600`}
|
class={`text-white block px-4 py-2 text-sm w-full text-left hover:bg-slate-600`}
|
||||||
onClick={() => onClickCreateNote()}
|
onClick={() => onClickCreateNote()}
|
||||||
|
type='button'
|
||||||
>
|
>
|
||||||
New Note
|
New Note
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class={`text-white block px-4 py-2 text-sm w-full text-left hover:bg-slate-600`}
|
class={`text-white block px-4 py-2 text-sm w-full text-left hover:bg-slate-600`}
|
||||||
onClick={() => onClickCreateDirectory()}
|
onClick={() => onClickCreateDirectory()}
|
||||||
|
type='button'
|
||||||
>
|
>
|
||||||
New Directory
|
New Directory
|
||||||
</button>
|
</button>
|
||||||
@@ -262,7 +264,7 @@ export default function MainNotes({ initialDirectories, initialFiles, initialPat
|
|||||||
files={files.value}
|
files={files.value}
|
||||||
onClickDeleteDirectory={onClickDeleteDirectory}
|
onClickDeleteDirectory={onClickDeleteDirectory}
|
||||||
onClickDeleteFile={onClickDeleteFile}
|
onClickDeleteFile={onClickDeleteFile}
|
||||||
isShowingNotes={true}
|
isShowingNotes
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ export default function MainPhotos({ initialDirectories, initialFiles, initialPa
|
|||||||
<>
|
<>
|
||||||
<section class='flex flex-row items-center justify-between mb-4'>
|
<section class='flex flex-row items-center justify-between mb-4'>
|
||||||
<section class='flex items-center justify-end w-full'>
|
<section class='flex items-center justify-end w-full'>
|
||||||
<FilesBreadcrumb path={path.value} isShowingPhotos={true} />
|
<FilesBreadcrumb path={path.value} isShowingPhotos />
|
||||||
|
|
||||||
<section class='relative inline-block text-left ml-2'>
|
<section class='relative inline-block text-left ml-2'>
|
||||||
<div>
|
<div>
|
||||||
@@ -167,12 +167,14 @@ export default function MainPhotos({ initialDirectories, initialFiles, initialPa
|
|||||||
<button
|
<button
|
||||||
class={`text-white block px-4 py-2 text-sm w-full text-left hover:bg-slate-600`}
|
class={`text-white block px-4 py-2 text-sm w-full text-left hover:bg-slate-600`}
|
||||||
onClick={() => onClickUploadFile()}
|
onClick={() => onClickUploadFile()}
|
||||||
|
type='button'
|
||||||
>
|
>
|
||||||
Upload Photo
|
Upload Photo
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class={`text-white block px-4 py-2 text-sm w-full text-left hover:bg-slate-600`}
|
class={`text-white block px-4 py-2 text-sm w-full text-left hover:bg-slate-600`}
|
||||||
onClick={() => onClickCreateDirectory()}
|
onClick={() => onClickCreateDirectory()}
|
||||||
|
type='button'
|
||||||
>
|
>
|
||||||
New Directory
|
New Directory
|
||||||
</button>
|
</button>
|
||||||
@@ -186,7 +188,7 @@ export default function MainPhotos({ initialDirectories, initialFiles, initialPa
|
|||||||
<ListFiles
|
<ListFiles
|
||||||
directories={directories.value}
|
directories={directories.value}
|
||||||
files={[]}
|
files={[]}
|
||||||
isShowingPhotos={true}
|
isShowingPhotos
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ListPhotos
|
<ListPhotos
|
||||||
|
|||||||
@@ -175,6 +175,7 @@ export default function Articles({ initialArticles }: ArticlesProps) {
|
|||||||
filter.value.status === 'unread' ? 'font-semibold' : ''
|
filter.value.status === 'unread' ? 'font-semibold' : ''
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setNewFilter({ status: 'unread' })}
|
onClick={() => setNewFilter({ status: 'unread' })}
|
||||||
|
type='button'
|
||||||
>
|
>
|
||||||
Show only unread
|
Show only unread
|
||||||
</button>
|
</button>
|
||||||
@@ -183,6 +184,7 @@ export default function Articles({ initialArticles }: ArticlesProps) {
|
|||||||
filter.value.status === 'all' ? 'font-semibold' : ''
|
filter.value.status === 'all' ? 'font-semibold' : ''
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setNewFilter({ status: 'all' })}
|
onClick={() => setNewFilter({ status: 'all' })}
|
||||||
|
type='button'
|
||||||
>
|
>
|
||||||
Show all
|
Show all
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -252,12 +252,14 @@ export default function Feeds({ initialFeeds }: FeedsProps) {
|
|||||||
<button
|
<button
|
||||||
class={`text-white block px-4 py-2 text-sm w-full text-left hover:bg-slate-600`}
|
class={`text-white block px-4 py-2 text-sm w-full text-left hover:bg-slate-600`}
|
||||||
onClick={() => onClickImportOpml()}
|
onClick={() => onClickImportOpml()}
|
||||||
|
type='button'
|
||||||
>
|
>
|
||||||
Import OPML
|
Import OPML
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class={`text-white block px-4 py-2 text-sm w-full text-left hover:bg-slate-600`}
|
class={`text-white block px-4 py-2 text-sm w-full text-left hover:bg-slate-600`}
|
||||||
onClick={() => onClickExportOpml()}
|
onClick={() => onClickExportOpml()}
|
||||||
|
type='button'
|
||||||
>
|
>
|
||||||
Export OPML
|
Export OPML
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export default function Note({ fileName, currentPath, contents }: NoteProps) {
|
|||||||
return (
|
return (
|
||||||
<section class='flex flex-col'>
|
<section class='flex flex-col'>
|
||||||
<section class='mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8 w-full flex flex-row items-center justify-start'>
|
<section class='mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8 w-full flex flex-row items-center justify-start'>
|
||||||
<FilesBreadcrumb path={currentPath} isShowingNotes={true} />
|
<FilesBreadcrumb path={currentPath} isShowingNotes />
|
||||||
<h3 class='text-base text-white font-semibold'>
|
<h3 class='text-base text-white font-semibold'>
|
||||||
<span class='mr-2 text-xs'>/</span>
|
<span class='mr-2 text-xs'>/</span>
|
||||||
{decodeURIComponent(fileName)}
|
{decodeURIComponent(fileName)}
|
||||||
|
|||||||
@@ -192,6 +192,15 @@ export async function createFile(
|
|||||||
const rootPath = join(getFilesRootPath(), userId, path);
|
const rootPath = join(getFilesRootPath(), userId, path);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Ensure the directory exist, if being requested
|
||||||
|
try {
|
||||||
|
await Deno.stat(rootPath);
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as Error).toString().includes('NotFound')) {
|
||||||
|
await Deno.mkdir(rootPath, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof contents === 'string') {
|
if (typeof contents === 'string') {
|
||||||
await Deno.writeTextFile(join(rootPath, name), contents, { append: false, createNew: true });
|
await Deno.writeTextFile(join(rootPath, name), contents, { append: false, createNew: true });
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { Handlers } from 'fresh/server.ts';
|
import { Handlers } from 'fresh/server.ts';
|
||||||
|
|
||||||
import { DirectoryFile, FreshContextState } from '/lib/types.ts';
|
import { Directory, DirectoryFile, FreshContextState } from '/lib/types.ts';
|
||||||
import { createFile, getFiles } from '/lib/data/files.ts';
|
import { createFile, getDirectories, getFiles } from '/lib/data/files.ts';
|
||||||
|
|
||||||
interface Data {}
|
interface Data {}
|
||||||
|
|
||||||
export interface ResponseBody {
|
export interface ResponseBody {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
newFiles: DirectoryFile[];
|
newFiles: DirectoryFile[];
|
||||||
|
newDirectories: Directory[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const handler: Handlers<Data, FreshContextState> = {
|
export const handler: Handlers<Data, FreshContextState> = {
|
||||||
@@ -18,13 +19,14 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
|
|
||||||
const requestBody = await request.clone().formData();
|
const requestBody = await request.clone().formData();
|
||||||
|
|
||||||
|
const pathInView = requestBody.get('path_in_view') as string;
|
||||||
const parentPath = requestBody.get('parent_path') as string;
|
const parentPath = requestBody.get('parent_path') as string;
|
||||||
const name = requestBody.get('name') as string;
|
const name = requestBody.get('name') as string;
|
||||||
const contents = requestBody.get('contents') as File | string;
|
const contents = requestBody.get('contents') as File | string;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!parentPath || !name.trim() || !contents || !parentPath.startsWith('/') ||
|
!parentPath || !pathInView || !name.trim() || !contents || !parentPath.startsWith('/') ||
|
||||||
parentPath.includes('../')
|
parentPath.includes('../') || !pathInView.startsWith('/') || pathInView.includes('../')
|
||||||
) {
|
) {
|
||||||
return new Response('Bad Request', { status: 400 });
|
return new Response('Bad Request', { status: 400 });
|
||||||
}
|
}
|
||||||
@@ -33,9 +35,10 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
|
|
||||||
const createdFile = await createFile(context.state.user.id, parentPath, name.trim(), fileContents);
|
const createdFile = await createFile(context.state.user.id, parentPath, name.trim(), fileContents);
|
||||||
|
|
||||||
const newFiles = await getFiles(context.state.user.id, parentPath);
|
const newFiles = await getFiles(context.state.user.id, pathInView);
|
||||||
|
const newDirectories = await getDirectories(context.state.user.id, pathInView);
|
||||||
|
|
||||||
const responseBody: ResponseBody = { success: createdFile, newFiles };
|
const responseBody: ResponseBody = { success: createdFile, newFiles, newDirectories };
|
||||||
|
|
||||||
return new Response(JSON.stringify(responseBody));
|
return new Response(JSON.stringify(responseBody));
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user