Added more endpoints and modals

- Added endpoints to create folders, upload files and unlink files and
  folders;
- Added modals to show loading indicators, confirmation and warnings.
This commit is contained in:
Rodrigo Pinto 2024-12-05 18:25:20 -03:00
commit 1885d43fa6
6 changed files with 324 additions and 81 deletions

View file

@ -0,0 +1,34 @@
<div class='overlay'></div>
<div class="wrap">
<svg viewBox="0 0 10 10" xmlns="http://www.w3.org/2000/svg">
<rect width="10" height="10">
<animate
attributeName="rx"
values="0;5;0"
dur="1s"
repeatCount="indefinite" />
</rect>
</svg>
</div>
<style>
.wrap {
position: absolute;
z-index: 51;
opacity: 0.6;
width: 3em;
left: calc(50% - 3em);
top: calc(50% - 2em);
}
.overlay {
position: fixed;
z-index: 50;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: var(--shade-loading);
}
</style>

View file

@ -0,0 +1,79 @@
import { env } from '$env/dynamic/private'
export const createAlias = () => {
return new Promise(async (resolve, reject) => {
try {
const url = `${env.TAHOE_API}/uri?t=mkdir`
const response = await fetch(url, { method: 'POST' })
const capKey = await response.json()
return resolve({ capKey })
} catch (error) {
return reject(error)
}
})
}
export const listDirectories = (capKey) => {
return new Promise(async (resolve, reject) => {
try {
const url = `${env.TAHOE_API}/uri/${capKey}?t=json`
const response = await fetch(url, { method: 'GET' })
const list = await response.json()
return resolve({ list })
} catch (error) {
return reject(error)
}
})
}
export const createDirectory = (path, dirName) => {
return new Promise(async (resolve, reject) => {
try {
const url = `${env.TAHOE_API}/uri/${path}?t=mkdir&name=${dirName}`
const response = await fetch(url, { method: 'POST' })
if (response.status !== 200) {
return json({ success: false, status: response.status, message: response.statusText })
}
const cap = await response.json()
return resolve({ cap })
} catch (error) {
return reject(error)
}
})
}
export const uploadFile = (path, file) => {
return new Promise(async (resolve, reject) => {
const url = `${env.TAHOE_API}/uri/${path}`
try {
const response = await fetch(url, {
method: 'PUT',
body: file
})
return resolve()
} catch (error) {
console.log({ error })
return reject(error)
}
})
}
export const unlink = (path) => {
return new Promise(async (resolve, reject) => {
try {
const url = `${env.TAHOE_API}/uri/${path}`
const response = await fetch(url, { method: 'DELETE' })
return resolve()
} catch (error) {
return reject(error)
}
})
}

View file

@ -16,6 +16,7 @@
--navbar-height: 3em; --navbar-height: 3em;
--shade-background: rgba(0, 0, 0, 0.2); --shade-background: rgba(0, 0, 0, 0.2);
--shade-color: #eee; --shade-color: #eee;
--shade-loading: rgba(0, 0, 0, 0.3);
--theme-background: white; --theme-background: white;
--theme-color: #444; --theme-color: #444;
} }

View file

@ -1,40 +0,0 @@
import { json } from '@sveltejs/kit'
import { env } from '$env/dynamic/private'
export const POST = async ({ params, request }) => {
const { slug } = params
const body = await request.json()
switch (slug) {
case 'createAlias': {
try {
const url = `${env.TAHOE_API}/uri?t=mkdir`
const response = await fetch(url, { method: 'POST' })
const capKey = await response.json()
return json({ success: true, capKey })
} catch (error) {
console.log({ error })
return json({ success: false, code: 500, error })
}
}
case 'listDirectories': {
try {
const encodedCapKey = body.capKey.replace(/:/g, '%3A')
const url = `${env.TAHOE_API}/uri/${encodedCapKey}?t=json`
const response = await fetch(url, { method: 'GET' })
const list = await response.json()
return json({ success: true, list })
} catch (error) {
console.log({ error })
return json({ success: false, code: 500, error, message: error.cause.message })
}
}
default: {
return json({ success: true })
}
}
}

View file

@ -1,15 +1,15 @@
import { fail } from '@sveltejs/kit' import { fail } from '@sveltejs/kit'
import { createAlias, createDirectory, listDirectories, unlink, uploadFile } from '$lib/utils/tahoe'
export const actions = { export const actions = {
createCapKey: async ({ request, fetch }) => { createCapKey: async ({ request, fetch }) => {
try { try {
const response = await fetch('/api/createAlias', { method: 'POST' }) const { capKey } = await createAlias()
const jsonResponse = await response.json()
return { endpoint: 'createCapKey', capKey: jsonResponse.capKey } return { endpoint: 'createCapKey', capKey }
} catch (err) { } catch (error) {
console.log({ err }) console.log({ error })
return fail(500, { endpoint: 'createCapKey', error: err }) return fail(500, { endpoint: 'createCapKey', error })
} }
}, },
@ -19,25 +19,84 @@ export const actions = {
const encoded = encodeURIComponent(capKey) const encoded = encodeURIComponent(capKey)
try { try {
const response = await fetch('/api/listDirectories', { const { list } = await listDirectories(capKey)
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ capKey: encoded })
})
const jsonResponse = await response.json()
if (!jsonResponse.success) { return { endpoint: 'listDirectories', list, capKey }
if (jsonResponse.message.includes('ECONNREFUSED')) { } catch (error) {
return { error: 'Tahoe server may be offline.' } if (error.cause.message.includes('ECONNREFUSED')) {
} else { return { error: 'Tahoe server may be offline.' }
throw new Error(jsonResponse.error)
}
} }
return { endpoint: 'listDirectories', list: jsonResponse.list, capKey } console.log({ error })
} catch (err) { return fail(500, { endpoint: 'listDirectories', error })
console.log({ err }) }
return fail(500, { endpoint: 'listDirectories', error: err }) },
createDirectory: async ({ request, fetch }) => {
const formData = await request.formData()
const capKey = formData.get('capKey')
const encoded = encodeURIComponent(capKey)
const dirName= formData.get('dirName')
let path = formData.get('path')
if (path.length === 0) path = '/'
if (path.slice(0, 1) !== '/') path = '/' + path
if (path.slice(-1) !== '/') path = path + '/'
path = encoded + encodeURI(path)
try {
const { cap } = await createDirectory(path, dirName)
return { success: true, endpoint: 'uploadFile' }
} catch (error) {
console.log({ error })
return fail(500, { endpoint: 'uploadFile', error})
}
},
uploadFile: async ({ request, fetch }) => {
const formData = await request.formData()
const capKey = formData.get('capKey')
const encoded = encodeURIComponent(capKey)
const file = formData.get('file')
let path = formData.get('path')
if (path.length === 0) path = '/'
if (path.slice(0, 1) !== '/') path = '/' + path
if (path.slice(-1) !== '/') path = path + '/'
path = encoded + encodeURI(path + file.name)
try {
await uploadFile(path, file.name, file)
return { success: true, endpoint: 'uploadFile' }
} catch (error) {
console.log({ error })
return fail(500, { endpoint: 'uploadFile', error})
}
},
deletePath: async ({ request, fetch }) => {
const formData = await request.formData()
const capKey = formData.get('capKey')
const encoded = encodeURIComponent(capKey)
const partialPath = formData.get('path')
const slash = partialPath.slice(0, 1) === '/' ? '' : '/'
const path = encoded + slash + partialPath
if (partialPath.length === 0) {
return { success: false, endpoint: 'deletePath', error: 'Path cannot be empty.'}
}
try {
await unlink(path)
return { success: true, endpoint: 'deletePath' }
} catch (error) {
console.log({ error })
return json({ success: false, code: 500, error })
} }
} }
} }

View file

@ -1,16 +1,25 @@
<script> <script>
import { enhance } from '$app/forms' import { enhance } from '$app/forms'
import { beforeNavigate } from '$app/navigation' import { beforeNavigate } from '$app/navigation'
import Loading from '$lib/components/loading.svelte'
import Modal from '$lib/components/modal.svelte' import Modal from '$lib/components/modal.svelte'
import { fade } from 'svelte/transition'
let { form } = $props() let { form } = $props()
let capKey = $state() let capKey = $state()
let capKeyInput = $state() let capKeyInput = $state()
let newCapKey = $state() let list = $state()
let listForm = $state()
let modalTitle = $state() let modalTitle = $state()
let modalText = $state() let modalText = $state()
let newCapKey = $state()
let checked = $state(false) let checked = $state(false)
let loading = $state(false)
let showModal = $state(false) let showModal = $state(false)
let showDelModal = $state(false)
let showUploadModal = $state(false)
let showAddFolderModal = $state(false)
const submitKey = () => { const submitKey = () => {
capKey = capKeyInput capKey = capKeyInput
@ -22,16 +31,33 @@
} }
const enhanceForm = () => { const enhanceForm = () => {
showDelModal = false
showUploadModal = false
showAddFolderModal = false
loading = true
return async ({ result, update }) => { return async ({ result, update }) => {
await update() await update()
loading = false
if (result.status === 200) { if (result.status === 200) {
switch (result.data.endpoint) { switch (result.data.endpoint) {
case 'createCapKey': { case 'createCapKey': {
newCapKey = result.data.capKey newCapKey = result.data.capKey
break
} }
case 'listDirectories': { case 'listDirectories': {
capKey = result.data.capKey capKey = result.data.capKey
list = result.data.list
break
}
case 'uploadFile': {
listForm.requestSubmit()
break
}
case 'deletePath': {
listForm.requestsubmit()
break
} }
} }
} }
@ -47,12 +73,13 @@
} }
}) })
// Store cap key in a variable for use while current page
// is on screen
$effect(() => { $effect(() => {
if (form?.capKey) capKey = form.capKey if (form?.capKey) capKey = form.capKey
}) })
$effect(() => console.log({ form })) // Show errors
$effect(() => { $effect(() => {
if (form?.error) { if (form?.error) {
modalTitle = 'Error' modalTitle = 'Error'
@ -66,13 +93,20 @@
<title>Private Facts dashboard</title> <title>Private Facts dashboard</title>
<meta name='description' content='Private Facts dashboard.'> <meta name='description' content='Private Facts dashboard.'>
</svelte:head> </svelte:head>
{#if loading}
<div class='loader' transition:fade|local>
<Loading />
</div>
{/if}
<h1>Dashboard</h1> <h1>Dashboard</h1>
{#if !capKey && !newCapKey} {#if !capKey && !newCapKey}
<div class='cap-key-div add'> <div class='cap-key-div add'>
<form action='?/listDirectories' method='post' enctype='form-data' use:enhance={enhanceForm}> <form action='?/listDirectories' method='post' enctype='form-data' use:enhance={enhanceForm} bind:this={listForm}>
<label for='capKeyInput'>Cap key:</label> <label for='capKeyInput'>Cap key:</label>
<input type='text' name='capKeyInput' /> <input type='text' name='capKeyInput' value={capKey} />
<button type='submit'>Submit</button> <button type='submit'>Submit</button>
</form> </form>
@ -106,35 +140,96 @@
{:else} {:else}
<h2>File tree</h2> <h2>File tree</h2>
<ul> {#if list}
{#each form?.list as item} <ul>
{#if item === 'dirnode'} {#each Object.keys(list[1].children) as item}
<li>/</li> <li>{item}</li>
{:else} {/each}
<li>{Object.keys(item.children)[0]}</li> </ul>
{/if} {/if}
{/each}
</ul>
<button>Add folder</button> <button onclick={() => showAddFolderModal = true}>Add folder</button>
<button>Upload file</button> <button onclick={() => showUploadModal = true}>Upload file</button>
<button onclick={() => showDelModal = true}>Unlink file / folder</button>
<button onclick={() => capKey = undefined}>Change cap key</button> <button onclick={() => capKey = undefined}>Change cap key</button>
{/if} {/if}
<!-- Modals -->
<Modal {showModal} escape={() => showModal = false}> <Modal {showModal} escape={() => showModal = false}>
<h2>{modalTitle}</h2> <h2>{modalTitle}</h2>
<p>{modalText}</p> <p>{modalText}</p>
<button onclick={() => showModal = false}>Ok</button> <button onclick={() => showModal = false}>Ok</button>
</Modal> </Modal>
<style> <Modal showModal={showDelModal} escape={() => showDelModal = false}>
form { <h2>Unlink</h2>
<p>Specify path of file or folder to unlink.</p>
<p>Ex.: /Documents/File-1.txt</p>
<form action='?/deletePath' method='post' enctype='form-data' use:enhance={enhanceForm}>
<input type='hidden' name='capKey' value={capKey} />
<label for='path'>Path:</label>
<input type='text' name='path' class='path-input' />
<div class='form-actions'>
<button onclick={() => showDelModal = false}>Cancel</button>
<button type='submit'>Confirm</button>
</div>
</form>
</Modal>
<Modal showModal={showUploadModal} escape={() => showUploadModal = false}>
<h2>Upload file</h2>
<form action='?/uploadFile' method='post' enctype='multipart/form-data' use:enhance={enhanceForm}>
<input type='hidden' name='capKey' value={capKey} />
<div class='file-input'>
<label for='file'>File:</label>
<input type='file' name='file' placeholder='file' required />
</div>
<label for='path'>Path:</label>
<input type='text' name='path' class='path-input' />
<div class='form-actions'>
<button onclick={() => showUploadModal = false}>Cancel</button>
<button type='submit'>Upload</button>
</div>
</form>
</Modal>
<Modal showModal={showAddFolderModal} escape={() => showAddFolderModal = false}>
<h2>Add folder</h2>
<form action='?/createDirectory' method='post' enctype='form-data' use:enhance={enhanceForm}>
<input type='hidden' name='capKey' value={capKey} />
<div class='new-folder-input'>
<label for='dirName'>New folder name:</label>
<input type='text' name='dirName' />
<label for='path'>Path:</label>
<input type='text' name='path' />
</div>
<div class='form-actions'>
<button onclick={() => showAddFolderModal = false}>Cancel</button>
<button type='submit'>Confirm</button>
</div>
</form>
</Modal>
<style>
.cap-key-div form {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
text-align: left; text-align: left;
} }
form button { .cap-key-div form button {
margin: 0 0 1em auto; margin: 0 0 1em auto;
} }
@ -180,4 +275,19 @@
padding: 1em; padding: 1em;
border: solid thin var(--border-color); border: solid thin var(--border-color);
} }
.form-actions {
display: flex;
justify-content: center;
}
.path-input {
width: 30em;
}
.new-folder-input {
text-align: left;
display: flex;
flex-direction: column;
}
</style> </style>