Frontend created and rewritten a few times, with some backend fixes (#1)
The software is fully operational. Reviewed-on: #1
This commit is contained in:
35
cista-front/src/repositories/Client.ts
Normal file
35
cista-front/src/repositories/Client.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
class ClientClass {
|
||||
async post(url: string, data?: Record<string, any>): Promise<any> {
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
'content-type': 'application/json'
|
||||
},
|
||||
body: data !== undefined ? JSON.stringify(data) : undefined
|
||||
})
|
||||
let msg
|
||||
try {
|
||||
msg = await res.json()
|
||||
} catch (e) {
|
||||
throw new SimpleError(res.status, `🛑 ${res.status} ${res.statusText}`)
|
||||
}
|
||||
if ('error' in msg) throw new SimpleError(msg.error.code, msg.error.message)
|
||||
return msg
|
||||
}
|
||||
}
|
||||
|
||||
export const Client = new ClientClass()
|
||||
export interface ISimpleError extends Error {
|
||||
code: number
|
||||
}
|
||||
|
||||
class SimpleError extends Error implements ISimpleError {
|
||||
code: number
|
||||
constructor(code: number, message: string) {
|
||||
super(message)
|
||||
this.code = code
|
||||
}
|
||||
}
|
||||
|
||||
export default Client
|
||||
55
cista-front/src/repositories/Document.ts
Normal file
55
cista-front/src/repositories/Document.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
export type FUID = string
|
||||
|
||||
export type Document = {
|
||||
loc: string
|
||||
name: string
|
||||
key: FUID
|
||||
size: number
|
||||
sizedisp: string
|
||||
mtime: number
|
||||
modified: string
|
||||
haystack: string
|
||||
dir: boolean
|
||||
}
|
||||
|
||||
export type errorEvent = {
|
||||
error: {
|
||||
code: number
|
||||
message: string
|
||||
redirect: string
|
||||
}
|
||||
}
|
||||
|
||||
// Raw types the backend /api/watch sends us
|
||||
|
||||
export type FileEntry = {
|
||||
key: FUID
|
||||
size: number
|
||||
mtime: number
|
||||
}
|
||||
|
||||
export type DirEntry = {
|
||||
key: FUID
|
||||
size: number
|
||||
mtime: number
|
||||
dir: DirList
|
||||
}
|
||||
|
||||
export type DirList = Record<string, FileEntry | DirEntry>
|
||||
|
||||
export type UpdateEntry = {
|
||||
name: string
|
||||
deleted?: boolean
|
||||
key?: FUID
|
||||
size?: number
|
||||
mtime?: number
|
||||
dir?: DirList
|
||||
}
|
||||
|
||||
// Helper structure for selections
|
||||
export interface SelectedItems {
|
||||
keys: FUID[]
|
||||
docs: Record<FUID, Document>
|
||||
recursive: Array<[string, string, Document]>
|
||||
missing: Set<FUID>
|
||||
}
|
||||
15
cista-front/src/repositories/User.ts
Normal file
15
cista-front/src/repositories/User.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import Client from '@/repositories/Client'
|
||||
export const url_login = '/login'
|
||||
export const url_logout = '/logout '
|
||||
|
||||
export async function loginUser(username: string, password: string) {
|
||||
const user = await Client.post(url_login, {
|
||||
username,
|
||||
password
|
||||
})
|
||||
return user
|
||||
}
|
||||
export async function logoutUser() {
|
||||
const data = await Client.post(url_logout)
|
||||
return data
|
||||
}
|
||||
133
cista-front/src/repositories/WS.ts
Normal file
133
cista-front/src/repositories/WS.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { useDocumentStore } from "@/stores/documents"
|
||||
import type { DirEntry, UpdateEntry, errorEvent } from "./Document"
|
||||
|
||||
export const controlUrl = '/api/control'
|
||||
export const uploadUrl = '/api/upload'
|
||||
export const watchUrl = '/api/watch'
|
||||
|
||||
let tree = null as DirEntry | null
|
||||
let reconnectDuration = 500
|
||||
let wsWatch = null as WebSocket | null
|
||||
|
||||
export const connect = (path: string, handlers: Partial<Record<keyof WebSocketEventMap, any>>) => {
|
||||
const webSocket = new WebSocket(new URL(path, location.origin.replace(/^http/, 'ws')))
|
||||
for (const [event, handler] of Object.entries(handlers)) webSocket.addEventListener(event, handler)
|
||||
return webSocket
|
||||
}
|
||||
|
||||
export const watchConnect = () => {
|
||||
if (watchTimeout !== null) {
|
||||
clearTimeout(watchTimeout)
|
||||
watchTimeout = null
|
||||
}
|
||||
const store = useDocumentStore()
|
||||
if (store.error !== 'Reconnecting...') store.error = 'Connecting...'
|
||||
console.log(store.error)
|
||||
|
||||
wsWatch = connect(watchUrl, {
|
||||
message: handleWatchMessage,
|
||||
close: watchReconnect,
|
||||
})
|
||||
wsWatch.addEventListener("message", event => {
|
||||
if (store.connected) return
|
||||
const msg = JSON.parse(event.data)
|
||||
if ('error' in msg) {
|
||||
if (msg.error.code === 401) {
|
||||
store.user.isLoggedIn = false
|
||||
store.user.isOpenLoginModal = true
|
||||
} else {
|
||||
store.error = msg.error.message
|
||||
}
|
||||
return
|
||||
}
|
||||
if ("server" in msg) {
|
||||
console.log('Connected to backend', msg)
|
||||
store.connected = true
|
||||
reconnectDuration = 500
|
||||
store.error = ''
|
||||
if (msg.user) store.login(msg.user.username, msg.user.privileged)
|
||||
else if (store.isUserLogged) store.logout()
|
||||
if (!msg.server.public && !msg.user) store.user.isOpenLoginModal = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const watchDisconnect = () => {
|
||||
if (!wsWatch) return
|
||||
wsWatch.close()
|
||||
wsWatch = null
|
||||
}
|
||||
|
||||
let watchTimeout: any = null
|
||||
|
||||
const watchReconnect = (event: MessageEvent) => {
|
||||
const store = useDocumentStore()
|
||||
if (store.connected) {
|
||||
console.warn("Disconnected from server", event)
|
||||
store.connected = false
|
||||
store.error = 'Reconnecting...'
|
||||
}
|
||||
reconnectDuration = Math.min(5000, reconnectDuration + 500)
|
||||
// The server closes the websocket after errors, so we need to reopen it
|
||||
if (watchTimeout !== null) clearTimeout(watchTimeout)
|
||||
watchTimeout = setTimeout(watchConnect, reconnectDuration)
|
||||
}
|
||||
|
||||
|
||||
const handleWatchMessage = (event: MessageEvent) => {
|
||||
const msg = JSON.parse(event.data)
|
||||
switch (true) {
|
||||
case !!msg.root:
|
||||
handleRootMessage(msg)
|
||||
break
|
||||
case !!msg.update:
|
||||
handleUpdateMessage(msg)
|
||||
break
|
||||
case !!msg.space:
|
||||
console.log('Watch space', msg.space)
|
||||
break
|
||||
case !!msg.error:
|
||||
handleError(msg)
|
||||
break
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
function handleRootMessage({ root }: { root: DirEntry }) {
|
||||
const store = useDocumentStore()
|
||||
console.log('Watch root', root)
|
||||
store.updateRoot(root)
|
||||
tree = root
|
||||
}
|
||||
|
||||
function handleUpdateMessage(updateData: { update: UpdateEntry[] }) {
|
||||
const store = useDocumentStore()
|
||||
console.log('Watch update', updateData.update)
|
||||
if (!tree) return console.error('Watch update before root')
|
||||
let node: DirEntry = tree
|
||||
for (const elem of updateData.update) {
|
||||
if (elem.deleted) {
|
||||
delete node.dir[elem.name]
|
||||
break // Deleted elements can't have further children
|
||||
}
|
||||
if (elem.name) {
|
||||
// @ts-ignore
|
||||
console.log(node, elem.name)
|
||||
node = node.dir[elem.name] ||= {}
|
||||
}
|
||||
if (elem.key !== undefined) node.key = elem.key
|
||||
if (elem.size !== undefined) node.size = elem.size
|
||||
if (elem.mtime !== undefined) node.mtime = elem.mtime
|
||||
if (elem.dir !== undefined) node.dir = elem.dir
|
||||
}
|
||||
store.updateRoot(tree)
|
||||
}
|
||||
|
||||
function handleError(msg: errorEvent) {
|
||||
const store = useDocumentStore()
|
||||
if (msg.error.code === 401) {
|
||||
store.user.isOpenLoginModal = true
|
||||
store.user.isLoggedIn = false
|
||||
return
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user