♻️ Update the typing for chunkedTranfer
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { fileSystemClient, type TransferProgress } from '$lib/filesystem/chunkedTransfer'
|
import { fileSystemClient } from '$lib/filesystem/chunkedTransfer'
|
||||||
|
import type { TransferProgress } from '$lib/types/models'
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
|
|
||||||
let currentPath = '/'
|
let currentPath = '/'
|
||||||
|
|||||||
@@ -13,78 +13,45 @@ import type {
|
|||||||
FSUploadComplete,
|
FSUploadComplete,
|
||||||
FSCancelTransfer
|
FSCancelTransfer
|
||||||
} from '$lib/platform_shared/filesystem'
|
} from '$lib/platform_shared/filesystem'
|
||||||
|
import type { Result, DataResult, ListResult, ProgressCallback } from '$lib/types/models'
|
||||||
|
|
||||||
const MAX_CHUNK_SIZE = 2 ** 14 // ~= 16 kb
|
const MAX_CHUNK_SIZE = 2 ** 14
|
||||||
|
|
||||||
export interface FileInfo {
|
type TimeoutId = ReturnType<typeof setTimeout>
|
||||||
name: string
|
type CleanupFn = (() => void) | null
|
||||||
size: number
|
|
||||||
|
interface TransferBase<T extends Result> {
|
||||||
|
resolve: (result: T) => void
|
||||||
|
reject: (error: Error) => void
|
||||||
|
onProgress?: ProgressCallback
|
||||||
|
timeoutId: TimeoutId
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DirectoryInfo {
|
interface ActiveDownload extends TransferBase<DataResult> {
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ListResult {
|
|
||||||
success: boolean
|
|
||||||
error?: string
|
|
||||||
files: FileInfo[]
|
|
||||||
directories: DirectoryInfo[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TransferProgress {
|
|
||||||
transferId: number
|
|
||||||
bytesTransferred: number
|
|
||||||
totalBytes: number
|
|
||||||
chunksCompleted: number
|
|
||||||
totalChunks: number
|
|
||||||
percentage: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ProgressCallback = (progress: TransferProgress) => void
|
|
||||||
|
|
||||||
// Active transfer tracking
|
|
||||||
interface ActiveDownload {
|
|
||||||
path: string
|
path: string
|
||||||
buffer: Uint8Array
|
buffer: Uint8Array
|
||||||
fileSize: number
|
fileSize: number
|
||||||
totalChunks: number
|
totalChunks: number
|
||||||
chunksReceived: number
|
chunksReceived: number
|
||||||
bytesReceived: number
|
bytesReceived: number
|
||||||
resolve: (result: { success: boolean; data?: Uint8Array; error?: string }) => void
|
|
||||||
reject: (error: Error) => void
|
|
||||||
onProgress?: ProgressCallback
|
|
||||||
timeoutId: ReturnType<typeof setTimeout>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ActiveUpload {
|
interface ActiveUpload extends TransferBase<Result> {
|
||||||
path: string
|
path: string
|
||||||
transferId: number
|
transferId: number
|
||||||
totalChunks: number
|
totalChunks: number
|
||||||
chunksSent: number
|
chunksSent: number
|
||||||
resolve: (result: { success: boolean; error?: string }) => void
|
|
||||||
reject: (error: Error) => void
|
|
||||||
onProgress?: ProgressCallback
|
|
||||||
timeoutId: ReturnType<typeof setTimeout>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FileSystemClient {
|
export class FileSystemClient {
|
||||||
private activeDownloads = new Map<number, ActiveDownload>()
|
private activeDownloads = new Map<number, ActiveDownload>()
|
||||||
private activeUploads = new Map<number, ActiveUpload>()
|
private activeUploads = new Map<number, ActiveUpload>()
|
||||||
private pendingDownloads = new Map<
|
private pendingDownloads = new Map<string, TransferBase<DataResult>>()
|
||||||
string,
|
private metadataListenerCleanup: CleanupFn = null
|
||||||
{
|
private downloadListenerCleanup: CleanupFn = null
|
||||||
resolve: (result: { success: boolean; data?: Uint8Array; error?: string }) => void
|
private completeListenerCleanup: CleanupFn = null
|
||||||
reject: (error: Error) => void
|
private uploadCompleteListenerCleanup: CleanupFn = null
|
||||||
onProgress?: ProgressCallback
|
private transferTimeout = 60000
|
||||||
timeoutId: ReturnType<typeof setTimeout>
|
|
||||||
}
|
|
||||||
>()
|
|
||||||
private metadataListenerCleanup: (() => void) | null = null
|
|
||||||
private downloadListenerCleanup: (() => void) | null = null
|
|
||||||
private completeListenerCleanup: (() => void) | null = null
|
|
||||||
private uploadCompleteListenerCleanup: (() => void) | null = null
|
|
||||||
private transferTimeout = 60000 // 60 seconds timeout for transfers
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.setupListeners()
|
this.setupListeners()
|
||||||
@@ -243,10 +210,8 @@ export class FileSystemClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Delete a file or directory on the ESP32 */
|
||||||
* Delete a file or directory on the ESP32
|
async deleteFile(path: string): Promise<Result> {
|
||||||
*/
|
|
||||||
async deleteFile(path: string): Promise<{ success: boolean; error?: string }> {
|
|
||||||
const request: FSDeleteRequest = { path }
|
const request: FSDeleteRequest = { path }
|
||||||
|
|
||||||
const response = await socket.request({
|
const response = await socket.request({
|
||||||
@@ -263,10 +228,8 @@ export class FileSystemClient {
|
|||||||
return { success: false, error: 'No response received' }
|
return { success: false, error: 'No response received' }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Create a directory on the ESP32 */
|
||||||
* Create a directory on the ESP32
|
async createDirectory(path: string): Promise<Result> {
|
||||||
*/
|
|
||||||
async createDirectory(path: string): Promise<{ success: boolean; error?: string }> {
|
|
||||||
const request: FSMkdirRequest = { path }
|
const request: FSMkdirRequest = { path }
|
||||||
|
|
||||||
const response = await socket.request({
|
const response = await socket.request({
|
||||||
@@ -283,10 +246,8 @@ export class FileSystemClient {
|
|||||||
return { success: false, error: 'No response received' }
|
return { success: false, error: 'No response received' }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** List files and directories at the given path */
|
||||||
* List files and directories at the given path
|
async listDirectory(path = '/'): Promise<ListResult> {
|
||||||
*/
|
|
||||||
async listDirectory(path: string = '/'): Promise<ListResult> {
|
|
||||||
const request: FSListRequest = { path }
|
const request: FSListRequest = { path }
|
||||||
|
|
||||||
const response = await socket.request({
|
const response = await socket.request({
|
||||||
@@ -306,14 +267,8 @@ export class FileSystemClient {
|
|||||||
return { success: false, error: 'No response received', files: [], directories: [] }
|
return { success: false, error: 'No response received', files: [], directories: [] }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Download a file from the ESP32 using streaming transfer */
|
||||||
* Download a file from the ESP32 using streaming transfer
|
async downloadFile(path: string, onProgress?: ProgressCallback): Promise<DataResult> {
|
||||||
* Server sends metadata first (with file size), then streams all chunks
|
|
||||||
*/
|
|
||||||
async downloadFile(
|
|
||||||
path: string,
|
|
||||||
onProgress?: ProgressCallback
|
|
||||||
): Promise<{ success: boolean; data?: Uint8Array; error?: string }> {
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// Send download request - server will send metadata first, then stream chunks
|
// Send download request - server will send metadata first, then stream chunks
|
||||||
const request: FSDownloadRequest = { path }
|
const request: FSDownloadRequest = { path }
|
||||||
@@ -341,15 +296,8 @@ export class FileSystemClient {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Upload a file to the ESP32 using streaming transfer */
|
||||||
* Upload a file to the ESP32 using streaming transfer
|
async uploadFile(path: string, data: Uint8Array, onProgress?: ProgressCallback): Promise<Result> {
|
||||||
* Client sends all chunks without waiting for ACKs
|
|
||||||
*/
|
|
||||||
async uploadFile(
|
|
||||||
path: string,
|
|
||||||
data: Uint8Array,
|
|
||||||
onProgress?: ProgressCallback
|
|
||||||
): Promise<{ success: boolean; error?: string }> {
|
|
||||||
const fileSize = data.length
|
const fileSize = data.length
|
||||||
const chunkSize = MAX_CHUNK_SIZE
|
const chunkSize = MAX_CHUNK_SIZE
|
||||||
const totalChunks = Math.ceil(fileSize / chunkSize) || 1
|
const totalChunks = Math.ceil(fileSize / chunkSize) || 1
|
||||||
@@ -430,10 +378,8 @@ export class FileSystemClient {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Cancel an ongoing transfer */
|
||||||
* Cancel an ongoing transfer
|
async cancelTransfer(transferId: number): Promise<Pick<Result, 'success'>> {
|
||||||
*/
|
|
||||||
async cancelTransfer(transferId: number): Promise<{ success: boolean }> {
|
|
||||||
const request: FSCancelTransfer = { transferId }
|
const request: FSCancelTransfer = { transferId }
|
||||||
|
|
||||||
// Clean up local state
|
// Clean up local state
|
||||||
@@ -462,27 +408,23 @@ export class FileSystemClient {
|
|||||||
return { success: false }
|
return { success: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Upload a File object from browser */
|
||||||
* Helper: Upload a File object from browser
|
|
||||||
*/
|
|
||||||
async uploadFileFromBrowser(
|
async uploadFileFromBrowser(
|
||||||
destinationPath: string,
|
destinationPath: string,
|
||||||
file: File,
|
file: File,
|
||||||
onProgress?: ProgressCallback
|
onProgress?: ProgressCallback
|
||||||
): Promise<{ success: boolean; error?: string }> {
|
): Promise<Result> {
|
||||||
const arrayBuffer = await file.arrayBuffer()
|
const arrayBuffer = await file.arrayBuffer()
|
||||||
const data = new Uint8Array(arrayBuffer)
|
const data = new Uint8Array(arrayBuffer)
|
||||||
return this.uploadFile(destinationPath, data, onProgress)
|
return this.uploadFile(destinationPath, data, onProgress)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Download a file and save it to browser */
|
||||||
* Helper: Download a file and save it to browser
|
|
||||||
*/
|
|
||||||
async downloadFileAndSave(
|
async downloadFileAndSave(
|
||||||
path: string,
|
path: string,
|
||||||
filename: string,
|
filename: string,
|
||||||
onProgress?: ProgressCallback
|
onProgress?: ProgressCallback
|
||||||
): Promise<{ success: boolean; error?: string }> {
|
): Promise<Result> {
|
||||||
const result = await this.downloadFile(path, onProgress)
|
const result = await this.downloadFile(path, onProgress)
|
||||||
|
|
||||||
if (!result.success || !result.data) {
|
if (!result.success || !result.data) {
|
||||||
@@ -503,9 +445,7 @@ export class FileSystemClient {
|
|||||||
return { success: true }
|
return { success: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Cleanup listeners when no longer needed */
|
||||||
* Cleanup listeners when no longer needed
|
|
||||||
*/
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this.metadataListenerCleanup?.()
|
this.metadataListenerCleanup?.()
|
||||||
this.downloadListenerCleanup?.()
|
this.downloadListenerCleanup?.()
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ function createWebSocket() {
|
|||||||
>()
|
>()
|
||||||
const { subscribe, set } = writable(false)
|
const { subscribe, set } = writable(false)
|
||||||
const reconnectTimeoutTime = 500000
|
const reconnectTimeoutTime = 500000
|
||||||
const requestTimeoutTime = 30000 // 30 seconds for chunked file transfers
|
const requestTimeoutTime = 30000
|
||||||
let correlationIdCounter = 0
|
let correlationIdCounter = 0
|
||||||
let unresponsiveTimeoutId: ReturnType<typeof setTimeout>
|
let unresponsiveTimeoutId: ReturnType<typeof setTimeout>
|
||||||
let reconnectTimeoutId: ReturnType<typeof setTimeout>
|
let reconnectTimeoutId: ReturnType<typeof setTimeout>
|
||||||
@@ -144,7 +144,6 @@ function createWebSocket() {
|
|||||||
ws.onmessage = frame => {
|
ws.onmessage = frame => {
|
||||||
resetUnresponsiveCheck()
|
resetUnresponsiveCheck()
|
||||||
|
|
||||||
// Reset all pending request timeouts when any message arrives (connection is alive)
|
|
||||||
for (const [correlationId, pending] of pending_requests) {
|
for (const [correlationId, pending] of pending_requests) {
|
||||||
clearTimeout(pending.timeoutId)
|
clearTimeout(pending.timeoutId)
|
||||||
pending.timeoutId = setTimeout(() => {
|
pending.timeoutId = setTimeout(() => {
|
||||||
|
|||||||
@@ -153,3 +153,37 @@ export interface MDNSStatus {
|
|||||||
services: MDNSService[]
|
services: MDNSService[]
|
||||||
global_txt_records: MDNSTxtRecord[]
|
global_txt_records: MDNSTxtRecord[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Result {
|
||||||
|
success: boolean
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataResult extends Result {
|
||||||
|
data?: Uint8Array
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileInfo {
|
||||||
|
name: string
|
||||||
|
size: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DirectoryInfo {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListResult extends Result {
|
||||||
|
files: FileInfo[]
|
||||||
|
directories: DirectoryInfo[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TransferProgress {
|
||||||
|
transferId: number
|
||||||
|
bytesTransferred: number
|
||||||
|
totalBytes: number
|
||||||
|
chunksCompleted: number
|
||||||
|
totalChunks: number
|
||||||
|
percentage: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProgressCallback = (progress: TransferProgress) => void
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Spinner from '$lib/components/Spinner.svelte'
|
import Spinner from '$lib/components/Spinner.svelte'
|
||||||
import { fileSystemClient, type TransferProgress } from '$lib/filesystem/chunkedTransfer'
|
import { fileSystemClient } from '$lib/filesystem/chunkedTransfer'
|
||||||
|
import type { TransferProgress } from '$lib/types/models'
|
||||||
import { FolderIcon, Add, FileIcon, UploadIcon, DownloadIcon, TrashIcon } from '$lib/components/icons'
|
import { FolderIcon, Add, FileIcon, UploadIcon, DownloadIcon, TrashIcon } from '$lib/components/icons'
|
||||||
import { modals } from 'svelte-modals'
|
import { modals } from 'svelte-modals'
|
||||||
import NewFolderDialog from './NewFolderDialog.svelte'
|
import NewFolderDialog from './NewFolderDialog.svelte'
|
||||||
|
|||||||
Reference in New Issue
Block a user