Implemented write access for share links via password
All checks were successful
/ Build the server (push) Successful in 2m31s
All checks were successful
/ Build the server (push) Successful in 2m31s
This commit is contained in:
11
frontend/icons/ShareEdit.svg
Normal file
11
frontend/icons/ShareEdit.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M 19.11,21.89 11.8,17.32 c 0.266416,-0.859836 0.266416,-1.780164 0,-2.64 l 7.31,-4.57 c 3.540201,4.383664 10.487936,0.476053 8.59716,-4.8215267 C 25.816385,-0.009106 17.964565,1.3654391 18,7 c 0.0047,0.4471789 0.07204,0.8914935 0.2,1.32 l -7.31,4.57 C 7.944435,9.1819161 1.9753536,11.265473 1.9753536,16 c 0,4.734527 5.9690814,6.818084 8.9146464,3.11 l 7.31,4.57 z M 23,4 c 2.6729,0 4.01101,3.2314157 2.121213,5.1212126 C 23.231416,11.01101 20,9.6729004 20,7 20,5.3431458 21.343146,4 23,4 Z M 7,19 C 4.3270996,19 2.9889905,15.768584 4.8787874,13.878787 6.7685843,11.98899 10,13.3271 10,16 c 0,1.656854 -1.3431458,3 -3,3 z"
|
||||
fill="currentColor" />
|
||||
<path
|
||||
transform="scale(0.75) translate(16 16)"
|
||||
d="M25.4 9c.8-.8.8-2 0-2.8l-3.6-3.6c-.8-.8-2-.8-2.8 0l-15 15V24h6.4l15-15zm-5-5L24 7.6l-3 3L17.4 7l3-3zM6 22v-3.6l10-10l3.6 3.6l-10 10H6z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 967 B |
@@ -3,18 +3,22 @@ import createClient from 'openapi-fetch';
|
||||
import {fetchEventSource} from '@microsoft/fetch-event-source';
|
||||
import {token} from '../store';
|
||||
import { replace } from 'svelte-spa-router';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export type Session = components['schemas']['de.mattv.fileserver.Response$Session'];
|
||||
export type Node = components['schemas']['de.mattv.fileserver.Response$Node'];
|
||||
export type PathSegment = components['schemas']['de.mattv.fileserver.Response$PathSegment'];
|
||||
export type CreateNodeInfo = components['schemas']['de.mattv.fileserver.Response$CreateNodeInfo'];
|
||||
export type UserInfo = components['schemas']['de.mattv.fileserver.Response$UserInfo'];
|
||||
export type AllPaths = keyof paths
|
||||
|
||||
export enum RpcClientType { AUTH, SHARE_RO, SHARE_RW }
|
||||
|
||||
const _getRpcClient = () => {
|
||||
let obj = {
|
||||
token: '',
|
||||
client: createClient<paths>(),
|
||||
share_root: null,
|
||||
ty: writable(RpcClientType.AUTH),
|
||||
|
||||
signup: (username: string, password: string) =>
|
||||
obj.client.POST('/api/public/auth/signup', { body: { username, password } }).then(v => v.data),
|
||||
@@ -48,7 +52,7 @@ const _getRpcClient = () => {
|
||||
downloadPreview: (node: number) => obj.client.POST('/api/user_share/fs/preview', { body: node }).then(v => v.data),
|
||||
|
||||
createNode: (name: string, parent: number, file: boolean) =>
|
||||
obj.client.POST('/api/user/fs/create', { body: { name, parent, file } }).then(v => v.data),
|
||||
obj.client.POST('/api/user_share/fs/create', { body: { name, parent, file } }).then(v => v.data),
|
||||
|
||||
deleteNodes: (nodes: number[], cbk: (v: string|null) => void) => fetchEventSource('/api/user/fs/delete', {
|
||||
method: 'POST',
|
||||
@@ -64,6 +68,8 @@ const _getRpcClient = () => {
|
||||
|
||||
shareNode: (node: number) => obj.client.POST('/api/user/fs/share', { body: node }).then(v => v.data),
|
||||
unshareNode: (node: number) => obj.client.POST('/api/user/fs/unshare', { body: node }).then(v => v.data),
|
||||
shareSetPw: (node: number, pw?: string) => obj.client.POST('/api/user/fs/share_pw', { body: { id: node, password: pw } }).then(v => v.data),
|
||||
shareUnlock: (pw: string) => obj.client.POST('/api/user_share/share_unlock', { body: pw }).then(v => v.data),
|
||||
|
||||
admin: {
|
||||
listUsers: () => obj.client.POST('/api/admin/users').then(v => v.data),
|
||||
@@ -91,7 +97,8 @@ const _getRpcClient = () => {
|
||||
return obj;
|
||||
}
|
||||
|
||||
export type RpcClient = Omit<ReturnType<typeof _getRpcClient>, 'share_root'> & { share_root: null | number };
|
||||
//export type RpcClient = Omit<ReturnType<typeof _getRpcClient>, 'share_root'> & { share_root: null | number };
|
||||
export type RpcClient = ReturnType<typeof _getRpcClient>;
|
||||
export const getRpcClient: () => RpcClient = () => _getRpcClient();
|
||||
export const globalRpc = getRpcClient();
|
||||
|
||||
|
||||
220
frontend/src/api/schema.d.ts
vendored
220
frontend/src/api/schema.d.ts
vendored
@@ -4,7 +4,23 @@
|
||||
*/
|
||||
|
||||
export interface paths {
|
||||
"/api/user_share/session": {
|
||||
"/api/user_share/upload/{id}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
post: operations["upload"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/user_share/share_unlock": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
@@ -20,6 +36,22 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/user_share/session": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
post: operations["session_1"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/user_share/fs/size": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -100,6 +132,22 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/user_share/fs/create": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
post: operations["create"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/user/tfa/setup_totp": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -180,6 +228,22 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/user/fs/share_pw": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
post: operations["setPw"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/user/fs/share": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -212,22 +276,6 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/user/fs/create": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
post: operations["create"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/user/auth/logout_all": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -504,6 +552,10 @@ export interface paths {
|
||||
export type webhooks = Record<string, never>;
|
||||
export interface components {
|
||||
schemas: {
|
||||
"de.mattv.fileserver.ResponseJava.lang.String": {
|
||||
e?: string;
|
||||
o?: string;
|
||||
};
|
||||
"de.mattv.fileserver.Response$Session": {
|
||||
name: string;
|
||||
tfaEnabled: boolean;
|
||||
@@ -511,6 +563,8 @@ export interface components {
|
||||
sudo: boolean;
|
||||
/** Format: int64 */
|
||||
shareRoot?: number;
|
||||
shareCanRw: boolean;
|
||||
shareIsRw: boolean;
|
||||
};
|
||||
"de.mattv.fileserver.Response$PathSegment": {
|
||||
name: string;
|
||||
@@ -523,6 +577,7 @@ export interface components {
|
||||
name: string;
|
||||
file: boolean;
|
||||
preview: boolean;
|
||||
shareHasRw: boolean;
|
||||
shareName?: string;
|
||||
/** Format: int64 */
|
||||
size?: number;
|
||||
@@ -530,14 +585,6 @@ export interface components {
|
||||
parent?: number;
|
||||
children?: components["schemas"]["de.mattv.fileserver.Response$Node"][];
|
||||
};
|
||||
"de.mattv.fileserver.ResponseJava.lang.String": {
|
||||
e?: string;
|
||||
o?: string;
|
||||
};
|
||||
"org.springframework.web.servlet.mvc.method.annotation.SseEmitter": {
|
||||
/** Format: int64 */
|
||||
timeout?: number;
|
||||
};
|
||||
"de.mattv.fileserver.routes.fs.Create$Body": {
|
||||
name: string;
|
||||
/** Format: int64 */
|
||||
@@ -554,6 +601,15 @@ export interface components {
|
||||
e?: string;
|
||||
o?: components["schemas"]["de.mattv.fileserver.Response$CreateNodeInfo"];
|
||||
};
|
||||
"de.mattv.fileserver.routes.fs.Share$SetPwBody": {
|
||||
/** Format: int64 */
|
||||
id: number;
|
||||
password?: string;
|
||||
};
|
||||
"org.springframework.web.servlet.mvc.method.annotation.SseEmitter": {
|
||||
/** Format: int64 */
|
||||
timeout?: number;
|
||||
};
|
||||
"de.mattv.fileserver.routes.auth.ChangePW$Body": {
|
||||
oldPassword: string;
|
||||
newPassword: string;
|
||||
@@ -601,7 +657,51 @@ export interface components {
|
||||
}
|
||||
export type $defs = Record<string, never>;
|
||||
export interface operations {
|
||||
upload: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
id: number;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
session: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": string;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": components["schemas"]["de.mattv.fileserver.ResponseJava.lang.String"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
session_1: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
@@ -741,6 +841,30 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
create: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["de.mattv.fileserver.routes.fs.Create$Body"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": components["schemas"]["de.mattv.fileserver.ResponseDe.mattv.fileserver.Response$CreateNodeInfo"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
setupTfaTotp: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -845,6 +969,28 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
setPw: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["de.mattv.fileserver.routes.fs.Share$SetPwBody"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
share: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -893,30 +1039,6 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
create: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["de.mattv.fileserver.routes.fs.Create$Body"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"*/*": components["schemas"]["de.mattv.fileserver.ResponseDe.mattv.fileserver.Response$CreateNodeInfo"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
logoutAll: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
||||
@@ -22,21 +22,23 @@
|
||||
Tooltip
|
||||
} from 'flowbite-svelte';
|
||||
import {filesize} from 'filesize';
|
||||
import {Folder, FolderParent, DocumentBlank, CaretLeft, FolderAdd, Share} from '../icons';
|
||||
import {Folder, FolderParent, DocumentBlank, CaretLeft, FolderAdd, Share, ShareEdit, Password} from '../icons';
|
||||
import {api, download, workingWrapperR, error_banner, type RpcClient, info_banner} from '../store';
|
||||
import LinkButton from './LinkButton.svelte';
|
||||
import DeleteModal from './DeleteModal.svelte';
|
||||
import A from './A.svelte';
|
||||
import {createEventDispatcher} from 'svelte';
|
||||
import { RpcClientType } from '../api';
|
||||
import InputModal from './InputModal.svelte';
|
||||
|
||||
export let node: api.Node;
|
||||
export let rpc: RpcClient
|
||||
export let rpc: RpcClient;
|
||||
export let path_prefix: string;
|
||||
|
||||
const not_share = rpc.share_root == null;
|
||||
|
||||
const dispatch = createEventDispatcher<{reload_node: null}>();
|
||||
|
||||
const rpc_ty = rpc.ty;
|
||||
|
||||
let selected: number[] = [];
|
||||
let nodes: api.Node[], dirs: api.Node[], files: api.Node[], previews: {[key: number]: string|null} = {};
|
||||
let total_size: number;
|
||||
@@ -53,29 +55,33 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let show_new_folder = false, new_folder_name = '';
|
||||
const new_folder_keyup = (e: KeyboardEvent) => { if(e.key == 'Enter') newFolder(); }
|
||||
async function newFolder() {
|
||||
if (new_folder_name.length === 0)
|
||||
//#region New Folder Dialog
|
||||
let show_new_folder = false;
|
||||
async function newFolder(e: CustomEvent<string>) {
|
||||
const name = e.detail;
|
||||
if (name.length === 0)
|
||||
return error_banner.set('Folder name can\'t be empty');
|
||||
|
||||
show_new_folder = false;
|
||||
const resp = await workingWrapperR<api.CreateNodeInfo>(() => rpc.createNode(new_folder_name, node.id, false));
|
||||
const resp = await workingWrapperR<api.CreateNodeInfo>(() => rpc.createNode(name, node.id, false));
|
||||
if (resp && resp.isFile)
|
||||
return error_banner.set('Folder already exists as file');
|
||||
|
||||
dispatch('reload_node');
|
||||
}
|
||||
//#endregion
|
||||
|
||||
async function getPreview(node: number) {
|
||||
const resp = await rpc.downloadPreview(node);
|
||||
if (!resp)
|
||||
return;
|
||||
previews[node] = 'data:image/png;base64,' + resp;
|
||||
previews = previews;
|
||||
//#region Input password Dialog
|
||||
let show_pw_dialog = false;
|
||||
async function pwInputDone(e: CustomEvent<string>) {
|
||||
const pw = e.detail;
|
||||
await rpc.shareSetPw(ctx_node.id, pw);
|
||||
show_pw_dialog = false;
|
||||
dispatch('reload_node');
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region Context Menu
|
||||
let ctx_node: api.Node;
|
||||
let ctx_hidden = true;
|
||||
let ctx_x = 0, ctx_y = 0;
|
||||
@@ -92,14 +98,6 @@
|
||||
ctx_hidden = false;
|
||||
}
|
||||
|
||||
const selectAll = () => selected = nodes.map(v => v.id);
|
||||
const selectFolders = () => selected = dirs.map(v => v.id);
|
||||
const selectFiles = () => selected = files.map(v => v.id);
|
||||
const selectNone = () => selected = [];
|
||||
const downloadSelected = () => download(rpc, nodes.filter(v => selected.includes(v.id)));
|
||||
|
||||
let del: (nodes: number[]) => Promise<void>; // bound to DeleteModal
|
||||
const deleteSelected = () => del(selected);
|
||||
const onCtxDelete = () => del([ctx_node.id]);
|
||||
|
||||
const onCtxDownload = () => download(rpc, [ctx_node]);
|
||||
@@ -120,8 +118,31 @@
|
||||
await rpc.unshareNode(ctx_node.id);
|
||||
dispatch('reload_node');
|
||||
}
|
||||
const onCtxSharePw = () => show_pw_dialog = true;
|
||||
const onCtxShareRmPw = async () => {
|
||||
await rpc.shareSetPw(ctx_node.id, undefined);
|
||||
dispatch('reload_node');
|
||||
}
|
||||
//#endregion
|
||||
|
||||
async function getPreview(node: number) {
|
||||
const resp = await rpc.downloadPreview(node);
|
||||
if (!resp)
|
||||
return;
|
||||
previews[node] = 'data:image/png;base64,' + resp;
|
||||
previews = previews;
|
||||
}
|
||||
|
||||
const onShowPreview = (e: Event) => { show_preview.set((e.target as HTMLInputElement).checked); }
|
||||
|
||||
const selectAll = () => selected = nodes.map(v => v.id);
|
||||
const selectFolders = () => selected = dirs.map(v => v.id);
|
||||
const selectFiles = () => selected = files.map(v => v.id);
|
||||
const selectNone = () => selected = [];
|
||||
const downloadSelected = () => download(rpc, nodes.filter(v => selected.includes(v.id)));
|
||||
|
||||
let del: (nodes: number[]) => Promise<void>; // bound to DeleteModal
|
||||
const deleteSelected = () => del(selected);
|
||||
</script>
|
||||
|
||||
<svelte:body on:click={() => (ctx_hidden = true)} />
|
||||
@@ -151,7 +172,9 @@
|
||||
<TableBodyCell class="p-2 pl-4 w-0 h-0"><Checkbox bind:group={selected} value={node.id}/></TableBodyCell>
|
||||
<TableBodyCell class="px-2 w-0"><Folder /></TableBodyCell>
|
||||
<TableBodyCell class="pl-0 flex flex-row gap-2">
|
||||
{#if node.shareName && not_share}<Share/>{/if}
|
||||
{#if node.shareName && $rpc_ty === RpcClientType.AUTH}
|
||||
{#if node.shareHasRw}<ShareEdit />{:else}<Share/>{/if}
|
||||
{/if}
|
||||
<A href={path_prefix + node.id}>{node.name}</A>
|
||||
</TableBodyCell>
|
||||
<TableBodyCell></TableBodyCell>
|
||||
@@ -172,7 +195,9 @@
|
||||
{/if}
|
||||
</TableBodyCell>
|
||||
<TableBodyCell class="pl-0 flex flex-row gap-2">
|
||||
{#if node.shareName && not_share}<Share/>{/if}
|
||||
{#if node.shareName && $rpc_ty === RpcClientType.AUTH}
|
||||
{#if node.shareHasRw}<ShareEdit />{:else}<Share/>{/if}
|
||||
{/if}
|
||||
<A href={path_prefix + node.id}>{node.name}</A>
|
||||
</TableBodyCell>
|
||||
<TableBodyCell>{filesize(node.size ?? 0, {base: 2, standard: 'jedec'})}</TableBodyCell>
|
||||
@@ -182,10 +207,14 @@
|
||||
<tfoot class="text-gray-700 bg-gray-50">
|
||||
<tr>
|
||||
<td class="px-6 py-3" colspan="3">
|
||||
{#if not_share}<LinkButton on:click={() => (show_new_folder = true)} class="mr-3">New folder</LinkButton>{/if}
|
||||
{#if $rpc_ty !== RpcClientType.SHARE_RO}
|
||||
<LinkButton on:click={() => (show_new_folder = true)} class="mr-3">New folder</LinkButton>
|
||||
{/if}
|
||||
{#if selected.length > 0}
|
||||
<LinkButton on:click={downloadSelected}>Download</LinkButton>
|
||||
{#if not_share}<LinkButton color="red" on:click={deleteSelected}>Delete</LinkButton>{/if}
|
||||
{#if $rpc_ty === RpcClientType.AUTH
|
||||
}<LinkButton color="red" on:click={deleteSelected}>Delete</LinkButton>
|
||||
{/if}
|
||||
{/if}
|
||||
</td>
|
||||
<td class="px-6 py-3">{filesize(total_size, {base: 2, standard: 'jedec'})}</td>
|
||||
@@ -193,16 +222,24 @@
|
||||
</tfoot>
|
||||
</Table>
|
||||
|
||||
<Modal bind:open={show_new_folder} outsideclose title="Create new folder">
|
||||
<ButtonGroup class="w-full mb-4">
|
||||
<InputAddon><FolderAdd /></InputAddon>
|
||||
<Input type="text" placeholder="Name" bind:value={new_folder_name} on:keyup={new_folder_keyup}></Input>
|
||||
</ButtonGroup>
|
||||
<span class="w-full flex">
|
||||
<span class="flex-1 mr-2"></span>
|
||||
<Button outline on:click={newFolder}>Create folder</Button>
|
||||
</span>
|
||||
</Modal>
|
||||
<InputModal
|
||||
on:done={newFolder}
|
||||
bind:show={show_new_folder}
|
||||
type="text"
|
||||
title="Create new folder"
|
||||
placeholder="Name"
|
||||
done_text="Create folder"
|
||||
icon={FolderAdd}
|
||||
/>
|
||||
<InputModal
|
||||
on:done={pwInputDone}
|
||||
bind:show={show_pw_dialog}
|
||||
type="password"
|
||||
title={`Set password for ${ctx_node?.name}`}
|
||||
placeholder="Password"
|
||||
done_text="Set password"
|
||||
icon={Password}
|
||||
/>
|
||||
|
||||
<Dropdown triggeredBy="#dropdown-button" trigger="hover" placement="left">
|
||||
<DropdownItem on:click={selectAll}>Select all</DropdownItem>
|
||||
@@ -214,8 +251,14 @@
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div style={ctx_style} hidden={ctx_hidden} class="z-50 shadow-md rounded-lg border-gray-100 bg-white" on:contextmenu={() => (ctx_hidden = true)}>
|
||||
<ul class="py-1">
|
||||
{#if not_share}
|
||||
{#if $rpc_ty === RpcClientType.AUTH}
|
||||
{#if ctx_node?.shareName}
|
||||
{#if !ctx_node?.file}
|
||||
<li><button class="font-medium py-2 px-4 text-sm hover:bg-gray-100 w-full text-left" on:click={onCtxSharePw}>Set share write password</button></li>
|
||||
{/if}
|
||||
{#if ctx_node?.shareHasRw}
|
||||
<li><button class="font-medium py-2 px-4 text-sm hover:bg-gray-100 w-full text-left" on:click={onCtxShareRmPw}>Remove share write password</button></li>
|
||||
{/if}
|
||||
<li><button class="font-medium py-2 px-4 text-sm hover:bg-gray-100 w-full text-left" on:click={onCtxCopyShare}>Copy share link</button></li>
|
||||
<li><button class="font-medium py-2 px-4 text-sm hover:bg-gray-100 w-full text-left" on:click={onCtxUnshare}>Unshare</button></li>
|
||||
{:else}
|
||||
@@ -223,7 +266,7 @@
|
||||
{/if}
|
||||
{/if}
|
||||
<li><button class="font-medium py-2 px-4 text-sm hover:bg-gray-100 w-full text-left" on:click={onCtxDownload}>Download</button></li>
|
||||
{#if not_share}
|
||||
{#if $rpc_ty === RpcClientType.AUTH}
|
||||
<li><button class="font-medium py-2 px-4 text-sm hover:bg-gray-100 text-red-400 w-full text-left" on:click={onCtxDelete}>Delete</button></li>
|
||||
{/if}
|
||||
</ul>
|
||||
|
||||
30
frontend/src/components/InputModal.svelte
Normal file
30
frontend/src/components/InputModal.svelte
Normal file
@@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
import {Button, ButtonGroup, Input, InputAddon, Modal, type InputType} from 'flowbite-svelte';
|
||||
import {createEventDispatcher} from 'svelte';
|
||||
import type { default as Icon } from '~icons/*';
|
||||
|
||||
const dispatch = createEventDispatcher<{done: string}>();
|
||||
|
||||
export let type: InputType;
|
||||
export let title: string;
|
||||
export let placeholder: string;
|
||||
export let done_text: string;
|
||||
export let icon: typeof Icon;
|
||||
export let show = false;
|
||||
|
||||
let value = '';
|
||||
|
||||
const on_keyup = (e: KeyboardEvent) => { if(e.key == 'Enter') done(); }
|
||||
const done = () => dispatch('done', value);
|
||||
</script>
|
||||
|
||||
<Modal bind:open={show} outsideclose title={title} on:close={() => value = ''}>
|
||||
<ButtonGroup class="w-full mb-4">
|
||||
<InputAddon><svelte:component this={icon}/></InputAddon>
|
||||
<Input type={type} placeholder={placeholder} bind:value={value} on:keyup={on_keyup}></Input>
|
||||
</ButtonGroup>
|
||||
<span class="w-full flex">
|
||||
<span class="flex-1 mr-2"></span>
|
||||
<Button outline on:click={done}>{done_text}</Button>
|
||||
</span>
|
||||
</Modal>
|
||||
@@ -1,9 +1,11 @@
|
||||
<script lang="ts">
|
||||
import {token, type UploadFile} from '../store';
|
||||
import {api, type RpcClient, type UploadFile} from '../store';
|
||||
import {Button, Modal, Progressbar} from 'flowbite-svelte';
|
||||
import {filesize} from 'filesize';
|
||||
import {createEventDispatcher} from 'svelte';
|
||||
import type { AllPaths } from '../api';
|
||||
|
||||
export let rpc: RpcClient;
|
||||
|
||||
const dispatch = createEventDispatcher<{reload_node: null}>();
|
||||
|
||||
@@ -39,7 +41,10 @@
|
||||
await new Promise((resolve) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.onloadend = resolve;
|
||||
xhr.onerror = resolve;
|
||||
xhr.onerror = e => {
|
||||
console.error(e);
|
||||
resolve(null);
|
||||
};
|
||||
xhr.upload.onprogress = ev => {
|
||||
current += ev.loaded - load_progress;
|
||||
load_progress = ev.loaded;
|
||||
@@ -49,8 +54,9 @@
|
||||
if (file.current == file.total)
|
||||
resolve(null);
|
||||
};
|
||||
xhr.open('POST', `/api/user/upload/${file.id}`, true);
|
||||
xhr.setRequestHeader('Authorization', 'Bearer ' + ($token ?? ''));
|
||||
const url: AllPaths = '/api/user_share/upload/{id}';
|
||||
xhr.open('POST', url.replace('{id}', file.id.toString()), true);
|
||||
xhr.setRequestHeader('Authorization', 'Bearer ' + rpc.token);
|
||||
xhr.send(file.file);
|
||||
});
|
||||
current += file.total - load_progress;
|
||||
@@ -145,4 +151,4 @@
|
||||
<Button class="w-full" color="red" on:click={() => (aborting = true)}>Abort</Button>
|
||||
{/if}
|
||||
</span>
|
||||
</Modal>
|
||||
</Modal>
|
||||
|
||||
@@ -11,6 +11,7 @@ export {default as CloudUpload} from '~icons/carbon/CloudUpload';
|
||||
export {default as Checkmark} from '~icons/carbon/Checkmark';
|
||||
export {default as Error} from '~icons/carbon/Error';
|
||||
export {default as Share} from '~icons/carbon/Share';
|
||||
export {default as ShareEdit} from '~icons/custom/ShareEdit';
|
||||
|
||||
export {default as CaretLeft} from '~icons/ph/CaretLeft';
|
||||
export {default as OTP} from '~icons/ph/Password';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { replace } from 'svelte-spa-router';
|
||||
import { getRpcClient } from '../../api';
|
||||
import { getRpcClient } from '../api';
|
||||
|
||||
export let params: {sid?: string} | undefined = {};
|
||||
|
||||
@@ -1,19 +1,35 @@
|
||||
<script lang="ts">
|
||||
import {Breadcrumb, Dropzone, Modal, Progressbar} from 'flowbite-svelte';
|
||||
import {writable} from 'svelte/store';
|
||||
import {CloudUpload} from '../icons';
|
||||
import {api, globalRpc, token, type UploadFile, workingWrapper, workingWrapperR} from '../store';
|
||||
import {Breadcrumb, Button, Dropzone, Modal, Progressbar} from 'flowbite-svelte';
|
||||
import {get, writable} from 'svelte/store';
|
||||
import {CloudUpload, Password} from '../icons';
|
||||
import {api, globalRpc, type UploadFile, workingWrapper, workingWrapperR} from '../store';
|
||||
import DirViewer from '../components/DirViewer.svelte';
|
||||
import UploadModal from '../components/UploadModal.svelte';
|
||||
import FileViewer from '../components/FileViewer.svelte';
|
||||
import A from '../components/A.svelte';
|
||||
import { getRpcClient, RpcClientType } from '../api';
|
||||
import { replace } from 'svelte-spa-router';
|
||||
import InputModal from '../components/InputModal.svelte';
|
||||
|
||||
interface Data {
|
||||
node: api.Node|null,
|
||||
segments: api.PathSegment[]
|
||||
segments: api.PathSegment[],
|
||||
root?: number,
|
||||
can_unlock?: boolean
|
||||
}
|
||||
|
||||
export let params: {id?: string}|undefined = {};
|
||||
export let params: {id?: string, sid?: string} | undefined = {};
|
||||
|
||||
let rpc = globalRpc;
|
||||
let base_link = '#/view/';
|
||||
if (params?.sid !== undefined) {
|
||||
rpc = getRpcClient();
|
||||
rpc.ty.set(RpcClientType.SHARE_RO);
|
||||
rpc.token = params.sid;
|
||||
base_link = `#/share/${params.sid}/`;
|
||||
}
|
||||
const rpc_ty = rpc.ty;
|
||||
|
||||
$: {
|
||||
let id = 0;
|
||||
if (params && params.id) {
|
||||
@@ -25,15 +41,25 @@
|
||||
|
||||
const data = writable<Data>({node: null, segments: []});
|
||||
async function updateData(id: number) {
|
||||
let node = await workingWrapper(() => globalRpc.getNode(id));
|
||||
let can_unlock = false;
|
||||
let root = undefined;
|
||||
if (get(rpc.ty) != RpcClientType.AUTH) {
|
||||
let info = await workingWrapper(() => rpc.sessionInfo());
|
||||
if (!info)
|
||||
return replace('/');
|
||||
root = info.shareRoot;
|
||||
can_unlock = info.shareCanRw;
|
||||
}
|
||||
let node = await workingWrapper(() => rpc.getNode(id));
|
||||
if (!node)
|
||||
return;
|
||||
let segments = await workingWrapper(() => globalRpc.getPath(id));
|
||||
let segments = await workingWrapper(() => rpc.getPath(id));
|
||||
if (!segments)
|
||||
return;
|
||||
data.set({node: node as Data['node'], segments });
|
||||
data.set({node: node as Data['node'], segments, root, can_unlock });
|
||||
}
|
||||
|
||||
//#region Upload
|
||||
const getFile = async (entry: FileSystemEntry) => new Promise<File>((o, e) => (entry as FileSystemFileEntry).file(o, e));
|
||||
|
||||
async function handleEntry(parent: number, parent_name: string, entry: FileSystemEntry): Promise<UploadFile[]> {
|
||||
@@ -50,7 +76,7 @@
|
||||
return [];
|
||||
}
|
||||
}
|
||||
const resp = await workingWrapperR<api.CreateNodeInfo>(() => globalRpc.createNode(entry.name, parent, false));
|
||||
const resp = await workingWrapperR<api.CreateNodeInfo>(() => rpc.createNode(entry.name, parent, false));
|
||||
if (!resp) return [];
|
||||
if (resp.isFile) return [];
|
||||
const reader = (entry as FileSystemDirectoryEntry).createReader();
|
||||
@@ -109,13 +135,28 @@
|
||||
upload_progress_data.current = 0;
|
||||
const upload_files: UploadFile[] = [];
|
||||
for (const file of files) {
|
||||
const resp = await workingWrapperR<api.CreateNodeInfo>(() => globalRpc.createNode(file.name, file.id, true));
|
||||
const resp = await workingWrapperR<api.CreateNodeInfo>(() => rpc.createNode(file.name, file.id, true));
|
||||
if (resp && resp.isFile)
|
||||
upload_files.push({ ...file, id: resp.id, overwrite: resp.exists });
|
||||
upload_progress_data.current++;
|
||||
}
|
||||
await real_upload(upload_files);
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region Unlock dialog
|
||||
let show_unlock_dialog = false;
|
||||
async function onUnlock(e: CustomEvent<string>) {
|
||||
const pw = e.detail;
|
||||
const resp = await workingWrapperR(() => rpc.shareUnlock(pw));
|
||||
if (!resp) return;
|
||||
rpc.token = resp;
|
||||
rpc.ty.set(RpcClientType.SHARE_RW);
|
||||
show_unlock_dialog = false;
|
||||
}
|
||||
//#endregion
|
||||
|
||||
const reload_node = () => updateData($data.node?.id ?? $data.root ?? 0);
|
||||
</script>
|
||||
|
||||
<div class="w-full max-w-4xl">
|
||||
@@ -125,7 +166,7 @@
|
||||
{#if i > 0}<li class="inline-flex items-center">/</li>{/if}
|
||||
<li class="inline-flex items-center">
|
||||
{#if segment.id !== null}
|
||||
<A href={'#/view/' + segment.id}>{segment.id === 0 ? 'Files' : segment.name}</A>
|
||||
<A href={base_link + segment.id}>{segment.id === 0 ? 'Files' : segment.name}</A>
|
||||
{:else}
|
||||
<span style="padding: 0 0.25em;">{segment.name}</span>
|
||||
{/if}
|
||||
@@ -133,24 +174,29 @@
|
||||
{/each}
|
||||
</Breadcrumb>
|
||||
<span class="flex-1"></span>
|
||||
{#if $data.node?.file === false}
|
||||
<Dropzone class="h-full w-64 cursor-pointer" on:drop={onDrop} on:dragover={e => e.preventDefault()} on:change={onChange} multiple>
|
||||
<span class="flex flex-row">
|
||||
<CloudUpload class="fill-gray-500 h-12 cursor-pointer" width="40%"/>
|
||||
<span class="inline text-gray-600 w-48 cursor-pointer">Click here or drag<br>to upload files</span>
|
||||
</span>
|
||||
</Dropzone>
|
||||
{#if $rpc_ty !== RpcClientType.SHARE_RO}
|
||||
{#if $data.node?.file === false}
|
||||
<Dropzone class="h-full w-64 cursor-pointer" on:drop={onDrop} on:dragover={e => e.preventDefault()} on:change={onChange} multiple>
|
||||
<span class="flex flex-row">
|
||||
<CloudUpload class="fill-gray-500 h-12 cursor-pointer" width="40%"/>
|
||||
<span class="inline text-gray-600 w-48 cursor-pointer">Click here or drag<br>to upload files</span>
|
||||
</span>
|
||||
</Dropzone>
|
||||
{/if}
|
||||
{:else if $data.can_unlock}
|
||||
<Button outline class="py-2 h-8 my-auto" on:click={() => show_unlock_dialog = true}>Unlock share</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if $data.node === null}
|
||||
<!-- Waiting for data -->
|
||||
{:else if $data.node.file}
|
||||
<FileViewer rpc={globalRpc} node={$data.node} />
|
||||
<FileViewer rpc={rpc} node={$data.node} />
|
||||
{:else}
|
||||
<DirViewer rpc={globalRpc} path_prefix="#/view/" node={$data.node} on:reload_node={() => updateData($data.node?.id ?? 0)} />
|
||||
<DirViewer rpc={rpc} path_prefix={base_link} node={$data.node} on:reload_node={reload_node} />
|
||||
{/if}
|
||||
</div>
|
||||
<UploadModal bind:upload={real_upload} on:reload_node={() => updateData($data.node?.id ?? 0)} />
|
||||
|
||||
<UploadModal rpc={rpc} bind:upload={real_upload} on:reload_node={() => updateData($data.node?.id ?? 0)} />
|
||||
{#if upload_progress_data.current !== upload_progress_data.total}
|
||||
<Modal open dismissable={false} title="Creating files">
|
||||
<div class="mb-1 flex justify-between">
|
||||
@@ -160,3 +206,13 @@
|
||||
<Progressbar class="!mt-0" size="h-4" bind:progress={upload_progress} />
|
||||
</Modal>
|
||||
{/if}
|
||||
|
||||
<InputModal
|
||||
on:done={onUnlock}
|
||||
bind:show={show_unlock_dialog}
|
||||
type="password"
|
||||
title="Unlock share"
|
||||
placeholder="Password"
|
||||
done_text="Unlock"
|
||||
icon={Password}
|
||||
/>
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { replace } from 'svelte-spa-router';
|
||||
import { getRpcClient } from '../../api';
|
||||
import { writable } from 'svelte/store';
|
||||
import { api, workingWrapper } from '../../store';
|
||||
import { Breadcrumb } from 'flowbite-svelte';
|
||||
import A from '../../components/A.svelte';
|
||||
import FileViewer from '../../components/FileViewer.svelte';
|
||||
import DirViewer from '../../components/DirViewer.svelte';
|
||||
|
||||
export let params: {sid?: string, id?: string} | undefined = {};
|
||||
|
||||
let sid = params?.sid ?? '';
|
||||
if (sid.length != 30) {
|
||||
replace('/');
|
||||
}
|
||||
|
||||
let base_link = `#/share/${sid}/`;
|
||||
|
||||
let rpc = getRpcClient();
|
||||
rpc.token = sid;
|
||||
|
||||
$: {
|
||||
let id = 0;
|
||||
if (params && params.id) {
|
||||
id = parseInt(params.id);
|
||||
if (id >= 0)
|
||||
updateData(id);
|
||||
}
|
||||
}
|
||||
|
||||
interface Data {
|
||||
node: api.Node | null,
|
||||
segments: api.PathSegment[],
|
||||
root: number,
|
||||
}
|
||||
|
||||
const data = writable<Data>({node: null, segments: [], root: 0});
|
||||
async function updateData(id: number) {
|
||||
let root = await workingWrapper(() => rpc.sessionInfo().then(v => v?.shareRoot));
|
||||
if (!root)
|
||||
return replace('/');
|
||||
let node = await workingWrapper(() => rpc.getNode(id));
|
||||
if (!node)
|
||||
return;
|
||||
let segments = await workingWrapper(() => rpc.getPath(id));
|
||||
if (!segments)
|
||||
return;
|
||||
rpc.share_root = root;
|
||||
data.set({node: node as Data['node'], segments, root });
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="w-full max-w-4xl">
|
||||
<div class="w-full flex mb-2 h-16">
|
||||
<Breadcrumb>
|
||||
{#each $data.segments as segment, i}
|
||||
{#if i > 0}<li class="inline-flex items-center">/</li>{/if}
|
||||
<li class="inline-flex items-center">
|
||||
{#if segment.id !== null}
|
||||
<A href={base_link + segment.id}>{segment.id === $data.root ? 'Share' : segment.name}</A>
|
||||
{:else}
|
||||
<span style="padding: 0 0.25em;">{segment.name}</span>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</Breadcrumb>
|
||||
<span class="flex-1"></span>
|
||||
</div>
|
||||
{#if $data.node === null}
|
||||
<!-- Waiting for data -->
|
||||
{:else if $data.node.file}
|
||||
<FileViewer rpc={rpc} node={$data.node} />
|
||||
{:else}
|
||||
<DirViewer rpc={rpc} path_prefix={base_link} node={$data.node} on:reload_node={() => updateData($data.node?.id ?? $data.root)} />
|
||||
{/if}
|
||||
</div>
|
||||
@@ -6,8 +6,7 @@ import Profile from './pages/profile/Profile.svelte';
|
||||
import TfaSetup from './pages/profile/TfaSetup.svelte';
|
||||
import Admin from './pages/profile/Admin.svelte';
|
||||
import View from './pages/View.svelte';
|
||||
import ShareHome from './pages/share/ShareHome.svelte';
|
||||
import ShareView from './pages/share/ShareView.svelte';
|
||||
import ShareHome from './pages/ShareHome.svelte';
|
||||
|
||||
export const routes = {
|
||||
'/': Home,
|
||||
@@ -23,5 +22,5 @@ export const routes = {
|
||||
'/view/:id': View,
|
||||
|
||||
'/share/:sid': ShareHome,
|
||||
'/share/:sid/:id': ShareView
|
||||
'/share/:sid/:id': View
|
||||
}
|
||||
|
||||
@@ -16,6 +16,6 @@
|
||||
"isolatedModules": true,
|
||||
"noUncheckedIndexedAccess": true
|
||||
},
|
||||
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
|
||||
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte", "src/icons/ShareEdit.svg", "icons/ShareEdit.svelte"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|
||||
@@ -4,11 +4,17 @@ import {viteSingleFile} from 'vite-plugin-singlefile';
|
||||
import {createHtmlPlugin} from 'vite-plugin-html';
|
||||
import purgeCss from 'vite-plugin-tailwind-purgecss';
|
||||
import Icons from 'unplugin-icons/vite';
|
||||
import {FileSystemIconLoader} from 'unplugin-icons/loaders';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
svelte(),
|
||||
Icons({ compiler: 'svelte' }),
|
||||
Icons({
|
||||
compiler: 'svelte',
|
||||
customCollections: {
|
||||
custom: FileSystemIconLoader('./icons')
|
||||
}
|
||||
}),
|
||||
purgeCss(),
|
||||
viteSingleFile({removeViteModuleLoader: true}),
|
||||
createHtmlPlugin({minify: true})
|
||||
|
||||
Reference in New Issue
Block a user