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:
parent
5072a69333
commit
1885d43fa6
6 changed files with 324 additions and 81 deletions
34
packages/src/lib/components/loading.svelte
Normal file
34
packages/src/lib/components/loading.svelte
Normal 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>
|
79
packages/src/lib/utils/tahoe.js
Normal file
79
packages/src/lib/utils/tahoe.js
Normal 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
<Modal showModal={showDelModal} escape={() => showDelModal = false}>
|
||||||
|
<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>
|
<style>
|
||||||
form {
|
.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>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue