220 lines
6.2 KiB
JavaScript
220 lines
6.2 KiB
JavaScript
//@ts-check
|
|
import { fsState, loadFolder } from './loadFolder'
|
|
import { oneOf } from '@zardoy/utils'
|
|
import JSZip from 'jszip'
|
|
import { join } from 'path'
|
|
|
|
const { promisify } = require('util')
|
|
const browserfs = require('browserfs')
|
|
const fs = require('fs')
|
|
|
|
browserfs.install(window)
|
|
// todo migrate to StorageManager API for localsave as localstorage has only 5mb limit, when localstorage is fallback test limit warning on 4mb
|
|
browserfs.configure({
|
|
// todo change to localstorage: mkdir doesnt work for some reason
|
|
fs: 'MountableFileSystem',
|
|
options: {
|
|
"/world": { fs: "LocalStorage" }
|
|
},
|
|
}, (e) => {
|
|
if (e) throw e
|
|
})
|
|
|
|
export const forceCachedDataPaths = {}
|
|
|
|
//@ts-ignore
|
|
fs.promises = new Proxy(Object.fromEntries(['readFile', 'writeFile', 'stat', 'mkdir', 'rename', /* 'copyFile', */'readdir'].map(key => [key, promisify(fs[key])])), {
|
|
get (target, p, receiver) {
|
|
//@ts-ignore
|
|
if (!target[p]) throw new Error(`Not implemented fs.promises.${p}`)
|
|
return (...args) => {
|
|
// browser fs bug: if path doesn't start with / dirname will return . which would cause infinite loop, so we need to normalize paths
|
|
if (typeof args[0] === 'string' && !args[0].startsWith('/')) args[0] = '/' + args[0]
|
|
// Write methods
|
|
// todo issue one-time warning (in chat I guess)
|
|
if (fsState.isReadonly) {
|
|
if (oneOf(p, 'readFile', 'writeFile') && forceCachedDataPaths[args[0]]) {
|
|
if (p === 'readFile') {
|
|
return Promise.resolve(forceCachedDataPaths[args[0]])
|
|
} else if (p === 'writeFile') {
|
|
forceCachedDataPaths[args[0]] = args[1]
|
|
return Promise.resolve()
|
|
}
|
|
}
|
|
if (oneOf(p, 'writeFile', 'mkdir', 'rename')) return
|
|
}
|
|
if (p === 'open' && fsState.isReadonly) {
|
|
args[1] = 'r' // read-only, zipfs throw otherwise
|
|
}
|
|
//@ts-ignore
|
|
return target[p](...args)
|
|
}
|
|
}
|
|
})
|
|
//@ts-ignore
|
|
fs.promises.open = async (...args) => {
|
|
const fd = await promisify(fs.open)(...args)
|
|
return {
|
|
...Object.fromEntries(['read', 'write', 'close'].map(x => [x, async (...args) => {
|
|
return await new Promise(resolve => {
|
|
// todo it results in world corruption on interactions eg block placements
|
|
if (x === 'write' && fsState.isReadonly) {
|
|
return resolve({ buffer: Buffer.from([]), bytesRead: 0 })
|
|
}
|
|
|
|
fs[x](fd, ...args, (err, bytesRead, buffer) => {
|
|
if (err) throw err
|
|
// todo if readonly probably there is no need to open at all (return some mocked version - check reload)?
|
|
if (x === 'write' && !fsState.isReadonly && fsState.syncFs) {
|
|
// flush data, though alternatively we can rely on close in unload
|
|
fs.fsync(fd, () => { })
|
|
}
|
|
resolve({ buffer, bytesRead })
|
|
})
|
|
})
|
|
}])),
|
|
// for debugging
|
|
fd,
|
|
filename: args[0],
|
|
close: () => {
|
|
return new Promise(resolve => {
|
|
fs.close(fd, (err) => {
|
|
if (err) {
|
|
throw err
|
|
} else {
|
|
resolve()
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// for testing purposes, todo move it to core patch
|
|
const removeFileRecursiveSync = (path) => {
|
|
fs.readdirSync(path).forEach((file) => {
|
|
const curPath = join(path, file)
|
|
if (fs.lstatSync(curPath).isDirectory()) {
|
|
// recurse
|
|
removeFileRecursiveSync(curPath)
|
|
fs.rmdirSync(curPath)
|
|
} else {
|
|
// delete file
|
|
fs.unlinkSync(curPath)
|
|
}
|
|
})
|
|
}
|
|
|
|
window.removeFileRecursiveSync = removeFileRecursiveSync
|
|
|
|
const SUPPORT_WRITE = true
|
|
|
|
export const openWorldDirectory = async (/** @type {FileSystemDirectoryHandle?} */dragndropHandle = undefined) => {
|
|
/** @type {FileSystemDirectoryHandle} */
|
|
let _directoryHandle
|
|
if (dragndropHandle) {
|
|
_directoryHandle = dragndropHandle
|
|
} else {
|
|
try {
|
|
_directoryHandle = await window.showDirectoryPicker()
|
|
} catch (err) {
|
|
if (err instanceof DOMException && err.name === 'AbortError') return
|
|
throw err
|
|
}
|
|
}
|
|
const directoryHandle = _directoryHandle
|
|
|
|
const requestResult = SUPPORT_WRITE ? await directoryHandle.requestPermission?.({ mode: 'readwrite' }) : undefined
|
|
const writeAccess = requestResult === 'granted'
|
|
|
|
const doContinue = writeAccess || !SUPPORT_WRITE || confirm('Continue in readonly mode?')
|
|
if (!doContinue) return
|
|
await new Promise(resolve => {
|
|
browserfs.configure({
|
|
// todo
|
|
fs: 'MountableFileSystem',
|
|
options: {
|
|
"/world": {
|
|
fs: "FileSystemAccess",
|
|
options: {
|
|
handle: directoryHandle
|
|
}
|
|
}
|
|
},
|
|
}, (e) => {
|
|
if (e) throw e
|
|
resolve()
|
|
})
|
|
})
|
|
|
|
fsState.isReadonly = !writeAccess
|
|
fsState.syncFs = false
|
|
loadFolder()
|
|
}
|
|
|
|
export const openWorldZip = async (/** @type {File} */file) => {
|
|
await new Promise(async resolve => {
|
|
browserfs.configure({
|
|
// todo
|
|
fs: 'MountableFileSystem',
|
|
options: {
|
|
"/world": {
|
|
fs: "ZipFS",
|
|
options: {
|
|
zipData: Buffer.from(await file.arrayBuffer()),
|
|
name: file.name
|
|
}
|
|
}
|
|
},
|
|
}, (e) => {
|
|
if (e) throw e
|
|
resolve()
|
|
})
|
|
})
|
|
|
|
fsState.isReadonly = true
|
|
fsState.syncFs = true
|
|
|
|
if (fs.existsSync('/world/level.dat')) {
|
|
loadFolder()
|
|
} else {
|
|
const dirs = fs.readdirSync('/world')
|
|
let availableWorlds = []
|
|
for (const dir of dirs) {
|
|
if (fs.existsSync(`/world/${dir}/level.dat`)) {
|
|
availableWorlds.push(dir)
|
|
}
|
|
}
|
|
|
|
if (availableWorlds.length === 0) {
|
|
alert('No worlds found in the zip')
|
|
return
|
|
}
|
|
|
|
if (availableWorlds.length === 1) {
|
|
loadFolder(`/world/${availableWorlds[0]}`)
|
|
}
|
|
|
|
alert(`Many (${availableWorlds.length}) worlds found in the zip!`)
|
|
// todo prompt picker
|
|
// const selectWorld
|
|
}
|
|
}
|
|
|
|
export async function generateZipAndWorld () {
|
|
const zip = new JSZip()
|
|
|
|
zip.folder('world')
|
|
|
|
// Generate the ZIP archive content
|
|
const zipContent = await zip.generateAsync({ type: "blob" })
|
|
|
|
// Create a download link and trigger the download
|
|
const downloadLink = document.createElement("a")
|
|
downloadLink.href = URL.createObjectURL(zipContent)
|
|
downloadLink.download = "prismarine-world.zip"
|
|
downloadLink.click()
|
|
|
|
// Clean up the URL object after download
|
|
URL.revokeObjectURL(downloadLink.href)
|
|
}
|