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:
		@@ -43,8 +43,8 @@ dependencies {
 | 
				
			|||||||
    implementation("org.springframework.boot:spring-boot-starter-actuator")
 | 
					    implementation("org.springframework.boot:spring-boot-starter-actuator")
 | 
				
			||||||
    implementation("org.springframework.boot:spring-boot-starter-security")
 | 
					    implementation("org.springframework.boot:spring-boot-starter-security")
 | 
				
			||||||
    implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0")
 | 
					    implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0")
 | 
				
			||||||
    implementation("com.thoughtworks.xstream:xstream:1.4.20")
 | 
					    implementation("com.thoughtworks.xstream:xstream:1.4.21")
 | 
				
			||||||
    implementation("dev.samstevens.totp:totp:1.7")
 | 
					    implementation("dev.samstevens.totp:totp:1.7.1")
 | 
				
			||||||
    implementation("com.twelvemonkeys.imageio:imageio-webp:3.11.0")
 | 
					    implementation("com.twelvemonkeys.imageio:imageio-webp:3.11.0")
 | 
				
			||||||
    implementation("com.twelvemonkeys.imageio:imageio-jpeg:3.11.0")
 | 
					    implementation("com.twelvemonkeys.imageio:imageio-jpeg:3.11.0")
 | 
				
			||||||
    implementation("net.coobird:thumbnailator:0.4.20")
 | 
					    implementation("net.coobird:thumbnailator:0.4.20")
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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 {fetchEventSource} from '@microsoft/fetch-event-source';
 | 
				
			||||||
import {token} from '../store';
 | 
					import {token} from '../store';
 | 
				
			||||||
import { replace } from 'svelte-spa-router';
 | 
					import { replace } from 'svelte-spa-router';
 | 
				
			||||||
 | 
					import { writable } from 'svelte/store';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type Session = components['schemas']['de.mattv.fileserver.Response$Session'];
 | 
					export type Session = components['schemas']['de.mattv.fileserver.Response$Session'];
 | 
				
			||||||
export type Node = components['schemas']['de.mattv.fileserver.Response$Node'];
 | 
					export type Node = components['schemas']['de.mattv.fileserver.Response$Node'];
 | 
				
			||||||
export type PathSegment = components['schemas']['de.mattv.fileserver.Response$PathSegment'];
 | 
					export type PathSegment = components['schemas']['de.mattv.fileserver.Response$PathSegment'];
 | 
				
			||||||
export type CreateNodeInfo = components['schemas']['de.mattv.fileserver.Response$CreateNodeInfo'];
 | 
					export type CreateNodeInfo = components['schemas']['de.mattv.fileserver.Response$CreateNodeInfo'];
 | 
				
			||||||
export type UserInfo = components['schemas']['de.mattv.fileserver.Response$UserInfo'];
 | 
					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 = () => {
 | 
					const _getRpcClient = () => {
 | 
				
			||||||
    let obj = {
 | 
					    let obj = {
 | 
				
			||||||
        token: '',
 | 
					        token: '',
 | 
				
			||||||
        client: createClient<paths>(),
 | 
					        client: createClient<paths>(),
 | 
				
			||||||
        share_root: null,
 | 
					        ty: writable(RpcClientType.AUTH),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        signup: (username: string, password: string) =>
 | 
					        signup: (username: string, password: string) =>
 | 
				
			||||||
            obj.client.POST('/api/public/auth/signup', { body: { username, password } }).then(v => v.data),
 | 
					            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),
 | 
					        downloadPreview: (node: number) => obj.client.POST('/api/user_share/fs/preview', { body: node }).then(v => v.data),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        createNode: (name: string, parent: number, file: boolean) =>
 | 
					        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', {
 | 
					        deleteNodes: (nodes: number[], cbk: (v: string|null) => void) => fetchEventSource('/api/user/fs/delete', {
 | 
				
			||||||
            method: 'POST',
 | 
					            method: 'POST',
 | 
				
			||||||
@@ -64,6 +68,8 @@ const _getRpcClient = () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        shareNode: (node: number) => obj.client.POST('/api/user/fs/share', { body: node }).then(v => v.data),
 | 
					        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),
 | 
					        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: {
 | 
					        admin: {
 | 
				
			||||||
            listUsers: () => obj.client.POST('/api/admin/users').then(v => v.data),
 | 
					            listUsers: () => obj.client.POST('/api/admin/users').then(v => v.data),
 | 
				
			||||||
@@ -91,7 +97,8 @@ const _getRpcClient = () => {
 | 
				
			|||||||
    return obj;
 | 
					    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 getRpcClient: () => RpcClient = () => _getRpcClient();
 | 
				
			||||||
export const globalRpc = 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 {
 | 
					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: {
 | 
					        parameters: {
 | 
				
			||||||
            query?: never;
 | 
					            query?: never;
 | 
				
			||||||
            header?: never;
 | 
					            header?: never;
 | 
				
			||||||
@@ -20,6 +36,22 @@ export interface paths {
 | 
				
			|||||||
        patch?: never;
 | 
					        patch?: never;
 | 
				
			||||||
        trace?: 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": {
 | 
					    "/api/user_share/fs/size": {
 | 
				
			||||||
        parameters: {
 | 
					        parameters: {
 | 
				
			||||||
            query?: never;
 | 
					            query?: never;
 | 
				
			||||||
@@ -100,6 +132,22 @@ export interface paths {
 | 
				
			|||||||
        patch?: never;
 | 
					        patch?: never;
 | 
				
			||||||
        trace?: 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": {
 | 
					    "/api/user/tfa/setup_totp": {
 | 
				
			||||||
        parameters: {
 | 
					        parameters: {
 | 
				
			||||||
            query?: never;
 | 
					            query?: never;
 | 
				
			||||||
@@ -180,6 +228,22 @@ export interface paths {
 | 
				
			|||||||
        patch?: never;
 | 
					        patch?: never;
 | 
				
			||||||
        trace?: 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": {
 | 
					    "/api/user/fs/share": {
 | 
				
			||||||
        parameters: {
 | 
					        parameters: {
 | 
				
			||||||
            query?: never;
 | 
					            query?: never;
 | 
				
			||||||
@@ -212,22 +276,6 @@ export interface paths {
 | 
				
			|||||||
        patch?: never;
 | 
					        patch?: never;
 | 
				
			||||||
        trace?: 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": {
 | 
					    "/api/user/auth/logout_all": {
 | 
				
			||||||
        parameters: {
 | 
					        parameters: {
 | 
				
			||||||
            query?: never;
 | 
					            query?: never;
 | 
				
			||||||
@@ -504,6 +552,10 @@ export interface paths {
 | 
				
			|||||||
export type webhooks = Record<string, never>;
 | 
					export type webhooks = Record<string, never>;
 | 
				
			||||||
export interface components {
 | 
					export interface components {
 | 
				
			||||||
    schemas: {
 | 
					    schemas: {
 | 
				
			||||||
 | 
					        "de.mattv.fileserver.ResponseJava.lang.String": {
 | 
				
			||||||
 | 
					            e?: string;
 | 
				
			||||||
 | 
					            o?: string;
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
        "de.mattv.fileserver.Response$Session": {
 | 
					        "de.mattv.fileserver.Response$Session": {
 | 
				
			||||||
            name: string;
 | 
					            name: string;
 | 
				
			||||||
            tfaEnabled: boolean;
 | 
					            tfaEnabled: boolean;
 | 
				
			||||||
@@ -511,6 +563,8 @@ export interface components {
 | 
				
			|||||||
            sudo: boolean;
 | 
					            sudo: boolean;
 | 
				
			||||||
            /** Format: int64 */
 | 
					            /** Format: int64 */
 | 
				
			||||||
            shareRoot?: number;
 | 
					            shareRoot?: number;
 | 
				
			||||||
 | 
					            shareCanRw: boolean;
 | 
				
			||||||
 | 
					            shareIsRw: boolean;
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
        "de.mattv.fileserver.Response$PathSegment": {
 | 
					        "de.mattv.fileserver.Response$PathSegment": {
 | 
				
			||||||
            name: string;
 | 
					            name: string;
 | 
				
			||||||
@@ -523,6 +577,7 @@ export interface components {
 | 
				
			|||||||
            name: string;
 | 
					            name: string;
 | 
				
			||||||
            file: boolean;
 | 
					            file: boolean;
 | 
				
			||||||
            preview: boolean;
 | 
					            preview: boolean;
 | 
				
			||||||
 | 
					            shareHasRw: boolean;
 | 
				
			||||||
            shareName?: string;
 | 
					            shareName?: string;
 | 
				
			||||||
            /** Format: int64 */
 | 
					            /** Format: int64 */
 | 
				
			||||||
            size?: number;
 | 
					            size?: number;
 | 
				
			||||||
@@ -530,14 +585,6 @@ export interface components {
 | 
				
			|||||||
            parent?: number;
 | 
					            parent?: number;
 | 
				
			||||||
            children?: components["schemas"]["de.mattv.fileserver.Response$Node"][];
 | 
					            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": {
 | 
					        "de.mattv.fileserver.routes.fs.Create$Body": {
 | 
				
			||||||
            name: string;
 | 
					            name: string;
 | 
				
			||||||
            /** Format: int64 */
 | 
					            /** Format: int64 */
 | 
				
			||||||
@@ -554,6 +601,15 @@ export interface components {
 | 
				
			|||||||
            e?: string;
 | 
					            e?: string;
 | 
				
			||||||
            o?: components["schemas"]["de.mattv.fileserver.Response$CreateNodeInfo"];
 | 
					            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": {
 | 
					        "de.mattv.fileserver.routes.auth.ChangePW$Body": {
 | 
				
			||||||
            oldPassword: string;
 | 
					            oldPassword: string;
 | 
				
			||||||
            newPassword: string;
 | 
					            newPassword: string;
 | 
				
			||||||
@@ -601,7 +657,51 @@ export interface components {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
export type $defs = Record<string, never>;
 | 
					export type $defs = Record<string, never>;
 | 
				
			||||||
export interface operations {
 | 
					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: {
 | 
					    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: {
 | 
					        parameters: {
 | 
				
			||||||
            query?: never;
 | 
					            query?: never;
 | 
				
			||||||
            header?: 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: {
 | 
					    setupTfaTotp: {
 | 
				
			||||||
        parameters: {
 | 
					        parameters: {
 | 
				
			||||||
            query?: never;
 | 
					            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: {
 | 
					    share: {
 | 
				
			||||||
        parameters: {
 | 
					        parameters: {
 | 
				
			||||||
            query?: never;
 | 
					            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: {
 | 
					    logoutAll: {
 | 
				
			||||||
        parameters: {
 | 
					        parameters: {
 | 
				
			||||||
            query?: never;
 | 
					            query?: never;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,21 +22,23 @@
 | 
				
			|||||||
        Tooltip
 | 
					        Tooltip
 | 
				
			||||||
    } from 'flowbite-svelte';
 | 
					    } from 'flowbite-svelte';
 | 
				
			||||||
    import {filesize} from 'filesize';
 | 
					    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 {api, download, workingWrapperR, error_banner, type RpcClient, info_banner} from '../store';
 | 
				
			||||||
    import LinkButton from './LinkButton.svelte';
 | 
					    import LinkButton from './LinkButton.svelte';
 | 
				
			||||||
    import DeleteModal from './DeleteModal.svelte';
 | 
					    import DeleteModal from './DeleteModal.svelte';
 | 
				
			||||||
    import A from './A.svelte';
 | 
					    import A from './A.svelte';
 | 
				
			||||||
    import {createEventDispatcher} from 'svelte';
 | 
					    import {createEventDispatcher} from 'svelte';
 | 
				
			||||||
 | 
					  import { RpcClientType } from '../api';
 | 
				
			||||||
 | 
					  import InputModal from './InputModal.svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    export let node: api.Node;
 | 
					    export let node: api.Node;
 | 
				
			||||||
    export let rpc: RpcClient
 | 
					    export let rpc: RpcClient;
 | 
				
			||||||
    export let path_prefix: string;
 | 
					    export let path_prefix: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const not_share = rpc.share_root == null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const dispatch = createEventDispatcher<{reload_node: null}>();
 | 
					    const dispatch = createEventDispatcher<{reload_node: null}>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const rpc_ty = rpc.ty;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let selected: number[] = [];
 | 
					    let selected: number[] = [];
 | 
				
			||||||
    let nodes: api.Node[], dirs: api.Node[], files: api.Node[], previews: {[key: number]: string|null} = {};
 | 
					    let nodes: api.Node[], dirs: api.Node[], files: api.Node[], previews: {[key: number]: string|null} = {};
 | 
				
			||||||
    let total_size: number;
 | 
					    let total_size: number;
 | 
				
			||||||
@@ -53,29 +55,33 @@
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    //#region New Folder Dialog
 | 
				
			||||||
    let show_new_folder = false, new_folder_name = '';
 | 
					    let show_new_folder = false;
 | 
				
			||||||
    const new_folder_keyup = (e: KeyboardEvent) => { if(e.key == 'Enter') newFolder(); }
 | 
					    async function newFolder(e: CustomEvent<string>) {
 | 
				
			||||||
    async function newFolder() {
 | 
					        const name = e.detail;
 | 
				
			||||||
        if (new_folder_name.length === 0)
 | 
					        if (name.length === 0)
 | 
				
			||||||
            return error_banner.set('Folder name can\'t be empty');
 | 
					            return error_banner.set('Folder name can\'t be empty');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        show_new_folder = false;
 | 
					        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)
 | 
					        if (resp && resp.isFile)
 | 
				
			||||||
            return error_banner.set('Folder already exists as file');
 | 
					            return error_banner.set('Folder already exists as file');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        dispatch('reload_node');
 | 
					        dispatch('reload_node');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    //#endregion
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async function getPreview(node: number) {
 | 
					    //#region Input password Dialog
 | 
				
			||||||
        const resp = await rpc.downloadPreview(node);
 | 
					    let show_pw_dialog = false;
 | 
				
			||||||
        if (!resp)
 | 
					    async function pwInputDone(e: CustomEvent<string>) {
 | 
				
			||||||
            return;
 | 
					        const pw = e.detail;
 | 
				
			||||||
        previews[node] = 'data:image/png;base64,' + resp;
 | 
					        await rpc.shareSetPw(ctx_node.id, pw);
 | 
				
			||||||
        previews = previews;
 | 
					        show_pw_dialog = false;
 | 
				
			||||||
 | 
					        dispatch('reload_node');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    //#endregion
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    //#region Context Menu
 | 
				
			||||||
    let ctx_node: api.Node;
 | 
					    let ctx_node: api.Node;
 | 
				
			||||||
    let ctx_hidden = true;
 | 
					    let ctx_hidden = true;
 | 
				
			||||||
    let ctx_x = 0, ctx_y = 0;
 | 
					    let ctx_x = 0, ctx_y = 0;
 | 
				
			||||||
@@ -92,14 +98,6 @@
 | 
				
			|||||||
        ctx_hidden = false;
 | 
					        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 onCtxDelete = () => del([ctx_node.id]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const onCtxDownload = () => download(rpc, [ctx_node]);
 | 
					    const onCtxDownload = () => download(rpc, [ctx_node]);
 | 
				
			||||||
@@ -120,8 +118,31 @@
 | 
				
			|||||||
        await rpc.unshareNode(ctx_node.id);
 | 
					        await rpc.unshareNode(ctx_node.id);
 | 
				
			||||||
        dispatch('reload_node');
 | 
					        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 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>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<svelte:body on:click={() => (ctx_hidden = true)} />
 | 
					<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="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="px-2 w-0"><Folder /></TableBodyCell>
 | 
				
			||||||
                <TableBodyCell class="pl-0 flex flex-row gap-2">
 | 
					                <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>
 | 
					                    <A href={path_prefix + node.id}>{node.name}</A>
 | 
				
			||||||
                </TableBodyCell>
 | 
					                </TableBodyCell>
 | 
				
			||||||
                <TableBodyCell></TableBodyCell>
 | 
					                <TableBodyCell></TableBodyCell>
 | 
				
			||||||
@@ -172,7 +195,9 @@
 | 
				
			|||||||
                    {/if}
 | 
					                    {/if}
 | 
				
			||||||
                </TableBodyCell>
 | 
					                </TableBodyCell>
 | 
				
			||||||
                <TableBodyCell class="pl-0 flex flex-row gap-2">
 | 
					                <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>
 | 
					                    <A href={path_prefix + node.id}>{node.name}</A>
 | 
				
			||||||
                </TableBodyCell>
 | 
					                </TableBodyCell>
 | 
				
			||||||
                <TableBodyCell>{filesize(node.size ?? 0, {base: 2, standard: 'jedec'})}</TableBodyCell>
 | 
					                <TableBodyCell>{filesize(node.size ?? 0, {base: 2, standard: 'jedec'})}</TableBodyCell>
 | 
				
			||||||
@@ -182,10 +207,14 @@
 | 
				
			|||||||
    <tfoot class="text-gray-700 bg-gray-50">
 | 
					    <tfoot class="text-gray-700 bg-gray-50">
 | 
				
			||||||
        <tr>
 | 
					        <tr>
 | 
				
			||||||
            <td class="px-6 py-3" colspan="3">
 | 
					            <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}
 | 
					                {#if selected.length > 0}
 | 
				
			||||||
                    <LinkButton on:click={downloadSelected}>Download</LinkButton>
 | 
					                    <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}
 | 
					                {/if}
 | 
				
			||||||
            </td>
 | 
					            </td>
 | 
				
			||||||
            <td class="px-6 py-3">{filesize(total_size, {base: 2, standard: 'jedec'})}</td>
 | 
					            <td class="px-6 py-3">{filesize(total_size, {base: 2, standard: 'jedec'})}</td>
 | 
				
			||||||
@@ -193,16 +222,24 @@
 | 
				
			|||||||
    </tfoot>
 | 
					    </tfoot>
 | 
				
			||||||
</Table>
 | 
					</Table>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<Modal bind:open={show_new_folder} outsideclose title="Create new folder">
 | 
					<InputModal
 | 
				
			||||||
    <ButtonGroup class="w-full mb-4">
 | 
					    on:done={newFolder}
 | 
				
			||||||
        <InputAddon><FolderAdd /></InputAddon>
 | 
					    bind:show={show_new_folder}
 | 
				
			||||||
        <Input type="text" placeholder="Name" bind:value={new_folder_name} on:keyup={new_folder_keyup}></Input>
 | 
					    type="text"
 | 
				
			||||||
    </ButtonGroup>
 | 
					    title="Create new folder"
 | 
				
			||||||
    <span class="w-full flex">
 | 
					    placeholder="Name"
 | 
				
			||||||
        <span class="flex-1 mr-2"></span>
 | 
					    done_text="Create folder"
 | 
				
			||||||
        <Button outline on:click={newFolder}>Create folder</Button>
 | 
					    icon={FolderAdd}
 | 
				
			||||||
    </span>
 | 
					/>
 | 
				
			||||||
</Modal>
 | 
					<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">
 | 
					<Dropdown triggeredBy="#dropdown-button" trigger="hover" placement="left">
 | 
				
			||||||
    <DropdownItem on:click={selectAll}>Select all</DropdownItem>
 | 
					    <DropdownItem on:click={selectAll}>Select all</DropdownItem>
 | 
				
			||||||
@@ -214,8 +251,14 @@
 | 
				
			|||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
 | 
					<!-- 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)}>
 | 
					<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">
 | 
					    <ul class="py-1">
 | 
				
			||||||
        {#if not_share}
 | 
					        {#if $rpc_ty === RpcClientType.AUTH}
 | 
				
			||||||
            {#if ctx_node?.shareName}
 | 
					            {#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={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>
 | 
					                <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}
 | 
					            {:else}
 | 
				
			||||||
@@ -223,7 +266,7 @@
 | 
				
			|||||||
            {/if}
 | 
					            {/if}
 | 
				
			||||||
        {/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>
 | 
					        <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>
 | 
					            <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}
 | 
					        {/if}
 | 
				
			||||||
    </ul>
 | 
					    </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">
 | 
					<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 {Button, Modal, Progressbar} from 'flowbite-svelte';
 | 
				
			||||||
    import {filesize} from 'filesize';
 | 
					    import {filesize} from 'filesize';
 | 
				
			||||||
    import {createEventDispatcher} from 'svelte';
 | 
					    import {createEventDispatcher} from 'svelte';
 | 
				
			||||||
 | 
					  import type { AllPaths } from '../api';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    export let rpc: RpcClient;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const dispatch = createEventDispatcher<{reload_node: null}>();
 | 
					    const dispatch = createEventDispatcher<{reload_node: null}>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -39,7 +41,10 @@
 | 
				
			|||||||
        await new Promise((resolve) => {
 | 
					        await new Promise((resolve) => {
 | 
				
			||||||
            const xhr = new XMLHttpRequest();
 | 
					            const xhr = new XMLHttpRequest();
 | 
				
			||||||
            xhr.onloadend = resolve;
 | 
					            xhr.onloadend = resolve;
 | 
				
			||||||
            xhr.onerror = resolve;
 | 
					            xhr.onerror = e => {
 | 
				
			||||||
 | 
					                console.error(e);
 | 
				
			||||||
 | 
					                resolve(null);
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
            xhr.upload.onprogress = ev => {
 | 
					            xhr.upload.onprogress = ev => {
 | 
				
			||||||
                current += ev.loaded - load_progress;
 | 
					                current += ev.loaded - load_progress;
 | 
				
			||||||
                load_progress = ev.loaded;
 | 
					                load_progress = ev.loaded;
 | 
				
			||||||
@@ -49,8 +54,9 @@
 | 
				
			|||||||
                if (file.current == file.total)
 | 
					                if (file.current == file.total)
 | 
				
			||||||
                    resolve(null);
 | 
					                    resolve(null);
 | 
				
			||||||
            };
 | 
					            };
 | 
				
			||||||
            xhr.open('POST', `/api/user/upload/${file.id}`, true);
 | 
					            const url: AllPaths = '/api/user_share/upload/{id}';
 | 
				
			||||||
            xhr.setRequestHeader('Authorization', 'Bearer ' + ($token ?? ''));
 | 
					            xhr.open('POST', url.replace('{id}', file.id.toString()), true);
 | 
				
			||||||
 | 
					            xhr.setRequestHeader('Authorization', 'Bearer ' + rpc.token);
 | 
				
			||||||
            xhr.send(file.file);
 | 
					            xhr.send(file.file);
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
        current += file.total - load_progress;
 | 
					        current += file.total - load_progress;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,6 +11,7 @@ export {default as CloudUpload} from '~icons/carbon/CloudUpload';
 | 
				
			|||||||
export {default as Checkmark} from '~icons/carbon/Checkmark';
 | 
					export {default as Checkmark} from '~icons/carbon/Checkmark';
 | 
				
			||||||
export {default as Error} from '~icons/carbon/Error';
 | 
					export {default as Error} from '~icons/carbon/Error';
 | 
				
			||||||
export {default as Share} from '~icons/carbon/Share';
 | 
					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 CaretLeft} from '~icons/ph/CaretLeft';
 | 
				
			||||||
export {default as OTP} from '~icons/ph/Password';
 | 
					export {default as OTP} from '~icons/ph/Password';
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
  import { replace } from 'svelte-spa-router';
 | 
					  import { replace } from 'svelte-spa-router';
 | 
				
			||||||
  import { getRpcClient } from '../../api';
 | 
					  import { getRpcClient } from '../api';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    export let params: {sid?: string} | undefined = {};
 | 
					    export let params: {sid?: string} | undefined = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1,19 +1,35 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
    import {Breadcrumb, Dropzone, Modal, Progressbar} from 'flowbite-svelte';
 | 
					    import {Breadcrumb, Button, Dropzone, Modal, Progressbar} from 'flowbite-svelte';
 | 
				
			||||||
    import {writable} from 'svelte/store';
 | 
					    import {get, writable} from 'svelte/store';
 | 
				
			||||||
    import {CloudUpload} from '../icons';
 | 
					    import {CloudUpload, Password} from '../icons';
 | 
				
			||||||
    import {api, globalRpc, token, type UploadFile, workingWrapper, workingWrapperR} from '../store';
 | 
					    import {api, globalRpc, type UploadFile, workingWrapper, workingWrapperR} from '../store';
 | 
				
			||||||
    import DirViewer from '../components/DirViewer.svelte';
 | 
					    import DirViewer from '../components/DirViewer.svelte';
 | 
				
			||||||
    import UploadModal from '../components/UploadModal.svelte';
 | 
					    import UploadModal from '../components/UploadModal.svelte';
 | 
				
			||||||
    import FileViewer from '../components/FileViewer.svelte';
 | 
					    import FileViewer from '../components/FileViewer.svelte';
 | 
				
			||||||
    import A from '../components/A.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 {
 | 
					    interface Data {
 | 
				
			||||||
        node: api.Node|null,
 | 
					        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;
 | 
					        let id = 0;
 | 
				
			||||||
        if (params && params.id) {
 | 
					        if (params && params.id) {
 | 
				
			||||||
@@ -25,15 +41,25 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const data = writable<Data>({node: null, segments: []});
 | 
					    const data = writable<Data>({node: null, segments: []});
 | 
				
			||||||
    async function updateData(id: number) {
 | 
					    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)
 | 
					        if (!node)
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
        let segments = await workingWrapper(() => globalRpc.getPath(id));
 | 
					        let segments = await workingWrapper(() => rpc.getPath(id));
 | 
				
			||||||
        if (!segments)
 | 
					        if (!segments)
 | 
				
			||||||
            return;
 | 
					            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));
 | 
					    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[]> {
 | 
					    async function handleEntry(parent: number, parent_name: string, entry: FileSystemEntry): Promise<UploadFile[]> {
 | 
				
			||||||
@@ -50,7 +76,7 @@
 | 
				
			|||||||
                return [];
 | 
					                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) return [];
 | 
				
			||||||
        if (resp.isFile) return [];
 | 
					        if (resp.isFile) return [];
 | 
				
			||||||
        const reader = (entry as FileSystemDirectoryEntry).createReader();
 | 
					        const reader = (entry as FileSystemDirectoryEntry).createReader();
 | 
				
			||||||
@@ -109,13 +135,28 @@
 | 
				
			|||||||
        upload_progress_data.current = 0;
 | 
					        upload_progress_data.current = 0;
 | 
				
			||||||
        const upload_files: UploadFile[] = [];
 | 
					        const upload_files: UploadFile[] = [];
 | 
				
			||||||
        for (const file of files) {
 | 
					        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)
 | 
					            if (resp && resp.isFile)
 | 
				
			||||||
                upload_files.push({ ...file, id: resp.id, overwrite: resp.exists });
 | 
					                upload_files.push({ ...file, id: resp.id, overwrite: resp.exists });
 | 
				
			||||||
            upload_progress_data.current++;
 | 
					            upload_progress_data.current++;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        await real_upload(upload_files);
 | 
					        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>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="w-full max-w-4xl">
 | 
					<div class="w-full max-w-4xl">
 | 
				
			||||||
@@ -125,7 +166,7 @@
 | 
				
			|||||||
                {#if i > 0}<li class="inline-flex items-center">/</li>{/if}
 | 
					                {#if i > 0}<li class="inline-flex items-center">/</li>{/if}
 | 
				
			||||||
                <li class="inline-flex items-center">
 | 
					                <li class="inline-flex items-center">
 | 
				
			||||||
                    {#if segment.id !== null}
 | 
					                    {#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}
 | 
					                    {:else}
 | 
				
			||||||
                        <span style="padding: 0 0.25em;">{segment.name}</span>
 | 
					                        <span style="padding: 0 0.25em;">{segment.name}</span>
 | 
				
			||||||
                    {/if}
 | 
					                    {/if}
 | 
				
			||||||
@@ -133,6 +174,7 @@
 | 
				
			|||||||
            {/each}
 | 
					            {/each}
 | 
				
			||||||
        </Breadcrumb>
 | 
					        </Breadcrumb>
 | 
				
			||||||
        <span class="flex-1"></span>
 | 
					        <span class="flex-1"></span>
 | 
				
			||||||
 | 
					        {#if $rpc_ty !== RpcClientType.SHARE_RO}
 | 
				
			||||||
            {#if $data.node?.file === false}
 | 
					            {#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>
 | 
					                <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">
 | 
					                    <span class="flex flex-row">
 | 
				
			||||||
@@ -141,16 +183,20 @@
 | 
				
			|||||||
                    </span>
 | 
					                    </span>
 | 
				
			||||||
                </Dropzone>
 | 
					                </Dropzone>
 | 
				
			||||||
            {/if}
 | 
					            {/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>
 | 
					    </div>
 | 
				
			||||||
    {#if $data.node === null}
 | 
					    {#if $data.node === null}
 | 
				
			||||||
        <!-- Waiting for data -->
 | 
					        <!-- Waiting for data -->
 | 
				
			||||||
    {:else if $data.node.file}
 | 
					    {:else if $data.node.file}
 | 
				
			||||||
        <FileViewer rpc={globalRpc} node={$data.node} />
 | 
					        <FileViewer rpc={rpc} node={$data.node} />
 | 
				
			||||||
    {:else}
 | 
					    {: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}
 | 
					    {/if}
 | 
				
			||||||
</div>
 | 
					</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}
 | 
					{#if upload_progress_data.current !== upload_progress_data.total}
 | 
				
			||||||
    <Modal open dismissable={false} title="Creating files">
 | 
					    <Modal open dismissable={false} title="Creating files">
 | 
				
			||||||
        <div class="mb-1 flex justify-between">
 | 
					        <div class="mb-1 flex justify-between">
 | 
				
			||||||
@@ -160,3 +206,13 @@
 | 
				
			|||||||
        <Progressbar class="!mt-0" size="h-4" bind:progress={upload_progress} />
 | 
					        <Progressbar class="!mt-0" size="h-4" bind:progress={upload_progress} />
 | 
				
			||||||
    </Modal>
 | 
					    </Modal>
 | 
				
			||||||
{/if}
 | 
					{/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 TfaSetup from './pages/profile/TfaSetup.svelte';
 | 
				
			||||||
import Admin from './pages/profile/Admin.svelte';
 | 
					import Admin from './pages/profile/Admin.svelte';
 | 
				
			||||||
import View from './pages/View.svelte';
 | 
					import View from './pages/View.svelte';
 | 
				
			||||||
import ShareHome from './pages/share/ShareHome.svelte';
 | 
					import ShareHome from './pages/ShareHome.svelte';
 | 
				
			||||||
import ShareView from './pages/share/ShareView.svelte';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const routes = {
 | 
					export const routes = {
 | 
				
			||||||
    '/': Home,
 | 
					    '/': Home,
 | 
				
			||||||
@@ -23,5 +22,5 @@ export const routes = {
 | 
				
			|||||||
    '/view/:id': View,
 | 
					    '/view/:id': View,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    '/share/:sid': ShareHome,
 | 
					    '/share/:sid': ShareHome,
 | 
				
			||||||
    '/share/:sid/:id': ShareView
 | 
					    '/share/:sid/:id': View
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,6 +16,6 @@
 | 
				
			|||||||
    "isolatedModules": true,
 | 
					    "isolatedModules": true,
 | 
				
			||||||
    "noUncheckedIndexedAccess": 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" }]
 | 
					  "references": [{ "path": "./tsconfig.node.json" }]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,11 +4,17 @@ import {viteSingleFile} from 'vite-plugin-singlefile';
 | 
				
			|||||||
import {createHtmlPlugin} from 'vite-plugin-html';
 | 
					import {createHtmlPlugin} from 'vite-plugin-html';
 | 
				
			||||||
import purgeCss from 'vite-plugin-tailwind-purgecss';
 | 
					import purgeCss from 'vite-plugin-tailwind-purgecss';
 | 
				
			||||||
import Icons from 'unplugin-icons/vite';
 | 
					import Icons from 'unplugin-icons/vite';
 | 
				
			||||||
 | 
					import {FileSystemIconLoader} from 'unplugin-icons/loaders';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineConfig({
 | 
					export default defineConfig({
 | 
				
			||||||
    plugins: [
 | 
					    plugins: [
 | 
				
			||||||
        svelte(),
 | 
					        svelte(),
 | 
				
			||||||
        Icons({ compiler: 'svelte' }),
 | 
					        Icons({
 | 
				
			||||||
 | 
					            compiler: 'svelte',
 | 
				
			||||||
 | 
					            customCollections: {
 | 
				
			||||||
 | 
					                custom: FileSystemIconLoader('./icons')
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
        purgeCss(),
 | 
					        purgeCss(),
 | 
				
			||||||
        viteSingleFile({removeViteModuleLoader: true}),
 | 
					        viteSingleFile({removeViteModuleLoader: true}),
 | 
				
			||||||
        createHtmlPlugin({minify: true})
 | 
					        createHtmlPlugin({minify: true})
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,7 +25,9 @@ public class Response<T> {
 | 
				
			|||||||
            @NonNull boolean tfaEnabled,
 | 
					            @NonNull boolean tfaEnabled,
 | 
				
			||||||
            @NonNull boolean admin,
 | 
					            @NonNull boolean admin,
 | 
				
			||||||
            @NonNull boolean sudo,
 | 
					            @NonNull boolean sudo,
 | 
				
			||||||
            @Nullable Long shareRoot
 | 
					            @Nullable Long shareRoot,
 | 
				
			||||||
 | 
					            @NonNull boolean shareCanRw,
 | 
				
			||||||
 | 
					            @NonNull boolean shareIsRw
 | 
				
			||||||
    ) {}
 | 
					    ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public record UserInfo(
 | 
					    public record UserInfo(
 | 
				
			||||||
@@ -52,6 +54,7 @@ public class Response<T> {
 | 
				
			|||||||
            @NonNull String name,
 | 
					            @NonNull String name,
 | 
				
			||||||
            @NonNull boolean file,
 | 
					            @NonNull boolean file,
 | 
				
			||||||
            @NonNull boolean preview,
 | 
					            @NonNull boolean preview,
 | 
				
			||||||
 | 
					            @NonNull boolean shareHasRw,
 | 
				
			||||||
            @Nullable String shareName,
 | 
					            @Nullable String shareName,
 | 
				
			||||||
            @Nullable Long size,
 | 
					            @Nullable Long size,
 | 
				
			||||||
            @Nullable Long parent,
 | 
					            @Nullable Long parent,
 | 
				
			||||||
@@ -63,6 +66,7 @@ public class Response<T> {
 | 
				
			|||||||
                    node.name,
 | 
					                    node.name,
 | 
				
			||||||
                    node.isFile,
 | 
					                    node.isFile,
 | 
				
			||||||
                    node.hasPreview,
 | 
					                    node.hasPreview,
 | 
				
			||||||
 | 
					                    node.shareWritePw != null,
 | 
				
			||||||
                    node.shareString,
 | 
					                    node.shareString,
 | 
				
			||||||
                    node.size,
 | 
					                    node.size,
 | 
				
			||||||
                    (node.parent == null || !hasParent) ? null : node.parent.id,
 | 
					                    (node.parent == null || !hasParent) ? null : node.parent.id,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,6 +21,7 @@ public class Node {
 | 
				
			|||||||
    public long size = 0;
 | 
					    public long size = 0;
 | 
				
			||||||
    public Node parent = null;
 | 
					    public Node parent = null;
 | 
				
			||||||
    public String shareString = null;
 | 
					    public String shareString = null;
 | 
				
			||||||
 | 
					    public String shareWritePw = null;
 | 
				
			||||||
    public final ArrayList<Node> children = new ArrayList<>();
 | 
					    public final ArrayList<Node> children = new ArrayList<>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public final File file;
 | 
					    public final File file;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,18 +14,20 @@ import java.util.List;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
public class Token implements Authentication {
 | 
					public class Token implements Authentication {
 | 
				
			||||||
    private static final Duration LIFETIME = Duration.ofHours(24);
 | 
					    private static final Duration LIFETIME = Duration.ofHours(24);
 | 
				
			||||||
    private static Instant nowPlusLifetime() { return Instant.now().plus(LIFETIME); }
 | 
					    private static final Duration LIFETIME_SRW = Duration.ofHours(1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private static final SimpleGrantedAuthority AUTHORITY_USER = new SimpleGrantedAuthority("ROLE_USER");
 | 
					    private static final SimpleGrantedAuthority AUTHORITY_USER = new SimpleGrantedAuthority("ROLE_USER");
 | 
				
			||||||
    private static final SimpleGrantedAuthority AUTHORITY_ADMIN = new SimpleGrantedAuthority("ROLE_ADMIN");
 | 
					    private static final SimpleGrantedAuthority AUTHORITY_ADMIN = new SimpleGrantedAuthority("ROLE_ADMIN");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private @NonNull Instant expiresAt = nowPlusLifetime();
 | 
					    private @NonNull Instant expiresAt;
 | 
				
			||||||
    @Getter private final @NonNull String token;
 | 
					    @Getter private final @NonNull String token;
 | 
				
			||||||
    @Getter private final Long shareRoot;
 | 
					    @Getter private final Long shareRoot;
 | 
				
			||||||
 | 
					    @Getter private final boolean shareCanRw;
 | 
				
			||||||
 | 
					    @Getter private final boolean shareRw;
 | 
				
			||||||
    @Getter private @NonNull User user;
 | 
					    @Getter private @NonNull User user;
 | 
				
			||||||
    @Getter private User sudoRealUser = null;
 | 
					    @Getter private User sudoRealUser = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public void refresh() { expiresAt = nowPlusLifetime(); }
 | 
					    public void refresh() { expiresAt = Instant.now().plus(shareRoot == null ? LIFETIME : LIFETIME_SRW); }
 | 
				
			||||||
    public boolean isShare() { return shareRoot != null; }
 | 
					    public boolean isShare() { return shareRoot != null; }
 | 
				
			||||||
    public boolean expired() { return Utils.instantExpired(expiresAt); }
 | 
					    public boolean expired() { return Utils.instantExpired(expiresAt); }
 | 
				
			||||||
    public boolean inSudo() { return sudoRealUser != null; }
 | 
					    public boolean inSudo() { return sudoRealUser != null; }
 | 
				
			||||||
@@ -44,10 +46,13 @@ public class Token implements Authentication {
 | 
				
			|||||||
        user = newUser;
 | 
					        user = newUser;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public Token(@NonNull String token, @NonNull User user, Long shareRoot) {
 | 
					    public Token(@NonNull String token, @NonNull User user, Long shareRoot, boolean shareCanRw, boolean shareRw) {
 | 
				
			||||||
        this.token = token;
 | 
					        this.token = token;
 | 
				
			||||||
        this.user = user;
 | 
					        this.user = user;
 | 
				
			||||||
        this.shareRoot = shareRoot;
 | 
					        this.shareRoot = shareRoot;
 | 
				
			||||||
 | 
					        this.shareCanRw = shareCanRw;
 | 
				
			||||||
 | 
					        this.shareRw = shareRw;
 | 
				
			||||||
 | 
					        this.refresh();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Override public Object getDetails() { return null; }
 | 
					    @Override public Object getDetails() { return null; }
 | 
				
			||||||
@@ -72,6 +77,6 @@ public class Token implements Authentication {
 | 
				
			|||||||
    @Override
 | 
					    @Override
 | 
				
			||||||
    public String toString() {
 | 
					    public String toString() {
 | 
				
			||||||
        if (shareRoot == null) return "Token{user=" + user + ", real_user=" + sudoRealUser + '}';
 | 
					        if (shareRoot == null) return "Token{user=" + user + ", real_user=" + sudoRealUser + '}';
 | 
				
			||||||
        else return "Share(" + token + ")";
 | 
					        else return (shareRw ? "ShareRW(" : "Share(") + token + ")";
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,6 +15,7 @@ public class NodeConverter {
 | 
				
			|||||||
    private static final String ATTR_FLAGS_NAME = "flags";
 | 
					    private static final String ATTR_FLAGS_NAME = "flags";
 | 
				
			||||||
    private static final String ATTR_SIZE_NAME = "size";
 | 
					    private static final String ATTR_SIZE_NAME = "size";
 | 
				
			||||||
    private static final String ATTR_SHARE_NAME = "share";
 | 
					    private static final String ATTR_SHARE_NAME = "share";
 | 
				
			||||||
 | 
					    private static final String ATTR_SHARE_RW_NAME = "share-pw";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private static class Flags {
 | 
					    private static class Flags {
 | 
				
			||||||
        public static final long FILE = 1;
 | 
					        public static final long FILE = 1;
 | 
				
			||||||
@@ -32,7 +33,10 @@ public class NodeConverter {
 | 
				
			|||||||
        writer.addAttribute(ATTR_FLAGS_NAME, String.valueOf(flags));
 | 
					        writer.addAttribute(ATTR_FLAGS_NAME, String.valueOf(flags));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (node.isFile) writer.addAttribute(ATTR_SIZE_NAME, String.valueOf(node.size));
 | 
					        if (node.isFile) writer.addAttribute(ATTR_SIZE_NAME, String.valueOf(node.size));
 | 
				
			||||||
        if (node.shareString != null) writer.addAttribute(ATTR_SHARE_NAME, node.shareString);
 | 
					        if (node.shareString != null) {
 | 
				
			||||||
 | 
					            writer.addAttribute(ATTR_SHARE_NAME, node.shareString);
 | 
				
			||||||
 | 
					            if (node.shareWritePw != null) writer.addAttribute(ATTR_SHARE_RW_NAME, node.shareWritePw);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        node.children.forEach(child -> toXml(child, writer));
 | 
					        node.children.forEach(child -> toXml(child, writer));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -65,8 +69,12 @@ public class NodeConverter {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        String share = reader.getAttribute(ATTR_SHARE_NAME);
 | 
					        String share = reader.getAttribute(ATTR_SHARE_NAME);
 | 
				
			||||||
        if (share != null)
 | 
					        if (share != null) {
 | 
				
			||||||
            node.shareString = share;
 | 
					            node.shareString = share;
 | 
				
			||||||
 | 
					            String sharePw = reader.getAttribute(ATTR_SHARE_RW_NAME);
 | 
				
			||||||
 | 
					            if (sharePw != null)
 | 
				
			||||||
 | 
					                node.shareWritePw = sharePw;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        while (reader.hasMoreChildren()) {
 | 
					        while (reader.hasMoreChildren()) {
 | 
				
			||||||
            Node child = fromXml(reader, user);
 | 
					            Node child = fromXml(reader, user);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,10 +4,10 @@ import de.mattv.fileserver.Utils;
 | 
				
			|||||||
import de.mattv.fileserver.data.Node;
 | 
					import de.mattv.fileserver.data.Node;
 | 
				
			||||||
import de.mattv.fileserver.data.Token;
 | 
					import de.mattv.fileserver.data.Token;
 | 
				
			||||||
import de.mattv.fileserver.data.User;
 | 
					import de.mattv.fileserver.data.User;
 | 
				
			||||||
 | 
					import de.mattv.fileserver.routes.fs.FsUtils;
 | 
				
			||||||
import de.mattv.fileserver.util.AuthUser;
 | 
					import de.mattv.fileserver.util.AuthUser;
 | 
				
			||||||
import de.mattv.fileserver.util.AutoCloseLock;
 | 
					import de.mattv.fileserver.util.AutoCloseLock;
 | 
				
			||||||
import de.mattv.fileserver.util.UserRestController;
 | 
					import de.mattv.fileserver.util.UserOrShareRestController;
 | 
				
			||||||
import io.swagger.v3.oas.annotations.Operation;
 | 
					 | 
				
			||||||
import jakarta.annotation.PostConstruct;
 | 
					import jakarta.annotation.PostConstruct;
 | 
				
			||||||
import jakarta.annotation.PreDestroy;
 | 
					import jakarta.annotation.PreDestroy;
 | 
				
			||||||
import jakarta.servlet.http.HttpServletRequest;
 | 
					import jakarta.servlet.http.HttpServletRequest;
 | 
				
			||||||
@@ -25,7 +25,7 @@ import java.nio.file.StandardCopyOption;
 | 
				
			|||||||
import java.util.concurrent.atomic.AtomicLong;
 | 
					import java.util.concurrent.atomic.AtomicLong;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Slf4j
 | 
					@Slf4j
 | 
				
			||||||
@UserRestController
 | 
					@UserOrShareRestController
 | 
				
			||||||
public class Upload {
 | 
					public class Upload {
 | 
				
			||||||
    private static final File TEMP_DIR = new File("temp");
 | 
					    private static final File TEMP_DIR = new File("temp");
 | 
				
			||||||
    private static final AtomicLong NEXT_TEMP_ID = new AtomicLong(0);
 | 
					    private static final AtomicLong NEXT_TEMP_ID = new AtomicLong(0);
 | 
				
			||||||
@@ -46,7 +46,6 @@ public class Upload {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @PostMapping("/upload/{id}")
 | 
					    @PostMapping("/upload/{id}")
 | 
				
			||||||
    @Operation(hidden = true)
 | 
					 | 
				
			||||||
    private void upload(@AuthUser Token token, @PathVariable long id, HttpServletRequest request, HttpServletResponse response) throws IOException {
 | 
					    private void upload(@AuthUser Token token, @PathVariable long id, HttpServletRequest request, HttpServletResponse response) throws IOException {
 | 
				
			||||||
        User user = token.getUser();
 | 
					        User user = token.getUser();
 | 
				
			||||||
        try (AutoCloseLock ignored = new AutoCloseLock(user.nodeLock.readLock())) {
 | 
					        try (AutoCloseLock ignored = new AutoCloseLock(user.nodeLock.readLock())) {
 | 
				
			||||||
@@ -55,6 +54,16 @@ public class Upload {
 | 
				
			|||||||
                response.sendError(400, "Invalid node");
 | 
					                response.sendError(400, "Invalid node");
 | 
				
			||||||
                return;
 | 
					                return;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					            if (token.isShare()) {
 | 
				
			||||||
 | 
					                if (!token.isShareRw()) {
 | 
				
			||||||
 | 
					                    response.sendError(401, "Share is not unlocked");
 | 
				
			||||||
 | 
					                    return;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                if (!FsUtils.shareHasAccess(token, node)) {
 | 
				
			||||||
 | 
					                    response.sendError(401, "No access to node");
 | 
				
			||||||
 | 
					                    return;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            File tempFile = getTempFile();
 | 
					            File tempFile = getTempFile();
 | 
				
			||||||
            FileCopyUtils.copy(request.getInputStream(), Files.newOutputStream(tempFile.toPath()));
 | 
					            FileCopyUtils.copy(request.getInputStream(), Files.newOutputStream(tempFile.toPath()));
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,6 +9,6 @@ import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
 | 
				
			|||||||
public class AuthUtils {
 | 
					public class AuthUtils {
 | 
				
			||||||
    private AuthUtils() {}
 | 
					    private AuthUtils() {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected static final Argon2PasswordEncoder PW_ENCODER = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
 | 
					    public static final Argon2PasswordEncoder PW_ENCODER = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
 | 
				
			||||||
    protected static final CodeVerifier TOTP_VERIFIER = new DefaultCodeVerifier(new DefaultCodeGenerator(), new SystemTimeProvider());
 | 
					    public static final CodeVerifier TOTP_VERIFIER = new DefaultCodeVerifier(new DefaultCodeGenerator(), new SystemTimeProvider());
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -54,7 +54,7 @@ public class Login {
 | 
				
			|||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return Response.o(new Response.Login(false, TokenService.createToken(user)));
 | 
					            return Response.o(new Response.Login(false, TokenService.createToken(user, null)));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,7 +18,9 @@ public class SessionInfo {
 | 
				
			|||||||
                    false,
 | 
					                    false,
 | 
				
			||||||
                    false,
 | 
					                    false,
 | 
				
			||||||
                    false,
 | 
					                    false,
 | 
				
			||||||
                    token.getShareRoot()
 | 
					                    token.getShareRoot(),
 | 
				
			||||||
 | 
					                    token.isShareCanRw(),
 | 
				
			||||||
 | 
					                    token.isShareRw()
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        User user = token.getUser();
 | 
					        User user = token.getUser();
 | 
				
			||||||
@@ -27,7 +29,9 @@ public class SessionInfo {
 | 
				
			|||||||
                user.tfaSecret != null,
 | 
					                user.tfaSecret != null,
 | 
				
			||||||
                token.isAdmin(),
 | 
					                token.isAdmin(),
 | 
				
			||||||
                token.inSudo(),
 | 
					                token.inSudo(),
 | 
				
			||||||
                null
 | 
					                null,
 | 
				
			||||||
 | 
					                false,
 | 
				
			||||||
 | 
					                false
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,31 @@
 | 
				
			|||||||
 | 
					package de.mattv.fileserver.routes.auth;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import de.mattv.fileserver.Response;
 | 
				
			||||||
 | 
					import de.mattv.fileserver.data.Token;
 | 
				
			||||||
 | 
					import de.mattv.fileserver.security.TokenService;
 | 
				
			||||||
 | 
					import de.mattv.fileserver.util.AuthUser;
 | 
				
			||||||
 | 
					import de.mattv.fileserver.util.AutoCloseLock;
 | 
				
			||||||
 | 
					import de.mattv.fileserver.util.NonNull;
 | 
				
			||||||
 | 
					import de.mattv.fileserver.util.UserOrShareRestController;
 | 
				
			||||||
 | 
					import org.springframework.web.bind.annotation.PostMapping;
 | 
				
			||||||
 | 
					import org.springframework.web.bind.annotation.RequestBody;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@UserOrShareRestController
 | 
				
			||||||
 | 
					public class ShareUnlock {
 | 
				
			||||||
 | 
					    @PostMapping("/share_unlock")
 | 
				
			||||||
 | 
					    private @NonNull Response<String> session(@AuthUser Token token, @RequestBody @NonNull String pw) {
 | 
				
			||||||
 | 
					        if (!token.isShare()) return Response.e("Not a share token");
 | 
				
			||||||
 | 
					        if (!token.isShareCanRw()) return Response.e("Share can not be unlocked");
 | 
				
			||||||
 | 
					        if (token.isShareRw()) return Response.e("Share already unlocked");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var user = token.getUser();
 | 
				
			||||||
 | 
					        var root = token.getShareRoot();
 | 
				
			||||||
 | 
					        try (AutoCloseLock ignored = new AutoCloseLock(user.nodeLock.readLock())) {
 | 
				
			||||||
 | 
					            var node = user.nodes.get(root);
 | 
				
			||||||
 | 
					            if (node == null) return Response.e("Invalid share");
 | 
				
			||||||
 | 
					            if (node.shareWritePw == null) return Response.e("Share can not be unlocked");
 | 
				
			||||||
 | 
					            if (!AuthUtils.PW_ENCODER.matches(pw, node.shareWritePw)) return Response.e("Wrong password");
 | 
				
			||||||
 | 
					            return Response.o(TokenService.createToken(user, node));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -6,7 +6,7 @@ import de.mattv.fileserver.data.Token;
 | 
				
			|||||||
import de.mattv.fileserver.data.User;
 | 
					import de.mattv.fileserver.data.User;
 | 
				
			||||||
import de.mattv.fileserver.util.AuthUser;
 | 
					import de.mattv.fileserver.util.AuthUser;
 | 
				
			||||||
import de.mattv.fileserver.util.AutoCloseLock;
 | 
					import de.mattv.fileserver.util.AutoCloseLock;
 | 
				
			||||||
import de.mattv.fileserver.util.UserRestController;
 | 
					import de.mattv.fileserver.util.UserOrShareRestController;
 | 
				
			||||||
import de.mattv.fileserver.util.NonNull;
 | 
					import de.mattv.fileserver.util.NonNull;
 | 
				
			||||||
import lombok.extern.slf4j.Slf4j;
 | 
					import lombok.extern.slf4j.Slf4j;
 | 
				
			||||||
import org.springframework.web.bind.annotation.PostMapping;
 | 
					import org.springframework.web.bind.annotation.PostMapping;
 | 
				
			||||||
@@ -16,7 +16,7 @@ import java.io.IOException;
 | 
				
			|||||||
import java.util.Optional;
 | 
					import java.util.Optional;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Slf4j
 | 
					@Slf4j
 | 
				
			||||||
@UserRestController
 | 
					@UserOrShareRestController
 | 
				
			||||||
public class Create {
 | 
					public class Create {
 | 
				
			||||||
    private record Body(@NonNull String name, @NonNull long parent, @NonNull boolean file) {}
 | 
					    private record Body(@NonNull String name, @NonNull long parent, @NonNull boolean file) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -25,7 +25,8 @@ public class Create {
 | 
				
			|||||||
        User user = token.getUser();
 | 
					        User user = token.getUser();
 | 
				
			||||||
        try (AutoCloseLock ignored = new AutoCloseLock(user.nodeLock.readLock())) {
 | 
					        try (AutoCloseLock ignored = new AutoCloseLock(user.nodeLock.readLock())) {
 | 
				
			||||||
            Node parent = user.nodes.get(body.parent);
 | 
					            Node parent = user.nodes.get(body.parent);
 | 
				
			||||||
            if (parent == null) return Response.e("Invalid parent node");
 | 
					            if (parent == null || !FsUtils.shareHasAccess(token, parent)) return Response.e("Invalid parent node");
 | 
				
			||||||
 | 
					            if (token.isShare() && !token.isShareRw()) return Response.e("Share is not unlocked");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            Optional<Node> existing = parent.children.stream().filter(n -> n.name.equals(body.name)).findFirst();
 | 
					            Optional<Node> existing = parent.children.stream().filter(n -> n.name.equals(body.name)).findFirst();
 | 
				
			||||||
            if (existing.isPresent()) return Response.o(new Response.CreateNodeInfo(
 | 
					            if (existing.isPresent()) return Response.o(new Response.CreateNodeInfo(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -37,7 +37,7 @@ public class FsUtils {
 | 
				
			|||||||
        return path.isEmpty() ? "/" : path.toString();
 | 
					        return path.isEmpty() ? "/" : path.toString();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public static boolean shareHasAccess(Token token, Node node) {
 | 
					    public static boolean shareHasAccess(@NonNull Token token, @NonNull Node node) {
 | 
				
			||||||
        Long rootId = token.getShareRoot();
 | 
					        Long rootId = token.getShareRoot();
 | 
				
			||||||
        if (rootId == null) return true;
 | 
					        if (rootId == null) return true;
 | 
				
			||||||
        do {
 | 
					        do {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,6 +8,7 @@ import de.mattv.fileserver.util.AuthUser;
 | 
				
			|||||||
import de.mattv.fileserver.util.AutoCloseLock;
 | 
					import de.mattv.fileserver.util.AutoCloseLock;
 | 
				
			||||||
import de.mattv.fileserver.util.NonNull;
 | 
					import de.mattv.fileserver.util.NonNull;
 | 
				
			||||||
import de.mattv.fileserver.util.UserRestController;
 | 
					import de.mattv.fileserver.util.UserRestController;
 | 
				
			||||||
 | 
					import jakarta.annotation.Nullable;
 | 
				
			||||||
import org.springframework.web.bind.annotation.PostMapping;
 | 
					import org.springframework.web.bind.annotation.PostMapping;
 | 
				
			||||||
import org.springframework.web.bind.annotation.RequestBody;
 | 
					import org.springframework.web.bind.annotation.RequestBody;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -28,10 +29,22 @@ public class Share {
 | 
				
			|||||||
    @PostMapping("/fs/unshare")
 | 
					    @PostMapping("/fs/unshare")
 | 
				
			||||||
    private void path(@AuthUser Token token, @RequestBody @NonNull long id) {
 | 
					    private void path(@AuthUser Token token, @RequestBody @NonNull long id) {
 | 
				
			||||||
        User user = token.getUser();
 | 
					        User user = token.getUser();
 | 
				
			||||||
        try (AutoCloseLock ignored = new AutoCloseLock(user.nodeLock.readLock())) {
 | 
					        try (AutoCloseLock ignored = new AutoCloseLock(user.nodeLock.writeLock())) {
 | 
				
			||||||
            Node node = user.nodes.get(id);
 | 
					            Node node = user.nodes.get(id);
 | 
				
			||||||
            if (node != null)
 | 
					            if (node != null)
 | 
				
			||||||
                ShareService.removeShare(node);
 | 
					                ShareService.removeShare(node);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private record SetPwBody(@NonNull long id, @Nullable String password) {}
 | 
				
			||||||
 | 
					    @PostMapping("/fs/share_pw")
 | 
				
			||||||
 | 
					    private void setPw(@AuthUser Token token, @RequestBody @NonNull SetPwBody body) {
 | 
				
			||||||
 | 
					        User user = token.getUser();
 | 
				
			||||||
 | 
					        try (AutoCloseLock ignored = new AutoCloseLock(user.nodeLock.writeLock())) {
 | 
				
			||||||
 | 
					            Node node = user.nodes.get(body.id);
 | 
				
			||||||
 | 
					            if (node != null)
 | 
				
			||||||
 | 
					                ShareService.setPassword(node, body.password);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,10 +3,11 @@ package de.mattv.fileserver.security;
 | 
				
			|||||||
import de.mattv.fileserver.data.Data;
 | 
					import de.mattv.fileserver.data.Data;
 | 
				
			||||||
import de.mattv.fileserver.data.Node;
 | 
					import de.mattv.fileserver.data.Node;
 | 
				
			||||||
import de.mattv.fileserver.data.User;
 | 
					import de.mattv.fileserver.data.User;
 | 
				
			||||||
 | 
					import de.mattv.fileserver.routes.auth.AuthUtils;
 | 
				
			||||||
import de.mattv.fileserver.util.NonNull;
 | 
					import de.mattv.fileserver.util.NonNull;
 | 
				
			||||||
 | 
					import jakarta.annotation.Nullable;
 | 
				
			||||||
import org.apache.commons.lang3.RandomStringUtils;
 | 
					import org.apache.commons.lang3.RandomStringUtils;
 | 
				
			||||||
import org.apache.commons.lang3.tuple.Pair;
 | 
					import org.apache.commons.lang3.tuple.Pair;
 | 
				
			||||||
import org.springframework.lang.Nullable;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import java.util.concurrent.ConcurrentHashMap;
 | 
					import java.util.concurrent.ConcurrentHashMap;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -27,7 +28,7 @@ public class ShareService {
 | 
				
			|||||||
        return SHARES.get(token);
 | 
					        return SHARES.get(token);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public static String createShare(User user, Node node) {
 | 
					    public static String createShare(@NonNull User user, @NonNull Node node) {
 | 
				
			||||||
        if (node.shareString != null)
 | 
					        if (node.shareString != null)
 | 
				
			||||||
            return node.shareString;
 | 
					            return node.shareString;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -37,9 +38,19 @@ public class ShareService {
 | 
				
			|||||||
        return name;
 | 
					        return name;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public static void removeShare(Node node) {
 | 
					    public static void setPassword(@NonNull Node node, @Nullable String password) {
 | 
				
			||||||
 | 
					        if (node.shareString == null) return;
 | 
				
			||||||
 | 
					        node.shareWritePw = (password == null)
 | 
				
			||||||
 | 
					                ? null
 | 
				
			||||||
 | 
					                : AuthUtils.PW_ENCODER.encode(password);
 | 
				
			||||||
 | 
					        TokenService.removeShareTokens(node.id);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static void removeShare(@NonNull Node node) {
 | 
				
			||||||
        if (node.shareString == null) return;
 | 
					        if (node.shareString == null) return;
 | 
				
			||||||
        SHARES.remove(node.shareString);
 | 
					        SHARES.remove(node.shareString);
 | 
				
			||||||
        node.shareString = null;
 | 
					        node.shareString = null;
 | 
				
			||||||
 | 
					        node.shareWritePw = null;
 | 
				
			||||||
 | 
					        TokenService.removeShareTokens(node.id);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,6 @@
 | 
				
			|||||||
package de.mattv.fileserver.security;
 | 
					package de.mattv.fileserver.security;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import de.mattv.fileserver.data.Node;
 | 
				
			||||||
import de.mattv.fileserver.data.Token;
 | 
					import de.mattv.fileserver.data.Token;
 | 
				
			||||||
import de.mattv.fileserver.data.User;
 | 
					import de.mattv.fileserver.data.User;
 | 
				
			||||||
import de.mattv.fileserver.util.NonNull;
 | 
					import de.mattv.fileserver.util.NonNull;
 | 
				
			||||||
@@ -20,27 +21,34 @@ public class TokenService {
 | 
				
			|||||||
        TOKENS.entrySet().removeIf(entry -> entry.getValue().expired());
 | 
					        TOKENS.entrySet().removeIf(entry -> entry.getValue().expired());
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public static String createToken(@NonNull User user) {
 | 
					    public static String createToken(@NonNull User user, Node shareRoot) {
 | 
				
			||||||
        String token = RandomStringUtils.random(TOKEN_LENGTH, true, true);
 | 
					        String token = RandomStringUtils.random(TOKEN_LENGTH, true, true);
 | 
				
			||||||
        TOKENS.put(token, new Token(token, user, null));
 | 
					        TOKENS.put(token, (shareRoot != null)
 | 
				
			||||||
 | 
					            ? new Token(token, user, shareRoot.id, true, true)
 | 
				
			||||||
 | 
					            : new Token(token, user, null, false, false)
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
        return token;
 | 
					        return token;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public static @Nullable Token getToken(@NonNull String token) {
 | 
					    public static @Nullable Token getToken(@NonNull String token) {
 | 
				
			||||||
        if (token.length() == TOKEN_LENGTH) {
 | 
					        return switch (token.length()) {
 | 
				
			||||||
 | 
					            case TOKEN_LENGTH -> {
 | 
				
			||||||
                Token found = TOKENS.get(token);
 | 
					                Token found = TOKENS.get(token);
 | 
				
			||||||
            if (found == null) return null;
 | 
					                if (found == null) yield null;
 | 
				
			||||||
                if (found.expired()) {
 | 
					                if (found.expired()) {
 | 
				
			||||||
                    TOKENS.remove(token);
 | 
					                    TOKENS.remove(token);
 | 
				
			||||||
                return null;
 | 
					                    yield null;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                found.refresh();
 | 
					                found.refresh();
 | 
				
			||||||
            return found;
 | 
					                yield found;
 | 
				
			||||||
        } else if (token.length() == SHARE_TOKEN_LENGTH) {
 | 
					            }
 | 
				
			||||||
 | 
					            case SHARE_TOKEN_LENGTH -> {
 | 
				
			||||||
                var share = ShareService.getShare(token);
 | 
					                var share = ShareService.getShare(token);
 | 
				
			||||||
            if (share == null) return null;
 | 
					                if (share == null) yield null;
 | 
				
			||||||
            return new Token(token, share.getLeft(), share.getRight().id);
 | 
					                yield new Token(token, share.getLeft(), share.getRight().id, share.getRight().shareWritePw != null, false);
 | 
				
			||||||
        } else return null;
 | 
					            }
 | 
				
			||||||
 | 
					            default -> null;
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public static void logout(@NonNull Token token) {
 | 
					    public static void logout(@NonNull Token token) {
 | 
				
			||||||
@@ -53,4 +61,12 @@ public class TokenService {
 | 
				
			|||||||
            return token.getUser().id == id || token.getRealUser().id == id;
 | 
					            return token.getUser().id == id || token.getRealUser().id == id;
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static void removeShareTokens(long nodeId) {
 | 
				
			||||||
 | 
					        TOKENS.entrySet().removeIf(entry -> {
 | 
				
			||||||
 | 
					            Token token = entry.getValue();
 | 
				
			||||||
 | 
					            var root = token.getShareRoot();
 | 
				
			||||||
 | 
					            return root != null && root == nodeId;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user