diff --git a/src/browserfs.ts b/src/browserfs.ts index 0c4c7664c..82d240582 100644 --- a/src/browserfs.ts +++ b/src/browserfs.ts @@ -233,6 +233,7 @@ export const mountGoogleDriveFolder = async (readonly: boolean, rootId: string) fsState.isReadonly = readonly fsState.syncFs = false fsState.inMemorySave = false + fsState.remoteBackend = true return true } @@ -313,6 +314,7 @@ export const openWorldDirectory = async (dragndropHandle?: FileSystemDirectoryHa fsState.isReadonly = !writeAccess fsState.syncFs = false fsState.inMemorySave = false + fsState.remoteBackend = false await loadSave() } @@ -352,7 +354,33 @@ export const possiblyCleanHandle = (callback = () => { }) => { } } -export const copyFilesAsyncWithProgress = async (pathSrc: string, pathDest: string, throwRootNotExist = true) => { +const readdirSafe = async (path: string) => { + try { + return await fs.promises.readdir(path) + } catch (err) { + return null + } +} + +export const collectFilesToCopy = async (basePath: string, safe = false): Promise => { + const result: string[] = [] + const countFiles = async (relPath: string) => { + const resolvedPath = join(basePath, relPath) + const files = relPath === '.' && !safe ? await fs.promises.readdir(resolvedPath) : await readdirSafe(resolvedPath) + if (!files) return null + await Promise.all(files.map(async file => { + const res = await countFiles(join(relPath, file)) + if (res === null) { + // is file + result.push(join(relPath, file)) + } + })) + } + await countFiles('.') + return result +} + +export const copyFilesAsyncWithProgress = async (pathSrc: string, pathDest: string, throwRootNotExist = true, addMsg = '') => { const stat = await existsViaStats(pathSrc) if (!stat) { if (throwRootNotExist) throw new Error(`Cannot copy. Source directory ${pathSrc} does not exist`) @@ -387,7 +415,7 @@ export const copyFilesAsyncWithProgress = async (pathSrc: string, pathDest: stri let copied = 0 await copyFilesAsync(pathSrc, pathDest, (name) => { copied++ - setLoadingScreenStatus(`Copying files (${copied}/${filesCount}): ${name}`) + setLoadingScreenStatus(`Copying files${addMsg} (${copied}/${filesCount}): ${name}`) }) } finally { setLoadingScreenStatus(undefined) @@ -402,6 +430,19 @@ export const existsViaStats = async (path: string) => { } } +export const fileExistsAsyncOptimized = async (path: string) => { + try { + await fs.promises.readdir(path) + } catch (err) { + if (err.code === 'ENOTDIR') return true + // eslint-disable-next-line sonarjs/prefer-single-boolean-return + if (err.code === 'ENOENT') return false + // throw err + return false + } + return true +} + export const copyFilesAsync = async (pathSrc: string, pathDest: string, fileCopied?: (name) => void) => { // query: can't use fs.copy! use fs.promises.writeFile and readFile const files = await fs.promises.readdir(pathSrc) @@ -467,6 +508,7 @@ export const openWorldFromHttpDir = async (fileDescriptorUrl: string/* | undefi fsState.isReadonly = true fsState.syncFs = false fsState.inMemorySave = false + fsState.remoteBackend = true await loadSave() } @@ -497,6 +539,7 @@ const openWorldZipInner = async (file: File | ArrayBuffer, name = file['name']) fsState.isReadonly = true fsState.syncFs = true fsState.inMemorySave = false + fsState.remoteBackend = false if (fs.existsSync('/world/level.dat')) { await loadSave() diff --git a/src/globalState.ts b/src/globalState.ts index d43663995..b0a447f21 100644 --- a/src/globalState.ts +++ b/src/globalState.ts @@ -63,7 +63,7 @@ export const hideModal = (modal = activeModalStack.at(-1), data: any = undefined } if (!cancel) { - let lastModal = activeModalStack.at(-1) + const lastModal = activeModalStack.at(-1) for (let i = activeModalStack.length - 1; i >= 0; i--) { if (activeModalStack[i].reactType === modal.reactType) { activeModalStack.splice(i, 1) diff --git a/src/loadSave.ts b/src/loadSave.ts index 48054094a..6c7da6bb2 100644 --- a/src/loadSave.ts +++ b/src/loadSave.ts @@ -21,6 +21,7 @@ export const fsState = proxy({ saveLoaded: false, openReadOperations: 0, openWriteOperations: 0, + remoteBackend: false }) const PROPOSE_BACKUP = true diff --git a/src/react/PauseScreen.tsx b/src/react/PauseScreen.tsx index 560412dea..96b00648e 100644 --- a/src/react/PauseScreen.tsx +++ b/src/react/PauseScreen.tsx @@ -1,4 +1,5 @@ import { join } from 'path' +import fs from 'fs' import { useEffect } from 'react' import { useSnapshot } from 'valtio' import { usedServerPathsV1 } from 'flying-squid/dist/lib/modules/world' @@ -14,7 +15,7 @@ import { fsState } from '../loadSave' import { disconnect } from '../flyingSquidUtils' import { pointerLock, setLoadingScreenStatus } from '../utils' import { closeWan, openToWanAndCopyJoinLink, getJoinLink } from '../localServerMultiplayer' -import { copyFilesAsyncWithProgress, mkdirRecursive, uniqueFileNameFromWorldName } from '../browserfs' +import { collectFilesToCopy, fileExistsAsyncOptimized, mkdirRecursive, uniqueFileNameFromWorldName } from '../browserfs' import { useIsModalActive } from './utilsApp' import { showOptionsModal } from './SelectOption' import Button from './Button' @@ -29,12 +30,44 @@ export const saveToBrowserMemory = async () => { const { worldFolder } = localServer.options const saveRootPath = await uniqueFileNameFromWorldName(worldFolder.split('/').pop(), `/data/worlds`) await mkdirRecursive(saveRootPath) - for (const copyPath of [...usedServerPathsV1, 'icon.png']) { - const srcPath = join(worldFolder, copyPath) - const savePath = join(saveRootPath, copyPath) + const allRootPaths = [...usedServerPathsV1] + const allFilesToCopy = [] as string[] + for (const dirBase of allRootPaths) { + if (dirBase.includes('.') && await fileExistsAsyncOptimized(join(worldFolder, dirBase))) { + allFilesToCopy.push(dirBase) + continue + } + let res = await collectFilesToCopy(join(worldFolder, dirBase), true) + if (dirBase === 'region') { + res = res.filter(x => x.endsWith('.mca')) + } + allFilesToCopy.push(...res.map(x => join(dirBase, x))) + } + const pathsSplit = allFilesToCopy.reduce((acc, cur, i) => { + if (i % 15 === 0) { + acc.push([]) + } + acc.at(-1)!.push(cur) + return acc + // eslint-disable-next-line @typescript-eslint/prefer-reduce-type-parameter + }, [] as string[][]) + let copied = 0 + const upProgress = () => { + copied++ + const action = fsState.remoteBackend ? 'Downloading & copying' : 'Copying' + setLoadingScreenStatus(`${action} files (${copied}/${allFilesToCopy.length})`) + } + for (const copyPaths of pathsSplit) { // eslint-disable-next-line no-await-in-loop - await copyFilesAsyncWithProgress(srcPath, savePath, false) + await Promise.all(copyPaths.map(async (copyPath) => { + const srcPath = join(worldFolder, copyPath) + const savePath = join(saveRootPath, copyPath) + await mkdirRecursive(savePath) + await fs.promises.writeFile(savePath, await fs.promises.readFile(srcPath)) + upProgress() + })) } + return saveRootPath } catch (err) { void showOptionsModal(`Error while saving the world: ${err.message}`, []) @@ -101,6 +134,10 @@ export default () => { const action = await showOptionsModal('World actions...', ['Save to browser memory']) if (action === 'Save to browser memory') { await saveToBrowserMemory() + // fsState.inMemorySave = true + // fsState.syncFs = false + // fsState.isReadonly = false + // fsState.remoteBackend = false } }