Merge pull request #58 from bewcloud/feature/upload-directories-web
Upload Directories via Web
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